// ==UserScript== // @name Camclient // @name:zh-CN Camclient // @name:de Camclient // @name:es Camclient // @name:pt-BR Camclient // @name:fr Camclient // @name:it Camclient // @name:ru Camclient // @name:ja Camclient // @name:pl Camclient // @name:ro Camclient // @name:nl Camclient // @name:tr Camclient // @namespace urn:emberhost:camclient // @version 20.116.1-custom // @description Local-first Camclient suite with workspaces, Discover, Preview Dock, SingleCam, MultiCam, Analytics Studio, optional official broadcaster statistics, privacy and diagnostics. // @description:de Lokale Camclient-Suite mit Arbeitsbereichen, Entdecken, Preview Dock, SingleCam, MultiCam, Analytics Studio, optionalen offiziellen Broadcaster-Statistiken, Datenschutz und Diagnose. // @description:es Suite Camclient local con espacios de trabajo, Descubrir, Preview Dock, SingleCam, MultiCam, Analytics Studio, estadísticas oficiales opcionales, privacidad y diagnóstico. // @description:pt-BR Suite Camclient local com espaços de trabalho, Descobrir, Preview Dock, SingleCam, MultiCam, Analytics Studio, estatísticas oficiais opcionais, privacidade e diagnóstico. // @description:fr Suite Camclient locale avec espaces de travail, découverte, dock d’aperçu, SingleCam, MultiCam, statistiques, confidentialité et diagnostic. // @description:it Suite Camclient locale con aree di lavoro, scoperta, dock di anteprima, SingleCam, MultiCam, statistiche, privacy e diagnostica. // @description:ru Локальный набор Camclient с рабочими областями, обзором, предпросмотром, SingleCam, MultiCam, статистикой, конфиденциальностью и диагностикой. // @description:ja ワークスペース、検索、プレビュードック、SingleCam、MultiCam、統計、プライバシー、診断を備えたローカルCamclientスイート。 // @description:pl Lokalny pakiet Camclient z obszarami roboczymi, odkrywaniem, podglądem, SingleCam, MultiCam, statystykami, prywatnością i diagnostyką. // @description:ro Suită Camclient locală cu spații de lucru, descoperire, previzualizări, SingleCam, MultiCam, statistici, confidențialitate și diagnosticare. // @description:nl Lokale Camclient-suite met werkruimtes, ontdekken, voorbeeldweergaven, SingleCam, MultiCam, statistieken, privacy en diagnose. // @description:tr Çalışma alanları, keşif, önizlemeler, SingleCam, MultiCam, istatistikler, gizlilik ve tanılama içeren yerel Camclient paketi. // @author Emberhost // @license MIT // @match https://chaturbate.com/* // @match https://*.chaturbate.com/* // @require https://cdn.jsdelivr.net/npm/hls.js@1.6.16/dist/hls.min.js // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.download // @grant GM.xmlHttpRequest // @grant GM_download // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect chaturbate.com // @connect *.chaturbate.com // @connect thumb.live.mmcdn.com // @connect jpeg.live.mmcdn.com // @run-at document-end // ==/UserScript== /* * MIT License * * Copyright (c) 2026 Emberhost * * Camclient product authorship and current implementation: Emberhost. * Legacy portions retain their original MIT notice in the third-party notice * below because the MIT license legally requires preservation of that notice. * Portions Copyright (c) 2026 RYUJO * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (async function () { 'use strict'; const ROOMGRID_BOOT_METRICS = window.__roomgridStartup = { ...(window.__roomgridStartup || {}), scriptStartedAt: typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now(), version: '20.116.1-custom', }; // Local capture uses only media tracks that the browser explicitly exposes. // No DRM, permission, CORS, or protected-media restriction is bypassed. const ROOMGRID_CAPTURE_TOOLS_ENABLED = true; // The public catalogue is already paged and the UI only materialises the // selected page. A second artificial total limit would therefore hide // valid public rooms without saving meaningful rendering work. Keep the // shared values numeric for the older import helpers, but make them // effectively unbounded; the server-reported end and a short final page are // the only catalogue stop conditions. const ROOMGRID_DATA_LIMITS = Object.freeze({ modelRecords: Number.MAX_SAFE_INTEGER, publicCatalogResults: Number.MAX_SAFE_INTEGER, }); function roomGridParseJsonTransportValue(value) { let candidate = value; for (let depth = 0; depth < 4; depth += 1) { if (candidate && typeof candidate === 'object' && !ArrayBuffer.isView(candidate) && !(candidate instanceof ArrayBuffer) && !(candidate instanceof Blob)) return candidate; if (candidate == null) break; let text = String(candidate).trim(); if (!text) break; text = text.replace(/^\)\]\}',?\s*/u, ''); try { candidate = JSON.parse(text); } catch (_) { break; } } return null; } function roomGridPrivilegedRequest() { try { if (typeof GM !== 'undefined' && typeof GM?.xmlHttpRequest === 'function') return GM.xmlHttpRequest.bind(GM); } catch (_) {} try { if (typeof GM_xmlhttpRequest === 'function') return GM_xmlhttpRequest; } catch (_) {} return null; } function roomGridRequestJson(url, options = {}) { const signal = options.signal; const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 12000); const gmRequest = roomGridPrivilegedRequest(); if (gmRequest) { return new Promise((resolve, reject) => { let settled = false; let requestHandle = null; const finish = (callback, value) => { if (settled) return; settled = true; try { signal?.removeEventListener('abort', abortRequest); } catch (_) {} callback(value); }; const abortRequest = () => { try { requestHandle?.abort?.(); } catch (_) {} finish(reject, new DOMException('Katalogabruf abgebrochen', 'AbortError')); }; if (signal?.aborted) { abortRequest(); return; } try { signal?.addEventListener('abort', abortRequest, { once: true }); requestHandle = gmRequest({ method: 'GET', url, headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' }, // Text is intentional: different Tampermonkey generations expose // responseType=json as a string, Blob or wrapper object. Parsing // responseText ourselves produces one deterministic result. responseType: 'text', timeout: timeoutMs, anonymous: false, onload: response => { if (response.status < 200 || response.status >= 300) { const retryAfter = String(response.responseHeaders || '').match(/^retry-after:\s*(.+)$/im)?.[1]?.trim(); const retryAfterMs = retryAfter ? Math.max(0, /^\d+(?:\.\d+)?$/.test(retryAfter) ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now()) : 0; finish(reject, Object.assign(new Error(`HTTP ${response.status || 0}`), { status: response.status, retryAfterMs: Number.isFinite(retryAfterMs) ? retryAfterMs : 0 })); return; } const parsed = roomGridParseJsonTransportValue(response.responseText) || roomGridParseJsonTransportValue(response.response); if (!parsed) { finish(reject, new Error('Katalogantwort ist kein JSON')); return; } finish(resolve, parsed); }, onerror: () => finish(reject, new Error('Katalog-Netzwerkfehler')), ontimeout: () => finish(reject, new Error('Katalog-Zeitüberschreitung')), onabort: () => finish(reject, new DOMException('Katalogabruf abgebrochen', 'AbortError')), }); } catch (error) { finish(reject, error); } }); } return fetch(url, { credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' }, signal, cache: 'no-store', }).then(async response => { if (!response.ok) { const retryAfter = response.headers.get('Retry-After'); const retryAfterMs = retryAfter ? Math.max(0, /^\d+(?:\.\d+)?$/.test(retryAfter) ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now()) : 0; throw Object.assign(new Error(`HTTP ${response.status}`), { status: response.status, retryAfterMs: Number.isFinite(retryAfterMs) ? retryAfterMs : 0 }); } const parsed = roomGridParseJsonTransportValue(await response.text()); if (!parsed) throw new Error('Katalogantwort ist kein JSON'); return parsed; }); } // v9.0: 防止脚本被重复粘贴 / 重复安装时同页运行两次。 // 你上传的 enhancer.txt 是两个相同脚本拼在一起;没有这个保护会导致 UI、轮询、HLS 实例重复。 const INSTANCE_KEY = '__roomGridMultiCamWorkstationRunning'; if (window[INSTANCE_KEY]) { try { console.warn('[RoomGrid] duplicate userscript instance blocked'); } catch (_) {} return; } window[INSTANCE_KEY] = true; /* ============================================================= * 0.5. Updatefester Einstellungstresor * * localStorage bleibt der schnelle, seitensynchrone Arbeitsspeicher. * Zusätzlich spiegelt dieser kleine Tresor alle dauerhaften RoomGrid- * Einstellungen in Tampermonkeys isolierten GM-Speicher. Der Schlüssel * enthält absichtlich keine Release-Version und überlebt Script-Updates. * ============================================================= */ const RoomGridPersistentVault = (() => { const { Math, Number, String, Array, Object, Date, Set, Promise, JSON, URL, TextEncoder } = globalThis; const VAULT_KEY = 'roomgrid_persistent_settings_v1'; const MAX_VAULT_BYTES = 6 * 1024 * 1024; const JSON_KEYS = new Set([ 'ryujo_multicam_v8', 'ryujo_multicam_workspace_archive_v1', 'ryujo_multicam_discover_v1', 'ryujo_multicam_modules_v1', 'ryujo_multicam_privacy_v1', 'ryujo_multicam_notifications_v1', 'ryujo_multicam_optional_tools_v1', 'ryujo_multicam_group_user_collapsed_v1', 'roomgrid_audio_silencer_v1', 'roomgrid_preview_layout_v1', 'roomgrid_native_hub_prefs_v1', 'roomgrid_native_account_import_v1', 'roomgrid_following_snapshot_v1', ]); const SCALAR_KEYS = new Set([ 'multicam_lang', 'ryujo_multicam_tip_sound_muted_v1', 'ryujo_fab_collapsed', ]); const ALL_KEYS = new Set([...JSON_KEYS, ...SCALAR_KEYS]); // New preference modules should automatically become update-safe without // having to change this vault again. Runtime state, credentials and // transient caches are deliberately excluded. const FUTURE_SETTINGS_KEY = /^(?:ryujo_|multicam_|roomgrid_)[a-z0-9_.:-]*(?:settings|preferences|prefs|config|layout|modules|notifications|privacy|language|lang|collapsed|muted)[a-z0-9_.:-]*$/iu; const PRIVATE_OR_TRANSIENT_KEY = /(?:vault|token|secret|credential|session|auth|cookie|diagnostic|debug|leader|windows|transfer|backup|corrupt|history|temporary|temp_url|recording_intent)/iu; const MAX_DYNAMIC_VALUE_BYTES = 512 * 1024; let timer = 0; let queued = 0; let writing = false; let activeWrite = null; let rerun = false; let lastEntries = null; let restoredCount = 0; const scopedVaultKey = () => { try { return `${VAULT_KEY}:${new URL(location.href).hostname.toLocaleLowerCase() || 'chaturbate.com'}`; } catch (_) { return `${VAULT_KEY}:chaturbate.com`; } }; const isMirroredKey = key => typeof key === 'string' && (ALL_KEYS.has(key) || (FUTURE_SETTINGS_KEY.test(key) && !PRIVATE_OR_TRANSIENT_KEY.test(key))); const isValidValue = (key, value) => { if (typeof value !== 'string' || !value.length) return false; if (key === 'multicam_lang') return /^(?:de|en|es|pt|fr|it|ru|ja|pl|ro|nl|tr)$/u.test(value); if (key === 'ryujo_multicam_tip_sound_muted_v1' || key === 'ryujo_fab_collapsed') return value === '0' || value === '1'; if (!isMirroredKey(key)) return false; if (!JSON_KEYS.has(key)) { if (new TextEncoder().encode(value).byteLength > MAX_DYNAMIC_VALUE_BYTES) return false; try { JSON.parse(value); return true; } catch (_) { return value.length <= 4096 && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(value); } } try { const parsed = JSON.parse(value); return !!parsed && typeof parsed === 'object' && !Array.isArray(parsed); } catch (_) { return false; } }; const readIsolated = async (timeoutMs = 240) => { try { if (!window.GM?.getValue) return ''; return await Promise.race([ (async () => { const scoped = await GM.getValue(scopedVaultKey(), ''); return scoped || await GM.getValue(VAULT_KEY, ''); })(), new Promise(resolve => setTimeout(() => resolve(''), Math.max(80, Number(timeoutMs) || 240))), ]); } catch (_) { return ''; } }; const writeIsolated = async value => { try { if (!window.GM?.setValue) return false; await GM.setValue(scopedVaultKey(), value); return true; } catch (_) { return false; } }; const parseBundle = raw => { try { const value = typeof raw === 'string' ? JSON.parse(raw || '{}') : raw; return value && typeof value === 'object' && !Array.isArray(value) && value.entries && typeof value.entries === 'object' ? value : null; } catch (_) { return null; } }; const collectEntries = () => { const entries = {}; const keys = new Set(ALL_KEYS); try { for (let index = 0; index < localStorage.length; index += 1) { const key = localStorage.key(index); if (isMirroredKey(key)) keys.add(key); } } catch (_) {} for (const key of keys) { try { const value = localStorage.getItem(key); if (isValidValue(key, value)) entries[key] = value; } catch (_) {} } return entries; }; // Compare the last successfully mirrored strings directly. A character-wise // hash every four seconds blocked large real Tampermonkey installations. // Exact equality also avoids treating a hash collision as unchanged data. const entriesUnchanged = entries => { if (!lastEntries) return false; const keys = Object.keys(entries); return keys.length === Object.keys(lastEntries).length && keys.every(key => lastEntries[key] === entries[key]); }; const restore = async (timeoutMs = 240) => { const bundle = parseBundle(await readIsolated(timeoutMs)); if (!bundle) return { restored: 0, available: false }; let restored = 0; for (const [key, value] of Object.entries(bundle.entries || {})) { if (!isMirroredKey(key) || !isValidValue(key, value)) continue; try { const current = localStorage.getItem(key); if (isValidValue(key, current)) continue; localStorage.setItem(key, value); restored += 1; } catch (_) {} } restoredCount = restored; lastEntries = collectEntries(); return { restored, available: true }; }; const flush = async (force = false) => { if (writing) { rerun = true; try { await activeWrite; } catch (_) {} return flush(force); } const entries = collectEntries(); if (!force && entriesUnchanged(entries)) return true; const serialized = JSON.stringify({ schema: 1, updatedAt: Date.now(), entries }); if (new TextEncoder().encode(serialized).byteLength > MAX_VAULT_BYTES) return false; writing = true; activeWrite = writeIsolated(serialized); const ok = await activeWrite; activeWrite = null; writing = false; if (ok) lastEntries = entries; if (rerun) { rerun = false; return flush(); } return ok; }; const queue = (delay = 250) => { clearTimeout(queued); queued = setTimeout(() => { queued = 0; void flush(); }, Math.max(0, delay)); }; const start = () => { void flush(true); timer = setInterval(() => queue(0), 4000); const onLocalWrite = () => queue(); const onStorage = event => { if (isMirroredKey(event.key)) queue(); }; const onVisibility = () => { if (document.hidden) queue(0); }; window.addEventListener('ryujo_multicam_storage', onLocalWrite); window.addEventListener('storage', onStorage); document.addEventListener('visibilitychange', onVisibility); window.addEventListener('pagehide', () => { clearInterval(timer); clearTimeout(queued); void flush(true); }, { once: true }); return () => { clearInterval(timer); clearTimeout(queued); window.removeEventListener('ryujo_multicam_storage', onLocalWrite); window.removeEventListener('storage', onStorage); document.removeEventListener('visibilitychange', onVisibility); }; }; return { restore, flush, queue, start, keys: () => Object.keys(collectEntries()), status: () => ({ restoredCount, mirroredKeys: Object.keys(collectEntries()).length }) }; })(); // Do not hold the complete userscript behind a slow extension-storage IPC. // Local settings are the synchronous primary source; the isolated vault is // only a recovery mirror. Existing installations restore in the background, // while a genuinely empty installation gets a short recovery window. let roomGridHasLocalPrimarySettings = false; try { const localPrimary = JSON.parse(localStorage.getItem('ryujo_multicam_v8') || 'null'); roomGridHasLocalPrimarySettings = !!localPrimary && typeof localPrimary === 'object' && Array.isArray(localPrimary.rooms) && Array.isArray(localPrimary.groups); } catch (_) {} try { // Never hold the first usable frame behind extension-storage IPC. A truly // empty installation can render the defaults immediately; if the recovery // vault later supplies an existing configuration, reload once with that // restored primary state. Normal updates already have local primary data // and therefore never need the reload path. void RoomGridPersistentVault.restore(240).then(result => { if (!roomGridHasLocalPrimarySettings && Number(result?.restored) > 0) location.reload(); }).catch(error => { try { console.warn('[RoomGrid] persistent settings restore failed', error); } catch (_) {} }); } catch (error) { try { console.warn('[RoomGrid] persistent settings restore failed', error); } catch (_) {} } // The switch is kept separately from the workstation config so it also works // on ordinary Chaturbate room pages and is shared by all same-origin tabs. const TIP_SOUND_MUTE_KEY = 'ryujo_multicam_tip_sound_muted_v1'; const TIP_SOUND_HOOK_FLAG = '__ryujoTipSoundMuteWrapper'; function isTipSoundMuted() { try { return localStorage.getItem(TIP_SOUND_MUTE_KEY) !== '0'; } catch (_) { return true; } } function setTipSoundMuted(muted) { try { localStorage.setItem(TIP_SOUND_MUTE_KEY, muted ? '1' : '0'); } catch (_) {} try { window.dispatchEvent(new CustomEvent('ryujo_tip_sound_setting')); } catch (_) {} } // Chaturbate's local tip notification has historically been routed through // PlayBeep. Wrapping that function leaves the HLS/video audio untouched. // The short retry window also catches pages that define it after document-end. function installTipSoundMuteHook() { const names = ['PlayBeep', 'playBeep']; const patch = () => { let installed = false; for (const name of names) { try { const original = window[name]; if (typeof original !== 'function') continue; if (original[TIP_SOUND_HOOK_FLAG]) { installed = true; continue; } const wrapped = function (...args) { if (isTipSoundMuted()) return undefined; return original.apply(this, args); }; Object.defineProperty(wrapped, TIP_SOUND_HOOK_FLAG, { value: true }); window[name] = wrapped; installed = true; } catch (_) {} } return installed; }; if (patch()) return; const timer = setInterval(() => { if (patch()) clearInterval(timer); }, 250); setTimeout(() => clearInterval(timer), 30000); } installTipSoundMuteHook(); /* Generated Camclient tag-artwork sprite. build.mjs replaces the placeholder * with the local transparent PNG as a data URL, so the installed userscript * has no external image dependency. */ const CAMCLIENT_TAG_EMOJI_SPRITE_DATA_URL = 'data:image/png;base64,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'; /* ============================================================= * 0. 工具层 / Utils * ============================================================= */ let roomGridUiText = value => String(value ?? ''); const ROOMGRID_RAW_TEXT = Symbol('roomgridRawText'); const rawUiText = value => ({ [ROOMGRID_RAW_TEXT]: true, value: String(value ?? '') }); const unwrapRawUiText = value => value?.[ROOMGRID_RAW_TEXT] ? value.value : value; const $ = (tag, props = {}, children = []) => { const el = document.createElement(tag); for (const [k, v] of Object.entries(props)) { const rawValue = unwrapRawUiText(v); const uiValue = typeof rawValue === 'string' && !v?.[ROOMGRID_RAW_TEXT] && ['title', 'placeholder', 'ariaLabel', 'alt'].includes(k) ? roomGridUiText(rawValue) : rawValue; if (k === 'style' && typeof uiValue === 'object') Object.assign(el.style, uiValue); else if (k === 'class') el.className = uiValue; else if (k === 'dataset') Object.assign(el.dataset, uiValue); else if (k.startsWith('on') && typeof uiValue === 'function') el.addEventListener(k.slice(2).toLowerCase(), uiValue); else if (k === 'html') setTrustedHtml(el, uiValue); else if (k in el) el[k] = uiValue; else el.setAttribute(k, uiValue); } for (const c of [].concat(children)) { if (c == null || c === false) continue; if (c?.[ROOMGRID_RAW_TEXT]) el.appendChild(document.createTextNode(c.value)); else el.appendChild(typeof c === 'string' ? document.createTextNode(roomGridUiText(c)) : c); } const title = typeof props.title === 'string' ? props.title.trim() : ''; if (title) setElementHint(el, title); return el; }; const RoomGridAudioSilencer = (() => { const KEY = 'roomgrid_audio_silencer_v1'; let enabled = true; let users = 0; let observer = null; let originalPlay = null; let originalBufferStart = null; let playPrototype = null; let bufferPrototype = null; let storageHandler = null; const touched = new Set(); const previous = new WeakMap(); function read() { try { const value = JSON.parse(localStorage.getItem(KEY) || '{}'); enabled = value.enabled === undefined ? true : !!value.enabled; } catch (_) { enabled = true; } return enabled; } function rememberAndMute(element) { if (!element || String(element.tagName || '').toUpperCase() !== 'AUDIO') return; if (!previous.has(element)) previous.set(element, { muted: !!element.muted, volume: Number(element.volume) || 0, autoplay: !!element.autoplay }); touched.add(element); try { element.autoplay = false; element.muted = true; element.volume = 0; element.pause(); } catch (_) {} } function apply() { document.body?.classList?.toggle('roomgrid-site-sounds-muted', enabled); if (enabled) document.querySelectorAll?.('audio')?.forEach(rememberAndMute); else { touched.forEach(element => { const old = previous.get(element); if (!element?.isConnected || !old) return; try { element.muted = old.muted; element.volume = old.volume; element.autoplay = old.autoplay; } catch (_) {} }); touched.clear(); } } function patch() { if (observer || originalPlay || !window.HTMLMediaElement?.prototype) return; playPrototype = window.HTMLMediaElement.prototype; originalPlay = playPrototype.play; try { playPrototype.play = function roomGridGuardedPlay(...args) { if (enabled && String(this?.tagName || '').toUpperCase() === 'AUDIO') { rememberAndMute(this); return Promise.resolve(); } return originalPlay.apply(this, args); }; } catch (_) { originalPlay = null; playPrototype = null; } if (window.AudioBufferSourceNode?.prototype?.start) { bufferPrototype = window.AudioBufferSourceNode.prototype; originalBufferStart = bufferPrototype.start; try { bufferPrototype.start = function roomGridGuardedBufferStart(...args) { if (enabled) return undefined; return originalBufferStart.apply(this, args); }; } catch (_) { originalBufferStart = null; bufferPrototype = null; } } observer = new MutationObserver(records => { if (!enabled) return; records.forEach(record => record.addedNodes.forEach(node => { if (node?.nodeType !== 1) return; if (String(node.tagName || '').toUpperCase() === 'AUDIO') rememberAndMute(node); node.querySelectorAll?.('audio')?.forEach(rememberAndMute); })); }); observer.observe(document.documentElement, { childList: true, subtree: true }); storageHandler = event => { if (event.key !== KEY) return; read(); apply(); try { window.dispatchEvent(new CustomEvent('roomgrid_audio_silencer_changed', { detail: { enabled } })); } catch (_) {} }; window.addEventListener('storage', storageHandler); } function unpatch() { observer?.disconnect(); observer = null; if (storageHandler) window.removeEventListener('storage', storageHandler); storageHandler = null; if (playPrototype && originalPlay) { try { playPrototype.play = originalPlay; } catch (_) {} } if (bufferPrototype && originalBufferStart) { try { bufferPrototype.start = originalBufferStart; } catch (_) {} } playPrototype = null; bufferPrototype = null; originalPlay = null; originalBufferStart = null; document.body?.classList?.remove('roomgrid-site-sounds-muted'); } function setEnabled(value) { enabled = !!value; try { localStorage.setItem(KEY, JSON.stringify({ schema: 1, enabled, updatedAt: Date.now() })); } catch (_) {} apply(); try { window.dispatchEvent(new CustomEvent('roomgrid_audio_silencer_changed', { detail: { enabled } })); } catch (_) {} return enabled; } function init() { users += 1; read(); patch(); apply(); return () => { users = Math.max(0, users - 1); if (!users) unpatch(); }; } return { init, setEnabled, isEnabled: () => enabled, read }; })(); function setElementHint(el, text) { if (!el || !text) return el; const v = roomGridUiText(String(text).trim()).trim(); if (!v) return el; try { el.title = v; } catch (_) {} try { el.dataset.hint = v; } catch (_) {} try { if (/^(BUTTON|INPUT|SELECT|TEXTAREA)$/i.test(el.tagName || '')) { el.setAttribute('aria-label', v); } } catch (_) {} return el; } function htmlEscape(s) { return String(s ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[ch])); } const TRUSTED_HTML_MARK = '__roomgridTrustedHtml'; function trustedHtml(html) { return { [TRUSTED_HTML_MARK]: true, html: String(html ?? '') }; } function setTrustedHtml(el, value) { if (!el) return; if (value && typeof value === 'object' && value[TRUSTED_HTML_MARK] === true) el.innerHTML = value.html; else el.textContent = String(value ?? ''); } const ICONS = { menu: '', refresh: '', pause: '', play: '', volume: '', volumeOff: '', camera: '', record: '', stop: '', pip: '', expand: '', more: '', close: '', drag: '', resize: '', grid: '', focus: '', clean: '', search: '', bell: '', external: '', copy: '', folder: '', star: '', trash: '', }; function iconSvg(name, size = 16) { const body = ICONS[name] || ICONS.more; return ``; } function iconLabel(name, text, size = 15) { return `${iconSvg(name, size)}${htmlEscape(text)}`; } const debounce = (fn, ms = 200) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }; const fmtTime = (ts) => { if (!ts) return '—'; const diff = (Date.now() - ts) / 1000; const ago = (n, u) => tx(`${n}${u} 前`, `${n}${u} ago`, `vor ${n} ${u}`, `hace ${n}${u}`, `há ${n}${u}`); if (diff < 60) return ago(Math.floor(diff), 's'); if (diff < 3600) return ago(Math.floor(diff / 60), 'm'); if (diff < 86400) return ago(Math.floor(diff / 3600), 'h'); return ago(Math.floor(diff / 86400), 'd'); }; const uuid = () => { try { if (crypto?.randomUUID) return 'g_' + crypto.randomUUID().replace(/-/g, '').slice(0, 10); if (crypto?.getRandomValues) { const buf = new Uint32Array(2); crypto.getRandomValues(buf); return 'g_' + [...buf].map(n => n.toString(36)).join('').slice(0, 10); } } catch (_) {} return 'g_' + Math.random().toString(36).slice(2, 12); }; const LIBRARY_GROUP_ID = 'library'; const DEFAULT_GROUP_ID = 'all'; const ONLINE_GROUP_ID = 'online'; const FAVORITE_GROUP_ID = 'fav'; // v15.5: 稳定状态。多工作台/多窗口同时轮询时,不再用 loading / transient error 覆盖 // 已确认的 online/offline/private,避免筛选、分页和 HLS 反复重建造成闪屏。 const STABLE_ROOM_STATUSES = new Set(['online', 'offline', 'private']); function isStableRoomStatus(status) { return STABLE_ROOM_STATUSES.has(String(status || '')); } function isTransientRoomStatus(status) { return status === 'loading' || status === 'error'; } function defaultShortcuts() { return { commandPalette: 'ctrl+k', globalSearch: 'ctrl+shift+k', focusAdd: '/', refreshAll: 'r', gridView: 'g', focusView: 'f', pureMode: 'alt+p', viewerMode: 'alt+v', focusThumbs: 'alt+t', recordingCenter: 'alt+shift+c', recordPage: 'alt+shift+r', }; } const WORKSPACE_LAYOUT_KEYS = Object.freeze([ 'activeGroup', 'viewMode', 'focusedRoomId', 'focusRoomSlots', 'gridRoomSlots', 'layoutSize', 'useCustomLayout', 'customLayoutRows', 'customLayoutCols', 'pageIndex', 'focusMainPct', 'focusMainHPct', 'focusAspect', 'focusThumbSize', 'videoFit', 'sidebarWidth', 'sidebarCollapsed', 'toolbarCollapsed', 'viewerMode', 'filter', 'sortBy', 'favoriteFirst', 'gridSize', 'gridCellSize', ]); function workspaceDeviceClass() { try { return window.innerWidth < 900 ? 'compact' : 'desktop'; } catch (_) { return 'desktop'; } } function workspaceLayoutFromSettings(settings) { const source = settings && typeof settings === 'object' ? settings : {}; const layout = {}; for (const key of WORKSPACE_LAYOUT_KEYS) { if (!(key in source)) continue; try { layout[key] = JSON.parse(JSON.stringify(source[key])); } catch (_) { layout[key] = source[key]; } } return layout; } function normalizeShortcutSpec(value) { const raw = String(value || '').trim().toLowerCase().replace(/\s+/g, ''); if (!raw) return ''; const parts = raw.split('+').filter(Boolean); const mods = new Set(); let key = ''; for (const part of parts) { if (part === 'control') mods.add('ctrl'); else if (part === 'cmd' || part === 'command') mods.add('meta'); else if (['ctrl', 'meta', 'alt', 'shift'].includes(part)) mods.add(part); else key = part === ' ' ? 'space' : part; } if (!key) return ''; const out = []; ['ctrl', 'meta', 'alt', 'shift'].forEach(m => { if (mods.has(m)) out.push(m); }); out.push(key); return out.join('+'); } function sanitizeShortcuts(input, fallback = defaultShortcuts()) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const out = {}; for (const [action, spec] of Object.entries(fallback)) { out[action] = normalizeShortcutSpec(source[action]) || normalizeShortcutSpec(spec); } return out; } function shortcutFromEvent(e) { const keyRaw = e.code === 'Space' ? 'space' : String(e.key || '').toLowerCase(); const key = keyRaw === ' ' ? 'space' : keyRaw; if (!key || ['control', 'shift', 'alt', 'meta'].includes(key)) return ''; const out = []; if (e.ctrlKey) out.push('ctrl'); if (e.metaKey) out.push('meta'); if (e.altKey) out.push('alt'); if (e.shiftKey) out.push('shift'); out.push(key); return normalizeShortcutSpec(out.join('+')); } function shortcutLabel(spec) { const normalized = normalizeShortcutSpec(spec); if (!normalized) return ''; const names = { ctrl: 'Ctrl', meta: 'Meta', alt: 'Alt', shift: 'Shift', space: 'Space', arrowleft: 'Left', arrowright: 'Right', arrowup: 'Up', arrowdown: 'Down' }; return normalized.split('+').map(part => names[part] || (part.length === 1 ? part.toUpperCase() : part)).join('+'); } function defaultVideoTransform() { return { mirror: false, flip: false, rotation: 0, zoom: 1, x: 0, y: 0, brightness: 1, contrast: 1, saturation: 1, hue: 0, sepia: 0, grayscale: 0, invert: 0, blur: 0 }; } function sanitizeVideoTransform(input) { const src = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const rotationRaw = Number(src.rotation) || 0; const rotation = ((Math.round(rotationRaw / 90) * 90) % 360 + 360) % 360; return { mirror: !!src.mirror, flip: !!src.flip, rotation, zoom: Math.max(1, Math.min(20, Number(src.zoom) || 1)), x: Math.max(-5000, Math.min(5000, Number(src.x) || 0)), y: Math.max(-5000, Math.min(5000, Number(src.y) || 0)), brightness: Math.max(.25, Math.min(2.5, Number(src.brightness) || 1)), contrast: Math.max(.25, Math.min(2.5, Number(src.contrast) || 1)), saturation: Math.max(0, Math.min(3, Number.isFinite(Number(src.saturation)) ? Number(src.saturation) : 1)), hue: Math.max(-180, Math.min(180, Number(src.hue) || 0)), sepia: Math.max(0, Math.min(1, Number(src.sepia) || 0)), grayscale: Math.max(0, Math.min(1, Number(src.grayscale) || 0)), invert: Math.max(0, Math.min(1, Number(src.invert) || 0)), blur: Math.max(0, Math.min(12, Number(src.blur) || 0)), }; } function isDefaultVideoTransform(transform) { const t = sanitizeVideoTransform(transform); return !t.mirror && !t.flip && t.rotation === 0 && t.zoom === 1 && t.x === 0 && t.y === 0 && t.brightness === 1 && t.contrast === 1 && t.saturation === 1 && t.hue === 0 && t.sepia === 0 && t.grayscale === 0 && t.invert === 0 && t.blur === 0; } function sanitizeVideoTransformMap(input) { const out = {}; if (input && typeof input === 'object' && !Array.isArray(input)) { for (const [id, value] of Object.entries(input)) { const roomId = normalizeUsername(id); if (!roomId) continue; const transform = sanitizeVideoTransform(value); if (!isDefaultVideoTransform(transform)) out[roomId] = transform; } } return out; } function sanitizeGridRoomSlots(input) { const out = {}; if (!input || typeof input !== 'object' || Array.isArray(input)) return out; for (const [rawKey, rawSlots] of Object.entries(input).slice(0, 80)) { const key = String(rawKey || '').slice(0, 2048); if (!key || !Array.isArray(rawSlots)) continue; const slots = Array.from({ length: MAX_LAYOUT_SLOTS }, (_, idx) => { const id = normalizeUsername(rawSlots[idx] || ''); return isLikelyUsername(id) ? id : null; }); if (slots.some(Boolean) || rawSlots.length) out[key] = slots; } return out; } function encodeSharePayload(data) { const json = JSON.stringify(data); const bytes = new TextEncoder().encode(json); let bin = ''; for (let i = 0; i < bytes.length; i += 0x8000) { bin += String.fromCharCode(...bytes.slice(i, i + 0x8000)); } return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function decodeSharePayload(data) { const normalized = String(data || '').replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4); const bin = atob(padded); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return JSON.parse(new TextDecoder().decode(bytes)); } function normalizeUsername(raw) { // Internal IDs are already lowercase canonical usernames. Avoid repeated // trimming and replacement when processing large local model collections. if (typeof raw === 'string' && /^[a-z0-9_-]+$/.test(raw)) return raw; let v = String(raw || '').trim(); if (!v) return ''; try { if (/^https?:\/\//i.test(v)) { const url = new URL(v); v = url.pathname.split('/').filter(Boolean)[0] || ''; } } catch (_) {} return v.replace(/^@+/, '').replace(/^\/+|\/+$/g, '').trim().toLowerCase(); } function usernameSyntaxOk(v) { v = normalizeUsername(v); return !!v && v.length >= 2 && v.length <= 32 && /^[a-z0-9_-]+$/i.test(v) && !/^\d+$/.test(v); } function safeGroupName(raw, fallback = '') { const v = String(raw ?? '').replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim(); return (v || fallback || '').slice(0, 80); } function isSafeHttpUrl(raw, base = location.href) { try { const u = new URL(String(raw || ''), base); return u.protocol === 'https:' || u.protocol === 'http:'; } catch (_) { return false; } } function isSafeStreamUrl(raw) { const s = String(raw || '').trim(); if (!s || s.length > 4096 || /[\u0000-\u001f\u007f]/.test(s)) return false; // Accept only http(s). Do not require a .m3u8 suffix because some CDNs serve playlists through signed routes. return isSafeHttpUrl(s); } function safeChaturbateHost(host) { const h = String(host || '').toLowerCase(); return h === 'chaturbate.com' || h.endsWith('.chaturbate.com'); } async function copyText(text) { text = String(text || ''); try { await navigator.clipboard.writeText(text); return true; } catch (_) { try { const ta = $('textarea', { value: text, style: { position: 'fixed', left: '-9999px', top: '-9999px', opacity: '0' } }); document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); ta.remove(); return ok; } catch (_) { return false; } } } function downloadBlob(blob, filename) { const a = $('a', { href: URL.createObjectURL(blob), download: filename }); document.body.appendChild(a); a.click(); setTimeout(() => { try { URL.revokeObjectURL(a.href); a.remove(); } catch (_) {} }, 1200); } async function downloadBlobToSubfolder(blob, filename, folder = '') { const safeName = String(filename || `camclient-${Date.now()}`) .replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/gu, '-') .replace(/[. ]+$/u, '') .slice(0, 180) || `camclient-${Date.now()}`; const safeFolder = String(folder || '').split(/[\\/]+/u).map(safeFilePart).filter(Boolean).join('/'); const name = safeFolder ? `${safeFolder}/${safeName}` : safeName; const details = { url: blob, name, saveAs: false, conflictAction: 'uniquify' }; try { if (typeof globalThis.GM?.download === 'function') { await GM.download(details); return { browserApi: true, name }; } if (typeof globalThis.GM_download === 'function') { await new Promise((resolve, reject) => GM_download({ ...details, onload: resolve, onerror: reject, ontimeout: reject })); return { browserApi: true, name }; } } catch (_) {} downloadBlob(blob, safeName); return { browserApi: false, name: safeName }; } function openNoopener(url, target = '_blank') { const safeTarget = target || '_blank'; const w = window.open(String(url || 'about:blank'), safeTarget, 'noopener,noreferrer'); try { if (w) w.opener = null; } catch (_) {} return w; } function safeFilePart(v) { return String(v || 'room').replace(/[^a-z0-9_-]+/gi, '_').slice(0, 48) || 'room'; } function stampForFile() { return new Date().toISOString().replace(/[:.]/g, '-'); } function stopMediaElement(media, remove = false) { if (!media) return; try { media.muted = true; media.volume = 0; media.pause(); } catch (_) {} try { const srcObject = media.srcObject; if (srcObject && typeof srcObject.getTracks === 'function') { srcObject.getTracks().forEach(track => { try { track.stop(); } catch (_) {} }); } } catch (_) {} try { media.srcObject = null; } catch (_) {} try { media.querySelectorAll?.('source').forEach(source => { try { source.removeAttribute('src'); source.src = ''; source.remove(); } catch (_) {} }); } catch (_) {} try { media.removeAttribute('src'); media.src = ''; media.load(); } catch (_) {} if (remove) { try { media.remove(); } catch (_) {} } } function stopAllPageMedia(root = document) { // 当前页打开工作台、新开工作台或整页卸载前,先切断页面上原有的音视频。 try { root.querySelectorAll('video,audio').forEach(media => stopMediaElement(media, false)); } catch (_) {} } /* Generated RoomGrid language catalog. Regenerate with tools/generate-latin-i18n.mjs. */ const ROOMGRID_LATIN_PACKED = {"en":"{\"phrases\":{},\"patterns\":{},\"visible\":{\"‹ Zurück\":\"‹ Back\",\"● 1.2k · #tag\":\"● 1.2k · #tag\",\"1 Jahr\":\"1 year\",\"30 Tage\":\"30 days\",\"7 Tage\":\"7 days\",\"90 Tage\":\"90 days\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Show similar models\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"All changes are saved locally and also apply to the next model.\",\"Alle eigenen Tags\":\"All custom tags\",\"Alle Gruppen\":\"All groups\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Request Older Chaturbate Data\",\"Automatisch abspielen\":\"Autoplay\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Automatically captured · fully organized when editing\",\"Automatisch neu verbinden\":\"Automatically reconnect\",\"Bei verstecktem Tab pausieren\":\"Pause while tab is hidden\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Apply existing Chaturbate account details\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Optimize image · large zoom, position, filter and rotation\",\"Bildanpassung\":\"Image adjustment\",\"Bildwerkzeuge einklappen\":\"Collapse image tools\",\"Breite des Informationsbereichs\":\"Width of information area\",\"Broadcaster-Verbindung verwalten\":\"Manage broadcaster connection\",\"Chat anzeigen\":\"Show chat\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"This means you are not limited to data since the script was installed: RoomGrid can read your existing following list and the clearly recognizable entries from My Collection. No messages, payments, media or passwords are copied.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"The Analytics Studio also contains time period comparisons, heatmaps, discover funnels, workspace and quality analyzes as well as data exports.\",\"Datenschutz & Export\":\"Data protection & export\",\"Deine Aktivität der letzten 14 Tage\":\"Your activity over the last 14 days\",\"Deine meistgesehenen Models\":\"Your most viewed models\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"The current connection is active; After the next measurement, the local course appears here.\",\"Diese Bibliotheksansicht ist noch leer.\":\"This library view is still empty.\",\"Eigene Priorität\":\"Own priority\",\"Eigene Tags\":\"Own tags\",\"Eigene Tags filtern\":\"Filter your own tags\",\"Eigenen Tag eingeben …\":\"Enter your own day...\",\"Einstellungen\":\"Settings\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter or “Add Tag” takes over the draft. It is only saved permanently with “Save profile”. Already used tags are suggested as you type.\",\"Ereignisse im Zeitraum\":\"Events in the period\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Create freely named collections, assign models to multiple groups and display one or more groups together here.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"First create your own group above.\",\"Follower\":\"Followers\",\"GAMING\":\"GAMING\",\"Gesamter Verlauf\":\"Entire course\",\"Grid +\":\"Grid+\",\"Größe der Bedienelemente\":\"Size of controls\",\"Gruppe löschen\":\"Delete group\",\"Gruppen umbenennen oder löschen\":\"Rename or delete groups\",\"HD + Gaming\":\"HD+ gaming\",\"IndexedDB\":\"IndexedDB\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Every saved tag is automatically available in the search, in the auto-completion and here as a direct multiple filter.\",\"Keine Models passen zu diesen Filtern.\":\"No models match these filters.\",\"Keine Priorität\":\"Not a priority\",\"Konto-Daten werden eingelesen …\":\"Account details are being read in...\",\"Lautstärke pro Model merken\":\"Note volume per model\",\"Layout & sichtbare Bereiche\":\"Layout & visible areas\",\"Letzte Messung\":\"Last measurement\",\"LIVE\":\"LIVE\",\"lokal · für Suche und Filter\":\"local · for search and filters\",\"Maximale Stream-Lautstärke\":\"Maximum stream volume\",\"Model besitzt alle gewählten Tags\":\"Model owns all selected tags\",\"Model besitzt mindestens einen gewählten Tag\":\"Model has at least one selected tag\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid cannot guess personal Chaturbate account values. These cards only appear if you have consciously linked the official broadcaster statistics to your own username and token.\",\"RoomGrid Suite\":\"RoomGrid Suite\",\"Seitengröße\":\"Page size\",\"SingleCam\":\"SingleCam\",\"SingleCam einstellen\":\"Configure SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Restore SingleCam default\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"As soon as you save your own tag for a model, it will appear here and in the autocomplete.\",\"Status im Player anzeigen\":\"Show status in player\",\"Status-Aktualisierung\":\"Status update\",\"Stream immer stumm starten\":\"Always start stream silently\",\"Tag hinzufügen\":\"Add tag\",\"Tipps letzte Stunde\":\"Tips last hour\",\"Token-Guthaben\":\"Token balance\",\"Ton & Ruhe\":\"Sound & calm\",\"Ungefähre Onlinezeiten\":\"Approximate online times\",\"Offizielle Statistik verbinden\":\"Connect official statistics\",\"Optionale offizielle Kontostatistik\":\"Optional official account statistics\",\"Persönlich organisiert\":\"Personally organized\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Player & connection\",\"Player neu verbinden\":\"Reconnect player\",\"Player-Ecken\":\"Player corners\",\"Player-Hintergrund\":\"Player background\",\"Player-Höhe\":\"Player height\",\"Player-Seitenverhältnis\":\"Player aspect ratio\",\"Position der Informationen\":\"Location of information\",\"Private Notiz\":\"Private note\",\"Private Notizen anzeigen\":\"View private notes\",\"Puffergröße\":\"Buffer size\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Verbunden\":\"Connected\",\"Vollständige Modelstatistik\":\"Full model statistics\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Read in existing account data now\",\"Weiter ›\":\"Next ›\",\"Zufriedenheit\":\"Satisfaction\",\"Model ist in allen gewählten Gruppen\":\"Model is in all selected groups\",\"Model ist in mindestens einer gewählten Gruppe\":\"Model is in at least one selected group\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Open My Collection\",\"Namen speichern\":\"Save name\",\"Nicht verbunden\":\"Not connected\",\"Niedrige Live-Latenz\":\"Low live latency\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"There are still no sufficient local usage values.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"No group of our own yet. Enter a name above and create your first collection.\",\"Noch keine Model-Sehzeit erfasst.\":\"No model viewing time recorded yet.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"No official account snapshots yet.\",\"Noch nicht eingelesen\":\"Not read yet\",\"Nur auf diesem Gerät\":\"Only on this device\",\"Öffentliche/erkannte Tags\":\"Public/recognized tags\",\"Öffentliches Profil anzeigen\":\"View public profile\",\"Gruppen\":\"Groups\"}}","de":"{\"phrases\":{},\"patterns\":{},\"visible\":{\"‹ Zurück\":\"‹ Zurück\",\"● 1.2k · #tag\":\"● 1.2k · #tag\",\"1 Jahr\":\"1 Jahr\",\"30 Tage\":\"30 Tage\",\"7 Tage\":\"7 Tage\",\"90 Tage\":\"90 Tage\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Ähnliche Models anzeigen\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\",\"Alle eigenen Tags\":\"Alle eigenen Tags\",\"Alle Gruppen\":\"Alle Gruppen\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Ältere Chaturbate-Daten anfordern\",\"Automatisch abspielen\":\"Automatisch abspielen\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Diese Bibliotheksansicht ist noch leer.\",\"Eigene Priorität\":\"Eigene Priorität\",\"Eigene Tags\":\"Eigene Tags\",\"Eigene Tags filtern\":\"Eigene Tags filtern\",\"Eigenen Tag eingeben …\":\"Eigenen Tag eingeben …\",\"Einstellungen\":\"Einstellungen\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\",\"Ereignisse im Zeitraum\":\"Ereignisse im Zeitraum\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Erstelle oben zuerst eine eigene Gruppe.\",\"Follower\":\"Follower\",\"GAMING\":\"GAMING\",\"Gesamter Verlauf\":\"Gesamter Verlauf\",\"Grid +\":\"Grid +\",\"Größe der Bedienelemente\":\"Größe der Bedienelemente\",\"Gruppe löschen\":\"Gruppe löschen\",\"Gruppen umbenennen oder löschen\":\"Gruppen umbenennen oder löschen\",\"HD + Gaming\":\"HD + Gaming\",\"IndexedDB\":\"IndexedDB\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\",\"Keine Models passen zu diesen Filtern.\":\"Keine Models passen zu diesen Filtern.\",\"Keine Priorität\":\"Keine Priorität\",\"Konto-Daten werden eingelesen …\":\"Konto-Daten werden eingelesen …\",\"Lautstärke pro Model merken\":\"Lautstärke pro Model merken\",\"Layout & sichtbare Bereiche\":\"Layout & sichtbare Bereiche\",\"Letzte Messung\":\"Letzte Messung\",\"LIVE\":\"LIVE\",\"lokal · für Suche und Filter\":\"lokal · für Suche und Filter\",\"Maximale Stream-Lautstärke\":\"Maximale Stream-Lautstärke\",\"Model besitzt alle gewählten Tags\":\"Model besitzt alle gewählten Tags\",\"Model besitzt mindestens einen gewählten Tag\":\"Model besitzt mindestens einen gewählten Tag\",\"Automatisch neu verbinden\":\"Automatisch neu verbinden\",\"Bei verstecktem Tab pausieren\":\"Bei verstecktem Tab pausieren\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Bestehende Chaturbate-Kontodaten übernehmen\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Bild optimieren · großer Zoom, Position, Filter und Drehung\",\"Bildanpassung\":\"Bildanpassung\",\"Bildwerkzeuge einklappen\":\"Bildwerkzeuge einklappen\",\"Breite des Informationsbereichs\":\"Breite des Informationsbereichs\",\"Broadcaster-Verbindung verwalten\":\"Broadcaster-Verbindung verwalten\",\"Chat anzeigen\":\"Chat anzeigen\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\",\"Datenschutz & Export\":\"Datenschutz & Export\",\"Deine Aktivität der letzten 14 Tage\":\"Deine Aktivität der letzten 14 Tage\",\"Deine meistgesehenen Models\":\"Deine meistgesehenen Models\",\"Offizielle Statistik verbinden\":\"Offizielle Statistik verbinden\",\"Optionale offizielle Kontostatistik\":\"Optionale offizielle Kontostatistik\",\"Persönlich organisiert\":\"Persönlich organisiert\",\"PiP\":\"PiP\",\"Player & Verbindung\":\"Player & Verbindung\",\"Player neu verbinden\":\"Player neu verbinden\",\"Player-Ecken\":\"Player-Ecken\",\"Player-Hintergrund\":\"Player-Hintergrund\",\"Player-Höhe\":\"Player-Höhe\",\"Player-Seitenverhältnis\":\"Player-Seitenverhältnis\",\"Position der Informationen\":\"Position der Informationen\",\"Private Notiz\":\"Private Notiz\",\"Private Notizen anzeigen\":\"Private Notizen anzeigen\",\"Puffergröße\":\"Puffergröße\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Model ist in allen gewählten Gruppen\":\"Model ist in allen gewählten Gruppen\",\"Model ist in mindestens einer gewählten Gruppe\":\"Model ist in mindestens einer gewählten Gruppe\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam +\",\"My Collection öffnen\":\"My Collection öffnen\",\"Namen speichern\":\"Namen speichern\",\"Nicht verbunden\":\"Nicht verbunden\",\"Niedrige Live-Latenz\":\"Niedrige Live-Latenz\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Noch keine ausreichenden lokalen Nutzungswerte.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\",\"Noch keine Model-Sehzeit erfasst.\":\"Noch keine Model-Sehzeit erfasst.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Noch keine offiziellen Konto-Snapshots vorhanden.\",\"Noch nicht eingelesen\":\"Noch nicht eingelesen\",\"Nur auf diesem Gerät\":\"Nur auf diesem Gerät\",\"Öffentliche/erkannte Tags\":\"Öffentliche/erkannte Tags\",\"Öffentliches Profil anzeigen\":\"Öffentliches Profil anzeigen\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\",\"RoomGrid Suite\":\"RoomGrid Suite\",\"Seitengröße\":\"Seitengröße\",\"SingleCam\":\"SingleCam\",\"SingleCam einstellen\":\"SingleCam einstellen\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"SingleCam-Standard wiederherstellen\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\",\"Status im Player anzeigen\":\"Status im Player anzeigen\",\"Status-Aktualisierung\":\"Status-Aktualisierung\",\"Stream immer stumm starten\":\"Stream immer stumm starten\",\"Tag hinzufügen\":\"Tag hinzufügen\",\"Tipps letzte Stunde\":\"Tipps letzte Stunde\",\"Token-Guthaben\":\"Token-Guthaben\",\"Ton & Ruhe\":\"Ton & Ruhe\",\"Ungefähre Onlinezeiten\":\"Ungefähre Onlinezeiten\",\"Verbunden\":\"Verbunden\",\"Vollständige Modelstatistik\":\"Vollständige Modelstatistik\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Vorhandene Konto-Daten jetzt einlesen\",\"Weiter ›\":\"Weiter ›\",\"Zufriedenheit\":\"Zufriedenheit\",\"Groups\":\"Gruppen\",\"Configure SingleCam\":\"SingleCam einstellen\"}}","es":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"El cambio de idioma no se pudo preparar de forma segura. Esta página no fue abandonada.\",\"Language\":\"Idioma\",\"Default workspace\":\"Espacio de trabajo predeterminado\",\"Workspace\":\"Espacio de trabajo\",\"New workspace\":\"Nuevo espacio de trabajo\",\"copy\":\"copiar\",\"New group\":\"Nuevo grupo\",\"Saved model\":\"Modelo guardado\",\"Search models, pages, workspaces and commands…\":\"Busque modelos, páginas, espacios de trabajo y comandos…\",\"Type to filter · Enter to run · Esc to close\":\"Escribe para filtrar · Enter para ejecutar · Esc para cerrar\",\"No matches\":\"No hay coincidencias\",\"Currently active RoomGrid version\":\"Versión RoomGrid actualmente activa\",\"years\":\"años\",\"Live\":\"en vivo\",\"Private\":\"Privado\",\"Away\":\"Ausente\",\"Offline\":\"Sin conexión\",\"sessions\":\"sesiones\",\"NEW\":\"NUEVO\",\"Name, subject, tag or place\":\"Nombre, tema, etiqueta o lugar\",\"Website order\":\"Orden del sitio\",\"Most viewers\":\"Más espectadores\",\"Fewest viewers\":\"Menos espectadores\",\"Longest online\":\"Más tiempo en línea\",\"Recently online\":\"Recién conectados\",\"Age ascending\":\"Edad ascendente\",\"Age descending\":\"Edad descendente\",\"New models first\":\"Modelos nuevos primero\",\"Saved first\":\"Guardados primero\",\"Favorites first\":\"Favoritos primero\",\"Most watched\":\"Lo más visto\",\"Recently watched\":\"Vistos recientemente\",\"Random mix\":\"Mezcla aleatoria\",\"All loaded\":\"Todos los cargados\",\"Previous page\":\"Pagina anterior\",\"Next page\":\"Página siguiente\",\"Reload complete live list\":\"Recargar lista completa en vivo\",\"All homepage options\":\"Todas las opciones de inicio\",\"Reset filters\":\"Restablecer filtros\",\"Quick filter\":\"filtro rápido\",\"Sort\":\"ordenar\",\"Models per page\":\"Modelos por página\",\"Load more results\":\"Cargar más resultados\",\"still to display\":\"aún por mostrar\",\"continues automatically while scrolling\":\"continúa automáticamente mientras se desplaza\",\"visible\":\"visibles\",\"loaded\":\"cargados\",\"page\":\"página\",\"active filters\":\"filtros activos\",\"loading complete live list\":\"cargando lista completa en vivo\",\"all matching live models found\":\"todos los modelos en vivo coincidentes encontrados\",\"live list incomplete, retry\":\"lista en vivo incompleta, vuelva a intentarlo\",\"Requests temporarily paused\":\"Solicitudes pausadas temporalmente\",\"Vault could not be unlocked\":\"La bóveda no se pudo desbloquear\",\"Preview Dock\":\"Muelle de vista previa\",\"Connecting…\":\"Conectando…\",\"Stream temporarily unavailable\":\"Transmisión no disponible temporalmente\",\"Error\":\"error\",\"Buffering\":\"Almacenamiento en búfer\",\"Preview unavailable\":\"Vista previa no disponible\",\"Connect\":\"Conectar\",\"Close\":\"Cerrar\",\"Opacity\":\"Opacidad\",\"Info\":\"Información\",\"Age\":\"Edad\",\"Gender\":\"Género\",\"Location\":\"Ubicación\",\"Country\":\"País\",\"Last broadcast\":\"Última emisión\",\"Fan club\":\"club de fans\",\"Private show\":\"show privado\",\"Spy show\":\"espectáculo de espías\",\"Model comparison list\":\"Lista de comparación de modelos\",\"Compare\":\"Comparar\",\"Open model details\":\"Abrir detalles del modelo\",\"Compare now\":\"Comparar ahora\",\"Clear\":\"Borrar\",\"Up to four models can be compared at once.\":\"Se pueden comparar hasta cuatro modelos a la vez.\",\"Remove from comparison\":\"Eliminar de la comparación\",\"Add to comparison\":\"Añadir a comparación\",\"Total watched\":\"total visto\",\"Sessions\":\"Sesiones\",\"Viewers\":\"Espectadores\",\"Rating\":\"Calificación\",\"Online samples\":\"Muestras en línea\",\"Loading quality data…\":\"Cargando datos de calidad…\",\"samples\":\"muestras\",\"stalls\":\"puestos\",\"No quality measurements yet\":\"Aún no hay mediciones de calidad\",\"Quality data unavailable\":\"Datos de calidad no disponibles\",\"Details\":\"Detalles\",\"Remove\":\"Quitar\",\"Model comparison\":\"Comparación de modelos\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Cada valor proviene de información pública, su propia organización u observaciones locales en este dispositivo. Los valores faltantes nunca se adivinan.\",\"Shared tags\":\"Etiquetas compartidas\",\"Loading public facts…\":\"Cargando datos públicos...\",\"Unknown status\":\"Estado desconocido\",\"Last activity\":\"Última actividad\",\"These statistics describe only your own local RoomGrid usage.\":\"Estas estadísticas describen únicamente su propio uso local de RoomGrid.\",\"At a glance\":\"De un vistazo\",\"Type\":\"Tipo\",\"Appearances\":\"Apariciones\",\"Public facts\":\"Hechos públicos\",\"No public biography available yet.\":\"Aún no hay biografía pública disponible.\",\"Public link\":\"Enlace público\",\"Public profile source\":\"Fuente del perfil público\",\"Profile updated\":\"Perfil actualizado\",\"Private note (local only)\":\"Nota privada (solo local)\",\"Your tags\":\"Tus etiquetas\",\"No rating\":\"Sin calificación\",\"No priority\":\"Sin prioridad\",\"Private library data\":\"Datos de biblioteca privada\",\"Private notes\":\"notas privadas\",\"Priority\":\"Prioridad\",\"Favorite\":\"Favorito\",\"Watchlist\":\"Lista de seguimiento\",\"Archived\":\"Archivado\",\"Watch again\":\"Mirar de nuevo\",\"Technical note\":\"Nota técnica\",\"Technical\":\"Técnico\",\"Save all changes\":\"Guardar todos los cambios\",\"Loading local activity…\":\"Cargando actividad local…\",\"Activity and playback quality\":\"Calidad de actividad y reproducción.\",\"Sun\":\"sol\",\"Mon\":\"lun\",\"Tue\":\"mar\",\"Wed\":\"mié\",\"Thu\":\"jueves\",\"Fri\":\"viernes\",\"Sat\":\"sábado\",\"Quality samples\":\"Muestras de calidad\",\"Average startup\":\"Inicio promedio\",\"Stalls\":\"Puestos\",\"Reconnects\":\"Se reconecta\",\"Dropped frames\":\"Marcos caídos\",\"Online observations\":\"Observaciones en línea\",\"Frequently observed online times\":\"Tiempos en línea observados con frecuencia\",\"Not enough online observations yet.\":\"Aún no hay suficientes observaciones en línea.\",\"Watched\":\"visto\",\"Preview\":\"Vista previa\",\"Quick view\":\"Vista rápida\",\"Discovered\":\"descubierto\",\"Quality\":\"Calidad\",\"Connection\":\"Conexión\",\"No local activity recorded yet.\":\"Aún no se ha registrado actividad local.\",\"Recent events\":\"Eventos recientes\",\"Activity data unavailable.\":\"Datos de actividad no disponibles.\",\"Similar models\":\"Modelos similares\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Calculado a partir de etiquetas locales/públicas, país y tipo. Cada sugerencia muestra su motivo y no se carga ningún dato.\",\"shared tags\":\"etiquetas compartidas\",\"same country\":\"mismo pais\",\"same type\":\"mismo tipo\",\"Not enough matching data yet.\":\"Aún no hay suficientes datos coincidentes.\",\"Overview\":\"Descripción general\",\"Profile\":\"Perfil\",\"Organize\":\"organizar\",\"Activity\":\"Actividad\",\"Similar\":\"similares\",\"Model detail sections\":\"Secciones de detalle del modelo\",\"Previous model\":\"modelo anterior\",\"Next model\":\"Siguiente modelo\",\"Open room\":\"Sala abierta\",\"Pin preview\":\"Vista previa del pin\",\"No public fields available\":\"No hay campos públicos disponibles\",\"Public facts currently unavailable\":\"Datos públicos actualmente no disponibles\",\"Session chat search\":\"Búsqueda de chat de sesión\",\"Search this session only…\":\"Buscar solo en esta sesión...\",\"New messages\":\"Nuevos mensajes\",\"Time for a short break.\":\"Es hora de un breve descanso.\",\"Your RoomGrid time budget for today has been reached.\":\"Se ha alcanzado tu presupuesto de tiempo de RoomGrid para hoy.\",\"RoomGrid action messages\":\"Mensajes de acción de RoomGrid\",\"This change is saved automatically.\":\"Este cambio se guarda automáticamente.\",\"Undo\":\"Deshacer\",\"Change undone\":\"Cambio deshecho\",\"Undo was not applied because newer changes already exist.\":\"No se aplicó Deshacer porque ya existen cambios más recientes.\",\"Chaturbate live catalog\":\"Chaturbate catálogo en vivo\",\"RoomGrid Discover\":\"RoomGrid Descubrir\",\"Chaturbate Discover page\":\"Página de descubrimiento de Chaturbate\",\"Following list\":\"siguiente lista\",\"My Collection\":\"Mi colección\",\"Chaturbate page\":\"página de chaturbate\",\"Local record\":\"Registro local\",\"Personally organized\":\"Organizado personalmente\",\"Local watch history\":\"Historial de reproducciones locales\",\"Public profile\":\"Perfil publico\",\"Local quality measurements\":\"Mediciones de calidad locales\",\"Local model index\":\"Índice de modelo local\",\"Current live signal\":\"Señal actual en vivo\",\"Within the last hour\":\"En la última hora\",\"Within the last 24 hours\":\"En las últimas 24 horas\",\"Within the last 30 days\":\"En los últimos 30 días\",\"Historical record\":\"Registro histórico\",\"Time unknown\":\"Hora desconocida\",\"All\":\"Todos\",\"Favorites\":\"Favoritos\",\"Library\":\"Biblioteca\",\"My Collection contained no reliably recognizable entries\":\"Mi colección no contenía entradas reconocibles de forma fiable\",\"No group filter active · all models are shown.\":\"No hay ningún filtro de grupo activo · Se muestran todos los modelos.\",\"Clear group filter\":\"Borrar filtro de grupo\",\"Filter homepage by groups\":\"Filtrar página de inicio por grupos\",\"Close group menu\":\"Cerrar menú de grupo\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Elija uno o más de sus propios grupos. Otras tarjetas de modelos se ocultan inmediatamente en la página de inicio de Chaturbate.\",\"Show all\":\"Mostrar todo\",\"Models in this group\":\"Modelos en este grupo\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Aún no existe ningún grupo personalizado. Cree uno en la biblioteca de modelos y asigne modelos allí.\",\"In any group\":\"en cualquier grupo\",\"In all groups\":\"en todos los grupos\",\"Manage groups\":\"Administrar grupos\",\"Create group\":\"Crear grupo\",\"Custom groups for the homepage\":\"Grupos personalizados para la página de inicio.\",\"Main views\":\"Vistas principales\",\"MultiCam workstation\":\"estación de trabajo multicámara\",\"Camera grid, audio and layouts\":\"Cuadrícula de cámara, audio y diseños.\",\"Discover\":\"Descubrir\",\"Full catalog, filters and sorting\":\"Catálogo completo, filtros y clasificación.\",\"Personal dashboard\":\"Panel personal\",\"Favorites, activity and shortcuts\":\"Favoritos, actividad y atajos\",\"Tag explorer\":\"Explorador de etiquetas\",\"Combinations, trends and related tags\":\"Combinaciones, tendencias y etiquetas relacionadas.\",\"Library and statistics\":\"Biblioteca y estadísticas\",\"Statistics Center\":\"Centro de estadísticas\",\"Model comparison and local statistics\":\"Comparación de modelos y estadísticas locales.\",\"Model library\":\"Biblioteca de modelos\",\"Profiles, tags, groups and notes\":\"Perfiles, etiquetas, grupos y notas.\",\"My user\":\"Mi usuario\",\"Preferences, usage and account data\":\"Preferencias, uso y datos de la cuenta\",\"Analytics Studio\":\"Estudio de análisis\",\"Ranges, heatmaps and exports\":\"Rangos, mapas de calor y exportaciones.\",\"Settings and maintenance\":\"Configuración y mantenimiento\",\"Customize homepage\":\"Personalizar inicio\",\"Cards, filters and visible details\":\"Tarjetas, filtros y detalles visibles\",\"Appearance\":\"Apariencia\",\"Colors, density and surfaces\":\"Colores, densidad y superficies.\",\"Workspaces and groups\":\"Espacios de trabajo y grupos\",\"Layouts, templates and assignments\":\"Diseños, plantillas y asignaciones.\",\"Data and backup\":\"Datos y respaldo\",\"Import, export and recovery\":\"Importación, exportación y recuperación\",\"Windows and performance\":\"Windows y rendimiento\",\"Resource profiles and preview limits\":\"Perfiles de recursos y límites de vista previa\",\"Privacy\":\"Privacidad\",\"History, private session and local data\":\"Historial, sesión privada y datos locales.\",\"Diagnostics\":\"Diagnóstico\",\"Self-check, routes and recovery\":\"Autocomprobación, rutas y recuperación\",\"RoomGrid main menu\":\"Menú principal de RoomGrid\",\"All pages, settings and tools in one place\":\"Todas las páginas, configuraciones y herramientas en un solo lugar\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"El menú permanece dentro de la ventana visible; Todos los datos de RoomGrid permanecen locales.\",\"RoomGrid pages\":\"Páginas de RoomGrid\",\"Statistics\":\"Estadísticas\",\"Customize original homepage\":\"Personalizar la página de inicio original\",\"Homepage\":\"Página de inicio\",\"Filter homepage by your own groups\":\"Filtra la página de inicio por tus propios grupos\",\"Groups\":\"Grupos\",\"Activity trend\":\"Tendencia de actividad\",\"Link\":\"Enlace\",\"Public profile text and links\":\"Texto y enlaces del perfil público.\",\"Last known public preview\":\"Última vista previa pública conocida\",\"Current public preview\":\"Vista previa pública actual\",\"Offline may only provide a placeholder\":\"Sin conexión solo puede proporcionar un marcador de posición\",\"Example images and covers\":\"Imágenes de ejemplo y portadas\",\"Reading local image archive…\":\"Leyendo el archivo de imágenes local…\",\"Save new snapshot\":\"Guardar nueva instantánea\",\"Saving public previews…\":\"Guardando vistas previas públicas...\",\"No saved image yet; current public previews are still shown.\":\"Aún no hay imagen guardada; Las vistas previas públicas actuales todavía se muestran.\",\"Current public cover\":\"Cobertura pública actual\",\"Current compact preview\":\"Vista previa compacta actual\",\"Current live image\":\"Imagen actual en vivo\",\"Room\":\"Sala\",\"More\":\"Más\",\"Unwatch\":\"Dejar de mirar\",\"Data sources and freshness\":\"Fuentes de datos y frescura\",\"Data source\":\"fuente de datos\",\"Full local data set\":\"Conjunto de datos locales completo\",\"Matching filters\":\"Filtros coincidentes\",\"Shown on this page\":\"Mostrado en esta página\",\"Latest live signals\":\"Últimas señales en vivo\",\"With watch history\":\"Con historial de reproducciones\",\"Public-profile coverage\":\"Cobertura de perfil público\",\"Country coverage\":\"Cobertura de países\",\"Age coverage\":\"Cobertura de edad\",\"Tag coverage\":\"Cobertura de etiquetas\",\"Which data set is being searched?\":\"¿Qué conjunto de datos se está buscando?\",\"Last local observation\":\"Última observación local\",\"No observation time yet\":\"Aún no hay tiempo de observación\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Público = de las páginas actuales de Chaturbate o del catálogo en vivo. Personal = guardado, etiquetado o agrupado por usted. Local = sesiones, vistas previas o valores de calidad observados solo en este dispositivo. Los campos desconocidos siguen siendo desconocidos y nunca se adivinan.\",\"Local model statistics\":\"Estadísticas del modelo local\",\"Automatically recorded · personally organized\":\"Grabado automáticamente · organizado personalmente\",\"Automatically recorded in the growing model catalog\":\"Registrado automáticamente en el creciente catálogo de modelos.\",\"Total\":\"totales\",\"Avg. session\":\"Sesión media\",\"Active days\":\"Días activos\",\"Share\":\"Compartir\",\"Longest session\":\"Sesión más larga\",\"Previews\":\"Vistas previas\",\"Quick views\":\"Vistas rápidas\",\"Data sources\":\"Fuentes de datos\",\"Edit in library\":\"Editar en biblioteca\",\"No experience data yet\":\"Aún no hay datos de experiencia\",\"Online observations will be collected locally from now on.\":\"Las observaciones en línea se recopilarán localmente a partir de ahora.\",\"Open\":\"Abierto\",\"Good trend\":\"buena tendencia\",\"Moderate trend\":\"Tendencia moderada\",\"Initial trend\":\"Tendencia inicial\",\"First observation\":\"Primera observación\",\"Estimated from watch time\":\"Estimado a partir del tiempo de reproducción\",\"weekday open\":\"abierto entre semana\",\"time open\":\"tiempo abierto\",\"Preview image\":\"Imagen de vista previa\",\"Biography / room subject\":\"Biografía / tema de la sala\",\"Catalog state\":\"Estado del catálogo\",\"Live status\":\"Estado en vivo\",\"Viewer count\":\"Número de espectadores\",\"Country/region\":\"País/región\",\"Total watch time\":\"Tiempo total de visualización\",\"Average session\":\"Sesión media\",\"First seen\":\"Visto por primera vez\",\"Last seen\":\"Visto por última vez\",\"Last live\":\"Último directo\",\"Approximate online times\":\"Horarios aproximados en línea\",\"Activity, sources and profile state\":\"Actividad, fuentes y estado del perfil\",\"Example images\":\"Imágenes de ejemplo\",\"Own groups\":\"Grupos propios\",\"Public/recognized tags\":\"Etiquetas públicas/reconocidas\",\"Notes, own tags and priority\":\"Notas, etiquetas propias y prioridad\",\"Local rating\":\"Valoración local\",\"Group assignment\":\"Asignación de grupos\",\"Room and preview actions\":\"Acciones de sala y vista previa\",\"Library display\":\"Visualización de la biblioteca\",\"Saved in this browser\":\"Guardado en este navegador\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Elige el tamaño de tarjeta, los modelos por fila, la cantidad por página y exactamente qué detalles muestra cada tarjeta.\",\"Compact overview\":\"Vista compacta\",\"Balanced\":\"Equilibrado\",\"All details\":\"Todos los detalles\",\"Show all details\":\"Mostrar todos los detalles\",\"Core details only\":\"Solo información básica\",\"Card zoom\":\"Ampliación de tarjetas\",\"Visible information per model\":\"Información visible por modelo\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Con columnas automáticas, la ampliación determina cuántos modelos caben uno junto a otro. Un número fijo de columnas tiene prioridad. Todas las opciones quedan guardadas en este navegador.\",\"No custom tags yet\":\"Aún no hay etiquetas personalizadas\",\"Name of the new group\":\"Nombre del nuevo grupo\",\"Models remain in the library.\":\"Los modelos permanecen en la biblioteca.\",\"Custom groups\":\"Grupos personalizados\",\"Create new group\":\"Crear nuevo grupo\",\"Nothing selected — show all\":\"No hay nada seleccionado: mostrar todo\",\"Any\":\"Cualquiera\",\"Women\":\"mujeres\",\"Men\":\"hombres\",\"Couples\":\"parejas\",\"Unknown\":\"Desconocido\",\"Error/loading\":\"Error/cargando\",\"Every weekday\":\"todos los dias de la semana\",\"Sunday\":\"domingo\",\"Monday\":\"lunes\",\"Tuesday\":\"martes\",\"Wednesday\":\"miércoles\",\"Thursday\":\"jueves\",\"Friday\":\"viernes\",\"Saturday\":\"sábado\",\"Every time\":\"cada vez\",\"Advanced filters and sorting\":\"Filtros y clasificación avanzados\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combina tantos filtros como quieras. Múltiples géneros, estados, países y etiquetas funcionan como selecciones múltiples. Cada configuración permanece guardada en este navegador.\",\"Gender/type\":\"Género/tipo\",\"Countries/regions (multiple)\":\"Países/regiones (múltiples)\",\"Add country…\":\"Añadir país…\",\"Public tags (multiple)\":\"Etiquetas públicas (múltiples)\",\"Add tag…\":\"Añadir etiqueta…\",\"Public-tag matching\":\"Coincidencia de etiquetas públicas\",\"At least one tag\":\"Al menos una etiqueta\",\"Every selected tag\":\"Cada etiqueta seleccionada\",\"Catalog source\":\"fuente del catálogo\",\"All models\":\"Todos los modelos\",\"Personally organized only\":\"Solo organizado personalmente\",\"Automatically discovered only\":\"Solo descubierto automáticamente\",\"Profile data\":\"Datos de perfil\",\"Loaded or missing\":\"Cargado o faltante\",\"Loaded only\":\"Solo cargado\",\"Missing only\":\"Sólo falta\",\"Picture/category\":\"Imagen/categoría\",\"Neither HD nor gaming\":\"Ni HD ni juegos\",\"Watch history\":\"Ver historial\",\"Never watched\":\"Nunca vi\",\"Own tags\":\"Etiquetas propias\",\"With or without\":\"Con o sin\",\"With tags only\":\"Solo con etiquetas\",\"Without tags only\":\"Solo sin etiquetas\",\"At least one group\":\"Al menos un grupo\",\"No group\":\"Ningún grupo\",\"With notes only\":\"solo con notas\",\"Without notes only\":\"Solo sin notas\",\"Numeric ranges and minimums\":\"Rangos numéricos y mínimos\",\"Minimum age\":\"Edad mínima\",\"Maximum age\":\"Edad máxima\",\"Minimum viewers\":\"Mínimo de espectadores\",\"Maximum viewers\":\"Máximo de espectadores\",\"Minimum followers\":\"Seguidores mínimos\",\"Maximum followers\":\"Seguidores máximos\",\"Minimum total watch time\":\"Tiempo total mínimo de visualización\",\"Currently online for at least\":\"Actualmente en línea desde hace al menos\",\"Minimum sessions\":\"Sesiones mínimas\",\"Minimum rating\":\"Calificación mínima\",\"Minimum priority\":\"Prioridad mínima\",\"Active within\":\"Activo dentro\",\"days\":\"dias\",\"Minimum appearances\":\"Apariciones mínimas\",\"Minimum live observations\":\"Observaciones mínimas en vivo\",\"Filter by typical online time\":\"Filtrar por tiempo típico en línea\",\"Weekday\":\"Día laborable\",\"Hour (local time)\":\"Hora (hora local)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Los campos cero o vacíos significan que no hay límite. Las edades desconocidas se excluyen mientras esté activo un límite de edad. Los filtros de tiempo utilizan los tiempos en vivo observados en este dispositivo y recurren con cautela a los tiempos de visualización cuando es necesario.\",\"Reset all library filters\":\"Restablecer todos los filtros de la biblioteca\",\"Open live room\":\"Sala abierta en vivo\",\"Open room page\":\"Abrir página de sala\",\"Status\":\"Estado\",\"Open model room in a new tab\":\"Abrir sala modelo en una nueva pestaña\",\"Open the Chaturbate single-model view in a new tab\":\"Abra la vista de modelo único de Chaturbate en una nueva pestaña\",\"Save profile\":\"Guardar perfil\",\"Restore\":\"Restaurar\",\"Archive\":\"Archivo\",\"Advanced statistics filters\":\"Filtros de estadísticas avanzadas\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Todos los filtros se pueden combinar; Los países, etiquetas, grupos y tipos admiten selección múltiple. Las opciones se guardan permanentemente.\",\"Own groups (multiple)\":\"Grupos propios (múltiples)\",\"Add group…\":\"Añadir grupo…\",\"Tag matching\":\"Coincidencia de etiquetas\",\"At least one\":\"Al menos uno\",\"All selected tags\":\"Todas las etiquetas seleccionadas\",\"Group matching\":\"Coincidencia de grupo\",\"All selected groups\":\"Todos los grupos seleccionados\",\"Cover/example image\":\"Imagen de portada/ejemplo\",\"Known public image available\":\"Imagen pública conocida disponible\",\"No known image\":\"Ninguna imagen conocida\",\"Currently live\":\"Actualmente vivo\",\"Maximum total watch time\":\"Tiempo total máximo de reproducción\",\"Maximum sessions\":\"Sesiones máximas\",\"Minimum active days\":\"Días activos mínimos\",\"Minimum previews\":\"Vistas previas mínimas\",\"Minimum quick views\":\"Vistas rápidas mínimas\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Los campos cero o vacíos significan que no hay límite. Los valores desconocidos nunca se adivinan; los rangos de edad excluyen edades desconocidas.\",\"Reset all statistics filters\":\"Restablecer todos los filtros de estadísticas\",\"Search name, tags or notes…\":\"Buscar nombre, etiquetas o notas…\",\"Search\":\"Buscar\",\"Sorting\":\"Ordenar\",\"Most watch time\":\"Mayor tiempo de visualización\",\"Least watch time\":\"Menos tiempo de visualización\",\"Recently active\":\"Activo recientemente\",\"Longest inactive\":\"Inactivo más largo\",\"Most sessions\":\"La mayoría de las sesiones\",\"Fewest sessions\":\"Menos sesiones\",\"Longest average session\":\"Sesión promedio más larga\",\"Shortest average session\":\"Sesión promedio más corta\",\"Longest single session\":\"Sesión única más larga\",\"Most active days\":\"Días más activos\",\"Highest watch share\":\"Mayor porcentaje de visualización\",\"Most previews\":\"La mayoría de las vistas previas\",\"Most quick views\":\"Vistas más rápidas\",\"Most discoveries\":\"La mayoría de los descubrimientos\",\"Most followers\":\"La mayoría de los seguidores\",\"Youngest first\":\"el más joven primero\",\"Oldest first\":\"El más viejo primero\",\"Highest rating\":\"Calificación más alta\",\"Highest priority\":\"Máxima prioridad\",\"Most appearances\":\"La mayoría de las apariciones\",\"Most live observations\":\"La mayoría de las observaciones en vivo.\",\"First observed\":\"observado por primera vez\",\"Country A–Z\":\"País A–Z\",\"Most public tags\":\"La mayoría de las etiquetas públicas\",\"Most groups\":\"La mayoría de los grupos\",\"Best technical quality\":\"La mejor calidad técnica\",\"Cover/example images first\":\"Imágenes de portada/ejemplo primero\",\"All statuses\":\"Todos los estados\",\"Live only\":\"vivir solo\",\"Library only\":\"Sólo biblioteca\",\"View\":\"Vista\",\"Active library\":\"Biblioteca activa\",\"Live now\":\"vivir ahora\",\"With notes\":\"con notas\",\"Newest saved/discovered\":\"Lo más nuevo guardado/descubierto\",\"Oldest saved/discovered\":\"Más antiguo guardado/descubierto\",\"Lowest rating\":\"Calificación más baja\",\"Lowest priority\":\"Prioridad más baja\",\"Live longest now\":\"Vive más tiempo ahora\",\"Age youngest first\":\"Edad más joven primero\",\"Age oldest first\":\"Edad mayor primero\",\"Most recently live\":\"En vivo más recientemente\",\"First discovered\":\"descubierto por primera vez\",\"Most own groups\":\"La mayoría de los grupos propios\",\"Card size\":\"Tamaño de tarjeta\",\"Compact\":\"Compacto\",\"Comfortable\":\"Cómodo\",\"Large\":\"Grande\",\"Cinema\":\"Cine\",\"Extra large\":\"Extra grande\",\"Models per row\":\"Modelos por fila\",\"Automatic for window\":\"Automático según la ventana\",\"Recorded models\":\"Modelos grabados\",\"Total cam time\":\"Tiempo total de cámara\",\"across all models\":\"en todos los modelos\",\"Dock + hover\":\"Acoplar + flotar\",\"Last 30 days\":\"últimos 30 días\",\"local events\":\"eventos locales\",\"Top model\":\"modelo superior\",\"no watch time yet\":\"aún no hay tiempo de visualización\",\"Deeper analysis\":\"Análisis más profundo\",\"Open Analytics Studio\":\"Abrir estudio de análisis\",\"Late night\":\"Tarde en la noche\",\"Morning\":\"mañana\",\"Daytime\":\"diurno\",\"Evening\":\"tarde\",\"Gaming\":\"Juegos\",\"Music & DJ\":\"Música y DJ\",\"Dance\":\"danza\",\"Cosplay & anime\":\"Cosplay y anime\",\"Fitness & sports\":\"Fitness y deportes\",\"Talk & social\":\"Charla y redes sociales\",\"Outdoor & travel\":\"Al aire libre y viajes\",\"Roleplay\":\"Juego de roles\",\"Performance\":\"Rendimiento\",\"My interest statistics\":\"Mis estadísticas de intereses\",\"All local data\":\"Todos los datos locales\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Calculado a partir del tiempo de visualización local real, sesiones, vistas previas, vistas rápidas, favoritos e información del modelo público actualmente conocido.\",\"Favorite tag\":\"Etiqueta favorita\",\"not enough data\":\"no hay suficientes datos\",\"Favorite genre\":\"Género favorito\",\"from public tags\":\"de etiquetas públicas\",\"Most-watched age\":\"Edad más vista\",\"no age data\":\"sin datos de edad\",\"Most-watched country\":\"País más visto\",\"no country data\":\"sin datos del país\",\"Favorite model type\":\"Tipo de modelo preferido\",\"no type data\":\"ningún tipo de datos\",\"Favorite viewing time\":\"Horario de visualización preferido\",\"no time data\":\"sin datos de tiempo\",\"Top-three concentration\":\"Concentración entre los tres primeros\",\"watch time on the top three models\":\"tiempo de visualización en los tres mejores modelos\",\"Favorite affinity\":\"afinidad favorita\",\"watch time on current favorites\":\"tiempo de visualización en favoritos actuales\",\"watch time on HD models\":\"tiempo de visualización en modelos HD\",\"Metadata coverage\":\"Cobertura de metadatos\",\"age\":\"edad\",\"country\":\"país\",\"Tags, genres and browsing interests\":\"Etiquetas, géneros e intereses de exploración\",\"Tags by watch time\":\"Etiquetas por tiempo de visualización\",\"Your ranking appears when watched models have public tags.\":\"Tu clasificación aparece cuando los modelos vistos tienen etiquetas públicas.\",\"Favorite genre worlds\":\"Mundos de género favoritos\",\"No genre world can yet be assigned from public tags.\":\"Aún no se puede asignar ningún mundo de género desde etiquetas públicas.\",\"Your own tags\":\"Tus propias etiquetas\",\"Your saved custom tags are also evaluated here by watch time.\":\"Sus etiquetas personalizadas guardadas también se evalúan aquí según el tiempo de reproducción.\",\"Preview-interest tags\":\"Etiquetas de interés de vista previa\",\"Browsing interests appear when you use live previews or quick views.\":\"Los intereses de navegación aparecen cuando utiliza vistas previas en vivo o vistas rápidas.\",\"Most-previewed models\":\"Modelos más vistos\",\"No previews have been recorded in this period yet.\":\"Aún no se han registrado avances en este período.\",\"Age, origin and model type\":\"Edad, origen y tipo de modelo\",\"Age groups by watch time\":\"Grupos de edad por tiempo de visualización\",\"Not enough public age data is available.\":\"No hay suficientes datos públicos sobre la edad disponibles.\",\"Countries by watch time\":\"Países por tiempo de visualización\",\"Not enough public country data is available.\":\"No hay suficientes datos públicos del país disponibles.\",\"Most-watched places\":\"Lugares más vistos\",\"Not enough public location data is available.\":\"No hay suficientes datos de ubicación pública disponibles.\",\"Model types by watch time\":\"Tipos de modelos por tiempo de reproducción\",\"Not enough public model-type data is available.\":\"No hay suficientes datos públicos sobre tipos de modelos disponibles.\",\"Viewing rhythm and favorite signals\":\"Ritmo de visualización y señales de favoritos\",\"Most active weekdays\":\"Días laborables más activos\",\"Not enough visit events are available.\":\"No hay suficientes eventos de visita disponibles.\",\"Times of day\":\"Horas del día\",\"Tags among favorites\":\"Etiquetas entre favoritos\",\"Preference signals appear when you mark models as favorites.\":\"Las señales de preferencia aparecen cuando marca modelos como favoritos.\",\"Countries among favorites\":\"Países entre favoritos\",\"No known countries among favorites yet.\":\"Aún no se conocen países entre los favoritos.\",\"Age groups among favorites\":\"Grupos de edad entre los favoritos\",\"No known ages among favorites yet.\":\"Aún no se conocen edades entre los favoritos.\",\"Model types among favorites\":\"Tipos de modelos entre los favoritos\",\"No known model types among favorites yet.\":\"Aún no se conocen tipos de modelos entre los favoritos.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Estos resultados no se envían a un servidor. Sólo el tiempo de visualización real cuenta para las preferencias de visualización; Las vistas previas y los favoritos se muestran por separado. Los datos faltantes de edad, país, ubicación o etiqueta nunca se adivinan.\",\"Open Following on Chaturbate\":\"Abrir seguimiento en Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"No se puede recuperar un historial completo de visitas o tiempo de visualización de forma retroactiva porque Chaturbate no proporciona una API de historial personal documentada. Puede solicitar una copia de sus datos personales almacenados a través de la exportación de datos oficial de Chaturbate. Si esa exportación contiene campos de historial utilizables, RoomGrid puede admitir una importación segura después de validar el formato del archivo. La API de estadísticas oficial proporciona valores actuales de la emisora; RoomGrid puede construir su propio historial sólo desde la primera conexión en adelante.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Compare todos los modelos observados, guardados y vistos localmente, con tiempo de visualización, sesiones, actividad, vistas previas, calidad y clasificaciones.\",\"Large model library\":\"Gran biblioteca de modelos\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Sus modelos como perfiles convenientes y ricos en imágenes con estado en vivo, notas, etiquetas, calificaciones, prioridad, grupos y acciones directas.\",\"My user dashboard\":\"Mi panel de usuario\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Su historial de uso local privado, hábitos, datos de la biblioteca y, solo cuando se conecta deliberadamente, datos de la cuenta oficial de la emisora.\",\"🔒 Local & private on this device\":\"🔒 Local y privado en este dispositivo\",\"Preparing data…\":\"Preparando datos…\",\"RoomGrid Comfort Center\":\"Centro de confort RoomGrid\",\"inside the standard Chaturbate page\":\"dentro de la página estándar de Chaturbate\",\"All RoomGrid pages and settings\":\"Todas las páginas y configuraciones de RoomGrid\",\"Media paused; names and images are covered.\":\"Medios en pausa; nombres e imágenes están cubiertos.\",\"Restore previous view\":\"Restaurar vista anterior\",\"Enter local PIN\":\"Ingrese el PIN local\",\"In-page notifications\":\"Notificaciones en la página\",\"No new notifications\":\"No hay notificaciones nuevas\",\"Notifications are off\":\"Las notificaciones están desactivadas\",\"RoomGrid notification\":\"Notificación de RoomGrid\",\"Dismiss as read\":\"Descartar como leído\",\"More unread notifications\":\"Más notificaciones no leídas\",\"RoomGrid update\":\"Actualización de RoomGrid\",\"A monitored source changed status.\":\"Una fuente monitoreada cambió de estado.\",\"This is a local RoomGrid test notification.\":\"Esta es una notificación de prueba local de RoomGrid.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Prueba exitosa: las notificaciones pueden aparecer directamente en la barra RoomGrid.\",\"Settings\":\"Configuración\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Espacios de trabajo, biblioteca, diseño, datos, historial, notificaciones, privacidad y rendimiento\",\"Workspaces\":\"Espacios de trabajo\",\"Data\":\"Datos\",\"History & statistics\":\"Historia y estadísticas\",\"Broadcaster stats\":\"Estadísticas de la emisora\",\"Notifications\":\"Notificaciones\",\"Windows & performance\":\"Windows y rendimiento\",\"Optional tools\":\"Herramientas opcionales\",\"Settings pages\":\"Páginas de configuración\",\"RoomGrid control center\":\"Centro de control RoomGrid\",\"Any online model\":\"Cualquier modelo en línea\",\"Workspace name\":\"Nombre del espacio de trabajo\",\"Optional description\":\"Descripción opcional\",\"Active\":\"Activo\",\"No description\":\"Sin descripción\",\"Name\":\"Nombre\",\"Description\":\"Descripción\",\"Replace offline models\":\"Reemplazar modelos fuera de línea\",\"Replacement group\":\"Grupo de reemplazo\",\"Delay seconds\":\"Segundos de retraso\",\"Save current state\":\"Guardar estado actual\",\"New window\":\"Nueva ventana\",\"Export this workspace\":\"Exportar este espacio de trabajo\",\"Duplicate\":\"Duplicar\",\"Workspace restored\":\"Espacio de trabajo restaurado\",\"Workspace archived\":\"Espacio de trabajo archivado\",\"Delete permanently\":\"Eliminar permanentemente\",\"Permanently delete this workspace?\":\"¿Eliminar permanentemente este espacio de trabajo?\",\"Saved layout and panel sizes\":\"Diseño guardado y tamaños de panel\",\"Apply\":\"Aplicar\",\"Layout template deleted\":\"Plantilla de diseño eliminada\",\"Parent group\":\"grupo de padres\",\"None\":\"Ninguno\",\"Group description\":\"Descripción del grupo\",\"Save current workspace\":\"Guardar espacio de trabajo actual\",\"Save layout template\":\"Guardar plantilla de diseño\",\"Template name\":\"Nombre de la plantilla\",\"Layout templates\":\"Plantillas de diseño\",\"Device layout\":\"Diseño del dispositivo\",\"Groups and subgroups\":\"Grupos y subgrupos\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Cada espacio de trabajo almacena diseños compactos y de escritorio separados. El reemplazo automático está desactivado de forma predeterminada y solo llena los espacios sin conexión después de habilitarlo para ese espacio de trabajo.\",\"No templates yet.\":\"Aún no hay plantillas.\",\"Group name\":\"Nombre del grupo\",\"New subgroup\":\"Nuevo subgrupo\",\"Enter parent group name\":\"Ingrese el nombre del grupo principal\",\"Subgroup name\":\"Nombre del subgrupo\",\"Name, note or local tag\":\"Nombre, nota o etiqueta local\",\"Choose group…\":\"Elige grupo…\",\"Add selected models to group\":\"Agregar modelos seleccionados al grupo\",\"Watch later\":\"Ver más tarde\",\"Paused\":\"En pausa\",\"Collection name\":\"Nombre de la colección\",\"Tags, comma separated\":\"Etiquetas, separadas por comas\",\"Countries, comma separated\":\"Países, separados por comas\",\"All types\":\"Todos los tipos\",\"Rule name\":\"Nombre de la regla\",\"Required tags\":\"Etiquetas obligatorias\",\"Local alert\":\"Alerta local\",\"Add to favorites\":\"Añadir a favoritos\",\"Add to group\":\"Agregar al grupo\",\"Any tag\":\"Cualquier etiqueta\",\"Any country\":\"cualquier pais\",\"Pause\":\"Pausa\",\"Enable\":\"Habilitar\",\"Delete\":\"Eliminar\",\"Automation rule deleted\":\"Regla de automatización eliminada\",\"Select model\":\"Seleccionar modelo\",\"Later\":\"Más tarde\",\"Never visited\":\"Nunca visitado\",\"Last online\":\"Último en línea\",\"Last visited\":\"Última visita\",\"Local note\":\"nota local\",\"Local tags, comma separated\":\"Etiquetas locales, separadas por comas\",\"Remove later\":\"Quitar más tarde\",\"Hide today\":\"Ocultar hoy\",\"Pause 7 days\":\"Pausa 7 días\",\"Never suggest\":\"Nunca sugiera\",\"Select results\":\"Seleccionar resultados\",\"Clear selection\":\"Borrar selección\",\"selected\":\"seleccionado\",\"Add to watch later\":\"Añadir para verlo más tarde\",\"Permanently delete selected models?\":\"¿Eliminar permanentemente los modelos seleccionados?\",\"Smart collections\":\"Colecciones inteligentes\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Las colecciones se actualizan automáticamente a medida que crecen los metadatos y las estadísticas locales; Los modelos no están duplicados.\",\"Tags\":\"Etiquetas\",\"Countries\":\"Países\",\"Favorites only\":\"Sólo favoritos\",\"Online only\":\"Sólo en línea\",\"Save smart collection\":\"Guardar colección inteligente\",\"Delete current collection\":\"Eliminar colección actual\",\"Delete this smart collection? Model data stays unchanged.\":\"¿Eliminar esta colección inteligente? Los datos del modelo permanecen sin cambios.\",\"Rules & automation center\":\"Centro de reglas y automatización\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Cuando los datos del modelo local coincidan, muestre una alerta, agregue un favorito o agregue el modelo a un grupo elegido.\",\"Action\":\"acción\",\"Target group\":\"Grupo objetivo\",\"Save rule\":\"Guardar regla\",\"No automation rules yet.\":\"Aún no hay reglas de automatización.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Los nombres de usuario se deduplican mediante una coincidencia normalizada exacta. La búsqueda cubre nombres, notas y etiquetas locales. Archivar, eliminar de una vista y eliminación permanente siguen siendo acciones independientes.\",\"No matching models.\":\"No hay modelos coincidentes.\",\"export\":\"exportar\",\"Models and notes\":\"Modelos y notas\",\"This backup could not be read. Current data was not changed.\":\"Esta copia de seguridad no se pudo leer. Los datos actuales no fueron modificados.\",\"Import preview (nothing changed yet)\":\"Vista previa de importación (nada ha cambiado todavía)\",\"conflicts\":\"conflictos\",\"Includes sensitive statistics\":\"Incluye estadísticas sensibles.\",\"models\":\"modelos\",\"events\":\"eventos\",\"quality samples\":\"muestras de calidad\",\"Import statistics too\":\"Importar estadísticas también\",\"Import method\":\"Método de importación\",\"Merge (recommended)\":\"Fusionar (recomendado)\",\"Replace selected sections\":\"Reemplazar secciones seleccionadas\",\"Apply import\":\"Aplicar importación\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Importación completa. La configuración anterior y las estadísticas se respaldaron por separado.\",\"The import could not be saved safely. The backup remains available.\":\"La importación no se pudo guardar de forma segura. La copia de seguridad sigue estando disponible.\",\"Cancel preview\":\"Cancelar vista previa\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Este archivo contiene datos de modelo, nota o espacio de trabajo y debe tratarse como confidencial.\",\"This import is not marked sensitive.\":\"Esta importación no está marcada como confidencial.\",\"Data version\":\"Versión de datos\",\"Software version\":\"Versión de software\",\"Revision\":\"Revisión\",\"Local models\":\"Modelos locales\",\"Selective export\":\"Exportación selectiva\",\"Export selected\":\"Exportar seleccionado\",\"Full export\":\"Exportación completa\",\"Complete safety package\":\"Paquete de seguridad completo\",\"Export statistics separately\":\"Exportar estadísticas por separado\",\"Choose file and preview\":\"Elija el archivo y obtenga una vista previa\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"El paquete de seguridad completo contiene configuración y estadísticas confidenciales, pero nunca el token de estadísticas oficial. Se obtiene una vista previa de cada importación y se realiza una copia de seguridad de la configuración y las estadísticas por separado antes de realizar la aplicación.\",\"Storage and backup center\":\"Centro de almacenamiento y respaldo\",\"Local storage\":\"Almacenamiento local\",\"Site storage\":\"Almacenamiento del sitio\",\"Available quota\":\"Cuota disponible\",\"Internal statistics backups\":\"Copias de seguridad de estadísticas internas\",\"Configuration\":\"Configuración\",\"workspaces\":\"espacios de trabajo\",\"LocalStorage + update vault\":\"Almacenamiento local + bóveda de actualización\",\"Statistics database\":\"Base de datos estadística\",\"Secrets\":\"Secretos\",\"The official statistics token is never included in normal or complete backups.\":\"El token de estadísticas oficial nunca se incluye en las copias de seguridad normales o completas.\",\"Excluded\":\"excluido\",\"Create configuration restore point\":\"Crear punto de restauración de configuración\",\"Create statistics restore point\":\"Crear punto de restauración de estadísticas\",\"Legacy cleanup\":\"Limpieza heredada\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Los datos heredados nunca se eliminan automáticamente. Elimine las claves antiguas v6/v7 solo después de una revisión exitosa.\",\"Remove old v6/v7 copies\":\"Eliminar copias antiguas de v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"¿Eliminar sólo las copias antiguas de v6/v7? Los datos actuales y las copias de seguridad permanecen.\",\"items removed\":\"elementos eliminados\",\"Aurora\":\"aurora\",\"Blue and teal, the RoomGrid signature\":\"Azul y verde azulado, la firma de RoomGrid\",\"Ember\":\"ascua\",\"Warm orange and rose\":\"Naranja cálido y rosa\",\"Frost\":\"Escarcha\",\"Crisp cyan and violet\":\"Cian y violeta nítidos\",\"Custom\":\"personalizado\",\"Use the colors selected below\":\"Utilice los colores seleccionados a continuación\",\"Name the current appearance\":\"Nombra la apariencia actual\",\"My appearance\":\"mi apariencia\",\"Delete appearance profile\":\"Eliminar perfil de apariencia\",\"This appearance profile could not be imported.\":\"Este perfil de apariencia no se pudo importar.\",\"Light\":\"Luz\",\"Dark\":\"oscuro\",\"Export\":\"Exportar\",\"Home\":\"Inicio\",\"Listings and tags\":\"Listados y etiquetas\",\"Model pages\":\"Páginas de modelo\",\"Use global appearance\":\"Usar apariencia global\",\"This logo format is unsafe or unsupported.\":\"Este formato de logotipo no es seguro o no es compatible.\",\"The logo file could not be read.\":\"No se pudo leer el archivo del logotipo.\",\"MultiCam tools\":\"Herramientas multicámara\",\"Page\":\"Página\",\"Panel\":\"panel\",\"Card surface\":\"Superficie de la tarjeta\",\"Input\":\"Entrada\",\"Border\":\"frontera\",\"Text\":\"Texto\",\"Muted text\":\"Texto silenciado\",\"Success\":\"Éxito\",\"Warning\":\"Advertencia\",\"Danger\":\"Peligro\",\"Info and focus\":\"Información y enfoque\",\"Data hub\":\"Centro de datos\",\"Text / page\":\"Texto/página\",\"Text / panel\":\"Texto/panel\",\"Muted text / panel\":\"Texto/panel silenciado\",\"Accent text / accent\":\"Texto acentuado/acento\",\"Desktop\":\"Escritorio\",\"Tablet\":\"tableta\",\"Mobile\":\"Móvil\",\"Brand and logo studio\":\"Estudio de marca y logotipo.\",\"Monogram\":\"Monograma\",\"Tagline\":\"Lema\",\"Brand display\":\"Visualización de la marca\",\"Full name\":\"nombre completo\",\"Uploaded logo\":\"Logotipo subido\",\"Logo shape\":\"Forma del logotipo\",\"Rounded\":\"redondeado\",\"Circle\":\"circulo\",\"Square\":\"cuadrado\",\"Pill\":\"pastilla\",\"Page title\":\"Título de la página\",\"Website original\":\"Original del sitio\",\"Brand plus original\":\"Marca más original\",\"Custom title\":\"Título personalizado\",\"Custom page title\":\"Título de página personalizado\",\"Image fit\":\"Ajuste de imagen\",\"Horizontal position\":\"posición horizontal\",\"Vertical position\":\"posición vertical\",\"Upload light-mode logo\":\"Subir logotipo en modo ligero\",\"Upload dark-mode logo\":\"Subir logotipo en modo oscuro\",\"Remove logos\":\"Quitar logotipos\",\"Show version\":\"Mostrar versión\",\"Use logo as tab icon on RoomGrid pages\":\"Utilice el logotipo como icono de pestaña en las páginas de RoomGrid\",\"Replace website logo in the original header (local only)\":\"Reemplazar el logotipo del sitio web en el encabezado original (solo local)\",\"Complete color system\":\"Sistema de color completo\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Controle los colores de página, panel, tarjeta, entrada, borde, texto y estado, además de los acentos.\",\"Derived automatically\":\"Derivado automáticamente\",\"Fully custom\":\"Totalmente personalizado\",\"Editing dark palette\":\"Editando paleta oscura\",\"Editing light palette\":\"Edición de paleta de luces\",\"Reset current palette\":\"Restablecer la paleta actual\",\"Layout editor\":\"editor de diseño\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Establezca el espaciado del contenido, el ancho de la tarjeta, el encabezado y la barra lateral de manera consistente en todas las páginas de RoomGrid.\",\"Minimum card width\":\"Ancho mínimo de tarjeta\",\"Section spacing\":\"Espaciado de secciones\",\"Content padding\":\"Relleno de contenido\",\"Header height\":\"altura del encabezado\",\"Maximum columns\":\"Columnas máximas\",\"Automatic\":\"Automático\",\"Card aspect\":\"Aspecto de la tarjeta\",\"Sidebar position\":\"Posición de la barra lateral\",\"Left\":\"Izquierda\",\"Right\":\"Derecha\",\"Keep header visible while scrolling\":\"Mantenga el encabezado visible mientras se desplaza\",\"Live device preview\":\"Vista previa del dispositivo en vivo\",\"Contrast and accessibility\":\"Contraste y accesibilidad\",\"High-contrast mode\":\"Modo de alto contraste\",\"Underline links\":\"Enlaces subrayados\",\"Reduce transparency\":\"Reducir la transparencia\",\"Boost base text size\":\"Aumentar el tamaño del texto base\",\"Focus ring width\":\"Ancho del anillo de enfoque\",\"Minimum target size\":\"Tamaño objetivo mínimo\",\"Respect system reduced-motion setting\":\"Respetar la configuración de movimiento reducido del sistema\",\"Apply accessible defaults\":\"Aplicar valores predeterminados accesibles\",\"Profiles by page and screen size\":\"Perfiles por página y tamaño de pantalla\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Guarde una o más apariencias y luego asígnelas por tipo de página para computadoras de escritorio, tabletas y dispositivos móviles. Los perfiles cambian automáticamente cuando cambia el tamaño de la pantalla.\",\"Enable responsive design profiles\":\"Habilitar perfiles de diseño responsivos\",\"Mobile maximum width\":\"Ancho máximo móvil\",\"Tablet maximum width\":\"Ancho máximo de la tableta\",\"RoomGrid design system\":\"Sistema de diseño RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Elija la dirección de la marca, el ancho del contenido y la densidad de la información de la tarjeta. Cada página sigue inmediatamente.\",\"Primary accent\":\"acento primario\",\"Secondary accent\":\"Acento secundario\",\"Content width\":\"Ancho del contenido\",\"Centered\":\"Centrado\",\"Wide\":\"ancho\",\"Full width\":\"Ancho completo\",\"Card style\":\"Estilo de tarjeta\",\"Visual\":\"visuales\",\"Data-rich\":\"Rico en datos\",\"Micro\":\"micrófono\",\"Widget density\":\"Densidad de widgets\",\"Spacious\":\"Espacioso\",\"Theme and surfaces\":\"Tema y superficies\",\"Surface style\":\"Estilo de superficie\",\"Soft\":\"suave\",\"Flat\":\"Piso\",\"Glass\":\"vidrio\",\"Page background\":\"Fondo de la página\",\"Solid\":\"Sólido\",\"Gradient\":\"gradiente\",\"Ambient glow\":\"Resplandor ambiental\",\"Surface opacity\":\"Opacidad de la superficie\",\"Custom accent color\":\"Color de acento personalizado\",\"Sizing and motion\":\"Dimensionamiento y movimiento\",\"Font style\":\"Estilo de fuente\",\"System\":\"Sistema\",\"Classic serif\":\"Serifa clásica\",\"Monospace\":\"monoespacio\",\"Density\":\"densidad\",\"Font scale\":\"Escala de fuente\",\"Control size\":\"Controlar el tamaño\",\"Corner radius\":\"Radio de esquina\",\"Card spacing\":\"Espaciado de tarjetas\",\"Shadow strength\":\"Fuerza de la sombra\",\"Sidebar width\":\"Ancho de la barra lateral\",\"Enable animations\":\"Habilitar animaciones\",\"Appearance profiles\":\"Perfiles de apariencia\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Guarde la apariencia completa como un perfil reutilizable. Los perfiles permanecen disponibles después de las actualizaciones del script.\",\"Save current appearance\":\"Guardar apariencia actual\",\"Import profile\":\"Importar perfil\",\"No profiles saved yet.\":\"Aún no se han guardado perfiles.\",\"Instant preview\":\"Vista previa instantánea\",\"Example card\":\"tarjeta de ejemplo\",\"This is the combined result of the current visual settings.\":\"Este es el resultado combinado de la configuración visual actual.\",\"Example action\":\"Acción de ejemplo\",\"Per-page design profiles\":\"Perfiles de diseño por página\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Elija una apariencia guardada diferente para Inicio, listados, páginas de modelos, MultiCam, Discover y el centro de datos.\",\"Enable per-page design\":\"Habilitar diseño por página\",\"Normal website pages\":\"Páginas web normales\",\"Enable RoomGrid page design\":\"Habilitar el diseño de página RoomGrid\",\"Tag and category pages\":\"Páginas de etiquetas y categorías\",\"Cards\":\"Tarjetas\",\"List\":\"Lista\",\"Page density\":\"Densidad de página\",\"Polish navigation\":\"navegación polaca\",\"Polish sidebars\":\"Barras laterales polacas\",\"Hide recognized promo and merch areas\":\"Ocultar áreas de promoción y merchandising reconocidas\",\"Compact footer\":\"Pie de página compacto\",\"Mark saved models\":\"Marcar modelos guardados\",\"Mark favorites separately\":\"Marcar favoritos por separado\",\"Mark visited models\":\"Mark visitó modelos\",\"Show card quick actions\":\"Mostrar acciones rápidas de tarjeta\",\"Animated live previews on home and listing pages\":\"Vistas previas animadas en vivo en las páginas de inicio y listado\",\"Preview refresh rate\":\"Vista previa de frecuencia de actualización\",\"Off\":\"Apagado\",\"Update mode\":\"Modo de actualización\",\"All visible cards simultaneously\":\"Todas las tarjetas visibles simultáneamente\",\"Performance-bounded batches\":\"Lotes limitados por el rendimiento\",\"Batch concurrency limit\":\"Límite de simultaneidad por lotes\",\"All visible\":\"Todo visible\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Sólo las tarjetas visibles reciben actualizaciones de cuadros JPEG silenciados; se detienen inmediatamente fuera de la pantalla o en una pestaña oculta. El modo sincronizado actualiza cada tarjeta visible en una sola ola; las velocidades altas aumentan notablemente el ancho de banda y el uso de la CPU.\",\"Live video on hover\":\"Video en vivo al pasar el mouse\",\"Play real-time video while hovering\":\"Reproduce videos en tiempo real mientras estás flotando\",\"Start delay\":\"Retraso de inicio\",\"Immediately\":\"Inmediatamente\",\"Hover enlargement\":\"Ampliación al pasar el cursor\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Sólo se reproduce la carta debajo del puntero, siempre silenciada. Alejar u ocultar la pestaña detiene la transmisión inmediatamente.\",\"Live Preview Dock\":\"Muelle de vista previa en vivo\",\"Enable low-load live previews\":\"Habilitar vistas previas en vivo de baja carga\",\"Concurrent previews\":\"Vistas previas simultáneas\",\"Window size\":\"Tamaño de ventana\",\"Small\":\"pequeño\",\"Medium\":\"Medio\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Las vistas previas comienzan silenciadas y se pausan en pestañas ocultas. El audio siempre requiere un clic explícito.\",\"Quick info & chat focus\":\"Información rápida y enfoque de chat\",\"Enable model Quick View\":\"Habilitar vista rápida del modelo\",\"Chat focus mode (opt-in)\":\"Modo de enfoque de chat (optar por participar)\",\"Show local timestamps\":\"Mostrar marcas de tiempo locales\",\"System messages\":\"Mensajes del sistema\",\"Show\":\"Mostrar\",\"Dim\":\"tenue\",\"Hide\":\"Ocultar\",\"Chat filter strength\":\"Fuerza del filtro de chat\",\"Low\":\"Bajo\",\"Strict\":\"estricto\",\"Highlight words (comma separated)\":\"Resaltar palabras (separadas por comas)\",\"Muted words (comma separated)\":\"Palabras silenciadas (separadas por comas)\",\"Collapse repeated messages\":\"Contraer mensajes repetidos\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Las mejoras del chat solo cambian la visualización actual. Nunca almacenan chats, envían mensajes ni dan propinas automáticamente.\",\"Shortcuts\":\"Atajos\",\"Command menu\":\"Menú de comando\",\"Global model search\":\"Búsqueda de modelo global\",\"Reset design defaults\":\"Restablecer los valores predeterminados de diseño\",\"Optional modules\":\"Módulos opcionales\",\"Normal-page dock and card actions\":\"Acciones de tarjeta y acoplamiento de página normal\",\"Real audio meters\":\"Medidores de audio reales\",\"Media, app and contest directories\":\"Directorios de medios, aplicaciones y concursos\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Los cambios de módulo surten pleno efecto después de recargar la página. Los módulos de datos centrales y de respaldo permanecen habilitados para evitar la pérdida de datos.\",\"Name the current filters and sorting\":\"Nombra los filtros y la clasificación actuales.\",\"My smart view\":\"mi vista inteligente\",\"Delete smart view\":\"Eliminar vista inteligente\",\"Smart view deleted\":\"Vista inteligente eliminada\",\"tags\":\"etiquetas\",\"countries\":\"países\",\"Saved selection\":\"Selección guardada\",\"Suggestions\":\"Sugerencias\",\"Selected\":\"Seleccionado\",\"Website tags\":\"Etiquetas de sitios web\",\"Countries (multiple selection)\":\"Países (selección múltiple)\",\"Select countries\":\"Seleccionar países\",\"Search countries…\":\"Buscar países…\",\"Recognized in current models\":\"Reconocido en los modelos actuales.\",\"All countries\":\"Todos los paises\",\"Tags (multiple selection)\":\"Etiquetas (selección múltiple)\",\"Select tags\":\"Seleccionar etiquetas\",\"Search tags…\":\"Buscar etiquetas…\",\"Room subject\":\"Tema de la sala\",\"Current online duration\":\"Tiempo en línea actual\",\"Local watch time and sessions\":\"Tiempo de visualización local y sesiones\",\"HD, new, gaming and favorite badges\":\"Insignias de HD, nuevo, gaming y favorito\",\"Homepage Studio\":\"Estudio de la página de inicio\",\"Balanced preset\":\"Preajuste equilibrado\",\"Compact preset\":\"Preajuste compacto\",\"Data-rich preset\":\"Preajuste con muchos datos\",\"Cinema preset\":\"Preajuste cine\",\"Layout and pagination\":\"Diseño y paginación\",\"Show quick toolbar above models\":\"Mostrar la barra rápida sobre los modelos\",\"Cards per row\":\"Tarjetas por fila\",\"Image ratio\":\"Proporción de imagen\",\"Information display\":\"Presentación de la información\",\"Detailed\":\"Detallado\",\"Overlay on image\":\"Superpuesto sobre la imagen\",\"Powerful sorting\":\"Ordenación avanzada\",\"Primary sort\":\"clasificación primaria\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"La ordenación solo cambia el orden de visualización de las tarjetas cargadas. No modifica los seguidos ni los favoritos de tu cuenta de Chaturbate.\",\"Restore website order\":\"Restaurar el orden del sitio\",\"Clear all filters\":\"Borrar todos los filtros\",\"Search and range filters\":\"Búsqueda y filtros de intervalo\",\"When several tags are selected\":\"Cuando se seleccionan varias etiquetas\",\"Model must contain every tag\":\"El modelo debe contener todas las etiquetas.\",\"At least one tag matches\":\"Al menos una etiqueta coincide\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"El filtrado de países muestra modelos de cualquier país seleccionado. Las etiquetas propias y las etiquetas de sitios web reconocidas permanecen disponibles en la lista y se pueden encontrar rápidamente mediante la búsqueda.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Después de seleccionar etiquetas o países, RoomGrid busca en el catálogo en vivo actual completo. La misma página de resultados muestra inicialmente 120 coincidencias y carga más en fragmentos mientras se desplaza, por lo que incluso las etiquetas muy comunes siguen respondiendo. El botón recargar en la barra de herramientas de la página de inicio actualiza la lista en vivo.\",\"Quick conditions\":\"Condiciones rápidas\",\"HD models only\":\"Solo modelos HD\",\"New models only\":\"Solo modelos nuevos\",\"Gaming models only\":\"Solo modelos de gaming\",\"Only saved in RoomGrid\":\"Solo guardados en RoomGrid\",\"Only own favorites\":\"Solo favoritos propios\",\"Following only\":\"Sólo siguiendo\",\"Hide previously watched models\":\"Ocultar modelos vistos anteriormente\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"El país, la edad y el tiempo en línea proceden de las tarjetas públicas actuales o del directorio público de salas. Si Chaturbate no proporciona un dato, RoomGrid no lo inventa.\",\"Smart filter views\":\"Vistas de filtro inteligente\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Guarde los filtros actuales y ordene como una vista dinámica. Los modelos que se conectan más tarde y coinciden con las reglas aparecen automáticamente.\",\"Save current filter view\":\"Guardar la vista de filtro actual\",\"No smart views saved yet.\":\"Aún no se han guardado vistas inteligentes.\",\"What to show for each model\":\"Qué mostrar para cada modelo\",\"Import complete\":\"Importación completa\",\"Invalid file\":\"Archivo no válido\",\"Range\":\"Rango\",\"Today\":\"hoy\",\"Week\":\"Semana\",\"Month\":\"Mes\",\"Year\":\"Año\",\"Recording enabled\":\"Grabación habilitada\",\"Private session (do not record)\":\"Sesión privada (no grabar)\",\"Retention days\":\"Días de retención\",\"Usage time\":\"tiempo de uso\",\"Summed camera time\":\"Tiempo de cámara sumado\",\"Visits\":\"Visitas\",\"Known models\":\"Modelos conocidos\",\"Weekly heatmap\":\"Mapa de calor semanal\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Agregado por día de la semana y hora; Más oscuro significa más tiempo observado.\",\"Sunday to Saturday, 24 hours per row\":\"Domingo a sábado, 24 horas por fila\",\"Watch ranking\":\"Ver clasificación\",\"little data\":\"pocos datos\",\"No watch history yet.\":\"Aún no hay historial de reproducciones.\",\"Workspace, filter and quality events\":\"Espacio de trabajo, filtro y eventos de calidad.\",\"Recent timeline\":\"Cronología reciente\",\"Export sensitive statistics\":\"Exportar estadísticas confidenciales\",\"Import statistics\":\"Estadísticas de importación\",\"Delete selected range\":\"Eliminar rango seleccionado\",\"Delete local events in the selected range?\":\"¿Eliminar eventos locales en el rango seleccionado?\",\"Clear all statistics\":\"Borrar todas las estadísticas\",\"Permanently clear all local statistics?\":\"¿Borrar permanentemente todas las estadísticas locales?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Las exportaciones de estadísticas contienen nombres de modelos y marcas de tiempo y deben tratarse como confidenciales. Los datos antiguos se limpian según la retención.\",\"new\":\"nuevo\",\"Under 1 minute\":\"Menos de 1 minuto\",\"1–5 minutes\":\"1 a 5 minutos\",\"5–15 minutes\":\"5 a 15 minutos\",\"15–30 minutes\":\"15 a 30 minutos\",\"30+ minutes\":\"30+ minutos\",\"Local observations and your usage\":\"Observaciones locales y su uso.\",\"Public profile loaded on request\":\"Perfil público cargado a pedido\",\"Local RoomGrid interaction\":\"Interacción local RoomGrid\",\"Local browser player\":\"Reproductor de navegador local\",\"Optional official statistics API\":\"API de estadísticas oficiales opcional\",\"Local daily aggregation\":\"Agregación diaria local\",\"Local data\":\"Datos locales\",\"Model summaries\":\"Resúmenes de modelos\",\"Public profiles\":\"Perfiles públicos\",\"Usage events\":\"Eventos de uso\",\"Official account snapshots\":\"Instantáneas de la cuenta oficial\",\"Daily rollups\":\"Resúmenes diarios\",\"Camera time\":\"tiempo de cámara\",\"vs previous period\":\"vs periodo anterior\",\"Trends\":\"Tendencias\",\"Daily activity\":\"Actividad diaria\",\"Playback quality\":\"Calidad de reproducción\",\"Privacy status\":\"Estado de privacidad\",\"Data provenance\":\"Procedencia de los datos\",\"Local RoomGrid usage\":\"Uso local de RoomGrid\",\"7 days\":\"7 dias\",\"30 days\":\"30 dias\",\"90 days\":\"90 dias\",\"Not enough data yet.\":\"Aún no hay suficientes datos.\",\"Models revisited\":\"Modelos revisados\",\"Previews opened\":\"Vistas previas abiertas\",\"Quick Views\":\"Vistas rápidas\",\"Samples\":\"Muestras\",\"Private session\":\"Sesión privada\",\"History paused\":\"Historia en pausa\",\"Recording locally\":\"Grabación local\",\"Disabled\":\"Discapacitado\",\"No cloud sync or telemetry.\":\"Sin sincronización en la nube ni telemetría.\",\"Raw events\":\"Eventos crudos\",\"Only local observations and actions from this browser.\":\"Sólo observaciones y acciones locales desde este navegador.\",\"Time\":\"tiempo\",\"Models\":\"Modelos\",\"Online planner\":\"Planificador en línea\",\"Data & privacy\":\"Datos y privacidad\",\"Configure statistics dashboard\":\"Configurar el panel de estadísticas\",\"Preset\":\"Preestablecido\",\"Minimal\":\"mínimo\",\"Quality focus\":\"Enfoque de calidad\",\"Privacy focus\":\"Enfoque de privacidad\",\"Reset dashboard\":\"Restablecer el panel\",\"Size\":\"Tamaño\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"El orden, la visibilidad y el ancho se almacenan en configuraciones seguras para actualizaciones. Cada widget muestra su fuente de datos.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"No hay widgets del panel visibles. Abra la configuración y seleccione al menos un widget.\",\"Median session\":\"Sesión mediana\",\"Daily usage trend\":\"Tendencia de uso diario\",\"Your trend appears here after you start using RoomGrid.\":\"Su tendencia aparece aquí después de comenzar a usar RoomGrid.\",\"Session-duration distribution\":\"Distribución de la duración de la sesión\",\"No session-duration data yet.\":\"Aún no hay datos sobre la duración de la sesión.\",\"Return rate\":\"Tasa de retorno\",\"Total sessions\":\"Sesiones totales\",\"Longest observed\":\"Observado por más tiempo\",\"Model ranking\":\"Clasificación de modelos\",\"Select up to four models for a local comparison.\":\"Seleccione hasta cuatro modelos para una comparación local.\",\"Quick View\":\"Vista rápida\",\"No model data yet.\":\"Aún no hay datos del modelo.\",\"Models with time patterns\":\"Modelos con patrones de tiempo.\",\"High confidence\":\"Alta confianza\",\"Moderate confidence\":\"Confianza moderada\",\"Planning horizon\":\"Horizonte de planificación\",\"Likely next active times\":\"Probables próximos tiempos activos\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Calculado a partir de muestras en línea observadas localmente; no garantiza una transmisión. La confianza mejora con más datos.\",\"High\":\"Alto\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Aún no hay suficientes observaciones en línea. El planificador aparece automáticamente con la biblioteca.\",\"Discovery events\":\"Eventos de descubrimiento\",\"Local interaction funnel\":\"Embudo de interacción local\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Sólo se cuentan tus acciones en este dispositivo; El contenido del chat y los pagos nunca se rastrean.\",\"Workspace events\":\"Eventos del espacio de trabajo\",\"Filter events\":\"Filtrar eventos\",\"Tag events\":\"Etiquetar eventos\",\"Top-four workspace comparison\":\"Comparación de los cuatro mejores espacios de trabajo\",\"Workspace usage\":\"Uso del espacio de trabajo\",\"No workspace data yet.\":\"Aún no hay datos del espacio de trabajo.\",\"Average active streams\":\"Flujos activos promedio\",\"Playback quality by model\":\"Calidad de reproducción por modelo\",\"No quality data yet.\":\"Aún no hay datos de calidad.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"El archivo de estadísticas no es válido o excede los límites de seguridad. Los datos actuales no fueron modificados.\",\"Statistics import assistant · preview\":\"Asistente de importación de estadísticas · vista previa\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Nada ha cambiado todavía. Seleccione las secciones para importar y revisar conflictos.\",\"Safe merge (recommended)\":\"Fusión segura (recomendado)\",\"Import complete. The previous statistics were backed up automatically.\":\"Importación completa. Las estadísticas anteriores fueron respaldadas automáticamente.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"La importación no se pudo aplicar de forma segura. Los datos actuales y las copias de seguridad permanecen sin cambios.\",\"File schema\":\"Esquema de archivo\",\"Date range\":\"Rango de fechas\",\"Sensitive data\":\"Datos sensibles\",\"Not marked sensitive\":\"No marcado sensible\",\"Until manually resumed\":\"Hasta que se reanude manualmente\",\"Not paused\":\"No pausado\",\"Restore this statistics backup? The current state will be backed up first.\":\"¿Restaurar esta copia de seguridad de estadísticas? Primero se realizará una copia de seguridad del estado actual.\",\"Permanently delete this backup?\":\"¿Eliminar permanentemente esta copia de seguridad?\",\"Events\":\"Eventos\",\"Estimated statistics storage\":\"Almacenamiento de estadísticas estimadas\",\"Recording, pauses and private sessions\":\"Grabación, pausas y sesiones privadas\",\"Record usage statistics\":\"Estadísticas de uso de registros\",\"Pause 15 minutes\":\"Pausa 15 minutos\",\"Pause 1 hour\":\"Pausa 1 hora\",\"Pause 8 hours\":\"Pausa 8 horas\",\"Pause indefinitely\":\"Pausa indefinidamente\",\"Resume history\":\"Reanudar historial\",\"History status\":\"Estado del historial\",\"Private for 30 minutes\":\"Privado por 30 minutos\",\"Private for 2 hours\":\"Privado por 2 horas\",\"Private until resumed\":\"Privado hasta que se reanude\",\"End private session\":\"Finalizar sesión privada\",\"Not active\":\"No activo\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Una sesión privada también detiene las observaciones públicas del modelo; una pausa normal del historial detiene el uso personal y los registros de calidad.\",\"Automatic deletion and retention\":\"Eliminación y retención automáticas\",\"Raw events (days)\":\"Eventos sin procesar (días)\",\"Quality samples (days)\":\"Muestras de calidad (días)\",\"Account snapshots (days)\":\"Instantáneas de la cuenta (días)\",\"Daily rollups (days)\":\"Resúmenes diarios (días)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Los eventos sin procesar vencidos se acumulan en totales diarios antes de la eliminación automática. Las instantáneas de calidad y de cuenta utilizan sus propios límites.\",\"Statistics backup center\":\"Centro de respaldo de estadísticas\",\"Create internal backup now\":\"Crea una copia de seguridad interna ahora\",\"Pseudonymized export\":\"Exportación seudónima\",\"Print report\":\"Imprimir informe\",\"Choose import file\":\"Elija el archivo de importación\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Aún no hay copias de seguridad de estadísticas internas. Se conservan hasta tres después de importaciones, restauraciones o creación manual.\",\"Delete local statistics in this range? A backup will be created first.\":\"¿Eliminar estadísticas locales en este rango? Primero se creará una copia de seguridad.\",\"Clear everything\":\"Limpiar todo\",\"Permanently clear all local statistics? A backup will be created first.\":\"¿Borrar permanentemente todas las estadísticas locales? Primero se creará una copia de seguridad.\",\"Data inventory and provenance\":\"Inventario de datos y procedencia\",\"Stored only in this browser\":\"Almacenado sólo en este navegador\",\"Coverage\":\"Cobertura\",\"Unknown or missing data is never inferred.\":\"Nunca se infieren datos desconocidos o faltantes.\",\"Metric definitions & data quality\":\"Definiciones de métricas y calidad de los datos\",\"Accumulated locally while a RoomGrid page is active.\":\"Acumulado localmente mientras una página RoomGrid está activa.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Suma de todas las sesiones de cámara observadas; Se agregan transmisiones simultáneas.\",\"browser samples; no interpolation.\":\"muestras de navegadores; sin interpolación.\",\"Privacy boundary\":\"Límite de privacidad\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"No se guarda ningún contenido de chat, pagos ni archivos multimedia. Las sesiones privadas no añaden eventos.\",\"No history yet\":\"Aún no hay historial\",\"Loading\":\"Cargando\",\"Connected\":\"Conectado\",\"Locked\":\"bloqueado\",\"Not connected\":\"No conectado\",\"Username\":\"Nombre de usuario\",\"24-character stats token\":\"Token de estadísticas de 24 caracteres\",\"At least 8 characters\":\"Al menos 8 caracteres\",\"Secure official statistics connection\":\"Conexión segura a estadísticas oficiales\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Opcional. El token nunca ingresa a la configuración, registros o exportaciones normales. El modo de sesión lo mantiene sólo en la memoria; El modo cifrado utiliza almacenamiento local aislado.\",\"Stats token\":\"Ficha de estadísticas\",\"Vault password\":\"Contraseña de la bóveda\",\"Connect for this session\":\"Conéctate para esta sesión\",\"Username or token is invalid.\":\"El nombre de usuario o el token no son válidos.\",\"Encrypt, save & connect\":\"Cifre, guarde y conéctese\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"No se pudo guardar cifrado. Verifique los permisos del token, la contraseña y el administrador de scripts de usuario.\",\"Get official token\":\"Obtener token oficial\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"La actualización automática se ejecuta como máximo una vez cada cinco minutos. Este módulo solo utiliza un token oficial que usted proporciona deliberadamente.\",\"Account\":\"cuenta\",\"History snapshots\":\"Instantáneas de la historia\",\"Last refresh\":\"Última actualización\",\"Unlock local vault\":\"Desbloquear la bóveda local\",\"Password\":\"Contraseña\",\"Unlock\":\"Desbloquear\",\"Unlock failed.\":\"Error de desbloqueo.\",\"Loading…\":\"Cargando…\",\"Refresh now\":\"Actualizar ahora\",\"Auto-refresh every 5 minutes\":\"Actualización automática cada 5 minutos\",\"Disconnect and delete token\":\"Desconectar y eliminar token\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"¿Eliminar la conexión local y el token cifrado? Las estadísticas históricas permanecen.\",\"Broadcast status\":\"Estado de transmisión\",\"Token balance\":\"Saldo de tokens\",\"Tips last hour\":\"Consejos de última hora\",\"Followers\":\"Seguidores\",\"Up votes\":\"Votos a favor\",\"Down votes\":\"Votos negativos\",\"Satisfaction\":\"Satisfacción\",\"Registered viewers\":\"Espectadores registrados\",\"Tokened viewers\":\"Espectadores tokenizados\",\"Time online\":\"tiempo en línea\",\"Follower trend\":\"Tendencia de seguidores\",\"Tips trend\":\"Tendencia de consejos\",\"Token-balance trend\":\"Tendencia del saldo de tokens\",\"Viewer trend\":\"Tendencia del espectador\",\"Account Health Center\":\"Centro de salud de cuentas\",\"Official My Collection\":\"Oficial Mi Colección\",\"Official support\":\"Soporte oficial\",\"Export local statistics\":\"Exportar estadísticas locales\",\"Delete snapshot history\":\"Eliminar el historial de instantáneas\",\"Delete the local official-statistics snapshots for this account?\":\"¿Eliminar las instantáneas de las estadísticas oficiales locales de esta cuenta?\",\"Last error\":\"último error\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"El estado del token, la cadencia de actualización, la historia local y los enlaces oficiales se encuentran aquí. RoomGrid nunca publica, envía mensajes ni realiza transacciones automáticamente.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"El navegador no otorgó permiso de notificación. Las notificaciones dentro de la página permanecen activas.\",\"Notification profile\":\"Perfil de notificación\",\"Go-live only\":\"Solo puesta en marcha\",\"Personalized\":\"Personalizado\",\"Always (all events)\":\"Siempre (todos los eventos)\",\"Inactive days\":\"Días inactivos\",\"Favorites, rating, priority and your tags decide relevance.\":\"Favoritos, calificación, prioridad y tus etiquetas deciden la relevancia.\",\"Includes go-live, offline and public-profile changes.\":\"Incluye cambios de perfil público, fuera de línea y en funcionamiento.\",\"Master switch\":\"interruptor maestro\",\"On\":\"encendido\",\"Off (default)\":\"Desactivado (predeterminado)\",\"Monitored models\":\"Modelos monitoreados\",\"Active tab\":\"Pestaña activa\",\"This tab\":\"Esta pestaña\",\"Another tab\":\"Otra pestaña\",\"Local notifications\":\"Notificaciones locales\",\"Notification delivery and display\":\"Entrega y visualización de notificaciones\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Las notificaciones dentro de la página utilizan el área libre en la barra superior de RoomGrid y no necesitan permiso del navegador. La posición, la duración y la densidad son configurables.\",\"Delivery\":\"Entrega\",\"RoomGrid bar only\":\"Solo barra RoomGrid\",\"Browser notifications only\":\"Sólo notificaciones del navegador\",\"RoomGrid bar and browser\":\"Barra y navegador RoomGrid\",\"No display, history only\":\"Sin visualización, solo historial\",\"In-page position\":\"Posición en la página\",\"Free top-bar area\":\"Zona de barra superior gratuita\",\"Floating top center\":\"Centro superior flotante\",\"Floating top right\":\"Flotante arriba a la derecha\",\"Display style\":\"Estilo de visualización\",\"Compact single line\":\"Línea única compacta\",\"Display duration\":\"Duración de la visualización\",\"Until dismissed\":\"Hasta que sea despedido\",\"Visible at once\":\"Visible a la vez\",\"Test notification\":\"Notificación de prueba\",\"General rules\":\"Reglas generales\",\"Enable notifications\":\"Habilitar notificaciones\",\"The browser did not grant notification permission.\":\"El navegador no otorgó permiso de notificación.\",\"Neutral text\":\"Texto neutro\",\"Quiet time starts\":\"Comienza el tiempo de tranquilidad\",\"Quiet time ends\":\"El tiempo de tranquilidad termina\",\"Cooldown (minutes)\":\"Enfriamiento (minutos)\",\"Hourly limit\":\"Límite horario\",\"Favorite threshold\":\"Umbral favorito\",\"Open on click\":\"Abrir al hacer clic\",\"Per model\":\"Por modelo\",\"Model\":\"modelo\",\"Library is empty.\":\"La biblioteca está vacía.\",\"Per group\":\"Por grupo\",\"Group\":\"grupo\",\"No groups.\":\"Sin grupos.\",\"Local notification list\":\"Lista de notificaciones locales\",\"Mark all read\":\"Marcar todo como leído\",\"Clear list\":\"Borrar lista\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Las verificaciones de antecedentes solo leen el estado y no inician ningún video. Un script de usuario sólo puede ejecutarse mientras una pestaña del sitio relacionado está abierta; varias pestañas eligen automáticamente una ficha.\",\"Shield status\":\"Estado del escudo\",\"Ready\":\"Listo\",\"Local PIN\":\"PIN local\",\"Set\":\"conjunto\",\"Not set\":\"No establecido\",\"Quick shield\":\"Escudo rápido\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Cubre inmediatamente nombres e imágenes y pausa/silencia todos los medios. La restauración devuelve el estado anterior del medio.\",\"Activate now\":\"Activar ahora\",\"Auto-shield after inactivity\":\"Protección automática después de inactividad\",\"Set or change local PIN\":\"Establecer o cambiar el PIN local\",\"Leave empty to remove\":\"Dejar vacío para eliminar\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"El PIN es sólo una barrera de acceso local, no un cifrado, y no reemplaza la protección de la cuenta del sistema operativo.\",\"History controls\":\"Controles de historial\",\"Usage statistics\":\"Estadísticas de uso\",\"Pause history 1 hour\":\"Pausar el historial 1 hora\",\"Clear statistics\":\"Borrar estadísticas\",\"Clear all local statistics? A backup will be created first.\":\"¿Borrar todas las estadísticas locales? Primero se creará una copia de seguridad.\",\"Clear Discover settings\":\"Borrar configuración de Descubrir\",\"Clear local Discover filters and feeds?\":\"¿Borrar filtros y feeds locales de Discover?\",\"Clear notification list\":\"Borrar lista de notificaciones\",\"Data boundary\":\"Límite de datos\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"La configuración, las estadísticas de visualización, el historial de notificaciones y las preferencias de Discover se almacenan localmente en este navegador. RoomGrid no contiene sincronización en la nube, carga de cuentas ni telemetría.\",\"Quota\":\"Cuota\",\"Used\":\"Usado\",\"Sensitivity & consent center\":\"Centro de sensibilidad y consentimiento\",\"Standard configuration\":\"Configuración estándar\",\"Theme, layout, filters\":\"Tema, diseño, filtros.\",\"Local\":\"locales\",\"Model names, notes, timestamps, statistics\":\"Nombres de modelos, notas, marcas de tiempo, estadísticas.\",\"Warn on export\":\"Advertir sobre la exportación\",\"Secret\":\"secreto\",\"Optional official statistics token\":\"Token de estadísticas oficiales opcional\",\"Session-only or encrypted vault\":\"Bóveda cifrada o de solo sesión\",\"Chat content\":\"Contenido del chat\",\"Never persisted\":\"Nunca persistió\",\"Ephemeral\":\"efímero\",\"Stream recording/screenshots\":\"Grabación de secuencias/capturas de pantalla\",\"Not provided\":\"No proporcionado\",\"Policy guard\":\"guardia de política\",\"Performance profile\":\"Perfil de rendimiento\",\"Playing streams\":\"Reproduciendo transmisiones\",\"Video elements\":\"Elementos de vídeo\",\"Measured memory\":\"Memoria medida\",\"Unavailable in browser\":\"No disponible en el navegador\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Las mediciones actuales están cerca del límite configurado. Pausa las cámaras no utilizadas o elige el modo Eco.\",\"Stream performance\":\"Rendimiento de la transmisión\",\"Eco\":\"ecológico\",\"Power\":\"poder\",\"Concurrent stream limit\":\"Límite de transmisión simultánea\",\"Adaptive quality\":\"Calidad adaptativa\",\"Gradual startup\":\"Inicio gradual\",\"Automatic performance mode\":\"Modo de rendimiento automático\",\"Memory threshold\":\"Umbral de memoria\",\"Stream-count threshold\":\"Umbral de recuento de transmisiones\",\"Current auto state\":\"Estado automático actual\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Se detienen las vistas previas no utilizadas. El vídeo nuevo permanece en pausa en el límite hasta que haya capacidad disponible. Los recuentos altos comienzan gradualmente; No existe garantía de “cámaras ilimitadas”.\",\"Multi-window workspaces\":\"Espacios de trabajo con múltiples ventanas\",\"Window name\":\"Nombre de la ventana\",\"Active and recent windows\":\"Ventanas activas y recientes\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"La biblioteca de modelos se sincroniza mediante fusión local; Cada ventana mantiene su propio espacio de trabajo y configuración de vista. Las asociaciones de ventanas recientes se pueden restaurar después de reiniciar cuando el navegador lo permita.\",\"Optional tools are disabled\":\"Las herramientas opcionales están deshabilitadas\",\"Media, app and contest directories will not scan the current page.\":\"Los directorios de medios, aplicaciones y concursos no escanearán la página actual.\",\"Enable optional tools\":\"Habilitar herramientas opcionales\",\"Local search\":\"búsqueda local\",\"Search saved items and current page\":\"Buscar elementos guardados y página actual\",\"Rescan current page\":\"Volver a escanear la página actual\",\"Disable optional tools\":\"Deshabilitar herramientas opcionales\",\"Clear local side data\":\"Borrar datos del lado local\",\"Clear app favorites, categories and media notes?\":\"¿Borrar favoritos de aplicaciones, categorías y notas multimedia?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Estos módulos solo organizan enlaces públicos y texto a los que ya se puede acceder en la página actual. No copian medios, no descargan automáticamente ni operan funciones de chat, token o pago. Los formularios de inicio de sesión, verificación de edad, seguridad y pago permanecen intactos.\",\"Photo and video areas\":\"Áreas de fotografía y vídeo.\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"En esta página no hay ningún área de medios públicos reconocible. Vuelva a escanear mientras se encuentra en una página de foto o video nativa.\",\"Apps directory\":\"Directorio de aplicaciones\",\"Local category\":\"categoría local\",\"Found on page\":\"Encontrado en la página\",\"No recognizable app links are present on this page.\":\"En esta página no hay enlaces de aplicaciones reconocibles.\",\"Public contest results\":\"Resultados del concurso público\",\"No readable public contest table is present on this page.\":\"En esta página no hay ninguna tabla de concurso pública legible.\",\"Native account, help and broadcaster links\":\"Cuenta nativa, ayuda y enlaces de emisoras\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"No se encontraron enlaces nativos seguros en esta página; Las rutas de pago e inicio de sesión se filtran deliberadamente.\",\"Homepage filters reset\":\"Restablecer filtros de página de inicio\",\"Global appearance reset\":\"Restablecimiento de apariencia global\",\"Version\":\"Versión\",\"Health score\":\"Puntuación de salud\",\"Page type\":\"Tipo de página\",\"Route generation\":\"Generación de ruta\",\"Running modules\":\"Módulos en ejecución\",\"Local errors\":\"Errores locales\",\"Repair center\":\"centro de reparacion\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Se crea una copia de seguridad de la configuración antes de cada reparación. Estas acciones no eliminan las estadísticas y los datos del modelo.\",\"Reset home filters\":\"Restablecer filtros de inicio\",\"Reset all home-page filters?\":\"¿Restablecer todos los filtros de la página de inicio?\",\"Reset global appearance\":\"Restablecer apariencia global\",\"Reset the global appearance? Saved design profiles remain.\":\"¿Restablecer la apariencia global? Los perfiles de diseño guardados permanecen.\",\"Normalize configuration\":\"Normalizar configuración\",\"Validate and rewrite the current configuration? A backup is created first.\":\"¿Validar y reescribir la configuración actual? Primero se crea una copia de seguridad.\",\"Close all previews\":\"Cerrar todas las vistas previas\",\"Compatibility self-check\":\"Autocomprobación de compatibilidad\",\"Module and page status\":\"Estado del módulo y de la página\",\"Page route test matrix\":\"Matriz de prueba de ruta de página\",\"Current route\":\"Ruta actual\",\"Trigger\":\"gatillo\",\"Recent page transitions\":\"Transiciones de página recientes\",\"Capability layer\":\"Capa de capacidad\",\"Available\":\"Disponible\",\"Fallback\":\"Respaldo\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Cuando falta una API, solo retrocede el módulo afectado. Los fallos repetidos de la red abren temporalmente un disyuntor.\",\"Backup and recovery status\":\"Estado de copia de seguridad y recuperación\",\"Primary data\":\"datos primarios\",\"Stable archive\":\"Archivo estable\",\"Rolling backups\":\"Copias de seguridad continuas\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"La autocomprobación sólo verifica la legibilidad y las capacidades del navegador; nunca sobrescribe los datos actuales automáticamente. La recuperación continúa utilizando el flujo de respaldo confirmado existente.\",\"Sanitized local error log\":\"Registro de errores local desinfectado\",\"No RoomGrid errors recorded.\":\"No se registraron errores de RoomGrid.\",\"Run self-check again\":\"Ejecute la autocomprobación nuevamente\",\"Export diagnostics\":\"Exportar diagnóstico\",\"Clear error log\":\"Borrar registro de errores\",\"Return to normal site\":\"Regresar al sitio normal\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"La exportación de diagnóstico no contiene espacios de trabajo, nombres de modelos, notas ni estadísticas. Los parámetros de consulta de URL y los posibles valores secretos se eliminan del texto de error.\",\"Local history on\":\"historia local en\",\"History off\":\"Historial desactivado\",\"This settings page could not be loaded\":\"Esta página de configuración no se pudo cargar\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Tus datos no fueron cambiados. Intentar otra vez; Si el problema persiste, abra Diagnóstico.\",\"Try again\":\"Inténtalo de nuevo\",\"Open Diagnostics\":\"Diagnóstico abierto\",\"Public results\":\"Resultados públicos\",\"Public contest links\":\"Enlaces de concurso público\",\"Page content has not appeared yet\":\"El contenido de la página aún no ha aparecido\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid revisó la página nuevamente pero aún no puede encontrar el área de contenido de Chaturbate. Es posible que la página aún se esté cargando o que el sitio haya mostrado una vista vacía.\",\"Check again\":\"comprobar de nuevo\",\"Open RoomGrid Discover\":\"Abrir RoomGrid Descubrir\",\"Reload page\":\"Recargar página\",\"Hide notice\":\"Ocultar aviso\",\"Open MultiCam\":\"Abrir multicámara\",\"Open workstation in a new window\":\"Abrir estación de trabajo en una nueva ventana\",\"Open Discover\":\"Abrir Descubrir\",\"Statistics center\":\"Centro de estadísticas\",\"Backup and import\":\"Copia de seguridad e importación\",\"Page and design options\":\"Opciones de página y diseño.\",\"Hide immediately\":\"Ocultar inmediatamente\",\"All tools\":\"Todas las herramientas\",\"Search & commands\":\"Búsqueda y comandos\",\"Page design\":\"Diseño de página\",\"Official collection\":\"Colección oficial\",\"Saved\":\"Guardado\",\"Quick info\":\"información rápida\",\"Loading live frame\":\"Cargando fotograma en vivo\",\"Live still\":\"vivir todavía\",\"Not live\":\"no vivir\",\"Pages\":\"paginas\",\"Refresh\":\"Actualizar\",\"Search…\":\"Buscar…\",\"Tag Explorer\":\"Explorador de etiquetas\",\"Refresh public catalog\":\"Actualizar catálogo público\",\"Online\":\"En línea\",\"Toggle favorite\":\"Alternar favorito\",\"Save\":\"Guardar\",\"Remove favorite\":\"Eliminar favorito\",\"Removed from library\":\"Eliminado de la biblioteca\",\"Saved to library\":\"Guardado en la biblioteca\",\"Added to favorites\":\"Añadido a favoritos\",\"Removed from favorites\":\"Eliminado de favoritos\",\"Watching\":\"mirando\",\"Watch\":\"ver\",\"Temporary watchlist\":\"Lista de vigilancia temporal\",\"Removed from watchlist\":\"Eliminado de la lista de seguimiento\",\"Added to watchlist\":\"Añadido a la lista de seguimiento\",\"Choose group\":\"Elige grupo\",\"Add to group…\":\"Añadir al grupo…\",\"Group updated\":\"Grupo actualizado\",\"Site room\":\"Sala del sitio\",\"Select for bulk actions\":\"Seleccionar para acciones masivas\",\"viewers\":\"espectadores\",\"No public description\":\"Sin descripción pública\",\"Open in Tag Explorer\":\"Abrir en el Explorador de etiquetas\",\"Feed name\":\"Nombre del feed\",\"View name\":\"Ver nombre\",\"Name already exists\":\"El nombre ya existe\",\"Saved-item limit reached\":\"Se alcanzó el límite de elementos guardados\",\"Feed saved\":\"Feed guardado\",\"View saved\":\"Ver guardado\",\"Applied\":\"Aplicado\",\"Backup exported\":\"Copia de seguridad exportada\",\"Backup imported\":\"Copia de seguridad importada\",\"Invalid backup\":\"Copia de seguridad no válida\",\"New models\":\"Nuevos modelos\",\"New since last visit\":\"Nuevo desde la última visita\",\"New to me\":\"nuevo para mi\",\"Returning\":\"Regresando\",\"Stable online\":\"Estable en línea\",\"Saved only\":\"Sólo guardado\",\"Previously watched\":\"Visto previamente\",\"Local collection\":\"Colección local\",\"Hide visited today\":\"Ocultar visitado hoy\",\"No feed selected\":\"No se ha seleccionado ningún feed\",\"No view selected\":\"Ninguna vista seleccionada\",\"Saved feeds\":\"Feeds guardados\",\"Saved views\":\"Vistas guardadas\",\"Save new\":\"Guardar nuevo\",\"All public rooms\":\"Todas las salas públicas\",\"Known HD\":\"Conocido HD\",\"Random result\":\"Resultado aleatorio\",\"Viewers ↓\":\"Espectadores ↓\",\"Viewers ↑\":\"Espectadores ↑\",\"Online duration ↓\":\"Duración en línea ↓\",\"Followers ↓\":\"Seguidores ↓\",\"Name A–Z\":\"Nombre A–Z\",\"Name Z–A\":\"Nombre Z–A\",\"New first\":\"Nuevo primero\",\"Watch time ↓\":\"Tiempo de visualización ↓\",\"Sessions ↓\":\"Sesiones ↓\",\"Last watched\":\"visto por última vez\",\"Name, description, tag, place\":\"Nombre, descripción, etiqueta, lugar.\",\"Secondary sort\":\"clasificación secundaria\",\"Shuffle again\":\"Mezclar de nuevo\",\"All groups\":\"Todos los grupos\",\"All tags (AND)\":\"Todas las etiquetas (Y)\",\"Any tag (OR)\":\"Cualquier etiqueta (O)\",\"Excluded tags (NOT)\":\"Etiquetas excluidas (NO)\",\"Language, region, or country\":\"Idioma, región o país\",\"Minimum online minutes\":\"Minutos mínimos en línea\",\"Maximum online minutes\":\"Minutos máximos en línea\",\"Minimum local watch minutes\":\"Minutos mínimos de visualización local\",\"Advanced filters and Boolean tags\":\"Filtros avanzados y etiquetas booleanas\",\"Saved views and feeds\":\"Vistas y feeds guardados\",\"Import\":\"Importar\",\"Recent searches\":\"Búsquedas recientes\",\"Live previews and performance\":\"Vistas previas en vivo y actuación\",\"Muted live preview for every visible card\":\"Vista previa en vivo silenciada para cada tarjeta visible\",\"Refresh rate\":\"Frecuencia de actualización\",\"Real-time video while hovering\":\"Vídeo en tiempo real mientras se desplaza\",\"Hover delay\":\"Retraso de desplazamiento\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Las tarjetas visibles utilizan marcos JPEG. Al pasar el mouse, solo una tarjeta cambia a video silenciado en tiempo real y se detiene por completo cuando el puntero se aleja. Las altas velocidades de fotogramas aumentan significativamente el ancho de banda y el uso de la CPU.\",\"saved\":\"salvado\",\"previews\":\"vistas previas\",\"Previews off\":\"Vistas previas desactivadas\",\"No more feeds can be saved\":\"No se pueden guardar más feeds\",\"Tag feed saved\":\"Feed de etiquetas guardado\",\"Select tag\":\"Seleccionar etiqueta\",\"Tag favorite\":\"Etiquetar favorito\",\"Block tag\":\"Etiqueta de bloque\",\"Select tags first\":\"Seleccione las etiquetas primero\",\"Tag-group name\":\"Nombre del grupo de etiquetas\",\"Tag group saved\":\"Grupo de etiquetas guardado\",\"No gaming rooms in the current catalog\":\"No hay salas de juego en el catálogo actual.\",\"All (AND)\":\"Todos (Y)\",\"Any (OR)\":\"Cualquiera (O)\",\"Exclude (NOT)\":\"Excluir (NO)\",\"Search tags\":\"Buscar etiquetas\",\"Tag search\":\"Búsqueda de etiquetas\",\"Combination\":\"combinación\",\"Model sorting\":\"Clasificación de modelos\",\"Show in Discover\":\"Mostrar en Descubrir\",\"Save tag group\":\"Guardar grupo de etiquetas\",\"Save tag feed\":\"Guardar feed de etiquetas\",\"No tag groups saved yet.\":\"Aún no se han guardado grupos de etiquetas.\",\"Unknown region\":\"Región desconocida\",\"Unknown age\":\"edad desconocida\",\"Public “New”\":\"Público “Nuevo”\",\"My new cams\":\"Mis nuevas cámaras\",\"New-cams feed saved\":\"Feed de nuevas cámaras guardado\",\"Save “My new cams” feed\":\"Guarde el feed \\\"Mis nuevas cámaras\\\"\",\"Create gaming workspace\":\"Crear un espacio de trabajo para juegos\",\"No matches for this combination.\":\"No hay coincidencias para esta combinación.\",\"Select tags to preview matching rooms here.\":\"Seleccione etiquetas para obtener una vista previa de las habitaciones coincidentes aquí.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Busque, marque como favorito, bloquee y combine etiquetas públicas. Las tendencias utilizan únicamente los cambios de catálogo observados en este dispositivo.\",\"Current tags\":\"Etiquetas actuales\",\"Counts show current matches and change since the prior fetch.\":\"Los recuentos muestran coincidencias actuales y cambios desde la búsqueda anterior.\",\"Results for selected tags\":\"Resultados para etiquetas seleccionadas\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Utiliza la misma clasificación y navegación SingleCam que Discover.\",\"Categories, age, and regions\":\"Categorías, edad y regiones\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Sólo se muestran campos públicos explícitos. Las regiones separadas por comas se combinan con OR; Los valores desconocidos permanecen visibles. No se adivina ninguna zona horaria.\",\"New and gaming\":\"Nuevo y juegos\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Visto por primera vez localmente, la bandera pública \\\"Nuevo\\\", la lista de vigilancia temporal y los temas de juegos permanecen separados.\",\"Tag groups\":\"Grupos de etiquetas\",\"Save AND, OR, or NOT combinations.\":\"Guarde combinaciones Y, O o NO.\",\"Favorite tags\":\"Etiquetas favoritas\",\"Your personal shortcut list.\":\"Tu lista de atajos personales.\",\"No favorites yet\":\"Aún no hay favoritos\",\"Related tags\":\"Etiquetas relacionadas\",\"Tags co-occurring on the currently matching models.\":\"Etiquetas que coexisten en los modelos coincidentes actualmente.\",\"Personal tag history\":\"Historial de etiquetas personales\",\"Recently and frequently used on this device.\":\"Utilizado recientemente y con frecuencia en este dispositivo.\",\"Bulk target group\":\"Grupo objetivo masivo\",\"Selected models added to group\":\"Modelos seleccionados agregados al grupo\",\"New workspace name\":\"Nuevo nombre del espacio de trabajo\",\"Created from a Discover selection\":\"Creado a partir de una selección de Discover\",\"Workspace created\":\"Espacio de trabajo creado\",\"Save all\":\"Guardar todo\",\"Create workspace\":\"Crear espacio de trabajo\",\"Loading public catalog…\":\"Cargando catálogo público…\",\"Loading the complete catalog in the background…\":\"Cargando el catálogo completo en segundo plano…\",\"Updated\":\"Actualizado\",\"Public rooms\":\"Salas publicas\",\"The catalog is loading.\":\"El catálogo se está cargando.\",\"No matching rooms. Adjust search or filters.\":\"No hay habitaciones que coincidan. Ajustar búsqueda o filtros.\",\"Previous\":\"Anterior\",\"Next\":\"Siguiente\",\"Move up\":\"Subir\",\"Move down\":\"Bajar\",\"Number shown\":\"Número mostrado\",\"Widget width\":\"Ancho del widget\",\"Full\":\"completo\",\"Nothing here yet.\":\"Nada aquí todavía.\",\"Newly online\":\"Recién en línea\",\"Recently visited\":\"Visitado recientemente\",\"Frequently visited\":\"Visitado con frecuencia\",\"Homepage appearance, pagination, and performance\":\"Aspecto, paginación y rendimiento de la página de inicio\",\"Information density\":\"Densidad de información\",\"Layout\":\"Diseño\",\"Data rich\":\"Rico en datos\",\"Static\":\"estática\",\"Show statistics summary\":\"Mostrar resumen de estadísticas\",\"Show personal widgets\":\"Mostrar widgets personales\",\"Sections below the live catalog\":\"Secciones debajo del catálogo en vivo\",\"Homepage live models\":\"Página de inicio modelos en vivo\",\"All live rooms in one place\":\"Todas las salas en vivo en un solo lugar\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Utilice los mismos filtros completos de búsqueda, clasificación, grupos, etiquetas e historial local que Discover.\",\"Refresh live catalog\":\"Actualizar catálogo en vivo\",\"Homepage settings\":\"Configuración de la página de inicio\",\"Matches a favorite gaming theme\":\"Coincide con un tema de juego favorito\",\"Saved models\":\"Modelos guardados\",\"Online favorites\":\"Favoritos en línea\",\"Local watch time\":\"Hora de visualización local\",\"Continue last MultiCam workspace\":\"Continuar con el último espacio de trabajo de MultiCam\",\"Open full Discover page\":\"Abrir la página Descubrir completa\",\"Online notifications\":\"Notificaciones en línea\",\"on\":\"en\",\"off\":\"apagado\",\"Offline notifications\":\"Notificaciones sin conexión\",\"Feed\":\"alimentar\",\"Recent\":\"Reciente\",\"Frequent\":\"frecuente\",\"Saved filter views\":\"Vistas de filtro guardadas\",\"Favorites currently public and online.\":\"Favoritos actualmente públicos y en línea.\",\"New models or public streams that started recently.\":\"Nuevos modelos o transmisiones públicas que comenzaron recientemente.\",\"Current results from the selected feed.\":\"Resultados actuales del feed seleccionado.\",\"Suggestions with reasons\":\"Sugerencias con razones\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Basado únicamente en etiquetas públicas compartidas y temas favoritos localmente; sin inferencia de identidad.\",\"Sorted by local last-visit time.\":\"Ordenado por hora de última visita local.\",\"Sorted by local watch time.\":\"Ordenado por hora de visualización local.\",\"Saved models never visited\":\"Modelos guardados nunca visitados\",\"Saved in the library without local watch time.\":\"Guardado en la biblioteca sin hora de visualización local.\",\"Draggable dashboard widgets\":\"Widgets de panel arrastrables\",\"Your RoomGrid homepage\":\"Tu página de inicio de RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Un catálogo en vivo rápido y personalizable con SingleCam, MultiCam, filtros completos, vistas guardadas y tus widgets personales.\",\"Audio activity\":\"Actividad de audio\",\"Auto quality\":\"Calidad automática\",\"Stream interrupted; status checks continue.\":\"Transmisión interrumpida; Las comprobaciones de estado continúan.\",\"This browser cannot play HLS.\":\"Este navegador no puede reproducir HLS.\",\"Connecting to live stream…\":\"Conectándose a la transmisión en vivo...\",\"The room is not public right now.\":\"La sala no es pública en este momento.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Actualmente fuera de línea. A continuación se muestran alternativas del mismo filtro.\",\"Status temporarily unavailable; retrying automatically.\":\"Estado no disponible temporalmente; reintentando automáticamente.\",\"Loading public profile…\":\"Cargando perfil público…\",\"Profile temporarily unavailable.\":\"Perfil no disponible temporalmente.\",\"No cached profile.\":\"Sin perfil en caché.\",\"Public profile changed\":\"Perfil público cambiado\",\"No public bio.\":\"Sin biografía pública.\",\"Source\":\"Fuente\",\"Observed\":\"Observado\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Sólo se muestran los enlaces autoeditados en el perfil público. Un nombre de usuario coincidente por sí solo no es prueba de identidad.\",\"Profile information\":\"Información de perfil\",\"Preparing…\":\"Preparando…\",\"Play\":\"Jugar\",\"Unmute\":\"Dejar de silenciar\",\"Mute\":\"Silenciar\",\"Volume\":\"Volumen\",\"Contain\":\"contener\",\"Cover\":\"Portada\",\"Brightness\":\"Brillo\",\"Contrast\":\"Contraste\",\"Saturation\":\"Saturación\",\"Hue\":\"tono\",\"Sepia\":\"sepia\",\"Grayscale\":\"Escala de grises\",\"Invert\":\"invertir\",\"Blur\":\"Desenfoque\",\"Image zoom\":\"Ampliar imagen\",\"Recording saved\":\"Grabación guardada\",\"Screenshot saved\":\"Captura de pantalla guardada\",\"Recording\":\"Grabación\",\"Mirror\":\"espejo\",\"Flip vertically\":\"Voltear verticalmente\",\"Rotate\":\"rotar\",\"Show edited\":\"Mostrar editado\",\"Show original\":\"Mostrar original\",\"Reset image\":\"Restablecer imagen\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Mantenga presionada la tecla Ctrl y desplácese para hacer zoom; arrastre la imagen cuando esté ampliada. Haga doble clic para restablecer.\",\"Zoom out\":\"alejar\",\"Zoom in\":\"Acercar\",\"Reset zoom and position\":\"Restablecer zoom y posición\",\"Player window\":\"ventana del jugador\",\"Fullscreen\":\"Pantalla completa\",\"Private notes, memories, or things to check next time (stored locally)\":\"Notas privadas, recuerdos o cosas para consultar la próxima vez (almacenados localmente)\",\"Autosave is on\":\"El guardado automático está activado\",\"Enter your own tag…\":\"Introduce tu propia etiqueta...\",\"Saved at\":\"Guardado en\",\"Local details saved\":\"Detalles locales guardados\",\"Remove tag\":\"Quitar etiqueta\",\"No personal tags yet.\":\"Aún no hay etiquetas personales.\",\"Saving…\":\"Guardando…\",\"Create your first group under Groups.\":\"Crea tu primer grupo en Grupos.\",\"Added to MultiCam\":\"Agregado a MultiCam\",\"Edit private details\":\"Editar detalles privados\",\"Region\":\"Región\",\"Current online time\":\"Tiempo actual en línea\",\"Stream attributes\":\"Atributos de flujo\",\"New\":\"Nuevo\",\"Yes\":\"si\",\"No / unknown\":\"No / desconocido\",\"Last observed\":\"último observado\",\"Local visits\":\"Visitas locales\",\"First watched\":\"visto por primera vez\",\"No public description.\":\"Sin descripción pública.\",\"No public tags available.\":\"No hay etiquetas públicas disponibles.\",\"Quick-add your own tag…\":\"Añade rápidamente tu propia etiqueta...\",\"Public tags\":\"Etiquetas públicas\",\"Add\":\"Añadir\",\"Private details\":\"Detalles privados\",\"Everything stays in this browser and survives updates.\":\"Todo permanece en este navegador y sobrevive a las actualizaciones.\",\"Private notebook\":\"cuaderno privado\",\"Keep on watchlist\":\"Mantener en la lista de vigilancia\",\"Group assignment (multiple)\":\"Asignación grupal (múltiple)\",\"Save now\":\"Ahorra ahora\",\"Chat loads only when this section is opened.\":\"El chat se carga solo cuando se abre esta sección.\",\"Official chat\":\"charla oficial\",\"Official chat connected.\":\"Chat oficial conectado.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Chat cargado; La estructura de la página actual no se pudo aislar completamente.\",\"Chat loaded.\":\"Chat cargado.\",\"Loading official chat…\":\"Cargando chat oficial…\",\"Official chat room\":\"Sala de chat oficial\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Cargado sólo cuando es necesario; RoomGrid nunca almacena ni envía automáticamente mensajes de chat.\",\"Reload chat\":\"Recargar chat\",\"Open official room in a new window\":\"Abrir sala oficial en una nueva ventana\",\"Hide chat\":\"Ocultar chat\",\"Live observations\":\"Observaciones en vivo\",\"Trend confidence\":\"Confianza en la tendencia\",\"Known time slots\":\"Franjas horarias conocidas\",\"Typical peak\":\"Pico típico\",\"Watched in 7 days\":\"Visto en 7 días\",\"Watched in 30 days\":\"Visto en 30 días\",\"Current streak\":\"Racha actual\",\"Typical weekly online times\":\"Tiempos semanales típicos en línea\",\"Online times will be learned on this device from now on.\":\"Los tiempos en línea se conocerán en este dispositivo a partir de ahora.\",\"Based on observations on this device; missing information is never guessed.\":\"Basado en observaciones de este dispositivo; La información que falta nunca se adivina.\",\"Activity by weekday\":\"Actividad por día laborable\",\"Most likely online times\":\"Tiempos en línea más probables\",\"Online-time map\":\"Mapa del tiempo en línea\",\"Public covers and locally saved example images.\":\"Portadas públicas e imágenes de ejemplo guardadas localmente.\",\"Import quality\":\"Calidad de importación\",\"Original file\":\"archivo original\",\"Very high (up to 4K)\":\"Muy alto (hasta 4K)\",\"High (up to 1440p)\":\"Alto (hasta 1440p)\",\"Space-saving (up to 900p)\":\"Ahorro de espacio (hasta 900p)\",\"Add images from computer\":\"Agregar imágenes desde la computadora\",\"Use website cover\":\"Usar portada del sitio web\",\"Use first image as cover\":\"Usar la primera imagen como portada.\",\"Covers and local gallery\":\"Portadas y galería local\",\"Updating images…\":\"Actualizando imágenes…\",\"Loading images…\":\"Cargando imágenes…\",\"Own image\":\"Imagen propia\",\"Example image\":\"Imagen de ejemplo\",\"Current cover\":\"portada actual\",\"Alternative preview\":\"Vista previa alternativa\",\"Current public image\":\"Imagen pública actual\",\"Saving cover…\":\"Guardando portada…\",\"Set as cover\":\"Establecer como portada\",\"Delete this image from the local gallery?\":\"¿Eliminar esta imagen de la galería local?\",\"No images yet.\":\"Aún no hay imágenes.\",\"saved locally\":\"guardado localmente\",\"own images\":\"propias imágenes\",\"Update public previews\":\"Actualizar vistas previas públicas\",\"Processing your images…\":\"Procesando tus imágenes...\",\"ALL PAGE SOUNDS OFF\":\"TODA LA PÁGINA SUENA APAGADA\",\"PAGE SOUNDS ALLOWED\":\"SONIDOS DE PÁGINA PERMITIDOS\",\"Chat/token sounds OFF\":\"Sonidos de chat/token APAGADOS\",\"Chat/token sounds ON\":\"Sonidos de chat/token activados\",\"Reconnecting the player with the new settings…\":\"Reconectando el reproductor con la nueva configuración…\",\"Turn off chat, token and tip sounds completely\":\"Desactivar completamente los sonidos de chat, tokens y propinas\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Bloquea los sonidos de la página de elementos de audio y Web Audio. La transmisión de audio sigue siendo controlable por separado a través de \\\"Sonido activado\\\".\",\"No alternatives available.\":\"No hay alternativas disponibles.\",\"Similar models and offline alternatives\":\"Modelos similares y alternativas fuera de línea.\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Priorizado por etiquetas compartidas; sin identidad ni inferencia de cuenta externa.\",\"Chat\":\"Charla\",\"Notes & organization\":\"Notas y organización\",\"Images\":\"Imágenes\",\"SingleCam sections\":\"Secciones SingleCam\",\"Back to original results\":\"Volver a los resultados originales\",\"Private workspace\":\"Espacio de trabajo privado\",\"Chat on/off\":\"Activar/desactivar chat\",\"Hide similar models\":\"Ocultar modelos similares\",\"Show similar models\":\"Mostrar modelos similares\",\"RoomGrid Home\":\"Inicio RoomGrid\",\"RoomGrid Tags\":\"RoomGrid Etiquetas\",\"Public catalog unavailable; showing the local library.\":\"Catálogo público no disponible; mostrando la biblioteca local.\",\"Drag to resize sidebar\":\"Arrastra para cambiar el tamaño de la barra lateral\",\"The workspace could not be saved safely. Export a backup now.\":\"El espacio de trabajo no se pudo guardar de forma segura. Exporta una copia de seguridad ahora.\",\"Open menu or restore controls\":\"Abrir menú o restaurar controles\",\"Controls restored\":\"Controles restaurados\",\"Menu\":\"Menú\",\"Show toolbar\":\"Mostrar barra de herramientas\",\"Tools\":\"Herramientas\",\"Show sidebar\":\"Mostrar barra lateral\",\"Restore controls\":\"Restaurar controles\",\"Show toolbar, groups, and menus\":\"Mostrar barra de herramientas, grupos y menús\",\"Open more tools\":\"Abrir más herramientas\",\"all watch states\":\"todos los estados de vigilancia\",\"Watch status\":\"Estado del reloj\",\"0 means no viewer limit.\":\"0 significa que no hay límite de espectadores.\",\"Import temporary URLs; they disappear after refresh\":\"Importar URL temporales; desaparecen después de actualizar\",\"Filter current group\":\"Filtrar grupo actual\",\"All status\":\"Todo el estado\",\"Hide offline/private\":\"Ocultar sin conexión/privado\",\"Collapse toolbar\":\"Contraer barra de herramientas\",\"Windows per page\":\"ventanas por página\",\"Custom grid\":\"Cuadrícula personalizada\",\"Choose MultiCam audio routing\":\"Elija el enrutamiento de audio MultiCam\",\"Audio: manual\":\"Audio: manuales\",\"Audio: solo cam\":\"Audio: cámara en solitario\",\"Audio: follow focus\":\"Audio: sigue el enfoque\",\"Auto\":\"Automático\",\"Userscript version\":\"Versión de script de usuario\",\"Open Discover and dashboard\":\"Abra Discover y el panel\",\"Global search and quick commands · Ctrl+K\":\"Búsqueda global y comandos rápidos · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Estado de la transmisión, presupuesto de calidad, audio, escenas y reemplazos\",\"Control\":\"controlar\",\"Rooms\":\"Salas\",\"Filter\":\"Filtrar\",\"Playback\":\"Reproducción\",\"Stabilizing live preview…\":\"Estabilizando la vista previa en vivo…\",\"Recovering live preview…\":\"Recuperando vista previa en vivo…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"La vista previa en vivo se pausa brevemente para evitar un bucle de recarga. Vuelve a intentarlo en breve.\",\"Collapse groups\":\"Contraer grupos\",\"Expand/collapse users\":\"Expandir/contraer usuarios\",\"Output volume; no live signal level is available for this stream\":\"Volumen de salida; no hay nivel de señal en vivo disponible para esta transmisión\",\"OFF\":\"APAGADO\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Pegue las URL. Se admiten páginas de sala, m3u8, mp4/webm. Las páginas normales se analizarán con el mejor esfuerzo.\",\"Actual resolution, connection and buffer\":\"Resolución real, conexión y buffer.\",\"Drag to reorder / move group\":\"Arrastre para reordenar/mover grupo\",\"All time\":\"Todo el tiempo\",\"Click a model for details.\":\"Haga clic en un modelo para obtener más detalles.\",\"Most watched groups\":\"Grupos más vistos\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Los campos de la cuenta, como la fecha de inscripción, se muestran solo cuando Chaturbate los expone claramente en la misma página. El guión no inventa valores.\",\"Back\":\"Atrás\",\"Statistics import failed: \":\"Error al importar estadísticas:\",\"Recent saves\":\"Guardados recientes\",\"Rooms per page\":\"Habitaciones por página\",\"Custom rows\":\"Filas personalizadas\",\"Custom columns\":\"Columnas personalizadas\",\"Main width\":\"Ancho principal\",\"Main height\":\"Altura principal\",\"Modules\":\"Módulos\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Los módulos comienzan solo en tipos de páginas coincidentes. Al guardar se recarga la página actual.\",\"Tools on normal pages\":\"Herramientas en páginas normales\",\"True audio-level meters\":\"Verdaderos medidores de nivel de audio\",\"MultiCam control center\":\"Centro de control MultiCam\",\"No active streams yet\":\"Aún no hay transmisiones activas\",\"Stream health\":\"Estado de la transmisión\",\"Budget off\":\"Presupuesto fuera\",\"Economy · 2160p total\":\"Economía · 2160p total\",\"Balanced · 4320p total\":\"Equilibrado · 4320p total\",\"High · 6480p total\":\"Alto · 6480p total\",\"Ultra · 8640p total\":\"Ultra · 8640p en total\",\"Quality budget is off; global and per-camera limits still apply.\":\"El presupuesto de calidad está fuera de lugar; Se siguen aplicando límites globales y por cámara.\",\"Quality budget\":\"Presupuesto de calidad\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Distribuye calidad entre cámaras activas. Una calidad explícita por cámara siempre gana.\",\"Give the focused cam double weight\":\"Dale doble peso a la cámara enfocada\",\"Manual per camera\":\"Manual por cámara\",\"Solo: one camera only\":\"Solo: una sola cámara\",\"Follow focused camera\":\"Seguir cámara enfocada\",\"Audio management\":\"Gestión de audio\",\"Routing never overwrites saved mute states or individual volume.\":\"El enrutamiento nunca sobrescribe los estados de silencio guardados ni el volumen individual.\",\"Mute all\":\"Silenciar todo\",\"Restore saved audio\":\"Restaurar audio guardado\",\"Scene name\":\"Nombre de la escena\",\"Focus\":\"Enfoque\",\"Grid\":\"Cuadrícula\",\"cams\":\"cámaras\",\"Load\":\"Cargar\",\"No scenes saved for this workspace yet.\":\"Aún no hay escenas guardadas para este espacio de trabajo.\",\"Scenes\":\"Escenas\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Guarde las ranuras, el diseño y el enrutamiento de audio actuales para restaurarlos con un solo clic.\",\"Save current view as scene\":\"Guardar vista actual como escena\",\"All saved online models\":\"Todos los modelos online guardados\",\"Replacement models\":\"Modelos de repuesto\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Los modelos preferidos se prueban en orden de lista; sólo entonces se utilizan los modelos en línea del grupo seleccionado. Las tareas manuales siempre ganan.\",\"Replace offline slots automatically\":\"Reemplace las ranuras sin conexión automáticamente\",\"Delay (seconds)\":\"Retraso (segundos)\",\"Candidate group\":\"grupo candidato\",\"Preferred replacements (multiple)\":\"Reemplazos preferidos (múltiples)\",\"Replacement rule saved\":\"Regla de reemplazo guardada\",\"Save replacement rule\":\"Guardar regla de reemplazo\",\"Resolve duplicate shortcuts first:\":\"Primero resuelva los atajos duplicados:\",\"Name, note or own tag…\":\"Nombre, nota o etiqueta propia…\",\"Camera menu\":\"Menú de cámara\",\"Camera menu · quality and sound\":\"Menú cámara · calidad y sonido\",\"Close menu\":\"Cerrar menú\",\"View and window\":\"Vista y ventana\",\"Quality and image\":\"Calidad e imagen\",\"More actions\":\"Más acciones\",\"Currently main\":\"Actualmente principal\",\"Set as main\":\"Establecer como principal\",\"Pin live preview\":\"Fijar vista previa en vivo\",\"Volume multiplier for this cam\":\"Multiplicador de volumen para esta cámara\",\"Reset individual volume to 100%\":\"Restablecer el volumen individual al 100%\",\"Individual volume\":\"Volumen individual\",\"Listen to this cam only\":\"Escuche solo esta cámara\",\"Set quality for this camera only\":\"Establecer calidad solo para esta cámara\",\"Automatic (global/adaptive)\":\"Automático (global/adaptativo)\",\" · available\":\"· disponible\",\"Quality for this camera\":\"Calidad para esta cámara\",\"Hold for original\":\"Esperar para el original\",\"Vivid\":\"vívido\",\"Mono\":\"monocromático\",\"Image adjustments\":\"Ajustes de imagen\",\"More tools\":\"Más herramientas\",\"Interface\":\"Interfaz\",\"Show top controls\":\"Mostrar controles superiores\",\"Collapse top controls\":\"Contraer controles superiores\",\"Show groups\":\"Mostrar grupos\",\"Windows\":\"ventanas\",\"Open workspace in new window\":\"Abrir espacio de trabajo en una nueva ventana\",\"Control center\":\"centro de control\",\"Choose the data to export\":\"Elige los datos a exportar\",\"Preview the file and conflicts before applying it\":\"Obtenga una vista previa del archivo y los conflictos antes de aplicarlo.\",\"Help\":\"Ayuda\",\"Changes in this version\":\"Cambios en esta versión.\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Espacios de trabajo, biblioteca de modelos, importaciones seguras, diseño global, páginas normales extendidas, panel/Descubrimiento ampliado y controles de calidad/imagen del reproductor.\",\"Muted\":\"silenciado\",\"Performance limit reached\":\"Límite de rendimiento alcanzado\",\"Drag to resize main width\":\"Arrastre para cambiar el tamaño del ancho principal\",\"Drag to resize main height\":\"Arrastre para cambiar el tamaño de la altura principal\",\"Stable\":\"Estable\",\"Unstable\":\"inestable\",\"Refresh all cameras\":\"Actualizar todas las cámaras\",\"Reconnect current models\":\"Reconectar los modelos actuales\",\"Switch to grid view\":\"Cambiar a vista de cuadrícula\",\"Switch to focus view\":\"Cambiar a vista de enfoque\",\"Manage workspaces\":\"Administrar espacios de trabajo\",\"Create, duplicate, archive and device layouts\":\"Crear, duplicar, archivar y diseños de dispositivos\",\"Open model library\":\"Biblioteca de modelos abierta\",\"Notes, tags, ratings and bulk actions\":\"Notas, etiquetas, valoraciones y acciones masivas\",\"Design options\":\"Opciones de diseño\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Estación de trabajo multivista todo en uno para salas, grabación, alertas, capturas de pantalla, reconexiones y herramientas de datos.\",\"Username ↵\":\"Nombre de usuario ↵\",\"Search rooms\":\"Buscar habitaciones\",\"Invalid username\":\"Nombre de usuario no válido\",\"Hide offline\":\"Ocultar sin conexión\",\"Hide private\":\"Ocultar privado\",\"Manual\":\"manuales\",\"By status\":\"Por estado\",\"By name\":\"Por nombre\",\"By added time\":\"Por tiempo añadido\",\"By watch time\":\"Por tiempo de visualización\",\"By sessions\":\"Por sesiones\",\"By last watched\":\"Por última vez visto\",\"By first watched\":\"Por primera vez visto\",\"By viewer count\":\"Por recuento de espectadores\",\"By last online\":\"Por última vez en línea\",\"Advanced filters\":\"Filtros avanzados\",\"Watched before\":\"visto antes\",\"Currently in view\":\"Actualmente a la vista\",\"Include tag\":\"incluir etiqueta\",\"Exclude tag\":\"Excluir etiqueta\",\"Minimum watch time (minutes)\":\"Tiempo mínimo de visualización (minutos)\",\"Watched within days\":\"Visto en unos días\",\"Refresh all\":\"Actualizar todo\",\"Online alerts\":\"Alertas en línea\",\"Offline alerts\":\"Alertas sin conexión\",\"Desktop notification + card flash when a model goes online\":\"Notificación de escritorio + flash de tarjeta cuando un modelo se conecta\",\"Desktop notification + card flash when a model goes offline\":\"Notificación de escritorio + flash de tarjeta cuando un modelo se desconecta\",\"Dark mode\":\"modo oscuro\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Utilice un tema de interfaz oscuro. La transmisión de video y audio no se modifica.\",\"Dark mode enabled\":\"Modo oscuro habilitado\",\"Light mode enabled\":\"Modo de luz habilitado\",\"Mute token/tip sounds\":\"Silenciar sonidos de tokens y propinas\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Bloquea los sonidos de notificación de tokens y propinas. El audio del stream se controla por separado.\",\"Token/tip sounds muted\":\"Sonidos de tokens y propinas silenciados\",\"Token/tip sounds enabled\":\"Sonidos de tokens y propinas activados\",\"Clean\":\"limpio\",\"Clean mode on\":\"Modo limpio activado\",\"Exit clean mode\":\"Salir del modo limpio\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Oculta temporalmente todos los controles y superposiciones. Atajo: Alt+P / Alt+C. Presione Esc para salir.\",\"Window first\":\"ventana primero\",\"Window-first mode on\":\"Modo de primera ventana activado\",\"Exit window-first\":\"Salir de la ventana primero\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Oculte el shell de la aplicación y mantenga visibles las ventanas de la habitación. Atajo: Alt+V.\",\"Show thumbnails\":\"Mostrar miniaturas\",\"Hide thumbnails\":\"Ocultar miniaturas\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Muestra u oculta el carril de miniaturas en el modo Enfoque. Atajo: Alt+T.\",\"Choose Focus rooms\":\"Elija salas de enfoque\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Elija las habitaciones para el modo de enfoque. La primera sala seleccionada es la pantalla principal; utilice ↑/↓ para establecer el orden de las ventanas secundarias.\",\"Main screen\":\"Pantalla principal\",\"Right window\":\"ventana derecha\",\"Bottom window\":\"ventana inferior\",\"Empty slot\":\"Ranura vacía\",\"Drag this model to a Focus slot\":\"Arrastre este modelo a una ranura de enfoque\",\"Choose model for this slot\":\"Elige modelo para esta ranura\",\"Select a saved model for this exact Focus slot.\":\"Seleccione un modelo guardado para esta ranura de Focus exacta.\",\"Choose model for this window\":\"Elija modelo para esta ventana\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Seleccione un modelo guardado para esta ventana de cuadrícula exacta. Los modelos existentes permanecen guardados.\",\"Remove from this grid window\":\"Eliminar de esta ventana de cuadrícula\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Sólo esta ranura está libre. El modelo guardado permanece en su espacio de trabajo.\",\"Show in grid window\":\"Mostrar en ventana de cuadrícula\",\"Choose the exact active grid window for this model.\":\"Elija la ventana de cuadrícula activa exacta para este modelo.\",\"Current\":\"Actual\",\"Model removed from this grid window.\":\"Modelo eliminado de esta ventana de cuadrícula.\",\"No saved models in this group.\":\"No hay modelos guardados en este grupo.\",\"Secondary window\":\"ventana secundaria\",\"Use current page automatically\":\"Usar la página actual automáticamente\",\"Clear custom selection\":\"Borrar selección personalizada\",\"Apply Focus layout\":\"Aplicar diseño de enfoque\",\"No rooms are available in the current group and filter.\":\"No hay habitaciones disponibles en el grupo y filtro actual.\",\"Fit: full image\":\"Ajuste: imagen completa\",\"Fit: fill window\":\"Ajustar: llenar ventana\",\"Switch between full image and cropped fill.\":\"Cambia entre imagen completa y relleno recortado.\",\"Playback settings\":\"Configuración de reproducción\",\"Max stream quality\":\"Calidad de transmisión máxima\",\"Auto / no cap\":\"Automático/sin límite\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Comando + zoom de rueda\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Haga zoom en una tarjeta con Ctrl/Comando + rueda del mouse, arrastre mientras está ampliada, haga doble clic para restablecer.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Salga del modo de limpieza. Atajo: Alt+P / Alt+C o Esc.\",\"Clean · Alt+P/C / Esc\":\"Limpiar · Alt+P/C / Esc\",\"(synced from another tab)\":\"(sincronizado desde otra pestaña)\",\"Type a username and press Enter. Shortcut: /\":\"Escriba un nombre de usuario y presione Entrar. Atajo: /\",\"Filter visible rooms by username\":\"Filtrar salas visibles por nombre de usuario\",\"Show / hide group sidebar\":\"Mostrar/ocultar barra lateral del grupo\",\"Global volume. Per-room mute is still respected.\":\"Volumen mundial. Todavía se respeta el silencio por habitación.\",\"Base tile size in grid view\":\"Tamaño del mosaico base en vista de cuadrícula\",\"Sort rooms in the current group\":\"Ordenar habitaciones en el grupo actual\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Altura de la pantalla principal en modo Focus. También puedes arrastrar el separador.\",\"Main-screen aspect ratio in Focus mode\":\"Relación de aspecto de la pantalla principal en el modo de enfoque\",\"Thumbnail width in Focus mode\":\"Ancho de miniatura en modo de enfoque\",\"Show every saved room. The remove button deletes globally in this view.\":\"Muestra todas las habitaciones guardadas. El botón Eliminar elimina globalmente en esta vista.\",\"Create an isolated group\":\"Crear un grupo aislado\",\"More tools and maintenance\":\"Más herramientas y mantenimiento\",\"GROUPS\":\"GRUPOS\",\"All saved\":\"Todos guardados\",\"Default\":\"Predeterminado\",\"Online now\":\"En línea ahora\",\"New group name:\":\"Nuevo nombre del grupo:\",\"Rename\":\"Cambiar nombre\",\"Rename to:\":\"Cambiar el nombre a:\",\"Delete group\":\"Eliminar grupo\",\"Live preview\":\"Vista previa en vivo\",\"Loading preview…\":\"Cargando vista previa…\",\"Private or away\":\"Privado o ausente\",\"Preview could not be loaded\":\"No se pudo cargar la vista previa\",\"Toggle mute\":\"Alternar silencio\",\"Resume\":\"Currículum\",\"Screenshot current frame\":\"Captura de pantalla del fotograma actual\",\"Record current card\":\"Registrar tarjeta actual\",\"Stop recording\":\"dejar de grabar\",\"Picture-in-Picture\":\"Imagen en imagen\",\"Remove from current group\":\"Eliminar del grupo actual\",\"Delete saved room\":\"Eliminar sala guardada\",\"Mirror image\":\"Imagen reflejada\",\"Flip image\":\"Voltear imagen\",\"Rotate left\":\"Girar a la izquierda\",\"Rotate right\":\"Girar a la derecha\",\"Reset view\":\"Restablecer vista\",\"Copy username\":\"Copiar nombre de usuario\",\"Add favorite\":\"Agregar favorito\",\"Move to favorites only\":\"Mover solo a favoritos\",\"Copied\":\"Copiado\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"La captura de pantalla falló. El navegador/CORS puede bloquear el dibujo de esta transmisión.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"La grabación es local y guarda este video con su audio correspondiente cuando el navegador lo expone. Úselo solo cuando tenga permiso para guardar este contenido. ¿Continuar?\",\"Recording started\":\"Grabación iniciada\",\"Recording is not supported for this video/browser\":\"La grabación no es compatible con este vídeo/navegador\",\"Recording segment saved\":\"Segmento de grabación guardado\",\"Final recording segment saved\":\"Segmento de grabación final guardado\",\"Recording paused; waiting for the stream to return\":\"Grabación en pausa; esperando que regrese la corriente\",\"Recording resumed\":\"Grabación reanudada\",\"Recording paused; click to stop\":\"Grabación en pausa; haga clic para detener\",\"No recording data to save\":\"No hay datos de grabación para guardar\",\"Recording settings saved\":\"Configuración de grabación guardada\",\"Segment length in minutes (1-180):\":\"Longitud del segmento en minutos (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Velocidad de bits de vídeo en Mbps (0,5-20):\",\"Warn before leaving while recording\":\"Avisar antes de salir mientras grabas\",\"Recording is still active. Leave anyway?\":\"La grabación aún está activa. ¿Irse de todos modos?\",\"Recording center\":\"centro de grabación\",\"No active recordings\":\"No hay grabaciones activas\",\"Waiting for source\":\"esperando fuente\",\"Saved segments\":\"Segmentos guardados\",\"Duration\":\"Duración\",\"Bitrate\":\"tasa de bits\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Formato: MP4 cuando sea compatible con este navegador; de lo contrario, es el recurso alternativo con mejor soporte.\",\"Record current page\":\"Grabar página actual\",\"Record current group\":\"Grabar grupo actual\",\"Record online rooms\":\"Grabar salas en línea\",\"Stop all recordings\":\"Detener todas las grabaciones\",\"Show recording only\":\"Mostrar solo grabación\",\"Show all rooms\":\"Mostrar todas las habitaciones\",\"Open current page rooms\":\"Abrir salas de la página actual\",\"Move current page to group\":\"Mover la página actual al grupo\",\"Layout settings\":\"Configuración de diseño\",\"Recording settings\":\"Configuración de grabación\",\"Save settings\":\"Guardar configuración\",\"Config backups\":\"Configurar copias de seguridad\",\"No config backups found\":\"No se encontraron copias de seguridad de configuración\",\"Online/offline history\":\"Historial en línea/fuera de línea\",\"Group rules\":\"Reglas del grupo\",\"Keep favorites first\":\"Mantenga los favoritos primero\",\"Temporary URL manager\":\"Administrador de URL temporal\",\"Save current temporary URLs\":\"Guardar las URL temporales actuales\",\"No temporary URLs\":\"Sin URL temporales\",\"Share workspace\":\"Compartir espacio de trabajo\",\"Share current rooms and layout\":\"Compartir habitaciones y distribución actuales\",\"Copy share link\":\"Copiar enlace para compartir\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Este enlace restaura salas, grupos y configuraciones de diseño en otra pestaña de la estación de trabajo.\",\"Shared workspace imported\":\"Espacio de trabajo compartido importado\",\"Shared workspace link is invalid\":\"El enlace del espacio de trabajo compartido no es válido\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Haga clic en un campo y presione el nuevo acceso directo. La tecla de retroceso borra ese campo.\",\"Reset shortcuts\":\"Restablecer atajos\",\"Focus add box\":\"Cuadro de enfoque para agregar\",\"Grid view\":\"Vista de cuadrícula\",\"Focus view\":\"Vista de enfoque\",\"Clean mode\":\"Modo limpio\",\"Window-first mode\":\"Modo de ventana primero\",\"Toggle thumbnails\":\"Alternar miniaturas\",\"Auto-detecting…\":\"Detección automática…\",\"No rooms in this group\":\"No hay habitaciones en este grupo.\",\"Add a username at the top, switch group, or clear filters\":\"Agregue un nombre de usuario en la parte superior, cambie de grupo o borre filtros\",\"Open workstation\":\"Estación de trabajo abierta\",\"Open here (replace page)\":\"Abrir aquí (reemplazar página)\",\"Replace this page with the workstation? You can go back via the browser.\":\"¿Reemplazar esta página con la estación de trabajo? Puedes volver a través del navegador.\",\"View saved list\":\"Ver lista guardada\",\"Collapse dock\":\"Muelle colapsado\",\"No room detected\":\"No se detectó ninguna habitación\",\"Open here\":\"Abrir aquí\",\"Add room\":\"Añadir habitación\",\"Remove room\":\"Quitar habitación\",\"Record in workstation\":\"Grabar en la estación de trabajo\",\"Screenshot video\":\"Vídeo de captura de pantalla\",\"Play / pause\":\"Reproducir/pausar\",\"Mute / unmute\":\"Silenciar/activar silencio\",\"No playable video found on this page\":\"No se encontró ningún vídeo reproducible en esta página.\",\"Recording intent queued in workstation\":\"Intento de grabación en cola en la estación de trabajo\",\"Added\":\"Añadido\",\"Already exists\":\"Ya existe\",\"Failed\":\"Fallido\",\"Add to workstation\":\"Agregar a la estación de trabajo\",\"Already in workstation — click to remove\":\"Ya en la estación de trabajo: haga clic para eliminar\",\"Model online\":\"Modelo en línea\",\"Model offline\":\"Modelo fuera de línea\",\"Show fewer\":\"Mostrar menos\",\"No measurable audio at the moment\":\"No hay audio medible en este momento.\",\"Playback paused\":\"Reproducción en pausa\",\"Audio measurement unavailable or blocked\":\"Medición de audio no disponible o bloqueada\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Permiso de notificación denegado. La tarjeta flash todavía funciona.\\n\\nPuede habilitarlo en la configuración del navegador.\",\"Batch add\":\"Agregar lote\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Pegue los nombres de usuario, uno por línea o separados por espacios/comas:\",\"Cancel\":\"Cancelar\",\"Change the language of RoomGrid and the website\":\"Cambiar el idioma de RoomGrid y del sitio web\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Cambia el idioma de RoomGrid. Chaturbate permanece en el idioma actual del sitio web porque no hay ningún idioma oficial disponible.\",\"About\":\"Acerca de\",\"Export config\":\"Exportar configuración\",\"Export usernames.txt\":\"Exportar nombres de usuario.txt\",\"Copy usernames\":\"Copiar nombres de usuario\",\"Unmute all\":\"Dejar de silenciar a todos\",\"⏸ Pause visible\":\"⏸ Pausa visible\",\"Resume visible\":\"Currículum visible\",\"Following\":\"Siguiendo\",\"Sync followed models now\":\"Sincronizar modelos seguidos ahora\",\"✓ Auto-sync followed models\":\"✓ Sincronización automática de modelos seguidos\",\"☐ Auto-sync followed models\":\"☐ Sincronización automática de modelos seguidos\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importa los modelos de Chaturbate que sigues actualmente al grupo Siguiendo persistente. Sólo ese grupo está sincronizado.\",\"Syncing followed models…\":\"Sincronizando modelos seguidos...\",\"Automatic Following sync disabled\":\"Sincronización automática de seguimiento desactivada\",\"Automatic Following sync enabled\":\"Sincronización automática de seguimiento habilitada\",\"Following sync needs an active Chaturbate login\":\"La siguiente sincronización necesita un inicio de sesión activo en Chaturbate\",\"Following sync failed safely; saved workspace data was not changed\":\"La siguiente sincronización falló de forma segura; Los datos guardados del espacio de trabajo no fueron cambiados.\",\"Enable dark mode\":\"Habilitar el modo oscuro\",\"Disable dark mode\":\"Desactivar el modo oscuro\",\"Status history\":\"Historial de estado\",\"Temporary URLs\":\"URL temporales\",\"Shortcut panel\":\"Panel de acceso directo\",\"Toggle video fit\":\"Alternar ajuste de vídeo\",\"Shortcuts / hints\":\"Atajos/sugerencias\",\"Ranking\":\"Clasificación\",\"Model details\":\"Detalles del modelo\",\"My profile\":\"mi perfil\",\"Loading local statistics…\":\"Cargando estadísticas locales…\",\"No watch data yet. Statistics start with this version.\":\"Aún no hay datos de visualización. Las estadísticas comienzan con esta versión.\",\"Statistics are disabled.\":\"Las estadísticas están deshabilitadas.\",\"Enable statistics\":\"Habilitar estadísticas\",\"Disable statistics\":\"Desactivar estadísticas\",\"Export statistics\":\"Estadísticas de exportación\",\"Delete all statistics\":\"Eliminar todas las estadísticas\",\"Delete this model data\":\"Eliminar los datos de este modelo\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"¿Eliminar todas las estadísticas de visualización recopiladas localmente? Su espacio de trabajo permanece intacto.\",\"Tracking since\":\"Seguimiento desde\",\"Models watched\":\"Modelos vistos\",\"Watch time\":\"tiempo de visualización\",\"Most watched tags\":\"Etiquetas más vistas\",\"Most watched hours\":\"Horas más vistas\",\"Observed online\":\"Observado en línea\",\"Public bio\":\"Biografía pública\",\"Public links\":\"Enlaces públicos\",\"No public profile data cached yet.\":\"Aún no hay datos de perfil público almacenados en caché.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Sólo cuentan las cartas visibles que se juegan activamente. Las vistas previas al pasar el mouse no cuentan.\",\"Statistics imported\":\"Estadísticas importadas\",\"Statistics deleted\":\"Estadísticas eliminadas\",\"Statistics exported\":\"Estadísticas exportadas\",\"Local observation only\":\"Sólo observación local\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"No se recopilan mensajes privados, tokens, pagos, contraseñas ni seguimiento externo.\",\"Not enough observations for a time estimate\":\"No hay suficientes observaciones para una estimación del tiempo.\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Pase el cursor sobre cualquier botón/control para ver su pista. Los atajos comunes se pueden editar en el panel de atajos.\\n\\nValores predeterminados:\\n/ Entrada de nombre de usuario de enfoque\\nActualizar todo\\ng Vista de cuadrícula\\nf Vista de enfoque\\nAlt+P o Alt+C Modo de limpieza activado/desactivado\\nAlt+Mayús+C Centro de grabación\\nAlt+Mayús+R Grabar página actual\\nEsc Salir del modo limpio/pantalla completa\\nEspacio Pausa/reanuda la pantalla principal en la vista Enfoque\\n←/→ o [/ ] Cambiar la pantalla principal en la vista de enfoque\\nTarjeta de doble clic Pantalla completa\",\"Visible windows paused\":\"Ventanas visibles en pausa\",\"Visible windows resumed\":\"Se reanudaron las ventanas visibles\",\"Import config\":\"Importar configuración\",\"Repair saved data\":\"Reparar datos guardados\",\"Saved data has been normalized.\":\"Los datos guardados se han normalizado.\",\"Clear all data\":\"Borrar todos los datos\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Esto borrará TODAS las habitaciones, grupos y configuraciones. ¿Continuar?\",\"English\":\"ingles\",\"Deutsch\":\"alemán\",\"Author\":\"Autor\",\"License\":\"Licencia\",\"If this script saves you time…\":\"Si este script te ahorra tiempo...\",\"Tip in ETH\":\"Propina en ETH\",\"ETH address\":\"dirección ETH\",\"Copy\":\"Copiar\",\"Live for a short time and already drawing public activity.\":\"Vive por poco tiempo y ya atrae actividad pública.\",\"Live stream audio activity\":\"Actividad de audio de transmisión en vivo\",\"Local watch\":\"vigilancia local\",\"Logo background\":\"Fondo del logotipo\",\"Logo height\":\"Altura del logotipo\",\"Logo width\":\"Ancho del logotipo\",\"Loud\":\"ruidoso\",\"Measured even while output is muted\":\"Medido incluso cuando la salida está silenciada\",\"Middle-click or Ctrl+click opens a new tab\":\"Hacer clic con el botón central o Ctrl+clic abre una nueva pestaña\",\"Models you have visited locally and that are public again now.\":\"Modelos que has visitado localmente y que ahora vuelven a ser públicos.\",\"New discoveries\":\"Nuevos descubrimientos\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nuevo en el catálogo, recién visto por Camclient o recién comenzado a transmitir.\",\"No audio track\":\"Sin pista de audio\",\"No filters active · all matching live models are shown\":\"No hay filtros activos · se muestran todos los modelos en vivo coincidentes\",\"No recommendations match the active filters.\":\"Ninguna recomendación coincide con los filtros activos.\",\"No recording data\":\"Sin datos de grabación\",\"Accent gradient\":\"gradiente de acento\",\"Active filters\":\"Filtros activos\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Agrega tus propias imágenes o usa el botón de captura de pantalla en el reproductor. Las capturas de pantalla pueden ingresar a la galería automáticamente y pueden usarse directamente como portada del modelo.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Todos los filtros de la página de inicio, diseño, vista previa y configuraciones de paginación se almacenan en la bóveda de configuración estable y sobreviven a las actualizaciones de script.\",\"All live models\":\"Todos los modelos en vivo\",\"All suggestions\":\"Todas las sugerencias\",\"Almost silent\":\"Casi en silencio\",\"Always active\":\"Siempre activo\",\"AND\":\"Y\",\"Automatic example\":\"Ejemplo automático\",\"Automatic segments\":\"Segmentos automáticos\",\"Automatically loading the complete catalog…\":\"Cargando automáticamente el catálogo completo…\",\"Available recording data was saved.\":\"Se guardaron los datos de grabación disponibles.\",\"Back online\":\"De nuevo en línea\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Basado en sus favoritos, etiquetas y actividad de visualización guardados localmente. Cada elección muestra su motivo.\",\"Black\":\"negro\",\"Brave exposed no recordable video track.\":\"Brave no expuso ninguna pista de vídeo grabable.\",\"Brave or the video source blocked frame capture.\":\"Brave o la fuente de vídeo bloqueó la captura de fotogramas.\",\"Browser exposed no audio track\":\"El navegador no muestra ninguna pista de audio\",\"Camclient\":\"cliente cam\",\"Camclient as the real homepage\":\"Camclient como página de inicio real\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient asigna estos ejemplos transparentes a etiquetas comunes automáticamente. Utilice el botón de imagen en cualquier etiqueta para elegir un ejemplo, cargar su propio PNG/WebP/SVG u ocultar la imagen.\",\"Choose a Camclient example or upload your own transparent image.\":\"Elija un ejemplo de Camclient o cargue su propia imagen transparente.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Elija un PNG, JPEG, WebP o SVG seguro de menos de 1,1 MB.\",\"Choose artwork for tag\":\"Elija ilustraciones para la etiqueta\",\"Click to enable meter\":\"Haga clic para habilitar el medidor\",\"Close more actions\":\"Cerrar más acciones\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Los colores, la tipografía, el espaciado, las esquinas, las sombras y los fondos se aplican inmediatamente a la Configuración, el espacio de trabajo, Discover y la biblioteca de modelos.\",\"Continue loading the complete catalog\":\"Continuar cargando el catálogo completo\",\"Continuing recording\":\"Grabación continua\",\"current matches\":\"partidos actuales\",\"Audio\":\"Audio\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Personalice el nombre, la marca, el eslogan y el logotipo local de Camclient. Los logotipos permanecen en este navegador.\",\"Discover something new\":\"Descubre algo nuevo\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Los carriles de recomendación distintos le ayudan a encontrar algo nuevo en lugar de repetir el catálogo de la página de inicio.\",\"Do not enlarge\":\"no ampliar\",\"Enable meter\":\"Habilitar medidor\",\"Fast starters\":\"entrantes rápidos\",\"Fill and crop\":\"Rellenar y recortar\",\"Filter suggestions\":\"Filtrar sugerencias\",\"Fit completely\":\"Encajar completamente\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Para transparencia, utilice PNG, WebP o SVG seguro y elija Transparente como fondo del logotipo. JPEG no tiene transparencia. Stretch utiliza el ancho y alto elegidos de forma independiente.\",\"For you\":\"para ti\",\"gallery\":\"galería\",\"HD\":\"alta definición\",\"Inner spacing\":\"Espaciado interior\",\"Inspiration, not repetition\":\"Inspiración, no repetición\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Permite usar, modificar y distribuir el software. Los avisos de licencia y derechos de autor deben permanecer y el software viene sin garantía. Camclient es escrito por Emberhost.\",\"NOT\":\"NO\",\"Only new\":\"Sólo nuevo\",\"Open Camclient homepage\":\"Abrir la página de inicio de Camclient\",\"OR\":\"O\",\"part\":\"parte\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Selecciones personales, nuevos descubrimientos, salas en vivo populares y modelos que nunca ha visitado, claramente separados del catálogo completo de la página de inicio.\",\"Place\":\"Lugar\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP y SVG seguro admiten transparencia. Las imágenes cambian de tamaño localmente a 144 × 144 píxeles y se guardan con su configuración.\",\"Popular now\":\"Popular ahora\",\"Public live models with no local watch time in Camclient.\":\"Modelos públicos en vivo sin tiempo de visualización local en Camclient.\",\"Quiet\":\"Tranquilo\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Clasificado según el recuento actual de espectadores públicos; Los datos del catálogo no se almacenan permanentemente.\",\"Recording is still active.\":\"La grabación aún está activa.\",\"Recording quality\":\"Calidad de grabación\",\"Recording stopped\":\"Grabación detenida\",\"Recording unavailable\":\"Grabación no disponible\",\"Remix\":\"remezclar\",\"Reset all\":\"Restablecer todo\",\"RoomGrid Model library\":\"Biblioteca de modelos RoomGrid\",\"RoomGrid My user\":\"RoomGrid Mi usuario\",\"RoomGrid Statistics\":\"Estadísticas de RoomGrid\",\"Save to model gallery\":\"Guardar en la galería de modelos\",\"Saved segment\":\"Segmento guardado\",\"Screenshot\":\"Captura de pantalla\",\"Screenshot failed\":\"Error en la captura de pantalla\",\"Screenshot format\":\"Formato de captura de pantalla\",\"Screenshot resolution\":\"Resolución de captura de pantalla\",\"Screenshot unavailable\":\"Captura de pantalla no disponible\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Las capturas de pantalla se descargan y también pueden ingresar a la galería de modelos local.\",\"segment(s)\":\"segmento(s)\",\"Show no image\":\"No mostrar imagen\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Muestra la actividad en la pista de audio de la transmisión independientemente de si la salida del reproductor está silenciada.\",\"Source ended\":\"Fuente terminada\",\"Source resolution\":\"Resolución de origen\",\"Start recording\":\"Empezar a grabar\",\"Starter pick · popular now\":\"Selección inicial · popular ahora\",\"Stop and save\":\"Detener y guardar\",\"Stretch\":\"Estirar\",\"Tag artwork and emoji\":\"Etiquetar ilustraciones y emoji\",\"Tag artwork editor\":\"Editor de ilustraciones de etiquetas\",\"The compressed image is still too large.\":\"La imagen comprimida todavía es demasiado grande.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"El encabezado solo muestra Inicio, Descubrir, Explorador de etiquetas, Centro de estadísticas, Biblioteca de modelos, Mi usuario y Configuración. Cada área de configuración está disponible como una subpágina de Configuración.\",\"The image could not be decoded.\":\"La imagen no se pudo decodificar.\",\"The image could not be read.\":\"La imagen no se pudo leer.\",\"The image format is unsafe or unsupported.\":\"El formato de imagen no es seguro o no es compatible.\",\"The lanes respect your active search and filter settings.\":\"Los carriles respetan su configuración de búsqueda y filtrado activo.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"La grabación se guarda solo en este dispositivo. Continúe solo si tiene permiso para guardar este contenido.\",\"The video has no usable frame yet.\":\"El vídeo aún no tiene fotogramas utilizables.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Aún no hay suficientes datos de preferencias locales, por lo que estas son selecciones iniciales transparentes basadas en la popularidad actual.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Estas configuraciones controlan el diseño de la página de inicio de Camclient, la paginación, la información del modelo, los filtros y el rendimiento.\",\"Transparent\":\"Transparente\",\"Unified main navigation\":\"Navegación principal unificada\",\"Unvisited\":\"no visitado\",\"Upload own image\":\"Subir imagen propia\",\"URL\":\"URL\",\"Use as model cover\":\"Usar como cubierta modelo\",\"Use the complete search, filters, and sorting for every current result.\":\"Utilice la búsqueda, los filtros y la clasificación completos para cada resultado actual.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Al visitar la página de inicio de Chaturbate siempre se abre la página de inicio completa de Camclient. Cada área principal permanece dentro de la misma interfaz personalizada.\",\"Waiting for audio\":\"esperando audio\",\"What does the MIT license mean?\":\"¿Qué significa la licencia MIT?\",\"White\":\"blanco\",\"Your discovery mix\":\"Tu mezcla de descubrimiento\",\"中文\":\"中文\",\"Trans\":\"Trans\",\"SingleCam\":\"SingleCam\",\"Country, region or location\":\"País, región o ubicación\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Controla el orden de los modelos, los filtros, la cantidad por página, el tamaño de las tarjetas y la información visible directamente en la página de inicio original de Chaturbate. Cada cambio se aplica de inmediato y se guarda en este navegador.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"Hace [[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] preview\":\"[[ROOMGRID_VALUE_0]] vista previa\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] perfil guardado\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Detalles del modelo para @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] modelos seguidos importados\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"[[ROOMGRID_VALUE_0]] Artículos de Mi colección reconocidos\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"[[ROOMGRID_VALUE_0]] de [[ROOMGRID_VALUE_1]] modelos actualmente cargados visibles.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Filtro de grupo activo: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] de [[ROOMGRID_VALUE_2]] tarjetas visibles coinciden ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Portada personalizada · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Portada guardada localmente · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Imágenes de ejemplo y portadas ([[ROOMGRID_VALUE_0]] guardadas)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Almacenado permanentemente en este dispositivo · último [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Imagen propia · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Guardado [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Visto por primera vez: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Visto por última vez: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Último directo: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Apariciones: [[ROOMGRID_VALUE_0]]× · primera [[ROOMGRID_VALUE_1]] · última [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Hora típica: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Técnico: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] observaciones locales en vivo, como máximo una cada 5 minutos\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] de tu tiempo de visualización anterior como estimación cautelosa\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Eliminar etiqueta [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modelo[[ROOMGRID_VALUE_1]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Administrar grupos personalizados · asignados a [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"¿Eliminar el grupo “[[ROOMGRID_VALUE_0]]”? Los modelos permanecerán en su biblioteca.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Grupo “[[ROOMGRID_VALUE_0]]” eliminado\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] grupos · selección múltiple posible\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] etiquetas guardadas\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Quitar filtro [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] coincidencias\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtros activos\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · La información del perfil público se puede cargar a través de \\\"Todos los detalles\\\".\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Organizado personalmente desde: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Catalogado automáticamente desde: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Última actividad: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Apariciones: [[ROOMGRID_VALUE_0]]× · última grabación [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Descubierto: [[ROOMGRID_VALUE_0]] · mediciones de calidad: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Información del perfil: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] archivado\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] restaurado\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Página [[ROOMGRID_VALUE_0]] de [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modelos\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] guardado\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] visto\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] activo · crece automáticamente\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] modelos · [[ROOMGRID_VALUE_1]] sesiones\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favoritos\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] modelos distintos\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] vistas previas\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Vista previa/vista rápida de los modelos [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] días\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"[[ROOMGRID_VALUE_0]] racha de días\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] visto repetidamente\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] días de datos locales\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"promedio ponderado [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] en vivo ahora\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modelos reconocidos\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"No se pudo leer todo: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Día más activo: [[ROOMGRID_VALUE_0]] con [[ROOMGRID_VALUE_1]]. Retención: [[ROOMGRID_VALUE_2]] días para eventos sin formato; Los valores más antiguos pueden permanecer como totales diarios.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sesiones\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] guardado localmente\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"No se pudieron cargar los datos locales: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] está en línea.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] está desconectado.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] no ha sido observado en línea por un tiempo.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favoritos están en línea juntos.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"El perfil público de [[ROOMGRID_VALUE_0]] cambió.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] modelos actualizados\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Vista previa de importación de estadísticas (nada ha cambiado todavía):\\n[[ROOMGRID_VALUE_0]] modelos\\n[[ROOMGRID_VALUE_1]] eventos\\n[[ROOMGRID_VALUE_2]] perfiles públicos\\n\\nContinuar reemplaza las estadísticas existentes.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Vista previa en vivo de [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] agregado a MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"¿Eliminar [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"¿Importar [[ROOMGRID_VALUE_0]] vistas y [[ROOMGRID_VALUE_1]] feeds?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] habitaciones agregadas al espacio de trabajo\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modelos guardados\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] resultados coincidentes\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] modelos en vivo coincidentes\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Comparte #[[ROOMGRID_VALUE_0]] con un favorito\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] imágenes agregadas; [[ROOMGRID_VALUE_1]] no se pudo procesar.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] imágenes agregadas.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] habitaciones\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] nombres de usuario válidos\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Página [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] habitaciones\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"URL temporales: [[ROOMGRID_VALUE_0]] agregadas, [[ROOMGRID_VALUE_1]] fallidas\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Vista previa de importación de estadísticas: [[ROOMGRID_VALUE_0]] modelos y [[ROOMGRID_VALUE_1]] eventos. Continuar fusiona de forma segura los datos y crea una copia de seguridad primero.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Los diseños personalizados admiten hasta [[ROOMGRID_VALUE_0]] habitaciones.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Escena “[[ROOMGRID_VALUE_0]]” guardada\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Escena “[[ROOMGRID_VALUE_0]]” cargada\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] transmisiones activas · [[ROOMGRID_VALUE_1]] inestable · [[ROOMGRID_VALUE_2]] para mirar\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] transmisiones · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p asignado\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"¿Eliminar escena “[[ROOMGRID_VALUE_0]]”?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Guardado solo para [[ROOMGRID_VALUE_0]]: hasta [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Esta cámara utiliza calidad global/adaptativa[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] fue reemplazado por [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Reproduciendo audio solo desde [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Colocado en la ranura de enfoque [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Colocado en la ventana de cuadrícula [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Resolución [[ROOMGRID_VALUE_0]], estado [[ROOMGRID_VALUE_1]], búfer [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Estado de la transmisión [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Quitar filtro: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Coincide con tus intereses: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catálogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] recuperado · [[ROOMGRID_VALUE_2]] modelos en vivo\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modelos locales\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catálogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modelos en vivo\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] salas de catálogo cargadas\"},\"visible\":{\"Gruppe löschen\":\"Eliminar grupo\",\"Gruppen umbenennen oder löschen\":\"Cambiar el nombre o eliminar grupos\",\"HD + Gaming\":\"Juegos HD+\",\"IndexedDB\":\"DB indexado\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Cada etiqueta guardada está disponible automáticamente en la búsqueda, en el autocompletado y aquí como un filtro múltiple directo.\",\"Keine Models passen zu diesen Filtern.\":\"Ningún modelo coincide con estos filtros.\",\"Keine Priorität\":\"No es una prioridad\",\"Konto-Daten werden eingelesen …\":\"Los detalles de la cuenta se están leyendo en...\",\"Lautstärke pro Model merken\":\"Volumen de notas por modelo\",\"Layout & sichtbare Bereiche\":\"Diseño y áreas visibles\",\"Letzte Messung\":\"Última medición\",\"LIVE\":\"EN VIVO\",\"lokal · für Suche und Filter\":\"local · para búsqueda y filtros\",\"Maximale Stream-Lautstärke\":\"Volumen máximo de transmisión\",\"Model besitzt alle gewählten Tags\":\"El modelo posee todas las etiquetas seleccionadas\",\"Model besitzt mindestens einen gewählten Tag\":\"El modelo tiene al menos una etiqueta seleccionada\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"La conexión actual está activa; Después de la siguiente medición, aquí aparece el rumbo local.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Esta vista de biblioteca todavía está vacía.\",\"Eigene Priorität\":\"Prioridad propia\",\"Eigene Tags\":\"Etiquetas propias\",\"Eigene Tags filtern\":\"Filtra tus propias etiquetas\",\"Eigenen Tag eingeben …\":\"Introduzca su propio día...\",\"Einstellungen\":\"Configuración\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Ingresar o \\\"Agregar etiqueta\\\" se hace cargo del borrador. Sólo se guarda permanentemente con “Guardar perfil”. Las etiquetas ya utilizadas se sugieren a medida que escribe.\",\"Ereignisse im Zeitraum\":\"Eventos en el periodo\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Cree colecciones con nombres libres, asigne modelos a múltiples grupos y muestre uno o más grupos juntos aquí.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Primero crea tu propio grupo arriba.\",\"Follower\":\"Seguidores\",\"GAMING\":\"JUEGOS\",\"Gesamter Verlauf\":\"Curso completo\",\"Grid +\":\"Cuadrícula+\",\"Größe der Bedienelemente\":\"Tamaño de los controles\",\"Offizielle Statistik verbinden\":\"Conectar estadísticas oficiales\",\"Optionale offizielle Kontostatistik\":\"Estadísticas de cuentas oficiales opcionales\",\"Persönlich organisiert\":\"Organizado personalmente\",\"PiP\":\"pepita\",\"Player & Verbindung\":\"Reproductor y conexión\",\"Player neu verbinden\":\"Reconectar jugador\",\"Player-Ecken\":\"Esquinas del jugador\",\"Player-Hintergrund\":\"Antecedentes del jugador\",\"Player-Höhe\":\"Altura del jugador\",\"Player-Seitenverhältnis\":\"Relación de aspecto del jugador\",\"Position der Informationen\":\"Ubicación de la información\",\"Private Notiz\":\"nota privada\",\"Private Notizen anzeigen\":\"Ver notas privadas\",\"Puffergröße\":\"Tamaño del búfer\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Model ist in allen gewählten Gruppen\":\"El modelo está en todos los grupos seleccionados.\",\"Model ist in mindestens einer gewählten Gruppe\":\"El modelo está en al menos un grupo seleccionado.\",\"MultiCam\":\"Multicámara\",\"MultiCam +\":\"Multicámara+\",\"My Collection öffnen\":\"Abrir mi colección\",\"Namen speichern\":\"Guardar nombre\",\"Nicht verbunden\":\"No conectado\",\"Niedrige Live-Latenz\":\"Baja latencia en vivo\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Todavía no hay suficientes valores de uso locales.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Ningún grupo propio todavía. Ingresa un nombre arriba y crea tu primera colección.\",\"Noch keine Model-Sehzeit erfasst.\":\"Aún no se ha registrado el tiempo de visualización del modelo.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Aún no hay instantáneas de la cuenta oficial.\",\"Noch nicht eingelesen\":\"Aún no leído\",\"Nur auf diesem Gerät\":\"Solo en este dispositivo\",\"Öffentliche/erkannte Tags\":\"Etiquetas públicas/reconocidas\",\"Öffentliches Profil anzeigen\":\"Ver perfil público\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid no puede adivinar los valores de la cuenta personal de Chaturbate. Estas tarjetas sólo aparecen si has vinculado conscientemente las estadísticas oficiales de la emisora ​​a tu propio nombre de usuario y token.\",\"RoomGrid Suite\":\"Suite RoomGrid\",\"Seitengröße\":\"Tamaño de página\",\"SingleCam\":\"Cámara única\",\"SingleCam einstellen\":\"Configurar SingleCam\",\"SingleCam Pro\":\"Cámara única Pro\",\"SingleCam-Standard wiederherstellen\":\"Restaurar el valor predeterminado de SingleCam\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Tan pronto como guardes tu propia etiqueta para un modelo, aparecerá aquí y en el autocompletado.\",\"Status im Player anzeigen\":\"Mostrar estado en el reproductor\",\"Status-Aktualisierung\":\"Actualización de estado\",\"Stream immer stumm starten\":\"Iniciar siempre la transmisión en silencio\",\"Tag hinzufügen\":\"Agregar etiqueta\",\"Tipps letzte Stunde\":\"Consejos de última hora\",\"Token-Guthaben\":\"Saldo de tokens\",\"Ton & Ruhe\":\"Sonido y calma\",\"Ungefähre Onlinezeiten\":\"Tiempos aproximados en línea\",\"Verbunden\":\"Conectado\",\"Vollständige Modelstatistik\":\"Estadísticas completas del modelo\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Leer los datos de la cuenta existente ahora\",\"Weiter ›\":\"Siguiente ›\",\"Zufriedenheit\":\"Satisfacción\",\"Automatisch neu verbinden\":\"Reconectarse automáticamente\",\"Bei verstecktem Tab pausieren\":\"Pausa mientras la pestaña está oculta\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Aplicar los detalles de la cuenta Chaturbate existente\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Optimizar imagen · gran zoom, posición, filtro y rotación\",\"Bildanpassung\":\"Ajuste de imagen\",\"Bildwerkzeuge einklappen\":\"Herramientas para contraer imágenes\",\"Breite des Informationsbereichs\":\"Ancho del área de información\",\"Broadcaster-Verbindung verwalten\":\"Administrar la conexión de la emisora\",\"Chat anzeigen\":\"Mostrar chat\",\"Ctrl + Shift + X\":\"Ctrl + Mayús + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Esto significa que no está limitado a los datos desde que se instaló el script: RoomGrid puede leer su lista de seguidores existente y las entradas claramente reconocibles de Mi colección. No se copian mensajes, pagos, medios o contraseñas.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio también contiene comparaciones de períodos de tiempo, mapas de calor, embudos de descubrimiento, análisis de calidad y espacio de trabajo, así como exportaciones de datos.\",\"Datenschutz & Export\":\"Protección y exportación de datos\",\"Deine Aktivität der letzten 14 Tage\":\"Tu actividad en los últimos 14 días\",\"Deine meistgesehenen Models\":\"Tus modelos más vistos\",\"‹ Zurück\":\"‹ Volver\",\"● 1.2k · #tag\":\"● 1,2k · #etiqueta\",\"1 Jahr\":\"1 año\",\"30 Tage\":\"30 dias\",\"7 Tage\":\"7 dias\",\"90 Tage\":\"90 dias\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Mostrar modelos similares\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Todos los cambios se guardan localmente y también se aplican al siguiente modelo.\",\"Alle eigenen Tags\":\"Todas las etiquetas personalizadas\",\"Alle Gruppen\":\"Todos los grupos\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Solicitar datos de Chaturbate más antiguos\",\"Automatisch abspielen\":\"Reproducción automática\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Capturado automáticamente · totalmente organizado al editar\",\"Chat-, Token- und Tip-Töne vollständig aus\":\"Desactivar completamente los sonidos de chat, tokens y propinas\",\"ALLE SEITENTÖNE AUS\":\"TODOS LOS SONIDOS DE LA PÁGINA DESACTIVADOS\",\"SEITENTÖNE ERLAUBT\":\"SONIDOS DE LA PÁGINA ACTIVADOS\",\"Chat/Token-Töne AUS\":\"Sonidos de chat/tokens DESACTIVADOS\",\"Chat/Token-Töne AN\":\"Sonidos de chat/tokens ACTIVADOS\",\"Gruppen\":\"Grupos\",\"RoomGrid Komfortzentrum\":\"Centro RoomGrid\",\"Mein Nutzer\":\"Mi usuario\",\"Mein Nutzer-Dashboard\":\"Panel de mi usuario\",\"Modellbibliothek\":\"Biblioteca de modelos\",\"Große Modellbibliothek\":\"Gran biblioteca de modelos\",\"Statistikzentrum\":\"Centro de estadísticas\",\"Sitzungen\":\"Sesiones\",\"Dock + Hover\":\"Fijar + pasar el cursor\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Local y privado en este dispositivo\",\"Sortierung\":\"Ordenar por\",\"Meiste Sehzeit\":\"Mayor tiempo de visualización\",\"Zuletzt aktiv\":\"Actividad reciente\",\"Meiste Sitzungen\":\"Más sesiones\",\"Meiste Zuschauer\":\"Más espectadores\",\"Status\":\"Estado\",\"Alle Status\":\"Todos los estados\",\"Nur live\":\"Solo en vivo\",\"Offline\":\"Sin conexión\",\"Unbekannt\":\"Desconocido\",\"Nur Bibliothek\":\"Solo en la biblioteca\",\"Pro Seite\":\"Por página\"}}","pt":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"A mudança de idioma não pôde ser preparada com segurança. Esta página não foi deixada.\",\"Language\":\"Idioma\",\"Default workspace\":\"Espaço de trabalho padrão\",\"Workspace\":\"Espaço de trabalho\",\"New workspace\":\"Novo espaço de trabalho\",\"copy\":\"copiar\",\"New group\":\"Novo grupo\",\"Saved model\":\"Modelo salvo\",\"Search models, pages, workspaces and commands…\":\"Pesquise modelos, páginas, espaços de trabalho e comandos…\",\"Type to filter · Enter to run · Esc to close\":\"Digite para filtrar · Enter para executar · Esc para fechar\",\"No matches\":\"Nenhuma correspondência\",\"Currently active RoomGrid version\":\"Versão RoomGrid atualmente ativa\",\"years\":\"anos\",\"Live\":\"Ao vivo\",\"Private\":\"Privado\",\"Away\":\"Ausente\",\"Offline\":\"Off-line\",\"sessions\":\"sessões\",\"NEW\":\"NOVO\",\"Name, subject, tag or place\":\"Nome, tema, tag ou local\",\"Website order\":\"Ordem do site\",\"Most viewers\":\"Mais espectadores\",\"Fewest viewers\":\"Menos espectadores\",\"Longest online\":\"Há mais tempo online\",\"Recently online\":\"Online recentemente\",\"Age ascending\":\"Idade crescente\",\"Age descending\":\"Idade decrescente\",\"New models first\":\"Modelos novos primeiro\",\"Saved first\":\"Salvos primeiro\",\"Favorites first\":\"Favoritos primeiro\",\"Most watched\":\"Mais assistidos\",\"Recently watched\":\"Vistos recentemente\",\"Random mix\":\"Mistura aleatória\",\"All loaded\":\"Todos os carregados\",\"Previous page\":\"Página anterior\",\"Next page\":\"Próxima página\",\"Reload complete live list\":\"Recarregar lista completa ao vivo\",\"All homepage options\":\"Todas as opções da página inicial\",\"Reset filters\":\"Redefinir filtros\",\"Quick filter\":\"Filtro rápido\",\"Sort\":\"Classificar\",\"Models per page\":\"Modelos por página\",\"Load more results\":\"Carregar mais resultados\",\"still to display\":\"ainda para exibir\",\"continues automatically while scrolling\":\"continua automaticamente enquanto rola\",\"visible\":\"visíveis\",\"loaded\":\"carregados\",\"page\":\"página\",\"active filters\":\"filtros ativos\",\"loading complete live list\":\"carregando lista completa ao vivo\",\"all matching live models found\":\"todos os modelos vivos correspondentes encontrados\",\"live list incomplete, retry\":\"lista ativa incompleta, tente novamente\",\"Requests temporarily paused\":\"Solicitações pausadas temporariamente\",\"Vault could not be unlocked\":\"Não foi possível desbloquear o cofre\",\"Preview Dock\":\"Doca de visualização\",\"Connecting…\":\"Conectando…\",\"Stream temporarily unavailable\":\"Transmissão temporariamente indisponível\",\"Error\":\"Erro\",\"Buffering\":\"Buffer\",\"Preview unavailable\":\"Visualização indisponível\",\"Connect\":\"Conectar\",\"Close\":\"Fechar\",\"Opacity\":\"Opacidade\",\"Info\":\"Informações\",\"Age\":\"Idade\",\"Gender\":\"Gênero\",\"Location\":\"Localização\",\"Country\":\"País\",\"Last broadcast\":\"Última transmissão\",\"Fan club\":\"Fã clube\",\"Private show\":\"Show privado\",\"Spy show\":\"Show de espionagem\",\"Model comparison list\":\"Lista de comparação de modelos\",\"Compare\":\"Comparar\",\"Open model details\":\"Detalhes do modelo aberto\",\"Compare now\":\"Compare agora\",\"Clear\":\"Limpar\",\"Up to four models can be compared at once.\":\"Até quatro modelos podem ser comparados ao mesmo tempo.\",\"Remove from comparison\":\"Remover da comparação\",\"Add to comparison\":\"Adicionar à comparação\",\"Total watched\":\"Total assistido\",\"Sessions\":\"Sessões\",\"Viewers\":\"Espectadores\",\"Rating\":\"Avaliação\",\"Online samples\":\"Amostras on-line\",\"Loading quality data…\":\"Carregando dados de qualidade…\",\"samples\":\"amostras\",\"stalls\":\"barracas\",\"No quality measurements yet\":\"Ainda não há medições de qualidade\",\"Quality data unavailable\":\"Dados de qualidade indisponíveis\",\"Details\":\"Detalhes\",\"Remove\":\"Remover\",\"Model comparison\":\"Comparação de modelos\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Cada valor vem de informações públicas, de sua própria organização ou de observações locais neste dispositivo. Valores ausentes nunca são adivinhados.\",\"Shared tags\":\"Tags compartilhadas\",\"Loading public facts…\":\"Carregando fatos públicos…\",\"Unknown status\":\"Estado desconhecido\",\"Last activity\":\"Última atividade\",\"These statistics describe only your own local RoomGrid usage.\":\"Essas estatísticas descrevem apenas seu próprio uso local do RoomGrid.\",\"At a glance\":\"De relance\",\"Type\":\"Tipo\",\"Appearances\":\"Aparências\",\"Public facts\":\"Fatos públicos\",\"No public biography available yet.\":\"Nenhuma biografia pública disponível ainda.\",\"Public link\":\"Link público\",\"Public profile source\":\"Fonte de perfil público\",\"Profile updated\":\"Perfil atualizado\",\"Private note (local only)\":\"Nota privada (somente local)\",\"Your tags\":\"Suas tags\",\"No rating\":\"Sem classificação\",\"No priority\":\"Sem prioridade\",\"Private library data\":\"Dados da biblioteca privada\",\"Private notes\":\"Notas privadas\",\"Priority\":\"Prioridade\",\"Favorite\":\"Favorito\",\"Watchlist\":\"Lista de observação\",\"Archived\":\"Arquivado\",\"Watch again\":\"Assistir novamente\",\"Technical note\":\"Nota técnica\",\"Technical\":\"Técnico\",\"Save all changes\":\"Salvar todas as alterações\",\"Loading local activity…\":\"Carregando atividade local…\",\"Activity and playback quality\":\"Qualidade de atividade e reprodução\",\"Sun\":\"Sol\",\"Mon\":\"segunda-feira\",\"Tue\":\"Terça\",\"Wed\":\"Quarta\",\"Thu\":\"Qui\",\"Fri\":\"Sexta-feira\",\"Sat\":\"Sábado\",\"Quality samples\":\"Amostras de qualidade\",\"Average startup\":\"Inicialização média\",\"Stalls\":\"Barracas\",\"Reconnects\":\"Reconecta\",\"Dropped frames\":\"Quadros descartados\",\"Online observations\":\"Observações on-line\",\"Frequently observed online times\":\"Horários online frequentemente observados\",\"Not enough online observations yet.\":\"Ainda não há observações online suficientes.\",\"Watched\":\"Assistido\",\"Preview\":\"Visualização\",\"Quick view\":\"Visualização rápida\",\"Discovered\":\"Descoberto\",\"Quality\":\"Qualidade\",\"Connection\":\"Conexão\",\"No local activity recorded yet.\":\"Nenhuma atividade local registrada ainda.\",\"Recent events\":\"Eventos recentes\",\"Activity data unavailable.\":\"Dados de atividade indisponíveis.\",\"Similar models\":\"Modelos semelhantes\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Calculado a partir de tags locais/públicas, país e tipo. Cada sugestão mostra seu motivo e nenhum dado é carregado.\",\"shared tags\":\"tags compartilhadas\",\"same country\":\"mesmo país\",\"same type\":\"mesmo tipo\",\"Not enough matching data yet.\":\"Ainda não há dados correspondentes suficientes.\",\"Overview\":\"Visão geral\",\"Profile\":\"Perfil\",\"Organize\":\"Organizar\",\"Activity\":\"Atividade\",\"Similar\":\"Semelhante\",\"Model detail sections\":\"Seções de detalhes do modelo\",\"Previous model\":\"Modelo anterior\",\"Next model\":\"Próximo modelo\",\"Open room\":\"Sala aberta\",\"Pin preview\":\"Pré-visualização do fixador\",\"No public fields available\":\"Nenhum campo público disponível\",\"Public facts currently unavailable\":\"Fatos públicos atualmente indisponíveis\",\"Session chat search\":\"Pesquisa de bate-papo da sessão\",\"Search this session only…\":\"Pesquisar apenas nesta sessão…\",\"New messages\":\"Novas mensagens\",\"Time for a short break.\":\"É hora de uma pequena pausa.\",\"Your RoomGrid time budget for today has been reached.\":\"Seu orçamento de tempo do RoomGrid para hoje foi atingido.\",\"RoomGrid action messages\":\"Mensagens de ação RoomGrid\",\"This change is saved automatically.\":\"Esta alteração é salva automaticamente.\",\"Undo\":\"Desfazer\",\"Change undone\":\"Alteração desfeita\",\"Undo was not applied because newer changes already exist.\":\"Desfazer não foi aplicado porque já existem alterações mais recentes.\",\"Chaturbate live catalog\":\"Catálogo ao vivo do Chaturbate\",\"RoomGrid Discover\":\"RoomGrid Descobrir\",\"Chaturbate Discover page\":\"Página de descoberta do Chaturbate\",\"Following list\":\"Lista a seguir\",\"My Collection\":\"Minha coleção\",\"Chaturbate page\":\"Página do Chaturbate\",\"Local record\":\"Registro local\",\"Personally organized\":\"Organizado pessoalmente\",\"Local watch history\":\"Histórico de exibição local\",\"Public profile\":\"Perfil público\",\"Local quality measurements\":\"Medições de qualidade locais\",\"Local model index\":\"Índice de modelo local\",\"Current live signal\":\"Sinal ao vivo atual\",\"Within the last hour\":\"Na última hora\",\"Within the last 24 hours\":\"Nas últimas 24 horas\",\"Within the last 30 days\":\"Nos últimos 30 dias\",\"Historical record\":\"Registro histórico\",\"Time unknown\":\"Hora desconhecida\",\"All\":\"Todos\",\"Favorites\":\"Favoritos\",\"Library\":\"Biblioteca\",\"My Collection contained no reliably recognizable entries\":\"Minha coleção não continha entradas reconhecíveis de forma confiável\",\"No group filter active · all models are shown.\":\"Nenhum filtro de grupo ativo · todos os modelos são mostrados.\",\"Clear group filter\":\"Limpar filtro de grupo\",\"Filter homepage by groups\":\"Filtrar a página inicial por grupos\",\"Close group menu\":\"Fechar menu do grupo\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Escolha um ou mais de seus próprios grupos. Outros cartões de modelo ficam imediatamente ocultos na página inicial do Chaturbate.\",\"Show all\":\"Mostrar tudo\",\"Models in this group\":\"Modelos neste grupo\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Ainda não existe um grupo personalizado. Crie um na biblioteca de modelos e atribua modelos lá.\",\"In any group\":\"Em qualquer grupo\",\"In all groups\":\"Em todos os grupos\",\"Manage groups\":\"Gerenciar grupos\",\"Create group\":\"Criar grupo\",\"Custom groups for the homepage\":\"Grupos personalizados para a página inicial\",\"Main views\":\"Visualizações principais\",\"MultiCam workstation\":\"Estação de trabalho MultiCam\",\"Camera grid, audio and layouts\":\"Grade de câmera, áudio e layouts\",\"Discover\":\"Descobrir\",\"Full catalog, filters and sorting\":\"Catálogo completo, filtros e classificação\",\"Personal dashboard\":\"Painel pessoal\",\"Favorites, activity and shortcuts\":\"Favoritos, atividades e atalhos\",\"Tag explorer\":\"Explorador de tags\",\"Combinations, trends and related tags\":\"Combinações, tendências e tags relacionadas\",\"Library and statistics\":\"Biblioteca e estatísticas\",\"Statistics Center\":\"Centro de Estatísticas\",\"Model comparison and local statistics\":\"Comparação de modelos e estatísticas locais\",\"Model library\":\"Biblioteca de modelos\",\"Profiles, tags, groups and notes\":\"Perfis, tags, grupos e notas\",\"My user\":\"Meu usuário\",\"Preferences, usage and account data\":\"Preferências, uso e dados da conta\",\"Analytics Studio\":\"Estúdio de análise\",\"Ranges, heatmaps and exports\":\"Intervalos, mapas de calor e exportações\",\"Settings and maintenance\":\"Configurações e manutenção\",\"Customize homepage\":\"Personalizar página inicial\",\"Cards, filters and visible details\":\"Cartões, filtros e detalhes visíveis\",\"Appearance\":\"Aparência\",\"Colors, density and surfaces\":\"Cores, densidade e superfícies\",\"Workspaces and groups\":\"Espaços de trabalho e grupos\",\"Layouts, templates and assignments\":\"Layouts, modelos e tarefas\",\"Data and backup\":\"Dados e backup\",\"Import, export and recovery\":\"Importação, exportação e recuperação\",\"Windows and performance\":\"Janelas e desempenho\",\"Resource profiles and preview limits\":\"Perfis de recursos e limites de visualização\",\"Privacy\":\"Privacidade\",\"History, private session and local data\":\"Histórico, sessão privada e dados locais\",\"Diagnostics\":\"Diagnóstico\",\"Self-check, routes and recovery\":\"Autoverificação, rotas e recuperação\",\"RoomGrid main menu\":\"Menu principal do Room Grid\",\"All pages, settings and tools in one place\":\"Todas as páginas, configurações e ferramentas em um só lugar\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"O menu fica dentro da janela visível; todos os dados do RoomGrid permanecem locais.\",\"RoomGrid pages\":\"Páginas RoomGrid\",\"Statistics\":\"Estatísticas\",\"Customize original homepage\":\"Personalizar a página inicial original\",\"Homepage\":\"Página inicial\",\"Filter homepage by your own groups\":\"Filtre a página inicial por seus próprios grupos\",\"Groups\":\"Grupos\",\"Activity trend\":\"Tendência de atividade\",\"Link\":\"Ligação\",\"Public profile text and links\":\"Texto e links de perfil público\",\"Last known public preview\":\"Última visualização pública conhecida\",\"Current public preview\":\"Visualização pública atual\",\"Offline may only provide a placeholder\":\"Off-line só pode fornecer um espaço reservado\",\"Example images and covers\":\"Exemplos de imagens e capas\",\"Reading local image archive…\":\"Lendo arquivo de imagens local…\",\"Save new snapshot\":\"Salvar novo instantâneo\",\"Saving public previews…\":\"Salvando visualizações públicas…\",\"No saved image yet; current public previews are still shown.\":\"Nenhuma imagem salva ainda; as visualizações públicas atuais ainda são mostradas.\",\"Current public cover\":\"Cobertura pública atual\",\"Current compact preview\":\"Visualização compacta atual\",\"Current live image\":\"Imagem ao vivo atual\",\"Room\":\"Sala\",\"More\":\"Mais\",\"Unwatch\":\"Desassistir\",\"Data sources and freshness\":\"Fontes de dados e atualização\",\"Data source\":\"Fonte de dados\",\"Full local data set\":\"Conjunto completo de dados locais\",\"Matching filters\":\"Filtros correspondentes\",\"Shown on this page\":\"Mostrado nesta página\",\"Latest live signals\":\"Últimos sinais ao vivo\",\"With watch history\":\"Com histórico de exibição\",\"Public-profile coverage\":\"Cobertura de perfil público\",\"Country coverage\":\"Cobertura do país\",\"Age coverage\":\"Cobertura etária\",\"Tag coverage\":\"Cobertura de tags\",\"Which data set is being searched?\":\"Qual conjunto de dados está sendo pesquisado?\",\"Last local observation\":\"Última observação local\",\"No observation time yet\":\"Ainda não há tempo de observação\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Público = das páginas atuais do Chaturbate ou do catálogo ativo. Pessoal = salvo, marcado ou agrupado por você. Local = sessões, visualizações ou valores de qualidade observados apenas neste dispositivo. Campos desconhecidos permanecem desconhecidos e nunca são adivinhados.\",\"Local model statistics\":\"Estatísticas do modelo local\",\"Automatically recorded · personally organized\":\"Gravado automaticamente · organizado pessoalmente\",\"Automatically recorded in the growing model catalog\":\"Registrado automaticamente no crescente catálogo de modelos\",\"Avg. session\":\"Sessão média\",\"Active days\":\"Dias ativos\",\"Share\":\"Compartilhar\",\"Longest session\":\"Sessão mais longa\",\"Previews\":\"Prévias\",\"Quick views\":\"Visualizações rápidas\",\"Data sources\":\"Fontes de dados\",\"Edit in library\":\"Editar na biblioteca\",\"No experience data yet\":\"Ainda não há dados de experiência\",\"Online observations will be collected locally from now on.\":\"As observações online serão coletadas localmente a partir de agora.\",\"Open\":\"Abrir\",\"Good trend\":\"Boa tendência\",\"Moderate trend\":\"Tendência moderada\",\"Initial trend\":\"Tendência inicial\",\"First observation\":\"Primeira observação\",\"Estimated from watch time\":\"Estimado a partir do tempo de exibição\",\"weekday open\":\"aberto durante a semana\",\"time open\":\"tempo aberto\",\"Preview image\":\"Imagem de prévia\",\"Biography / room subject\":\"Biografia / tema da sala\",\"Catalog state\":\"Status do catálogo\",\"Live status\":\"Status ao vivo\",\"Viewer count\":\"Número de espectadores\",\"Country/region\":\"País/região\",\"Total watch time\":\"Tempo total de visualização\",\"Average session\":\"Sessão média\",\"First seen\":\"Visto pela primeira vez\",\"Last seen\":\"Visto pela última vez\",\"Last live\":\"Última transmissão\",\"Approximate online times\":\"Horários aproximados online\",\"Activity, sources and profile state\":\"Atividade, fontes e status do perfil\",\"Example images\":\"Imagens de exemplo\",\"Own groups\":\"Grupos próprios\",\"Public/recognized tags\":\"Tags públicas/reconhecidas\",\"Notes, own tags and priority\":\"Notas, tags próprias e prioridade\",\"Local rating\":\"Avaliação local\",\"Group assignment\":\"Atribuição de grupos\",\"Room and preview actions\":\"Ações da sala e de prévia\",\"Library display\":\"Exibição da biblioteca\",\"Saved in this browser\":\"Salvo neste navegador\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Escolha o tamanho do cartão, os modelos por linha, a quantidade por página e exatamente quais detalhes cada cartão mostra.\",\"Compact overview\":\"Visão compacta\",\"Balanced\":\"Equilibrado\",\"All details\":\"Todos os detalhes\",\"Show all details\":\"Mostrar todos os detalhes\",\"Core details only\":\"Somente informações principais\",\"Card zoom\":\"Ampliação dos cartões\",\"Visible information per model\":\"Informações visíveis por modelo\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Com colunas automáticas, a ampliação determina quantos modelos cabem lado a lado. Uma quantidade fixa de colunas tem prioridade. Todas as opções ficam salvas neste navegador.\",\"No custom tags yet\":\"Ainda não há tags personalizadas\",\"Name of the new group\":\"Nome do novo grupo\",\"Models remain in the library.\":\"Os modelos permanecem na biblioteca.\",\"Custom groups\":\"Grupos personalizados\",\"Create new group\":\"Criar novo grupo\",\"Nothing selected — show all\":\"Nada selecionado – mostrar tudo\",\"Any\":\"Qualquer\",\"Women\":\"Mulheres\",\"Men\":\"Homens\",\"Couples\":\"Casais\",\"Unknown\":\"Desconhecido\",\"Error/loading\":\"Erro/carregamento\",\"Every weekday\":\"Todos os dias da semana\",\"Sunday\":\"Domingo\",\"Monday\":\"Segunda-feira\",\"Tuesday\":\"Terça-feira\",\"Wednesday\":\"Quarta-feira\",\"Thursday\":\"Quinta-feira\",\"Friday\":\"Sexta-feira\",\"Saturday\":\"Sábado\",\"Every time\":\"Toda vez\",\"Advanced filters and sorting\":\"Filtros e classificação avançados\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combine quantos filtros desejar. Vários gêneros, estados, países e tags funcionam como seleções múltiplas. Cada configuração permanece salva neste navegador.\",\"Gender/type\":\"Gênero/tipo\",\"Countries/regions (multiple)\":\"Países/regiões (múltiplos)\",\"Add country…\":\"Adicionar país…\",\"Public tags (multiple)\":\"Tags públicas (múltiplas)\",\"Add tag…\":\"Adicionar etiqueta…\",\"Public-tag matching\":\"Correspondência de tags públicas\",\"At least one tag\":\"Pelo menos uma etiqueta\",\"Every selected tag\":\"Cada tag selecionada\",\"Catalog source\":\"Fonte do catálogo\",\"All models\":\"Todos os modelos\",\"Personally organized only\":\"Somente organizado pessoalmente\",\"Automatically discovered only\":\"Somente descoberto automaticamente\",\"Profile data\":\"Dados do perfil\",\"Loaded or missing\":\"Carregado ou ausente\",\"Loaded only\":\"Somente carregado\",\"Missing only\":\"Faltando apenas\",\"Picture/category\":\"Imagem/categoria\",\"Neither HD nor gaming\":\"Nem HD nem jogos\",\"Watch history\":\"Assistir histórico\",\"Never watched\":\"Nunca assisti\",\"Own tags\":\"Etiquetas próprias\",\"With or without\":\"Com ou sem\",\"With tags only\":\"Somente com tags\",\"Without tags only\":\"Somente sem tags\",\"At least one group\":\"Pelo menos um grupo\",\"No group\":\"Nenhum grupo\",\"With notes only\":\"Somente com notas\",\"Without notes only\":\"Somente sem notas\",\"Numeric ranges and minimums\":\"Intervalos numéricos e mínimos\",\"Minimum age\":\"Idade mínima\",\"Maximum age\":\"Idade máxima\",\"Minimum viewers\":\"Mínimo de espectadores\",\"Maximum viewers\":\"Máximo de espectadores\",\"Minimum followers\":\"Seguidores mínimos\",\"Maximum followers\":\"Máximo de seguidores\",\"Minimum total watch time\":\"Tempo total mínimo de exibição\",\"Currently online for at least\":\"Atualmente online há pelo menos\",\"Minimum sessions\":\"Sessões mínimas\",\"Minimum rating\":\"Classificação mínima\",\"Minimum priority\":\"Prioridade mínima\",\"Active within\":\"Ativo dentro\",\"days\":\"dias\",\"Minimum appearances\":\"Aparições mínimas\",\"Minimum live observations\":\"Observações mínimas ao vivo\",\"Filter by typical online time\":\"Filtrar por horário on-line típico\",\"Weekday\":\"Dia da semana\",\"Hour (local time)\":\"Hora (hora local)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Campos zero ou vazios significam que não há limite. Idades desconhecidas são excluídas enquanto um limite de idade estiver ativo. Os filtros de tempo usam tempos ao vivo observados neste dispositivo, recorrendo cautelosamente aos tempos de exibição quando necessário.\",\"Reset all library filters\":\"Redefinir todos os filtros da biblioteca\",\"Open live room\":\"Abrir sala ao vivo\",\"Open room page\":\"Abrir página da sala\",\"Open model room in a new tab\":\"Abra a sala do modelo em uma nova guia\",\"Open the Chaturbate single-model view in a new tab\":\"Abra a visualização de modelo único do Chaturbate em uma nova guia\",\"Save profile\":\"Salvar perfil\",\"Restore\":\"Restaurar\",\"Archive\":\"Arquivo\",\"Advanced statistics filters\":\"Filtros de estatísticas avançadas\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Cada filtro pode ser combinado; países, tags, grupos e tipos suportam seleção múltipla. As escolhas são salvas permanentemente.\",\"Own groups (multiple)\":\"Grupos próprios (múltiplos)\",\"Add group…\":\"Adicionar grupo…\",\"Tag matching\":\"Correspondência de tags\",\"At least one\":\"Pelo menos um\",\"All selected tags\":\"Todas as tags selecionadas\",\"Group matching\":\"Correspondência de grupo\",\"All selected groups\":\"Todos os grupos selecionados\",\"Cover/example image\":\"Imagem de capa/exemplo\",\"Known public image available\":\"Imagem pública conhecida disponível\",\"No known image\":\"Nenhuma imagem conhecida\",\"Currently live\":\"Atualmente ao vivo\",\"Maximum total watch time\":\"Tempo total máximo de exibição\",\"Maximum sessions\":\"Máximo de sessões\",\"Minimum active days\":\"Dias ativos mínimos\",\"Minimum previews\":\"Visualizações mínimas\",\"Minimum quick views\":\"Visualizações rápidas mínimas\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Campos zero ou vazios significam que não há limite. Valores desconhecidos nunca são adivinhados; as faixas etárias excluem idades desconhecidas.\",\"Reset all statistics filters\":\"Redefinir todos os filtros de estatísticas\",\"Search name, tags or notes…\":\"Pesquise nome, tags ou notas…\",\"Search\":\"Pesquisar\",\"Sorting\":\"Classificando\",\"Most watch time\":\"Maior tempo de exibição\",\"Least watch time\":\"Menor tempo de exibição\",\"Recently active\":\"Recentemente ativo\",\"Longest inactive\":\"Maior inatividade\",\"Most sessions\":\"A maioria das sessões\",\"Fewest sessions\":\"Menos sessões\",\"Longest average session\":\"Sessão média mais longa\",\"Shortest average session\":\"Sessão média mais curta\",\"Longest single session\":\"Sessão única mais longa\",\"Most active days\":\"Dias mais ativos\",\"Highest watch share\":\"Maior parcela de exibição\",\"Most previews\":\"A maioria das visualizações\",\"Most quick views\":\"Visualizações mais rápidas\",\"Most discoveries\":\"A maioria das descobertas\",\"Most followers\":\"Maioria dos seguidores\",\"Youngest first\":\"O mais novo primeiro\",\"Oldest first\":\"Mais antigo primeiro\",\"Highest rating\":\"Classificação mais alta\",\"Highest priority\":\"Maior prioridade\",\"Most appearances\":\"A maioria das aparições\",\"Most live observations\":\"A maioria das observações ao vivo\",\"First observed\":\"Observado pela primeira vez\",\"Country A–Z\":\"País A–Z\",\"Most public tags\":\"A maioria das tags públicas\",\"Most groups\":\"A maioria dos grupos\",\"Best technical quality\":\"Melhor qualidade técnica\",\"Cover/example images first\":\"Imagens de capa/exemplo primeiro\",\"All statuses\":\"Todos os status\",\"Live only\":\"Somente ao vivo\",\"Library only\":\"Somente biblioteca\",\"View\":\"Visualização\",\"Active library\":\"Biblioteca ativa\",\"Live now\":\"Viva agora\",\"With notes\":\"Com notas\",\"Newest saved/discovered\":\"Mais recente salvo/descoberto\",\"Oldest saved/discovered\":\"Mais antigo salvo/descoberto\",\"Lowest rating\":\"Classificação mais baixa\",\"Lowest priority\":\"Prioridade mais baixa\",\"Live longest now\":\"Viva mais agora\",\"Age youngest first\":\"Idade mais nova primeiro\",\"Age oldest first\":\"Idade mais velha primeiro\",\"Most recently live\":\"Mais recentemente ao vivo\",\"First discovered\":\"Descoberto pela primeira vez\",\"Most own groups\":\"A maioria dos próprios grupos\",\"Card size\":\"Tamanho do cartão\",\"Compact\":\"Compacto\",\"Comfortable\":\"Confortável\",\"Large\":\"Grande\",\"Extra large\":\"Extra grande\",\"Models per row\":\"Modelos por linha\",\"Automatic for window\":\"Automático para a janela\",\"Recorded models\":\"Modelos gravados\",\"Total cam time\":\"Tempo total de câmera\",\"across all models\":\"em todos os modelos\",\"Dock + hover\":\"Dock + pairar\",\"Last 30 days\":\"Últimos 30 dias\",\"local events\":\"eventos locais\",\"Top model\":\"Modelo superior\",\"no watch time yet\":\"ainda não há tempo de exibição\",\"Deeper analysis\":\"Análise mais profunda\",\"Open Analytics Studio\":\"Abra o Analytics Studio\",\"Late night\":\"Tarde da noite\",\"Morning\":\"Manhã\",\"Daytime\":\"Diurno\",\"Evening\":\"Noite\",\"Gaming\":\"Jogos\",\"Music & DJ\":\"Música e DJ\",\"Dance\":\"Dança\",\"Cosplay & anime\":\"Cosplay e anime\",\"Fitness & sports\":\"Fitness e esportes\",\"Talk & social\":\"Conversa e redes sociais\",\"Outdoor & travel\":\"Atividades ao ar livre e viagens\",\"Roleplay\":\"RPG\",\"Performance\":\"Desempenho\",\"My interest statistics\":\"Minhas estatísticas de interesses\",\"All local data\":\"Todos os dados locais\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Calculado a partir do tempo real de exibição local, sessões, visualizações, visualizações rápidas, favoritos e informações de modelos públicos atualmente conhecidos.\",\"Favorite tag\":\"Tag favorita\",\"not enough data\":\"dados insuficientes\",\"Favorite genre\":\"Gênero favorito\",\"from public tags\":\"de tags públicas\",\"Most-watched age\":\"Idade mais assistida\",\"no age data\":\"sem dados de idade\",\"Most-watched country\":\"País mais assistido\",\"no country data\":\"sem dados do país\",\"Favorite model type\":\"Tipo de modelo favorito\",\"no type data\":\"nenhum tipo de dados\",\"Favorite viewing time\":\"Horário de visualização favorito\",\"no time data\":\"sem dados de tempo\",\"Top-three concentration\":\"Concentração dos três primeiros\",\"watch time on the top three models\":\"assista o tempo nos três principais modelos\",\"Favorite affinity\":\"Afinidade favorita\",\"watch time on current favorites\":\"tempo de exibição nos favoritos atuais\",\"watch time on HD models\":\"tempo de exibição em modelos HD\",\"Metadata coverage\":\"Cobertura de metadados\",\"age\":\"idade\",\"country\":\"país\",\"Tags, genres and browsing interests\":\"Tags, gêneros e interesses de navegação\",\"Tags by watch time\":\"Tags por tempo de exibição\",\"Your ranking appears when watched models have public tags.\":\"Sua classificação aparece quando os modelos observados possuem tags públicas.\",\"Favorite genre worlds\":\"Mundos de gênero favoritos\",\"No genre world can yet be assigned from public tags.\":\"Nenhum mundo de gênero ainda pode ser atribuído a partir de tags públicas.\",\"Your own tags\":\"Suas próprias tags\",\"Your saved custom tags are also evaluated here by watch time.\":\"Suas tags personalizadas salvas também são avaliadas aqui por tempo de exibição.\",\"Preview-interest tags\":\"Tags de interesse de visualização\",\"Browsing interests appear when you use live previews or quick views.\":\"Os interesses de navegação aparecem quando você usa visualizações ao vivo ou visualizações rápidas.\",\"Most-previewed models\":\"Modelos mais visualizados\",\"No previews have been recorded in this period yet.\":\"Nenhuma prévia foi registrada neste período ainda.\",\"Age, origin and model type\":\"Idade, origem e tipo de modelo\",\"Age groups by watch time\":\"Faixas etárias por tempo de exibição\",\"Not enough public age data is available.\":\"Não estão disponíveis dados públicos suficientes sobre a idade.\",\"Countries by watch time\":\"Países por tempo de exibição\",\"Not enough public country data is available.\":\"Não estão disponíveis dados públicos suficientes sobre os países.\",\"Most-watched places\":\"Lugares mais assistidos\",\"Not enough public location data is available.\":\"Não há dados suficientes de localização pública disponíveis.\",\"Model types by watch time\":\"Tipos de modelo por tempo de exibição\",\"Not enough public model-type data is available.\":\"Não estão disponíveis dados suficientes do tipo de modelo público.\",\"Viewing rhythm and favorite signals\":\"Ritmo de visualização e sinais de favoritos\",\"Most active weekdays\":\"Dias de semana mais ativos\",\"Not enough visit events are available.\":\"Não há eventos de visita suficientes disponíveis.\",\"Times of day\":\"Horas do dia\",\"Tags among favorites\":\"Tags entre favoritos\",\"Preference signals appear when you mark models as favorites.\":\"Os sinais de preferência aparecem quando você marca modelos como favoritos.\",\"Countries among favorites\":\"Países entre favoritos\",\"No known countries among favorites yet.\":\"Nenhum país conhecido entre os favoritos ainda.\",\"Age groups among favorites\":\"Faixas etárias entre os favoritos\",\"No known ages among favorites yet.\":\"Nenhuma idade conhecida entre os favoritos ainda.\",\"Model types among favorites\":\"Tipos de modelos entre favoritos\",\"No known model types among favorites yet.\":\"Nenhum tipo de modelo conhecido entre os favoritos ainda.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Esses resultados não são enviados para um servidor. Apenas o tempo de exibição real conta para as preferências de visualização; visualizações e favoritos são mostrados separadamente. Dados ausentes de idade, país, localização ou etiqueta nunca são adivinhados.\",\"Open Following on Chaturbate\":\"Abrir seguindo no Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Um histórico completo de tempo de exibição ou de visita não pode ser recuperado retroativamente porque o Chaturbate não fornece nenhuma API de histórico pessoal documentada. Você pode solicitar uma cópia dos seus dados pessoais armazenados através da exportação oficial de dados do Chaturbate. Se essa exportação contiver campos de histórico utilizáveis, o RoomGrid poderá oferecer suporte a uma importação segura após validar o formato do arquivo. A API oficial de estatísticas fornece os valores atuais da emissora; RoomGrid pode construir seu próprio histórico somente a partir da primeira conexão.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Compare cada modelo observado, salvo e assistido localmente, com tempo de exibição, sessões, atividades, visualizações, qualidade e classificações.\",\"Large model library\":\"Grande biblioteca de modelos\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Seus modelos como perfis convenientes e ricos em imagens com status ao vivo, notas, tags, classificações, prioridade, grupos e ações diretas.\",\"My user dashboard\":\"Meu painel de usuário\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Seu histórico privado de uso local, hábitos, dados de biblioteca e - somente quando conectado deliberadamente - dados oficiais da conta da emissora.\",\"🔒 Local & private on this device\":\"🔒 Local e privado neste dispositivo\",\"Preparing data…\":\"Preparando dados…\",\"RoomGrid Comfort Center\":\"Centro de Conforto RoomGrid\",\"inside the standard Chaturbate page\":\"dentro da página padrão do Chaturbate\",\"All RoomGrid pages and settings\":\"Todas as páginas e configurações do RoomGrid\",\"Media paused; names and images are covered.\":\"Mídia pausada; nomes e imagens são cobertos.\",\"Restore previous view\":\"Restaurar visualização anterior\",\"Enter local PIN\":\"Insira o PIN local\",\"In-page notifications\":\"Notificações na página\",\"No new notifications\":\"Nenhuma nova notificação\",\"Notifications are off\":\"As notificações estão desativadas\",\"RoomGrid notification\":\"Notificação RoomGrid\",\"Dismiss as read\":\"Descartar como lido\",\"More unread notifications\":\"Mais notificações não lidas\",\"RoomGrid update\":\"Atualização do RoomGrid\",\"A monitored source changed status.\":\"Uma fonte monitorada mudou de status.\",\"This is a local RoomGrid test notification.\":\"Esta é uma notificação de teste local do RoomGrid.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Teste bem-sucedido: as notificações podem aparecer diretamente na barra RoomGrid.\",\"Settings\":\"Configurações\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Espaços de trabalho, biblioteca, design, dados, histórico, notificações, privacidade e desempenho\",\"Workspaces\":\"Espaços de trabalho\",\"Data\":\"Dados\",\"History & statistics\":\"História e estatísticas\",\"Broadcaster stats\":\"Estatísticas da emissora\",\"Notifications\":\"Notificações\",\"Windows & performance\":\"Janelas e desempenho\",\"Optional tools\":\"Ferramentas opcionais\",\"Settings pages\":\"Páginas de configurações\",\"RoomGrid control center\":\"Centro de controle RoomGrid\",\"Any online model\":\"Qualquer modelo online\",\"Workspace name\":\"Nome do espaço de trabalho\",\"Optional description\":\"Descrição opcional\",\"Active\":\"Ativo\",\"No description\":\"Sem descrição\",\"Name\":\"Nome\",\"Description\":\"Descrição\",\"Replace offline models\":\"Substitua modelos off-line\",\"Replacement group\":\"Grupo de substituição\",\"Delay seconds\":\"Atrasar segundos\",\"Save current state\":\"Salvar estado atual\",\"New window\":\"Nova janela\",\"Export this workspace\":\"Exportar este espaço de trabalho\",\"Duplicate\":\"Duplicar\",\"Workspace restored\":\"Espaço de trabalho restaurado\",\"Workspace archived\":\"Espaço de trabalho arquivado\",\"Delete permanently\":\"Excluir permanentemente\",\"Permanently delete this workspace?\":\"Excluir permanentemente este espaço de trabalho?\",\"Saved layout and panel sizes\":\"Layout salvo e tamanhos de painel\",\"Apply\":\"Aplicar\",\"Layout template deleted\":\"Modelo de layout excluído\",\"Parent group\":\"Grupo pai\",\"None\":\"Nenhum\",\"Group description\":\"Descrição do grupo\",\"Save current workspace\":\"Salvar o espaço de trabalho atual\",\"Save layout template\":\"Salvar modelo de layout\",\"Template name\":\"Nome do modelo\",\"Layout templates\":\"Modelos de layout\",\"Device layout\":\"Layout do dispositivo\",\"Groups and subgroups\":\"Grupos e subgrupos\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Cada espaço de trabalho armazena desktops separados e layouts compactos. A substituição automática está desativada por padrão e só preenche slots offline depois de habilitada para esse espaço de trabalho.\",\"No templates yet.\":\"Ainda não há modelos.\",\"Group name\":\"Nome do grupo\",\"New subgroup\":\"Novo subgrupo\",\"Enter parent group name\":\"Insira o nome do grupo pai\",\"Subgroup name\":\"Nome do subgrupo\",\"Name, note or local tag\":\"Nome, nota ou tag local\",\"Choose group…\":\"Escolha o grupo…\",\"Add selected models to group\":\"Adicione modelos selecionados ao grupo\",\"Watch later\":\"Assistir mais tarde\",\"Paused\":\"Pausado\",\"Collection name\":\"Nome da coleção\",\"Tags, comma separated\":\"Tags, separadas por vírgula\",\"Countries, comma separated\":\"Países, separados por vírgula\",\"All types\":\"Todos os tipos\",\"Rule name\":\"Nome da regra\",\"Required tags\":\"Tags obrigatórias\",\"Local alert\":\"Alerta local\",\"Add to favorites\":\"Adicionar aos favoritos\",\"Add to group\":\"Adicionar ao grupo\",\"Any tag\":\"Qualquer etiqueta\",\"Any country\":\"Qualquer país\",\"Pause\":\"Pausa\",\"Enable\":\"Habilitar\",\"Delete\":\"Excluir\",\"Automation rule deleted\":\"Regra de automação excluída\",\"Select model\":\"Selecione o modelo\",\"Later\":\"Mais tarde\",\"Never visited\":\"Nunca visitei\",\"Last online\":\"Última vez on-line\",\"Last visited\":\"Última visita\",\"Local note\":\"Nota local\",\"Local tags, comma separated\":\"Tags locais, separadas por vírgula\",\"Remove later\":\"Remover mais tarde\",\"Hide today\":\"Esconda-se hoje\",\"Pause 7 days\":\"Pausa 7 dias\",\"Never suggest\":\"Nunca sugira\",\"Select results\":\"Selecione os resultados\",\"Clear selection\":\"Limpar seleção\",\"selected\":\"selecionado\",\"Add to watch later\":\"Adicionar para assistir mais tarde\",\"Permanently delete selected models?\":\"Excluir permanentemente os modelos selecionados?\",\"Smart collections\":\"Coleções inteligentes\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"As coleções são atualizadas automaticamente à medida que os metadados e estatísticas locais aumentam; os modelos não são duplicados.\",\"Tags\":\"Etiquetas\",\"Countries\":\"Países\",\"Favorites only\":\"Somente favoritos\",\"Online only\":\"Somente on-line\",\"Save smart collection\":\"Salvar coleção inteligente\",\"Delete current collection\":\"Excluir coleção atual\",\"Delete this smart collection? Model data stays unchanged.\":\"Excluir esta coleção inteligente? Os dados do modelo permanecem inalterados.\",\"Rules & automation center\":\"Centro de regras e automação\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Quando os dados do modelo local corresponderem, mostre um alerta, adicione um favorito ou adicione o modelo a um grupo escolhido.\",\"Action\":\"Ação\",\"Target group\":\"Grupo-alvo\",\"Save rule\":\"Salvar regra\",\"No automation rules yet.\":\"Ainda não há regras de automação.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Os nomes de usuário são desduplicados por correspondência normalizada exata. A pesquisa abrange nomes, notas e tags locais. Arquivar, remover de uma visualização e excluir permanentemente permanecem ações separadas.\",\"No matching models.\":\"Não há modelos correspondentes.\",\"export\":\"exportar\",\"Models and notes\":\"Modelos e notas\",\"This backup could not be read. Current data was not changed.\":\"Este backup não pôde ser lido. Os dados atuais não foram alterados.\",\"Import preview (nothing changed yet)\":\"Pré-visualização da importação (nada mudou ainda)\",\"conflicts\":\"conflitos\",\"Includes sensitive statistics\":\"Inclui estatísticas confidenciais\",\"models\":\"modelos\",\"events\":\"eventos\",\"quality samples\":\"amostras de qualidade\",\"Import statistics too\":\"Importe estatísticas também\",\"Import method\":\"Método de importação\",\"Merge (recommended)\":\"Mesclar (recomendado)\",\"Replace selected sections\":\"Substituir seções selecionadas\",\"Apply import\":\"Aplicar importação\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Importação concluída. A configuração e as estatísticas anteriores foram armazenadas em backup separadamente.\",\"The import could not be saved safely. The backup remains available.\":\"A importação não pôde ser salva com segurança. O backup permanece disponível.\",\"Cancel preview\":\"Cancelar visualização\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Este arquivo contém dados de modelo, nota ou espaço de trabalho e deve ser tratado como confidencial.\",\"This import is not marked sensitive.\":\"Esta importação não está marcada como sensível.\",\"Data version\":\"Versão dos dados\",\"Software version\":\"Versão do software\",\"Revision\":\"Revisão\",\"Local models\":\"Modelos locais\",\"Selective export\":\"Exportação seletiva\",\"Export selected\":\"Exportar selecionado\",\"Full export\":\"Exportação completa\",\"Complete safety package\":\"Pacote completo de segurança\",\"Export statistics separately\":\"Exportar estatísticas separadamente\",\"Choose file and preview\":\"Escolha o arquivo e visualize\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"O pacote completo de segurança contém configuração e estatísticas confidenciais, mas nunca o token de estatísticas oficial. Cada importação é visualizada e faz backup da configuração e das estatísticas separadamente antes da aplicação.\",\"Storage and backup center\":\"Centro de armazenamento e backup\",\"Local storage\":\"Armazenamento local\",\"Site storage\":\"Armazenamento de sites\",\"Available quota\":\"Cota disponível\",\"Internal statistics backups\":\"Backups de estatísticas internas\",\"Configuration\":\"Configuração\",\"workspaces\":\"espaços de trabalho\",\"LocalStorage + update vault\":\"LocalStorage + atualização do cofre\",\"Statistics database\":\"Banco de dados de estatísticas\",\"Secrets\":\"Segredos\",\"The official statistics token is never included in normal or complete backups.\":\"O token de estatísticas oficial nunca é incluído em backups normais ou completos.\",\"Excluded\":\"Excluído\",\"Create configuration restore point\":\"Criar ponto de restauração de configuração\",\"Create statistics restore point\":\"Criar ponto de restauração de estatísticas\",\"Legacy cleanup\":\"Limpeza legada\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Os dados legados nunca são excluídos automaticamente. Remova as chaves v6/v7 antigas somente após uma revisão bem-sucedida.\",\"Remove old v6/v7 copies\":\"Remover cópias antigas v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Remover apenas cópias antigas da v6/v7? Os dados e backups atuais permanecem.\",\"items removed\":\"itens removidos\",\"Aurora\":\"aurora\",\"Blue and teal, the RoomGrid signature\":\"Azul e verde-azulado, a assinatura RoomGrid\",\"Ember\":\"Brasa\",\"Warm orange and rose\":\"Laranja quente e rosa\",\"Frost\":\"Geada\",\"Crisp cyan and violet\":\"Ciano e violeta nítidos\",\"Custom\":\"Personalizado\",\"Use the colors selected below\":\"Utilize as cores selecionadas abaixo\",\"Name the current appearance\":\"Nomeie a aparência atual\",\"My appearance\":\"Minha aparência\",\"Delete appearance profile\":\"Excluir perfil de aparência\",\"This appearance profile could not be imported.\":\"Este perfil de aparência não pôde ser importado.\",\"Light\":\"Luz\",\"Dark\":\"Escuro\",\"Export\":\"Exportar\",\"Home\":\"Página inicial\",\"Listings and tags\":\"Listagens e tags\",\"Model pages\":\"Páginas de modelo\",\"Use global appearance\":\"Use aparência global\",\"This logo format is unsafe or unsupported.\":\"Este formato de logotipo não é seguro ou não é compatível.\",\"The logo file could not be read.\":\"O arquivo do logotipo não pôde ser lido.\",\"MultiCam tools\":\"Ferramentas MultiCam\",\"Page\":\"Página\",\"Panel\":\"Painel\",\"Card surface\":\"Superfície do cartão\",\"Input\":\"Entrada\",\"Border\":\"Fronteira\",\"Text\":\"Texto\",\"Muted text\":\"Texto silenciado\",\"Success\":\"Sucesso\",\"Warning\":\"Aviso\",\"Danger\":\"Perigo\",\"Info and focus\":\"Informação e foco\",\"Data hub\":\"Centro de dados\",\"Text / page\":\"Texto/página\",\"Text / panel\":\"Texto/painel\",\"Muted text / panel\":\"Texto/painel silenciado\",\"Accent text / accent\":\"Texto acentuado/acento\",\"Desktop\":\"Área de trabalho\",\"Tablet\":\"Tábua\",\"Mobile\":\"Móvel\",\"Brand and logo studio\":\"Estúdio de marca e logotipo\",\"Monogram\":\"Monograma\",\"Tagline\":\"Slogan\",\"Brand display\":\"Exibição da marca\",\"Full name\":\"Nome completo\",\"Uploaded logo\":\"Logotipo enviado\",\"Logo shape\":\"Formato do logotipo\",\"Rounded\":\"Arredondado\",\"Circle\":\"Círculo\",\"Square\":\"Quadrado\",\"Pill\":\"Pílula\",\"Page title\":\"Título da página\",\"Website original\":\"Original do site\",\"Brand plus original\":\"Marca mais original\",\"Custom title\":\"Título personalizado\",\"Custom page title\":\"Título da página personalizado\",\"Image fit\":\"Ajuste de imagem\",\"Horizontal position\":\"Posição horizontal\",\"Vertical position\":\"Posição vertical\",\"Upload light-mode logo\":\"Carregar logotipo do modo claro\",\"Upload dark-mode logo\":\"Carregar logotipo do modo escuro\",\"Remove logos\":\"Remover logotipos\",\"Show version\":\"Mostrar versão\",\"Use logo as tab icon on RoomGrid pages\":\"Use o logotipo como ícone de guia nas páginas RoomGrid\",\"Replace website logo in the original header (local only)\":\"Substitua o logotipo do site no cabeçalho original (somente local)\",\"Complete color system\":\"Sistema de cores completo\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Controle as cores da página, painel, cartão, entrada, borda, texto e status, além de acentos.\",\"Derived automatically\":\"Derivado automaticamente\",\"Fully custom\":\"Totalmente personalizado\",\"Editing dark palette\":\"Editando paleta escura\",\"Editing light palette\":\"Editando paleta de luz\",\"Reset current palette\":\"Redefinir paleta atual\",\"Layout editor\":\"Editor de layout\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Defina o espaçamento do conteúdo, a largura do cartão, o cabeçalho e a barra lateral de forma consistente nas páginas do RoomGrid.\",\"Minimum card width\":\"Largura mínima do cartão\",\"Section spacing\":\"Espaçamento entre seções\",\"Content padding\":\"Preenchimento de conteúdo\",\"Header height\":\"Altura do cabeçalho\",\"Maximum columns\":\"Colunas máximas\",\"Automatic\":\"Automático\",\"Card aspect\":\"Aspecto da carta\",\"Sidebar position\":\"Posição da barra lateral\",\"Left\":\"Esquerda\",\"Right\":\"Certo\",\"Keep header visible while scrolling\":\"Mantenha o cabeçalho visível durante a rolagem\",\"Live device preview\":\"Pré-visualização ao vivo do dispositivo\",\"Contrast and accessibility\":\"Contraste e acessibilidade\",\"High-contrast mode\":\"Modo de alto contraste\",\"Underline links\":\"Sublinhar links\",\"Reduce transparency\":\"Reduza a transparência\",\"Boost base text size\":\"Aumentar o tamanho do texto base\",\"Focus ring width\":\"Largura do anel de foco\",\"Minimum target size\":\"Tamanho mínimo de destino\",\"Respect system reduced-motion setting\":\"Respeite a configuração de movimento reduzido do sistema\",\"Apply accessible defaults\":\"Aplicar padrões acessíveis\",\"Profiles by page and screen size\":\"Perfis por página e tamanho de tela\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Salve uma ou mais aparências e atribua-as por tipo de página para desktop, tablet e celular. Os perfis mudam automaticamente quando o tamanho da tela muda.\",\"Enable responsive design profiles\":\"Habilite perfis de design responsivos\",\"Mobile maximum width\":\"Largura máxima móvel\",\"Tablet maximum width\":\"Largura máxima do tablet\",\"RoomGrid design system\":\"Sistema de design RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Escolha a direção da marca, a largura do conteúdo e a densidade das informações do cartão. Cada página segue imediatamente.\",\"Primary accent\":\"Sotaque primário\",\"Secondary accent\":\"Sotaque secundário\",\"Content width\":\"Largura do conteúdo\",\"Centered\":\"Centralizado\",\"Wide\":\"Largo\",\"Full width\":\"Largura total\",\"Card style\":\"Estilo de cartão\",\"Visual\":\"Visuais\",\"Data-rich\":\"Rico em dados\",\"Widget density\":\"Densidade de widgets\",\"Spacious\":\"Espaçoso\",\"Theme and surfaces\":\"Tema e superfícies\",\"Surface style\":\"Estilo de superfície\",\"Soft\":\"Suave\",\"Flat\":\"Plano\",\"Glass\":\"Vidro\",\"Page background\":\"Plano de fundo da página\",\"Solid\":\"Sólido\",\"Gradient\":\"Gradiente\",\"Ambient glow\":\"Brilho ambiente\",\"Surface opacity\":\"Opacidade da superfície\",\"Custom accent color\":\"Cor de destaque personalizada\",\"Sizing and motion\":\"Dimensionamento e movimento\",\"Font style\":\"Estilo da fonte\",\"System\":\"Sistema\",\"Classic serif\":\"Serifa clássica\",\"Monospace\":\"Monoespaçado\",\"Density\":\"Densidade\",\"Font scale\":\"Escala de fonte\",\"Control size\":\"Tamanho do controle\",\"Corner radius\":\"Raio de canto\",\"Card spacing\":\"Espaçamento entre cartões\",\"Shadow strength\":\"Força da sombra\",\"Sidebar width\":\"Largura da barra lateral\",\"Enable animations\":\"Habilitar animações\",\"Appearance profiles\":\"Perfis de aparência\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Salve a aparência completa como um perfil reutilizável. Os perfis permanecem disponíveis após as atualizações do script.\",\"Save current appearance\":\"Salvar a aparência atual\",\"Import profile\":\"Perfil de importação\",\"No profiles saved yet.\":\"Nenhum perfil salvo ainda.\",\"Instant preview\":\"Pré-visualização instantânea\",\"Example card\":\"Cartão de exemplo\",\"This is the combined result of the current visual settings.\":\"Este é o resultado combinado das configurações visuais atuais.\",\"Example action\":\"Ação de exemplo\",\"Per-page design profiles\":\"Perfis de design por página\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Escolha uma aparência salva diferente para Home, listagens, páginas de modelo, MultiCam, Discover e hub de dados.\",\"Enable per-page design\":\"Ativar design por página\",\"Normal website pages\":\"Páginas normais do site\",\"Enable RoomGrid page design\":\"Ativar o design da página RoomGrid\",\"Tag and category pages\":\"Páginas de tags e categorias\",\"Cards\":\"Cartões\",\"List\":\"Lista\",\"Page density\":\"Densidade de página\",\"Polish navigation\":\"Navegação polonesa\",\"Polish sidebars\":\"Barras laterais polonesas\",\"Hide recognized promo and merch areas\":\"Ocultar áreas promocionais e de produtos reconhecidos\",\"Compact footer\":\"Rodapé compacto\",\"Mark saved models\":\"Marcar modelos salvos\",\"Mark favorites separately\":\"Marcar favoritos separadamente\",\"Mark visited models\":\"Marcar modelos visitados\",\"Show card quick actions\":\"Mostrar ações rápidas do cartão\",\"Animated live previews on home and listing pages\":\"Visualizações animadas ao vivo nas páginas inicial e de listagem\",\"Preview refresh rate\":\"Taxa de atualização de visualização\",\"Off\":\"Desligado\",\"Update mode\":\"Modo de atualização\",\"All visible cards simultaneously\":\"Todos os cartões visíveis simultaneamente\",\"Performance-bounded batches\":\"Lotes limitados por desempenho\",\"Batch concurrency limit\":\"Limite de simultaneidade em lote\",\"All visible\":\"Tudo visível\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Somente cartões visíveis recebem atualizações de quadros JPEG silenciados; eles pausam imediatamente fora da tela ou em uma guia oculta. O modo sincronizado atualiza todos os cartões visíveis em uma onda; altas taxas aumentam visivelmente a largura de banda e o uso da CPU.\",\"Live video on hover\":\"Vídeo ao vivo ao passar o mouse\",\"Play real-time video while hovering\":\"Reproduza vídeo em tempo real enquanto paira\",\"Start delay\":\"Atraso de início\",\"Immediately\":\"Imediatamente\",\"Hover enlargement\":\"Ampliação instantânea\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Apenas a carta sob o ponteiro é reproduzida, sempre silenciada. Afastar ou ocultar a guia interrompe o fluxo imediatamente.\",\"Live Preview Dock\":\"Doca de visualização ao vivo\",\"Enable low-load live previews\":\"Habilite visualizações ao vivo com pouca carga\",\"Concurrent previews\":\"Visualizações simultâneas\",\"Window size\":\"Tamanho da janela\",\"Small\":\"Pequeno\",\"Medium\":\"Médio\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"As visualizações começam sem som e pausam em guias ocultas. O áudio sempre requer um clique explícito.\",\"Quick info & chat focus\":\"Informações rápidas e foco no bate-papo\",\"Enable model Quick View\":\"Ativar visualização rápida do modelo\",\"Chat focus mode (opt-in)\":\"Modo de foco de bate-papo (opt-in)\",\"Show local timestamps\":\"Mostrar carimbos de data/hora locais\",\"System messages\":\"Mensagens do sistema\",\"Show\":\"Mostrar\",\"Dim\":\"Escurecer\",\"Hide\":\"Esconder\",\"Chat filter strength\":\"Força do filtro de bate-papo\",\"Low\":\"Baixo\",\"Strict\":\"Estrito\",\"Highlight words (comma separated)\":\"Destacar palavras (separadas por vírgula)\",\"Muted words (comma separated)\":\"Palavras silenciadas (separadas por vírgula)\",\"Collapse repeated messages\":\"Recolher mensagens repetidas\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"As melhorias no bate-papo alteram apenas a exibição atual. Eles nunca armazenam bate-papo, enviam mensagens ou dão gorjetas automaticamente.\",\"Shortcuts\":\"Atalhos\",\"Command menu\":\"Menu de comando\",\"Global model search\":\"Pesquisa de modelo global\",\"Reset design defaults\":\"Redefinir padrões de design\",\"Optional modules\":\"Módulos opcionais\",\"Normal-page dock and card actions\":\"Encaixe de página normal e ações de cartão\",\"Real audio meters\":\"Medidores de áudio reais\",\"Media, app and contest directories\":\"Diretórios de mídia, aplicativos e concursos\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"As opções de módulo entram em vigor após recarregar a página. Os dados principais e os módulos de backup permanecem ativados para evitar perda de dados.\",\"Name the current filters and sorting\":\"Nomeie os filtros e classificação atuais\",\"My smart view\":\"Minha visão inteligente\",\"Delete smart view\":\"Excluir visualização inteligente\",\"Smart view deleted\":\"Visualização inteligente excluída\",\"tags\":\"etiquetas\",\"countries\":\"países\",\"Saved selection\":\"Seleção salva\",\"Suggestions\":\"Sugestões\",\"Selected\":\"Selecionado\",\"Website tags\":\"Tags do site\",\"Countries (multiple selection)\":\"Países (seleção múltipla)\",\"Select countries\":\"Selecione os países\",\"Search countries…\":\"Pesquisar países…\",\"Recognized in current models\":\"Reconhecido nos modelos atuais\",\"All countries\":\"Todos os países\",\"Tags (multiple selection)\":\"Tags (seleção múltipla)\",\"Select tags\":\"Selecione tags\",\"Search tags…\":\"Pesquisar tags…\",\"Room subject\":\"Tema da sala\",\"Current online duration\":\"Tempo online atual\",\"Local watch time and sessions\":\"Tempo de visualização local e sessões\",\"HD, new, gaming and favorite badges\":\"Selos de HD, novo, jogos e favorito\",\"Homepage Studio\":\"Estúdio da página inicial\",\"Balanced preset\":\"Predefinição equilibrada\",\"Compact preset\":\"Predefinição compacta\",\"Data-rich preset\":\"Predefinição rica em dados\",\"Cinema preset\":\"Predefinição cinema\",\"Layout and pagination\":\"Layout e paginação\",\"Show quick toolbar above models\":\"Mostrar a barra rápida acima dos modelos\",\"Cards per row\":\"Cartões por linha\",\"Image ratio\":\"Proporção da imagem\",\"Information display\":\"Exibição de informações\",\"Detailed\":\"Detalhado\",\"Overlay on image\":\"Sobreposição na imagem\",\"Powerful sorting\":\"Ordenação avançada\",\"Primary sort\":\"Classificação primária\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"A ordenação altera apenas a ordem de exibição dos cartões carregados. Ela não muda os seguidos nem os favoritos da sua conta do Chaturbate.\",\"Restore website order\":\"Restaurar a ordem do site\",\"Clear all filters\":\"Limpar todos os filtros\",\"Search and range filters\":\"Pesquisa e filtros de intervalo\",\"When several tags are selected\":\"Quando várias tags são selecionadas\",\"Model must contain every tag\":\"O modelo deve conter todas as tags\",\"At least one tag matches\":\"Pelo menos uma tag corresponde\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"A filtragem de país mostra modelos de qualquer país selecionado. Tags próprias e tags de sites reconhecidos permanecem disponíveis na lista e podem ser encontradas rapidamente com a pesquisa.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Depois de selecionar tags ou países, o RoomGrid pesquisa todo o catálogo ativo atual. A mesma página de resultados mostra inicialmente 120 correspondências e carrega mais em partes durante a rolagem, de modo que mesmo tags muito comuns permanecem responsivas. O botão recarregar na barra de ferramentas da página inicial atualiza a lista ativa.\",\"Quick conditions\":\"Condições rápidas\",\"HD models only\":\"Somente modelos HD\",\"New models only\":\"Somente modelos novos\",\"Gaming models only\":\"Somente modelos de jogos\",\"Only saved in RoomGrid\":\"Somente salvos no RoomGrid\",\"Only own favorites\":\"Somente favoritos próprios\",\"Following only\":\"Seguindo apenas\",\"Hide previously watched models\":\"Ocultar modelos vistos anteriormente\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"País, idade e tempo online vêm dos cartões públicos atuais ou do diretório público de salas. Se o Chaturbate não fornecer um campo, o RoomGrid não o adivinha.\",\"Smart filter views\":\"Visualizações de filtros inteligentes\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Salve os filtros e a classificação atuais como uma visualização dinâmica. Os modelos que ficam online posteriormente e atendem às regras aparecem automaticamente.\",\"Save current filter view\":\"Salvar visualização de filtro atual\",\"No smart views saved yet.\":\"Nenhuma visualização inteligente foi salva ainda.\",\"What to show for each model\":\"O que mostrar para cada modelo\",\"Import complete\":\"Importação concluída\",\"Invalid file\":\"Arquivo inválido\",\"Range\":\"Alcance\",\"Today\":\"Hoje\",\"Week\":\"Semana\",\"Month\":\"Mês\",\"Year\":\"Ano\",\"Recording enabled\":\"Gravação ativada\",\"Private session (do not record)\":\"Sessão privada (não gravar)\",\"Retention days\":\"Dias de retenção\",\"Usage time\":\"Tempo de uso\",\"Summed camera time\":\"Tempo de câmera somado\",\"Visits\":\"Visitas\",\"Known models\":\"Modelos conhecidos\",\"Weekly heatmap\":\"Mapa de calor semanal\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Agregado por dia da semana e hora; mais escuro significa mais tempo observado.\",\"Sunday to Saturday, 24 hours per row\":\"Domingo a sábado, 24 horas por fila\",\"Watch ranking\":\"Assistir classificação\",\"little data\":\"poucos dados\",\"No watch history yet.\":\"Ainda não há histórico de exibição.\",\"Workspace, filter and quality events\":\"Espaço de trabalho, filtro e eventos de qualidade\",\"Recent timeline\":\"Linha do tempo recente\",\"Export sensitive statistics\":\"Exportar estatísticas confidenciais\",\"Import statistics\":\"Estatísticas de importação\",\"Delete selected range\":\"Excluir intervalo selecionado\",\"Delete local events in the selected range?\":\"Excluir eventos locais no intervalo selecionado?\",\"Clear all statistics\":\"Limpar todas as estatísticas\",\"Permanently clear all local statistics?\":\"Limpar permanentemente todas as estatísticas locais?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"As exportações de estatísticas contêm nomes de modelos e carimbos de data/hora e devem ser tratadas como confidenciais. Os dados antigos são limpos de acordo com a retenção.\",\"new\":\"novo\",\"Under 1 minute\":\"Menos de 1 minuto\",\"1–5 minutes\":\"1–5 minutos\",\"5–15 minutes\":\"5–15 minutos\",\"15–30 minutes\":\"15–30 minutos\",\"30+ minutes\":\"Mais de 30 minutos\",\"Local observations and your usage\":\"Observações locais e seu uso\",\"Public profile loaded on request\":\"Perfil público carregado mediante solicitação\",\"Local RoomGrid interaction\":\"Interação RoomGrid local\",\"Local browser player\":\"Player de navegador local\",\"Optional official statistics API\":\"API de estatísticas oficiais opcional\",\"Local daily aggregation\":\"Agregação diária local\",\"Local data\":\"Dados locais\",\"Model summaries\":\"Resumos de modelos\",\"Public profiles\":\"Perfis públicos\",\"Usage events\":\"Eventos de uso\",\"Official account snapshots\":\"Instantâneos da conta oficial\",\"Daily rollups\":\"Rollups diários\",\"Camera time\":\"Tempo da câmera\",\"vs previous period\":\"versus período anterior\",\"Trends\":\"Tendências\",\"Daily activity\":\"Atividade diária\",\"Playback quality\":\"Qualidade de reprodução\",\"Privacy status\":\"Status de privacidade\",\"Data provenance\":\"Proveniência dos dados\",\"Local RoomGrid usage\":\"Uso local do RoomGrid\",\"7 days\":\"7 dias\",\"30 days\":\"30 dias\",\"90 days\":\"90 dias\",\"Not enough data yet.\":\"Ainda não há dados suficientes.\",\"Models revisited\":\"Modelos revisitados\",\"Previews opened\":\"Pré-visualizações abertas\",\"Quick Views\":\"Visualizações rápidas\",\"Samples\":\"Amostras\",\"Private session\":\"Sessão privada\",\"History paused\":\"Histórico pausado\",\"Recording locally\":\"Gravando localmente\",\"Disabled\":\"Desativado\",\"No cloud sync or telemetry.\":\"Sem sincronização na nuvem ou telemetria.\",\"Raw events\":\"Eventos brutos\",\"Only local observations and actions from this browser.\":\"Apenas observações e ações locais deste navegador.\",\"Time\":\"Hora\",\"Models\":\"Modelos\",\"Online planner\":\"Planejador on-line\",\"Data & privacy\":\"Dados e privacidade\",\"Configure statistics dashboard\":\"Configurar painel de estatísticas\",\"Preset\":\"Predefinido\",\"Minimal\":\"Mínimo\",\"Quality focus\":\"Foco na qualidade\",\"Privacy focus\":\"Foco na privacidade\",\"Reset dashboard\":\"Redefinir painel\",\"Size\":\"Tamanho\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Ordem, visibilidade e largura são armazenadas em configurações seguras para atualização. Cada widget mostra sua fonte de dados.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Nenhum widget do painel está visível. Abra a configuração e selecione pelo menos um widget.\",\"Median session\":\"Sessão mediana\",\"Daily usage trend\":\"Tendência de uso diário\",\"Your trend appears here after you start using RoomGrid.\":\"Sua tendência aparece aqui depois que você começa a usar o RoomGrid.\",\"Session-duration distribution\":\"Distribuição da duração da sessão\",\"No session-duration data yet.\":\"Ainda não há dados de duração da sessão.\",\"Return rate\":\"Taxa de retorno\",\"Total sessions\":\"Total de sessões\",\"Longest observed\":\"Mais longo observado\",\"Model ranking\":\"Classificação do modelo\",\"Select up to four models for a local comparison.\":\"Selecione até quatro modelos para uma comparação local.\",\"Quick View\":\"Visualização rápida\",\"No model data yet.\":\"Ainda não há dados do modelo.\",\"Models with time patterns\":\"Modelos com padrões de tempo\",\"High confidence\":\"Alta confiança\",\"Moderate confidence\":\"Confiança moderada\",\"Planning horizon\":\"Horizonte de planejamento\",\"Likely next active times\":\"Prováveis próximos horários ativos\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Calculado a partir de amostras online observadas localmente; não garante uma transmissão. A confiança melhora com mais dados.\",\"High\":\"Alto\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Ainda não há observações online suficientes. O planejador aparece automaticamente com a biblioteca.\",\"Discovery events\":\"Eventos de descoberta\",\"Local interaction funnel\":\"Funil de interação local\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Somente suas ações neste dispositivo serão contadas; o conteúdo do bate-papo e os pagamentos nunca são rastreados.\",\"Workspace events\":\"Eventos do espaço de trabalho\",\"Filter events\":\"Filtrar eventos\",\"Tag events\":\"Marcar eventos\",\"Top-four workspace comparison\":\"Comparação dos quatro principais espaços de trabalho\",\"Workspace usage\":\"Uso do espaço de trabalho\",\"No workspace data yet.\":\"Ainda não há dados do espaço de trabalho.\",\"Average active streams\":\"Média de streams ativos\",\"Playback quality by model\":\"Qualidade de reprodução por modelo\",\"No quality data yet.\":\"Ainda não há dados de qualidade.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"O arquivo de estatísticas é inválido ou excede os limites seguros. Os dados atuais não foram alterados.\",\"Statistics import assistant · preview\":\"Assistente de importação de estatísticas · visualização\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Nada mudou ainda. Selecione as seções para importar e revisar conflitos.\",\"Safe merge (recommended)\":\"Mesclagem segura (recomendado)\",\"Import complete. The previous statistics were backed up automatically.\":\"Importação concluída. As estatísticas anteriores foram copiadas automaticamente.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"A importação não pôde ser aplicada com segurança. Os dados e backups atuais permanecem inalterados.\",\"File schema\":\"Esquema de arquivo\",\"Date range\":\"Período\",\"Sensitive data\":\"Dados confidenciais\",\"Not marked sensitive\":\"Não marcado como sensível\",\"Until manually resumed\":\"Até ser retomado manualmente\",\"Not paused\":\"Não pausado\",\"Restore this statistics backup? The current state will be backed up first.\":\"Restaurar este backup de estatísticas? O estado atual será copiado primeiro.\",\"Permanently delete this backup?\":\"Excluir permanentemente este backup?\",\"Events\":\"Eventos\",\"Estimated statistics storage\":\"Armazenamento estimado de estatísticas\",\"Recording, pauses and private sessions\":\"Gravação, pausas e sessões privadas\",\"Record usage statistics\":\"Registrar estatísticas de uso\",\"Pause 15 minutes\":\"Pausa 15 minutos\",\"Pause 1 hour\":\"Pausa 1 hora\",\"Pause 8 hours\":\"Pausa 8 horas\",\"Pause indefinitely\":\"Pausar indefinidamente\",\"Resume history\":\"Retomar histórico\",\"History status\":\"Status do histórico\",\"Private for 30 minutes\":\"Privado por 30 minutos\",\"Private for 2 hours\":\"Privado por 2 horas\",\"Private until resumed\":\"Privado até ser retomado\",\"End private session\":\"Encerrar sessão privada\",\"Not active\":\"Não ativo\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Uma sessão privada também interrompe observações de modelos públicos; uma pausa normal no histórico interrompe o uso pessoal e os registros de qualidade.\",\"Automatic deletion and retention\":\"Exclusão e retenção automáticas\",\"Raw events (days)\":\"Eventos brutos (dias)\",\"Quality samples (days)\":\"Amostras de qualidade (dias)\",\"Account snapshots (days)\":\"Instantâneos da conta (dias)\",\"Daily rollups (days)\":\"Rollups diários (dias)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Os eventos brutos expirados são acumulados em totais diários antes da exclusão automática. Os instantâneos de qualidade e de conta usam seus próprios limites.\",\"Statistics backup center\":\"Centro de backup de estatísticas\",\"Create internal backup now\":\"Crie backup interno agora\",\"Pseudonymized export\":\"Exportação pseudonimizada\",\"Print report\":\"Imprimir relatório\",\"Choose import file\":\"Escolha o arquivo de importação\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Ainda não há backups de estatísticas internas. Até três são mantidos após importações, restaurações ou criação manual.\",\"Delete local statistics in this range? A backup will be created first.\":\"Excluir estatísticas locais neste intervalo? Um backup será criado primeiro.\",\"Clear everything\":\"Limpar tudo\",\"Permanently clear all local statistics? A backup will be created first.\":\"Limpar permanentemente todas as estatísticas locais? Um backup será criado primeiro.\",\"Data inventory and provenance\":\"Inventário e proveniência de dados\",\"Stored only in this browser\":\"Armazenado apenas neste navegador\",\"Coverage\":\"Cobertura\",\"Unknown or missing data is never inferred.\":\"Dados desconhecidos ou ausentes nunca são inferidos.\",\"Metric definitions & data quality\":\"Definições de métricas e qualidade de dados\",\"Accumulated locally while a RoomGrid page is active.\":\"Acumulado localmente enquanto uma página RoomGrid está ativa.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Soma de todas as sessões de câmera observadas; fluxos simultâneos são adicionados.\",\"browser samples; no interpolation.\":\"amostras de navegador; sem interpolação.\",\"Privacy boundary\":\"Limite de privacidade\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Nenhum conteúdo de bate-papo, pagamentos ou arquivos de mídia são salvos. Sessões privadas não adicionam eventos.\",\"No history yet\":\"Ainda não há histórico\",\"Loading\":\"Carregando\",\"Connected\":\"Conectado\",\"Locked\":\"Bloqueado\",\"Not connected\":\"Não conectado\",\"Username\":\"Nome de usuário\",\"24-character stats token\":\"Token de estatísticas de 24 caracteres\",\"At least 8 characters\":\"Pelo menos 8 caracteres\",\"Secure official statistics connection\":\"Conexão segura de estatísticas oficiais\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Opcional. O token nunca entra em configurações, registros ou exportações normais. O modo de sessão mantém apenas na memória; o modo criptografado usa armazenamento local isolado.\",\"Stats token\":\"Token de estatísticas\",\"Vault password\":\"Senha do cofre\",\"Connect for this session\":\"Conecte-se para esta sessão\",\"Username or token is invalid.\":\"O nome de usuário ou token é inválido.\",\"Encrypt, save & connect\":\"Criptografar, salvar e conectar\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Não foi possível salvar criptografado. Verifique o token, a senha e as permissões do gerenciador de userscript.\",\"Get official token\":\"Obtenha token oficial\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"A atualização automática é executada no máximo uma vez a cada cinco minutos. Este módulo usa apenas um token oficial que você fornece deliberadamente.\",\"Account\":\"Conta\",\"History snapshots\":\"Instantâneos do histórico\",\"Last refresh\":\"Última atualização\",\"Unlock local vault\":\"Desbloquear cofre local\",\"Password\":\"Senha\",\"Unlock\":\"Desbloquear\",\"Unlock failed.\":\"Falha no desbloqueio.\",\"Loading…\":\"Carregando…\",\"Refresh now\":\"Atualizar agora\",\"Auto-refresh every 5 minutes\":\"Atualização automática a cada 5 minutos\",\"Disconnect and delete token\":\"Desconectar e excluir token\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Excluir a conexão local e o token criptografado? As estatísticas históricas permanecem.\",\"Broadcast status\":\"Status da transmissão\",\"Token balance\":\"Saldo de tokens\",\"Tips last hour\":\"Dicas de última hora\",\"Followers\":\"Seguidores\",\"Up votes\":\"Votos positivos\",\"Down votes\":\"Votos negativos\",\"Satisfaction\":\"Satisfação\",\"Registered viewers\":\"Espectadores registrados\",\"Tokened viewers\":\"Visualizadores tokenizados\",\"Time online\":\"Tempo on-line\",\"Follower trend\":\"Tendência de seguidor\",\"Tips trend\":\"Tendência de dicas\",\"Token-balance trend\":\"Tendência de saldo de token\",\"Viewer trend\":\"Tendência do visualizador\",\"Account Health Center\":\"Centro de saúde da conta\",\"Official My Collection\":\"Oficial minha coleção\",\"Official support\":\"Suporte oficial\",\"Export local statistics\":\"Exportar estatísticas locais\",\"Delete snapshot history\":\"Excluir histórico de snapshot\",\"Delete the local official-statistics snapshots for this account?\":\"Excluir os instantâneos de estatísticas oficiais locais desta conta?\",\"Last error\":\"Último erro\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Estado do token, cadência de atualização, história local e links oficiais vivem aqui. RoomGrid nunca publica, envia mensagens ou realiza transações automaticamente.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"O navegador não concedeu permissão de notificação. As notificações na página permanecem ativas.\",\"Notification profile\":\"Perfil de notificação\",\"Go-live only\":\"Somente transmissão ao vivo\",\"Personalized\":\"Personalizado\",\"Always (all events)\":\"Sempre (todos os eventos)\",\"Inactive days\":\"Dias inativos\",\"Favorites, rating, priority and your tags decide relevance.\":\"Favoritos, classificação, prioridade e suas tags decidem a relevância.\",\"Includes go-live, offline and public-profile changes.\":\"Inclui alterações de entrada ao vivo, off-line e de perfil público.\",\"Master switch\":\"Interruptor mestre\",\"On\":\"Ligado\",\"Off (default)\":\"Desativado (padrão)\",\"Monitored models\":\"Modelos monitorados\",\"Active tab\":\"Guia ativa\",\"This tab\":\"Esta guia\",\"Another tab\":\"Outra aba\",\"Local notifications\":\"Notificações locais\",\"Notification delivery and display\":\"Entrega e exibição de notificações\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"As notificações na página usam a área livre na barra superior do RoomGrid e não precisam de permissão do navegador. Posição, duração e densidade são configuráveis.\",\"Delivery\":\"Entrega\",\"RoomGrid bar only\":\"Somente barra RoomGrid\",\"Browser notifications only\":\"Apenas notificações do navegador\",\"RoomGrid bar and browser\":\"Barra e navegador RoomGrid\",\"No display, history only\":\"Sem exibição, apenas histórico\",\"In-page position\":\"Posição na página\",\"Free top-bar area\":\"Área gratuita do bar superior\",\"Floating top center\":\"Centro superior flutuante\",\"Floating top right\":\"Flutuando no canto superior direito\",\"Display style\":\"Estilo de exibição\",\"Compact single line\":\"Linha única compacta\",\"Display duration\":\"Duração da exibição\",\"Until dismissed\":\"Até ser demitido\",\"Visible at once\":\"Visível de uma vez\",\"Test notification\":\"Notificação de teste\",\"General rules\":\"Regras gerais\",\"Enable notifications\":\"Habilitar notificações\",\"The browser did not grant notification permission.\":\"O navegador não concedeu permissão de notificação.\",\"Neutral text\":\"Texto neutro\",\"Quiet time starts\":\"O tempo de silêncio começa\",\"Quiet time ends\":\"O tempo de silêncio termina\",\"Cooldown (minutes)\":\"Tempo de espera (minutos)\",\"Hourly limit\":\"Limite por hora\",\"Favorite threshold\":\"Limite favorito\",\"Open on click\":\"Abrir com clique\",\"Per model\":\"Por modelo\",\"Model\":\"Modelo\",\"Library is empty.\":\"A biblioteca está vazia.\",\"Per group\":\"Por grupo\",\"Group\":\"Grupo\",\"No groups.\":\"Sem grupos.\",\"Local notification list\":\"Lista de notificações locais\",\"Mark all read\":\"Marcar tudo como lido\",\"Clear list\":\"Limpar lista\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"As verificações em segundo plano apenas leem o status e não iniciam nenhum vídeo. Um script de usuário só pode ser executado enquanto uma guia do site relacionada estiver aberta; várias guias escolhem automaticamente um verificador.\",\"Shield status\":\"Status do escudo\",\"Ready\":\"Pronto\",\"Local PIN\":\"PIN local\",\"Set\":\"Definir\",\"Not set\":\"Não definido\",\"Quick shield\":\"Escudo rápido\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Cobre imediatamente nomes e imagens e pausa/silencia todas as mídias. A restauração retorna o estado anterior da mídia.\",\"Activate now\":\"Ative agora\",\"Auto-shield after inactivity\":\"Proteção automática após inatividade\",\"Set or change local PIN\":\"Definir ou alterar o PIN local\",\"Leave empty to remove\":\"Deixe em branco para remover\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"O PIN é apenas uma barreira de acesso local, não é criptografia, e não substitui a proteção da conta do sistema operacional.\",\"History controls\":\"Controles de histórico\",\"Usage statistics\":\"Estatísticas de uso\",\"Pause history 1 hour\":\"Pausar histórico por 1 hora\",\"Clear statistics\":\"Limpar estatísticas\",\"Clear all local statistics? A backup will be created first.\":\"Limpar todas as estatísticas locais? Um backup será criado primeiro.\",\"Clear Discover settings\":\"Limpar configurações do Discover\",\"Clear local Discover filters and feeds?\":\"Limpar filtros e feeds locais do Discover?\",\"Clear notification list\":\"Limpar lista de notificações\",\"Data boundary\":\"Limite de dados\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Configuração, estatísticas de observação, histórico de notificações e preferências do Discover são armazenados localmente neste navegador. RoomGrid não contém sincronização na nuvem, upload de conta ou telemetria.\",\"Quota\":\"Cota\",\"Used\":\"Usado\",\"Sensitivity & consent center\":\"Centro de sensibilidade e consentimento\",\"Standard configuration\":\"Configuração padrão\",\"Theme, layout, filters\":\"Tema, layout, filtros\",\"Local\":\"locais\",\"Model names, notes, timestamps, statistics\":\"Nomes de modelos, notas, carimbos de data/hora, estatísticas\",\"Warn on export\":\"Alertar sobre exportação\",\"Secret\":\"Segredo\",\"Optional official statistics token\":\"Token de estatísticas oficiais opcional\",\"Session-only or encrypted vault\":\"Cofre somente de sessão ou criptografado\",\"Chat content\":\"Conteúdo de bate-papo\",\"Never persisted\":\"Nunca persisti\",\"Ephemeral\":\"Efêmero\",\"Stream recording/screenshots\":\"Gravação de streaming/capturas de tela\",\"Not provided\":\"Não fornecido\",\"Policy guard\":\"Guarda de política\",\"Performance profile\":\"Perfil de desempenho\",\"Playing streams\":\"Reproduzindo transmissões\",\"Video elements\":\"Elementos de vídeo\",\"Measured memory\":\"Memória medida\",\"Unavailable in browser\":\"Indisponível no navegador\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"As medições atuais estão próximas do limite configurado. Pause câmeras não utilizadas ou escolha o modo Eco.\",\"Stream performance\":\"Desempenho de transmissão\",\"Eco\":\"Ecológico\",\"Power\":\"Poder\",\"Concurrent stream limit\":\"Limite de fluxo simultâneo\",\"Adaptive quality\":\"Qualidade adaptativa\",\"Gradual startup\":\"Inicialização gradual\",\"Automatic performance mode\":\"Modo de desempenho automático\",\"Memory threshold\":\"Limite de memória\",\"Stream-count threshold\":\"Limite de contagem de fluxo\",\"Current auto state\":\"Estado automático atual\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"As visualizações não utilizadas são interrompidas. O novo vídeo permanece pausado no limite até que a capacidade esteja disponível. As contagens altas começam gradualmente; não há garantia de “câmeras ilimitadas”.\",\"Multi-window workspaces\":\"Espaços de trabalho com múltiplas janelas\",\"Window name\":\"Nome da janela\",\"Active and recent windows\":\"Janelas ativas e recentes\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"A biblioteca de modelos é sincronizada por meio de fusão local; cada janela mantém seu próprio espaço de trabalho e configurações de visualização. As associações recentes de janelas podem ser restauradas após a reinicialização, onde o navegador permitir.\",\"Optional tools are disabled\":\"As ferramentas opcionais estão desativadas\",\"Media, app and contest directories will not scan the current page.\":\"Os diretórios de mídia, aplicativos e concursos não verificarão a página atual.\",\"Enable optional tools\":\"Habilite ferramentas opcionais\",\"Local search\":\"Pesquisa local\",\"Search saved items and current page\":\"Pesquise itens salvos e página atual\",\"Rescan current page\":\"Verificar novamente a página atual\",\"Disable optional tools\":\"Desative ferramentas opcionais\",\"Clear local side data\":\"Limpar dados locais\",\"Clear app favorites, categories and media notes?\":\"Limpar favoritos de aplicativos, categorias e notas de mídia?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Esses módulos organizam apenas links públicos e textos já acessíveis na página atual. Eles não copiam mídia, não baixam automaticamente nem operam funções de bate-papo, token ou pagamento. Login, verificação de idade, segurança e formas de pagamento permanecem intactos.\",\"Photo and video areas\":\"Áreas de fotos e vídeos\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Nenhuma área de mídia pública reconhecível está presente nesta página. Digitalize novamente enquanto estiver em uma página nativa de foto ou vídeo.\",\"Apps directory\":\"Diretório de aplicativos\",\"Local category\":\"Categoria local\",\"Found on page\":\"Encontrado na página\",\"No recognizable app links are present on this page.\":\"Nenhum link de aplicativo reconhecível está presente nesta página.\",\"Public contest results\":\"Resultados do concurso público\",\"No readable public contest table is present on this page.\":\"Nenhuma tabela legível do concurso público está presente nesta página.\",\"Native account, help and broadcaster links\":\"Conta nativa, ajuda e links de emissora\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Nenhum link nativo seguro foi encontrado nesta página; os caminhos de pagamento e login são filtrados deliberadamente.\",\"Homepage filters reset\":\"Filtros da página inicial redefinidos\",\"Global appearance reset\":\"Redefinição de aparência global\",\"Version\":\"Versão\",\"Health score\":\"Pontuação de saúde\",\"Page type\":\"Tipo de página\",\"Route generation\":\"Geração de rota\",\"Running modules\":\"Executando módulos\",\"Local errors\":\"Erros locais\",\"Repair center\":\"Centro de reparos\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Um backup da configuração é criado antes de cada reparo. As estatísticas e os dados do modelo não são excluídos por essas ações.\",\"Reset home filters\":\"Redefinir filtros iniciais\",\"Reset all home-page filters?\":\"Redefinir todos os filtros da página inicial?\",\"Reset global appearance\":\"Redefinir a aparência global\",\"Reset the global appearance? Saved design profiles remain.\":\"Redefinir a aparência global? Os perfis de design salvos permanecem.\",\"Normalize configuration\":\"Normalizar configuração\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Validar e reescrever a configuração atual? Um backup é criado primeiro.\",\"Close all previews\":\"Fechar todas as visualizações\",\"Compatibility self-check\":\"Autoverificação de compatibilidade\",\"Module and page status\":\"Status do módulo e da página\",\"Page route test matrix\":\"Matriz de teste de rota de página\",\"Current route\":\"Rota atual\",\"Trigger\":\"Gatilho\",\"Recent page transitions\":\"Transições de página recentes\",\"Capability layer\":\"Camada de capacidade\",\"Available\":\"Disponível\",\"Fallback\":\"Alternativa\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Quando uma API está faltando, apenas o módulo afetado retorna. Falhas repetidas na rede abrem temporariamente um disjuntor.\",\"Backup and recovery status\":\"Status de backup e recuperação\",\"Primary data\":\"Dados primários\",\"Stable archive\":\"Arquivo estável\",\"Rolling backups\":\"Backups contínuos\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"A autoverificação verifica apenas a legibilidade e os recursos do navegador; ele nunca substitui os dados atuais automaticamente. A recuperação continua a usar o fluxo de backup confirmado existente.\",\"Sanitized local error log\":\"Log de erros locais higienizados\",\"No RoomGrid errors recorded.\":\"Nenhum erro RoomGrid registrado.\",\"Run self-check again\":\"Execute a autoverificação novamente\",\"Export diagnostics\":\"Exportar diagnóstico\",\"Clear error log\":\"Limpar log de erros\",\"Return to normal site\":\"Voltar ao site normal\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"A exportação de diagnóstico não contém espaço de trabalho, nomes de modelos, notas ou estatísticas. Os parâmetros de consulta de URL e possíveis valores secretos são removidos do texto do erro.\",\"Local history on\":\"História local em\",\"History off\":\"Histórico desativado\",\"This settings page could not be loaded\":\"Esta página de configurações não pôde ser carregada\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Seus dados não foram alterados. Tente novamente; se o problema persistir, abra Diagnóstico.\",\"Try again\":\"Tente novamente\",\"Open Diagnostics\":\"Abrir Diagnóstico\",\"Public results\":\"Resultados públicos\",\"Public contest links\":\"Links de concursos públicos\",\"Page content has not appeared yet\":\"O conteúdo da página ainda não apareceu\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid verificou a página novamente, mas ainda não consegue encontrar a área de conteúdo do Chaturbate. A página ainda pode estar carregando ou o site retornou uma visualização vazia.\",\"Check again\":\"Verifique novamente\",\"Open RoomGrid Discover\":\"Abrir RoomGrid Discover\",\"Reload page\":\"Recarregar página\",\"Hide notice\":\"Ocultar aviso\",\"Open MultiCam\":\"Abra MultiCam\",\"Open workstation in a new window\":\"Abra a estação de trabalho em uma nova janela\",\"Open Discover\":\"Abra a descoberta\",\"Statistics center\":\"Centro de estatísticas\",\"Backup and import\":\"Backup e importação\",\"Page and design options\":\"Opções de página e design\",\"Hide immediately\":\"Ocultar imediatamente\",\"All tools\":\"Todas as ferramentas\",\"Search & commands\":\"Pesquisa e comandos\",\"Page design\":\"Design da página\",\"Official collection\":\"Coleção oficial\",\"Saved\":\"Salvo\",\"Quick info\":\"Informações rápidas\",\"Loading live frame\":\"Carregando quadro ao vivo\",\"Live still\":\"Viva ainda\",\"Not live\":\"Não vivo\",\"Pages\":\"Páginas\",\"Refresh\":\"Atualizar\",\"Search…\":\"Pesquisar…\",\"Tag Explorer\":\"Explorador de tags\",\"Refresh public catalog\":\"Atualizar catálogo público\",\"Online\":\"On-line\",\"Toggle favorite\":\"Alternar favorito\",\"Save\":\"Salvar\",\"Remove favorite\":\"Remover favorito\",\"Removed from library\":\"Removido da biblioteca\",\"Saved to library\":\"Salvo na biblioteca\",\"Added to favorites\":\"Adicionado aos favoritos\",\"Removed from favorites\":\"Removido dos favoritos\",\"Watching\":\"Assistindo\",\"Watch\":\"Assistir\",\"Temporary watchlist\":\"Lista de observação temporária\",\"Removed from watchlist\":\"Removido da lista de observação\",\"Added to watchlist\":\"Adicionado à lista de observação\",\"Choose group\":\"Escolha o grupo\",\"Add to group…\":\"Adicionar ao grupo…\",\"Group updated\":\"Grupo atualizado\",\"Site room\":\"Sala do local\",\"Select for bulk actions\":\"Selecione para ações em massa\",\"viewers\":\"espectadores\",\"No public description\":\"Nenhuma descrição pública\",\"Open in Tag Explorer\":\"Abrir no Tag Explorer\",\"Feed name\":\"Nome do feed\",\"View name\":\"Ver nome\",\"Name already exists\":\"O nome já existe\",\"Saved-item limit reached\":\"Limite de itens salvos atingido\",\"Feed saved\":\"Feed salvo\",\"View saved\":\"Ver salvo\",\"Applied\":\"Aplicado\",\"Backup exported\":\"Backup exportado\",\"Backup imported\":\"Backup importado\",\"Invalid backup\":\"Backup inválido\",\"New models\":\"Novos modelos\",\"New since last visit\":\"Novo desde a última visita\",\"New to me\":\"Novo para mim\",\"Returning\":\"Retornando\",\"Stable online\":\"Estável on-line\",\"Saved only\":\"Somente salvo\",\"Previously watched\":\"Assistido anteriormente\",\"Local collection\":\"Coleção local\",\"Hide visited today\":\"Ocultar visitado hoje\",\"No feed selected\":\"Nenhum feed selecionado\",\"No view selected\":\"Nenhuma visualização selecionada\",\"Saved feeds\":\"Feeds salvos\",\"Saved views\":\"Visualizações salvas\",\"Save new\":\"Salvar novo\",\"All public rooms\":\"Todas as salas públicas\",\"Known HD\":\"HD conhecido\",\"Random result\":\"Resultado aleatório\",\"Viewers ↓\":\"Espectadores ↓\",\"Viewers ↑\":\"Espectadores ↑\",\"Online duration ↓\":\"Duração on-line ↓\",\"Followers ↓\":\"Seguidores ↓\",\"Name A–Z\":\"Nome A – Z\",\"Name Z–A\":\"Nome Z–A\",\"New first\":\"Novo primeiro\",\"Watch time ↓\":\"Tempo de exibição ↓\",\"Sessions ↓\":\"Sessões ↓\",\"Last watched\":\"Última visualização\",\"Name, description, tag, place\":\"Nome, descrição, tag, local\",\"Secondary sort\":\"Classificação secundária\",\"Shuffle again\":\"Embaralhe novamente\",\"All groups\":\"Todos os grupos\",\"All tags (AND)\":\"Todas as tags (E)\",\"Any tag (OR)\":\"Qualquer tag (OU)\",\"Excluded tags (NOT)\":\"Tags excluídas (NÃO)\",\"Language, region, or country\":\"Idioma, região ou país\",\"Minimum online minutes\":\"Minutos mínimos on-line\",\"Maximum online minutes\":\"Máximo de minutos online\",\"Minimum local watch minutes\":\"Minutos mínimos de exibição local\",\"Advanced filters and Boolean tags\":\"Filtros avançados e tags booleanas\",\"Saved views and feeds\":\"Visualizações e feeds salvos\",\"Import\":\"Importar\",\"Recent searches\":\"Pesquisas recentes\",\"Live previews and performance\":\"Prévias e desempenho ao vivo\",\"Muted live preview for every visible card\":\"Visualização ao vivo silenciada para todos os cartões visíveis\",\"Refresh rate\":\"Taxa de atualização\",\"Real-time video while hovering\":\"Vídeo em tempo real enquanto paira\",\"Hover delay\":\"Atraso ao passar o mouse\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Os cartões visíveis usam quadros JPEG. Ao passar o mouse, apenas um cartão muda para vídeo em tempo real sem som e para completamente quando o ponteiro sai. Altas taxas de quadros aumentam significativamente a largura de banda e o uso da CPU.\",\"saved\":\"salvo\",\"previews\":\"visualizações\",\"Previews off\":\"Pré-visualizações desativadas\",\"No more feeds can be saved\":\"Não é possível salvar mais feeds\",\"Tag feed saved\":\"Feed de tags salvo\",\"Select tag\":\"Selecione a etiqueta\",\"Tag favorite\":\"Marcar favorito\",\"Block tag\":\"Etiqueta de bloqueio\",\"Select tags first\":\"Selecione as tags primeiro\",\"Tag-group name\":\"Nome do grupo de tags\",\"Tag group saved\":\"Grupo de tags salvo\",\"No gaming rooms in the current catalog\":\"Nenhuma sala de jogos no catálogo atual\",\"All (AND)\":\"Todos (E)\",\"Any (OR)\":\"Qualquer (OU)\",\"Exclude (NOT)\":\"Excluir (NÃO)\",\"Search tags\":\"Tags de pesquisa\",\"Tag search\":\"Pesquisa de tags\",\"Combination\":\"Combinação\",\"Model sorting\":\"Classificação de modelo\",\"Show in Discover\":\"Mostrar no Descobrir\",\"Save tag group\":\"Salvar grupo de tags\",\"Save tag feed\":\"Salvar feed de tags\",\"No tag groups saved yet.\":\"Nenhum grupo de tags salvo ainda.\",\"Unknown region\":\"Região desconhecida\",\"Unknown age\":\"Idade desconhecida\",\"Public “New”\":\"Público “Novo”\",\"My new cams\":\"Minhas novas câmeras\",\"New-cams feed saved\":\"Feed de novas câmeras salvo\",\"Save “My new cams” feed\":\"Salvar feed “Minhas novas câmeras”\",\"Create gaming workspace\":\"Crie um espaço de trabalho para jogos\",\"No matches for this combination.\":\"Não há correspondências para esta combinação.\",\"Select tags to preview matching rooms here.\":\"Selecione tags para visualizar as salas correspondentes aqui.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Pesquise, adicione como favorito, bloqueie e combine tags públicas. O Trends usa apenas alterações de catálogo observadas neste dispositivo.\",\"Current tags\":\"Tags atuais\",\"Counts show current matches and change since the prior fetch.\":\"As contagens mostram as correspondências atuais e as alterações desde a busca anterior.\",\"Results for selected tags\":\"Resultados para tags selecionadas\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Usa a mesma classificação e navegação SingleCam do Discover.\",\"Categories, age, and regions\":\"Categorias, idade e regiões\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Somente campos públicos explícitos são mostrados. Regiões separadas por vírgula combinam-se com OR; valores desconhecidos permanecem visíveis. Nenhum fuso horário é adivinhado.\",\"New and gaming\":\"Novo e jogos\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Visto pela primeira vez localmente, a bandeira pública “Novo”, a lista de observação temporária e os temas de jogos permanecem separados.\",\"Tag groups\":\"Grupos de tags\",\"Save AND, OR, or NOT combinations.\":\"Salve combinações AND, OR ou NOT.\",\"Favorite tags\":\"Etiquetas favoritas\",\"Your personal shortcut list.\":\"Sua lista de atalhos pessoais.\",\"No favorites yet\":\"Ainda não há favoritos\",\"Related tags\":\"Tags relacionadas\",\"Tags co-occurring on the currently matching models.\":\"Tags que ocorrem simultaneamente nos modelos atualmente correspondentes.\",\"Personal tag history\":\"Histórico de tags pessoais\",\"Recently and frequently used on this device.\":\"Usado recentemente e com frequência neste dispositivo.\",\"Bulk target group\":\"Grupo-alvo em massa\",\"Selected models added to group\":\"Modelos selecionados adicionados ao grupo\",\"New workspace name\":\"Novo nome do espaço de trabalho\",\"Created from a Discover selection\":\"Criado a partir de uma seleção do Discover\",\"Workspace created\":\"Espaço de trabalho criado\",\"Save all\":\"Salvar tudo\",\"Create workspace\":\"Criar espaço de trabalho\",\"Loading public catalog…\":\"Carregando catálogo público…\",\"Loading the complete catalog in the background…\":\"Carregando o catálogo completo em segundo plano…\",\"Updated\":\"Atualizado\",\"Public rooms\":\"Salas públicas\",\"The catalog is loading.\":\"O catálogo está carregando.\",\"No matching rooms. Adjust search or filters.\":\"Não há quartos correspondentes. Ajuste a pesquisa ou os filtros.\",\"Previous\":\"Anterior\",\"Next\":\"Próximo\",\"Move up\":\"Subir\",\"Move down\":\"Mover para baixo\",\"Number shown\":\"Número mostrado\",\"Widget width\":\"Largura do widget\",\"Full\":\"Completo\",\"Nothing here yet.\":\"Nada aqui ainda.\",\"Newly online\":\"Recentemente on-line\",\"Recently visited\":\"Visitado recentemente\",\"Frequently visited\":\"Frequentemente visitado\",\"Homepage appearance, pagination, and performance\":\"Aparência, paginação e desempenho da página inicial\",\"Information density\":\"Densidade de informação\",\"Layout\":\"Disposição\",\"Data rich\":\"Rico em dados\",\"Static\":\"Estático\",\"Show statistics summary\":\"Mostrar resumo de estatísticas\",\"Show personal widgets\":\"Mostrar widgets pessoais\",\"Sections below the live catalog\":\"Seções abaixo do catálogo ativo\",\"Homepage live models\":\"Modelos ao vivo da página inicial\",\"All live rooms in one place\":\"Todas as salas ao vivo em um só lugar\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Use a mesma pesquisa completa, classificação, grupos, tags e filtros de histórico local do Discover.\",\"Refresh live catalog\":\"Atualizar catálogo ativo\",\"Homepage settings\":\"Configurações da página inicial\",\"Matches a favorite gaming theme\":\"Corresponde a um tema de jogo favorito\",\"Saved models\":\"Modelos salvos\",\"Online favorites\":\"Favoritos on-line\",\"Local watch time\":\"Horário de exibição local\",\"Continue last MultiCam workspace\":\"Continuar o último espaço de trabalho MultiCam\",\"Open full Discover page\":\"Abra a página completa do Discover\",\"Online notifications\":\"Notificações on-line\",\"on\":\"ligado\",\"off\":\"desligado\",\"Offline notifications\":\"Notificações off-line\",\"Feed\":\"Alimentação\",\"Recent\":\"Recente\",\"Frequent\":\"Frequente\",\"Saved filter views\":\"Visualizações de filtro salvas\",\"Favorites currently public and online.\":\"Favoritos atualmente públicos e online.\",\"New models or public streams that started recently.\":\"Novos modelos ou streams públicos iniciados recentemente.\",\"Current results from the selected feed.\":\"Resultados atuais do feed selecionado.\",\"Suggestions with reasons\":\"Sugestões com motivos\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Baseado apenas em tags públicas compartilhadas e temas preferidos localmente; nenhuma inferência de identidade.\",\"Sorted by local last-visit time.\":\"Classificado pela hora local da última visita.\",\"Sorted by local watch time.\":\"Classificado por horário de exibição local.\",\"Saved models never visited\":\"Modelos salvos nunca visitados\",\"Saved in the library without local watch time.\":\"Salvo na biblioteca sem horário de exibição local.\",\"Draggable dashboard widgets\":\"Widgets de painel arrastáveis\",\"Your RoomGrid homepage\":\"Sua página inicial do RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Um catálogo ao vivo rápido e personalizável com SingleCam, MultiCam, filtros completos, visualizações salvas e seus widgets pessoais.\",\"Audio activity\":\"Atividade de áudio\",\"Auto quality\":\"Qualidade automática\",\"Stream interrupted; status checks continue.\":\"Fluxo interrompido; as verificações de status continuam.\",\"This browser cannot play HLS.\":\"Este navegador não pode reproduzir HLS.\",\"Connecting to live stream…\":\"Conectando à transmissão ao vivo…\",\"The room is not public right now.\":\"A sala não é pública no momento.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Atualmente off-line. Alternativas do mesmo filtro são mostradas abaixo.\",\"Status temporarily unavailable; retrying automatically.\":\"Status temporariamente indisponível; tentando novamente automaticamente.\",\"Loading public profile…\":\"Carregando perfil público…\",\"Profile temporarily unavailable.\":\"Perfil temporariamente indisponível.\",\"No cached profile.\":\"Nenhum perfil em cache.\",\"Public profile changed\":\"Perfil público alterado\",\"No public bio.\":\"Nenhuma biografia pública.\",\"Source\":\"Fonte\",\"Observed\":\"Observado\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Somente links autopublicados no perfil público são mostrados. Um nome de usuário correspondente por si só não é prova de identidade.\",\"Profile information\":\"Informações do perfil\",\"Preparing…\":\"Preparando…\",\"Play\":\"Jogar\",\"Unmute\":\"Ativar som\",\"Mute\":\"Mudo\",\"Contain\":\"Conter\",\"Cover\":\"Capa\",\"Brightness\":\"Brilho\",\"Contrast\":\"Contraste\",\"Saturation\":\"Saturação\",\"Hue\":\"Matiz\",\"Sepia\":\"Sépia\",\"Grayscale\":\"Tons de cinza\",\"Invert\":\"Inverter\",\"Blur\":\"Desfoque\",\"Image zoom\":\"Zoom da imagem\",\"Recording saved\":\"Gravação salva\",\"Screenshot saved\":\"Captura de tela salva\",\"Recording\":\"Gravação\",\"Mirror\":\"Espelho\",\"Flip vertically\":\"Virar verticalmente\",\"Rotate\":\"Girar\",\"Show edited\":\"Mostrar editado\",\"Show original\":\"Mostrar original\",\"Reset image\":\"Redefinir imagem\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Segure Ctrl e role para ampliar; arraste a imagem quando ampliada. Clique duas vezes para redefinir.\",\"Zoom out\":\"Diminuir zoom\",\"Zoom in\":\"Ampliar\",\"Reset zoom and position\":\"Redefinir zoom e posição\",\"Player window\":\"Janela do jogador\",\"Fullscreen\":\"Tela cheia\",\"Private notes, memories, or things to check next time (stored locally)\":\"Notas privadas, lembranças ou coisas para verificar na próxima vez (armazenadas localmente)\",\"Autosave is on\":\"O salvamento automático está ativado\",\"Enter your own tag…\":\"Insira sua própria tag…\",\"Saved at\":\"Salvo em\",\"Local details saved\":\"Detalhes locais salvos\",\"Remove tag\":\"Remover etiqueta\",\"No personal tags yet.\":\"Ainda não há tags pessoais.\",\"Saving…\":\"Salvando…\",\"Create your first group under Groups.\":\"Crie seu primeiro grupo em Grupos.\",\"Added to MultiCam\":\"Adicionado ao MultiCam\",\"Edit private details\":\"Editar detalhes privados\",\"Region\":\"Região\",\"Current online time\":\"Horário on-line atual\",\"Stream attributes\":\"Atributos de fluxo\",\"New\":\"Novo\",\"Yes\":\"Sim\",\"No / unknown\":\"Não/desconhecido\",\"Last observed\":\"Última observação\",\"Local visits\":\"Visitas locais\",\"First watched\":\"Assistido pela primeira vez\",\"No public description.\":\"Nenhuma descrição pública.\",\"No public tags available.\":\"Nenhuma tag pública disponível.\",\"Quick-add your own tag…\":\"Adicione rapidamente sua própria tag…\",\"Public tags\":\"Tags públicas\",\"Add\":\"Adicionar\",\"Private details\":\"Detalhes privados\",\"Everything stays in this browser and survives updates.\":\"Tudo permanece neste navegador e sobrevive às atualizações.\",\"Private notebook\":\"Caderno privado\",\"Keep on watchlist\":\"Manter na lista de observação\",\"Group assignment (multiple)\":\"Atribuição de grupo (múltiplo)\",\"Save now\":\"Salve agora\",\"Chat loads only when this section is opened.\":\"O bate-papo carrega apenas quando esta seção é aberta.\",\"Official chat\":\"Bate-papo oficial\",\"Official chat connected.\":\"Bate-papo oficial conectado.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Bate-papo carregado; a estrutura da página atual não pôde ser totalmente isolada.\",\"Chat loaded.\":\"Bate-papo carregado.\",\"Loading official chat…\":\"Carregando bate-papo oficial…\",\"Official chat room\":\"Sala de bate-papo oficial\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Carregado apenas quando necessário; RoomGrid nunca armazena ou envia mensagens de bate-papo automaticamente.\",\"Reload chat\":\"Recarregar bate-papo\",\"Open official room in a new window\":\"Abra a sala oficial em uma nova janela\",\"Hide chat\":\"Ocultar bate-papo\",\"Live observations\":\"Observações ao vivo\",\"Trend confidence\":\"Confiança na tendência\",\"Known time slots\":\"Horários conhecidos\",\"Typical peak\":\"Pico típico\",\"Watched in 7 days\":\"Assisti em 7 dias\",\"Watched in 30 days\":\"Assisti em 30 dias\",\"Current streak\":\"Sequência atual\",\"Typical weekly online times\":\"Horários online semanais típicos\",\"Online times will be learned on this device from now on.\":\"Os horários online serão aprendidos neste dispositivo a partir de agora.\",\"Based on observations on this device; missing information is never guessed.\":\"Com base em observações deste dispositivo; informações faltantes nunca são adivinhadas.\",\"Activity by weekday\":\"Atividade por dia da semana\",\"Most likely online times\":\"Provavelmente horários online\",\"Online-time map\":\"Mapa do tempo online\",\"Public covers and locally saved example images.\":\"Capas públicas e imagens de exemplo salvas localmente.\",\"Import quality\":\"Qualidade de importação\",\"Original file\":\"Arquivo original\",\"Very high (up to 4K)\":\"Muito alto (até 4K)\",\"High (up to 1440p)\":\"Alto (até 1440p)\",\"Space-saving (up to 900p)\":\"Economia de espaço (até 900p)\",\"Add images from computer\":\"Adicione imagens do computador\",\"Use website cover\":\"Usar capa do site\",\"Use first image as cover\":\"Use a primeira imagem como capa\",\"Covers and local gallery\":\"Capas e galeria local\",\"Updating images…\":\"Atualizando imagens…\",\"Loading images…\":\"Carregando imagens…\",\"Own image\":\"Imagem própria\",\"Example image\":\"Imagem de exemplo\",\"Current cover\":\"Capa atual\",\"Alternative preview\":\"Visualização alternativa\",\"Current public image\":\"Imagem pública atual\",\"Saving cover…\":\"Salvando capa…\",\"Set as cover\":\"Definir como capa\",\"Delete this image from the local gallery?\":\"Excluir esta imagem da galeria local?\",\"No images yet.\":\"Ainda não há imagens.\",\"saved locally\":\"salvo localmente\",\"own images\":\"próprias imagens\",\"Update public previews\":\"Atualizar visualizações públicas\",\"Processing your images…\":\"Processando suas imagens…\",\"ALL PAGE SOUNDS OFF\":\"TODOS OS SONS DA PÁGINA DESLIGADOS\",\"PAGE SOUNDS ALLOWED\":\"SONS DE PÁGINA PERMITIDOS\",\"Chat/token sounds OFF\":\"Bate-papo/token soa DESLIGADO\",\"Chat/token sounds ON\":\"Sons de bate-papo/token ativados\",\"Reconnecting the player with the new settings…\":\"Reconectando o player com as novas configurações…\",\"Turn off chat, token and tip sounds completely\":\"Desligue completamente os sons de bate-papo, token e gorjeta\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Bloqueia sons de página de elementos de áudio e áudio da Web. O streaming de áudio permanece controlável separadamente através de “Sound on”.\",\"No alternatives available.\":\"Não há alternativas disponíveis.\",\"Similar models and offline alternatives\":\"Modelos semelhantes e alternativas offline\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Priorizado por tags compartilhadas; nenhuma identidade ou inferência de conta externa.\",\"Chat\":\"Bate-papo\",\"Notes & organization\":\"Notas e organização\",\"Images\":\"Imagens\",\"SingleCam sections\":\"Seções SingleCam\",\"Back to original results\":\"Voltar aos resultados originais\",\"Private workspace\":\"Espaço de trabalho privado\",\"Chat on/off\":\"Ativar/desativar bate-papo\",\"Hide similar models\":\"Ocultar modelos semelhantes\",\"Show similar models\":\"Mostrar modelos semelhantes\",\"RoomGrid Home\":\"Página inicial do RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Catálogo público indisponível; mostrando a biblioteca local.\",\"Drag to resize sidebar\":\"Arraste para redimensionar a barra lateral\",\"The workspace could not be saved safely. Export a backup now.\":\"A área de trabalho não pôde ser salva com segurança. Exporte um backup agora.\",\"Open menu or restore controls\":\"Abra o menu ou restaure os controles\",\"Controls restored\":\"Controles restaurados\",\"Menu\":\"Cardápio\",\"Show toolbar\":\"Mostrar barra de ferramentas\",\"Tools\":\"Ferramentas\",\"Show sidebar\":\"Mostrar barra lateral\",\"Restore controls\":\"Restaurar controles\",\"Show toolbar, groups, and menus\":\"Mostrar barra de ferramentas, grupos e menus\",\"Open more tools\":\"Abra mais ferramentas\",\"all watch states\":\"todos os estados de observação\",\"Watch status\":\"Assistir status\",\"0 means no viewer limit.\":\"0 significa que não há limite de visualizadores.\",\"Import temporary URLs; they disappear after refresh\":\"Importe URLs temporários; eles desaparecem após a atualização\",\"Filter current group\":\"Filtrar grupo atual\",\"All status\":\"Todos os status\",\"Hide offline/private\":\"Ocultar off-line/privado\",\"Collapse toolbar\":\"Recolher barra de ferramentas\",\"Windows per page\":\"Janelas por página\",\"Custom grid\":\"Grade personalizada\",\"Choose MultiCam audio routing\":\"Escolha o roteamento de áudio MultiCam\",\"Audio: manual\":\"Áudio: manual\",\"Audio: solo cam\":\"Áudio: câmera solo\",\"Audio: follow focus\":\"Áudio: siga o foco\",\"Auto\":\"Automático\",\"Userscript version\":\"Versão do script do usuário\",\"Open Discover and dashboard\":\"Abra o Discover e o painel\",\"Global search and quick commands · Ctrl+K\":\"Pesquisa global e comandos rápidos · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Transmita saúde, orçamento de qualidade, áudio, cenas e substituições\",\"Control\":\"Controle\",\"Rooms\":\"Salas\",\"Filter\":\"Filtro\",\"Playback\":\"Reprodução\",\"Stabilizing live preview…\":\"Estabilizando a visualização ao vivo…\",\"Recovering live preview…\":\"Recuperando visualização ao vivo…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"A visualização ao vivo é brevemente pausada para evitar um loop de recarga. Tente novamente em breve.\",\"Collapse groups\":\"Recolher grupos\",\"Expand/collapse users\":\"Expandir/recolher usuários\",\"Output volume; no live signal level is available for this stream\":\"Volume de saída; nenhum nível de sinal ao vivo está disponível para esta transmissão\",\"OFF\":\"DESLIGADO\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Cole URLs. Páginas de sala, m3u8, mp4/webm são suportadas. As páginas normais serão analisadas com o melhor esforço.\",\"Actual resolution, connection and buffer\":\"Resolução real, conexão e buffer\",\"Drag to reorder / move group\":\"Arraste para reordenar/mover grupo\",\"All time\":\"Todo o tempo\",\"Click a model for details.\":\"Clique em um modelo para obter detalhes.\",\"Most watched groups\":\"Grupos mais assistidos\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Os campos da conta, como data de adesão, são mostrados apenas quando o Chaturbate os expõe claramente na mesma página. O script não inventa valores.\",\"Back\":\"Voltar\",\"Statistics import failed: \":\"Falha na importação de estatísticas:\",\"Recent saves\":\"Salvamentos recentes\",\"Rooms per page\":\"Salas por página\",\"Custom rows\":\"Linhas personalizadas\",\"Custom columns\":\"Colunas personalizadas\",\"Main width\":\"Largura principal\",\"Main height\":\"Altura principal\",\"Modules\":\"Módulos\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Os módulos iniciam apenas nos tipos de página correspondentes. Salvar recarrega a página atual.\",\"Tools on normal pages\":\"Ferramentas em páginas normais\",\"True audio-level meters\":\"Medidores de nível de áudio verdadeiros\",\"MultiCam control center\":\"Centro de controle MultiCam\",\"No active streams yet\":\"Ainda não há transmissões ativas\",\"Stream health\":\"Saúde do fluxo\",\"Budget off\":\"Orçamento desativado\",\"Economy · 2160p total\":\"Economia · 2160p total\",\"Balanced · 4320p total\":\"Equilibrado · 4320p total\",\"High · 6480p total\":\"Alto · 6480p total\",\"Ultra · 8640p total\":\"Ultra · 8640p no total\",\"Quality budget is off; global and per-camera limits still apply.\":\"O orçamento de qualidade está errado; limites globais e por câmera ainda se aplicam.\",\"Quality budget\":\"Orçamento de qualidade\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Distribui qualidade entre câmeras ativas. Uma qualidade explícita por câmera sempre vence.\",\"Give the focused cam double weight\":\"Dê peso duplo à câmera focada\",\"Manual per camera\":\"Manual por câmera\",\"Solo: one camera only\":\"Solo: apenas uma câmera\",\"Follow focused camera\":\"Siga a câmera focada\",\"Audio management\":\"Gerenciamento de áudio\",\"Routing never overwrites saved mute states or individual volume.\":\"O roteamento nunca substitui estados mudos salvos ou volumes individuais.\",\"Mute all\":\"Silenciar tudo\",\"Restore saved audio\":\"Restaurar áudio salvo\",\"Scene name\":\"Nome da cena\",\"Focus\":\"Foco\",\"Grid\":\"Grade\",\"cams\":\"câmeras\",\"Load\":\"Carregar\",\"No scenes saved for this workspace yet.\":\"Nenhuma cena salva para este espaço de trabalho ainda.\",\"Scenes\":\"Cenas\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Salve os slots, layout e roteamento de áudio atuais para restauração com um clique.\",\"Save current view as scene\":\"Salvar a visualização atual como cena\",\"All saved online models\":\"Todos os modelos online salvos\",\"Replacement models\":\"Modelos de substituição\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Os modelos preferidos são testados em ordem de lista; só então são utilizados os modelos online do grupo selecionado. As atribuições manuais sempre vencem.\",\"Replace offline slots automatically\":\"Substitua slots offline automaticamente\",\"Delay (seconds)\":\"Atraso (segundos)\",\"Candidate group\":\"Grupo de candidatos\",\"Preferred replacements (multiple)\":\"Substituições preferidas (múltiplas)\",\"Replacement rule saved\":\"Regra de substituição salva\",\"Save replacement rule\":\"Salvar regra de substituição\",\"Resolve duplicate shortcuts first:\":\"Resolva primeiro os atalhos duplicados:\",\"Name, note or own tag…\":\"Nome, nota ou etiqueta própria…\",\"Camera menu\":\"Menu da câmera\",\"Camera menu · quality and sound\":\"Menu da câmera · qualidade e som\",\"Close menu\":\"Fechar menu\",\"View and window\":\"Vista e janela\",\"Audio\":\"Áudio\",\"Quality and image\":\"Qualidade e imagem\",\"More actions\":\"Mais ações\",\"Currently main\":\"Atualmente principal\",\"Set as main\":\"Definir como principal\",\"Pin live preview\":\"Fixar visualização ao vivo\",\"Volume multiplier for this cam\":\"Multiplicador de volume para esta câmera\",\"Reset individual volume to 100%\":\"Redefinir o volume individual para 100%\",\"Individual volume\":\"Volume individual\",\"Listen to this cam only\":\"Ouça apenas esta câmera\",\"Set quality for this camera only\":\"Definir qualidade apenas para esta câmera\",\"Automatic (global/adaptive)\":\"Automático (global/adaptativo)\",\" · available\":\"· disponível\",\"Quality for this camera\":\"Qualidade para esta câmera\",\"Hold for original\":\"Espere pelo original\",\"Vivid\":\"Vívido\",\"Image adjustments\":\"Ajustes de imagem\",\"More tools\":\"Mais ferramentas\",\"Show top controls\":\"Mostrar controles principais\",\"Collapse top controls\":\"Recolher os controles superiores\",\"Show groups\":\"Mostrar grupos\",\"Windows\":\"Janelas\",\"Open workspace in new window\":\"Abra o espaço de trabalho em uma nova janela\",\"Control center\":\"Centro de controle\",\"Choose the data to export\":\"Escolha os dados para exportar\",\"Preview the file and conflicts before applying it\":\"Visualize o arquivo e os conflitos antes de aplicá-lo\",\"Help\":\"Ajuda\",\"Changes in this version\":\"Mudanças nesta versão\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Espaços de trabalho, biblioteca de modelos, importações seguras, design global, páginas normais estendidas, Discover/painel expandido e controles de qualidade/imagem do player.\",\"Muted\":\"Silenciado\",\"Performance limit reached\":\"Limite de desempenho atingido\",\"Drag to resize main width\":\"Arraste para redimensionar a largura principal\",\"Drag to resize main height\":\"Arraste para redimensionar a altura principal\",\"Stable\":\"Estável\",\"Unstable\":\"Instável\",\"Refresh all cameras\":\"Atualizar todas as câmeras\",\"Reconnect current models\":\"Reconecte os modelos atuais\",\"Switch to grid view\":\"Mudar para visualização em grade\",\"Switch to focus view\":\"Mudar para visualização de foco\",\"Manage workspaces\":\"Gerenciar espaços de trabalho\",\"Create, duplicate, archive and device layouts\":\"Criar, duplicar, arquivar e layouts de dispositivos\",\"Open model library\":\"Abrir biblioteca de modelos\",\"Notes, tags, ratings and bulk actions\":\"Notas, tags, classificações e ações em massa\",\"Design options\":\"Opções de design\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Estação de trabalho multivisualização completa para salas, gravação, alertas, capturas de tela, reconexões e ferramentas de dados.\",\"Username ↵\":\"Nome de usuário ↵\",\"Search rooms\":\"Pesquisar salas\",\"Invalid username\":\"Nome de usuário inválido\",\"Hide offline\":\"Ocultar off-line\",\"Hide private\":\"Ocultar privado\",\"Manual\":\"Manuais\",\"By status\":\"Por status\",\"By name\":\"Por nome\",\"By added time\":\"Por tempo adicional\",\"By watch time\":\"Por tempo de exibição\",\"By sessions\":\"Por sessões\",\"By last watched\":\"Por último assistido\",\"By first watched\":\"Pela primeira vez assistido\",\"By viewer count\":\"Por contagem de espectadores\",\"By last online\":\"Por último on-line\",\"Advanced filters\":\"Filtros avançados\",\"Watched before\":\"Assisti antes\",\"Currently in view\":\"Atualmente em exibição\",\"Include tag\":\"Incluir etiqueta\",\"Exclude tag\":\"Excluir etiqueta\",\"Minimum watch time (minutes)\":\"Tempo mínimo de exibição (minutos)\",\"Watched within days\":\"Assistido em poucos dias\",\"Refresh all\":\"Atualizar tudo\",\"Online alerts\":\"Alertas on-line\",\"Offline alerts\":\"Alertas off-line\",\"Desktop notification + card flash when a model goes online\":\"Notificação na área de trabalho + flash do cartão quando um modelo fica online\",\"Desktop notification + card flash when a model goes offline\":\"Notificação na área de trabalho + flash do cartão quando um modelo fica offline\",\"Dark mode\":\"Modo escuro\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Use um tema de interface escuro. O streaming de vídeo e áudio permanece inalterado.\",\"Dark mode enabled\":\"Modo escuro ativado\",\"Light mode enabled\":\"Modo claro ativado\",\"Mute token/tip sounds\":\"Silenciar sons de tokens e gorjetas\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Bloqueia os sons de notificação de tokens e gorjetas. O áudio da transmissão é controlado separadamente.\",\"Token/tip sounds muted\":\"Sons de tokens e gorjetas silenciados\",\"Token/tip sounds enabled\":\"Sons de tokens e gorjetas ativados\",\"Clean\":\"Limpar\",\"Clean mode on\":\"Modo limpo ativado\",\"Exit clean mode\":\"Sair do modo limpo\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Oculte temporariamente todos os controles e sobreposições. Atalho: Alt+P / Alt+C. Pressione Esc para sair.\",\"Window first\":\"Janela primeiro\",\"Window-first mode on\":\"Modo janela primeiro ativado\",\"Exit window-first\":\"Sair da janela primeiro\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Oculte o shell do aplicativo e mantenha as janelas da sala visíveis. Atalho: Alt+V.\",\"Show thumbnails\":\"Mostrar miniaturas\",\"Hide thumbnails\":\"Ocultar miniaturas\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Mostre ou oculte o painel de miniaturas no modo Foco. Atalho: Alt+T.\",\"Choose Focus rooms\":\"Escolha salas Focus\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Escolha as salas para o modo Focus. A primeira sala selecionada é a tela principal; use ↑/↓ para definir a ordem das janelas secundárias.\",\"Main screen\":\"Tela principal\",\"Right window\":\"Janela direita\",\"Bottom window\":\"Janela inferior\",\"Empty slot\":\"Espaço vazio\",\"Drag this model to a Focus slot\":\"Arraste este modelo para um slot Focus\",\"Choose model for this slot\":\"Escolha o modelo para este slot\",\"Select a saved model for this exact Focus slot.\":\"Selecione um modelo salvo para este slot Focus exato.\",\"Choose model for this window\":\"Escolha o modelo para esta janela\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Selecione um modelo salvo para esta janela de grade exata. Os modelos existentes permanecem salvos.\",\"Remove from this grid window\":\"Remover desta janela de grade\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Somente este slot é liberado. O modelo salvo permanece na sua área de trabalho.\",\"Show in grid window\":\"Mostrar na janela da grade\",\"Choose the exact active grid window for this model.\":\"Escolha a janela de grade ativa exata para este modelo.\",\"Current\":\"Atual\",\"Model removed from this grid window.\":\"Modelo removido desta janela de grade.\",\"No saved models in this group.\":\"Nenhum modelo salvo neste grupo.\",\"Secondary window\":\"Janela secundária\",\"Use current page automatically\":\"Usar a página atual automaticamente\",\"Clear custom selection\":\"Limpar seleção personalizada\",\"Apply Focus layout\":\"Aplicar layout de foco\",\"No rooms are available in the current group and filter.\":\"Não há salas disponíveis no grupo e filtro atuais.\",\"Fit: full image\":\"Ajuste: imagem completa\",\"Fit: fill window\":\"Ajustar: preencher janela\",\"Switch between full image and cropped fill.\":\"Alternar entre imagem completa e preenchimento recortado.\",\"Playback settings\":\"Configurações de reprodução\",\"Max stream quality\":\"Qualidade máxima de transmissão\",\"Auto / no cap\":\"Automático/sem limite\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + zoom da roda\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Amplie um cartão com Ctrl/Command + roda do mouse, arraste enquanto amplia, clique duas vezes para redefinir.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Saia do modo limpo. Atalho: Alt+P / Alt+C ou Esc.\",\"Clean · Alt+P/C / Esc\":\"Limpar · Alt+P/C / Esc\",\"(synced from another tab)\":\"(sincronizado de outra guia)\",\"Type a username and press Enter. Shortcut: /\":\"Digite um nome de usuário e pressione Enter. Atalho: /\",\"Filter visible rooms by username\":\"Filtrar salas visíveis por nome de usuário\",\"Show / hide group sidebar\":\"Mostrar/ocultar barra lateral do grupo\",\"Global volume. Per-room mute is still respected.\":\"Volume mundial. O mudo por quarto ainda é respeitado.\",\"Base tile size in grid view\":\"Tamanho base do bloco na visualização em grade\",\"Sort rooms in the current group\":\"Classificar salas no grupo atual\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Altura da tela principal no modo Foco. Você também pode arrastar o separador.\",\"Main-screen aspect ratio in Focus mode\":\"Proporção da tela principal no modo Foco\",\"Thumbnail width in Focus mode\":\"Largura da miniatura no modo Foco\",\"Show every saved room. The remove button deletes globally in this view.\":\"Mostre todos os quartos salvos. O botão remover exclui globalmente nesta visualização.\",\"Create an isolated group\":\"Crie um grupo isolado\",\"More tools and maintenance\":\"Mais ferramentas e manutenção\",\"GROUPS\":\"GRUPOS\",\"All saved\":\"Todos salvos\",\"Default\":\"Padrão\",\"Online now\":\"On-line agora\",\"New group name:\":\"Novo nome do grupo:\",\"Rename\":\"Renomear\",\"Rename to:\":\"Renomear para:\",\"Delete group\":\"Excluir grupo\",\"Live preview\":\"Visualização ao vivo\",\"Loading preview…\":\"Carregando visualização…\",\"Private or away\":\"Privado ou ausente\",\"Preview could not be loaded\":\"Não foi possível carregar a visualização\",\"Toggle mute\":\"Alternar mudo\",\"Resume\":\"Currículo\",\"Screenshot current frame\":\"Captura de tela do quadro atual\",\"Record current card\":\"Gravar cartão atual\",\"Stop recording\":\"Pare de gravar\",\"Picture-in-Picture\":\"Imagem em imagem\",\"Remove from current group\":\"Remover do grupo atual\",\"Delete saved room\":\"Excluir sala salva\",\"Mirror image\":\"Imagem espelhada\",\"Flip image\":\"Virar imagem\",\"Rotate left\":\"Girar para a esquerda\",\"Rotate right\":\"Girar para a direita\",\"Reset view\":\"Redefinir visualização\",\"Copy username\":\"Copiar nome de usuário\",\"Add favorite\":\"Adicionar favorito\",\"Move to favorites only\":\"Mover apenas para favoritos\",\"Copied\":\"Copiado\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Falha na captura de tela. O navegador/CORS pode bloquear o desenho deste fluxo.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"A gravação é local e salva este vídeo com o áudio correspondente quando o navegador o expõe. Use-o somente quando tiver permissão para salvar este conteúdo. Continuar?\",\"Recording started\":\"Gravação iniciada\",\"Recording is not supported for this video/browser\":\"A gravação não é compatível com este vídeo/navegador\",\"Recording segment saved\":\"Segmento de gravação salvo\",\"Final recording segment saved\":\"Segmento de gravação final salvo\",\"Recording paused; waiting for the stream to return\":\"Gravação pausada; esperando a transmissão retornar\",\"Recording resumed\":\"Gravação retomada\",\"Recording paused; click to stop\":\"Gravação pausada; clique para parar\",\"No recording data to save\":\"Não há dados de gravação para salvar\",\"Recording settings saved\":\"Configurações de gravação salvas\",\"Segment length in minutes (1-180):\":\"Comprimento do segmento em minutos (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Taxa de bits de vídeo em Mbps (0,5-20):\",\"Warn before leaving while recording\":\"Avise antes de sair durante a gravação\",\"Recording is still active. Leave anyway?\":\"A gravação ainda está ativa. Sair mesmo assim?\",\"Recording center\":\"Centro de gravação\",\"No active recordings\":\"Nenhuma gravação ativa\",\"Waiting for source\":\"Aguardando fonte\",\"Saved segments\":\"Segmentos salvos\",\"Duration\":\"Duração\",\"Bitrate\":\"Taxa de bits\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Formato: MP4 quando compatível com este navegador; caso contrário, o substituto com melhor suporte.\",\"Record current page\":\"Gravar página atual\",\"Record current group\":\"Gravar grupo atual\",\"Record online rooms\":\"Grave salas online\",\"Stop all recordings\":\"Pare todas as gravações\",\"Show recording only\":\"Mostrar apenas gravação\",\"Show all rooms\":\"Mostrar todos os quartos\",\"Open current page rooms\":\"Abrir salas da página atual\",\"Move current page to group\":\"Mover a página atual para o grupo\",\"Layout settings\":\"Configurações de layout\",\"Recording settings\":\"Configurações de gravação\",\"Save settings\":\"Salvar configurações\",\"Config backups\":\"Backups de configuração\",\"No config backups found\":\"Nenhum backup de configuração encontrado\",\"Online/offline history\":\"Histórico on-line/off-line\",\"Group rules\":\"Regras do grupo\",\"Keep favorites first\":\"Mantenha os favoritos primeiro\",\"Temporary URL manager\":\"Gerenciador de URL temporário\",\"Save current temporary URLs\":\"Salvar URLs temporários atuais\",\"No temporary URLs\":\"Sem URLs temporários\",\"Share workspace\":\"Compartilhar espaço de trabalho\",\"Share current rooms and layout\":\"Compartilhe salas e layout atuais\",\"Copy share link\":\"Copiar link de compartilhamento\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Este link restaura salas, grupos e configurações de layout em outra guia da estação de trabalho.\",\"Shared workspace imported\":\"Espaço de trabalho compartilhado importado\",\"Shared workspace link is invalid\":\"O link do espaço de trabalho compartilhado é inválido\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Clique em um campo e pressione o novo atalho. Backspace limpa esse campo.\",\"Reset shortcuts\":\"Redefinir atalhos\",\"Focus add box\":\"Caixa de adição de foco\",\"Grid view\":\"Visualização em grade\",\"Focus view\":\"Visualização de foco\",\"Clean mode\":\"Modo limpo\",\"Window-first mode\":\"Modo janela primeiro\",\"Toggle thumbnails\":\"Alternar miniaturas\",\"Auto-detecting…\":\"Detecção automática…\",\"No rooms in this group\":\"Não há quartos neste grupo\",\"Add a username at the top, switch group, or clear filters\":\"Adicione um nome de usuário na parte superior, troque de grupo ou limpe os filtros\",\"Open workstation\":\"Estação de trabalho aberta\",\"Open here (replace page)\":\"Abra aqui (substituir página)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Substituir esta página pela estação de trabalho? Você pode voltar pelo navegador.\",\"View saved list\":\"Ver lista salva\",\"Collapse dock\":\"Recolher doca\",\"No room detected\":\"Nenhuma sala detectada\",\"Open here\":\"Abra aqui\",\"Add room\":\"Adicionar quarto\",\"Remove room\":\"Remover quarto\",\"Record in workstation\":\"Gravar na estação de trabalho\",\"Screenshot video\":\"Captura de tela do vídeo\",\"Play / pause\":\"Reproduzir/pausar\",\"Mute / unmute\":\"Ativar/desativar som\",\"No playable video found on this page\":\"Nenhum vídeo reproduzível encontrado nesta página\",\"Recording intent queued in workstation\":\"Gravando intenção enfileirada na estação de trabalho\",\"Added\":\"Adicionado\",\"Already exists\":\"Já existe\",\"Failed\":\"Falha\",\"Add to workstation\":\"Adicionar à estação de trabalho\",\"Already in workstation — click to remove\":\"Já na estação de trabalho — clique para remover\",\"Model online\":\"Modelo on-line\",\"Model offline\":\"Modelo off-line\",\"Show fewer\":\"Mostrar menos\",\"No measurable audio at the moment\":\"Nenhum áudio mensurável no momento\",\"Playback paused\":\"Reprodução pausada\",\"Audio measurement unavailable or blocked\":\"Medição de áudio indisponível ou bloqueada\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Permissão de notificação negada. O flash do cartão ainda funciona.\\n\\nVocê pode habilitá-lo nas configurações do navegador.\",\"Batch add\":\"Adição em lote\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Cole os nomes de usuário, um por linha ou separados por espaços/vírgulas:\",\"Cancel\":\"Cancelar\",\"Change the language of RoomGrid and the website\":\"Alterar o idioma do RoomGrid e do site\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Altere o idioma do RoomGrid. O Chaturbate permanece no idioma atual do site porque nenhum idioma oficial do site está disponível.\",\"About\":\"Sobre\",\"Export config\":\"Exportar configuração\",\"Export usernames.txt\":\"Exportar nomes de usuário.txt\",\"Copy usernames\":\"Copiar nomes de usuário\",\"Unmute all\":\"Ativar som de tudo\",\"⏸ Pause visible\":\"⏸ Pausa visível\",\"Resume visible\":\"Currículo visível\",\"Following\":\"Seguindo\",\"Sync followed models now\":\"Sincronize os modelos seguidos agora\",\"✓ Auto-sync followed models\":\"✓ Sincronização automática de modelos seguidos\",\"☐ Auto-sync followed models\":\"☐ Sincronização automática de modelos seguidos\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importe seus modelos do Chaturbate atualmente seguidos para o grupo persistente Seguindo. Somente esse grupo está sincronizado.\",\"Syncing followed models…\":\"Sincronizando modelos seguidos…\",\"Automatic Following sync disabled\":\"Sincronização automática seguinte desativada\",\"Automatic Following sync enabled\":\"Sincronização automática de seguimento ativada\",\"Following sync needs an active Chaturbate login\":\"Após a sincronização, é necessário um login ativo do Chaturbate\",\"Following sync failed safely; saved workspace data was not changed\":\"A sincronização seguinte falhou com segurança; os dados salvos do espaço de trabalho não foram alterados\",\"Enable dark mode\":\"Ativar modo escuro\",\"Disable dark mode\":\"Desative o modo escuro\",\"Status history\":\"Histórico de status\",\"Temporary URLs\":\"URLs temporários\",\"Shortcut panel\":\"Painel de atalho\",\"Toggle video fit\":\"Alternar ajuste de vídeo\",\"Shortcuts / hints\":\"Atalhos/dicas\",\"Ranking\":\"Classificação\",\"Model details\":\"Detalhes do modelo\",\"My profile\":\"Meu perfil\",\"Loading local statistics…\":\"Carregando estatísticas locais…\",\"No watch data yet. Statistics start with this version.\":\"Ainda não há dados de exibição. As estatísticas começam com esta versão.\",\"Statistics are disabled.\":\"As estatísticas estão desativadas.\",\"Enable statistics\":\"Habilitar estatísticas\",\"Disable statistics\":\"Desativar estatísticas\",\"Export statistics\":\"Exportar estatísticas\",\"Delete all statistics\":\"Excluir todas as estatísticas\",\"Delete this model data\":\"Exclua os dados deste modelo\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Excluir todas as estatísticas de observação coletadas localmente? Seu espaço de trabalho permanece intocado.\",\"Tracking since\":\"Acompanhamento desde\",\"Models watched\":\"Modelos assistidos\",\"Watch time\":\"Tempo de exibição\",\"Most watched tags\":\"Tags mais assistidas\",\"Most watched hours\":\"Horas mais assistidas\",\"Observed online\":\"Observado on-line\",\"Public bio\":\"Biografia pública\",\"Public links\":\"Links públicos\",\"No public profile data cached yet.\":\"Nenhum dado de perfil público armazenado em cache ainda.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Somente cartas de jogo visíveis e ativas contam. As visualizações instantâneas não contam.\",\"Statistics imported\":\"Estatísticas importadas\",\"Statistics deleted\":\"Estatísticas excluídas\",\"Statistics exported\":\"Estatísticas exportadas\",\"Local observation only\":\"Apenas observação local\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Nenhuma mensagem privada, tokens, pagamentos, senhas ou rastreamento externo são coletados.\",\"Not enough observations for a time estimate\":\"Observações insuficientes para uma estimativa de tempo\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Passe o mouse sobre qualquer botão/controle para ver sua dica. Atalhos comuns podem ser editados no painel de atalhos.\\n\\nPadrões:\\n/ Foco na entrada do nome de usuário\\nr Atualizar tudo\\ng Visualização em grade\\nf Visualização de foco\\nAlt+P ou Alt+C Modo de limpeza ativado/desativado\\nAlt+Shift+C Centro de gravação\\nAlt+Shift+R Gravar página atual\\nEsc Sair do modo limpo/tela cheia\\nEspaço Pausar/retomar a tela principal na visualização Focus\\n←/→ ou [/ ] Alternar a tela principal na visualização Focus\\nClique duas vezes no cartão em tela cheia\",\"Visible windows paused\":\"Janelas visíveis pausadas\",\"Visible windows resumed\":\"Janelas visíveis retomadas\",\"Import config\":\"Importar configuração\",\"Repair saved data\":\"Reparar dados salvos\",\"Saved data has been normalized.\":\"Os dados salvos foram normalizados.\",\"Clear all data\":\"Limpar todos os dados\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Isso apagará TODAS as salas, grupos e configurações. Continuar?\",\"English\":\"Inglês\",\"Deutsch\":\"Alemão\",\"Author\":\"Autor\",\"License\":\"Licença\",\"If this script saves you time…\":\"Se este script economizar seu tempo…\",\"Tip in ETH\":\"Dica em ETH\",\"ETH address\":\"Endereço ETH\",\"Copy\":\"Copiar\",\"Accent gradient\":\"Gradiente de acento\",\"Active filters\":\"Filtros ativos\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Adicione suas próprias imagens ou use o botão de captura de tela no player. As capturas de tela podem entrar na galeria automaticamente e podem ser usadas diretamente como capa do modelo.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Todos os filtros da página inicial, layout, visualização e configurações de paginação são armazenados no cofre de configurações estáveis e sobrevivem às atualizações de script.\",\"All live models\":\"Todos os modelos ao vivo\",\"All suggestions\":\"Todas as sugestões\",\"Almost silent\":\"Quase silencioso\",\"Always active\":\"Sempre ativo\",\"AND\":\"E\",\"Automatic example\":\"Exemplo automático\",\"Automatic segments\":\"Segmentos automáticos\",\"Automatically loading the complete catalog…\":\"Carregando automaticamente o catálogo completo…\",\"Available recording data was saved.\":\"Os dados de gravação disponíveis foram salvos.\",\"Back online\":\"De volta on-line\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Com base em seus favoritos, tags e atividades de visualização salvos localmente. Cada escolha mostra seu motivo.\",\"Black\":\"Preto\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Personalize o nome, marca, slogan e logotipo local do Camclient. Os logotipos permanecem neste navegador.\",\"Discover something new\":\"Descubra algo novo\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Faixas de recomendação distintas ajudam você a encontrar algo novo em vez de repetir o catálogo da página inicial.\",\"Do not enlarge\":\"Não amplie\",\"Enable meter\":\"Ativar medidor\",\"Fast starters\":\"Iniciantes rápidos\",\"Fill and crop\":\"Preencher e cortar\",\"Filter suggestions\":\"Filtrar sugestões\",\"Fit completely\":\"Ajuste completamente\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Para transparência, use PNG, WebP ou SVG seguro e escolha Transparente como plano de fundo do logotipo. JPEG não tem transparência. Stretch usa a largura e a altura escolhidas de forma independente.\",\"For you\":\"Para você\",\"gallery\":\"galeria\",\"HD\":\"alta definição\",\"Inner spacing\":\"Espaçamento interno\",\"Inspiration, not repetition\":\"Inspiração, não repetição\",\"Interface\":\"Interface\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Permite usar, modificar e distribuir o software. Os avisos de direitos autorais e licença devem permanecer e o software é fornecido sem garantia. Camclient é de autoria de Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Vive por pouco tempo e já atrai atividade pública.\",\"Live stream audio activity\":\"Atividade de áudio de transmissão ao vivo\",\"Local watch\":\"Vigilância local\",\"Logo background\":\"Fundo do logotipo\",\"Logo height\":\"Altura do logotipo\",\"Logo width\":\"Largura do logotipo\",\"Loud\":\"Alto\",\"Measured even while output is muted\":\"Medido mesmo quando a saída está silenciada\",\"Micro\":\"Micro\",\"Middle-click or Ctrl+click opens a new tab\":\"Clique com o botão do meio ou Ctrl + clique para abrir uma nova guia\",\"Models you have visited locally and that are public again now.\":\"Modelos que você visitou localmente e que agora são públicos novamente.\",\"Mono\":\"Mono\",\"New discoveries\":\"Novas descobertas\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Novo no catálogo, visto recentemente pelo Camclient ou apenas começando a transmitir.\",\"No audio track\":\"Sem faixa de áudio\",\"Brave exposed no recordable video track.\":\"Brave não expôs nenhuma trilha de vídeo gravável.\",\"Brave or the video source blocked frame capture.\":\"Brave ou a fonte de vídeo bloqueou a captura de quadros.\",\"Browser exposed no audio track\":\"O navegador não expôs nenhuma trilha de áudio\",\"Camclient\":\"Cliente Cam\",\"Camclient as the real homepage\":\"Camclient como a verdadeira página inicial\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient atribui esses exemplos transparentes a tags comuns automaticamente. Use o botão de imagem em qualquer tag para escolher um exemplo, fazer upload de seu próprio PNG/WebP/SVG ou ocultar a imagem.\",\"Choose a Camclient example or upload your own transparent image.\":\"Escolha um exemplo do Camclient ou carregue sua própria imagem transparente.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Escolha um PNG, JPEG, WebP ou SVG seguro menor que 1,1 MB.\",\"Choose artwork for tag\":\"Escolha a arte da tag\",\"Cinema\":\"Cinema\",\"Click to enable meter\":\"Clique para ativar o medidor\",\"Close more actions\":\"Fechar mais ações\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Cores, tipografia, espaçamento, cantos, sombras e planos de fundo aplicam-se imediatamente às Configurações, à área de trabalho, ao Discover e à biblioteca de modelos.\",\"Continue loading the complete catalog\":\"Continue carregando o catálogo completo\",\"Continuing recording\":\"Continuando a gravação\",\"current matches\":\"partidas atuais\",\"Recording quality\":\"Qualidade de gravação\",\"Recording stopped\":\"Gravação interrompida\",\"Recording unavailable\":\"Gravação indisponível\",\"Remix\":\"Remixar\",\"Reset all\":\"Redefinir tudo\",\"RoomGrid Model library\":\"Biblioteca de modelos RoomGrid\",\"RoomGrid My user\":\"RoomGrid Meu usuário\",\"RoomGrid Statistics\":\"Estatísticas RoomGrid\",\"RoomGrid Tags\":\"RoomGrid Tags\",\"Save to model gallery\":\"Salvar na galeria de modelos\",\"Saved segment\":\"Segmento salvo\",\"Screenshot\":\"Captura de tela\",\"Screenshot failed\":\"Falha na captura de tela\",\"Screenshot format\":\"Formato de captura de tela\",\"Screenshot resolution\":\"Resolução da captura de tela\",\"Screenshot unavailable\":\"Captura de tela indisponível\",\"No filters active · all matching live models are shown\":\"Nenhum filtro ativo · todos os modelos ao vivo correspondentes são mostrados\",\"No recommendations match the active filters.\":\"Nenhuma recomendação corresponde aos filtros ativos.\",\"No recording data\":\"Sem gravação de dados\",\"NOT\":\"NÃO\",\"Only new\":\"Apenas novo\",\"Open Camclient homepage\":\"Abra a página inicial do Camclient\",\"OR\":\"OU\",\"part\":\"parte\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Escolhas pessoais, novas descobertas, salas ao vivo populares e modelos que você nunca visitou – claramente separados do catálogo completo da página inicial.\",\"Place\":\"Lugar\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP e SVG seguro suportam transparência. As imagens são redimensionadas localmente para 144 × 144 pixels e salvas com suas configurações.\",\"Popular now\":\"Popular agora\",\"Public live models with no local watch time in Camclient.\":\"Modelos públicos ao vivo sem tempo de exibição local no Camclient.\",\"Quiet\":\"Silencioso\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Classificado pela contagem atual de espectadores públicos; os dados do catálogo não são armazenados permanentemente.\",\"Recording is still active.\":\"A gravação ainda está ativa.\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"As capturas de tela são baixadas e também podem entrar na galeria de modelos locais.\",\"segment(s)\":\"segmento(s)\",\"Show no image\":\"Não mostrar nenhuma imagem\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Mostra a atividade na trilha de áudio do stream, independentemente de a saída do player estar silenciada.\",\"Source ended\":\"Fonte encerrada\",\"Source resolution\":\"Resolução de origem\",\"Start recording\":\"Comece a gravar\",\"Starter pick · popular now\":\"Escolha inicial · popular agora\",\"Status\":\"Status\",\"Stop and save\":\"Pare e economize\",\"Stretch\":\"Esticar\",\"Tag artwork and emoji\":\"Marcar arte e emoji\",\"Tag artwork editor\":\"Editor de arte de tags\",\"The compressed image is still too large.\":\"A imagem compactada ainda é muito grande.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"O cabeçalho mostra apenas Home, Discover, Tag Explorer, Statistics Center, Model Library, My User e Settings. Cada área de configuração está disponível como uma subpágina Configurações.\",\"The image could not be decoded.\":\"A imagem não pôde ser decodificada.\",\"The image could not be read.\":\"A imagem não pôde ser lida.\",\"The image format is unsafe or unsupported.\":\"O formato da imagem não é seguro ou não é compatível.\",\"The lanes respect your active search and filter settings.\":\"As pistas respeitam suas configurações ativas de pesquisa e filtro.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"A gravação é salva apenas neste dispositivo. Continue somente se você tiver permissão para salvar este conteúdo.\",\"The video has no usable frame yet.\":\"O vídeo ainda não tem quadro utilizável.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Ainda não há dados de preferências locais suficientes, portanto, essas são escolhas iniciais transparentes com base na popularidade atual.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Essas configurações controlam o layout da página inicial do Camclient, paginação, informações do modelo, filtros e desempenho.\",\"Total\":\"Total\",\"Transparent\":\"Transparente\",\"Unified main navigation\":\"Navegação principal unificada\",\"Unvisited\":\"Não visitado\",\"Upload own image\":\"Carregar imagem própria\",\"URL\":\"URL\",\"Use as model cover\":\"Use como capa de modelo\",\"Use the complete search, filters, and sorting for every current result.\":\"Use a pesquisa, os filtros e a classificação completos para cada resultado atual.\",\"Trans\":\"Trans\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Visitar a página inicial do Chaturbate sempre abre a página inicial completa do Camclient. Cada área principal permanece dentro da mesma interface personalizada.\",\"Volume\":\"Volume\",\"Waiting for audio\":\"Aguardando áudio\",\"What does the MIT license mean?\":\"O que significa a licença do MIT?\",\"White\":\"Branco\",\"Your discovery mix\":\"Seu mix de descoberta\",\"中文\":\"中文\",\"SingleCam\":\"SingleCam\",\"Country, region or location\":\"País, região ou local\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Controle a ordem dos modelos, filtros, quantidade por página, tamanho dos cartões e informações visíveis diretamente na página inicial original do Chaturbate. Cada alteração é aplicada imediatamente e salva neste navegador.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] atrás\",\"[[ROOMGRID_VALUE_0]] preview\":\"Visualização de [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] perfil salvo\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Detalhes do modelo para @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] modelos seguidos importados\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"[[ROOMGRID_VALUE_0]] Itens da minha coleção reconhecidos\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"[[ROOMGRID_VALUE_0]] de [[ROOMGRID_VALUE_1]] modelos atualmente carregados visíveis.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Filtro de grupo ativo: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] de [[ROOMGRID_VALUE_2]] cartas visíveis correspondem ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Capa personalizada · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Capa salva localmente · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Imagens e capas de exemplo ([[ROOMGRID_VALUE_0]] salvas)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Armazenado permanentemente neste dispositivo · último [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Imagem própria · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Salvo [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Visto pela primeira vez: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Visto pela última vez: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Última live: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Aparições: [[ROOMGRID_VALUE_0]]× · primeira [[ROOMGRID_VALUE_1]] · última [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Horário típico: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Técnico: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] observações locais ao vivo, no máximo uma a cada 5 minutos\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] do seu tempo de exibição anterior como uma estimativa cautelosa\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Remover etiqueta [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modelo[[ROOMGRID_VALUE_1]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Gerenciar grupos personalizados · atribuídos a [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Excluir grupo “[[ROOMGRID_VALUE_0]]”? Os modelos permanecerão em sua biblioteca.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Grupo “[[ROOMGRID_VALUE_0]]” excluído\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] grupos · seleção múltipla possível\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] tags salvas\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Remover filtro [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] corresponde\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtros ativos\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · As informações do perfil público podem ser carregadas em “Todos os detalhes”.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Organizado pessoalmente desde: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Catalogado automaticamente desde: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Última atividade: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Aparições: [[ROOMGRID_VALUE_0]]× · última gravação [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Descoberto: [[ROOMGRID_VALUE_0]] · medições de qualidade: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Informações do perfil: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] arquivado\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] restaurado\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Página [[ROOMGRID_VALUE_0]] dos modelos [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] salvo\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] assistiu\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] ativo · cresce automaticamente\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] modelos · [[ROOMGRID_VALUE_1]] sessões\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favoritos\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] modelos distintos\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] visualizações\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Pré-visualização/visualização rápida dos modelos [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] dias\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Sequência de [[ROOMGRID_VALUE_0]] dias\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] assistiu repetidamente\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] dias de dados locais\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"média ponderada [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] ao vivo agora\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modelos reconhecidos\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Nem tudo pode ser lido: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Dia mais ativo: [[ROOMGRID_VALUE_0]] com [[ROOMGRID_VALUE_1]]. Retenção: [[ROOMGRID_VALUE_2]] dias para eventos brutos; valores mais antigos podem permanecer como totais diários.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sessões\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] salvo localmente\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Não foi possível carregar os dados locais: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] está on-line.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] está offline.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] não é observado online há algum tempo.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favoritos estão online juntos.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"O perfil público de [[ROOMGRID_VALUE_0]] foi alterado.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] modelos atualizados\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Pré-visualização da importação de estatísticas (nada mudou ainda):\\n[[ROOMGRID_VALUE_0]] modelos\\n[[ROOMGRID_VALUE_1]] eventos\\n[[ROOMGRID_VALUE_2]] perfis públicos\\n\\nContinuar substitui as estatísticas existentes.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Visualização ao vivo de [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] adicionado ao MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Excluir [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Importar visualizações de [[ROOMGRID_VALUE_0]] e feeds de [[ROOMGRID_VALUE_1]]?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] salas adicionadas ao espaço de trabalho\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modelos salvos\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] resultados correspondentes\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] modelos ao vivo correspondentes\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Compartilha #[[ROOMGRID_VALUE_0]] com um favorito\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] imagens adicionadas; [[ROOMGRID_VALUE_1]] não pôde ser processado.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] imagens adicionadas.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] quartos\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] nomes de usuário válidos\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Página [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] salas\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"URLs temporários: [[ROOMGRID_VALUE_0]] adicionado, [[ROOMGRID_VALUE_1]] falhou\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Visualização da importação de estatísticas: modelos [[ROOMGRID_VALUE_0]] e eventos [[ROOMGRID_VALUE_1]]. Continuar mescla os dados com segurança e cria um backup primeiro.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Layouts personalizados suportam até [[ROOMGRID_VALUE_0]] salas.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Cena “[[ROOMGRID_VALUE_0]]” salva\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Cena “[[ROOMGRID_VALUE_0]]” carregada\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] transmissões ativas · [[ROOMGRID_VALUE_1]] instável · [[ROOMGRID_VALUE_2]] para assistir\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] fluxos · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p atribuído\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Excluir cena “[[ROOMGRID_VALUE_0]]”?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Salvo apenas para [[ROOMGRID_VALUE_0]]: até [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Esta câmera usa qualidade global/adaptativa[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] foi substituído por [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Reproduzindo áudio apenas de [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Colocado no slot Focus [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Colocado na janela da grade [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Resolução [[ROOMGRID_VALUE_0]], estado [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Saúde do fluxo [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Remover filtro: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Corresponde aos seus interesses: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catálogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] buscado · [[ROOMGRID_VALUE_2]] modelos ao vivo\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modelos locais\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catálogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modelos vivos\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] salas de catálogo carregadas\"},\"visible\":{\"Automatisch neu verbinden\":\"Reconectar automaticamente\",\"Bei verstecktem Tab pausieren\":\"Pausar enquanto a guia estiver oculta\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Aplicar detalhes da conta existente do Chaturbate\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Otimizar imagem · grande zoom, posição, filtro e rotação\",\"Bildanpassung\":\"Ajuste de imagem\",\"Bildwerkzeuge einklappen\":\"Recolher ferramentas de imagem\",\"Breite des Informationsbereichs\":\"Largura da área de informações\",\"Broadcaster-Verbindung verwalten\":\"Gerenciar conexão da emissora\",\"Chat anzeigen\":\"Mostrar bate-papo\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Isso significa que você não está limitado aos dados desde que o script foi instalado: RoomGrid pode ler sua lista a seguir existente e as entradas claramente reconhecíveis de Minha coleção. Nenhuma mensagem, pagamento, mídia ou senha é copiada.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"O Analytics Studio também contém comparações de períodos de tempo, mapas de calor, funis de descoberta, espaço de trabalho e análises de qualidade, bem como exportações de dados.\",\"Datenschutz & Export\":\"Proteção e exportação de dados\",\"Deine Aktivität der letzten 14 Tage\":\"Sua atividade nos últimos 14 dias\",\"Deine meistgesehenen Models\":\"Seus modelos mais vistos\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"A conexão atual está ativa; Após a próxima medição, o curso local aparece aqui.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Esta visualização da biblioteca ainda está vazia.\",\"Eigene Priorität\":\"Prioridade própria\",\"Eigene Tags\":\"Etiquetas próprias\",\"Eigene Tags filtern\":\"Filtre suas próprias tags\",\"Eigenen Tag eingeben …\":\"Entre no seu próprio dia...\",\"Einstellungen\":\"Configurações\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter ou “Adicionar Tag” assume o rascunho. Só é salvo permanentemente com “Salvar perfil”. Tags já usadas são sugeridas conforme você digita.\",\"Ereignisse im Zeitraum\":\"Eventos no período\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Crie coleções com nomes livres, atribua modelos a vários grupos e exiba um ou mais grupos juntos aqui.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Primeiro crie seu próprio grupo acima.\",\"Follower\":\"Seguidores\",\"GAMING\":\"JOGOS\",\"Gesamter Verlauf\":\"Curso inteiro\",\"Grid +\":\"Grade+\",\"Größe der Bedienelemente\":\"Tamanho dos controles\",\"Offizielle Statistik verbinden\":\"Conecte estatísticas oficiais\",\"Optionale offizielle Kontostatistik\":\"Estatísticas opcionais da conta oficial\",\"Persönlich organisiert\":\"Organizado pessoalmente\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Jogador e conexão\",\"Player neu verbinden\":\"Reconectar jogador\",\"Player-Ecken\":\"Cantos do jogador\",\"Player-Hintergrund\":\"Histórico do jogador\",\"Player-Höhe\":\"Altura do jogador\",\"Player-Seitenverhältnis\":\"Proporção do jogador\",\"Position der Informationen\":\"Localização das informações\",\"Private Notiz\":\"Nota privada\",\"Private Notizen anzeigen\":\"Ver notas privadas\",\"Puffergröße\":\"Tamanho do buffer\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"‹ Zurück\":\"‹ Voltar\",\"● 1.2k · #tag\":\"● 1,2 mil · #tag\",\"1 Jahr\":\"1 ano\",\"30 Tage\":\"30 dias\",\"7 Tage\":\"7 dias\",\"90 Tage\":\"90 dias\",\"AA ×\":\"AA×\",\"AA ✓\":\"AA✓\",\"Ähnliche Models anzeigen\":\"Mostrar modelos semelhantes\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Todas as alterações são salvas localmente e também se aplicam ao próximo modelo.\",\"Alle eigenen Tags\":\"Todas as tags personalizadas\",\"Alle Gruppen\":\"Todos os grupos\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Solicitar dados mais antigos do Chaturbate\",\"Automatisch abspielen\":\"Reprodução automática\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Capturado automaticamente · totalmente organizado durante a edição\",\"Gruppe löschen\":\"Excluir grupo\",\"Gruppen umbenennen oder löschen\":\"Renomear ou excluir grupos\",\"HD + Gaming\":\"Jogos HD+\",\"IndexedDB\":\"Banco de dados indexado\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Cada tag salva fica automaticamente disponível na pesquisa, no preenchimento automático e aqui como filtro múltiplo direto.\",\"Keine Models passen zu diesen Filtern.\":\"Nenhum modelo corresponde a esses filtros.\",\"Keine Priorität\":\"Não é uma prioridade\",\"Konto-Daten werden eingelesen …\":\"Os detalhes da conta estão sendo lidos em...\",\"Lautstärke pro Model merken\":\"Observe o volume por modelo\",\"Layout & sichtbare Bereiche\":\"Layout e áreas visíveis\",\"Letzte Messung\":\"Última medição\",\"LIVE\":\"AO VIVO\",\"lokal · für Suche und Filter\":\"local · para pesquisa e filtros\",\"Maximale Stream-Lautstärke\":\"Volume máximo de transmissão\",\"Model besitzt alle gewählten Tags\":\"O modelo possui todas as tags selecionadas\",\"Model besitzt mindestens einen gewählten Tag\":\"O modelo tem pelo menos uma tag selecionada\",\"Verbunden\":\"Conectado\",\"Vollständige Modelstatistik\":\"Estatísticas completas do modelo\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Leia os dados da conta existente agora\",\"Weiter ›\":\"Próximo ›\",\"Zufriedenheit\":\"Satisfação\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid não consegue adivinhar os valores pessoais da conta Chaturbate. Esses cartões só aparecem se você tiver vinculado conscientemente as estatísticas oficiais da emissora ao seu próprio nome de usuário e token.\",\"RoomGrid Suite\":\"Suíte RoomGrid\",\"Seitengröße\":\"Tamanho da página\",\"SingleCam\":\"Câmera única\",\"SingleCam einstellen\":\"Configurar SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Restaurar o padrão SingleCam\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Assim que você salvar sua própria tag para um modelo, ela aparecerá aqui e no preenchimento automático.\",\"Status im Player anzeigen\":\"Mostrar status no player\",\"Status-Aktualisierung\":\"Atualização de status\",\"Stream immer stumm starten\":\"Sempre inicie a transmissão silenciosamente\",\"Tag hinzufügen\":\"Adicionar etiqueta\",\"Tipps letzte Stunde\":\"Dicas de última hora\",\"Token-Guthaben\":\"Saldo de tokens\",\"Ton & Ruhe\":\"Som e calma\",\"Ungefähre Onlinezeiten\":\"Tempos on-line aproximados\",\"Model ist in allen gewählten Gruppen\":\"O modelo está em todos os grupos selecionados\",\"Model ist in mindestens einer gewählten Gruppe\":\"O modelo está em pelo menos um grupo selecionado\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Abra minha coleção\",\"Namen speichern\":\"Salvar nome\",\"Nicht verbunden\":\"Não conectado\",\"Niedrige Live-Latenz\":\"Baixa latência ao vivo\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Ainda não existem valores de utilização locais suficientes.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Nenhum grupo nosso ainda. Digite um nome acima e crie sua primeira coleção.\",\"Noch keine Model-Sehzeit erfasst.\":\"Nenhum tempo de visualização do modelo registrado ainda.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Ainda não há instantâneos oficiais da conta.\",\"Noch nicht eingelesen\":\"Ainda não li\",\"Nur auf diesem Gerät\":\"Somente neste dispositivo\",\"Öffentliche/erkannte Tags\":\"Tags públicas/reconhecidas\",\"Öffentliches Profil anzeigen\":\"Ver perfil público\",\"Chat-, Token- und Tip-Töne vollständig aus\":\"Desativar completamente os sons de chat, tokens e gorjetas\",\"ALLE SEITENTÖNE AUS\":\"TODOS OS SONS DA PÁGINA DESATIVADOS\",\"SEITENTÖNE ERLAUBT\":\"SONS DA PÁGINA ATIVADOS\",\"Chat/Token-Töne AUS\":\"Sons de chat/tokens DESATIVADOS\",\"Chat/Token-Töne AN\":\"Sons de chat/tokens ATIVADOS\",\"Gruppen\":\"Grupos\",\"RoomGrid Komfortzentrum\":\"Central RoomGrid\",\"Mein Nutzer\":\"Meu usuário\",\"Mein Nutzer-Dashboard\":\"Painel do meu usuário\",\"Modellbibliothek\":\"Biblioteca de modelos\",\"Große Modellbibliothek\":\"Grande biblioteca de modelos\",\"Statistikzentrum\":\"Central de estatísticas\",\"Sitzungen\":\"Sessões\",\"Dock + Hover\":\"Fixar + passar o cursor\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Local e privado neste dispositivo\",\"Sortierung\":\"Ordenar por\",\"Meiste Sehzeit\":\"Maior tempo de visualização\",\"Zuletzt aktiv\":\"Atividade recente\",\"Meiste Sitzungen\":\"Mais sessões\",\"Meiste Zuschauer\":\"Mais espectadores\",\"Status\":\"Status\",\"Alle Status\":\"Todos os status\",\"Nur live\":\"Somente ao vivo\",\"Offline\":\"Offline\",\"Unbekannt\":\"Desconhecido\",\"Nur Bibliothek\":\"Somente na biblioteca\",\"Pro Seite\":\"Por página\"}}","fr":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Le changement de langue n'a pas pu être préparé en toute sécurité. Cette page n'a pas été abandonnée.\",\"Language\":\"Langue\",\"Default workspace\":\"Espace de travail par défaut\",\"Workspace\":\"Espace de travail\",\"New workspace\":\"Nouvel espace de travail\",\"copy\":\"copie\",\"New group\":\"Nouveau groupe\",\"Saved model\":\"Modèle enregistré\",\"Search models, pages, workspaces and commands…\":\"Rechercher des modèles, des pages, des espaces de travail et des commandes…\",\"Type to filter · Enter to run · Esc to close\":\"Tapez pour filtrer · Entrée pour exécuter · Échap pour fermer\",\"No matches\":\"Aucune correspondance\",\"Currently active RoomGrid version\":\"Version RoomGrid actuellement active\",\"years\":\"ans\",\"Live\":\"En direct\",\"Private\":\"Privé\",\"Away\":\"Absent\",\"Offline\":\"Hors ligne\",\"sessions\":\"séances\",\"NEW\":\"NOUVEAU\",\"Name, subject, tag or place\":\"Nom, sujet, tag ou lieu\",\"Website order\":\"Ordre du site\",\"Most viewers\":\"Plus de spectateurs\",\"Fewest viewers\":\"Moins de spectateurs\",\"Longest online\":\"En ligne depuis le plus longtemps\",\"Recently online\":\"Connectés récemment\",\"Age ascending\":\"Âge croissant\",\"Age descending\":\"Âge décroissant\",\"New models first\":\"Nouveaux modèles en premier\",\"Saved first\":\"Enregistrés en premier\",\"Favorites first\":\"Favoris en premier\",\"Most watched\":\"Les plus regardés\",\"Recently watched\":\"Vus récemment\",\"Random mix\":\"Mélange aléatoire\",\"All loaded\":\"Tous les modèles chargés\",\"Previous page\":\"Page précédente\",\"Next page\":\"Page suivante\",\"Reload complete live list\":\"Recharger la liste complète des live\",\"All homepage options\":\"Toutes les options de la page d’accueil\",\"Reset filters\":\"Réinitialiser les filtres\",\"Quick filter\":\"Filtre rapide\",\"Sort\":\"Trier\",\"Models per page\":\"Modèles par page\",\"Load more results\":\"Charger plus de résultats\",\"still to display\":\"reste à afficher\",\"continues automatically while scrolling\":\"continue automatiquement pendant le défilement\",\"visible\":\"visibles\",\"loaded\":\"chargés\",\"active filters\":\"filtres actifs\",\"loading complete live list\":\"chargement de la liste complète en direct\",\"all matching live models found\":\"tous les modèles vivants correspondants trouvés\",\"live list incomplete, retry\":\"liste en direct incomplète, réessayez\",\"Requests temporarily paused\":\"Demandes temporairement suspendues\",\"Vault could not be unlocked\":\"Le coffre-fort n'a pas pu être déverrouillé\",\"Preview Dock\":\"Aperçu du Dock\",\"Connecting…\":\"Connexion…\",\"Stream temporarily unavailable\":\"Flux temporairement indisponible\",\"Error\":\"Erreur\",\"Buffering\":\"Mise en mémoire tampon\",\"Preview unavailable\":\"Aperçu indisponible\",\"Connect\":\"Se connecter\",\"Close\":\"Fermer\",\"Opacity\":\"Opacité\",\"Info\":\"Informations\",\"Age\":\"Âge\",\"Gender\":\"Sexe\",\"Location\":\"Lieu\",\"Country\":\"Pays\",\"Last broadcast\":\"Dernière diffusion\",\"Fan club\":\"Fan-club\",\"Private show\":\"Spectacle privé\",\"Spy show\":\"Spectacle d'espionnage\",\"Model comparison list\":\"Liste de comparaison des modèles\",\"Compare\":\"Comparez\",\"Open model details\":\"Ouvrir les détails du modèle\",\"Compare now\":\"Comparez maintenant\",\"Clear\":\"Effacer\",\"Up to four models can be compared at once.\":\"Jusqu'à quatre modèles peuvent être comparés à la fois.\",\"Remove from comparison\":\"Supprimer de la comparaison\",\"Add to comparison\":\"Ajouter à la comparaison\",\"Total watched\":\"Total regardé\",\"Viewers\":\"Téléspectateurs\",\"Rating\":\"Note\",\"Online samples\":\"Échantillons en ligne\",\"Loading quality data…\":\"Chargement des données de qualité…\",\"samples\":\"échantillons\",\"stalls\":\"stands\",\"No quality measurements yet\":\"Aucune mesure de qualité pour l'instant\",\"Quality data unavailable\":\"Données de qualité indisponibles\",\"Details\":\"Détails\",\"Remove\":\"Supprimer\",\"Model comparison\":\"Comparaison de modèles\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Chaque valeur provient d'informations publiques, de votre propre organisation ou d'observations locales sur cet appareil. Les valeurs manquantes ne sont jamais devinées.\",\"Shared tags\":\"Balises partagées\",\"Loading public facts…\":\"Chargement des faits publics…\",\"Unknown status\":\"Statut inconnu\",\"Last activity\":\"Dernière activité\",\"These statistics describe only your own local RoomGrid usage.\":\"Ces statistiques décrivent uniquement votre propre utilisation locale de RoomGrid.\",\"At a glance\":\"En un coup d'œil\",\"Appearances\":\"Apparitions\",\"Public facts\":\"Faits publics\",\"No public biography available yet.\":\"Aucune biographie publique disponible pour l'instant.\",\"Public link\":\"Lien public\",\"Public profile source\":\"Source du profil public\",\"Profile updated\":\"Profil mis à jour\",\"Private note (local only)\":\"Note privée (locale uniquement)\",\"Your tags\":\"Vos balises\",\"No rating\":\"Aucune note\",\"No priority\":\"Aucune priorité\",\"Private library data\":\"Données de bibliothèque privée\",\"Private notes\":\"Notes privées\",\"Priority\":\"Priorité\",\"Favorite\":\"Favoris\",\"Watchlist\":\"Liste de surveillance\",\"Archived\":\"Archivé\",\"Watch again\":\"Revoir\",\"Technical note\":\"Note technique\",\"Technical\":\"Technique\",\"Save all changes\":\"Enregistrez toutes les modifications\",\"Loading local activity…\":\"Chargement de l'activité locale…\",\"Activity and playback quality\":\"Qualité d’activité et de lecture\",\"Sun\":\"Soleil\",\"Mon\":\"lundi\",\"Tue\":\"mar.\",\"Wed\":\"mer\",\"Thu\":\"jeu.\",\"Fri\":\"vendredi\",\"Sat\":\"Samedi\",\"Quality samples\":\"Des échantillons de qualité\",\"Average startup\":\"Démarrage moyen\",\"Stalls\":\"Stands\",\"Reconnects\":\"Se reconnecte\",\"Dropped frames\":\"Images perdues\",\"Online observations\":\"Observations en ligne\",\"Frequently observed online times\":\"Horaires en ligne fréquemment observés\",\"Not enough online observations yet.\":\"Pas encore assez d’observations en ligne.\",\"Watched\":\"Regardé\",\"Preview\":\"Aperçu\",\"Quick view\":\"Aperçu rapide\",\"Discovered\":\"Découvert\",\"Quality\":\"Qualité\",\"Connection\":\"Connexion\",\"No local activity recorded yet.\":\"Aucune activité locale enregistrée pour le moment.\",\"Recent events\":\"Événements récents\",\"Activity data unavailable.\":\"Données d'activité indisponibles.\",\"Similar models\":\"Modèles similaires\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Calculé à partir des balises locales/publiques, du pays et du type. Chaque suggestion montre sa raison et aucune donnée n'est téléchargée.\",\"shared tags\":\"balises partagées\",\"same country\":\"même pays\",\"same type\":\"même type\",\"Not enough matching data yet.\":\"Pas encore assez de données correspondantes.\",\"Overview\":\"Aperçu\",\"Profile\":\"Profil\",\"Organize\":\"Organiser\",\"Activity\":\"Activité\",\"Similar\":\"Similaire\",\"Model detail sections\":\"Sections de détails du modèle\",\"Previous model\":\"Modèle précédent\",\"Next model\":\"Modèle suivant\",\"Open room\":\"Salle ouverte\",\"Pin preview\":\"Aperçu de l'épingle\",\"No public fields available\":\"Aucun champ public disponible\",\"Public facts currently unavailable\":\"Faits publics actuellement indisponibles\",\"Session chat search\":\"Recherche de chat de session\",\"Search this session only…\":\"Rechercher cette session uniquement…\",\"New messages\":\"Nouveaux messages\",\"Time for a short break.\":\"C'est le moment d'une petite pause.\",\"Your RoomGrid time budget for today has been reached.\":\"Votre budget temps RoomGrid pour aujourd’hui a été atteint.\",\"RoomGrid action messages\":\"Messages d'action RoomGrid\",\"This change is saved automatically.\":\"Cette modification est enregistrée automatiquement.\",\"Undo\":\"Annuler\",\"Change undone\":\"Modification annulée\",\"Undo was not applied because newer changes already exist.\":\"L'annulation n'a pas été appliquée car des modifications plus récentes existent déjà.\",\"Chaturbate live catalog\":\"Catalogue en direct de Chaturbate\",\"RoomGrid Discover\":\"RoomGrid Découvrir\",\"Chaturbate Discover page\":\"Page de découverte de Chaturbate\",\"Following list\":\"Liste suivante\",\"My Collection\":\"Ma collection\",\"Chaturbate page\":\"Page Chaturbate\",\"Local record\":\"Enregistrement local\",\"Personally organized\":\"Personnellement organisé\",\"Local watch history\":\"Historique des vidéos regardées localement\",\"Public profile\":\"Profil public\",\"Local quality measurements\":\"Mesures de qualité locales\",\"Local model index\":\"Index des modèles locaux\",\"Current live signal\":\"Signal en direct actuel\",\"Within the last hour\":\"Au cours de la dernière heure\",\"Within the last 24 hours\":\"Au cours des dernières 24 heures\",\"Within the last 30 days\":\"Au cours des 30 derniers jours\",\"Historical record\":\"Dossier historique\",\"Time unknown\":\"Heure inconnue\",\"All\":\"Tout\",\"Favorites\":\"Favoris\",\"Library\":\"Bibliothèque\",\"My Collection contained no reliably recognizable entries\":\"Ma collection ne contenait aucune entrée identifiable de manière fiable\",\"No group filter active · all models are shown.\":\"Aucun filtre de groupe actif · tous les modèles sont affichés.\",\"Clear group filter\":\"Effacer le filtre de groupe\",\"Filter homepage by groups\":\"Filtrer la page d'accueil par groupes\",\"Close group menu\":\"Fermer le menu du groupe\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Choisissez un ou plusieurs de vos propres groupes. D'autres cartes modèles sont immédiatement cachées sur la page d'accueil de Chaturbate.\",\"Show all\":\"Afficher tout\",\"Models in this group\":\"Modèles dans ce groupe\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Il n'existe pas encore de groupe personnalisé. Créez-en un dans la bibliothèque de modèles et attribuez-y des modèles.\",\"In any group\":\"Dans n'importe quel groupe\",\"In all groups\":\"Dans tous les groupes\",\"Manage groups\":\"Gérer les groupes\",\"Create group\":\"Créer un groupe\",\"Custom groups for the homepage\":\"Groupes personnalisés pour la page d'accueil\",\"Main views\":\"Vues principales\",\"MultiCam workstation\":\"Poste de travail MultiCam\",\"Camera grid, audio and layouts\":\"Grille de caméra, audio et mises en page\",\"Discover\":\"Découvrir\",\"Full catalog, filters and sorting\":\"Catalogue complet, filtres et tri\",\"Personal dashboard\":\"Tableau de bord personnel\",\"Favorites, activity and shortcuts\":\"Favoris, activités et raccourcis\",\"Tag explorer\":\"Explorateur de balises\",\"Combinations, trends and related tags\":\"Combinaisons, tendances et balises associées\",\"Library and statistics\":\"Bibliothèque et statistiques\",\"Statistics Center\":\"Centre de statistiques\",\"Model comparison and local statistics\":\"Comparaison de modèles et statistiques locales\",\"Model library\":\"Bibliothèque de modèles\",\"Profiles, tags, groups and notes\":\"Profils, balises, groupes et notes\",\"My user\":\"Mon profil\",\"Preferences, usage and account data\":\"Préférences, utilisation et données de compte\",\"Analytics Studio\":\"Studio d'analyse\",\"Ranges, heatmaps and exports\":\"Gammes, cartes thermiques et exportations\",\"Settings and maintenance\":\"Paramètres et entretien\",\"Customize homepage\":\"Personnaliser l’accueil\",\"Cards, filters and visible details\":\"Cartes, filtres et détails visibles\",\"Appearance\":\"Apparence\",\"Colors, density and surfaces\":\"Couleurs, densités et surfaces\",\"Workspaces and groups\":\"Espaces de travail et groupes\",\"Layouts, templates and assignments\":\"Mises en page, modèles et affectations\",\"Data and backup\":\"Données et sauvegarde\",\"Import, export and recovery\":\"Importation, exportation et récupération\",\"Windows and performance\":\"Windows et performances\",\"Resource profiles and preview limits\":\"Profils de ressources et limites d'aperçu\",\"Privacy\":\"Confidentialité\",\"History, private session and local data\":\"Historique, session privée et données locales\",\"Diagnostics\":\"Diagnostic\",\"Self-check, routes and recovery\":\"Autocontrôle, itinéraires et récupération\",\"RoomGrid main menu\":\"Menu principal de RoomGrid\",\"All pages, settings and tools in one place\":\"Toutes les pages, paramètres et outils en un seul endroit\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Le menu reste à l'intérieur de la fenêtre visible ; toutes les données RoomGrid restent locales.\",\"RoomGrid pages\":\"Pages RoomGrid\",\"Statistics\":\"Statistiques\",\"Customize original homepage\":\"Personnaliser la page d’accueil d’origine\",\"Homepage\":\"Page d’accueil\",\"Filter homepage by your own groups\":\"Filtrer la page d'accueil par vos propres groupes\",\"Groups\":\"Groupes\",\"Activity trend\":\"Tendance d'activité\",\"Link\":\"Lien\",\"Public profile text and links\":\"Texte et liens du profil public\",\"Last known public preview\":\"Dernier aperçu public connu\",\"Current public preview\":\"Aperçu public actuel\",\"Offline may only provide a placeholder\":\"Hors ligne ne peut fournir qu'un espace réservé\",\"Example images and covers\":\"Exemples d'images et de couvertures\",\"Reading local image archive…\":\"Lecture des archives d’images locales…\",\"Save new snapshot\":\"Enregistrer un nouvel instantané\",\"Saving public previews…\":\"Enregistrement des aperçus publics…\",\"No saved image yet; current public previews are still shown.\":\"Aucune image enregistrée pour l'instant ; les aperçus publics actuels sont toujours affichés.\",\"Current public cover\":\"Couverture publique actuelle\",\"Current compact preview\":\"Aperçu compact actuel\",\"Current live image\":\"Image en direct actuelle\",\"Room\":\"Salon\",\"More\":\"Plus\",\"Unwatch\":\"Ne plus regarder\",\"Data sources and freshness\":\"Sources de données et fraîcheur\",\"Data source\":\"Source de données\",\"Full local data set\":\"Ensemble de données locales complet\",\"Matching filters\":\"Filtres correspondants\",\"Shown on this page\":\"Affiché sur cette page\",\"Latest live signals\":\"Derniers signaux en direct\",\"With watch history\":\"Avec historique des vidéos regardées\",\"Public-profile coverage\":\"Couverture de profil public\",\"Country coverage\":\"Couverture nationale\",\"Age coverage\":\"Couverture d'âge\",\"Tag coverage\":\"Couverture des balises\",\"Which data set is being searched?\":\"Quel ensemble de données est recherché ?\",\"Last local observation\":\"Dernière observation locale\",\"No observation time yet\":\"Pas encore d'heure d'observation\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Public = à partir des pages Chaturbate actuelles ou du catalogue en direct. Personnel = enregistré, tagué ou regroupé par vous. Local = sessions, aperçus ou valeurs de qualité observées uniquement sur cet appareil. Les champs inconnus restent inconnus et ne sont jamais devinés.\",\"Local model statistics\":\"Statistiques du modèle local\",\"Automatically recorded · personally organized\":\"Enregistré automatiquement · organisé personnellement\",\"Automatically recorded in the growing model catalog\":\"Enregistré automatiquement dans le catalogue de modèles en pleine croissance\",\"Avg. session\":\"Session moyenne\",\"Active days\":\"Jours actifs\",\"Share\":\"Partager\",\"Longest session\":\"Session la plus longue\",\"Previews\":\"Aperçus\",\"Quick views\":\"Vues rapides\",\"Data sources\":\"Sources de données\",\"Edit in library\":\"Modifier dans la bibliothèque\",\"No experience data yet\":\"Aucune donnée d'expérience pour l'instant\",\"Online observations will be collected locally from now on.\":\"Les observations en ligne seront désormais collectées localement.\",\"Open\":\"Ouvert\",\"Good trend\":\"Bonne tendance\",\"Moderate trend\":\"Tendance modérée\",\"Initial trend\":\"Tendance initiale\",\"First observation\":\"Premier constat\",\"Estimated from watch time\":\"Estimation à partir de la durée de visionnage\",\"weekday open\":\"ouvert en semaine\",\"time open\":\"temps ouvert\",\"Preview image\":\"Image d’aperçu\",\"Biography / room subject\":\"Biographie / sujet du salon\",\"Catalog state\":\"État du catalogue\",\"Live status\":\"Statut du direct\",\"Viewer count\":\"Nombre de spectateurs\",\"Country/region\":\"Pays/région\",\"Total watch time\":\"Temps de visionnage total\",\"Average session\":\"Session moyenne\",\"First seen\":\"Vu pour la première fois\",\"Last seen\":\"Vu pour la dernière fois\",\"Last live\":\"Dernier direct\",\"Approximate online times\":\"Horaires de présence approximatifs\",\"Activity, sources and profile state\":\"Activité, sources et état du profil\",\"Example images\":\"Images d’exemple\",\"Own groups\":\"Groupes personnels\",\"Public/recognized tags\":\"Tags publics/reconnus\",\"Notes, own tags and priority\":\"Notes, tags personnels et priorité\",\"Local rating\":\"Évaluation locale\",\"Group assignment\":\"Affectation aux groupes\",\"Room and preview actions\":\"Actions du salon et d’aperçu\",\"Library display\":\"Affichage de la bibliothèque\",\"Saved in this browser\":\"Enregistré dans ce navigateur\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Choisissez la taille des cartes, le nombre de modèles par ligne et par page, ainsi que les informations affichées sur chaque carte.\",\"Compact overview\":\"Vue compacte\",\"Balanced\":\"Équilibré\",\"All details\":\"Tous les détails\",\"Show all details\":\"Afficher tous les détails\",\"Core details only\":\"Informations essentielles uniquement\",\"Card zoom\":\"Zoom des cartes\",\"Visible information per model\":\"Informations visibles par modèle\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Avec les colonnes automatiques, le zoom détermine combien de modèles tiennent côte à côte. Un nombre fixe de colonnes est prioritaire. Tous les choix restent enregistrés dans ce navigateur.\",\"No custom tags yet\":\"Aucune balise personnalisée pour l'instant\",\"Name of the new group\":\"Nom du nouveau groupe\",\"Models remain in the library.\":\"Les modèles restent dans la bibliothèque.\",\"Custom groups\":\"Groupes personnalisés\",\"Create new group\":\"Créer un nouveau groupe\",\"Nothing selected — show all\":\"Rien de sélectionné — tout afficher\",\"Any\":\"N'importe lequel\",\"Women\":\"Femmes\",\"Men\":\"Hommes\",\"Unknown\":\"Inconnu\",\"Error/loading\":\"Erreur/chargement\",\"Every weekday\":\"Chaque jour de la semaine\",\"Sunday\":\"dimanche\",\"Monday\":\"Lundi\",\"Tuesday\":\"mardi\",\"Wednesday\":\"mercredi\",\"Thursday\":\"jeudi\",\"Friday\":\"vendredi\",\"Saturday\":\"samedi\",\"Every time\":\"Chaque fois\",\"Advanced filters and sorting\":\"Filtres et tri avancés\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combinez autant de filtres que vous le souhaitez. Plusieurs genres, états, pays et tags fonctionnent comme des sélections multiples. Chaque paramètre reste enregistré dans ce navigateur.\",\"Gender/type\":\"Sexe/type\",\"Countries/regions (multiple)\":\"Pays/régions (plusieurs)\",\"Add country…\":\"Ajouter un pays…\",\"Public tags (multiple)\":\"Balises publiques (plusieurs)\",\"Add tag…\":\"Ajouter une balise…\",\"Public-tag matching\":\"Correspondance de balises publiques\",\"At least one tag\":\"Au moins une balise\",\"Every selected tag\":\"Chaque balise sélectionnée\",\"Catalog source\":\"Source du catalogue\",\"All models\":\"Tous les modèles\",\"Personally organized only\":\"Organisé personnellement uniquement\",\"Automatically discovered only\":\"Découvert automatiquement uniquement\",\"Profile data\":\"Données de profil\",\"Loaded or missing\":\"Chargé ou manquant\",\"Loaded only\":\"Chargé uniquement\",\"Missing only\":\"Manquant seulement\",\"Picture/category\":\"Image/catégorie\",\"Neither HD nor gaming\":\"Ni HD ni jeux\",\"Watch history\":\"Regarder l'historique\",\"Never watched\":\"Jamais regardé\",\"Own tags\":\"Propres balises\",\"With or without\":\"Avec ou sans\",\"With tags only\":\"Avec des balises uniquement\",\"Without tags only\":\"Sans balises uniquement\",\"At least one group\":\"Au moins un groupe\",\"No group\":\"Aucun groupe\",\"With notes only\":\"Avec des notes uniquement\",\"Without notes only\":\"Uniquement sans notes\",\"Numeric ranges and minimums\":\"Plages numériques et minimums\",\"Minimum age\":\"Âge minimum\",\"Maximum age\":\"Âge maximum\",\"Minimum viewers\":\"Nombre minimum de spectateurs\",\"Maximum viewers\":\"Nombre maximum de spectateurs\",\"Minimum followers\":\"Nombre minimum d'abonnés\",\"Maximum followers\":\"Nombre maximum d'abonnés\",\"Minimum total watch time\":\"Durée totale minimale de visionnage\",\"Currently online for at least\":\"Actuellement en ligne depuis au moins\",\"Minimum sessions\":\"Séances minimales\",\"Minimum rating\":\"Note minimale\",\"Minimum priority\":\"Priorité minimale\",\"Active within\":\"Actif à l'intérieur\",\"days\":\"jours\",\"Minimum appearances\":\"Apparitions minimales\",\"Minimum live observations\":\"Observations minimales en direct\",\"Filter by typical online time\":\"Filtrer par temps de connexion habituel\",\"Weekday\":\"Jour de la semaine\",\"Hour (local time)\":\"Heure (heure locale)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Les champs nuls ou vides signifient qu'il n'y a pas de limite. Les âges inconnus sont exclus lorsqu'une limite d'âge est active. Les filtres temporels utilisent les heures en direct observées sur cet appareil, en retombant prudemment sur les heures de visionnage en cas de besoin.\",\"Reset all library filters\":\"Réinitialiser tous les filtres de la bibliothèque\",\"Open live room\":\"Salon ouvert\",\"Open room page\":\"Ouvrir la page de la salle\",\"Status\":\"Statut\",\"Open model room in a new tab\":\"Ouvrir la salle modèle dans un nouvel onglet\",\"Open the Chaturbate single-model view in a new tab\":\"Ouvrez la vue modèle unique de Chaturbate dans un nouvel onglet\",\"Save profile\":\"Enregistrer le profil\",\"Restore\":\"Restaurer\",\"Archive\":\"Archiver\",\"Advanced statistics filters\":\"Filtres de statistiques avancés\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Chaque filtre peut être combiné ; les pays, les balises, les groupes et les types prennent en charge la sélection multiple. Les choix sont enregistrés de manière permanente.\",\"Own groups (multiple)\":\"Propres groupes (plusieurs)\",\"Add group…\":\"Ajouter un groupe…\",\"Tag matching\":\"Correspondance des balises\",\"At least one\":\"Au moins un\",\"All selected tags\":\"Toutes les balises sélectionnées\",\"Group matching\":\"Appariement de groupe\",\"All selected groups\":\"Tous les groupes sélectionnés\",\"Cover/example image\":\"Image de couverture/exemple\",\"Known public image available\":\"Image publique connue disponible\",\"No known image\":\"Aucune image connue\",\"Currently live\":\"Actuellement en direct\",\"Maximum total watch time\":\"Durée totale maximale de visionnage\",\"Maximum sessions\":\"Sessions maximales\",\"Minimum active days\":\"Jours d'activité minimum\",\"Minimum previews\":\"Aperçus minimaux\",\"Minimum quick views\":\"Vues rapides minimales\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Les champs nuls ou vides signifient qu'il n'y a pas de limite. Les valeurs inconnues ne sont jamais devinées ; les tranches d’âge excluent les âges inconnus.\",\"Reset all statistics filters\":\"Réinitialiser tous les filtres de statistiques\",\"Search name, tags or notes…\":\"Rechercher un nom, des tags ou des notes…\",\"Search\":\"Rechercher\",\"Sorting\":\"Tri\",\"Most watch time\":\"La plupart du temps de visionnage\",\"Least watch time\":\"Durée de visionnage minimale\",\"Recently active\":\"Récemment actif\",\"Longest inactive\":\"Inactivité la plus longue\",\"Most sessions\":\"La plupart des séances\",\"Fewest sessions\":\"Moins de séances\",\"Longest average session\":\"Séance moyenne la plus longue\",\"Shortest average session\":\"Séance moyenne la plus courte\",\"Longest single session\":\"Session unique la plus longue\",\"Most active days\":\"Jours les plus actifs\",\"Highest watch share\":\"Part de surveillance la plus élevée\",\"Most previews\":\"La plupart des aperçus\",\"Most quick views\":\"Vues les plus rapides\",\"Most discoveries\":\"La plupart des découvertes\",\"Most followers\":\"La plupart des abonnés\",\"Youngest first\":\"Le plus jeune en premier\",\"Oldest first\":\"Le plus ancien en premier\",\"Highest rating\":\"Note la plus élevée\",\"Highest priority\":\"Priorité la plus élevée\",\"Most appearances\":\"La plupart des apparitions\",\"Most live observations\":\"La plupart des observations en direct\",\"First observed\":\"Observé pour la première fois\",\"Country A–Z\":\"Pays de A à Z\",\"Most public tags\":\"Balises les plus publiques\",\"Most groups\":\"La plupart des groupes\",\"Best technical quality\":\"Meilleure qualité technique\",\"Cover/example images first\":\"Images de couverture/exemples en premier\",\"All statuses\":\"Tous les statuts\",\"Live only\":\"En direct uniquement\",\"Library only\":\"Bibliothèque uniquement\",\"View\":\"Vue\",\"Active library\":\"Bibliothèque active\",\"Live now\":\"Vivez maintenant\",\"With notes\":\"Avec des notes\",\"Newest saved/discovered\":\"Le plus récent enregistré/découvert\",\"Oldest saved/discovered\":\"Le plus ancien enregistré/découvert\",\"Lowest rating\":\"Note la plus basse\",\"Lowest priority\":\"Priorité la plus basse\",\"Live longest now\":\"Vivez le plus longtemps maintenant\",\"Age youngest first\":\"Âge le plus jeune en premier\",\"Age oldest first\":\"Âge le plus âgé en premier\",\"Most recently live\":\"En direct le plus récemment\",\"First discovered\":\"Découvert pour la première fois\",\"Most own groups\":\"La plupart possèdent des groupes\",\"Card size\":\"Taille de la carte\",\"Compact\":\"Compacte\",\"Comfortable\":\"Confortable\",\"Large\":\"Grand\",\"Cinema\":\"Cinéma\",\"Extra large\":\"Très grand\",\"Models per row\":\"Modèles par ligne\",\"Automatic for window\":\"Automatique selon la fenêtre\",\"Recorded models\":\"Modèles enregistrés\",\"Total cam time\":\"Durée totale de la caméra\",\"across all models\":\"sur tous les modèles\",\"Dock + hover\":\"Dock + survol\",\"Last 30 days\":\"30 derniers jours\",\"local events\":\"événements locaux\",\"Top model\":\"Top modèle\",\"no watch time yet\":\"pas encore d'heure de visionnage\",\"Deeper analysis\":\"Analyse plus approfondie\",\"Open Analytics Studio\":\"Ouvrir Analytics Studio\",\"Late night\":\"Tard dans la nuit\",\"Morning\":\"Matin\",\"Daytime\":\"Jour\",\"Evening\":\"Soirée\",\"Gaming\":\"Jeux\",\"Music & DJ\":\"Musique et DJ\",\"Dance\":\"Danse\",\"Cosplay & anime\":\"Cosplays et dessins animés\",\"Fitness & sports\":\"Remise en forme et sport\",\"Talk & social\":\"Discussions et réseaux sociaux\",\"Outdoor & travel\":\"Plein air et voyages\",\"Roleplay\":\"Jeu de rôle\",\"Performance\":\"Performances\",\"My interest statistics\":\"Mes statistiques d’intérêts\",\"All local data\":\"Toutes les données locales\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Calculé à partir de l'heure de visionnage locale réelle, des sessions, des aperçus, des vues rapides, des favoris et des informations sur le modèle public actuellement connu.\",\"Favorite tag\":\"Tag favori\",\"not enough data\":\"pas assez de données\",\"Favorite genre\":\"Genre favori\",\"from public tags\":\"à partir de balises publiques\",\"Most-watched age\":\"Âge le plus regardé\",\"no age data\":\"pas de données sur l'âge\",\"Most-watched country\":\"Pays le plus regardé\",\"no country data\":\"pas de données sur le pays\",\"Favorite model type\":\"Type de modèle préféré\",\"no type data\":\"aucune donnée de type\",\"Favorite viewing time\":\"Moment de visionnage préféré\",\"no time data\":\"pas de données temporelles\",\"Top-three concentration\":\"Concentration parmi les trois premiers\",\"watch time on the top three models\":\"regarder l'heure sur les trois meilleurs modèles\",\"Favorite affinity\":\"Affinité préférée\",\"watch time on current favorites\":\"regarder l'heure sur les favoris actuels\",\"watch time on HD models\":\"regarder l'heure sur les modèles HD\",\"Metadata coverage\":\"Couverture des métadonnées\",\"age\":\"âge\",\"country\":\"pays\",\"Tags, genres and browsing interests\":\"Tags, genres et centres d’intérêt explorés\",\"Tags by watch time\":\"Tags par heure de visionnage\",\"Your ranking appears when watched models have public tags.\":\"Votre classement apparaît lorsque les modèles regardés ont des balises publiques.\",\"Favorite genre worlds\":\"Mondes de genre préférés\",\"No genre world can yet be assigned from public tags.\":\"Aucun monde de genre ne peut encore être attribué à partir de balises publiques.\",\"Your own tags\":\"Vos propres balises\",\"Your saved custom tags are also evaluated here by watch time.\":\"Vos balises personnalisées enregistrées sont également évaluées ici en fonction de la durée de visionnage.\",\"Preview-interest tags\":\"Balises d'intérêt pour l'aperçu\",\"Browsing interests appear when you use live previews or quick views.\":\"Les centres d'intérêt de navigation apparaissent lorsque vous utilisez des aperçus en direct ou des vues rapides.\",\"Most-previewed models\":\"Modèles les plus aperçus\",\"No previews have been recorded in this period yet.\":\"Aucune avant-première n'a encore été enregistrée au cours de cette période.\",\"Age, origin and model type\":\"Âge, origine et type de modèle\",\"Age groups by watch time\":\"Tranches d'âge par durée de visionnage\",\"Not enough public age data is available.\":\"Il n’existe pas suffisamment de données publiques sur l’âge.\",\"Countries by watch time\":\"Pays par heure de visionnage\",\"Not enough public country data is available.\":\"Il n’existe pas suffisamment de données publiques par pays.\",\"Most-watched places\":\"Lieux les plus regardés\",\"Not enough public location data is available.\":\"Il n'y a pas suffisamment de données de localisation publiques disponibles.\",\"Model types by watch time\":\"Types de modèles par heure de visionnage\",\"Not enough public model-type data is available.\":\"Il n’y a pas suffisamment de données publiques de type modèle disponibles.\",\"Viewing rhythm and favorite signals\":\"Rythme de visionnage et signaux des favoris\",\"Most active weekdays\":\"Jours de semaine les plus actifs\",\"Not enough visit events are available.\":\"Il n'y a pas assez d'événements de visite disponibles.\",\"Times of day\":\"Heures de la journée\",\"Tags among favorites\":\"Tags parmi les favoris\",\"Preference signals appear when you mark models as favorites.\":\"Des signaux de préférence apparaissent lorsque vous marquez des modèles comme favoris.\",\"Countries among favorites\":\"Pays parmi les favoris\",\"No known countries among favorites yet.\":\"Aucun pays connu parmi les favoris pour l'instant.\",\"Age groups among favorites\":\"Les tranches d’âge parmi les favoris\",\"No known ages among favorites yet.\":\"Aucun âge connu parmi les favoris pour l’instant.\",\"Model types among favorites\":\"Types de modèles parmi les favoris\",\"No known model types among favorites yet.\":\"Aucun type de modèle connu parmi les favoris pour l'instant.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Ces résultats ne sont pas envoyés à un serveur. Seule la durée réelle de visionnage compte dans les préférences de visualisation ; les aperçus et les favoris sont affichés séparément. Les données manquantes sur l’âge, le pays, le lieu ou l’étiquette ne sont jamais devinées.\",\"Open Following on Chaturbate\":\"Ouvrir le suivi sur Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Un historique complet de la durée de visionnage ou des visites ne peut pas être récupéré rétroactivement car Chaturbate ne fournit aucune API d'historique personnel documentée. Vous pouvez demander une copie de vos données personnelles stockées via l’exportation de données officielle de Chaturbate. Si cette exportation contient des champs d'historique utilisables, RoomGrid peut prendre en charge une importation sécurisée après avoir validé le format de fichier. L'API Stats officielle fournit les valeurs actuelles du diffuseur ; RoomGrid ne peut créer son propre historique qu'à partir de la première connexion.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Comparez chaque modèle observé, enregistré et regardé localement, avec la durée de visionnage, les sessions, l'activité, les aperçus, la qualité et les classements.\",\"Large model library\":\"Grande bibliothèque de modèles\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Vos modèles sous forme de profils pratiques et riches en images avec statut en direct, notes, balises, évaluations, priorité, groupes et actions directes.\",\"My user dashboard\":\"Mon tableau de bord utilisateur\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Votre historique d'utilisation local privé, vos habitudes, les données de votre bibliothèque et, uniquement lorsque vous êtes délibérément connecté, les données de votre compte de diffuseur officiel.\",\"🔒 Local & private on this device\":\"🔒 Local et privé sur cet appareil\",\"Preparing data…\":\"Préparation des données…\",\"RoomGrid Comfort Center\":\"Centre de confort RoomGrid\",\"inside the standard Chaturbate page\":\"dans la page Chaturbate standard\",\"All RoomGrid pages and settings\":\"Toutes les pages et paramètres de RoomGrid\",\"Media paused; names and images are covered.\":\"Médias en pause ; les noms et les images sont couverts.\",\"Restore previous view\":\"Restaurer la vue précédente\",\"Enter local PIN\":\"Entrez le code PIN local\",\"In-page notifications\":\"Notifications sur la page\",\"No new notifications\":\"Aucune nouvelle notification\",\"Notifications are off\":\"Les notifications sont désactivées\",\"RoomGrid notification\":\"Notification RoomGrid\",\"Dismiss as read\":\"Ignorer comme lu\",\"More unread notifications\":\"Plus de notifications non lues\",\"RoomGrid update\":\"Mise à jour de RoomGrid\",\"A monitored source changed status.\":\"Une source surveillée a changé de statut.\",\"This is a local RoomGrid test notification.\":\"Il s'agit d'une notification de test RoomGrid locale.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Test réussi : les notifications peuvent apparaître directement dans la barre RoomGrid.\",\"Settings\":\"Paramètres\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Espaces de travail, bibliothèque, conception, données, historique, notifications, confidentialité et performances\",\"Workspaces\":\"Espaces de travail\",\"Data\":\"Données\",\"History & statistics\":\"Histoire et statistiques\",\"Broadcaster stats\":\"Statistiques des diffuseurs\",\"Windows & performance\":\"Windows et performances\",\"Optional tools\":\"Outils optionnels\",\"Settings pages\":\"Pages de paramètres\",\"RoomGrid control center\":\"Centre de contrôle RoomGrid\",\"Any online model\":\"N'importe quel modèle en ligne\",\"Workspace name\":\"Nom de l'espace de travail\",\"Optional description\":\"Description facultative\",\"Active\":\"Actif\",\"No description\":\"Pas de description\",\"Name\":\"Nom\",\"Description\":\"Descriptif\",\"Replace offline models\":\"Remplacer les modèles hors ligne\",\"Replacement group\":\"Groupe de remplacement\",\"Delay seconds\":\"Secondes de retard\",\"Save current state\":\"Enregistrer l'état actuel\",\"New window\":\"Nouvelle fenêtre\",\"Export this workspace\":\"Exporter cet espace de travail\",\"Duplicate\":\"Dupliquer\",\"Workspace restored\":\"Espace de travail restauré\",\"Workspace archived\":\"Espace de travail archivé\",\"Delete permanently\":\"Supprimer définitivement\",\"Permanently delete this workspace?\":\"Supprimer définitivement cet espace de travail ?\",\"Saved layout and panel sizes\":\"Disposition enregistrée et tailles de panneau\",\"Apply\":\"Postuler\",\"Layout template deleted\":\"Modèle de mise en page supprimé\",\"Parent group\":\"Groupe de parents\",\"None\":\"Aucun\",\"Group description\":\"Descriptif du groupe\",\"Save current workspace\":\"Enregistrer l'espace de travail actuel\",\"Save layout template\":\"Enregistrer le modèle de mise en page\",\"Template name\":\"Nom du modèle\",\"Layout templates\":\"Modèles de mise en page\",\"Device layout\":\"Disposition de l'appareil\",\"Groups and subgroups\":\"Groupes et sous-groupes\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Chaque espace de travail stocke des dispositions de bureau et compactes distinctes. Le remplacement automatique est désactivé par défaut et ne remplit les emplacements hors ligne qu'une fois que vous l'avez activé pour cet espace de travail.\",\"No templates yet.\":\"Aucun modèle pour l'instant.\",\"Group name\":\"Nom du groupe\",\"New subgroup\":\"Nouveau sous-groupe\",\"Enter parent group name\":\"Entrez le nom du groupe parent\",\"Subgroup name\":\"Nom du sous-groupe\",\"Name, note or local tag\":\"Nom, note ou étiquette locale\",\"Choose group…\":\"Choisissez un groupe…\",\"Add selected models to group\":\"Ajouter les modèles sélectionnés au groupe\",\"Watch later\":\"A regarder plus tard\",\"Paused\":\"En pause\",\"Collection name\":\"Nom de la collection\",\"Tags, comma separated\":\"Balises, séparées par des virgules\",\"Countries, comma separated\":\"Pays, séparés par des virgules\",\"All types\":\"Tous types\",\"Rule name\":\"Nom de la règle\",\"Required tags\":\"Tags requis\",\"Local alert\":\"Alerte locale\",\"Add to favorites\":\"Ajouter aux favoris\",\"Add to group\":\"Ajouter au groupe\",\"Any tag\":\"N'importe quelle balise\",\"Any country\":\"N'importe quel pays\",\"Enable\":\"Activer\",\"Delete\":\"Supprimer\",\"Automation rule deleted\":\"Règle d'automatisation supprimée\",\"Select model\":\"Sélectionnez le modèle\",\"Later\":\"Plus tard\",\"Never visited\":\"Jamais visité\",\"Last online\":\"Dernière connexion\",\"Last visited\":\"Dernière visite\",\"Local note\":\"Remarque locale\",\"Local tags, comma separated\":\"Balises locales, séparées par des virgules\",\"Remove later\":\"Supprimer plus tard\",\"Hide today\":\"Cacher aujourd'hui\",\"Pause 7 days\":\"Pause 7 jours\",\"Never suggest\":\"Ne jamais suggérer\",\"Select results\":\"Sélectionnez les résultats\",\"Clear selection\":\"Effacer la sélection\",\"selected\":\"sélectionné\",\"Add to watch later\":\"Ajouter à regarder plus tard\",\"Permanently delete selected models?\":\"Supprimer définitivement les modèles sélectionnés ?\",\"Smart collections\":\"Collections intelligentes\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Les collections sont mises à jour automatiquement à mesure que les métadonnées et statistiques locales augmentent ; les modèles ne sont pas dupliqués.\",\"Tags\":\"Balises\",\"Countries\":\"Pays\",\"Favorites only\":\"Favoris uniquement\",\"Online only\":\"En ligne uniquement\",\"Save smart collection\":\"Enregistrer la collection intelligente\",\"Delete current collection\":\"Supprimer la collection actuelle\",\"Delete this smart collection? Model data stays unchanged.\":\"Supprimer cette collection intelligente ? Les données du modèle restent inchangées.\",\"Rules & automation center\":\"Centre de règles et d'automatisation\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Lorsque les données du modèle local correspondent, affichez une alerte, ajoutez un favori ou ajoutez le modèle à un groupe choisi.\",\"Target group\":\"Groupe cible\",\"Save rule\":\"Enregistrer la règle\",\"No automation rules yet.\":\"Aucune règle d'automatisation pour l'instant.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Les noms d’utilisateur sont dédupliqués par correspondance exacte normalisée. La recherche couvre les noms, les notes et les balises locales. L'archivage, la suppression d'une vue et la suppression définitive restent des actions distinctes.\",\"No matching models.\":\"Aucun modèle correspondant.\",\"export\":\"exporter\",\"Models and notes\":\"Modèles et remarques\",\"This backup could not be read. Current data was not changed.\":\"Cette sauvegarde n'a pas pu être lue. Les données actuelles n'ont pas été modifiées.\",\"Import preview (nothing changed yet)\":\"Aperçu de l'importation (rien n'a encore changé)\",\"conflicts\":\"conflits\",\"Includes sensitive statistics\":\"Comprend des statistiques sensibles\",\"models\":\"modèles\",\"events\":\"événements\",\"quality samples\":\"échantillons de qualité\",\"Import statistics too\":\"Importez également des statistiques\",\"Import method\":\"Méthode d'importation\",\"Merge (recommended)\":\"Fusionner (recommandé)\",\"Replace selected sections\":\"Remplacer les sections sélectionnées\",\"Apply import\":\"Appliquer l'importation\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Importation terminée. La configuration et les statistiques précédentes ont été sauvegardées séparément.\",\"The import could not be saved safely. The backup remains available.\":\"L'importation n'a pas pu être enregistrée en toute sécurité. La sauvegarde reste disponible.\",\"Cancel preview\":\"Annuler l'aperçu\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Ce fichier contient des données de modèle, de note ou d'espace de travail et doit être traité comme sensible.\",\"This import is not marked sensitive.\":\"Cette importation n’est pas marquée comme sensible.\",\"Data version\":\"Version des données\",\"Software version\":\"Version du logiciel\",\"Revision\":\"Révision\",\"Local models\":\"Modèles locaux\",\"Selective export\":\"Exportation sélective\",\"Export selected\":\"Exporter la sélection\",\"Full export\":\"Exportation complète\",\"Complete safety package\":\"Pack de sécurité complet\",\"Export statistics separately\":\"Exporter les statistiques séparément\",\"Choose file and preview\":\"Choisir le fichier et prévisualiser\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Le package de sécurité complet contient la configuration et les statistiques sensibles, mais jamais le jeton de statistiques officiel. Chaque importation est prévisualisée et sauvegarde la configuration et les statistiques séparément avant de l'appliquer.\",\"Storage and backup center\":\"Centre de stockage et de sauvegarde\",\"Local storage\":\"Stockage local\",\"Site storage\":\"Stockage du site\",\"Available quota\":\"Quota disponible\",\"Internal statistics backups\":\"Sauvegardes de statistiques internes\",\"workspaces\":\"espaces de travail\",\"LocalStorage + update vault\":\"LocalStorage + mise à jour du coffre-fort\",\"Statistics database\":\"Base de données statistiques\",\"The official statistics token is never included in normal or complete backups.\":\"Le jeton de statistiques officiel n'est jamais inclus dans les sauvegardes normales ou complètes.\",\"Excluded\":\"Exclus\",\"Create configuration restore point\":\"Créer un point de restauration de configuration\",\"Create statistics restore point\":\"Créer un point de restauration des statistiques\",\"Legacy cleanup\":\"Nettoyage hérité\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Les données héritées ne sont jamais supprimées automatiquement. Supprimez les anciennes clés v6/v7 uniquement après un examen réussi.\",\"Remove old v6/v7 copies\":\"Supprimer les anciennes copies v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Supprimer uniquement les anciennes copies v6/v7 ? Les données et sauvegardes actuelles restent.\",\"items removed\":\"éléments supprimés\",\"Aurora\":\"Aurore\",\"Blue and teal, the RoomGrid signature\":\"Bleu et bleu sarcelle, la signature RoomGrid\",\"Ember\":\"Braise\",\"Warm orange and rose\":\"Orange chaude et rose\",\"Frost\":\"Gel\",\"Crisp cyan and violet\":\"Cyan et violet vifs\",\"Custom\":\"Personnalisé\",\"Use the colors selected below\":\"Utilisez les couleurs sélectionnées ci-dessous\",\"Name the current appearance\":\"Nommer l'apparence actuelle\",\"My appearance\":\"Mon apparence\",\"Delete appearance profile\":\"Supprimer le profil d'apparence\",\"This appearance profile could not be imported.\":\"Ce profil d'apparence n'a pas pu être importé.\",\"Light\":\"Lumière\",\"Dark\":\"Sombre\",\"Export\":\"Exporter\",\"Home\":\"Accueil\",\"Listings and tags\":\"Annonces et balises\",\"Model pages\":\"Pages modèles\",\"Use global appearance\":\"Utiliser l'apparence globale\",\"This logo format is unsafe or unsupported.\":\"Ce format de logo n'est pas sûr ou n'est pas pris en charge.\",\"The logo file could not be read.\":\"Le fichier du logo n'a pas pu être lu.\",\"MultiCam tools\":\"Outils MultiCam\",\"Page\":\"Pages\",\"Panel\":\"Panneau\",\"Card surface\":\"Surface de la carte\",\"Input\":\"Entrée\",\"Border\":\"Frontière\",\"Text\":\"Texte\",\"Muted text\":\"Texte masqué\",\"Success\":\"Succès\",\"Warning\":\"Avertissement\",\"Info and focus\":\"Infos et focus\",\"Data hub\":\"Centre de données\",\"Text / page\":\"Texte/page\",\"Text / panel\":\"Texte / panneau\",\"Muted text / panel\":\"Texte/panneau masqué\",\"Accent text / accent\":\"Texte d'accent / accent\",\"Desktop\":\"Bureau\",\"Tablet\":\"Tablette\",\"Brand and logo studio\":\"Atelier de marque et de logo\",\"Monogram\":\"Monogramme\",\"Tagline\":\"Slogan\",\"Brand display\":\"Affichage de la marque\",\"Full name\":\"Nom complet\",\"Uploaded logo\":\"Logo téléchargé\",\"Logo shape\":\"Forme du logo\",\"Rounded\":\"Arrondi\",\"Circle\":\"Cercle\",\"Square\":\"Carré\",\"Pill\":\"Pilule\",\"Page title\":\"Titre de la page\",\"Website original\":\"Format d’origine du site\",\"Brand plus original\":\"Marque plus original\",\"Custom title\":\"Titre personnalisé\",\"Custom page title\":\"Titre de page personnalisé\",\"Image fit\":\"Ajustement de l'image\",\"Horizontal position\":\"Position horizontale\",\"Vertical position\":\"Position verticale\",\"Upload light-mode logo\":\"Télécharger le logo en mode clair\",\"Upload dark-mode logo\":\"Télécharger le logo en mode sombre\",\"Remove logos\":\"Supprimer les logos\",\"Show version\":\"Afficher la version\",\"Use logo as tab icon on RoomGrid pages\":\"Utiliser le logo comme icône d'onglet sur les pages RoomGrid\",\"Replace website logo in the original header (local only)\":\"Remplacer le logo du site Web dans l'en-tête d'origine (local uniquement)\",\"Complete color system\":\"Système de couleurs complet\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Couleurs de la page de contrôle, du panneau, de la carte, de la saisie, de la bordure, du texte et de l'état en plus des accents.\",\"Derived automatically\":\"Dérivé automatiquement\",\"Fully custom\":\"Entièrement personnalisé\",\"Editing dark palette\":\"Modification de la palette sombre\",\"Editing light palette\":\"Modification de la palette de lumière\",\"Reset current palette\":\"Réinitialiser la palette actuelle\",\"Layout editor\":\"Éditeur de mise en page\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Définissez l'espacement du contenu, la largeur de la carte, l'en-tête et la barre latérale de manière cohérente sur les pages RoomGrid.\",\"Minimum card width\":\"Largeur minimale de la carte\",\"Section spacing\":\"Espacement des sections\",\"Content padding\":\"Remplissage du contenu\",\"Header height\":\"Hauteur du linteau\",\"Maximum columns\":\"Nombre maximal de colonnes\",\"Automatic\":\"Automatique\",\"Card aspect\":\"Aspect carte\",\"Sidebar position\":\"Position de la barre latérale\",\"Left\":\"Gauche\",\"Right\":\"À droite\",\"Keep header visible while scrolling\":\"Garder l'en-tête visible lors du défilement\",\"Live device preview\":\"Aperçu de l'appareil en direct\",\"Contrast and accessibility\":\"Contraste et accessibilité\",\"High-contrast mode\":\"Mode contraste élevé\",\"Underline links\":\"Souligner les liens\",\"Reduce transparency\":\"Réduire la transparence\",\"Boost base text size\":\"Augmenter la taille du texte de base\",\"Focus ring width\":\"Largeur de la bague de mise au point\",\"Minimum target size\":\"Taille cible minimale\",\"Respect system reduced-motion setting\":\"Respecter le réglage de mouvement réduit du système\",\"Apply accessible defaults\":\"Appliquer les valeurs par défaut accessibles\",\"Profiles by page and screen size\":\"Profils par page et taille d'écran\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Enregistrez une ou plusieurs apparences, puis attribuez-les par type de page pour ordinateur, tablette et mobile. Les profils changent automatiquement lorsque la taille de l'écran change.\",\"Enable responsive design profiles\":\"Activer les profils de conception réactifs\",\"Mobile maximum width\":\"Largeur maximale mobile\",\"Tablet maximum width\":\"Largeur maximale de la tablette\",\"RoomGrid design system\":\"Système de conception RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Choisissez une orientation de marque, une largeur de contenu et une densité d'informations sur la carte. Chaque page suit immédiatement.\",\"Primary accent\":\"Accent principal\",\"Secondary accent\":\"Accent secondaire\",\"Content width\":\"Largeur du contenu\",\"Centered\":\"Centré\",\"Wide\":\"Large\",\"Full width\":\"Pleine largeur\",\"Card style\":\"Style de carte\",\"Visual\":\"Visuel\",\"Data-rich\":\"Riche en données\",\"Widget density\":\"Densité des widgets\",\"Spacious\":\"Spacieux\",\"Theme and surfaces\":\"Thème et surfaces\",\"Surface style\":\"Style de surface\",\"Soft\":\"Doux\",\"Flat\":\"Plat\",\"Glass\":\"Verre\",\"Page background\":\"Fond de page\",\"Solid\":\"Solide\",\"Gradient\":\"Dégradé\",\"Ambient glow\":\"Lueur ambiante\",\"Surface opacity\":\"Opacité des surfaces\",\"Custom accent color\":\"Couleur d'accent personnalisée\",\"Sizing and motion\":\"Dimensionnement et mouvement\",\"Font style\":\"Style de police\",\"System\":\"Système\",\"Classic serif\":\"Empattement classique\",\"Density\":\"Densité\",\"Font scale\":\"Échelle de police\",\"Control size\":\"Taille du contrôle\",\"Corner radius\":\"Rayon d'angle\",\"Card spacing\":\"Espacement des cartes\",\"Shadow strength\":\"Force de l'ombre\",\"Sidebar width\":\"Largeur de la barre latérale\",\"Enable animations\":\"Activer les animations\",\"Appearance profiles\":\"Profils d'apparence\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Enregistrez l'apparence complète en tant que profil réutilisable. Les profils restent disponibles après les mises à jour du script.\",\"Save current appearance\":\"Enregistrer l'apparence actuelle\",\"Import profile\":\"Profil d'importation\",\"No profiles saved yet.\":\"Aucun profil enregistré pour le moment.\",\"Instant preview\":\"Aperçu instantané\",\"Example card\":\"Exemple de carte\",\"This is the combined result of the current visual settings.\":\"C'est le résultat combiné des paramètres visuels actuels.\",\"Example action\":\"Exemple d'action\",\"Per-page design profiles\":\"Profils de conception par page\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Choisissez une apparence enregistrée différente pour l'accueil, les listes, les pages de modèles, MultiCam, Discover et le hub de données.\",\"Enable per-page design\":\"Activer la conception par page\",\"Normal website pages\":\"Pages normales du site Web\",\"Enable RoomGrid page design\":\"Activer la conception de pages RoomGrid\",\"Tag and category pages\":\"Pages de balises et de catégories\",\"Cards\":\"Cartes\",\"List\":\"Liste\",\"Page density\":\"Densité des pages\",\"Polish navigation\":\"Navigation polonaise\",\"Polish sidebars\":\"Encadrés polonais\",\"Hide recognized promo and merch areas\":\"Masquer les zones promotionnelles et commerciales reconnues\",\"Compact footer\":\"Pied de page compact\",\"Mark saved models\":\"Marquer les modèles enregistrés\",\"Mark favorites separately\":\"Marquer les favoris séparément\",\"Mark visited models\":\"Marquer les modèles visités\",\"Show card quick actions\":\"Afficher les actions rapides de la carte\",\"Animated live previews on home and listing pages\":\"Aperçus animés en direct sur les pages d'accueil et de référencement\",\"Preview refresh rate\":\"Taux de rafraîchissement de l'aperçu\",\"Off\":\"Désactivé\",\"Update mode\":\"Mode mise à jour\",\"All visible cards simultaneously\":\"Toutes les cartes visibles simultanément\",\"Performance-bounded batches\":\"Lots limités par les performances\",\"Batch concurrency limit\":\"Limite de simultanéité des lots\",\"All visible\":\"Tout visible\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Seules les cartes visibles reçoivent des mises à jour de trames JPEG en sourdine ; ils s'arrêtent immédiatement hors de l'écran ou dans un onglet caché. Le mode synchronisé actualise chaque carte visible en une seule vague ; des taux élevés augmentent sensiblement la bande passante et l’utilisation du processeur.\",\"Live video on hover\":\"Vidéo en direct au survol\",\"Play real-time video while hovering\":\"Lire une vidéo en temps réel en survolant\",\"Start delay\":\"Départ différé\",\"Immediately\":\"Immédiatement\",\"Hover enlargement\":\"Agrandissement au survol\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Seule la carte sous le pointeur joue, toujours en sourdine. S'éloigner ou masquer l'onglet arrête immédiatement le flux.\",\"Live Preview Dock\":\"Dock de prévisualisation en direct\",\"Enable low-load live previews\":\"Activer les aperçus en direct à faible charge\",\"Concurrent previews\":\"Aperçus simultanés\",\"Window size\":\"Taille de la fenêtre\",\"Small\":\"Petit\",\"Medium\":\"Moyen\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Les aperçus démarrent en mode silencieux et s'arrêtent dans les onglets masqués. L'audio nécessite toujours un clic explicite.\",\"Quick info & chat focus\":\"Informations rapides et focus sur le chat\",\"Enable model Quick View\":\"Activer l'aperçu rapide du modèle\",\"Chat focus mode (opt-in)\":\"Mode de discussion (opt-in)\",\"Show local timestamps\":\"Afficher les horodatages locaux\",\"System messages\":\"Messages système\",\"Show\":\"Afficher\",\"Dim\":\"Faible\",\"Hide\":\"Masquer\",\"Chat filter strength\":\"Force du filtre de discussion\",\"Low\":\"Faible\",\"Strict\":\"Stricte\",\"Highlight words (comma separated)\":\"Surligner les mots (séparés par des virgules)\",\"Muted words (comma separated)\":\"Mots muets (séparés par des virgules)\",\"Collapse repeated messages\":\"Réduire les messages répétés\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Les améliorations du chat modifient uniquement l'affichage actuel. Ils ne stockent jamais de chat, n’envoient jamais de messages ou ne donnent jamais de pourboires automatiquement.\",\"Shortcuts\":\"Raccourcis\",\"Command menu\":\"Menu de commande\",\"Global model search\":\"Recherche de modèle globale\",\"Reset design defaults\":\"Réinitialiser les paramètres de conception par défaut\",\"Optional modules\":\"Modules optionnels\",\"Normal-page dock and card actions\":\"Actions de dock et de carte sur page normale\",\"Real audio meters\":\"De vrais audiomètres\",\"Media, app and contest directories\":\"Annuaires de médias, d'applications et de concours\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Les commutateurs de module prennent pleinement effet après le rechargement de la page. Les données de base et les modules de sauvegarde restent activés pour éviter la perte de données.\",\"Name the current filters and sorting\":\"Nommer les filtres et le tri actuels\",\"My smart view\":\"Ma vue intelligente\",\"Delete smart view\":\"Supprimer la vue intelligente\",\"Smart view deleted\":\"Vue intelligente supprimée\",\"tags\":\"balises\",\"countries\":\"pays\",\"Saved selection\":\"Sélection enregistrée\",\"Selected\":\"Sélectionné\",\"Website tags\":\"Balises du site Web\",\"Countries (multiple selection)\":\"Pays (sélection multiple)\",\"Select countries\":\"Sélectionnez des pays\",\"Search countries…\":\"Rechercher des pays…\",\"Recognized in current models\":\"Reconnu dans les modèles actuels\",\"All countries\":\"Tous les pays\",\"Tags (multiple selection)\":\"Balises (sélection multiple)\",\"Select tags\":\"Sélectionnez les balises\",\"Search tags…\":\"Rechercher des balises…\",\"Room subject\":\"Sujet du salon\",\"Current online duration\":\"Durée actuelle en ligne\",\"Local watch time and sessions\":\"Temps de visionnage local et sessions\",\"HD, new, gaming and favorite badges\":\"Badges HD, nouveau, gaming et favori\",\"Homepage Studio\":\"Studio de la page d’accueil\",\"Balanced preset\":\"Préréglage équilibré\",\"Compact preset\":\"Préréglage compact\",\"Data-rich preset\":\"Préréglage riche en données\",\"Cinema preset\":\"Préréglage cinéma\",\"Layout and pagination\":\"Mise en page et pagination\",\"Show quick toolbar above models\":\"Afficher la barre rapide au-dessus des modèles\",\"Cards per row\":\"Cartes par ligne\",\"Image ratio\":\"Format de l’image\",\"Information display\":\"Affichage des informations\",\"Detailed\":\"Détaillé\",\"Overlay on image\":\"Superposé sur l’image\",\"Powerful sorting\":\"Tri avancé\",\"Primary sort\":\"Tri primaire\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Le tri modifie uniquement l’ordre d’affichage des cartes chargées. Il ne change ni les suivis ni les favoris de votre compte Chaturbate.\",\"Restore website order\":\"Rétablir l’ordre du site\",\"Clear all filters\":\"Effacer tous les filtres\",\"Search and range filters\":\"Recherche et filtres de plage\",\"When several tags are selected\":\"Lorsque plusieurs balises sont sélectionnées\",\"Model must contain every tag\":\"Le modèle doit contenir toutes les balises\",\"At least one tag matches\":\"Au moins une balise correspond\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Le filtrage par pays affiche les modèles de n'importe quel pays sélectionné. Les balises personnelles et les balises de sites Web reconnues restent disponibles dans la liste et peuvent être trouvées rapidement grâce à la recherche.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Après avoir sélectionné des balises ou des pays, RoomGrid recherche dans le catalogue complet actuel. La même page de résultats affiche initialement 120 correspondances et en charge davantage par morceaux lors du défilement, de sorte que même les balises très courantes restent réactives. Le bouton de rechargement dans la barre d'outils de la page d'accueil actualise la liste en direct.\",\"Quick conditions\":\"Conditions rapides\",\"HD models only\":\"Modèles HD uniquement\",\"New models only\":\"Nouveaux modèles uniquement\",\"Gaming models only\":\"Modèles gaming uniquement\",\"Only saved in RoomGrid\":\"Uniquement enregistrés dans RoomGrid\",\"Only own favorites\":\"Uniquement mes favoris\",\"Following only\":\"Suite seulement\",\"Hide previously watched models\":\"Masquer les modèles déjà regardés\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Le pays, l’âge et la durée en ligne proviennent des cartes publiques actuelles ou de l’annuaire public des salons. Si Chaturbate ne fournit pas une donnée, RoomGrid ne l’invente pas.\",\"Smart filter views\":\"Vues de filtre intelligent\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Enregistrez les filtres et le tri actuels sous forme de vue dynamique. Les modèles mis en ligne ultérieurement et correspondant aux règles apparaissent automatiquement.\",\"Save current filter view\":\"Enregistrer la vue actuelle du filtre\",\"No smart views saved yet.\":\"Aucune vue intelligente enregistrée pour l'instant.\",\"What to show for each model\":\"Éléments affichés pour chaque modèle\",\"Import complete\":\"Importation terminée\",\"Invalid file\":\"Fichier invalide\",\"Range\":\"Gamme\",\"Today\":\"Aujourd'hui\",\"Week\":\"Semaine\",\"Month\":\"Mois\",\"Year\":\"Année\",\"Recording enabled\":\"Enregistrement activé\",\"Private session (do not record)\":\"Séance privée (ne pas enregistrer)\",\"Retention days\":\"Jours de rétention\",\"Usage time\":\"Temps d'utilisation\",\"Summed camera time\":\"Durée cumulée de la caméra\",\"Visits\":\"Visites\",\"Known models\":\"Modèles connus\",\"Weekly heatmap\":\"Carte thermique hebdomadaire\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Agrégé par jour et heure de la semaine ; plus sombre signifie plus de temps observé.\",\"Sunday to Saturday, 24 hours per row\":\"Du dimanche au samedi, 24 heures par ligne\",\"Watch ranking\":\"Regarder le classement\",\"little data\":\"peu de données\",\"No watch history yet.\":\"Pas d'historique des vidéos regardées pour l'instant.\",\"Workspace, filter and quality events\":\"Espace de travail, filtres et événements de qualité\",\"Recent timeline\":\"Chronologie récente\",\"Export sensitive statistics\":\"Exporter des statistiques sensibles\",\"Import statistics\":\"Statistiques d'importation\",\"Delete selected range\":\"Supprimer la plage sélectionnée\",\"Delete local events in the selected range?\":\"Supprimer les événements locaux dans la plage sélectionnée ?\",\"Clear all statistics\":\"Effacer toutes les statistiques\",\"Permanently clear all local statistics?\":\"Effacer définitivement toutes les statistiques locales ?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Les exportations de statistiques contiennent des noms de modèles et des horodatages et doivent être traitées comme sensibles. Les anciennes données sont nettoyées en fonction de leur conservation.\",\"new\":\"nouveau\",\"Under 1 minute\":\"Moins d'une minute\",\"1–5 minutes\":\"1 à 5 minutes\",\"5–15 minutes\":\"5 à 15 minutes\",\"15–30 minutes\":\"15 à 30 minutes\",\"30+ minutes\":\"30+minutes\",\"Local observations and your usage\":\"Observations locales et votre utilisation\",\"Public profile loaded on request\":\"Profil public chargé sur demande\",\"Local RoomGrid interaction\":\"Interaction locale avec RoomGrid\",\"Local browser player\":\"Lecteur de navigateur local\",\"Optional official statistics API\":\"API de statistiques officielles facultative\",\"Local daily aggregation\":\"Agrégation quotidienne locale\",\"Local data\":\"Données locales\",\"Model summaries\":\"Résumés des modèles\",\"Public profiles\":\"Profils publics\",\"Usage events\":\"Événements d'utilisation\",\"Official account snapshots\":\"Instantanés du compte officiel\",\"Daily rollups\":\"Cumuls quotidiens\",\"Camera time\":\"Temps de caméra\",\"vs previous period\":\"par rapport à la période précédente\",\"Trends\":\"Tendances\",\"Daily activity\":\"Activité quotidienne\",\"Playback quality\":\"Qualité de lecture\",\"Privacy status\":\"Statut de confidentialité\",\"Data provenance\":\"Provenance des données\",\"Local RoomGrid usage\":\"Utilisation locale de RoomGrid\",\"7 days\":\"7 jours\",\"30 days\":\"30 jours\",\"90 days\":\"90 jours\",\"Not enough data yet.\":\"Pas encore assez de données.\",\"Models revisited\":\"Des modèles revisités\",\"Previews opened\":\"Aperçus ouverts\",\"Quick Views\":\"Vues rapides\",\"Samples\":\"Échantillons\",\"Private session\":\"Séance privée\",\"History paused\":\"L'histoire en pause\",\"Recording locally\":\"Enregistrement local\",\"Disabled\":\"Désactivé\",\"No cloud sync or telemetry.\":\"Pas de synchronisation cloud ni de télémétrie.\",\"Raw events\":\"Événements bruts\",\"Only local observations and actions from this browser.\":\"Uniquement les observations et actions locales de ce navigateur.\",\"Time\":\"Temps\",\"Models\":\"Modèles\",\"Online planner\":\"Planificateur en ligne\",\"Data & privacy\":\"Données et confidentialité\",\"Configure statistics dashboard\":\"Configurer le tableau de bord des statistiques\",\"Preset\":\"Préréglage\",\"Minimal\":\"Minime\",\"Quality focus\":\"Priorité à la qualité\",\"Privacy focus\":\"Accent sur la confidentialité\",\"Reset dashboard\":\"Réinitialiser le tableau de bord\",\"Size\":\"Taille\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"L'ordre, la visibilité et la largeur sont stockés dans des paramètres sécurisés pour la mise à jour. Chaque widget affiche sa source de données.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Aucun widget de tableau de bord n'est visible. Ouvrez la configuration et sélectionnez au moins un widget.\",\"Median session\":\"Séance médiane\",\"Daily usage trend\":\"Tendance d'utilisation quotidienne\",\"Your trend appears here after you start using RoomGrid.\":\"Votre tendance apparaît ici après avoir commencé à utiliser RoomGrid.\",\"Session-duration distribution\":\"Répartition de la durée des sessions\",\"No session-duration data yet.\":\"Aucune donnée sur la durée de la session pour l'instant.\",\"Return rate\":\"Taux de retour\",\"Total sessions\":\"Nombre total de séances\",\"Longest observed\":\"Le plus longtemps observé\",\"Model ranking\":\"Classement des modèles\",\"Select up to four models for a local comparison.\":\"Sélectionnez jusqu'à quatre modèles pour une comparaison locale.\",\"Quick View\":\"Aperçu rapide\",\"No model data yet.\":\"Aucune donnée de modèle pour l'instant.\",\"Models with time patterns\":\"Modèles avec des modèles temporels\",\"High confidence\":\"Confiance élevée\",\"Moderate confidence\":\"Confiance modérée\",\"Planning horizon\":\"Horizon de planification\",\"Likely next active times\":\"Prochaines heures d'activité probables\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Calculé à partir d’échantillons en ligne observés localement ; cela ne garantit pas une diffusion. La confiance s’améliore avec plus de données.\",\"High\":\"Élevé\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Pas encore assez d’observations en ligne. Le planning apparaît automatiquement avec la bibliothèque.\",\"Discovery events\":\"Événements découverte\",\"Local interaction funnel\":\"Entonnoir d'interaction locale\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Seules vos actions sur cet appareil sont prises en compte ; le contenu du chat et les paiements ne sont jamais suivis.\",\"Workspace events\":\"Événements de l'espace de travail\",\"Filter events\":\"Filtrer les événements\",\"Tag events\":\"Événements de balise\",\"Top-four workspace comparison\":\"Comparaison des quatre principaux espaces de travail\",\"Workspace usage\":\"Utilisation de l'espace de travail\",\"No workspace data yet.\":\"Aucune donnée sur l'espace de travail pour l'instant.\",\"Average active streams\":\"Flux actifs moyens\",\"Playback quality by model\":\"Qualité de lecture par modèle\",\"No quality data yet.\":\"Aucune donnée de qualité pour l'instant.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Le fichier de statistiques n'est pas valide ou dépasse les limites de sécurité. Les données actuelles n'ont pas été modifiées.\",\"Statistics import assistant · preview\":\"Assistant d'importation de statistiques · aperçu\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Rien n'a encore changé. Sélectionnez les sections pour importer et examiner les conflits.\",\"Safe merge (recommended)\":\"Fusion sécurisée (recommandé)\",\"Import complete. The previous statistics were backed up automatically.\":\"Importation terminée. Les statistiques précédentes ont été sauvegardées automatiquement.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"L'importation n'a pas pu être effectuée en toute sécurité. Les données et sauvegardes actuelles restent inchangées.\",\"File schema\":\"Schéma de fichier\",\"Date range\":\"Plage de dates\",\"Sensitive data\":\"Données sensibles\",\"Not marked sensitive\":\"Non marqué comme sensible\",\"Until manually resumed\":\"Jusqu'à la reprise manuelle\",\"Not paused\":\"Pas en pause\",\"Restore this statistics backup? The current state will be backed up first.\":\"Restaurer cette sauvegarde de statistiques ? L'état actuel sera sauvegardé en premier.\",\"Permanently delete this backup?\":\"Supprimer définitivement cette sauvegarde ?\",\"Events\":\"Événements\",\"Estimated statistics storage\":\"Stockage des statistiques estimées\",\"Recording, pauses and private sessions\":\"Enregistrement, pauses et sessions privées\",\"Record usage statistics\":\"Enregistrer les statistiques d'utilisation\",\"Pause 15 minutes\":\"Mettre en pause 15 minutes\",\"Pause 1 hour\":\"Pause 1 heure\",\"Pause 8 hours\":\"Pause 8 heures\",\"Pause indefinitely\":\"Pause indéfiniment\",\"Resume history\":\"Reprendre l'historique\",\"History status\":\"Statut de l'historique\",\"Private for 30 minutes\":\"Privé pendant 30 minutes\",\"Private for 2 hours\":\"Privé pendant 2 heures\",\"Private until resumed\":\"Privé jusqu'à la reprise\",\"End private session\":\"Fin de la séance privée\",\"Not active\":\"Pas actif\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Une session privée arrête également les observations publiques du modèle ; une pause normale dans l'historique arrête l'utilisation personnelle et les enregistrements de qualité.\",\"Automatic deletion and retention\":\"Suppression et conservation automatiques\",\"Raw events (days)\":\"Événements bruts (jours)\",\"Quality samples (days)\":\"Échantillons de qualité (jours)\",\"Account snapshots (days)\":\"Instantanés du compte (jours)\",\"Daily rollups (days)\":\"Cumuls quotidiens (jours)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Les événements bruts expirés sont regroupés dans les totaux quotidiens avant leur suppression automatique. Les instantanés de qualité et de compte utilisent leurs propres limites.\",\"Statistics backup center\":\"Centre de sauvegarde des statistiques\",\"Create internal backup now\":\"Créez une sauvegarde interne maintenant\",\"Pseudonymized export\":\"Exportation pseudonymisée\",\"Print report\":\"Imprimer le rapport\",\"Choose import file\":\"Choisissez le fichier d'importation\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Aucune sauvegarde de statistiques internes pour le moment. Jusqu’à trois sont conservés après des importations, des restaurations ou une création manuelle.\",\"Delete local statistics in this range? A backup will be created first.\":\"Supprimer les statistiques locales dans cette plage ? Une sauvegarde sera créée en premier.\",\"Clear everything\":\"Tout effacer\",\"Permanently clear all local statistics? A backup will be created first.\":\"Effacer définitivement toutes les statistiques locales ? Une sauvegarde sera créée en premier.\",\"Data inventory and provenance\":\"Inventaire et provenance des données\",\"Stored only in this browser\":\"Stocké uniquement dans ce navigateur\",\"Coverage\":\"Couverture\",\"Unknown or missing data is never inferred.\":\"Les données inconnues ou manquantes ne sont jamais déduites.\",\"Metric definitions & data quality\":\"Définitions des métriques et qualité des données\",\"Accumulated locally while a RoomGrid page is active.\":\"Accumulé localement lorsqu'une page RoomGrid est active.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Somme de toutes les sessions de caméra observées ; des flux simultanés sont ajoutés.\",\"browser samples; no interpolation.\":\"exemples de navigateur ; pas d'interpolation.\",\"Privacy boundary\":\"Limite de confidentialité\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Aucun contenu de discussion, aucun paiement ou fichier multimédia n'est enregistré. Les sessions privées n'ajoutent aucun événement.\",\"No history yet\":\"Pas encore d'historique\",\"Loading\":\"Chargement\",\"Connected\":\"Connecté\",\"Locked\":\"Verrouillé\",\"Not connected\":\"Non connecté\",\"Username\":\"Nom d'utilisateur\",\"24-character stats token\":\"Jeton de statistiques à 24 caractères\",\"At least 8 characters\":\"Au moins 8 caractères\",\"Secure official statistics connection\":\"Connexion sécurisée aux statistiques officielles\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Facultatif. Le jeton n’entre jamais dans les paramètres, journaux ou exportations normaux. Le mode session le conserve uniquement en mémoire ; le mode crypté utilise un stockage local isolé.\",\"Stats token\":\"Jeton de statistiques\",\"Vault password\":\"Mot de passe du coffre-fort\",\"Connect for this session\":\"Connectez-vous pour cette session\",\"Username or token is invalid.\":\"Le nom d'utilisateur ou le jeton n'est pas valide.\",\"Encrypt, save & connect\":\"Chiffrez, enregistrez et connectez-vous\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Impossible d'enregistrer le chiffrement. Vérifiez les autorisations du jeton, du mot de passe et du gestionnaire de script utilisateur.\",\"Get official token\":\"Obtenez un jeton officiel\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"L’actualisation automatique s’exécute au maximum toutes les cinq minutes. Ce module utilise uniquement un token officiel que vous fournissez délibérément.\",\"Account\":\"Compte\",\"History snapshots\":\"Instantanés de l'historique\",\"Last refresh\":\"Dernière actualisation\",\"Unlock local vault\":\"Déverrouiller le coffre-fort local\",\"Password\":\"Mot de passe\",\"Unlock\":\"Déverrouiller\",\"Unlock failed.\":\"Le déverrouillage a échoué.\",\"Loading…\":\"Chargement…\",\"Refresh now\":\"Actualiser maintenant\",\"Auto-refresh every 5 minutes\":\"Actualisation automatique toutes les 5 minutes\",\"Disconnect and delete token\":\"Déconnecter et supprimer le jeton\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Supprimer la connexion locale et le jeton chiffré ? Les statistiques historiques demeurent.\",\"Broadcast status\":\"Statut de diffusion\",\"Token balance\":\"Solde des jetons\",\"Tips last hour\":\"Conseils dernière heure\",\"Followers\":\"Abonnés\",\"Up votes\":\"Votes en hausse\",\"Down votes\":\"Votes négatifs\",\"Satisfaction\":\"Satisfaite\",\"Registered viewers\":\"Spectateurs enregistrés\",\"Tokened viewers\":\"Spectateurs tokenisés\",\"Time online\":\"Temps en ligne\",\"Follower trend\":\"Tendance des suiveurs\",\"Tips trend\":\"Tendance des conseils\",\"Token-balance trend\":\"Tendance du solde des jetons\",\"Viewer trend\":\"Tendance des téléspectateurs\",\"Account Health Center\":\"Centre de santé des comptes\",\"Official My Collection\":\"Ma collection officielle\",\"Official support\":\"Soutien officiel\",\"Export local statistics\":\"Exporter les statistiques locales\",\"Delete snapshot history\":\"Supprimer l'historique des instantanés\",\"Delete the local official-statistics snapshots for this account?\":\"Supprimer les instantanés des statistiques officielles locales pour ce compte ?\",\"Last error\":\"Dernière erreur\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"L'état du jeton, la cadence de rafraîchissement, l'histoire locale et les liens officiels sont en direct ici. RoomGrid ne publie, n'envoie jamais de messages ou n'effectue jamais de transactions automatiquement.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Le navigateur n'a pas accordé l'autorisation de notification. Les notifications sur la page restent actives.\",\"Notification profile\":\"Profil de notification\",\"Go-live only\":\"Mise en ligne uniquement\",\"Personalized\":\"Personnalisé\",\"Always (all events)\":\"Toujours (tous les événements)\",\"Inactive days\":\"Jours inactifs\",\"Favorites, rating, priority and your tags decide relevance.\":\"Les favoris, la note, la priorité et vos balises déterminent la pertinence.\",\"Includes go-live, offline and public-profile changes.\":\"Inclut les modifications de mise en ligne, hors ligne et de profil public.\",\"Master switch\":\"Interrupteur principal\",\"On\":\"Sur\",\"Off (default)\":\"Désactivé (par défaut)\",\"Monitored models\":\"Modèles surveillés\",\"Active tab\":\"Onglet actif\",\"This tab\":\"Cet onglet\",\"Another tab\":\"Un autre onglet\",\"Local notifications\":\"Notifications locales\",\"Notification delivery and display\":\"Envoi et affichage des notifications\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Les notifications sur la page utilisent la zone libre de la barre supérieure de RoomGrid et ne nécessitent aucune autorisation du navigateur. La position, la durée et la densité sont configurables.\",\"Delivery\":\"Livraison\",\"RoomGrid bar only\":\"Barre RoomGrid uniquement\",\"Browser notifications only\":\"Notifications du navigateur uniquement\",\"RoomGrid bar and browser\":\"Barre et navigateur RoomGrid\",\"No display, history only\":\"Pas d'affichage, historique uniquement\",\"In-page position\":\"Position sur la page\",\"Free top-bar area\":\"Zone de bar supérieure gratuite\",\"Floating top center\":\"Centre supérieur flottant\",\"Floating top right\":\"Flottant en haut à droite\",\"Display style\":\"Style d'affichage\",\"Compact single line\":\"Ligne unique compacte\",\"Display duration\":\"Durée d'affichage\",\"Until dismissed\":\"Jusqu'à son licenciement\",\"Visible at once\":\"Visible immédiatement\",\"Test notification\":\"Notification de test\",\"General rules\":\"Règles générales\",\"Enable notifications\":\"Activer les notifications\",\"The browser did not grant notification permission.\":\"Le navigateur n'a pas accordé l'autorisation de notification.\",\"Neutral text\":\"Texte neutre\",\"Quiet time starts\":\"Le temps calme commence\",\"Quiet time ends\":\"Le temps calme se termine\",\"Cooldown (minutes)\":\"Temps de recharge (minutes)\",\"Hourly limit\":\"Limite horaire\",\"Favorite threshold\":\"Seuil favori\",\"Open on click\":\"Ouvrir en cliquant\",\"Per model\":\"Par modèle\",\"Model\":\"Modèle\",\"Library is empty.\":\"La bibliothèque est vide.\",\"Per group\":\"Par groupe\",\"Group\":\"Groupe\",\"No groups.\":\"Pas de groupes.\",\"Local notification list\":\"Liste de notifications locales\",\"Mark all read\":\"Marquer tout comme lu\",\"Clear list\":\"Effacer la liste\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"La vérification des antécédents lit uniquement l'état et ne démarre aucune vidéo. Un script utilisateur ne peut s'exécuter que lorsqu'un onglet de site associé est ouvert ; plusieurs onglets choisissent automatiquement un vérificateur.\",\"Shield status\":\"Statut du bouclier\",\"Ready\":\"Prêt\",\"Local PIN\":\"Code PIN local\",\"Set\":\"Ensemble\",\"Not set\":\"Non défini\",\"Quick shield\":\"Bouclier rapide\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Couvre immédiatement les noms et les images et met en pause/coupe tous les médias. La restauration renvoie l'état précédent du média.\",\"Activate now\":\"Activer maintenant\",\"Auto-shield after inactivity\":\"Bouclier automatique après inactivité\",\"Set or change local PIN\":\"Définir ou modifier le code PIN local\",\"Leave empty to remove\":\"Laisser vide pour supprimer\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"Le code PIN constitue uniquement une barrière d'accès locale, pas un cryptage, et ne remplace pas la protection du compte du système d'exploitation.\",\"History controls\":\"Contrôles de l'historique\",\"Usage statistics\":\"Statistiques d'utilisation\",\"Pause history 1 hour\":\"Suspendre l'historique 1 heure\",\"Clear statistics\":\"Des statistiques claires\",\"Clear all local statistics? A backup will be created first.\":\"Effacer toutes les statistiques locales ? Une sauvegarde sera créée en premier.\",\"Clear Discover settings\":\"Effacer les paramètres de découverte\",\"Clear local Discover filters and feeds?\":\"Effacer les filtres et flux Discover locaux ?\",\"Clear notification list\":\"Effacer la liste de notifications\",\"Data boundary\":\"Limite des données\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"La configuration, les statistiques de surveillance, l'historique des notifications et les préférences Discover sont stockés localement dans ce navigateur. RoomGrid ne contient aucune synchronisation cloud, téléchargement de compte ou télémétrie.\",\"Used\":\"Utilisé\",\"Sensitivity & consent center\":\"Centre de sensibilité et de consentement\",\"Standard configuration\":\"Configuration standard\",\"Theme, layout, filters\":\"Thème, mise en page, filtres\",\"Local\":\"Locale\",\"Model names, notes, timestamps, statistics\":\"Noms de modèles, notes, horodatages, statistiques\",\"Warn on export\":\"Avertir à l'exportation\",\"Secret\":\"Secrète\",\"Optional official statistics token\":\"Jeton de statistiques officielles facultatif\",\"Session-only or encrypted vault\":\"Coffre-fort de session uniquement ou chiffré\",\"Chat content\":\"Contenu du chat\",\"Never persisted\":\"N'a jamais persisté\",\"Ephemeral\":\"Éphémère\",\"Stream recording/screenshots\":\"Enregistrement de flux/captures d'écran\",\"Not provided\":\"Non fourni\",\"Policy guard\":\"Garde politique\",\"Performance profile\":\"Profil de performances\",\"Playing streams\":\"Lecture de flux\",\"Video elements\":\"Éléments vidéo\",\"Measured memory\":\"Mémoire mesurée\",\"Unavailable in browser\":\"Indisponible dans le navigateur\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Les mesures actuelles sont proches de la limite configurée. Mettez en pause les caméras inutilisées ou choisissez le mode Eco.\",\"Stream performance\":\"Performances du flux\",\"Eco\":\"Écologique\",\"Power\":\"Puissance\",\"Concurrent stream limit\":\"Limite de flux simultanés\",\"Adaptive quality\":\"Qualité adaptative\",\"Gradual startup\":\"Démarrage progressif\",\"Automatic performance mode\":\"Mode de performances automatique\",\"Memory threshold\":\"Seuil de mémoire\",\"Stream-count threshold\":\"Seuil de nombre de flux\",\"Current auto state\":\"État automatique actuel\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Les aperçus inutilisés sont arrêtés. La nouvelle vidéo reste en pause à la limite jusqu'à ce que la capacité soit disponible. Les comptes élevés commencent progressivement ; il n'y a pas de garantie « caméras illimitées ».\",\"Multi-window workspaces\":\"Espaces de travail multi-fenêtres\",\"Window name\":\"Nom de la fenêtre\",\"Active and recent windows\":\"Fenêtres actives et récentes\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"La bibliothèque de modèles est synchronisée via une fusion locale ; chaque fenêtre conserve son propre espace de travail et ses propres paramètres d'affichage. Les associations de fenêtres récentes peuvent être restaurées après le redémarrage lorsque le navigateur le permet.\",\"Optional tools are disabled\":\"Les outils facultatifs sont désactivés\",\"Media, app and contest directories will not scan the current page.\":\"Les répertoires de médias, d'applications et de concours n'analyseront pas la page actuelle.\",\"Enable optional tools\":\"Activer les outils facultatifs\",\"Local search\":\"Recherche locale\",\"Search saved items and current page\":\"Rechercher des éléments enregistrés et la page actuelle\",\"Rescan current page\":\"Réanalyser la page actuelle\",\"Disable optional tools\":\"Désactiver les outils facultatifs\",\"Clear local side data\":\"Effacer les données secondaires locales\",\"Clear app favorites, categories and media notes?\":\"Effacer les favoris, les catégories et les notes multimédias des applications ?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Ces modules organisent uniquement les liens publics et le texte déjà accessibles sur la page en cours. Ils ne copient pas les médias, ne les téléchargent pas automatiquement et n'exploitent pas de fonctions de chat, de jeton ou de paiement. La connexion, la vérification de l’âge, la sécurité et les formulaires de paiement restent intacts.\",\"Photo and video areas\":\"Espaces photo et vidéo\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Aucune zone médiatique publique reconnaissable n’est présente sur cette page. Effectuez une nouvelle numérisation sur une page photo ou vidéo native.\",\"Apps directory\":\"Répertoire des applications\",\"Local category\":\"Catégorie locale\",\"Found on page\":\"Trouvé sur la page\",\"No recognizable app links are present on this page.\":\"Aucun lien d'application reconnaissable n'est présent sur cette page.\",\"Public contest results\":\"Résultats du concours public\",\"No readable public contest table is present on this page.\":\"Aucun tableau de concours public lisible n'est présent sur cette page.\",\"Native account, help and broadcaster links\":\"Compte natif, aide et liens vers les diffuseurs\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Aucun lien natif sûr n'a été trouvé sur cette page ; les chemins de paiement et de connexion sont délibérément filtrés.\",\"Homepage filters reset\":\"Réinitialisation des filtres de la page d'accueil\",\"Global appearance reset\":\"Réinitialisation de l'apparence globale\",\"Health score\":\"Bilan de santé\",\"Page type\":\"Type de page\",\"Route generation\":\"Génération d'itinéraire\",\"Running modules\":\"Modules en cours d'exécution\",\"Local errors\":\"Erreurs locales\",\"Repair center\":\"Centre de réparation\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Une sauvegarde de configuration est créée avant chaque réparation. Les statistiques et les données de modèle ne sont pas supprimées par ces actions.\",\"Reset home filters\":\"Réinitialiser les filtres d'accueil\",\"Reset all home-page filters?\":\"Réinitialiser tous les filtres de la page d'accueil ?\",\"Reset global appearance\":\"Réinitialiser l'apparence globale\",\"Reset the global appearance? Saved design profiles remain.\":\"Réinitialiser l'apparence globale ? Les profils de conception enregistrés restent.\",\"Normalize configuration\":\"Normaliser la configuration\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Valider et réécrire la configuration actuelle ? Une sauvegarde est créée en premier.\",\"Close all previews\":\"Fermer tous les aperçus\",\"Compatibility self-check\":\"Auto-vérification de compatibilité\",\"Module and page status\":\"Statut des modules et des pages\",\"Page route test matrix\":\"Matrice de test de routage de page\",\"Current route\":\"Itinéraire actuel\",\"Trigger\":\"Déclencheur\",\"Recent page transitions\":\"Transitions de pages récentes\",\"Capability layer\":\"Couche de capacités\",\"Available\":\"Disponible\",\"Fallback\":\"Repli\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Lorsqu'une API est manquante, seul le module concerné revient. Des pannes de réseau répétées ouvrent temporairement un disjoncteur.\",\"Backup and recovery status\":\"État de sauvegarde et de récupération\",\"Primary data\":\"Données primaires\",\"Stable archive\":\"Archives stables\",\"Rolling backups\":\"Sauvegardes glissantes\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"L'autovérification vérifie uniquement la lisibilité et les capacités du navigateur ; il n'écrase jamais automatiquement les données actuelles. La récupération continue d'utiliser le flux de sauvegarde confirmé existant.\",\"Sanitized local error log\":\"Journal d'erreurs local nettoyé\",\"No RoomGrid errors recorded.\":\"Aucune erreur RoomGrid enregistrée.\",\"Run self-check again\":\"Exécutez à nouveau l'auto-vérification\",\"Export diagnostics\":\"Exporter les diagnostics\",\"Clear error log\":\"Effacer le journal des erreurs\",\"Return to normal site\":\"Retour au site normal\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"L'exportation de diagnostic ne contient aucun espace de travail, noms de modèles, notes ou statistiques. Les paramètres de requête d'URL et les valeurs secrètes possibles sont supprimés du texte d'erreur.\",\"Local history on\":\"Histoire locale sur\",\"History off\":\"Historique désactivé\",\"This settings page could not be loaded\":\"Cette page de paramètres n'a pas pu être chargée\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Vos données n'ont pas été modifiées. Essayer à nouveau; si le problème persiste, ouvrez Diagnostics.\",\"Try again\":\"Réessayez\",\"Open Diagnostics\":\"Ouvrir le diagnostic\",\"Public results\":\"Résultats publics\",\"Public contest links\":\"Liens du concours public\",\"Page content has not appeared yet\":\"Le contenu de la page n'est pas encore apparu\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid a vérifié à nouveau la page mais ne trouve toujours pas la zone de contenu de Chaturbate. La page est peut-être encore en cours de chargement ou le site a renvoyé une vue vide.\",\"Check again\":\"Vérifiez à nouveau\",\"Open RoomGrid Discover\":\"Ouvrir RoomGrid Découvrir\",\"Reload page\":\"Recharger la page\",\"Hide notice\":\"Masquer l'avis\",\"Open MultiCam\":\"Ouvrir MultiCam\",\"Open workstation in a new window\":\"Ouvrir le poste de travail dans une nouvelle fenêtre\",\"Open Discover\":\"Ouvrir Découvrir\",\"Statistics center\":\"Centre de statistiques\",\"Backup and import\":\"Sauvegarde et importation\",\"Page and design options\":\"Options de page et de conception\",\"Hide immediately\":\"Cacher immédiatement\",\"All tools\":\"Tous les outils\",\"Search & commands\":\"Recherche et commandes\",\"Page design\":\"Conception des pages\",\"Official collection\":\"Collection officielle\",\"Saved\":\"Enregistré\",\"Quick info\":\"Informations rapides\",\"Loading live frame\":\"Chargement du live frame\",\"Live still\":\"Vivre encore\",\"Not live\":\"Pas en direct\",\"Refresh\":\"Actualiser\",\"Search…\":\"Rechercher…\",\"Tag Explorer\":\"Explorateur de balises\",\"Refresh public catalog\":\"Actualiser le catalogue public\",\"Online\":\"En ligne\",\"Toggle favorite\":\"Changer de favori\",\"Save\":\"Enregistrer\",\"Remove favorite\":\"Supprimer le favori\",\"Removed from library\":\"Supprimé de la bibliothèque\",\"Saved to library\":\"Enregistré dans la bibliothèque\",\"Added to favorites\":\"Ajouté aux favoris\",\"Removed from favorites\":\"Supprimé des favoris\",\"Watching\":\"Regarder\",\"Watch\":\"Regarder\",\"Temporary watchlist\":\"Liste de surveillance temporaire\",\"Removed from watchlist\":\"Supprimé de la liste de surveillance\",\"Added to watchlist\":\"Ajouté à la liste de surveillance\",\"Choose group\":\"Choisir un groupe\",\"Add to group…\":\"Ajouter au groupe…\",\"Group updated\":\"Groupe mis à jour\",\"Site room\":\"Salle de chantier\",\"Select for bulk actions\":\"Sélectionner pour les actions groupées\",\"viewers\":\"téléspectateurs\",\"No public description\":\"Aucune description publique\",\"Open in Tag Explorer\":\"Ouvrir dans l'explorateur de balises\",\"Feed name\":\"Nom du flux\",\"View name\":\"Afficher le nom\",\"Name already exists\":\"Le nom existe déjà\",\"Saved-item limit reached\":\"Limite d'éléments enregistrés atteinte\",\"Feed saved\":\"Flux enregistré\",\"View saved\":\"Vue enregistrée\",\"Applied\":\"Appliqué\",\"Backup exported\":\"Sauvegarde exportée\",\"Backup imported\":\"Sauvegarde importée\",\"Invalid backup\":\"Sauvegarde invalide\",\"New models\":\"Nouveaux modèles\",\"New since last visit\":\"Nouveau depuis la dernière visite\",\"New to me\":\"Nouveau pour moi\",\"Returning\":\"De retour\",\"Stable online\":\"Stable en ligne\",\"Saved only\":\"Enregistré uniquement\",\"Previously watched\":\"Déjà regardé\",\"Local collection\":\"Collecte locale\",\"Hide visited today\":\"Masquer visité aujourd'hui\",\"No feed selected\":\"Aucun flux sélectionné\",\"No view selected\":\"Aucune vue sélectionnée\",\"Saved feeds\":\"Flux enregistrés\",\"Saved views\":\"Vues enregistrées\",\"Save new\":\"Enregistrer nouveau\",\"All public rooms\":\"Toutes les salles publiques\",\"Known HD\":\"HD connue\",\"Random result\":\"Résultat aléatoire\",\"Viewers ↓\":\"Téléspectateurs ↓\",\"Viewers ↑\":\"Téléspectateurs ↑\",\"Online duration ↓\":\"Durée en ligne ↓\",\"Followers ↓\":\"Abonnés ↓\",\"Name A–Z\":\"Nom A à Z\",\"Name Z–A\":\"Nom Z-A\",\"New first\":\"Nouveau premier\",\"Watch time ↓\":\"Durée de visionnage ↓\",\"Sessions ↓\":\"Séances ↓\",\"Last watched\":\"Vu pour la dernière fois\",\"Name, description, tag, place\":\"Nom, description, tag, lieu\",\"Secondary sort\":\"Tri secondaire\",\"Shuffle again\":\"Mélangez à nouveau\",\"All groups\":\"Tous les groupes\",\"All tags (AND)\":\"Toutes les balises (ET)\",\"Any tag (OR)\":\"N'importe quelle balise (OR)\",\"Excluded tags (NOT)\":\"Balises exclues (PAS)\",\"Language, region, or country\":\"Langue, région ou pays\",\"Minimum online minutes\":\"Minutes minimales en ligne\",\"Maximum online minutes\":\"Minutes en ligne maximales\",\"Minimum local watch minutes\":\"Minutes minimales de surveillance locale\",\"Advanced filters and Boolean tags\":\"Filtres avancés et balises booléennes\",\"Saved views and feeds\":\"Vues et flux enregistrés\",\"Import\":\"Importer\",\"Recent searches\":\"Recherches récentes\",\"Live previews and performance\":\"Aperçus et performances en direct\",\"Muted live preview for every visible card\":\"Aperçu en direct en sourdine pour chaque carte visible\",\"Refresh rate\":\"Taux de rafraîchissement\",\"Real-time video while hovering\":\"Vidéo en temps réel en survol\",\"Hover delay\":\"Délai de survol\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Les cartes visibles utilisent des cadres JPEG. En survol, une seule carte passe en vidéo en temps réel coupée et s'arrête complètement lorsque le pointeur s'éloigne. Des fréquences d'images élevées augmentent considérablement la bande passante et l'utilisation du processeur.\",\"saved\":\"enregistré\",\"previews\":\"aperçus\",\"Previews off\":\"Aperçus désactivés\",\"No more feeds can be saved\":\"Aucun autre flux ne peut être enregistré\",\"Tag feed saved\":\"Flux de tags enregistré\",\"Select tag\":\"Sélectionner une balise\",\"Tag favorite\":\"Tag favori\",\"Block tag\":\"Balise de bloc\",\"Select tags first\":\"Sélectionnez d'abord les balises\",\"Tag-group name\":\"Nom du groupe de balises\",\"Tag group saved\":\"Groupe de balises enregistré\",\"No gaming rooms in the current catalog\":\"Aucune salle de jeux dans le catalogue actuel\",\"All (AND)\":\"Tous (ET)\",\"Any (OR)\":\"N'importe lequel (OU)\",\"Exclude (NOT)\":\"Exclure (PAS)\",\"Search tags\":\"Rechercher des balises\",\"Tag search\":\"Recherche de balises\",\"Combination\":\"Combinaison\",\"Model sorting\":\"Tri des modèles\",\"Show in Discover\":\"Afficher dans Découvrir\",\"Save tag group\":\"Enregistrer le groupe de balises\",\"Save tag feed\":\"Enregistrer le flux de balises\",\"No tag groups saved yet.\":\"Aucun groupe de balises n'a encore été enregistré.\",\"Unknown region\":\"Région inconnue\",\"Unknown age\":\"Âge inconnu\",\"Public “New”\":\"Public « Nouveau »\",\"My new cams\":\"Mes nouvelles caméras\",\"New-cams feed saved\":\"Flux de nouvelles caméras enregistré\",\"Save “My new cams” feed\":\"Enregistrer le flux « Mes nouvelles webcams »\",\"Create gaming workspace\":\"Créer un espace de travail de jeu\",\"No matches for this combination.\":\"Aucune correspondance pour cette combinaison.\",\"Select tags to preview matching rooms here.\":\"Sélectionnez les balises pour prévisualiser les pièces correspondantes ici.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Recherchez, ajoutez des favoris, bloquez et combinez des balises publiques. Les tendances utilisent uniquement les modifications de catalogue observées sur cet appareil.\",\"Current tags\":\"Balises actuelles\",\"Counts show current matches and change since the prior fetch.\":\"Les décomptes affichent les correspondances en cours et les changements depuis la récupération précédente.\",\"Results for selected tags\":\"Résultats pour les balises sélectionnées\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Utilise le même tri et la même navigation SingleCam que Discover.\",\"Categories, age, and regions\":\"Catégories, âge et régions\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Seuls les champs publics explicites sont affichés. Les régions séparées par des virgules se combinent avec OR ; les valeurs inconnues restent visibles. Aucun fuseau horaire n'est deviné.\",\"New and gaming\":\"Nouveautés et jeux\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Local premièrement vu, le drapeau public « Nouveau », la liste de surveillance temporaire et les thèmes de jeu restent séparés.\",\"Tag groups\":\"Groupes de balises\",\"Save AND, OR, or NOT combinations.\":\"Enregistrez les combinaisons AND, OR ou NOT.\",\"Favorite tags\":\"Balises favorites\",\"Your personal shortcut list.\":\"Votre liste de raccourcis personnelle.\",\"No favorites yet\":\"Pas encore de favoris\",\"Related tags\":\"Balises associées\",\"Tags co-occurring on the currently matching models.\":\"Balises co-occupantes sur les modèles actuellement correspondants.\",\"Personal tag history\":\"Historique des tags personnels\",\"Recently and frequently used on this device.\":\"Récemment et fréquemment utilisé sur cet appareil.\",\"Bulk target group\":\"Groupe cible groupé\",\"Selected models added to group\":\"Modèles sélectionnés ajoutés au groupe\",\"New workspace name\":\"Nom du nouvel espace de travail\",\"Created from a Discover selection\":\"Créé à partir d'une sélection Découvrir\",\"Workspace created\":\"Espace de travail créé\",\"Save all\":\"Enregistrer tout\",\"Create workspace\":\"Créer un espace de travail\",\"Loading public catalog…\":\"Chargement du catalogue public…\",\"Loading the complete catalog in the background…\":\"Chargement du catalogue complet en arrière-plan…\",\"Updated\":\"Mis à jour\",\"Public rooms\":\"Salles publiques\",\"The catalog is loading.\":\"Le catalogue est en cours de chargement.\",\"No matching rooms. Adjust search or filters.\":\"Aucune chambre correspondante. Ajustez la recherche ou les filtres.\",\"Previous\":\"Précédent\",\"Next\":\"Suivant\",\"Move up\":\"Monter\",\"Move down\":\"Descendre\",\"Number shown\":\"Numéro affiché\",\"Widget width\":\"Largeur du widget\",\"Full\":\"Plein\",\"Nothing here yet.\":\"Rien ici encore.\",\"Newly online\":\"Nouvellement en ligne\",\"Recently visited\":\"Récemment visité\",\"Frequently visited\":\"Fréquemment visité\",\"Homepage appearance, pagination, and performance\":\"Apparence, pagination et performances de la page d'accueil\",\"Information density\":\"Densité de l'information\",\"Layout\":\"Disposition\",\"Data rich\":\"Riche en données\",\"Static\":\"Statique\",\"Show statistics summary\":\"Afficher le résumé des statistiques\",\"Show personal widgets\":\"Afficher les widgets personnels\",\"Sections below the live catalog\":\"Sections sous le catalogue en direct\",\"Homepage live models\":\"Modèles vivants de la page d'accueil\",\"All live rooms in one place\":\"Toutes les salles de concert en un seul endroit\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Utilisez les mêmes filtres complets de recherche, de tri, de groupes, de balises et d’historique local que Discover.\",\"Refresh live catalog\":\"Actualiser le catalogue en direct\",\"Homepage settings\":\"Paramètres de la page d'accueil\",\"Matches a favorite gaming theme\":\"Correspond à un thème de jeu préféré\",\"Saved models\":\"Modèles enregistrés\",\"Online favorites\":\"Favoris en ligne\",\"Local watch time\":\"Heure de visionnage locale\",\"Continue last MultiCam workspace\":\"Continuer le dernier espace de travail MultiCam\",\"Open full Discover page\":\"Ouvrir la page de découverte complète\",\"Online notifications\":\"Notifications en ligne\",\"on\":\"sur\",\"off\":\"éteint\",\"Offline notifications\":\"Notifications hors ligne\",\"Feed\":\"Nourrir\",\"Recent\":\"Récent\",\"Frequent\":\"Fréquent\",\"Saved filter views\":\"Vues filtrées enregistrées\",\"Favorites currently public and online.\":\"Favoris actuellement publics et en ligne.\",\"New models or public streams that started recently.\":\"Nouveaux modèles ou flux publics qui ont commencé récemment.\",\"Current results from the selected feed.\":\"Résultats actuels du flux sélectionné.\",\"Suggestions with reasons\":\"Suggestions avec raisons\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Basé uniquement sur des balises publiques partagées et des thèmes privilégiés localement ; aucune déduction d’identité.\",\"Sorted by local last-visit time.\":\"Triés par heure locale de la dernière visite.\",\"Sorted by local watch time.\":\"Triés par heure de visionnage locale.\",\"Saved models never visited\":\"Modèles enregistrés jamais visités\",\"Saved in the library without local watch time.\":\"Enregistré dans la bibliothèque sans heure de visionnage locale.\",\"Draggable dashboard widgets\":\"Widgets de tableau de bord déplaçables\",\"Your RoomGrid homepage\":\"Votre page d'accueil RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Un catalogue en direct rapide et personnalisable avec SingleCam, MultiCam, des filtres complets, des vues enregistrées et vos widgets personnels.\",\"Audio activity\":\"Activité audio\",\"Auto quality\":\"Qualité automatique\",\"Stream interrupted; status checks continue.\":\"Flux interrompu ; les contrôles de statut se poursuivent.\",\"This browser cannot play HLS.\":\"Ce navigateur ne peut pas lire HLS.\",\"Connecting to live stream…\":\"Connexion à la diffusion en direct…\",\"The room is not public right now.\":\"La salle n'est pas publique pour le moment.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Actuellement hors ligne. Les alternatives du même filtre sont présentées ci-dessous.\",\"Status temporarily unavailable; retrying automatically.\":\"Statut temporairement indisponible ; réessayer automatiquement.\",\"Loading public profile…\":\"Chargement du profil public…\",\"Profile temporarily unavailable.\":\"Profil temporairement indisponible.\",\"No cached profile.\":\"Aucun profil en cache.\",\"Public profile changed\":\"Profil public modifié\",\"No public bio.\":\"Aucune biographie publique.\",\"Observed\":\"Observé\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Seuls les liens auto-publiés sur le profil public sont affichés. Un nom d’utilisateur correspondant ne constitue pas à lui seul une preuve d’identité.\",\"Profile information\":\"Informations sur le profil\",\"Preparing…\":\"Préparation…\",\"Play\":\"Jouer\",\"Unmute\":\"Activer le son\",\"Mute\":\"Muet\",\"Contain\":\"Contenir\",\"Cover\":\"Couverture\",\"Brightness\":\"Luminosité\",\"Contrast\":\"Contraste\",\"Hue\":\"Teinte\",\"Sepia\":\"Sépia\",\"Grayscale\":\"Niveaux de gris\",\"Invert\":\"Inverser\",\"Blur\":\"Flou\",\"Image zoom\":\"Zoom sur les images\",\"Recording saved\":\"Enregistrement sauvegardé\",\"Screenshot saved\":\"Capture d'écran enregistrée\",\"Recording\":\"Enregistrement\",\"Mirror\":\"Miroir\",\"Flip vertically\":\"Retourner verticalement\",\"Rotate\":\"Rotation\",\"Show edited\":\"Afficher modifié\",\"Show original\":\"Afficher l'original\",\"Reset image\":\"Réinitialiser l'image\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Maintenez Ctrl et faites défiler pour zoomer ; faites glisser l'image lors du zoom. Double-cliquez pour réinitialiser.\",\"Zoom out\":\"Zoom arrière\",\"Zoom in\":\"Zoomer\",\"Reset zoom and position\":\"Réinitialiser le zoom et la position\",\"Player window\":\"Fenêtre du lecteur\",\"Fullscreen\":\"Plein écran\",\"Private notes, memories, or things to check next time (stored locally)\":\"Notes privées, souvenirs ou éléments à vérifier la prochaine fois (stockés localement)\",\"Autosave is on\":\"La sauvegarde automatique est activée\",\"Enter your own tag…\":\"Entrez votre propre balise…\",\"Saved at\":\"Enregistré à\",\"Local details saved\":\"Détails locaux enregistrés\",\"Remove tag\":\"Supprimer la balise\",\"No personal tags yet.\":\"Pas encore de tags personnels.\",\"Saving…\":\"Sauvegarde…\",\"Create your first group under Groups.\":\"Créez votre premier groupe sous Groupes.\",\"Added to MultiCam\":\"Ajouté à MultiCam\",\"Edit private details\":\"Modifier les détails privés\",\"Region\":\"Région\",\"Current online time\":\"Heure de connexion actuelle\",\"Stream attributes\":\"Attributs du flux\",\"New\":\"Nouveau\",\"Yes\":\"Oui\",\"No / unknown\":\"Non / inconnu\",\"Last observed\":\"Dernière observation\",\"Local visits\":\"Visites locales\",\"First watched\":\"Regardé pour la première fois\",\"No public description.\":\"Aucune description publique.\",\"No public tags available.\":\"Aucune balise publique disponible.\",\"Quick-add your own tag…\":\"Ajoutez rapidement votre propre balise…\",\"Public tags\":\"Balises publiques\",\"Add\":\"Ajouter\",\"Private details\":\"Détails privés\",\"Everything stays in this browser and survives updates.\":\"Tout reste dans ce navigateur et survit aux mises à jour.\",\"Private notebook\":\"Carnet privé\",\"Keep on watchlist\":\"Rester sur la liste de surveillance\",\"Group assignment (multiple)\":\"Travail de groupe (multiple)\",\"Save now\":\"Économisez maintenant\",\"Chat loads only when this section is opened.\":\"Le chat se charge uniquement lorsque cette section est ouverte.\",\"Official chat\":\"Chat officiel\",\"Official chat connected.\":\"Chat officiel connecté.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Chat chargé ; la structure actuelle des pages n'a pas pu être entièrement isolée.\",\"Chat loaded.\":\"Chat chargé.\",\"Loading official chat…\":\"Chargement du chat officiel…\",\"Official chat room\":\"Salon de discussion officiel\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Chargé uniquement en cas de besoin ; RoomGrid ne stocke ni n'envoie automatiquement de messages de discussion.\",\"Reload chat\":\"Recharger le chat\",\"Open official room in a new window\":\"Ouvrir la salle officielle dans une nouvelle fenêtre\",\"Hide chat\":\"Masquer la discussion\",\"Live observations\":\"Observations en direct\",\"Trend confidence\":\"Confiance dans les tendances\",\"Known time slots\":\"Plages horaires connues\",\"Typical peak\":\"Pic typique\",\"Watched in 7 days\":\"Regardé en 7 jours\",\"Watched in 30 days\":\"Regardé dans 30 jours\",\"Current streak\":\"Série actuelle\",\"Typical weekly online times\":\"Horaires de connexion hebdomadaires typiques\",\"Online times will be learned on this device from now on.\":\"Les horaires en ligne seront désormais appris sur cet appareil.\",\"Based on observations on this device; missing information is never guessed.\":\"Basé sur des observations sur cet appareil ; les informations manquantes ne sont jamais devinées.\",\"Activity by weekday\":\"Activité par jour de la semaine\",\"Most likely online times\":\"Horaires de connexion les plus probables\",\"Online-time map\":\"Carte horaire en ligne\",\"Public covers and locally saved example images.\":\"Couvertures publiques et exemples d’images enregistrées localement.\",\"Import quality\":\"Qualité d'importation\",\"Original file\":\"Fichier original\",\"Very high (up to 4K)\":\"Très élevé (jusqu'à 4K)\",\"High (up to 1440p)\":\"Élevé (jusqu'à 1440p)\",\"Space-saving (up to 900p)\":\"Gain de place (jusqu'à 900p)\",\"Add images from computer\":\"Ajouter des images depuis un ordinateur\",\"Use website cover\":\"Utiliser la couverture du site Web\",\"Use first image as cover\":\"Utiliser la première image comme couverture\",\"Covers and local gallery\":\"Couvertures et galerie locale\",\"Updating images…\":\"Mise à jour des images…\",\"Loading images…\":\"Chargement des images…\",\"Own image\":\"Propre image\",\"Example image\":\"Exemple d'image\",\"Current cover\":\"Couverture actuelle\",\"Alternative preview\":\"Aperçu alternatif\",\"Current public image\":\"Image publique actuelle\",\"Saving cover…\":\"Sauvegarde de la couverture…\",\"Set as cover\":\"Définir comme couverture\",\"Delete this image from the local gallery?\":\"Supprimer cette image de la galerie locale ?\",\"No images yet.\":\"Aucune image pour l'instant.\",\"saved locally\":\"enregistré localement\",\"own images\":\"propres images\",\"Update public previews\":\"Mettre à jour les aperçus publics\",\"Processing your images…\":\"Traitement de vos images…\",\"ALL PAGE SOUNDS OFF\":\"TOUTES LES PAGES SONT DÉSACTIVÉES\",\"PAGE SOUNDS ALLOWED\":\"SONS DE PAGE AUTORISÉS\",\"Chat/token sounds OFF\":\"Le chat/le jeton sonne désactivé\",\"Chat/token sounds ON\":\"Le chat/le jeton sonne activé\",\"Reconnecting the player with the new settings…\":\"Reconnexion du lecteur avec les nouveaux paramètres…\",\"Turn off chat, token and tip sounds completely\":\"Désactivez complètement les sons du chat, des jetons et des pourboires\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Bloque les sons de page des éléments audio et Web Audio. Le flux audio reste contrôlable séparément via « Son activé ».\",\"No alternatives available.\":\"Aucune alternative disponible.\",\"Similar models and offline alternatives\":\"Modèles similaires et alternatives hors ligne\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Priorisé par les balises partagées ; aucune inférence d’identité ou de compte externe.\",\"Chat\":\"Discuter\",\"Notes & organization\":\"Remarques et organisation\",\"SingleCam sections\":\"Sections SingleCam\",\"Back to original results\":\"Retour aux résultats originaux\",\"Private workspace\":\"Espace de travail privé\",\"Chat on/off\":\"Chat activé/désactivé\",\"Hide similar models\":\"Masquer les modèles similaires\",\"Show similar models\":\"Afficher des modèles similaires\",\"RoomGrid Home\":\"Accueil RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Catalogue public indisponible ; montrant la bibliothèque locale.\",\"Drag to resize sidebar\":\"Faites glisser pour redimensionner la barre latérale\",\"The workspace could not be saved safely. Export a backup now.\":\"L'espace de travail n'a pas pu être enregistré en toute sécurité. Exportez une sauvegarde maintenant.\",\"Open menu or restore controls\":\"Ouvrir le menu ou restaurer les commandes\",\"Controls restored\":\"Contrôles restaurés\",\"Menu\":\"Menus\",\"Show toolbar\":\"Afficher la barre d'outils\",\"Tools\":\"Outils\",\"Show sidebar\":\"Afficher la barre latérale\",\"Restore controls\":\"Restaurer les contrôles\",\"Show toolbar, groups, and menus\":\"Afficher la barre d'outils, les groupes et les menus\",\"Open more tools\":\"Ouvrir plus d'outils\",\"all watch states\":\"tous les états de surveillance\",\"Watch status\":\"Statut de la montre\",\"0 means no viewer limit.\":\"0 signifie aucune limite de spectateurs.\",\"Import temporary URLs; they disappear after refresh\":\"Importer des URL temporaires ; ils disparaissent après actualisation\",\"Filter current group\":\"Filtrer le groupe actuel\",\"All status\":\"Tous les statuts\",\"Hide offline/private\":\"Masquer hors ligne/privé\",\"Collapse toolbar\":\"Réduire la barre d'outils\",\"Windows per page\":\"Fenêtres par page\",\"Custom grid\":\"Grille personnalisée\",\"Choose MultiCam audio routing\":\"Choisissez le routage audio MultiCam\",\"Audio: manual\":\"Audio : manuel\",\"Audio: solo cam\":\"Audio : caméra solo\",\"Audio: follow focus\":\"Audio : suivez la mise au point\",\"Auto\":\"Automatique\",\"Userscript version\":\"Version du script utilisateur\",\"Open Discover and dashboard\":\"Ouvrir la découverte et le tableau de bord\",\"Global search and quick commands · Ctrl+K\":\"Recherche globale et commandes rapides · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"État du flux, budget qualité, audio, scènes et remplacements\",\"Control\":\"Contrôle\",\"Rooms\":\"Salons\",\"Filter\":\"Filtrer\",\"Playback\":\"Lecture\",\"Stabilizing live preview…\":\"Stabilisation de l'aperçu en direct…\",\"Recovering live preview…\":\"Récupération de l'aperçu en direct…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"L'aperçu en direct est brièvement interrompu pour éviter une boucle de rechargement. Réessayez sous peu.\",\"Collapse groups\":\"Réduire les groupes\",\"Expand/collapse users\":\"Développer/réduire les utilisateurs\",\"Output volume; no live signal level is available for this stream\":\"Volume de sortie ; aucun niveau de signal en direct n'est disponible pour ce flux\",\"OFF\":\"DÉSACTIVÉ\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Collez les URL. Les pages de salle, m3u8, mp4/webm sont prises en charge. Les pages normales seront analysées au mieux.\",\"Actual resolution, connection and buffer\":\"Résolution réelle, connexion et tampon\",\"Drag to reorder / move group\":\"Faites glisser pour réorganiser/déplacer le groupe\",\"All time\":\"Tout le temps\",\"Click a model for details.\":\"Cliquez sur un modèle pour plus de détails.\",\"Most watched groups\":\"Groupes les plus regardés\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Les champs du compte tels que la date d'adhésion ne sont affichés que lorsque Chaturbate les expose clairement sur la même page. Le script n'invente pas de valeurs.\",\"Back\":\"Retour\",\"Statistics import failed: \":\"Échec de l'importation des statistiques :\",\"Recent saves\":\"Sauvegardes récentes\",\"Rooms per page\":\"Pièces par page\",\"Custom rows\":\"Lignes personnalisées\",\"Custom columns\":\"Colonnes personnalisées\",\"Main width\":\"Largeur principale\",\"Main height\":\"Hauteur principale\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Les modules démarrent uniquement sur les types de pages correspondants. L'enregistrement recharge la page actuelle.\",\"Tools on normal pages\":\"Outils sur les pages normales\",\"True audio-level meters\":\"Véritables indicateurs de niveau audio\",\"MultiCam control center\":\"Centre de contrôle MultiCam\",\"No active streams yet\":\"Aucun flux actif pour le moment\",\"Stream health\":\"Santé du flux\",\"Budget off\":\"Budget réduit\",\"Economy · 2160p total\":\"Économie · 2160p total\",\"Balanced · 4320p total\":\"Équilibré · 4320p total\",\"High · 6480p total\":\"Élevé · 6480p total\",\"Ultra · 8640p total\":\"Ultra · 8640p au total\",\"Quality budget is off; global and per-camera limits still apply.\":\"Le budget qualité est épuisé ; les limites globales et par caméra s'appliquent toujours.\",\"Quality budget\":\"Budget de qualité\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Distribue la qualité entre les caméras actives. Une qualité explicite par caméra gagne toujours.\",\"Give the focused cam double weight\":\"Donnez à la caméra focalisée un double poids\",\"Manual per camera\":\"Manuel par caméra\",\"Solo: one camera only\":\"Solo : une seule caméra\",\"Follow focused camera\":\"Suivez la caméra focalisée\",\"Audio management\":\"Gestion audio\",\"Routing never overwrites saved mute states or individual volume.\":\"Le routage n’écrase jamais les états muets enregistrés ou les volumes individuels.\",\"Mute all\":\"Tout désactiver\",\"Restore saved audio\":\"Restaurer l'audio enregistré\",\"Scene name\":\"Nom de la scène\",\"Focus\":\"Concentrez-vous\",\"Grid\":\"Grille\",\"cams\":\"cames\",\"Load\":\"Charger\",\"No scenes saved for this workspace yet.\":\"Aucune scène enregistrée pour cet espace de travail pour le moment.\",\"Scenes\":\"Scènes\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Enregistrez les emplacements, la disposition et le routage audio actuels pour une restauration en un clic.\",\"Save current view as scene\":\"Enregistrer la vue actuelle en tant que scène\",\"All saved online models\":\"Tous les modèles en ligne enregistrés\",\"Replacement models\":\"Modèles de remplacement\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Les modèles préférés sont essayés dans l’ordre de la liste ; ce n'est qu'alors que les modèles en ligne du groupe sélectionné sont utilisés. Les devoirs manuels gagnent toujours.\",\"Replace offline slots automatically\":\"Remplacez automatiquement les emplacements hors ligne\",\"Delay (seconds)\":\"Délai (secondes)\",\"Candidate group\":\"Groupe de candidats\",\"Preferred replacements (multiple)\":\"Remplacements préférés (plusieurs)\",\"Replacement rule saved\":\"Règle de remplacement enregistrée\",\"Save replacement rule\":\"Enregistrer la règle de remplacement\",\"Resolve duplicate shortcuts first:\":\"Résolvez d'abord les raccourcis en double :\",\"Name, note or own tag…\":\"Nom, note ou propre tag…\",\"Camera menu\":\"Menu de l'appareil photo\",\"Camera menu · quality and sound\":\"Menu de l'appareil photo · qualité et son\",\"Close menu\":\"Fermer le menu\",\"View and window\":\"Vue et fenêtre\",\"Quality and image\":\"Qualité et image\",\"More actions\":\"Plus de propositions\",\"Currently main\":\"Actuellement principal\",\"Set as main\":\"Définir comme principal\",\"Pin live preview\":\"Épingler l'aperçu en direct\",\"Volume multiplier for this cam\":\"Multiplicateur de volume pour cette caméra\",\"Reset individual volume to 100%\":\"Réinitialiser le volume individuel à 100 %\",\"Individual volume\":\"Volume individuel\",\"Listen to this cam only\":\"Écoutez uniquement cette caméra\",\"Set quality for this camera only\":\"Définir la qualité pour cette caméra uniquement\",\"Automatic (global/adaptive)\":\"Automatique (global/adaptatif)\",\" · available\":\"· disponible\",\"Quality for this camera\":\"Qualité pour cet appareil photo\",\"Hold for original\":\"Conserver pour l'original\",\"Vivid\":\"Vif\",\"Image adjustments\":\"Ajustements des images\",\"More tools\":\"Plus d'outils\",\"Show top controls\":\"Afficher les principaux contrôles\",\"Collapse top controls\":\"Réduire les commandes principales\",\"Show groups\":\"Afficher les groupes\",\"Windows\":\"Fenêtres\",\"Open workspace in new window\":\"Ouvrir l'espace de travail dans une nouvelle fenêtre\",\"Control center\":\"Centre de contrôle\",\"Choose the data to export\":\"Choisissez les données à exporter\",\"Preview the file and conflicts before applying it\":\"Prévisualisez le fichier et les conflits avant de l'appliquer\",\"Help\":\"Aide\",\"Changes in this version\":\"Changements dans cette version\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Espaces de travail, bibliothèque de modèles, importations sécurisées, conception globale, pages normales étendues, découverte/tableau de bord étendu et contrôles de qualité/image du lecteur.\",\"Muted\":\"En sourdine\",\"Performance limit reached\":\"Limite de performance atteinte\",\"Drag to resize main width\":\"Faites glisser pour redimensionner la largeur principale\",\"Drag to resize main height\":\"Faites glisser pour redimensionner la hauteur principale\",\"Stable\":\"Écurie\",\"Unstable\":\"Instable\",\"Refresh all cameras\":\"Actualiser toutes les caméras\",\"Reconnect current models\":\"Reconnecter les modèles actuels\",\"Switch to grid view\":\"Passer à la vue grille\",\"Switch to focus view\":\"Passer à la vue ciblée\",\"Manage workspaces\":\"Gérer les espaces de travail\",\"Create, duplicate, archive and device layouts\":\"Créer, dupliquer, archiver et mettre en page des appareils\",\"Open model library\":\"Ouvrir la bibliothèque de modèles\",\"Notes, tags, ratings and bulk actions\":\"Notes, balises, évaluations et actions groupées\",\"Design options\":\"Options de conception\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Station de travail multi-vues tout-en-un pour les salles, l'enregistrement, les alertes, les captures d'écran, les reconnexions et les outils de données.\",\"Username ↵\":\"Nom d'utilisateur ↵\",\"Search rooms\":\"Rechercher des salles\",\"Invalid username\":\"Nom d'utilisateur invalide\",\"Hide offline\":\"Masquer hors ligne\",\"Hide private\":\"Masquer le privé\",\"Manual\":\"Manuel\",\"By status\":\"Par statut\",\"By name\":\"Par nom\",\"By added time\":\"Par temps ajouté\",\"By watch time\":\"Par heure de visionnage\",\"By sessions\":\"Par séances\",\"By last watched\":\"Par dernière visionnage\",\"By first watched\":\"En regardant pour la première fois\",\"By viewer count\":\"Par nombre de spectateurs\",\"By last online\":\"Dernière mise en ligne\",\"Advanced filters\":\"Filtres avancés\",\"Watched before\":\"Regardé avant\",\"Currently in view\":\"Actuellement en vue\",\"Include tag\":\"Inclure la balise\",\"Exclude tag\":\"Exclure la balise\",\"Minimum watch time (minutes)\":\"Durée minimale de visionnage (minutes)\",\"Watched within days\":\"Regardé en quelques jours\",\"Refresh all\":\"Actualiser tout\",\"Online alerts\":\"Alertes en ligne\",\"Offline alerts\":\"Alertes hors ligne\",\"Desktop notification + card flash when a model goes online\":\"Notification sur le bureau + flash de la carte lorsqu'un modèle se connecte\",\"Desktop notification + card flash when a model goes offline\":\"Notification sur le bureau + flash de la carte lorsqu'un modèle se déconnecte\",\"Dark mode\":\"Mode sombre\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Utilisez un thème d'interface sombre. Le flux vidéo et audio reste inchangé.\",\"Dark mode enabled\":\"Mode sombre activé\",\"Light mode enabled\":\"Mode lumière activé\",\"Mute token/tip sounds\":\"Couper les sons des jetons et pourboires\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Bloque uniquement les sons de notification des jetons et pourboires. Le son du direct reste activé.\",\"Token/tip sounds muted\":\"Sons des jetons et pourboires coupés\",\"Token/tip sounds enabled\":\"Sons des jetons et pourboires activés\",\"Clean\":\"Nettoyer\",\"Clean mode on\":\"Mode nettoyage activé\",\"Exit clean mode\":\"Quitter le mode nettoyage\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Masquez temporairement tous les contrôles et superpositions. Raccourci : Alt+P / Alt+C. Appuyez sur Échap pour quitter.\",\"Window first\":\"Fenêtre d'abord\",\"Window-first mode on\":\"Mode fenêtre d'abord activé\",\"Exit window-first\":\"Quitter la fenêtre en premier\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Masquez le shell de l'application et gardez les fenêtres de la pièce visibles. Raccourci : Alt+V.\",\"Show thumbnails\":\"Afficher les vignettes\",\"Hide thumbnails\":\"Masquer les vignettes\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Afficher ou masquer le rail des vignettes en mode Focus. Raccourci : Alt+T.\",\"Choose Focus rooms\":\"Choisissez les salles Focus\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Choisissez les pièces pour le mode Focus. La première pièce sélectionnée est l’écran principal ; utilisez ↑/↓ pour définir l’ordre des fenêtres secondaires.\",\"Main screen\":\"Écran principal\",\"Right window\":\"Fenêtre droite\",\"Bottom window\":\"Fenêtre inférieure\",\"Empty slot\":\"Emplacement vide\",\"Drag this model to a Focus slot\":\"Faites glisser ce modèle vers un emplacement Focus\",\"Choose model for this slot\":\"Choisissez le modèle pour cet emplacement\",\"Select a saved model for this exact Focus slot.\":\"Sélectionnez un modèle enregistré pour cet emplacement Focus exact.\",\"Choose model for this window\":\"Choisir le modèle pour cette fenêtre\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Sélectionnez un modèle enregistré pour cette fenêtre de grille exacte. Les modèles existants restent enregistrés.\",\"Remove from this grid window\":\"Supprimer de cette fenêtre de grille\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Seul cet emplacement est libéré. Le modèle enregistré reste dans votre espace de travail.\",\"Show in grid window\":\"Afficher dans la fenêtre de la grille\",\"Choose the exact active grid window for this model.\":\"Choisissez la fenêtre de grille active exacte pour ce modèle.\",\"Current\":\"Actuel\",\"Model removed from this grid window.\":\"Modèle supprimé de cette fenêtre de grille.\",\"No saved models in this group.\":\"Aucun modèle enregistré dans ce groupe.\",\"Secondary window\":\"Fenêtre secondaire\",\"Use current page automatically\":\"Utiliser automatiquement la page actuelle\",\"Clear custom selection\":\"Effacer la sélection personnalisée\",\"Apply Focus layout\":\"Appliquer la mise en page Focus\",\"No rooms are available in the current group and filter.\":\"Aucune salle n'est disponible dans le groupe et le filtre actuels.\",\"Fit: full image\":\"Ajustement : image complète\",\"Fit: fill window\":\"Ajuster : remplir la fenêtre\",\"Switch between full image and cropped fill.\":\"Basculez entre l’image complète et le remplissage recadré.\",\"Playback settings\":\"Paramètres de lecture\",\"Max stream quality\":\"Qualité de flux maximale\",\"Auto / no cap\":\"Auto / pas de plafond\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Commande + zoom à molette\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Zoomez sur une carte avec Ctrl/Commande + molette de la souris, faites glisser pendant le zoom, double-cliquez pour réinitialiser.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Quittez le mode nettoyage. Raccourci : Alt+P / Alt+C ou Esc.\",\"Clean · Alt+P/C / Esc\":\"Nettoyer · Alt+P/C / Échap\",\"(synced from another tab)\":\"(synchronisé depuis un autre onglet)\",\"Type a username and press Enter. Shortcut: /\":\"Tapez un nom d'utilisateur et appuyez sur Entrée. Raccourci : /\",\"Filter visible rooms by username\":\"Filtrer les salles visibles par nom d'utilisateur\",\"Show / hide group sidebar\":\"Afficher/masquer la barre latérale du groupe\",\"Global volume. Per-room mute is still respected.\":\"Volume global. La sourdine par pièce est toujours respectée.\",\"Base tile size in grid view\":\"Taille de la tuile de base en vue grille\",\"Sort rooms in the current group\":\"Trier les pièces du groupe actuel\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Hauteur de l'écran principal en mode Focus. Vous pouvez également faire glisser le séparateur.\",\"Main-screen aspect ratio in Focus mode\":\"Rapport hauteur/largeur de l'écran principal en mode Focus\",\"Thumbnail width in Focus mode\":\"Largeur des vignettes en mode Focus\",\"Show every saved room. The remove button deletes globally in this view.\":\"Afficher chaque pièce enregistrée. Le bouton Supprimer supprime globalement dans cette vue.\",\"Create an isolated group\":\"Créer un groupe isolé\",\"More tools and maintenance\":\"Plus d'outils et de maintenance\",\"GROUPS\":\"GROUPES\",\"All saved\":\"Tous sauvés\",\"Default\":\"Par défaut\",\"Online now\":\"En ligne maintenant\",\"New group name:\":\"Nouveau nom de groupe :\",\"Rename\":\"Renommer\",\"Rename to:\":\"Renommer en :\",\"Delete group\":\"Supprimer le groupe\",\"Live preview\":\"Aperçu en direct\",\"Loading preview…\":\"Chargement de l'aperçu…\",\"Private or away\":\"Privé ou à l'extérieur\",\"Preview could not be loaded\":\"L'aperçu n'a pas pu être chargé\",\"Toggle mute\":\"Activer le mode muet\",\"Resume\":\"CV\",\"Screenshot current frame\":\"Capture d'écran de l'image actuelle\",\"Record current card\":\"Enregistrer la carte actuelle\",\"Stop recording\":\"Arrêter l'enregistrement\",\"Picture-in-Picture\":\"Image dans l'image\",\"Remove from current group\":\"Supprimer du groupe actuel\",\"Delete saved room\":\"Supprimer la salle enregistrée\",\"Mirror image\":\"Image miroir\",\"Flip image\":\"Retourner l'image\",\"Rotate left\":\"Faire pivoter à gauche\",\"Rotate right\":\"Faire pivoter à droite\",\"Reset view\":\"Réinitialiser la vue\",\"Copy username\":\"Copier le nom d'utilisateur\",\"Add favorite\":\"Ajouter un favori\",\"Move to favorites only\":\"Passer aux favoris uniquement\",\"Copied\":\"Copié\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"La capture d'écran a échoué. Le navigateur/CORS peut bloquer l’affichage de ce flux.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"L'enregistrement est local et enregistre cette vidéo avec son audio correspondant lorsque le navigateur l'expose. Utilisez-le uniquement lorsque vous êtes autorisé à enregistrer ce contenu. Continuer?\",\"Recording started\":\"L'enregistrement a commencé\",\"Recording is not supported for this video/browser\":\"L'enregistrement n'est pas pris en charge pour cette vidéo/navigateur\",\"Recording segment saved\":\"Segment d'enregistrement enregistré\",\"Final recording segment saved\":\"Segment d'enregistrement final enregistré\",\"Recording paused; waiting for the stream to return\":\"Enregistrement en pause ; en attendant le retour du flux\",\"Recording resumed\":\"L'enregistrement a repris\",\"Recording paused; click to stop\":\"Enregistrement en pause ; cliquez pour arrêter\",\"No recording data to save\":\"Aucune donnée d'enregistrement à sauvegarder\",\"Recording settings saved\":\"Paramètres d'enregistrement enregistrés\",\"Segment length in minutes (1-180):\":\"Durée du segment en minutes (1-180) :\",\"Video bitrate in Mbps (0.5-20):\":\"Débit vidéo en Mbps (0,5-20) :\",\"Warn before leaving while recording\":\"Avertir avant de partir pendant l'enregistrement\",\"Recording is still active. Leave anyway?\":\"L'enregistrement est toujours actif. Partir quand même ?\",\"Recording center\":\"Centre d'enregistrement\",\"No active recordings\":\"Aucun enregistrement actif\",\"Waiting for source\":\"En attente de la source\",\"Saved segments\":\"Segments enregistrés\",\"Duration\":\"Durée\",\"Bitrate\":\"Débit\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Format : MP4 lorsqu'il est pris en charge par ce navigateur ; sinon, c'est la solution de secours la mieux prise en charge.\",\"Record current page\":\"Enregistrer la page actuelle\",\"Record current group\":\"Enregistrer le groupe actuel\",\"Record online rooms\":\"Enregistrez des salles en ligne\",\"Stop all recordings\":\"Arrêtez tous les enregistrements\",\"Show recording only\":\"Afficher uniquement l'enregistrement\",\"Show all rooms\":\"Afficher toutes les chambres\",\"Open current page rooms\":\"Ouvrir la page actuelle\",\"Move current page to group\":\"Déplacer la page actuelle vers le groupe\",\"Layout settings\":\"Paramètres de mise en page\",\"Recording settings\":\"Paramètres d'enregistrement\",\"Save settings\":\"Enregistrer les paramètres\",\"Config backups\":\"Sauvegardes de configuration\",\"No config backups found\":\"Aucune sauvegarde de configuration trouvée\",\"Online/offline history\":\"Historique en ligne/hors ligne\",\"Group rules\":\"Règles de groupe\",\"Keep favorites first\":\"Gardez d'abord les favoris\",\"Temporary URL manager\":\"Gestionnaire d'URL temporaire\",\"Save current temporary URLs\":\"Enregistrer les URL temporaires actuelles\",\"No temporary URLs\":\"Aucune URL temporaire\",\"Share workspace\":\"Partager l'espace de travail\",\"Share current rooms and layout\":\"Partager les pièces et la disposition actuelles\",\"Copy share link\":\"Copier le lien de partage\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Ce lien restaure les paramètres des pièces, des groupes et de la disposition dans un autre onglet du poste de travail.\",\"Shared workspace imported\":\"Espace de travail partagé importé\",\"Shared workspace link is invalid\":\"Le lien de l'espace de travail partagé n'est pas valide\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Cliquez sur un champ et appuyez sur le nouveau raccourci. Retour arrière efface ce champ.\",\"Reset shortcuts\":\"Réinitialiser les raccourcis\",\"Focus add box\":\"Focus sur la zone d'ajout\",\"Grid view\":\"Vue grille\",\"Focus view\":\"Vue de mise au point\",\"Clean mode\":\"Mode nettoyage\",\"Window-first mode\":\"Mode fenêtre d'abord\",\"Toggle thumbnails\":\"Basculer les vignettes\",\"Auto-detecting…\":\"Détection automatique…\",\"No rooms in this group\":\"Aucune chambre dans ce groupe\",\"Add a username at the top, switch group, or clear filters\":\"Ajoutez un nom d'utilisateur en haut, changez de groupe ou supprimez les filtres\",\"Open workstation\":\"Poste de travail ouvert\",\"Open here (replace page)\":\"Ouvrir ici (remplacer la page)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Remplacer cette page par le poste de travail ? Vous pouvez revenir en arrière via le navigateur.\",\"View saved list\":\"Afficher la liste enregistrée\",\"Collapse dock\":\"Réduire le quai\",\"No room detected\":\"Aucune pièce détectée\",\"Open here\":\"Ouvrir ici\",\"Add room\":\"Ajouter une pièce\",\"Remove room\":\"Supprimer la pièce\",\"Record in workstation\":\"Enregistrer sur le poste de travail\",\"Screenshot video\":\"Vidéo de capture d'écran\",\"Play / pause\":\"Lecture/pause\",\"Mute / unmute\":\"Désactiver/Réactiver le son\",\"No playable video found on this page\":\"Aucune vidéo jouable trouvée sur cette page\",\"Recording intent queued in workstation\":\"Intention d'enregistrement mise en file d'attente sur le poste de travail\",\"Added\":\"Ajouté\",\"Already exists\":\"Existe déjà\",\"Failed\":\"Échec\",\"Add to workstation\":\"Ajouter au poste de travail\",\"Already in workstation — click to remove\":\"Déjà sur le poste de travail — cliquez pour supprimer\",\"Model online\":\"Modèle en ligne\",\"Model offline\":\"Modèle hors ligne\",\"Show fewer\":\"Afficher moins\",\"No measurable audio at the moment\":\"Aucun audio mesurable pour le moment\",\"Playback paused\":\"Lecture interrompue\",\"Audio measurement unavailable or blocked\":\"Mesure audio indisponible ou bloquée\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Autorisation de notification refusée. Le flash de la carte fonctionne toujours.\\n\\nVous pouvez l'activer dans les paramètres du navigateur.\",\"Batch add\":\"Ajout par lots\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Collez les noms d'utilisateur, un par ligne ou séparés par des espaces/virgules :\",\"Cancel\":\"Annuler\",\"Change the language of RoomGrid and the website\":\"Changer la langue de RoomGrid et du site Web\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Changez la langue de RoomGrid. Chaturbate reste dans la langue actuelle du site car aucune langue officielle du site n’est disponible.\",\"About\":\"À propos\",\"Export config\":\"Exporter la configuration\",\"Export usernames.txt\":\"Exporter les noms d'utilisateur.txt\",\"Copy usernames\":\"Copier les noms d'utilisateur\",\"Unmute all\":\"Réactiver tout le son\",\"Resume visible\":\"Reprendre visible\",\"Following\":\"Suite\",\"Sync followed models now\":\"Synchronisez les modèles suivis maintenant\",\"✓ Auto-sync followed models\":\"✓ Synchronisation automatique des modèles suivis\",\"☐ Auto-sync followed models\":\"☐ Synchronisation automatique des modèles suivis\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importez vos modèles Chaturbate actuellement suivis dans le groupe suivant persistant. Seul ce groupe est synchronisé.\",\"Syncing followed models…\":\"Synchronisation des modèles suivis…\",\"Automatic Following sync disabled\":\"Synchronisation automatique du suivi désactivée\",\"Automatic Following sync enabled\":\"Synchronisation de suivi automatique activée\",\"Following sync needs an active Chaturbate login\":\"La synchronisation suivante nécessite une connexion Chaturbate active\",\"Following sync failed safely; saved workspace data was not changed\":\"La synchronisation suivante a échoué en toute sécurité ; les données de l'espace de travail enregistrées n'ont pas été modifiées\",\"Enable dark mode\":\"Activer le mode sombre\",\"Disable dark mode\":\"Désactiver le mode sombre\",\"Status history\":\"Historique du statut\",\"Temporary URLs\":\"URL temporaires\",\"Shortcut panel\":\"Panneau de raccourcis\",\"Toggle video fit\":\"Activer/désactiver l'ajustement vidéo\",\"Shortcuts / hints\":\"Raccourcis / astuces\",\"Ranking\":\"Classement\",\"Model details\":\"Détails du modèle\",\"My profile\":\"Mon profil\",\"Loading local statistics…\":\"Chargement des statistiques locales…\",\"No watch data yet. Statistics start with this version.\":\"Aucune donnée de surveillance pour l'instant. Les statistiques commencent avec cette version.\",\"Statistics are disabled.\":\"Les statistiques sont désactivées.\",\"Enable statistics\":\"Activer les statistiques\",\"Disable statistics\":\"Désactiver les statistiques\",\"Export statistics\":\"Statistiques d'exportation\",\"Delete all statistics\":\"Supprimer toutes les statistiques\",\"Delete this model data\":\"Supprimer les données de ce modèle\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Supprimer toutes les statistiques de surveillance collectées localement ? Votre espace de travail reste intact.\",\"Tracking since\":\"Suivi depuis\",\"Models watched\":\"Modèles regardés\",\"Watch time\":\"Regarder l'heure\",\"Most watched tags\":\"Balises les plus regardées\",\"Most watched hours\":\"Heures les plus regardées\",\"Observed online\":\"Observé en ligne\",\"Public bio\":\"Biographie publique\",\"Public links\":\"Liens publics\",\"No public profile data cached yet.\":\"Aucune donnée de profil public mise en cache pour le moment.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Seules les cartes visibles et jouées activement comptent. Les aperçus au survol ne comptent pas.\",\"Statistics imported\":\"Statistiques importées\",\"Statistics deleted\":\"Statistiques supprimées\",\"Statistics exported\":\"Statistiques exportées\",\"Local observation only\":\"Observation locale uniquement\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Aucun message privé, jeton, paiement, mot de passe ou suivi externe n'est collecté.\",\"Not enough observations for a time estimate\":\"Pas assez d'observations pour une estimation de temps\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Survolez n’importe quel bouton/contrôle pour voir son indice. Les raccourcis courants peuvent être modifiés dans le panneau de raccourcis.\\n\\nValeurs par défaut :\\n/ Focus sur la saisie du nom d'utilisateur\\nr Actualiser tout\\ng Vue Grille\\nf Vue de mise au point\\nAlt+P ou Alt+C Mode nettoyage activé/désactivé\\nAlt+Maj+C Centre d'enregistrement\\nAlt+Maj+R Enregistrer la page actuelle\\nEsc Quitter le mode nettoyage / plein écran\\nEspace Pause/reprise de l'écran principal en mode Focus\\n←/→ ou [/ ] Changer l'écran principal en vue Focus\\nDouble-cliquez sur la carte Plein écran\",\"Visible windows paused\":\"Fenêtres visibles en pause\",\"Visible windows resumed\":\"Fenêtres visibles reprises\",\"Import config\":\"Importer la configuration\",\"Repair saved data\":\"Réparer les données enregistrées\",\"Saved data has been normalized.\":\"Les données enregistrées ont été normalisées.\",\"Clear all data\":\"Effacer toutes les données\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Cela effacera TOUTES les pièces, groupes et paramètres. Continuer?\",\"中文\":\"Chine\",\"English\":\"Anglais\",\"Deutsch\":\"Allemand\",\"Author\":\"Auteur\",\"License\":\"Licence\",\"If this script saves you time…\":\"Si ce script vous fait gagner du temps…\",\"Tip in ETH\":\"Pourboire en ETH\",\"ETH address\":\"Adresse EPF\",\"Copy\":\"Copier\",\"Continue loading the complete catalog\":\"Continuer à charger le catalogue complet\",\"Continuing recording\":\"Poursuite de l'enregistrement\",\"Couples\":\"Couples\",\"current matches\":\"matchs en cours\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Personnalisez le nom, la marque, le slogan et le logo local de Camclient. Les logos restent dans ce navigateur.\",\"Discover something new\":\"Découvrez quelque chose de nouveau\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Des voies de recommandation distinctes vous aident à trouver quelque chose de nouveau au lieu de répéter le catalogue de la page d'accueil.\",\"Do not enlarge\":\"Ne pas agrandir\",\"Enable meter\":\"Activer le compteur\",\"Fast starters\":\"Démarreurs rapides\",\"Fill and crop\":\"Remplir et recadrer\",\"Filter suggestions\":\"Filtrer les suggestions\",\"Fit completely\":\"S'adapter complètement\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Pour plus de transparence, utilisez PNG, WebP ou SVG sécurisé et choisissez Transparent comme arrière-plan du logo. JPEG n'a pas de transparence. Stretch utilise indépendamment la largeur et la hauteur choisies.\",\"For you\":\"Pour toi\",\"Danger\":\"Danger\",\"Back online\":\"De retour en ligne\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"En fonction de vos favoris, tags et activités de visualisation enregistrés localement. Chaque choix montre sa raison.\",\"Black\":\"Noir\",\"Brave exposed no recordable video track.\":\"Brave n'a exposé aucune piste vidéo enregistrable.\",\"Brave or the video source blocked frame capture.\":\"Brave ou la source vidéo a bloqué la capture d'image.\",\"Browser exposed no audio track\":\"Le navigateur n'a exposé aucune piste audio\",\"Camclient\":\"Client Cam\",\"Camclient as the real homepage\":\"Camclient comme véritable page d'accueil\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient attribue automatiquement ces exemples transparents aux balises communes. Utilisez le bouton image sur n'importe quelle balise pour choisir un exemple, télécharger votre propre PNG/WebP/SVG ou masquer l'image.\",\"Choose a Camclient example or upload your own transparent image.\":\"Choisissez un exemple Camclient ou téléchargez votre propre image transparente.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Choisissez un format PNG, JPEG, WebP ou SVG sécurisé inférieur à 1,1 Mo.\",\"Choose artwork for tag\":\"Choisir une illustration pour le tag\",\"Click to enable meter\":\"Cliquez pour activer le compteur\",\"Close more actions\":\"Fermer plus d'actions\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Les couleurs, la typographie, l'espacement, les coins, les ombres et les arrière-plans s'appliquent immédiatement aux paramètres, à l'espace de travail, à Discover et à la bibliothèque de modèles.\",\"gallery\":\"galerie\",\"HD\":\"HD\",\"Inner spacing\":\"Espacement intérieur\",\"Inspiration, not repetition\":\"Inspiration, pas répétition\",\"Interface\":\"Interface\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Il permet d'utiliser, de modifier et de distribuer le logiciel. Les avis de droits d'auteur et de licence doivent rester, et le logiciel est livré sans garantie. Camclient est écrit par Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Je vis depuis peu de temps et attire déjà l'activité publique.\",\"Live stream audio activity\":\"Activité audio en direct\",\"Local watch\":\"Surveillance locale\",\"Logo background\":\"Fond de logo\",\"Logo height\":\"Hauteur du logo\",\"Logo width\":\"Largeur du logo\",\"Loud\":\"Fort\",\"Measured even while output is muted\":\"Mesuré même lorsque la sortie est coupée\",\"Micro\":\"Micro\",\"⏸ Pause visible\":\"⏸ Pause visible\",\"Accent gradient\":\"Dégradé d'accentuation\",\"Active filters\":\"Filtres actifs\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Ajoutez vos propres images ou utilisez le bouton de capture d'écran dans le lecteur. Les captures d'écran peuvent entrer automatiquement dans la galerie et peuvent être utilisées directement comme couverture de modèle.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Tous les paramètres de filtres de page d'accueil, de mise en page, d'aperçu et de pagination sont stockés dans le coffre-fort des paramètres stables et survivent aux mises à jour de script.\",\"All live models\":\"Tous les modèles vivants\",\"All suggestions\":\"Toutes les suggestions\",\"Almost silent\":\"Presque silencieux\",\"Always active\":\"Toujours actif\",\"AND\":\"ET\",\"Automatic example\":\"Exemple automatique\",\"Automatic segments\":\"Segments automatiques\",\"Automatically loading the complete catalog…\":\"Chargement automatique du catalogue complet…\",\"Available recording data was saved.\":\"Les données d'enregistrement disponibles ont été enregistrées.\",\"Configuration\":\"Configuration\",\"Images\":\"Images\",\"Audio\":\"Audio\",\"Action\":\"Action\",\"segment(s)\":\"segments\",\"Sessions\":\"Sessions\",\"Show no image\":\"Ne montrer aucune image\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Affiche l'activité dans la piste audio du flux, indépendamment du fait que la sortie du lecteur soit coupée ou non.\",\"Source\":\"Source\",\"Source ended\":\"Source terminée\",\"Source resolution\":\"Résolution source\",\"Start recording\":\"Commencer l'enregistrement\",\"Starter pick · popular now\":\"Choix de départ · populaire maintenant\",\"Stop and save\":\"Arrêtez et enregistrez\",\"Stretch\":\"Étirer\",\"Suggestions\":\"Suggestions\",\"Tag artwork and emoji\":\"Taguer des illustrations et des emoji\",\"Tag artwork editor\":\"Éditeur d'illustrations de tags\",\"The compressed image is still too large.\":\"L'image compressée est encore trop grande.\",\"Recording unavailable\":\"Enregistrement indisponible\",\"Remix\":\"Remixer\",\"Reset all\":\"Tout réinitialiser\",\"RoomGrid Model library\":\"Bibliothèque de modèles RoomGrid\",\"RoomGrid My user\":\"RoomGrid Mon utilisateur\",\"RoomGrid Statistics\":\"Statistiques de RoomGrid\",\"RoomGrid Tags\":\"RoomGrid Tags\",\"Saturation\":\"Saturation\",\"Save to model gallery\":\"Enregistrer dans la galerie de modèles\",\"Saved segment\":\"Segment enregistré\",\"Screenshot\":\"Capture d'écran\",\"Screenshot failed\":\"La capture d'écran a échoué\",\"Screenshot format\":\"Format de capture d'écran\",\"Screenshot resolution\":\"Résolution de capture d'écran\",\"Screenshot unavailable\":\"Capture d'écran indisponible\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Les captures d'écran sont téléchargées et peuvent également accéder à la galerie de modèles locale.\",\"OR\":\"OU\",\"page\":\"page\",\"Pages\":\"Pages\",\"part\":\"partie\",\"Pause\":\"Pause\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Choix personnels, nouvelles découvertes, salles de concert populaires et modèles que vous n'avez jamais visités, clairement séparés du catalogue complet de la page d'accueil.\",\"Place\":\"Lieu\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP et SVG sécurisé prennent en charge la transparence. Les images sont redimensionnées localement à 144 × 144 pixels et enregistrées avec vos paramètres.\",\"Popular now\":\"Populaire maintenant\",\"Public live models with no local watch time in Camclient.\":\"Modèles publics en direct sans heure de visionnage locale dans Camclient.\",\"Quiet\":\"Calme\",\"Quota\":\"Quota\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Classé selon le nombre actuel de téléspectateurs publics ; les données du catalogue ne sont pas stockées de manière permanente.\",\"Recording is still active.\":\"L'enregistrement est toujours actif.\",\"Recording quality\":\"Qualité d'enregistrement\",\"Recording stopped\":\"L'enregistrement s'est arrêté\",\"Middle-click or Ctrl+click opens a new tab\":\"Un clic central ou Ctrl+clic ouvre un nouvel onglet\",\"Models you have visited locally and that are public again now.\":\"Modèles que vous avez visités localement et qui sont à nouveau publics maintenant.\",\"Modules\":\"Modules\",\"Mono\":\"Mono\",\"New discoveries\":\"Nouvelles découvertes\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nouveau dans le catalogue, nouvellement vu par Camclient, ou tout juste commencé à diffuser.\",\"No audio track\":\"Aucune piste audio\",\"No filters active · all matching live models are shown\":\"Aucun filtre actif · tous les modèles vivants correspondants sont affichés\",\"No recommendations match the active filters.\":\"Aucune recommandation ne correspond aux filtres actifs.\",\"No recording data\":\"Aucune donnée d'enregistrement\",\"NOT\":\"PAS\",\"Notifications\":\"Notifications\",\"Only new\":\"Seulement du neuf\",\"Open Camclient homepage\":\"Ouvrir la page d'accueil de Camclient\",\"Secrets\":\"Secrets\",\"Mobile\":\"Mobile\",\"Monospace\":\"Monospace\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"L'en-tête affiche uniquement Accueil, Découverte, Explorateur de balises, Centre de statistiques, Bibliothèque de modèles, Mon utilisateur et Paramètres. Chaque zone de configuration est disponible sous forme de sous-page Paramètres.\",\"The image could not be decoded.\":\"L'image n'a pas pu être décodée.\",\"The image could not be read.\":\"L'image n'a pas pu être lue.\",\"The image format is unsafe or unsupported.\":\"Le format d'image n'est pas sécurisé ou n'est pas pris en charge.\",\"The lanes respect your active search and filter settings.\":\"Les voies respectent vos paramètres de recherche active et de filtrage.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"L'enregistrement est sauvegardé uniquement sur cet appareil. Continuez uniquement si vous êtes autorisé à enregistrer ce contenu.\",\"The video has no usable frame yet.\":\"La vidéo n'a pas encore d'image utilisable.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Il n'y a pas encore suffisamment de données sur les préférences locales, ce sont donc des choix de départ transparents basés sur la popularité actuelle.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Ces paramètres contrôlent la présentation de la page d'accueil de Camclient, la pagination, les informations sur le modèle, les filtres et les performances.\",\"Total\":\"Total\",\"Transparent\":\"Transparente\",\"Type\":\"Type\",\"Unified main navigation\":\"Navigation principale unifiée\",\"Unvisited\":\"Non visité\",\"Upload own image\":\"Télécharger votre propre image\",\"URL\":\"URL\",\"Use as model cover\":\"Utiliser comme couverture de modèle\",\"Use the complete search, filters, and sorting for every current result.\":\"Utilisez la recherche complète, les filtres et le tri pour chaque résultat actuel.\",\"Version\":\"Version\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Visiter la page d’accueil de Chaturbate ouvre toujours la page d’accueil complète de Camclient. Chaque zone principale reste dans la même interface personnalisée.\",\"Volume\":\"Volume\",\"Waiting for audio\":\"En attente du son\",\"What does the MIT license mean?\":\"Que signifie la licence MIT ?\",\"White\":\"Blanc\",\"Your discovery mix\":\"Votre mix découverte\",\"Trans\":\"Trans\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Découvrez les salons publics en direct\",\"all groups\":\"tous les groupes\",\"at least one group\":\"au moins un groupe\",\"no measurements yet\":\"aucune mesure pour le moment\",\"Country, region or location\":\"Pays, région ou lieu\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Contrôlez l’ordre des modèles, les filtres, le nombre par page, la taille des cartes et les informations visibles directement sur la page d’accueil Chaturbate d’origine. Chaque modification s’applique immédiatement et est enregistrée dans ce navigateur.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"Il y a [[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] preview\":\"Aperçu [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] profil enregistré\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Détails du modèle pour @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] modèles suivis importés\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"Éléments My Collection reconnus : [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"[[ROOMGRID_VALUE_0]] modèles visibles sur [[ROOMGRID_VALUE_1]] actuellement chargés.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Filtre de groupe actif : [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] cartes correspondantes sur [[ROOMGRID_VALUE_2]] ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Couverture personnalisée · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Couverture enregistrée localement · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Exemples d'images et de couvertures ([[ROOMGRID_VALUE_0]] enregistrées)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Stocké en permanence sur cet appareil · dernier [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Propre image · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Enregistré [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Première apparition : [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Dernière apparition : [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Dernier direct : [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Apparitions : [[ROOMGRID_VALUE_0]]× · première [[ROOMGRID_VALUE_1]] · dernière [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Horaire habituel : [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Technique : [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] observations locales en direct, au maximum une toutes les 5 minutes\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] de votre temps de visionnage précédent, estimation prudente\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Retirer le tag [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Gérer les groupes personnalisés · attribué à [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Supprimer le groupe « [[ROOMGRID_VALUE_0]] » ? Les modèles resteront dans votre bibliothèque.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Groupe « [[ROOMGRID_VALUE_0]] » supprimé\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] groupes · sélection multiple possible\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] tags enregistrés\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Supprimer le filtre [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] correspondances\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtres actifs\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Les informations publiques du profil peuvent être chargées via « Tous les détails ».\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Organisation personnelle depuis : [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Catalogué automatiquement depuis : [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Dernière activité : [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Apparitions : [[ROOMGRID_VALUE_0]]× · dernière enregistrée [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Découverts : [[ROOMGRID_VALUE_0]] · mesures de qualité : [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Informations du profil : [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] archivé\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] restauré\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Page [[ROOMGRID_VALUE_0]] sur [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modèles\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] enregistrés\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] regardés\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] actifs · augmente automatiquement\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"Modèles [[ROOMGRID_VALUE_0]] · Séances [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favoris\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] modèles distincts\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] aperçus\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Aperçu/aperçu rapide des modèles [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] jours\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Série de [[ROOMGRID_VALUE_0]] jours\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] regardés plusieurs fois\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] jours de données locales\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"moyenne pondérée [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] en direct maintenant\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modèles reconnus\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Certaines données n’ont pas pu être lues : [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Jour le plus actif : [[ROOMGRID_VALUE_0]] avec [[ROOMGRID_VALUE_1]]. Conservation : [[ROOMGRID_VALUE_2]] jours pour les événements bruts ; les valeurs plus anciennes peuvent rester sous forme de totaux quotidiens.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sessions\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] enregistrés localement\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Impossible de charger les données locales : [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] est en ligne.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] est hors ligne.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] n’a pas été vu en ligne depuis un certain temps.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favoris sont en ligne ensemble.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Le profil public de [[ROOMGRID_VALUE_0]] a changé.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"Modèles [[ROOMGRID_VALUE_0]] mis à jour\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Aperçu de l’importation des statistiques (aucune modification) :\\n[[ROOMGRID_VALUE_0]] modèles\\n[[ROOMGRID_VALUE_1]] événements\\n[[ROOMGRID_VALUE_2]] profils publics\\n\\nContinuer remplacera les statistiques existantes.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Aperçu en direct de [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] ajouté à MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Supprimer [[ROOMGRID_VALUE_0]] ?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Importer [[ROOMGRID_VALUE_0]] vues et [[ROOMGRID_VALUE_1]] flux ?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] salons ajoutés à l’espace de travail\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modèles enregistrés\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] résultats correspondants\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] modèles vivants correspondants\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Partage le n° [[ROOMGRID_VALUE_0]] avec un favori\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] images ajoutées ; [[ROOMGRID_VALUE_1]] n'a pas pu être traité.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] images ajoutées.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] salons\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] noms d’utilisateur valides\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] salons\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"URL temporaires : [[ROOMGRID_VALUE_0]] ajoutées, [[ROOMGRID_VALUE_1]] échouées\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Aperçu de l'importation des statistiques : modèles [[ROOMGRID_VALUE_0]] et événements [[ROOMGRID_VALUE_1]]. Continuer en toute sécurité fusionne les données et crée d'abord une sauvegarde.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Les dispositions personnalisées acceptent jusqu’à [[ROOMGRID_VALUE_0]] salons.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Scène «[[ROOMGRID_VALUE_0]]» enregistrée\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Scène « [[ROOMGRID_VALUE_0]] » chargée\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] flux actifs · [[ROOMGRID_VALUE_1]] instable · [[ROOMGRID_VALUE_2]] à regarder\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] flux · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p attribués\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Supprimer la scène « [[ROOMGRID_VALUE_0]] » ?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Enregistré uniquement pour [[ROOMGRID_VALUE_0]] : jusqu'à [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Cette caméra utilise la qualité globale/adaptative[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] a été remplacé par [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Lecture audio uniquement à partir de [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Placé dans l’emplacement Focus [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Placé dans la fenêtre de grille [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Résolution [[ROOMGRID_VALUE_0]], état [[ROOMGRID_VALUE_1]], mémoire tampon [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Santé du flux [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modèle(s)\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Supprimer le filtre : [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Correspond à vos centres d'intérêt : [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catalogue [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] récupéré · [[ROOMGRID_VALUE_2]] modèles vivants\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modèles locaux\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catalogue [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modèles vivants\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] salles de catalogue chargées\",\"Detected via: [[ROOMGRID_VALUE_0]]\":\"Détecté via : [[ROOMGRID_VALUE_0]]\",\"Sources: [[ROOMGRID_VALUE_0]]\":\"Sources : [[ROOMGRID_VALUE_0]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing replaces the current statistics.\":\"Aperçu de l’importation : [[ROOMGRID_VALUE_0]] modèles et [[ROOMGRID_VALUE_1]] événements. Continuer remplacera les statistiques actuelles.\",\"[[ROOMGRID_VALUE_0]] ms startup · [[ROOMGRID_VALUE_1]] stalls · [[ROOMGRID_VALUE_2]]% dropped frames\":\"démarrage [[ROOMGRID_VALUE_0]] ms · [[ROOMGRID_VALUE_1]] interruptions · [[ROOMGRID_VALUE_2]] % d’images perdues\"},\"visible\":{\"Gruppe löschen\":\"Supprimer le groupe\",\"Gruppen umbenennen oder löschen\":\"Renommer ou supprimer des groupes\",\"HD + Gaming\":\"Jeux HD+\",\"IndexedDB\":\"Base de données indexée\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Chaque balise enregistrée est automatiquement disponible dans la recherche, dans la saisie semi-automatique et ici sous forme de filtre multiple direct.\",\"Keine Models passen zu diesen Filtern.\":\"Aucun modèle ne correspond à ces filtres.\",\"Keine Priorität\":\"Pas une priorité\",\"Konto-Daten werden eingelesen …\":\"Les détails du compte sont en cours de lecture...\",\"Lautstärke pro Model merken\":\"Volume de notes par modèle\",\"Layout & sichtbare Bereiche\":\"Disposition et zones visibles\",\"Letzte Messung\":\"Dernière mesure\",\"LIVE\":\"EN DIRECT\",\"lokal · für Suche und Filter\":\"local · pour la recherche et les filtres\",\"Maximale Stream-Lautstärke\":\"Volume de flux maximal\",\"Model besitzt alle gewählten Tags\":\"Le modèle est propriétaire de toutes les balises sélectionnées\",\"Model besitzt mindestens einen gewählten Tag\":\"Le modèle a au moins une balise sélectionnée\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"La connexion actuelle est active ; Après la mesure suivante, le cap local apparaît ici.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Cette vue de bibliothèque est toujours vide.\",\"Eigene Priorität\":\"Propre priorité\",\"Eigene Tags\":\"Propres balises\",\"Eigene Tags filtern\":\"Filtrez vos propres balises\",\"Eigenen Tag eingeben …\":\"Entrez votre propre journée...\",\"Einstellungen\":\"Paramètres\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Entrée ou « Ajouter une balise » reprend le brouillon. Il n'est enregistré de manière permanente qu'avec « Enregistrer le profil ». Les balises déjà utilisées sont suggérées au fur et à mesure que vous tapez.\",\"Ereignisse im Zeitraum\":\"Événements de la période\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Créez des collections librement nommées, attribuez des modèles à plusieurs groupes et affichez ici un ou plusieurs groupes ensemble.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Créez d’abord votre propre groupe ci-dessus.\",\"Follower\":\"Abonnés\",\"GAMING\":\"JEU\",\"Gesamter Verlauf\":\"Cours entier\",\"Grid +\":\"Grille+\",\"Größe der Bedienelemente\":\"Taille des contrôles\",\"Offizielle Statistik verbinden\":\"Connectez les statistiques officielles\",\"Optionale offizielle Kontostatistik\":\"Statistiques facultatives du compte officiel\",\"Persönlich organisiert\":\"Personnellement organisé\",\"PiP\":\"Pépin\",\"Player & Verbindung\":\"Lecteur et connexion\",\"Player neu verbinden\":\"Reconnecter le joueur\",\"Player-Ecken\":\"Coins des joueurs\",\"Player-Hintergrund\":\"Contexte du joueur\",\"Player-Höhe\":\"Taille du joueur\",\"Player-Seitenverhältnis\":\"Format d'image du joueur\",\"Position der Informationen\":\"Localisation des informations\",\"Private Notiz\":\"Note privée\",\"Private Notizen anzeigen\":\"Afficher les notes privées\",\"Puffergröße\":\"Taille du tampon\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Automatisch neu verbinden\":\"Se reconnecter automatiquement\",\"Bei verstecktem Tab pausieren\":\"Pause pendant que l'onglet est masqué\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Appliquer les détails du compte Chaturbate existant\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Optimiser l'image · grand zoom, position, filtre et rotation\",\"Bildanpassung\":\"Ajustement des images\",\"Bildwerkzeuge einklappen\":\"Outils de réduction d’image\",\"Breite des Informationsbereichs\":\"Largeur de la zone d'information\",\"Broadcaster-Verbindung verwalten\":\"Gérer la connexion du diffuseur\",\"Chat anzeigen\":\"Afficher le chat\",\"Ctrl + Shift + X\":\"Ctrl + Maj + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Cela signifie que vous n'êtes pas limité aux données depuis l'installation du script : RoomGrid peut lire votre liste suivante existante et les entrées clairement reconnaissables de Ma collection. Aucun message, paiement, média ou mot de passe n'est copié.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio contient également des comparaisons de périodes, des cartes thermiques, des entonnoirs de découverte, des analyses d'espace de travail et de qualité ainsi que des exportations de données.\",\"Datenschutz & Export\":\"Protection des données et exportation\",\"Deine Aktivität der letzten 14 Tage\":\"Votre activité des 14 derniers jours\",\"Deine meistgesehenen Models\":\"Vos modèles les plus consultés\",\"Model ist in allen gewählten Gruppen\":\"Le modèle est dans tous les groupes sélectionnés\",\"Model ist in mindestens einer gewählten Gruppe\":\"Le modèle fait partie d'au moins un groupe sélectionné\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Ouvrir ma collection\",\"Namen speichern\":\"Enregistrer le nom\",\"Nicht verbunden\":\"Non connecté\",\"Niedrige Live-Latenz\":\"Faible latence en direct\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Il n’existe toujours pas de valeurs d’usage locales suffisantes.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Pas encore de groupe à nous. Entrez un nom ci-dessus et créez votre première collection.\",\"Noch keine Model-Sehzeit erfasst.\":\"Aucune durée de visionnage du modèle n'a encore été enregistrée.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Pas encore d'instantanés de compte officiel.\",\"Noch nicht eingelesen\":\"Pas encore lu\",\"Nur auf diesem Gerät\":\"Uniquement sur cet appareil\",\"Öffentliche/erkannte Tags\":\"Balises publiques/reconnues\",\"Öffentliches Profil anzeigen\":\"Afficher le profil public\",\"‹ Zurück\":\"‹ Retour\",\"● 1.2k · #tag\":\"● 1,2k · #tag\",\"1 Jahr\":\"1 an\",\"30 Tage\":\"30 jours\",\"7 Tage\":\"7 jours\",\"90 Tage\":\"90 jours\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Afficher des modèles similaires\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Toutes les modifications sont enregistrées localement et s'appliquent également au modèle suivant.\",\"Alle eigenen Tags\":\"Toutes les balises personnalisées\",\"Alle Gruppen\":\"Tous les groupes\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Demander des données Chaturbate plus anciennes\",\"Automatisch abspielen\":\"Lecture automatique\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Capture automatique · entièrement organisé lors de l'édition\",\"Verbunden\":\"Connecté\",\"Vollständige Modelstatistik\":\"Statistiques complètes du modèle\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Lire les données de compte existantes maintenant\",\"Weiter ›\":\"Suivant ›\",\"Zufriedenheit\":\"Satisfaite\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid ne peut pas deviner les valeurs des comptes Chaturbate personnels. Ces cartes n'apparaissent que si vous avez consciemment lié les statistiques officielles du diffuseur à votre propre nom d'utilisateur et à votre token.\",\"RoomGrid Suite\":\"ChambreGrid Suite\",\"Seitengröße\":\"Taille des pages\",\"SingleCam\":\"Caméra unique\",\"SingleCam einstellen\":\"Configurer SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Restaurer les paramètres par défaut de SingleCam\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Dès que vous enregistrez votre propre balise pour un modèle, elle apparaîtra ici et dans la saisie semi-automatique.\",\"Status im Player anzeigen\":\"Afficher le statut dans le joueur\",\"Status-Aktualisierung\":\"Mise à jour du statut\",\"Stream immer stumm starten\":\"Démarrez toujours la diffusion en mode silencieux\",\"Tag hinzufügen\":\"Ajouter une balise\",\"Tipps letzte Stunde\":\"Conseils dernière heure\",\"Token-Guthaben\":\"Solde des jetons\",\"Ton & Ruhe\":\"Son et calme\",\"Ungefähre Onlinezeiten\":\"Horaires de connexion approximatifs\",\"Gruppen\":\"Groupes\",\"Schließen\":\"Fermer\",\"Startseite nach eigenen Gruppen filtern\":\"Filtrer la page d’accueil par vos propres groupes\",\"RoomGrid Komfortzentrum\":\"Centre de confort RoomGrid\",\"Mein Nutzer\":\"Mon profil\",\"Mein Nutzer-Dashboard\":\"Tableau de bord de mon profil\",\"Modellbibliothek\":\"Bibliothèque de modèles\",\"Große Modellbibliothek\":\"Grande bibliothèque de modèles\",\"Statistikzentrum\":\"Centre de statistiques\",\"Sitzungen\":\"Sessions\",\"Dock + Hover\":\"Dock + survol\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Local et privé sur cet appareil\",\"Sortierung\":\"Trier par\",\"Meiste Sehzeit\":\"Temps de visionnage maximal\",\"Zuletzt aktiv\":\"Activité récente\",\"Meiste Sitzungen\":\"Plus de sessions\",\"Meiste Zuschauer\":\"Plus de spectateurs\",\"Status\":\"Statut\",\"Alle Status\":\"Tous les statuts\",\"Nur live\":\"En direct uniquement\",\"Nur Live\":\"En direct uniquement\",\"Offline\":\"Hors ligne\",\"Unbekannt\":\"Inconnu\",\"Nur Bibliothek\":\"Bibliothèque uniquement\",\"Pro Seite\":\"Par page\"}}","it":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Non è stato possibile preparare in modo sicuro il cambio di lingua. Questa pagina non è stata lasciata.\",\"Language\":\"Lingua\",\"Default workspace\":\"Area di lavoro predefinita\",\"Workspace\":\"Spazio di lavoro\",\"New workspace\":\"Nuovo spazio di lavoro\",\"copy\":\"copia\",\"New group\":\"Nuovo gruppo\",\"Saved model\":\"Modello salvato\",\"Search models, pages, workspaces and commands…\":\"Cerca modelli, pagine, aree di lavoro e comandi...\",\"Type to filter · Enter to run · Esc to close\":\"Digita per filtrare · Invio per eseguire · Esc per chiudere\",\"No matches\":\"Nessuna corrispondenza\",\"Currently active RoomGrid version\":\"Versione RoomGrid attualmente attiva\",\"years\":\"anni\",\"Live\":\"Vivi\",\"Private\":\"Privato\",\"Away\":\"Assente\",\"Offline\":\"Non in linea\",\"sessions\":\"sessioni\",\"NEW\":\"NUOVO\",\"Name, subject, tag or place\":\"Nome, argomento, tag o luogo\",\"Website order\":\"Ordine del sito\",\"Most viewers\":\"Più spettatori\",\"Fewest viewers\":\"Meno spettatori\",\"Longest online\":\"Online da più tempo\",\"Recently online\":\"Online di recente\",\"Age ascending\":\"Età crescente\",\"Age descending\":\"Età decrescente\",\"New models first\":\"Nuovi modelli prima\",\"Saved first\":\"Salvati prima\",\"Favorites first\":\"Preferiti prima\",\"Most watched\":\"I più visti\",\"Recently watched\":\"Visti di recente\",\"Random mix\":\"Ordine casuale\",\"All loaded\":\"Tutti i modelli caricati\",\"Previous page\":\"Pagina precedente\",\"Next page\":\"Pagina successiva\",\"Reload complete live list\":\"Ricarica l'elenco live completo\",\"All homepage options\":\"Tutte le opzioni della pagina iniziale\",\"Reset filters\":\"Reimposta i filtri\",\"Quick filter\":\"Filtro rapido\",\"Sort\":\"Ordina\",\"Models per page\":\"Modelli per pagina\",\"Load more results\":\"Carica più risultati\",\"still to display\":\"ancora da esporre\",\"continues automatically while scrolling\":\"continua automaticamente durante lo scorrimento\",\"visible\":\"visibili\",\"loaded\":\"caricati\",\"page\":\"pagina\",\"active filters\":\"filtri attivi\",\"loading complete live list\":\"caricamento dell'elenco live completo\",\"all matching live models found\":\"sono stati trovati tutti i modelli live corrispondenti\",\"live list incomplete, retry\":\"elenco live incompleto, riprova\",\"Requests temporarily paused\":\"Richieste temporaneamente sospese\",\"Vault could not be unlocked\":\"Impossibile sbloccare il vault\",\"Preview Dock\":\"Dock di anteprima\",\"Connecting…\":\"Connessione…\",\"Stream temporarily unavailable\":\"Streaming temporaneamente non disponibile\",\"Error\":\"Errore\",\"Buffering\":\"Bufferizzazione\",\"Preview unavailable\":\"Anteprima non disponibile\",\"Connect\":\"Connettiti\",\"Close\":\"Chiudi\",\"Opacity\":\"Opacità\",\"Info\":\"Informazioni\",\"Age\":\"Età\",\"Gender\":\"Genere\",\"Location\":\"Località\",\"Country\":\"Paese\",\"Last broadcast\":\"Ultima trasmissione\",\"Fan club\":\"Fanclub\",\"Private show\":\"Spettacolo privato\",\"Spy show\":\"Spettacolo di spionaggio\",\"Model comparison list\":\"Elenco di confronto dei modelli\",\"Compare\":\"Confronta\",\"Open model details\":\"Apri i dettagli del modello\",\"Compare now\":\"Confronta ora\",\"Clear\":\"Chiaro\",\"Up to four models can be compared at once.\":\"È possibile confrontare fino a quattro modelli contemporaneamente.\",\"Remove from comparison\":\"Rimuovi dal confronto\",\"Add to comparison\":\"Aggiungi al confronto\",\"Total watched\":\"Totale guardato\",\"Sessions\":\"Sessioni\",\"Viewers\":\"Spettatori\",\"Rating\":\"Valutazione\",\"Online samples\":\"Campioni in linea\",\"Loading quality data…\":\"Caricamento dati sulla qualità...\",\"samples\":\"campioni\",\"stalls\":\"bancarelle\",\"No quality measurements yet\":\"Nessuna misurazione della qualità ancora\",\"Quality data unavailable\":\"Dati sulla qualità non disponibili\",\"Details\":\"Dettagli\",\"Remove\":\"Rimuovi\",\"Model comparison\":\"Confronto dei modelli\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Ogni valore proviene da informazioni pubbliche, dalla tua organizzazione o da osservazioni locali su questo dispositivo. I valori mancanti non vengono mai indovinati.\",\"Shared tags\":\"Tag condivisi\",\"Loading public facts…\":\"Caricamento fatti pubblici…\",\"Unknown status\":\"Stato sconosciuto\",\"Last activity\":\"Ultima attività\",\"These statistics describe only your own local RoomGrid usage.\":\"Queste statistiche descrivono solo il tuo utilizzo locale di RoomGrid.\",\"At a glance\":\"A colpo d'occhio\",\"Type\":\"Tipo\",\"Appearances\":\"Apparenze\",\"Public facts\":\"Fatti pubblici\",\"No public biography available yet.\":\"Nessuna biografia pubblica ancora disponibile.\",\"Public link\":\"Collegamento pubblico\",\"Public profile source\":\"Origine del profilo pubblico\",\"Profile updated\":\"Profilo aggiornato\",\"Private note (local only)\":\"Nota privata (solo locale)\",\"Your tags\":\"I tuoi tag\",\"No rating\":\"Nessuna valutazione\",\"No priority\":\"Nessuna priorità\",\"Private library data\":\"Dati della biblioteca privata\",\"Private notes\":\"Note private\",\"Priority\":\"Priorità\",\"Favorite\":\"Preferito\",\"Watchlist\":\"Lista di controllo\",\"Archived\":\"Archiviato\",\"Watch again\":\"Guarda di nuovo\",\"Technical note\":\"Nota tecnica\",\"Technical\":\"Tecnico\",\"Save all changes\":\"Salva tutte le modifiche\",\"Loading local activity…\":\"Caricamento attività locale…\",\"Activity and playback quality\":\"Attività e qualità della riproduzione\",\"Sun\":\"Sole\",\"Mon\":\"Lun\",\"Tue\":\"Mar\",\"Wed\":\"Mercoledì\",\"Thu\":\"Gio\",\"Fri\":\"Ven\",\"Sat\":\"Sab\",\"Quality samples\":\"Campioni di qualità\",\"Average startup\":\"Avvio medio\",\"Stalls\":\"Bancarelle\",\"Reconnects\":\"Si riconnette\",\"Dropped frames\":\"Fotogrammi saltati\",\"Online observations\":\"Osservazioni in linea\",\"Frequently observed online times\":\"Orari online osservati frequentemente\",\"Not enough online observations yet.\":\"Non ci sono ancora abbastanza osservazioni online.\",\"Watched\":\"Guardato\",\"Preview\":\"Anteprima\",\"Quick view\":\"Visualizzazione rapida\",\"Discovered\":\"Scoperto\",\"Quality\":\"Qualità\",\"Connection\":\"Connessione\",\"No local activity recorded yet.\":\"Nessuna attività locale ancora registrata.\",\"Recent events\":\"Eventi recenti\",\"Activity data unavailable.\":\"Dati sull'attività non disponibili.\",\"Similar models\":\"Modelli simili\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Calcolato in base a tag locali/pubblici, paese e tipo. Ogni suggerimento mostra il suo motivo e nessun dato viene caricato.\",\"shared tags\":\"tag condivisi\",\"same country\":\"stesso paese\",\"same type\":\"stesso tipo\",\"Not enough matching data yet.\":\"Dati corrispondenti non ancora sufficienti.\",\"Overview\":\"Panoramica\",\"Profile\":\"Profilo\",\"Organize\":\"Organizzare\",\"Activity\":\"Attività\",\"Similar\":\"Simile\",\"Model detail sections\":\"Sezioni di dettaglio del modello\",\"Previous model\":\"Modello precedente\",\"Next model\":\"Modello successivo\",\"Open room\":\"Stanza aperta\",\"Pin preview\":\"Anteprima del perno\",\"No public fields available\":\"Nessun campo pubblico disponibile\",\"Public facts currently unavailable\":\"Fatti pubblici attualmente non disponibili\",\"Session chat search\":\"Ricerca chat di sessione\",\"Search this session only…\":\"Cerca solo in questa sessione...\",\"New messages\":\"Nuovi messaggi\",\"Time for a short break.\":\"È ora di fare una breve pausa.\",\"Your RoomGrid time budget for today has been reached.\":\"Il tuo budget di tempo per RoomGrid per oggi è stato raggiunto.\",\"RoomGrid action messages\":\"Messaggi di azione RoomGrid\",\"This change is saved automatically.\":\"Questa modifica viene salvata automaticamente.\",\"Undo\":\"Annulla\",\"Change undone\":\"Modifica annullata\",\"Undo was not applied because newer changes already exist.\":\"L'annullamento non è stato applicato perché esistono già modifiche più recenti.\",\"Chaturbate live catalog\":\"Catalogo live di Chaturbate\",\"RoomGrid Discover\":\"RoomGrid Scopri\",\"Chaturbate Discover page\":\"Pagina Scopri Chaturbate\",\"Following list\":\"Elenco seguente\",\"My Collection\":\"La mia collezione\",\"Chaturbate page\":\"Pagina Chaturbate\",\"Local record\":\"Registro locale\",\"Personally organized\":\"Organizzato personalmente\",\"Local watch history\":\"Cronologia visualizzazioni locali\",\"Public profile\":\"Profilo pubblico\",\"Local quality measurements\":\"Misurazioni della qualità locale\",\"Local model index\":\"Indice del modello locale\",\"Current live signal\":\"Segnale live attuale\",\"Within the last hour\":\"Nell'ultima ora\",\"Within the last 24 hours\":\"Nelle ultime 24 ore\",\"Within the last 30 days\":\"Negli ultimi 30 giorni\",\"Historical record\":\"Documentazione storica\",\"Time unknown\":\"Orario sconosciuto\",\"All\":\"Tutto\",\"Favorites\":\"Preferiti\",\"Library\":\"Biblioteca\",\"My Collection contained no reliably recognizable entries\":\"La mia collezione non conteneva voci riconoscibili in modo affidabile\",\"No group filter active · all models are shown.\":\"Nessun filtro di gruppo attivo · vengono visualizzati tutti i modelli.\",\"Clear group filter\":\"Cancella filtro gruppo\",\"Filter homepage by groups\":\"Filtra la home page per gruppi\",\"Close group menu\":\"Chiudi il menu del gruppo\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Scegli uno o più dei tuoi gruppi. Altre carte modello vengono immediatamente nascoste nella home page di Chaturbate.\",\"Show all\":\"Mostra tutto\",\"Models in this group\":\"Modelli in questo gruppo\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Non esiste ancora un gruppo personalizzato. Creane uno nella libreria dei modelli e assegna lì i modelli.\",\"In any group\":\"In qualsiasi gruppo\",\"In all groups\":\"In tutti i gruppi\",\"Manage groups\":\"Gestisci gruppi\",\"Create group\":\"Crea gruppo\",\"Custom groups for the homepage\":\"Gruppi personalizzati per la home page\",\"Main views\":\"Viste principali\",\"MultiCam workstation\":\"Stazione di lavoro MultiCam\",\"Camera grid, audio and layouts\":\"Griglia della fotocamera, audio e layout\",\"Discover\":\"Scopri\",\"Full catalog, filters and sorting\":\"Catalogo completo, filtri e ordinamento\",\"Personal dashboard\":\"Cruscotto personale\",\"Favorites, activity and shortcuts\":\"Preferiti, attività e scorciatoie\",\"Tag explorer\":\"Esplora tag\",\"Combinations, trends and related tags\":\"Combinazioni, tendenze e tag correlati\",\"Library and statistics\":\"Biblioteca e statistica\",\"Statistics Center\":\"Centro statistiche\",\"Model comparison and local statistics\":\"Confronto tra modelli e statistiche locali\",\"Model library\":\"Libreria modelli\",\"Profiles, tags, groups and notes\":\"Profili, tag, gruppi e note\",\"My user\":\"Il mio profilo\",\"Preferences, usage and account data\":\"Preferenze, utilizzo e dati dell'account\",\"Analytics Studio\":\"Studio di analisi\",\"Ranges, heatmaps and exports\":\"Intervalli, mappe termiche ed esportazioni\",\"Settings and maintenance\":\"Impostazioni e manutenzione\",\"Customize homepage\":\"Personalizza pagina iniziale\",\"Cards, filters and visible details\":\"Schede, filtri e dettagli visibili\",\"Appearance\":\"Aspetto\",\"Colors, density and surfaces\":\"Colori, densità e superfici\",\"Workspaces and groups\":\"Aree di lavoro e gruppi\",\"Layouts, templates and assignments\":\"Layout, modelli e compiti\",\"Data and backup\":\"Dati e backup\",\"Import, export and recovery\":\"Importazione, esportazione e recupero\",\"Windows and performance\":\"Finestre e prestazioni\",\"Resource profiles and preview limits\":\"Profili delle risorse e limiti di anteprima\",\"History, private session and local data\":\"Cronologia, sessione privata e dati locali\",\"Diagnostics\":\"Diagnostica\",\"Self-check, routes and recovery\":\"Autocontrollo, percorsi e recupero\",\"RoomGrid main menu\":\"Menu principale di RoomGrid\",\"All pages, settings and tools in one place\":\"Tutte le pagine, le impostazioni e gli strumenti in un unico posto\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Il menù rimane all'interno della finestra visibile; tutti i dati RoomGrid rimangono locali.\",\"RoomGrid pages\":\"Pagine RoomGrid\",\"Statistics\":\"Statistiche\",\"Customize original homepage\":\"Personalizza la pagina iniziale originale\",\"Homepage\":\"Pagina iniziale\",\"Filter homepage by your own groups\":\"Filtra la home page in base ai tuoi gruppi\",\"Groups\":\"Gruppi\",\"Activity trend\":\"Andamento dell'attività\",\"Link\":\"Collegamento\",\"Public profile text and links\":\"Testo e collegamenti del profilo pubblico\",\"Last known public preview\":\"Ultima anteprima pubblica conosciuta\",\"Current public preview\":\"Anteprima pubblica attuale\",\"Offline may only provide a placeholder\":\"Offline può fornire solo un segnaposto\",\"Example images and covers\":\"Immagini e copertine di esempio\",\"Reading local image archive…\":\"Lettura dell'archivio immagini locale…\",\"Save new snapshot\":\"Salva nuova istantanea\",\"Saving public previews…\":\"Salvataggio delle anteprime pubbliche…\",\"No saved image yet; current public previews are still shown.\":\"Nessuna immagine ancora salvata; le anteprime pubbliche attuali sono ancora mostrate.\",\"Current public cover\":\"Copertura pubblica attuale\",\"Current compact preview\":\"Anteprima compatta corrente\",\"Current live image\":\"Immagine dal vivo attuale\",\"Room\":\"Stanza\",\"More\":\"Di più\",\"Unwatch\":\"Non guardare\",\"Data sources and freshness\":\"Origini dei dati e freschezza\",\"Data source\":\"Origine dati\",\"Full local data set\":\"Set completo di dati locali\",\"Matching filters\":\"Filtri corrispondenti\",\"Shown on this page\":\"Mostrato in questa pagina\",\"Latest live signals\":\"Ultimi segnali in tempo reale\",\"With watch history\":\"Con cronologia visualizzazioni\",\"Public-profile coverage\":\"Copertura del profilo pubblico\",\"Country coverage\":\"Copertura del paese\",\"Age coverage\":\"Copertura dell'età\",\"Tag coverage\":\"Copertura dei tag\",\"Which data set is being searched?\":\"Quale set di dati viene cercato?\",\"Last local observation\":\"Ultima osservazione locale\",\"No observation time yet\":\"Nessun tempo di osservazione ancora\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Pubblico = dalle pagine attuali di Chaturbate o dal catalogo live. Personale = salvato, taggato o raggruppato da te. Locale = sessioni, anteprime o valori di qualità osservati solo su questo dispositivo. I campi sconosciuti rimangono sconosciuti e non vengono mai indovinati.\",\"Local model statistics\":\"Statistiche del modello locale\",\"Automatically recorded · personally organized\":\"Registrato automaticamente · organizzato personalmente\",\"Automatically recorded in the growing model catalog\":\"Registrato automaticamente nel catalogo dei modelli in crescita\",\"Total\":\"Totale\",\"Avg. session\":\"Sessione media\",\"Active days\":\"Giorni attivi\",\"Share\":\"Condividi\",\"Longest session\":\"Sessione più lunga\",\"Previews\":\"Anteprime\",\"Quick views\":\"Viste rapide\",\"Data sources\":\"Origini dei dati\",\"Edit in library\":\"Modifica nella libreria\",\"No experience data yet\":\"Nessun dato sull'esperienza ancora\",\"Online observations will be collected locally from now on.\":\"D'ora in poi le osservazioni online verranno raccolte localmente.\",\"Open\":\"Aperto\",\"Good trend\":\"Buona tendenza\",\"Moderate trend\":\"Tendenza moderata\",\"Initial trend\":\"Tendenza iniziale\",\"First observation\":\"Prima osservazione\",\"Estimated from watch time\":\"Stima dal tempo di visualizzazione\",\"weekday open\":\"aperto nei giorni feriali\",\"time open\":\"tempo aperto\",\"Preview image\":\"Immagine di anteprima\",\"Biography / room subject\":\"Biografia / argomento della stanza\",\"Catalog state\":\"Stato del catalogo\",\"Live status\":\"Stato della diretta\",\"Viewer count\":\"Numero di spettatori\",\"Country/region\":\"Paese/regione\",\"Total watch time\":\"Tempo di visione totale\",\"Average session\":\"Sessione media\",\"First seen\":\"Visto per la prima volta\",\"Last seen\":\"Visto l’ultima volta\",\"Last live\":\"Ultima diretta\",\"Approximate online times\":\"Orari online approssimativi\",\"Activity, sources and profile state\":\"Attività, fonti e stato del profilo\",\"Example images\":\"Immagini di esempio\",\"Own groups\":\"Gruppi personali\",\"Public/recognized tags\":\"Tag pubblici/riconosciuti\",\"Notes, own tags and priority\":\"Note, tag personali e priorità\",\"Local rating\":\"Valutazione locale\",\"Group assignment\":\"Assegnazione ai gruppi\",\"Room and preview actions\":\"Azioni stanza e anteprima\",\"Library display\":\"Visualizzazione della libreria\",\"Saved in this browser\":\"Salvato in questo browser\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Scegli le dimensioni delle schede, i modelli per riga, il numero per pagina e quali dettagli mostrare in ogni scheda.\",\"Compact overview\":\"Panoramica compatta\",\"Balanced\":\"Bilanciato\",\"All details\":\"Tutti i dettagli\",\"Show all details\":\"Mostra tutti i dettagli\",\"Core details only\":\"Solo informazioni essenziali\",\"Card zoom\":\"Ingrandimento schede\",\"Visible information per model\":\"Informazioni visibili per modello\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Con le colonne automatiche, l’ingrandimento determina quanti modelli entrano affiancati. Un numero fisso di colonne ha la priorità. Tutte le scelte restano salvate nel browser.\",\"No custom tags yet\":\"Nessun tag personalizzato ancora\",\"Name of the new group\":\"Nome del nuovo gruppo\",\"Models remain in the library.\":\"I modelli rimangono nella biblioteca.\",\"Custom groups\":\"Gruppi personalizzati\",\"Create new group\":\"Crea nuovo gruppo\",\"Nothing selected — show all\":\"Niente selezionato: mostra tutto\",\"Any\":\"Qualunque\",\"Women\":\"Donne\",\"Men\":\"Uomini\",\"Couples\":\"Coppie\",\"Unknown\":\"Sconosciuto\",\"Error/loading\":\"Errore/caricamento\",\"Every weekday\":\"Tutti i giorni feriali\",\"Sunday\":\"Domenica\",\"Monday\":\"Lunedì\",\"Tuesday\":\"Martedì\",\"Wednesday\":\"Mercoledì\",\"Thursday\":\"Giovedì\",\"Friday\":\"Venerdì\",\"Saturday\":\"Sabato\",\"Every time\":\"Ogni volta\",\"Advanced filters and sorting\":\"Filtri e ordinamenti avanzati\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combina tutti i filtri che desideri. Più generi, stati, paesi e tag funzionano come selezioni multiple. Ogni impostazione rimane salvata in questo browser.\",\"Gender/type\":\"Genere/tipo\",\"Countries/regions (multiple)\":\"Paesi/regioni (diversi)\",\"Add country…\":\"Aggiungi paese...\",\"Public tags (multiple)\":\"Tag pubblici (multipli)\",\"Add tag…\":\"Aggiungi tag…\",\"Public-tag matching\":\"Corrispondenza del tag pubblico\",\"At least one tag\":\"Almeno un tag\",\"Every selected tag\":\"Ogni tag selezionato\",\"Catalog source\":\"Fonte del catalogo\",\"All models\":\"Tutti i modelli\",\"Personally organized only\":\"Solo organizzato personalmente\",\"Automatically discovered only\":\"Solo scoperto automaticamente\",\"Profile data\":\"Dati del profilo\",\"Loaded or missing\":\"Caricato o mancante\",\"Loaded only\":\"Solo caricato\",\"Missing only\":\"Manca solo\",\"Picture/category\":\"Immagine/categoria\",\"Neither HD nor gaming\":\"Né HD né gaming\",\"Watch history\":\"Guarda la storia\",\"Never watched\":\"Mai guardato\",\"Own tags\":\"Tag propri\",\"With or without\":\"Con o senza\",\"With tags only\":\"Solo con tag\",\"Without tags only\":\"Solo senza tag\",\"At least one group\":\"Almeno un gruppo\",\"No group\":\"Nessun gruppo\",\"With notes only\":\"Solo con appunti\",\"Without notes only\":\"Solo senza appunti\",\"Numeric ranges and minimums\":\"Intervalli numerici e minimi\",\"Minimum age\":\"Età minima\",\"Maximum age\":\"Età massima\",\"Minimum viewers\":\"Spettatori minimi\",\"Maximum viewers\":\"Numero massimo di spettatori\",\"Minimum followers\":\"Follower minimi\",\"Maximum followers\":\"Massimo follower\",\"Minimum total watch time\":\"Tempo di visualizzazione totale minimo\",\"Currently online for at least\":\"Attualmente online per almeno\",\"Minimum sessions\":\"Sessioni minime\",\"Minimum rating\":\"Voto minimo\",\"Minimum priority\":\"Priorità minima\",\"Active within\":\"Attivo dentro\",\"days\":\"giorni\",\"Minimum appearances\":\"Minime presenze\",\"Minimum live observations\":\"Osservazioni dal vivo minime\",\"Filter by typical online time\":\"Filtra per tempo online tipico\",\"Weekday\":\"Giorno feriale\",\"Hour (local time)\":\"Ora (ora locale)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"I campi zero o vuoti non significano alcun limite. Le età sconosciute sono escluse mentre è attivo un limite di età. I filtri temporali utilizzano i tempi in tempo reale osservati su questo dispositivo, tornando con cautela ai tempi di visualizzazione quando necessario.\",\"Reset all library filters\":\"Reimposta tutti i filtri della libreria\",\"Open live room\":\"Apri la sala dal vivo\",\"Open room page\":\"Apri la pagina della stanza\",\"Status\":\"Stato\",\"Open model room in a new tab\":\"Apri la stanza modello in una nuova scheda\",\"Open the Chaturbate single-model view in a new tab\":\"Apri la visualizzazione del modello singolo di Chaturbate in una nuova scheda\",\"Save profile\":\"Salva profilo\",\"Restore\":\"Ripristina\",\"Archive\":\"Archivio\",\"Advanced statistics filters\":\"Filtri statistici avanzati\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Ogni filtro può essere combinato; paesi, tag, gruppi e tipi supportano la selezione multipla. Le scelte vengono salvate in modo permanente.\",\"Own groups (multiple)\":\"Propri gruppi (multipli)\",\"Add group…\":\"Aggiungi gruppo…\",\"Tag matching\":\"Corrispondenza dei tag\",\"At least one\":\"Almeno uno\",\"All selected tags\":\"Tutti i tag selezionati\",\"Group matching\":\"Corrispondenza del gruppo\",\"All selected groups\":\"Tutti i gruppi selezionati\",\"Cover/example image\":\"Immagine di copertina/esempio\",\"Known public image available\":\"Immagine pubblica conosciuta disponibile\",\"No known image\":\"Nessuna immagine conosciuta\",\"Currently live\":\"Attualmente in diretta\",\"Maximum total watch time\":\"Tempo di visualizzazione totale massimo\",\"Maximum sessions\":\"Sessioni massime\",\"Minimum active days\":\"Giorni attivi minimi\",\"Minimum previews\":\"Anteprime minime\",\"Minimum quick views\":\"Visualizzazioni rapide minime\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"I campi zero o vuoti non significano alcun limite. I valori sconosciuti non vengono mai indovinati; gli intervalli di età escludono età sconosciute.\",\"Reset all statistics filters\":\"Reimposta tutti i filtri statistici\",\"Search name, tags or notes…\":\"Cerca nome, tag o note...\",\"Search\":\"Cerca\",\"Sorting\":\"Ordinamento\",\"Most watch time\":\"La maggior parte del tempo di visualizzazione\",\"Least watch time\":\"Minimo tempo di visualizzazione\",\"Recently active\":\"Attivo di recente\",\"Longest inactive\":\"Inattivo da più tempo\",\"Most sessions\":\"La maggior parte delle sessioni\",\"Fewest sessions\":\"Meno sessioni\",\"Longest average session\":\"Sessione media più lunga\",\"Shortest average session\":\"Sessione media più breve\",\"Longest single session\":\"Sessione singola più lunga\",\"Most active days\":\"Giorni più attivi\",\"Highest watch share\":\"Quota di visualizzazioni più alta\",\"Most previews\":\"La maggior parte delle anteprime\",\"Most quick views\":\"Viste più veloci\",\"Most discoveries\":\"La maggior parte delle scoperte\",\"Most followers\":\"La maggior parte dei follower\",\"Youngest first\":\"Prima il più giovane\",\"Oldest first\":\"Prima il più vecchio\",\"Highest rating\":\"Voto più alto\",\"Highest priority\":\"Massima priorità\",\"Most appearances\":\"La maggior parte delle apparizioni\",\"Most live observations\":\"La maggior parte delle osservazioni dal vivo\",\"First observed\":\"Primo osservato\",\"Country A–Z\":\"Paese dalla A alla Z\",\"Most public tags\":\"La maggior parte dei tag pubblici\",\"Most groups\":\"La maggior parte dei gruppi\",\"Best technical quality\":\"La migliore qualità tecnica\",\"Cover/example images first\":\"Prima le immagini di copertina/esempio\",\"All statuses\":\"Tutti gli stati\",\"Live only\":\"Solo vivere\",\"Library only\":\"Solo biblioteca\",\"View\":\"Vista\",\"Active library\":\"Biblioteca attiva\",\"Live now\":\"Vivi adesso\",\"With notes\":\"Con note\",\"Newest saved/discovered\":\"Ultimo salvato/scoperto\",\"Oldest saved/discovered\":\"Il più vecchio salvato/scoperto\",\"Lowest rating\":\"Voto più basso\",\"Lowest priority\":\"Priorità più bassa\",\"Live longest now\":\"Vivi più a lungo adesso\",\"Age youngest first\":\"Prima l'età del più giovane\",\"Age oldest first\":\"Prima l'età più vecchia\",\"Most recently live\":\"Più recentemente in diretta\",\"First discovered\":\"Scoperto per primo\",\"Most own groups\":\"La maggior parte dei propri gruppi\",\"Card size\":\"Formato della carta\",\"Compact\":\"Compatto\",\"Comfortable\":\"Confortevole\",\"Large\":\"Grande\",\"Extra large\":\"Molto grande\",\"Models per row\":\"Modelli per riga\",\"Automatic for window\":\"Automatico per la finestra\",\"Recorded models\":\"Modelli registrati\",\"Total cam time\":\"Tempo totale della camma\",\"across all models\":\"su tutti i modelli\",\"Dock + hover\":\"Dock + passaggio del mouse\",\"Last 30 days\":\"Ultimi 30 giorni\",\"local events\":\"eventi locali\",\"Top model\":\"Modello di punta\",\"no watch time yet\":\"nessun tempo di visualizzazione ancora\",\"Deeper analysis\":\"Analisi più approfondita\",\"Open Analytics Studio\":\"Apri Analytics Studio\",\"Late night\":\"A tarda notte\",\"Morning\":\"Mattina\",\"Daytime\":\"Diurno\",\"Evening\":\"Sera\",\"Gaming\":\"Gioco\",\"Music & DJ\":\"Musica e DJ\",\"Dance\":\"Danza\",\"Cosplay & anime\":\"Cosplay e anime\",\"Fitness & sports\":\"Fitness e sport\",\"Talk & social\":\"Conversazione e socialità\",\"Outdoor & travel\":\"All'aperto e in viaggio\",\"Roleplay\":\"Gioco di ruolo\",\"Performance\":\"Prestazioni\",\"My interest statistics\":\"Le mie statistiche sugli interessi\",\"All local data\":\"Tutti i dati locali\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Calcolato in base al tempo di visualizzazione locale effettivo, alle sessioni, alle anteprime, alle visualizzazioni rapide, ai preferiti e alle informazioni sui modelli pubblici attualmente conosciuti.\",\"Favorite tag\":\"Tag preferito\",\"not enough data\":\"dati non sufficienti\",\"Favorite genre\":\"Genere preferito\",\"from public tags\":\"dai tag pubblici\",\"Most-watched age\":\"Età più guardata\",\"no age data\":\"nessun dato sull'età\",\"Most-watched country\":\"Paese più guardato\",\"no country data\":\"nessun dato nazionale\",\"Favorite model type\":\"Tipo di modello preferito\",\"no type data\":\"nessun tipo di dato\",\"Favorite viewing time\":\"Orario di visione preferito\",\"no time data\":\"nessun dato temporale\",\"Top-three concentration\":\"Le prime tre concentrazioni\",\"watch time on the top three models\":\"tempo di visualizzazione sui primi tre modelli\",\"Favorite affinity\":\"Affinità preferita\",\"watch time on current favorites\":\"guardare il tempo sui preferiti attuali\",\"watch time on HD models\":\"tempo di visualizzazione sui modelli HD\",\"Metadata coverage\":\"Copertura dei metadati\",\"age\":\"età\",\"country\":\"paese\",\"Tags, genres and browsing interests\":\"Tag, generi e interessi di esplorazione\",\"Tags by watch time\":\"Tag in base al tempo di visualizzazione\",\"Your ranking appears when watched models have public tags.\":\"La tua classifica appare quando i modelli guardati hanno tag pubblici.\",\"Favorite genre worlds\":\"Mondi di genere preferiti\",\"No genre world can yet be assigned from public tags.\":\"Nessun mondo di genere può ancora essere assegnato dai tag pubblici.\",\"Your own tags\":\"I tuoi tag\",\"Your saved custom tags are also evaluated here by watch time.\":\"I tag personalizzati salvati vengono valutati qui anche in base al tempo di visualizzazione.\",\"Preview-interest tags\":\"Tag di interesse in anteprima\",\"Browsing interests appear when you use live previews or quick views.\":\"Gli interessi di navigazione vengono visualizzati quando utilizzi le anteprime in tempo reale o le visualizzazioni rapide.\",\"Most-previewed models\":\"I modelli più visualizzati in anteprima\",\"No previews have been recorded in this period yet.\":\"Non sono state ancora registrate anteprime in questo periodo.\",\"Age, origin and model type\":\"Età, provenienza e tipo di modello\",\"Age groups by watch time\":\"Gruppi di età in base al tempo di visualizzazione\",\"Not enough public age data is available.\":\"Non sono disponibili dati sufficienti sull'età pubblica.\",\"Countries by watch time\":\"Paesi in base al tempo di visualizzazione\",\"Not enough public country data is available.\":\"Non sono disponibili dati pubblici sufficienti sui paesi.\",\"Most-watched places\":\"I luoghi più visti\",\"Not enough public location data is available.\":\"Non sono disponibili dati sufficienti sulla posizione pubblica.\",\"Model types by watch time\":\"Tipi di modelli in base al tempo di visualizzazione\",\"Not enough public model-type data is available.\":\"Non sono disponibili dati sufficienti sul tipo di modello pubblico.\",\"Viewing rhythm and favorite signals\":\"Ritmo di visione e segnali dei preferiti\",\"Most active weekdays\":\"Giorni feriali più attivi\",\"Not enough visit events are available.\":\"Non sono disponibili abbastanza eventi di visita.\",\"Times of day\":\"Momenti della giornata\",\"Tags among favorites\":\"Tag tra i preferiti\",\"Preference signals appear when you mark models as favorites.\":\"I segnali di preferenza vengono visualizzati quando si contrassegnano i modelli come preferiti.\",\"Countries among favorites\":\"Paesi tra i favoriti\",\"No known countries among favorites yet.\":\"Nessun paese conosciuto ancora tra i preferiti.\",\"Age groups among favorites\":\"Fasce d'età tra le favorite\",\"No known ages among favorites yet.\":\"Non è ancora nota l'età tra i preferiti.\",\"Model types among favorites\":\"Tipi di modelli tra i preferiti\",\"No known model types among favorites yet.\":\"Nessun tipo di modello noto ancora tra i preferiti.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Questi risultati non vengono inviati a un server. Ai fini delle preferenze di visualizzazione viene conteggiato solo il tempo di visualizzazione effettivo; le anteprime e i preferiti vengono visualizzati separatamente. I dati mancanti su età, paese, posizione o tag non vengono mai indovinati.\",\"Open Following on Chaturbate\":\"Apri Segui su Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Un tempo di visualizzazione completo o una cronologia delle visite non possono essere recuperati retroattivamente perché Chaturbate non fornisce alcuna API di cronologia personale documentata. Puoi richiedere una copia dei tuoi dati personali memorizzati tramite l'esportazione dati ufficiale di Chaturbate. Se l'esportazione contiene campi di cronologia utilizzabili, RoomGrid può supportare un'importazione sicura dopo aver convalidato il formato del file. L'API ufficiale delle statistiche fornisce i valori attuali dell'emittente; RoomGrid può costruire la propria cronologia solo dalla prima connessione in poi.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Confronta ogni modello osservato, salvato e guardato localmente, con tempo di visualizzazione, sessioni, attività, anteprime, qualità e classifiche.\",\"Large model library\":\"Ampia libreria di modelli\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"I tuoi modelli come profili pratici e ricchi di immagini con stato in tempo reale, note, tag, valutazioni, priorità, gruppi e azioni dirette.\",\"My user dashboard\":\"La mia dashboard utente\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"La tua cronologia privata di utilizzo locale, le tue abitudini, i dati della libreria e, solo se connesso deliberatamente, i dati dell'account ufficiale dell'emittente.\",\"🔒 Local & private on this device\":\"🔒 Locale e privato su questo dispositivo\",\"Preparing data…\":\"Preparazione dei dati...\",\"RoomGrid Comfort Center\":\"Centro comfort RoomGrid\",\"inside the standard Chaturbate page\":\"nella pagina standard di Chaturbate\",\"All RoomGrid pages and settings\":\"Tutte le pagine e le impostazioni di RoomGrid\",\"Media paused; names and images are covered.\":\"I media sono stati messi in pausa; nomi e immagini sono coperti.\",\"Restore previous view\":\"Ripristina la visualizzazione precedente\",\"Enter local PIN\":\"Inserisci il PIN locale\",\"In-page notifications\":\"Notifiche in-page\",\"No new notifications\":\"Nessuna nuova notifica\",\"Notifications are off\":\"Le notifiche sono disattivate\",\"RoomGrid notification\":\"Notifica RoomGrid\",\"Dismiss as read\":\"Ignora come letto\",\"More unread notifications\":\"Altre notifiche non lette\",\"RoomGrid update\":\"Aggiornamento di RoomGrid\",\"A monitored source changed status.\":\"Una fonte monitorata ha cambiato stato.\",\"This is a local RoomGrid test notification.\":\"Questa è una notifica di test RoomGrid locale.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Test riuscito: le notifiche possono apparire direttamente nella barra RoomGrid.\",\"Settings\":\"Impostazioni\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Aree di lavoro, libreria, progettazione, dati, cronologia, notifiche, privacy e prestazioni\",\"Workspaces\":\"Spazi di lavoro\",\"Data\":\"Dati\",\"History & statistics\":\"Storia e statistiche\",\"Broadcaster stats\":\"Statistiche dell'emittente\",\"Notifications\":\"Notifiche\",\"Windows & performance\":\"Finestre e prestazioni\",\"Optional tools\":\"Strumenti opzionali\",\"Settings pages\":\"Pagine delle impostazioni\",\"RoomGrid control center\":\"Centro di controllo RoomGrid\",\"Any online model\":\"Qualsiasi modello online\",\"Workspace name\":\"Nome dell'area di lavoro\",\"Optional description\":\"Descrizione facoltativa\",\"Active\":\"Attivo\",\"No description\":\"Nessuna descrizione\",\"Name\":\"Nome\",\"Description\":\"Descrizione\",\"Replace offline models\":\"Sostituisci i modelli offline\",\"Replacement group\":\"Gruppo sostitutivo\",\"Delay seconds\":\"Ritardo secondi\",\"Save current state\":\"Salva lo stato attuale\",\"New window\":\"Nuova finestra\",\"Export this workspace\":\"Esporta questo spazio di lavoro\",\"Duplicate\":\"Duplicato\",\"Workspace restored\":\"Area di lavoro ripristinata\",\"Workspace archived\":\"Area di lavoro archiviata\",\"Delete permanently\":\"Elimina definitivamente\",\"Permanently delete this workspace?\":\"Eliminare definitivamente questo spazio di lavoro?\",\"Saved layout and panel sizes\":\"Layout e dimensioni del pannello salvati\",\"Apply\":\"Applicare\",\"Layout template deleted\":\"Modello di layout eliminato\",\"Parent group\":\"Gruppo genitore\",\"None\":\"Nessuno\",\"Group description\":\"Descrizione del gruppo\",\"Save current workspace\":\"Salva l'area di lavoro corrente\",\"Save layout template\":\"Salva modello di layout\",\"Template name\":\"Nome del modello\",\"Layout templates\":\"Modelli di layout\",\"Device layout\":\"Disposizione del dispositivo\",\"Groups and subgroups\":\"Gruppi e sottogruppi\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Ogni area di lavoro memorizza desktop separati e layout compatti. La sostituzione automatica è disattivata per impostazione predefinita e riempie gli slot offline solo dopo averla abilitata per quell'area di lavoro.\",\"No templates yet.\":\"Nessun modello ancora.\",\"Group name\":\"Nome del gruppo\",\"New subgroup\":\"Nuovo sottogruppo\",\"Enter parent group name\":\"Inserisci il nome del gruppo principale\",\"Subgroup name\":\"Nome del sottogruppo\",\"Name, note or local tag\":\"Nome, nota o tag locale\",\"Choose group…\":\"Scegli gruppo...\",\"Add selected models to group\":\"Aggiungi i modelli selezionati al gruppo\",\"Watch later\":\"Guarda più tardi\",\"Paused\":\"In pausa\",\"Collection name\":\"Nome della raccolta\",\"Tags, comma separated\":\"Tag separati da virgole\",\"Countries, comma separated\":\"Paesi, separati da virgole\",\"All types\":\"Tutti i tipi\",\"Rule name\":\"Nome della regola\",\"Required tags\":\"Tag richiesti\",\"Local alert\":\"Avviso locale\",\"Add to favorites\":\"Aggiungi ai preferiti\",\"Add to group\":\"Aggiungi al gruppo\",\"Any tag\":\"Qualsiasi etichetta\",\"Any country\":\"Qualsiasi paese\",\"Pause\":\"Pausa\",\"Enable\":\"Abilita\",\"Delete\":\"Elimina\",\"Automation rule deleted\":\"Regola di automazione eliminata\",\"Select model\":\"Seleziona modello\",\"Later\":\"Più tardi\",\"Never visited\":\"Mai visitato\",\"Last online\":\"L'ultima on-line\",\"Last visited\":\"Ultima visita\",\"Local note\":\"Nota locale\",\"Local tags, comma separated\":\"Tag locali, separati da virgole\",\"Remove later\":\"Rimuovi più tardi\",\"Hide today\":\"Nasconditi oggi\",\"Pause 7 days\":\"Pausa 7 giorni\",\"Never suggest\":\"Non suggerire mai\",\"Select results\":\"Seleziona i risultati\",\"Clear selection\":\"Cancella selezione\",\"selected\":\"selezionato\",\"Add to watch later\":\"Aggiungi per guardarlo più tardi\",\"Permanently delete selected models?\":\"Eliminare definitivamente i modelli selezionati?\",\"Smart collections\":\"Collezioni intelligenti\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Le raccolte si aggiornano automaticamente man mano che i metadati e le statistiche locali crescono; i modelli non sono duplicati.\",\"Tags\":\"Tag\",\"Countries\":\"Paesi\",\"Favorites only\":\"Solo preferiti\",\"Online only\":\"Solo on-line\",\"Save smart collection\":\"Salva la raccolta intelligente\",\"Delete current collection\":\"Elimina la raccolta corrente\",\"Delete this smart collection? Model data stays unchanged.\":\"Eliminare questa raccolta intelligente? I dati del modello rimangono invariati.\",\"Rules & automation center\":\"Centro regole e automazione\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Quando i dati del modello locale corrispondono, mostra un avviso, aggiungi un preferito o aggiungi il modello a un gruppo scelto.\",\"Action\":\"Azione\",\"Target group\":\"Gruppo target\",\"Save rule\":\"Salva regola\",\"No automation rules yet.\":\"Nessuna regola di automazione ancora.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"I nomi utente vengono deduplicati in base alla corrispondenza esatta normalizzata. La ricerca copre nomi, note e tag locali. L'archiviazione, la rimozione da una visualizzazione e l'eliminazione permanente rimangono azioni separate.\",\"No matching models.\":\"Nessun modello corrispondente.\",\"export\":\"esportazione\",\"Models and notes\":\"Modelli e note\",\"This backup could not be read. Current data was not changed.\":\"Impossibile leggere questo backup. I dati attuali non sono stati modificati.\",\"Import preview (nothing changed yet)\":\"Anteprima di importazione (ancora nulla è cambiato)\",\"conflicts\":\"conflitti\",\"Includes sensitive statistics\":\"Include statistiche sensibili\",\"models\":\"modelli\",\"events\":\"eventi\",\"quality samples\":\"campioni di qualità\",\"Import statistics too\":\"Importa anche le statistiche\",\"Import method\":\"Metodo di importazione\",\"Merge (recommended)\":\"Unisci (consigliato)\",\"Replace selected sections\":\"Sostituisci le sezioni selezionate\",\"Apply import\":\"Applica l'importazione\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Importazione completata. La configurazione e le statistiche precedenti sono state sottoposte a backup separatamente.\",\"The import could not be saved safely. The backup remains available.\":\"Non è stato possibile salvare l'importazione in modo sicuro. Il backup rimane disponibile.\",\"Cancel preview\":\"Annulla l'anteprima\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Questo file contiene dati di modello, nota o area di lavoro e deve essere trattato come sensibile.\",\"This import is not marked sensitive.\":\"Questa importazione non è contrassegnata come sensibile.\",\"Data version\":\"Versione dei dati\",\"Software version\":\"Versione del software\",\"Revision\":\"Revisione\",\"Local models\":\"Modelli locali\",\"Selective export\":\"Esportazione selettiva\",\"Export selected\":\"Esporta selezionato\",\"Full export\":\"Esportazione completa\",\"Complete safety package\":\"Pacchetto di sicurezza completo\",\"Export statistics separately\":\"Esporta le statistiche separatamente\",\"Choose file and preview\":\"Scegli il file e visualizza l'anteprima\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Il pacchetto di sicurezza completo contiene configurazione e statistiche sensibili, ma mai il token statistico ufficiale. Ogni importazione viene visualizzata in anteprima ed esegue il backup della configurazione e delle statistiche separatamente prima dell'applicazione.\",\"Storage and backup center\":\"Centro di archiviazione e backup\",\"Local storage\":\"Archiviazione locale\",\"Site storage\":\"Archiviazione del sito\",\"Available quota\":\"Quota disponibile\",\"Internal statistics backups\":\"Backup delle statistiche interne\",\"Configuration\":\"Configurazione\",\"workspaces\":\"spazi di lavoro\",\"LocalStorage + update vault\":\"LocalStorage + archivio di aggiornamento\",\"Statistics database\":\"Banca dati statistica\",\"Secrets\":\"Segreti\",\"The official statistics token is never included in normal or complete backups.\":\"Il token delle statistiche ufficiali non viene mai incluso nei backup normali o completi.\",\"Excluded\":\"Escluso\",\"Create configuration restore point\":\"Crea punto di ripristino della configurazione\",\"Create statistics restore point\":\"Crea punto di ripristino delle statistiche\",\"Legacy cleanup\":\"Pulizia dell'eredità\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"I dati legacy non vengono mai eliminati automaticamente. Rimuovere le vecchie chiavi v6/v7 solo dopo una revisione riuscita.\",\"Remove old v6/v7 copies\":\"Rimuovi le vecchie copie v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Rimuovere solo le vecchie copie v6/v7? I dati attuali e i backup rimangono.\",\"items removed\":\"elementi rimossi\",\"Blue and teal, the RoomGrid signature\":\"Blu e verde acqua, la firma di RoomGrid\",\"Ember\":\"Brace\",\"Warm orange and rose\":\"Arancio caldo e rosa\",\"Frost\":\"Gelo\",\"Crisp cyan and violet\":\"Ciano e viola nitidi\",\"Custom\":\"Personalizzato\",\"Use the colors selected below\":\"Utilizza i colori selezionati di seguito\",\"Name the current appearance\":\"Dai un nome all'aspetto attuale\",\"My appearance\":\"Il mio aspetto\",\"Delete appearance profile\":\"Elimina il profilo dell'aspetto\",\"This appearance profile could not be imported.\":\"Impossibile importare questo profilo aspetto.\",\"Light\":\"Luce\",\"Dark\":\"Buio\",\"Export\":\"Esportazione\",\"Home\":\"Casa\",\"Listings and tags\":\"Elenchi e tag\",\"Model pages\":\"Pagine del modello\",\"Use global appearance\":\"Utilizza l'aspetto globale\",\"This logo format is unsafe or unsupported.\":\"Questo formato del logo non è sicuro o non è supportato.\",\"The logo file could not be read.\":\"Impossibile leggere il file del logo.\",\"MultiCam tools\":\"Strumenti MultiCam\",\"Page\":\"Pagina\",\"Panel\":\"Pannello\",\"Card surface\":\"Superficie della carta\",\"Input\":\"Ingresso\",\"Border\":\"Confine\",\"Text\":\"Testo\",\"Muted text\":\"Testo disattivato\",\"Success\":\"Successo\",\"Warning\":\"Avvertimento\",\"Danger\":\"Pericolo\",\"Info and focus\":\"Informazioni e focus\",\"Data hub\":\"Centro dati\",\"Text / page\":\"Testo/pagina\",\"Text / panel\":\"Testo/pannello\",\"Muted text / panel\":\"Testo/pannello disattivato\",\"Accent text / accent\":\"Accento testo/accento\",\"Tablet\":\"Tavoletta\",\"Mobile\":\"Cellulare\",\"Brand and logo studio\":\"Studio di marchi e loghi\",\"Monogram\":\"Monogramma\",\"Tagline\":\"Slogan\",\"Brand display\":\"Visualizzazione del marchio\",\"Full name\":\"Nome completo\",\"Uploaded logo\":\"Logo caricato\",\"Logo shape\":\"Forma del logo\",\"Rounded\":\"Arrotondato\",\"Circle\":\"Cerchio\",\"Square\":\"Quadrato\",\"Pill\":\"Pillola\",\"Page title\":\"Titolo della pagina\",\"Website original\":\"Originale del sito\",\"Brand plus original\":\"Marca più originale\",\"Custom title\":\"Titolo personalizzato\",\"Custom page title\":\"Titolo della pagina personalizzato\",\"Image fit\":\"Adattamento dell'immagine\",\"Horizontal position\":\"Posizione orizzontale\",\"Vertical position\":\"Posizione verticale\",\"Upload light-mode logo\":\"Carica il logo della modalità luce\",\"Upload dark-mode logo\":\"Carica il logo in modalità oscura\",\"Remove logos\":\"Rimuovere i loghi\",\"Show version\":\"Mostra versione\",\"Use logo as tab icon on RoomGrid pages\":\"Utilizza il logo come icona della scheda nelle pagine RoomGrid\",\"Replace website logo in the original header (local only)\":\"Sostituisci il logo del sito web nell'intestazione originale (solo locale)\",\"Complete color system\":\"Sistema colore completo\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Controlla i colori di pagina, pannello, scheda, input, bordo, testo e stato oltre agli accenti.\",\"Derived automatically\":\"Derivato automaticamente\",\"Fully custom\":\"Completamente personalizzato\",\"Editing dark palette\":\"Modifica della tavolozza scura\",\"Editing light palette\":\"Modifica della tavolozza delle luci\",\"Reset current palette\":\"Reimposta la tavolozza corrente\",\"Layout editor\":\"Redattore del layout\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Imposta la spaziatura del contenuto, la larghezza della scheda, l'intestazione e la barra laterale in modo coerente su tutte le pagine RoomGrid.\",\"Minimum card width\":\"Larghezza minima della carta\",\"Section spacing\":\"Spaziatura delle sezioni\",\"Content padding\":\"Imbottitura del contenuto\",\"Header height\":\"Altezza dell'intestazione\",\"Maximum columns\":\"Colonne massime\",\"Automatic\":\"Automatico\",\"Card aspect\":\"Aspetto della carta\",\"Sidebar position\":\"Posizione della barra laterale\",\"Left\":\"A sinistra\",\"Right\":\"Giusto\",\"Keep header visible while scrolling\":\"Mantieni l'intestazione visibile durante lo scorrimento\",\"Live device preview\":\"Anteprima del dispositivo in tempo reale\",\"Contrast and accessibility\":\"Contrasto e accessibilità\",\"High-contrast mode\":\"Modalità ad alto contrasto\",\"Underline links\":\"Sottolinea i collegamenti\",\"Reduce transparency\":\"Ridurre la trasparenza\",\"Boost base text size\":\"Aumenta la dimensione del testo di base\",\"Focus ring width\":\"Larghezza dell'anello di messa a fuoco\",\"Minimum target size\":\"Dimensione minima dell'obiettivo\",\"Respect system reduced-motion setting\":\"Rispettare l'impostazione del movimento ridotto del sistema\",\"Apply accessible defaults\":\"Applicare impostazioni predefinite accessibili\",\"Profiles by page and screen size\":\"Profili per pagina e dimensioni dello schermo\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Salva uno o più aspetti, quindi assegnali per tipo di pagina per desktop, tablet e dispositivi mobili. I profili cambiano automaticamente quando cambiano le dimensioni dello schermo.\",\"Enable responsive design profiles\":\"Abilita i profili di progettazione reattiva\",\"Mobile maximum width\":\"Larghezza massima mobile\",\"Tablet maximum width\":\"Larghezza massima della tavoletta\",\"RoomGrid design system\":\"Sistema di progettazione RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Scegli la direzione del marchio, la larghezza del contenuto e la densità delle informazioni sulla carta. Ogni pagina segue immediatamente.\",\"Primary accent\":\"Accento primario\",\"Secondary accent\":\"Accento secondario\",\"Content width\":\"Larghezza del contenuto\",\"Centered\":\"Centrato\",\"Wide\":\"Largo\",\"Full width\":\"Tutta la larghezza\",\"Card style\":\"Stile della carta\",\"Visual\":\"Visivo\",\"Data-rich\":\"Ricco di dati\",\"Widget density\":\"Densità dei widget\",\"Spacious\":\"Spazioso\",\"Theme and surfaces\":\"Tema e superfici\",\"Surface style\":\"Stile di superficie\",\"Soft\":\"Morbido\",\"Flat\":\"Piatto\",\"Glass\":\"Vetro\",\"Page background\":\"Sfondo della pagina\",\"Solid\":\"Solido\",\"Gradient\":\"Gradiente\",\"Ambient glow\":\"Bagliore ambientale\",\"Surface opacity\":\"Opacità della superficie\",\"Custom accent color\":\"Colore accento personalizzato\",\"Sizing and motion\":\"Dimensionamento e movimento\",\"Font style\":\"Stile del carattere\",\"System\":\"Sistema\",\"Classic serif\":\"Serif classico\",\"Monospace\":\"Monospazio\",\"Density\":\"Densità\",\"Font scale\":\"Scala dei caratteri\",\"Control size\":\"Controllare le dimensioni\",\"Corner radius\":\"Raggio dell'angolo\",\"Card spacing\":\"Spaziatura delle carte\",\"Shadow strength\":\"La forza dell'ombra\",\"Sidebar width\":\"Larghezza della barra laterale\",\"Enable animations\":\"Abilita animazioni\",\"Appearance profiles\":\"Profili estetici\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Salva l'aspetto completo come profilo riutilizzabile. I profili rimangono disponibili dopo gli aggiornamenti dello script.\",\"Save current appearance\":\"Salva l'aspetto corrente\",\"Import profile\":\"Importa profilo\",\"No profiles saved yet.\":\"Nessun profilo ancora salvato.\",\"Instant preview\":\"Anteprima istantanea\",\"Example card\":\"Carta di esempio\",\"This is the combined result of the current visual settings.\":\"Questo è il risultato combinato delle attuali impostazioni visive.\",\"Example action\":\"Azione di esempio\",\"Per-page design profiles\":\"Profili di progettazione per pagina\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Scegli un aspetto salvato diverso per Home, elenchi, pagine modello, MultiCam, Discover e hub dati.\",\"Enable per-page design\":\"Abilita la progettazione per pagina\",\"Normal website pages\":\"Pagine normali del sito web\",\"Enable RoomGrid page design\":\"Abilita il design della pagina RoomGrid\",\"Tag and category pages\":\"Pagine di tag e categorie\",\"Cards\":\"Carte\",\"List\":\"Elenco\",\"Page density\":\"Densità della pagina\",\"Polish navigation\":\"Navigazione polacca\",\"Polish sidebars\":\"Barre laterali polacche\",\"Hide recognized promo and merch areas\":\"Nascondi le aree promozionali e di merchandising riconosciute\",\"Compact footer\":\"Piè di pagina compatto\",\"Mark saved models\":\"Contrassegna i modelli salvati\",\"Mark favorites separately\":\"Contrassegna i preferiti separatamente\",\"Mark visited models\":\"Mark ha visitato le modelle\",\"Show card quick actions\":\"Mostra le azioni rapide della scheda\",\"Animated live previews on home and listing pages\":\"Anteprime live animate sulla home page e sulle pagine degli annunci\",\"Preview refresh rate\":\"Frequenza di aggiornamento dell'anteprima\",\"Off\":\"Spento\",\"Update mode\":\"Modalità di aggiornamento\",\"All visible cards simultaneously\":\"Tutte le carte visibili contemporaneamente\",\"Performance-bounded batches\":\"Batch limitati dalle prestazioni\",\"Batch concurrency limit\":\"Limite di concorrenza batch\",\"All visible\":\"Tutto visibile\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Solo le schede visibili ricevono aggiornamenti dei fotogrammi JPEG disattivati; si fermano immediatamente fuori dallo schermo o in una scheda nascosta. La modalità sincronizzata aggiorna ogni carta visibile in un'ondata; velocità elevate aumentano notevolmente la larghezza di banda e l'utilizzo della CPU.\",\"Live video on hover\":\"Video in diretta al passaggio del mouse\",\"Play real-time video while hovering\":\"Riproduci video in tempo reale mentre passi il mouse\",\"Start delay\":\"Inizio ritardo\",\"Immediately\":\"Immediatamente\",\"Hover enlargement\":\"Ingrandimento al passaggio del mouse\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Viene riprodotta solo la carta sotto il puntatore, sempre in modalità silenziosa. Allontanandosi o nascondendo la scheda si interrompe immediatamente lo streaming.\",\"Live Preview Dock\":\"Dock di anteprima dal vivo\",\"Enable low-load live previews\":\"Abilita anteprime live a basso carico\",\"Concurrent previews\":\"Anteprime simultanee\",\"Window size\":\"Dimensione della finestra\",\"Small\":\"Piccolo\",\"Medium\":\"Medio\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Le anteprime iniziano con l'audio disattivato e vengono messe in pausa nelle schede nascoste. L'audio richiede sempre un clic esplicito.\",\"Quick info & chat focus\":\"Informazioni rapide e focus sulla chat\",\"Enable model Quick View\":\"Abilita la visualizzazione rapida del modello\",\"Chat focus mode (opt-in)\":\"Modalità focus chat (attivazione)\",\"Show local timestamps\":\"Mostra timestamp locali\",\"System messages\":\"Messaggi di sistema\",\"Show\":\"Mostra\",\"Dim\":\"Fioco\",\"Hide\":\"Nasconditi\",\"Chat filter strength\":\"Intensità del filtro della chat\",\"Low\":\"Basso\",\"Strict\":\"Rigoroso\",\"Highlight words (comma separated)\":\"Evidenzia parole (separate da virgole)\",\"Muted words (comma separated)\":\"Parole disattivate (separate da virgole)\",\"Collapse repeated messages\":\"Comprimi i messaggi ripetuti\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"I miglioramenti della chat modificano solo la visualizzazione corrente. Non memorizzano mai chat, inviano messaggi o danno mance automaticamente.\",\"Shortcuts\":\"Scorciatoie\",\"Command menu\":\"Menù comandi\",\"Global model search\":\"Ricerca globale del modello\",\"Reset design defaults\":\"Ripristina le impostazioni predefinite del progetto\",\"Optional modules\":\"Moduli opzionali\",\"Normal-page dock and card actions\":\"Dock della pagina normale e azioni delle carte\",\"Real audio meters\":\"Misuratori audio reali\",\"Media, app and contest directories\":\"Directory di media, app e concorsi\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Le opzioni del modulo diventano effettive dopo aver ricaricato la pagina. I dati principali e i moduli di backup rimangono abilitati per prevenire la perdita di dati.\",\"Name the current filters and sorting\":\"Assegna un nome ai filtri e all'ordinamento correnti\",\"My smart view\":\"La mia visione intelligente\",\"Delete smart view\":\"Elimina la visualizzazione intelligente\",\"Smart view deleted\":\"Visualizzazione intelligente eliminata\",\"tags\":\"tag\",\"countries\":\"paesi\",\"Saved selection\":\"Selezione salvata\",\"Suggestions\":\"Suggerimenti\",\"Selected\":\"Selezionato\",\"Website tags\":\"Tag del sito web\",\"Countries (multiple selection)\":\"Paesi (selezione multipla)\",\"Select countries\":\"Seleziona i paesi\",\"Search countries…\":\"Cerca paesi…\",\"Recognized in current models\":\"Riconosciuto nei modelli attuali\",\"All countries\":\"Tutti i paesi\",\"Tags (multiple selection)\":\"Tag (selezione multipla)\",\"Select tags\":\"Seleziona i tag\",\"Search tags…\":\"Cerca tag…\",\"Room subject\":\"Argomento della stanza\",\"Current online duration\":\"Durata online attuale\",\"Local watch time and sessions\":\"Tempo di visione locale e sessioni\",\"HD, new, gaming and favorite badges\":\"Badge HD, nuovo, gaming e preferito\",\"Homepage Studio\":\"Studio della pagina iniziale\",\"Balanced preset\":\"Preset bilanciato\",\"Compact preset\":\"Preset compatto\",\"Data-rich preset\":\"Preset ricco di dati\",\"Cinema preset\":\"Preset cinema\",\"Layout and pagination\":\"Layout e paginazione\",\"Show quick toolbar above models\":\"Mostra la barra rapida sopra i modelli\",\"Cards per row\":\"Schede per riga\",\"Image ratio\":\"Proporzioni immagine\",\"Information display\":\"Visualizzazione delle informazioni\",\"Detailed\":\"Dettagliato\",\"Overlay on image\":\"Sovrapposto all’immagine\",\"Powerful sorting\":\"Ordinamento avanzato\",\"Primary sort\":\"Ordinamento primario\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"L’ordinamento modifica solo l’ordine di visualizzazione delle schede caricate. Non cambia i seguiti o i preferiti del tuo account Chaturbate.\",\"Restore website order\":\"Ripristina l’ordine del sito\",\"Clear all filters\":\"Cancella tutti i filtri\",\"Search and range filters\":\"Ricerca e filtri per intervallo\",\"When several tags are selected\":\"Quando sono selezionati più tag\",\"Model must contain every tag\":\"Il modello deve contenere tutti i tag\",\"At least one tag matches\":\"Almeno un tag corrisponde\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Il filtraggio per paese mostra i modelli di qualsiasi paese selezionato. I tag propri e quelli dei siti Web riconosciuti rimangono disponibili nell'elenco e possono essere trovati rapidamente con la ricerca.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Dopo aver selezionato tag o paesi, RoomGrid cerca l'intero catalogo live corrente. La stessa pagina dei risultati mostra inizialmente 120 corrispondenze e ne carica altre in blocchi durante lo scorrimento, quindi anche i tag molto comuni rimangono reattivi. Il pulsante Ricarica nella barra degli strumenti della home page aggiorna l'elenco live.\",\"Quick conditions\":\"Condizioni rapide\",\"HD models only\":\"Solo modelli HD\",\"New models only\":\"Solo nuovi modelli\",\"Gaming models only\":\"Solo modelli gaming\",\"Only saved in RoomGrid\":\"Solo salvati in RoomGrid\",\"Only own favorites\":\"Solo i miei preferiti\",\"Following only\":\"Solo a seguire\",\"Hide previously watched models\":\"Nascondi i modelli già visti\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Paese, età e durata online provengono dalle schede pubbliche attuali o dall’elenco pubblico delle stanze. Se Chaturbate non fornisce un dato, RoomGrid non lo ipotizza.\",\"Smart filter views\":\"Viste con filtro intelligente\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Salva i filtri e l'ordinamento correnti come vista dinamica. I modelli che vengono online più tardi e corrispondono alle regole vengono visualizzati automaticamente.\",\"Save current filter view\":\"Salva la visualizzazione filtrata corrente\",\"No smart views saved yet.\":\"Nessuna visualizzazione intelligente ancora salvata.\",\"What to show for each model\":\"Cosa mostrare per ogni modello\",\"Import complete\":\"Importazione completata\",\"Invalid file\":\"File non valido\",\"Range\":\"Gamma\",\"Today\":\"Oggi\",\"Week\":\"Settimana\",\"Month\":\"Mese\",\"Year\":\"Anno\",\"Recording enabled\":\"Registrazione abilitata\",\"Private session (do not record)\":\"Sessione privata (non registrare)\",\"Retention days\":\"Giorni di conservazione\",\"Usage time\":\"Tempo di utilizzo\",\"Summed camera time\":\"Tempo sommato della fotocamera\",\"Visits\":\"Visite\",\"Known models\":\"Modelli conosciuti\",\"Weekly heatmap\":\"Mappa termica settimanale\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Aggregato per giorno feriale e ora; più scuro significa più tempo osservato.\",\"Sunday to Saturday, 24 hours per row\":\"Dalla domenica al sabato, 24 ore per fila\",\"Watch ranking\":\"Guarda la classifica\",\"little data\":\"pochi dati\",\"No watch history yet.\":\"Nessuna cronologia visualizzazioni ancora.\",\"Workspace, filter and quality events\":\"Spazio di lavoro, filtro ed eventi di qualità\",\"Recent timeline\":\"Cronologia recente\",\"Export sensitive statistics\":\"Esporta statistiche sensibili\",\"Import statistics\":\"Importa statistiche\",\"Delete selected range\":\"Elimina l'intervallo selezionato\",\"Delete local events in the selected range?\":\"Eliminare gli eventi locali nell'intervallo selezionato?\",\"Clear all statistics\":\"Cancella tutte le statistiche\",\"Permanently clear all local statistics?\":\"Cancellare definitivamente tutte le statistiche locali?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Le esportazioni di statistiche contengono nomi di modelli e timestamp e devono essere trattate come sensibili. I vecchi dati vengono puliti in base alla conservazione.\",\"new\":\"nuovo\",\"Under 1 minute\":\"Meno di 1 minuto\",\"1–5 minutes\":\"1–5 minuti\",\"5–15 minutes\":\"5–15 minuti\",\"15–30 minutes\":\"15-30 minuti\",\"30+ minutes\":\"30+ minuti\",\"Local observations and your usage\":\"Osservazioni locali e utilizzo\",\"Public profile loaded on request\":\"Profilo pubblico caricato su richiesta\",\"Local RoomGrid interaction\":\"Interazione RoomGrid locale\",\"Local browser player\":\"Lettore browser locale\",\"Optional official statistics API\":\"API statistica ufficiale opzionale\",\"Local daily aggregation\":\"Aggregazione quotidiana locale\",\"Local data\":\"Dati locali\",\"Model summaries\":\"Sintesi dei modelli\",\"Public profiles\":\"Profili pubblici\",\"Usage events\":\"Eventi di utilizzo\",\"Official account snapshots\":\"Istantanee ufficiali dell'account\",\"Daily rollups\":\"Rollup giornalieri\",\"Camera time\":\"Tempo della fotocamera\",\"vs previous period\":\"rispetto al periodo precedente\",\"Trends\":\"Tendenze\",\"Daily activity\":\"Attività quotidiana\",\"Playback quality\":\"Qualità di riproduzione\",\"Privacy status\":\"Stato della privacy\",\"Data provenance\":\"Provenienza dei dati\",\"Local RoomGrid usage\":\"Utilizzo di RoomGrid locale\",\"7 days\":\"7 giorni\",\"30 days\":\"30 giorni\",\"90 days\":\"90 giorni\",\"Not enough data yet.\":\"Dati non ancora sufficienti.\",\"Models revisited\":\"Modelli rivisitati\",\"Previews opened\":\"Anteprime aperte\",\"Quick Views\":\"Visualizzazioni rapide\",\"Samples\":\"Campioni\",\"Private session\":\"Sessione privata\",\"History paused\":\"La storia si è fermata\",\"Recording locally\":\"Registrazione locale\",\"Disabled\":\"Disabilitato\",\"No cloud sync or telemetry.\":\"Nessuna sincronizzazione cloud o telemetria.\",\"Raw events\":\"Eventi grezzi\",\"Only local observations and actions from this browser.\":\"Solo osservazioni e azioni locali da questo browser.\",\"Time\":\"Tempo\",\"Models\":\"Modelli\",\"Online planner\":\"Pianificatore in linea\",\"Data & privacy\":\"Dati e privacy\",\"Configure statistics dashboard\":\"Configura il dashboard delle statistiche\",\"Preset\":\"Preimpostato\",\"Minimal\":\"Minimo\",\"Quality focus\":\"Focus sulla qualità\",\"Privacy focus\":\"Attenzione alla privacy\",\"Reset dashboard\":\"Reimposta dashboard\",\"Size\":\"Dimensioni\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Ordine, visibilità e larghezza vengono archiviati in impostazioni sicure per gli aggiornamenti. Ogni widget mostra la sua origine dati.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Nessun widget della dashboard è visibile. Apri la configurazione e seleziona almeno un widget.\",\"Median session\":\"Sessione mediana\",\"Daily usage trend\":\"Andamento dell'utilizzo quotidiano\",\"Your trend appears here after you start using RoomGrid.\":\"La tua tendenza viene visualizzata qui dopo aver iniziato a utilizzare RoomGrid.\",\"Session-duration distribution\":\"Distribuzione della durata della sessione\",\"No session-duration data yet.\":\"Nessun dato ancora sulla durata della sessione.\",\"Return rate\":\"Tasso di rendimento\",\"Total sessions\":\"Sessioni totali\",\"Longest observed\":\"Osservato più a lungo\",\"Model ranking\":\"Classifica dei modelli\",\"Select up to four models for a local comparison.\":\"Seleziona fino a quattro modelli per un confronto locale.\",\"Quick View\":\"Visualizzazione rapida\",\"No model data yet.\":\"Nessun dato del modello ancora.\",\"Models with time patterns\":\"Modelli con schemi temporali\",\"High confidence\":\"Alta fiducia\",\"Moderate confidence\":\"Fiducia moderata\",\"Planning horizon\":\"Orizzonte di pianificazione\",\"Likely next active times\":\"Probabili prossimi orari attivi\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Calcolato da campioni online osservati localmente; non garantisce una trasmissione. La fiducia migliora con più dati.\",\"High\":\"Alto\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Non ci sono ancora abbastanza osservazioni online. Il pianificatore appare automaticamente con la libreria.\",\"Discovery events\":\"Eventi di scoperta\",\"Local interaction funnel\":\"Imbuto di interazione locale\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Vengono conteggiate solo le tue azioni su questo dispositivo; i contenuti della chat e i pagamenti non vengono mai tracciati.\",\"Workspace events\":\"Eventi dello spazio di lavoro\",\"Filter events\":\"Filtra eventi\",\"Tag events\":\"Taggare gli eventi\",\"Top-four workspace comparison\":\"Confronto dei primi quattro spazi di lavoro\",\"Workspace usage\":\"Utilizzo dello spazio di lavoro\",\"No workspace data yet.\":\"Nessun dato sull'area di lavoro ancora.\",\"Average active streams\":\"Flussi attivi medi\",\"Playback quality by model\":\"Qualità di riproduzione per modello\",\"No quality data yet.\":\"Nessun dato sulla qualità ancora.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Il file delle statistiche non è valido o supera i limiti sicuri. I dati attuali non sono stati modificati.\",\"Statistics import assistant · preview\":\"Assistente all'importazione delle statistiche · anteprima\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Nulla è ancora cambiato. Seleziona le sezioni per importare ed esaminare i conflitti.\",\"Safe merge (recommended)\":\"Unione sicura (consigliata)\",\"Import complete. The previous statistics were backed up automatically.\":\"Importazione completata. È stato eseguito automaticamente il backup delle statistiche precedenti.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"Non è stato possibile applicare l'importazione in modo sicuro. I dati attuali e i backup rimangono invariati.\",\"File schema\":\"Schema del file\",\"Date range\":\"Intervallo di date\",\"Sensitive data\":\"Dati sensibili\",\"Not marked sensitive\":\"Non contrassegnato come sensibile\",\"Until manually resumed\":\"Fino alla ripresa manuale\",\"Not paused\":\"Non in pausa\",\"Restore this statistics backup? The current state will be backed up first.\":\"Ripristinare questo backup delle statistiche? Verrà eseguito prima il backup dello stato corrente.\",\"Permanently delete this backup?\":\"Eliminare definitivamente questo backup?\",\"Events\":\"Eventi\",\"Estimated statistics storage\":\"Archiviazione delle statistiche stimate\",\"Recording, pauses and private sessions\":\"Registrazione, pause e sessioni private\",\"Record usage statistics\":\"Registra le statistiche di utilizzo\",\"Pause 15 minutes\":\"Pausa 15 minuti\",\"Pause 1 hour\":\"Pausa 1 ora\",\"Pause 8 hours\":\"Pausa 8 ore\",\"Pause indefinitely\":\"Pausa indefinita\",\"Resume history\":\"Riprendi la storia\",\"History status\":\"Stato della storia\",\"Private for 30 minutes\":\"Privato per 30 minuti\",\"Private for 2 hours\":\"Privato per 2 ore\",\"Private until resumed\":\"Privato fino alla ripresa\",\"End private session\":\"Termina la sessione privata\",\"Not active\":\"Non attivo\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Una sessione privata interrompe anche le osservazioni del modello pubblico; una normale pausa nella cronologia interrompe l'utilizzo personale e le registrazioni di qualità.\",\"Automatic deletion and retention\":\"Cancellazione e conservazione automatiche\",\"Raw events (days)\":\"Eventi grezzi (giorni)\",\"Quality samples (days)\":\"Campioni di qualità (giorni)\",\"Account snapshots (days)\":\"Istantanee dell'account (giorni)\",\"Daily rollups (days)\":\"Rollup giornalieri (giorni)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Gli eventi grezzi scaduti vengono inseriti nei totali giornalieri prima dell'eliminazione automatica. Gli snapshot di qualità e account utilizzano i propri limiti.\",\"Statistics backup center\":\"Centro di backup statistico\",\"Create internal backup now\":\"Crea subito il backup interno\",\"Pseudonymized export\":\"Esportazione pseudonimizzata\",\"Print report\":\"Stampa rapporto\",\"Choose import file\":\"Scegli il file di importazione\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Nessun backup delle statistiche interne ancora. Ne vengono conservati fino a tre dopo l'importazione, il ripristino o la creazione manuale.\",\"Delete local statistics in this range? A backup will be created first.\":\"Eliminare le statistiche locali in questo intervallo? Verrà prima creato un backup.\",\"Clear everything\":\"Cancella tutto\",\"Permanently clear all local statistics? A backup will be created first.\":\"Cancellare definitivamente tutte le statistiche locali? Verrà prima creato un backup.\",\"Data inventory and provenance\":\"Inventario e provenienza dei dati\",\"Stored only in this browser\":\"Memorizzato solo in questo browser\",\"Coverage\":\"Copertura\",\"Unknown or missing data is never inferred.\":\"I dati sconosciuti o mancanti non vengono mai dedotti.\",\"Metric definitions & data quality\":\"Definizioni metriche e qualità dei dati\",\"Accumulated locally while a RoomGrid page is active.\":\"Accumulato localmente mentre è attiva una pagina RoomGrid.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Somma di tutte le sessioni della telecamera osservate; vengono aggiunti flussi simultanei.\",\"browser samples; no interpolation.\":\"esempi di browser; nessuna interpolazione.\",\"Privacy boundary\":\"Confine della privacy\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Nessun contenuto di chat, pagamenti o file multimediali viene salvato. Le sessioni private non aggiungono eventi.\",\"No history yet\":\"Nessuna storia ancora\",\"Loading\":\"Caricamento in corso\",\"Connected\":\"Connesso\",\"Locked\":\"Bloccato\",\"Not connected\":\"Non connesso\",\"Username\":\"Nome utente\",\"24-character stats token\":\"Segnalino statistiche da 24 caratteri\",\"At least 8 characters\":\"Almeno 8 caratteri\",\"Secure official statistics connection\":\"Connessione sicura alle statistiche ufficiali\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Facoltativo. Il token non entra mai nelle normali impostazioni, registri o esportazioni. La modalità Sessione lo mantiene solo in memoria; la modalità crittografata utilizza l'archiviazione locale isolata.\",\"Stats token\":\"Gettone statistiche\",\"Vault password\":\"Password della cassaforte\",\"Connect for this session\":\"Connettiti per questa sessione\",\"Username or token is invalid.\":\"Il nome utente o il token non sono validi.\",\"Encrypt, save & connect\":\"Crittografa, salva e connettiti\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Impossibile salvare crittografato. Controlla le autorizzazioni di token, password e userscript-manager.\",\"Get official token\":\"Ottieni il token ufficiale\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"L'aggiornamento automatico viene eseguito al massimo una volta ogni cinque minuti. Questo modulo utilizza solo un token ufficiale fornito deliberatamente.\",\"Account\":\"Conto\",\"History snapshots\":\"Istantanee della storia\",\"Last refresh\":\"Ultimo aggiornamento\",\"Unlock local vault\":\"Sblocca il caveau locale\",\"Password\":\"Parola d'ordine\",\"Unlock\":\"Sblocca\",\"Unlock failed.\":\"Sblocco fallito.\",\"Loading…\":\"Caricamento…\",\"Refresh now\":\"Aggiorna ora\",\"Auto-refresh every 5 minutes\":\"Aggiornamento automatico ogni 5 minuti\",\"Disconnect and delete token\":\"Disconnetti ed elimina il token\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Eliminare la connessione locale e il token crittografato? Restano le statistiche storiche.\",\"Broadcast status\":\"Stato della trasmissione\",\"Token balance\":\"Saldo dei gettoni\",\"Tips last hour\":\"Suggerimenti dell'ultima ora\",\"Followers\":\"Seguaci\",\"Up votes\":\"Voti positivi\",\"Down votes\":\"Voti negativi\",\"Satisfaction\":\"Soddisfazione\",\"Registered viewers\":\"Spettatori registrati\",\"Tokened viewers\":\"Spettatori con token\",\"Time online\":\"Tempo in linea\",\"Follower trend\":\"Andamento dei follower\",\"Tips trend\":\"Tendenza dei suggerimenti\",\"Token-balance trend\":\"Andamento del saldo dei token\",\"Viewer trend\":\"Tendenza degli spettatori\",\"Account Health Center\":\"Centro salute dell'account\",\"Official My Collection\":\"La mia collezione ufficiale\",\"Official support\":\"Supporto ufficiale\",\"Export local statistics\":\"Esporta statistiche locali\",\"Delete snapshot history\":\"Elimina la cronologia delle istantanee\",\"Delete the local official-statistics snapshots for this account?\":\"Eliminare le istantanee delle statistiche ufficiali locali per questo account?\",\"Last error\":\"Ultimo errore\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Lo stato del token, la cadenza di aggiornamento, la storia locale e i collegamenti ufficiali si trovano qui. RoomGrid non pubblica, invia messaggi o esegue transazioni automaticamente.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Il browser non ha concesso l'autorizzazione alla notifica. Le notifiche in-page rimangono attive.\",\"Notification profile\":\"Profilo di notifica\",\"Go-live only\":\"Solo go-live\",\"Personalized\":\"Personalizzato\",\"Always (all events)\":\"Sempre (tutti gli eventi)\",\"Inactive days\":\"Giorni inattivi\",\"Favorites, rating, priority and your tags decide relevance.\":\"Preferiti, valutazione, priorità e i tuoi tag determinano la pertinenza.\",\"Includes go-live, offline and public-profile changes.\":\"Include modifiche al go-live, offline e al profilo pubblico.\",\"Master switch\":\"Interruttore principale\",\"On\":\"Su\",\"Off (default)\":\"Disattivato (impostazione predefinita)\",\"Monitored models\":\"Modelli monitorati\",\"Active tab\":\"Scheda attiva\",\"This tab\":\"Questa scheda\",\"Another tab\":\"Un'altra scheda\",\"Local notifications\":\"Notifiche locali\",\"Notification delivery and display\":\"Invio e visualizzazione delle notifiche\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Le notifiche in-page utilizzano l'area libera nella barra superiore di RoomGrid e non richiedono l'autorizzazione del browser. Posizione, durata e densità sono configurabili.\",\"Delivery\":\"Consegna\",\"RoomGrid bar only\":\"Solo barra RoomGrid\",\"Browser notifications only\":\"Solo notifiche del browser\",\"RoomGrid bar and browser\":\"Barra e browser RoomGrid\",\"No display, history only\":\"Nessuna visualizzazione, solo cronologia\",\"In-page position\":\"Posizione in-page\",\"Free top-bar area\":\"Area bar superiore gratuita\",\"Floating top center\":\"Centro superiore mobile\",\"Floating top right\":\"Galleggiante in alto a destra\",\"Display style\":\"Stile di visualizzazione\",\"Compact single line\":\"Linea singola compatta\",\"Display duration\":\"Durata della visualizzazione\",\"Until dismissed\":\"Fino al licenziamento\",\"Visible at once\":\"Visibile subito\",\"Test notification\":\"Notifica di prova\",\"General rules\":\"Regole generali\",\"Enable notifications\":\"Abilita notifiche\",\"The browser did not grant notification permission.\":\"Il browser non ha concesso l'autorizzazione alla notifica.\",\"Neutral text\":\"Testo neutro\",\"Quiet time starts\":\"Inizia il momento tranquillo\",\"Quiet time ends\":\"Il tempo tranquillo finisce\",\"Cooldown (minutes)\":\"Recupero (minuti)\",\"Hourly limit\":\"Limite orario\",\"Favorite threshold\":\"Soglia preferita\",\"Open on click\":\"Apri con un clic\",\"Per model\":\"Per modello\",\"Model\":\"Modello\",\"Library is empty.\":\"La biblioteca è vuota.\",\"Per group\":\"Per gruppo\",\"Group\":\"Gruppo\",\"No groups.\":\"Nessun gruppo.\",\"Local notification list\":\"Elenco notifiche locali\",\"Mark all read\":\"Segna tutto letto\",\"Clear list\":\"Cancella elenco\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"I controlli in background eseguono solo la lettura dello stato e non avviano alcun video. Uno script utente può essere eseguito solo mentre è aperta la scheda di un sito correlato; più schede scelgono automaticamente una pedina.\",\"Shield status\":\"Stato dello scudo\",\"Ready\":\"Pronto\",\"Local PIN\":\"PIN locale\",\"Set\":\"Impostato\",\"Not set\":\"Non impostato\",\"Quick shield\":\"Scudo rapido\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Copre immediatamente nomi e immagini e mette in pausa/disattiva tutti i media. Il ripristino restituisce lo stato precedente del supporto.\",\"Activate now\":\"Attiva ora\",\"Auto-shield after inactivity\":\"Protezione automatica dopo inattività\",\"Set or change local PIN\":\"Imposta o modifica il PIN locale\",\"Leave empty to remove\":\"Lasciare vuoto per rimuovere\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"Il PIN è solo una barriera di accesso locale, non la crittografia e non sostituisce la protezione dell'account del sistema operativo.\",\"History controls\":\"Controlli della cronologia\",\"Usage statistics\":\"Statistiche sull'utilizzo\",\"Pause history 1 hour\":\"Sospendi la cronologia per 1 ora\",\"Clear statistics\":\"Statistiche chiare\",\"Clear all local statistics? A backup will be created first.\":\"Cancellare tutte le statistiche locali? Verrà prima creato un backup.\",\"Clear Discover settings\":\"Deseleziona le impostazioni di Scopri\",\"Clear local Discover filters and feeds?\":\"Cancellare i filtri e i feed locali di Discover?\",\"Clear notification list\":\"Cancella elenco notifiche\",\"Data boundary\":\"Confine dei dati\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"La configurazione, le statistiche di visualizzazione, la cronologia delle notifiche e le preferenze di Discover vengono archiviate localmente in questo browser. RoomGrid non contiene sincronizzazione cloud, caricamento di account o telemetria.\",\"Used\":\"Usato\",\"Sensitivity & consent center\":\"Centro sensibilità e consenso\",\"Standard configuration\":\"Configurazione standard\",\"Theme, layout, filters\":\"Tema, layout, filtri\",\"Local\":\"Locale\",\"Model names, notes, timestamps, statistics\":\"Nomi dei modelli, note, timestamp, statistiche\",\"Warn on export\":\"Avvisa sull'esportazione\",\"Secret\":\"Segreto\",\"Optional official statistics token\":\"Token statistico ufficiale opzionale\",\"Session-only or encrypted vault\":\"Archivio di sola sessione o crittografato\",\"Chat content\":\"Contenuti della chat\",\"Never persisted\":\"Non ho mai insistito\",\"Ephemeral\":\"Effimero\",\"Stream recording/screenshots\":\"Registrazioni/screenshot in streaming\",\"Not provided\":\"Non fornito\",\"Policy guard\":\"Guardia politica\",\"Performance profile\":\"Profilo prestazionale\",\"Playing streams\":\"Riproduzione di flussi\",\"Video elements\":\"Elementi video\",\"Measured memory\":\"Memoria misurata\",\"Unavailable in browser\":\"Non disponibile nel browser\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Le misurazioni attuali sono vicine al limite configurato. Metti in pausa le fotocamere inutilizzate o scegli la modalità Eco.\",\"Stream performance\":\"Prestazioni in streaming\",\"Power\":\"Potenza\",\"Concurrent stream limit\":\"Limite di streaming simultaneo\",\"Adaptive quality\":\"Qualità adattiva\",\"Gradual startup\":\"Avvio graduale\",\"Automatic performance mode\":\"Modalità di prestazione automatica\",\"Memory threshold\":\"Soglia di memoria\",\"Stream-count threshold\":\"Soglia di conteggio dei flussi\",\"Current auto state\":\"Stato automatico attuale\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Le anteprime non utilizzate vengono interrotte. Il nuovo video rimane in pausa al limite finché la capacità non è disponibile. I conteggi elevati iniziano gradualmente; non esiste una garanzia di “fotocamere illimitate”.\",\"Multi-window workspaces\":\"Aree di lavoro multi-finestra\",\"Window name\":\"Nome della finestra\",\"Active and recent windows\":\"Finestre attive e recenti\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"La libreria del modello viene sincronizzata tramite l'unione locale; ogni finestra mantiene la propria area di lavoro e le proprie impostazioni di visualizzazione. Le associazioni recenti delle finestre possono essere ripristinate dopo il riavvio laddove il browser lo consente.\",\"Optional tools are disabled\":\"Gli strumenti facoltativi sono disabilitati\",\"Media, app and contest directories will not scan the current page.\":\"Le directory dei media, delle app e dei concorsi non eseguiranno la scansione della pagina corrente.\",\"Enable optional tools\":\"Abilita strumenti opzionali\",\"Local search\":\"Ricerca locale\",\"Search saved items and current page\":\"Cerca gli elementi salvati e la pagina corrente\",\"Rescan current page\":\"Nuova scansione della pagina corrente\",\"Disable optional tools\":\"Disabilita gli strumenti opzionali\",\"Clear local side data\":\"Cancella i dati lato locale\",\"Clear app favorites, categories and media notes?\":\"Cancellare i preferiti, le categorie e le note multimediali delle app?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Questi moduli organizzano solo i collegamenti pubblici e il testo già accessibile nella pagina corrente. Non copiano contenuti multimediali, non scaricano automaticamente né gestiscono funzioni di chat, token o pagamento. I moduli di accesso, verifica dell'età, sicurezza e pagamento rimangono intatti.\",\"Photo and video areas\":\"Aree foto e video\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"In questa pagina non è presente alcuna area media pubblica riconoscibile. Eseguire nuovamente la scansione mentre ci si trova su una pagina nativa di foto o video.\",\"Apps directory\":\"Directory delle app\",\"Local category\":\"Categoria locale\",\"Found on page\":\"Trovato a pag\",\"No recognizable app links are present on this page.\":\"In questa pagina non sono presenti collegamenti ad app riconoscibili.\",\"Public contest results\":\"Risultati del concorso pubblico\",\"No readable public contest table is present on this page.\":\"In questa pagina non è presente alcuna tabella del concorso pubblica leggibile.\",\"Native account, help and broadcaster links\":\"Account nativi, guida e collegamenti alle emittenti\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"In questa pagina non è stato trovato alcun collegamento nativo sicuro; i percorsi di pagamento e di accesso vengono deliberatamente filtrati.\",\"Homepage filters reset\":\"Reimpostazione dei filtri della home page\",\"Global appearance reset\":\"Ripristino dell'aspetto globale\",\"Version\":\"Versione\",\"Health score\":\"Punteggio sulla salute\",\"Page type\":\"Tipo di pagina\",\"Route generation\":\"Generazione del percorso\",\"Running modules\":\"Moduli in esecuzione\",\"Local errors\":\"Errori locali\",\"Repair center\":\"Centro riparazioni\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Prima di ogni riparazione viene creato un backup della configurazione. Le statistiche e i dati del modello non vengono eliminati da queste azioni.\",\"Reset home filters\":\"Reimposta i filtri domestici\",\"Reset all home-page filters?\":\"Reimpostare tutti i filtri della home page?\",\"Reset global appearance\":\"Ripristina l'aspetto globale\",\"Reset the global appearance? Saved design profiles remain.\":\"Reimpostare l'aspetto globale? I profili di progettazione salvati rimangono.\",\"Normalize configuration\":\"Normalizza la configurazione\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Convalidare e riscrivere la configurazione corrente? Per prima cosa viene creato un backup.\",\"Close all previews\":\"Chiudi tutte le anteprime\",\"Compatibility self-check\":\"Autoverifica di compatibilità\",\"Module and page status\":\"Stato del modulo e della pagina\",\"Page route test matrix\":\"Matrice di test del percorso della pagina\",\"Current route\":\"Percorso attuale\",\"Trigger\":\"Innesco\",\"Recent page transitions\":\"Transizioni di pagina recenti\",\"Capability layer\":\"Livello di capacità\",\"Available\":\"Disponibile\",\"Fallback\":\"Ripiego\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Quando manca un'API, viene ripristinato solo il modulo interessato. Ripetuti guasti alla rete aprono temporaneamente un interruttore automatico.\",\"Backup and recovery status\":\"Stato del backup e del ripristino\",\"Primary data\":\"Dati primari\",\"Stable archive\":\"Archivio stabile\",\"Rolling backups\":\"Backup in sequenza\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"L'autocontrollo verifica solo la leggibilità e le funzionalità del browser; non sovrascrive mai automaticamente i dati correnti. Il ripristino continua a utilizzare il flusso di backup confermato esistente.\",\"Sanitized local error log\":\"Log degli errori locale ripulito\",\"No RoomGrid errors recorded.\":\"Nessun errore RoomGrid registrato.\",\"Run self-check again\":\"Eseguire nuovamente l'autocontrollo\",\"Export diagnostics\":\"Esportazione della diagnostica\",\"Clear error log\":\"Cancella il registro degli errori\",\"Return to normal site\":\"Ritorna al sito normale\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"L'esportazione diagnostica non contiene area di lavoro, nomi di modelli, note o statistiche. I parametri di query dell'URL e i possibili valori segreti vengono rimossi dal testo dell'errore.\",\"Local history on\":\"Storia locale attiva\",\"History off\":\"Cronologia disattivata\",\"This settings page could not be loaded\":\"Impossibile caricare questa pagina delle impostazioni\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"I tuoi dati non sono stati modificati. Riprova; se il problema persiste, apri Diagnostica.\",\"Try again\":\"Riprova\",\"Open Diagnostics\":\"Apri Diagnostica\",\"Public results\":\"Risultati pubblici\",\"Public contest links\":\"Link al concorso pubblico\",\"Page content has not appeared yet\":\"Il contenuto della pagina non è ancora apparso\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid ha controllato di nuovo la pagina ma non riesce ancora a trovare l'area dei contenuti di Chaturbate. La pagina potrebbe essere ancora in fase di caricamento oppure il sito ha restituito una visualizzazione vuota.\",\"Check again\":\"Controlla di nuovo\",\"Open RoomGrid Discover\":\"Apri RoomGrid Scopri\",\"Reload page\":\"Ricarica la pagina\",\"Hide notice\":\"Nascondi avviso\",\"Open MultiCam\":\"Apri MultiCam\",\"Open workstation in a new window\":\"Apri la postazione di lavoro in una nuova finestra\",\"Open Discover\":\"Apri Scopri\",\"Statistics center\":\"Centro statistico\",\"Backup and import\":\"Backup e importazione\",\"Page and design options\":\"Opzioni di pagina e design\",\"Hide immediately\":\"Nasconditi immediatamente\",\"All tools\":\"Tutti gli strumenti\",\"Search & commands\":\"Cerca e comandi\",\"Page design\":\"Progettazione della pagina\",\"Official collection\":\"Collezione ufficiale\",\"Saved\":\"Salvato\",\"Quick info\":\"Informazioni rapide\",\"Loading live frame\":\"Caricamento fotogramma live\",\"Live still\":\"Vivi ancora\",\"Not live\":\"Non vivere\",\"Pages\":\"Pagine\",\"Refresh\":\"Aggiorna\",\"Search…\":\"Cerca...\",\"Tag Explorer\":\"Esplora tag\",\"Refresh public catalog\":\"Aggiorna il catalogo pubblico\",\"Online\":\"In linea\",\"Toggle favorite\":\"Attiva/disattiva preferito\",\"Save\":\"Salva\",\"Remove favorite\":\"Rimuovi preferito\",\"Removed from library\":\"Rimosso dalla libreria\",\"Saved to library\":\"Salvato nella libreria\",\"Added to favorites\":\"Aggiunto ai preferiti\",\"Removed from favorites\":\"Rimosso dai preferiti\",\"Watching\":\"Guardando\",\"Watch\":\"Guarda\",\"Temporary watchlist\":\"Lista di controllo temporanea\",\"Removed from watchlist\":\"Rimosso dalla lista di controllo\",\"Added to watchlist\":\"Aggiunto alla lista di controllo\",\"Choose group\":\"Scegli gruppo\",\"Add to group…\":\"Aggiungi al gruppo...\",\"Group updated\":\"Gruppo aggiornato\",\"Site room\":\"Sala del cantiere\",\"Select for bulk actions\":\"Seleziona per azioni collettive\",\"viewers\":\"spettatori\",\"No public description\":\"Nessuna descrizione pubblica\",\"Open in Tag Explorer\":\"Apri in Esplora tag\",\"Feed name\":\"Nome del feed\",\"View name\":\"Visualizza nome\",\"Name already exists\":\"Il nome esiste già\",\"Saved-item limit reached\":\"Limite di elementi salvati raggiunto\",\"Feed saved\":\"Feed salvato\",\"View saved\":\"Vista salvata\",\"Applied\":\"Applicato\",\"Backup exported\":\"Backup esportato\",\"Backup imported\":\"Backup importato\",\"Invalid backup\":\"Backup non valido\",\"New models\":\"Nuovi modelli\",\"New since last visit\":\"Nuovo dall'ultima visita\",\"New to me\":\"Nuovo per me\",\"Returning\":\"Ritorno\",\"Stable online\":\"Stabile in linea\",\"Saved only\":\"Solo salvato\",\"Previously watched\":\"Guardato in precedenza\",\"Local collection\":\"Collezione locale\",\"Hide visited today\":\"Nascondi visitato oggi\",\"No feed selected\":\"Nessun feed selezionato\",\"No view selected\":\"Nessuna vista selezionata\",\"Saved feeds\":\"Feed salvati\",\"Saved views\":\"Visualizzazioni salvate\",\"Save new\":\"Salva nuovo\",\"All public rooms\":\"Tutte le stanze pubbliche\",\"Known HD\":\"HD conosciuto\",\"Random result\":\"Risultato casuale\",\"Viewers ↓\":\"Spettatori ↓\",\"Viewers ↑\":\"Spettatori ↑\",\"Online duration ↓\":\"Durata online ↓\",\"Followers ↓\":\"Seguaci ↓\",\"Name A–Z\":\"Nome dalla A alla Z\",\"Name Z–A\":\"Nome Z-A\",\"New first\":\"Nuovo prima\",\"Watch time ↓\":\"Guarda l'ora ↓\",\"Sessions ↓\":\"Sessioni ↓\",\"Last watched\":\"Ultimo visto\",\"Name, description, tag, place\":\"Nome, descrizione, tag, luogo\",\"Secondary sort\":\"Ordinamento secondario\",\"Shuffle again\":\"Mescola di nuovo\",\"All groups\":\"Tutti i gruppi\",\"All tags (AND)\":\"Tutti i tag (AND)\",\"Any tag (OR)\":\"Qualsiasi tag (OR)\",\"Excluded tags (NOT)\":\"Tag esclusi (NOT)\",\"Language, region, or country\":\"Lingua, regione o paese\",\"Minimum online minutes\":\"Minuti online minimi\",\"Maximum online minutes\":\"Minuti online massimi\",\"Minimum local watch minutes\":\"Minuti di orologio locali minimi\",\"Advanced filters and Boolean tags\":\"Filtri avanzati e tag booleani\",\"Saved views and feeds\":\"Visualizzazioni e feed salvati\",\"Import\":\"Importa\",\"Recent searches\":\"Ricerche recenti\",\"Live previews and performance\":\"Anteprime e performance dal vivo\",\"Muted live preview for every visible card\":\"Anteprima live disattivata per ogni carta visibile\",\"Refresh rate\":\"Frequenza di aggiornamento\",\"Real-time video while hovering\":\"Video in tempo reale mentre sei in bilico\",\"Hover delay\":\"Ritardo al passaggio del mouse\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Le carte visibili utilizzano fotogrammi JPEG. Al passaggio del mouse, solo una scheda passa al video in tempo reale disattivato e si ferma completamente quando il puntatore si allontana. I frame rate elevati aumentano significativamente la larghezza di banda e l'utilizzo della CPU.\",\"saved\":\"salvato\",\"previews\":\"anteprime\",\"Previews off\":\"Anteprime disattivate\",\"No more feeds can be saved\":\"Non è possibile salvare altri feed\",\"Tag feed saved\":\"Feed di tag salvato\",\"Select tag\":\"Seleziona etichetta\",\"Tag favorite\":\"Etichetta preferito\",\"Block tag\":\"Blocca etichetta\",\"Select tags first\":\"Seleziona prima i tag\",\"Tag-group name\":\"Nome del gruppo di tag\",\"Tag group saved\":\"Gruppo di tag salvato\",\"No gaming rooms in the current catalog\":\"Nessuna sala da gioco nel catalogo attuale\",\"All (AND)\":\"Tutti (E)\",\"Any (OR)\":\"Qualsiasi (OR)\",\"Exclude (NOT)\":\"Escludi (NOT)\",\"Search tags\":\"Cerca tag\",\"Tag search\":\"Ricerca tag\",\"Combination\":\"Combinazione\",\"Model sorting\":\"Ordinamento dei modelli\",\"Show in Discover\":\"Mostra in Scopri\",\"Save tag group\":\"Salva gruppo di tag\",\"Save tag feed\":\"Salva feed di tag\",\"No tag groups saved yet.\":\"Nessun gruppo di tag ancora salvato.\",\"Unknown region\":\"Regione sconosciuta\",\"Unknown age\":\"Età sconosciuta\",\"Public “New”\":\"Pubblico “Nuovo”\",\"My new cams\":\"Le mie nuove webcam\",\"New-cams feed saved\":\"Feed delle nuove webcam salvato\",\"Save “My new cams” feed\":\"Salva il feed \\\"Le mie nuove webcam\\\".\",\"Create gaming workspace\":\"Crea uno spazio di lavoro di gioco\",\"No matches for this combination.\":\"Nessuna corrispondenza per questa combinazione.\",\"Select tags to preview matching rooms here.\":\"Seleziona i tag per visualizzare in anteprima le stanze corrispondenti qui.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Cerca, aggiungi ai preferiti, blocca e combina tag pubblici. Le tendenze utilizzano solo le modifiche del catalogo osservate su questo dispositivo.\",\"Current tags\":\"Tag attuali\",\"Counts show current matches and change since the prior fetch.\":\"I conteggi mostrano le corrispondenze attuali e cambiano rispetto al recupero precedente.\",\"Results for selected tags\":\"Risultati per i tag selezionati\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Utilizza lo stesso ordinamento e navigazione SingleCam di Discover.\",\"Categories, age, and regions\":\"Categorie, età e regioni\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Vengono visualizzati solo i campi pubblici espliciti. Le regioni separate da virgole si combinano con OR; i valori sconosciuti rimangono visibili. Nessun fuso orario è indovinabile.\",\"New and gaming\":\"Novità e giochi\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Locale per la prima volta, il flag pubblico \\\"Nuovo\\\", la lista di controllo temporanea e i temi di gioco rimangono separati.\",\"Tag groups\":\"Gruppi di tag\",\"Save AND, OR, or NOT combinations.\":\"Salva le combinazioni AND, OR o NOT.\",\"Favorite tags\":\"Tag preferiti\",\"Your personal shortcut list.\":\"La tua lista di scorciatoie personali.\",\"No favorites yet\":\"Nessun preferito ancora\",\"Related tags\":\"Tag correlati\",\"Tags co-occurring on the currently matching models.\":\"Tag ricorrenti sui modelli attualmente corrispondenti.\",\"Personal tag history\":\"Cronologia dei tag personali\",\"Recently and frequently used on this device.\":\"Utilizzato di recente e frequentemente su questo dispositivo.\",\"Bulk target group\":\"Gruppo target di massa\",\"Selected models added to group\":\"Modelli selezionati aggiunti al gruppo\",\"New workspace name\":\"Nuovo nome dell'area di lavoro\",\"Created from a Discover selection\":\"Creato da una selezione Scopri\",\"Workspace created\":\"Area di lavoro creata\",\"Save all\":\"Salva tutto\",\"Create workspace\":\"Crea spazio di lavoro\",\"Loading public catalog…\":\"Caricamento catalogo pubblico...\",\"Loading the complete catalog in the background…\":\"Caricamento del catalogo completo in background…\",\"Updated\":\"Aggiornato\",\"Public rooms\":\"Sale pubbliche\",\"The catalog is loading.\":\"Il catalogo è in caricamento.\",\"No matching rooms. Adjust search or filters.\":\"Nessuna stanza corrispondente. Modifica la ricerca o i filtri.\",\"Previous\":\"Precedente\",\"Next\":\"Avanti\",\"Move up\":\"Vai su\",\"Move down\":\"Spostati giù\",\"Number shown\":\"Numero mostrato\",\"Widget width\":\"Larghezza del widget\",\"Full\":\"Pieno\",\"Nothing here yet.\":\"Niente qui ancora.\",\"Newly online\":\"Recentemente online\",\"Recently visited\":\"Visitato di recente\",\"Frequently visited\":\"Visitato frequentemente\",\"Homepage appearance, pagination, and performance\":\"Aspetto, impaginazione e prestazioni della home page\",\"Information density\":\"Densità delle informazioni\",\"Layout\":\"Disposizione\",\"Data rich\":\"Ricco di dati\",\"Static\":\"Statico\",\"Show statistics summary\":\"Mostra riepilogo delle statistiche\",\"Show personal widgets\":\"Mostra widget personali\",\"Sections below the live catalog\":\"Sezioni sotto il catalogo live\",\"Homepage live models\":\"Modelli live sulla home page\",\"All live rooms in one place\":\"Tutte le stanze dal vivo in un unico posto\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Utilizza la stessa ricerca completa, ordinamento, gruppi, tag e filtri della cronologia locale di Discover.\",\"Refresh live catalog\":\"Aggiorna il catalogo live\",\"Homepage settings\":\"Impostazioni della home page\",\"Matches a favorite gaming theme\":\"Corrisponde a un tema di gioco preferito\",\"Saved models\":\"Modelli salvati\",\"Online favorites\":\"Preferiti in linea\",\"Local watch time\":\"Orario di visualizzazione locale\",\"Continue last MultiCam workspace\":\"Continua l'ultima area di lavoro MultiCam\",\"Open full Discover page\":\"Apri la pagina Scopri completa\",\"Online notifications\":\"Notifiche in linea\",\"on\":\"su\",\"off\":\"spento\",\"Offline notifications\":\"Notifiche offline\",\"Feed\":\"Nutrire\",\"Recent\":\"Recente\",\"Frequent\":\"Frequente\",\"Saved filter views\":\"Visualizzazioni filtrate salvate\",\"Favorites currently public and online.\":\"Preferiti attualmente pubblici e online.\",\"New models or public streams that started recently.\":\"Nuovi modelli o flussi pubblici iniziati di recente.\",\"Current results from the selected feed.\":\"Risultati attuali dal feed selezionato.\",\"Suggestions with reasons\":\"Suggerimenti con motivazioni\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Basato solo su tag pubblici condivisi e temi preferiti a livello locale; nessuna deduzione sull'identità.\",\"Sorted by local last-visit time.\":\"Ordinati per ora locale dell'ultima visita.\",\"Sorted by local watch time.\":\"Ordinati per ora di visualizzazione locale.\",\"Saved models never visited\":\"I modelli salvati non sono mai stati visitati\",\"Saved in the library without local watch time.\":\"Salvato nella libreria senza orario di visualizzazione locale.\",\"Draggable dashboard widgets\":\"Widget del dashboard trascinabili\",\"Your RoomGrid homepage\":\"La tua home page di RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Un catalogo live veloce e personalizzabile con SingleCam, MultiCam, filtri completi, visualizzazioni salvate e widget personali.\",\"Audio activity\":\"Attività audio\",\"Auto quality\":\"Qualità automatica\",\"Stream interrupted; status checks continue.\":\"Flusso interrotto; continuano i controlli sullo stato.\",\"This browser cannot play HLS.\":\"Questo browser non può riprodurre HLS.\",\"Connecting to live stream…\":\"Connessione al live streaming…\",\"The room is not public right now.\":\"La stanza non è pubblica al momento.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Attualmente offline. Le alternative dello stesso filtro sono mostrate di seguito.\",\"Status temporarily unavailable; retrying automatically.\":\"Stato temporaneamente non disponibile; riprovare automaticamente.\",\"Loading public profile…\":\"Caricamento profilo pubblico…\",\"Profile temporarily unavailable.\":\"Profilo temporaneamente non disponibile.\",\"No cached profile.\":\"Nessun profilo memorizzato nella cache.\",\"Public profile changed\":\"Il profilo pubblico è cambiato\",\"No public bio.\":\"Nessuna biografia pubblica.\",\"Source\":\"Fonte\",\"Observed\":\"Osservato\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Vengono mostrati solo i link autopubblicati sul profilo pubblico. Un nome utente corrispondente da solo non è una prova di identità.\",\"Profile information\":\"Informazioni sul profilo\",\"Preparing…\":\"Preparazione…\",\"Play\":\"Gioca\",\"Unmute\":\"Riattiva\",\"Mute\":\"Muto\",\"Contain\":\"Contenere\",\"Cover\":\"Copertina\",\"Brightness\":\"Luminosità\",\"Contrast\":\"Contrasto\",\"Saturation\":\"Saturazione\",\"Hue\":\"Tonalità\",\"Sepia\":\"Seppia\",\"Grayscale\":\"Scala di grigi\",\"Invert\":\"Inverti\",\"Blur\":\"Sfocatura\",\"Image zoom\":\"Zoom dell'immagine\",\"Recording saved\":\"Registrazione salvata\",\"Screenshot saved\":\"Schermata salvata\",\"Recording\":\"Registrazione\",\"Mirror\":\"Specchio\",\"Flip vertically\":\"Capovolgi verticalmente\",\"Rotate\":\"Ruota\",\"Show edited\":\"Mostra modificato\",\"Show original\":\"Mostra originale\",\"Reset image\":\"Reimposta immagine\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Tieni premuto Ctrl e scorri per ingrandire; trascina l'immagine quando è ingrandita. Fare doppio clic per reimpostare.\",\"Zoom out\":\"Rimpicciolisci\",\"Zoom in\":\"Ingrandisci\",\"Reset zoom and position\":\"Reimposta zoom e posizione\",\"Player window\":\"Finestra del giocatore\",\"Fullscreen\":\"Schermo intero\",\"Private notes, memories, or things to check next time (stored locally)\":\"Note private, ricordi o cose da controllare la prossima volta (archiviati localmente)\",\"Autosave is on\":\"Il salvataggio automatico è attivo\",\"Enter your own tag…\":\"Inserisci il tuo tag...\",\"Saved at\":\"Salvato a\",\"Local details saved\":\"Dettagli locali salvati\",\"Remove tag\":\"Rimuovi l'etichetta\",\"No personal tags yet.\":\"Nessun tag personale ancora.\",\"Saving…\":\"Salvataggio…\",\"Create your first group under Groups.\":\"Crea il tuo primo gruppo in Gruppi.\",\"Added to MultiCam\":\"Aggiunto a MultiCam\",\"Edit private details\":\"Modifica i dettagli privati\",\"Trans\":\"Trad\",\"Region\":\"Regione\",\"Current online time\":\"Tempo online attuale\",\"Stream attributes\":\"Attributi del flusso\",\"New\":\"Nuovo\",\"Yes\":\"Sì\",\"No / unknown\":\"No/sconosciuto\",\"Last observed\":\"Ultimo osservato\",\"Local visits\":\"Visite locali\",\"First watched\":\"Prima guardato\",\"No public description.\":\"Nessuna descrizione pubblica.\",\"No public tags available.\":\"Nessun tag pubblico disponibile.\",\"Quick-add your own tag…\":\"Aggiungi rapidamente il tuo tag…\",\"Public tags\":\"Tag pubblici\",\"Add\":\"Aggiungi\",\"Private details\":\"Dettagli privati\",\"Everything stays in this browser and survives updates.\":\"Tutto rimane in questo browser e sopravvive agli aggiornamenti.\",\"Private notebook\":\"Taccuino privato\",\"Keep on watchlist\":\"Continua nella watchlist\",\"Group assignment (multiple)\":\"Assegnazione di gruppo (multipli)\",\"Save now\":\"Risparmia ora\",\"Chat loads only when this section is opened.\":\"La chat si carica solo quando questa sezione è aperta.\",\"Official chat\":\"Chiacchierata ufficiale\",\"Official chat connected.\":\"Chat ufficiale collegata.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Chat caricata; non è stato possibile isolare completamente la struttura della pagina corrente.\",\"Chat loaded.\":\"Chat caricata.\",\"Loading official chat…\":\"Caricamento chat ufficiale...\",\"Official chat room\":\"Chat room ufficiale\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Caricato solo quando necessario; RoomGrid non memorizza né invia mai automaticamente messaggi di chat.\",\"Reload chat\":\"Ricarica la chat\",\"Open official room in a new window\":\"Apri la stanza ufficiale in una nuova finestra\",\"Hide chat\":\"Nascondi chat\",\"Live observations\":\"Osservazioni dal vivo\",\"Trend confidence\":\"Tendenza alla fiducia\",\"Known time slots\":\"Fasce orarie conosciute\",\"Typical peak\":\"Picco tipico\",\"Watched in 7 days\":\"Visto in 7 giorni\",\"Watched in 30 days\":\"Visto in 30 giorni\",\"Current streak\":\"Serie attuale\",\"Typical weekly online times\":\"Tempi online settimanali tipici\",\"Online times will be learned on this device from now on.\":\"D'ora in poi i tempi online verranno memorizzati su questo dispositivo.\",\"Based on observations on this device; missing information is never guessed.\":\"Sulla base delle osservazioni su questo dispositivo; le informazioni mancanti non vengono mai indovinate.\",\"Activity by weekday\":\"Attività entro i giorni feriali\",\"Most likely online times\":\"Molto probabilmente tempi online\",\"Online-time map\":\"Mappa del tempo in linea\",\"Public covers and locally saved example images.\":\"Copertine pubbliche e immagini di esempio salvate localmente.\",\"Import quality\":\"Importa qualità\",\"Original file\":\"Fascicolo originale\",\"Very high (up to 4K)\":\"Molto alto (fino a 4K)\",\"High (up to 1440p)\":\"Alto (fino a 1440p)\",\"Space-saving (up to 900p)\":\"Salvaspazio (fino a 900p)\",\"Add images from computer\":\"Aggiungi immagini dal computer\",\"Use website cover\":\"Utilizza la copertina del sito web\",\"Use first image as cover\":\"Usa la prima immagine come copertina\",\"Covers and local gallery\":\"Copertine e gallery locale\",\"Updating images…\":\"Aggiornamento delle immagini…\",\"Loading images…\":\"Caricamento immagini...\",\"Own image\":\"Propria immagine\",\"Example image\":\"Immagine di esempio\",\"Current cover\":\"Copertura attuale\",\"Alternative preview\":\"Anteprima alternativa\",\"Current public image\":\"Immagine pubblica attuale\",\"Saving cover…\":\"Salvataggio della copertura...\",\"Set as cover\":\"Imposta come copertina\",\"Delete this image from the local gallery?\":\"Eliminare questa immagine dalla galleria locale?\",\"No images yet.\":\"Nessuna immagine ancora.\",\"saved locally\":\"salvato localmente\",\"own images\":\"proprie immagini\",\"Update public previews\":\"Aggiorna le anteprime pubbliche\",\"Processing your images…\":\"Elaborazione delle immagini…\",\"ALL PAGE SOUNDS OFF\":\"TUTTA LA PAGINA SUONO DISATTIVATA\",\"PAGE SOUNDS ALLOWED\":\"SUONI DELLA PAGINA CONSENTITI\",\"Chat/token sounds OFF\":\"Suoni chat/token disattivati\",\"Chat/token sounds ON\":\"Suoni chat/token attivati\",\"Reconnecting the player with the new settings…\":\"Riconnessione del lettore con le nuove impostazioni…\",\"Turn off chat, token and tip sounds completely\":\"Disattiva completamente i suoni della chat, dei token e dei suggerimenti\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Blocca i suoni della pagina provenienti da elementi audio e audio Web. Lo streaming audio rimane controllabile separatamente tramite \\\"Suono attivato\\\".\",\"No alternatives available.\":\"Nessuna alternativa disponibile.\",\"Similar models and offline alternatives\":\"Modelli simili e alternative offline\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Priorità tramite tag condivisi; nessuna identità o inferenza su account esterni.\",\"Chat\":\"Chatta\",\"Notes & organization\":\"Note e organizzazione\",\"Images\":\"Immagini\",\"SingleCam sections\":\"Sezioni SingleCam\",\"Back to original results\":\"Ritorno ai risultati originali\",\"Private workspace\":\"Spazio di lavoro privato\",\"Chat on/off\":\"Chatta attivata/disattivata\",\"Hide similar models\":\"Nascondi modelli simili\",\"Show similar models\":\"Mostra modelli simili\",\"RoomGrid Home\":\"Casa RoomGrid\",\"RoomGrid Tags\":\"Tag RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Catalogo pubblico non disponibile; mostrando la biblioteca locale.\",\"Drag to resize sidebar\":\"Trascina per ridimensionare la barra laterale\",\"The workspace could not be saved safely. Export a backup now.\":\"Non è stato possibile salvare l'area di lavoro in modo sicuro. Esporta un backup adesso.\",\"Open menu or restore controls\":\"Apri il menu o ripristina i controlli\",\"Controls restored\":\"Controlli ripristinati\",\"Menu\":\"Menù\",\"Show toolbar\":\"Mostra barra degli strumenti\",\"Tools\":\"Strumenti\",\"Show sidebar\":\"Mostra barra laterale\",\"Restore controls\":\"Ripristina i controlli\",\"Show toolbar, groups, and menus\":\"Mostra barra degli strumenti, gruppi e menu\",\"Open more tools\":\"Apri più strumenti\",\"all watch states\":\"tutti gli stati dell'orologio\",\"Watch status\":\"Guarda lo stato\",\"0 means no viewer limit.\":\"0 significa nessun limite di spettatori.\",\"Import temporary URLs; they disappear after refresh\":\"Importa URL temporanei; scompaiono dopo l'aggiornamento\",\"Filter current group\":\"Filtra il gruppo corrente\",\"All status\":\"Tutto lo stato\",\"Hide offline/private\":\"Nascondi offline/privato\",\"Collapse toolbar\":\"Comprimi barra degli strumenti\",\"Windows per page\":\"Finestre per pagina\",\"Custom grid\":\"Griglia personalizzata\",\"Choose MultiCam audio routing\":\"Scegli il routing audio MultiCam\",\"Audio: manual\":\"Audio: manuale\",\"Audio: solo cam\":\"Audio: videocamera solista\",\"Audio: follow focus\":\"Audio: segui la messa a fuoco\",\"Auto\":\"Automatico\",\"Userscript version\":\"Versione dello script utente\",\"Open Discover and dashboard\":\"Apri Scopri e dashboard\",\"Global search and quick commands · Ctrl+K\":\"Ricerca globale e comandi rapidi · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Streaming di salute, budget di qualità, audio, scene e sostituzioni\",\"Control\":\"Controllo\",\"Rooms\":\"Stanze\",\"Filter\":\"Filtra\",\"Playback\":\"Riproduzione\",\"Stabilizing live preview…\":\"Stabilizzazione dell'anteprima dal vivo…\",\"Recovering live preview…\":\"Recupero dell'anteprima dal vivo…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"L'anteprima dal vivo viene brevemente messa in pausa per evitare un ciclo di ricarica. Riprova a breve.\",\"Collapse groups\":\"Comprimi gruppi\",\"Expand/collapse users\":\"Espandi/comprimi utenti\",\"Output volume; no live signal level is available for this stream\":\"Volume di uscita; nessun livello del segnale live è disponibile per questo streaming\",\"OFF\":\"SPENTO\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Incolla gli URL. Sono supportate le pagine delle stanze, m3u8, mp4/webm. Le pagine normali verranno analizzate al meglio.\",\"Actual resolution, connection and buffer\":\"Risoluzione effettiva, connessione e buffer\",\"Drag to reorder / move group\":\"Trascina per riordinare/spostare il gruppo\",\"All time\":\"Tutto il tempo\",\"Click a model for details.\":\"Fare clic su un modello per i dettagli.\",\"Most watched groups\":\"Gruppi più visti\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"I campi dell'account come la data di iscrizione vengono mostrati solo quando Chaturbate li espone chiaramente nella stessa pagina. Lo script non inventa valori.\",\"Back\":\"Indietro\",\"Statistics import failed: \":\"Importazione delle statistiche non riuscita:\",\"Recent saves\":\"Salvataggi recenti\",\"Rooms per page\":\"Stanze per pagina\",\"Custom rows\":\"Righe personalizzate\",\"Custom columns\":\"Colonne personalizzate\",\"Main width\":\"Larghezza principale\",\"Main height\":\"Altezza principale\",\"Modules\":\"Moduli\",\"Modules start only on matching page types. Saving reloads the current page.\":\"I moduli iniziano solo sui tipi di pagina corrispondenti. Il salvataggio ricarica la pagina corrente.\",\"Tools on normal pages\":\"Strumenti su pagine normali\",\"True audio-level meters\":\"Veri misuratori di livello audio\",\"MultiCam control center\":\"Centro di controllo MultiCam\",\"No active streams yet\":\"Nessun flusso attivo ancora\",\"Stream health\":\"Trasmetti la salute\",\"Budget off\":\"Budget ridotto\",\"Economy · 2160p total\":\"Economia · 2160p totale\",\"Balanced · 4320p total\":\"Bilanciato · 4320p totale\",\"High · 6480p total\":\"Alto · 6480p totale\",\"Ultra · 8640p total\":\"Ultra · 8640p totale\",\"Quality budget is off; global and per-camera limits still apply.\":\"Il budget per la qualità è disattivato; si applicano ancora i limiti globali e per fotocamera.\",\"Quality budget\":\"Bilancio di qualità\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Distribuisce la qualità tra le camme attive. Una qualità esplicita per fotocamera vince sempre.\",\"Give the focused cam double weight\":\"Dai il doppio peso alla camma focalizzata\",\"Manual per camera\":\"Manuale per telecamera\",\"Solo: one camera only\":\"Solo: una sola telecamera\",\"Follow focused camera\":\"Segui la telecamera focalizzata\",\"Audio management\":\"Gestione dell'audio\",\"Routing never overwrites saved mute states or individual volume.\":\"Il routing non sovrascrive mai gli stati di disattivazione salvati o il singolo volume.\",\"Mute all\":\"Disattiva tutto\",\"Restore saved audio\":\"Ripristina l'audio salvato\",\"Scene name\":\"Nome della scena\",\"Focus\":\"Concentrarsi\",\"Grid\":\"Griglia\",\"cams\":\"camme\",\"Load\":\"Caricare\",\"No scenes saved for this workspace yet.\":\"Nessuna scena ancora salvata per quest'area di lavoro.\",\"Scenes\":\"Scene\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Salva gli slot, il layout e il routing audio correnti per il ripristino con un clic.\",\"Save current view as scene\":\"Salva la vista corrente come scena\",\"All saved online models\":\"Tutti i modelli online salvati\",\"Replacement models\":\"Modelli sostitutivi\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"I modelli preferiti vengono provati nell'ordine dell'elenco; solo allora vengono utilizzati i modelli online del gruppo selezionato. Gli incarichi manuali vincono sempre.\",\"Replace offline slots automatically\":\"Sostituisci automaticamente gli slot offline\",\"Delay (seconds)\":\"Ritardo (secondi)\",\"Candidate group\":\"Gruppo di candidati\",\"Preferred replacements (multiple)\":\"Sostituzioni preferite (diverse)\",\"Replacement rule saved\":\"Regola sostitutiva salvata\",\"Save replacement rule\":\"Salva regola di sostituzione\",\"Resolve duplicate shortcuts first:\":\"Risolvi prima le scorciatoie duplicate:\",\"Name, note or own tag…\":\"Nome, nota o tag personale...\",\"Camera menu\":\"Menù della fotocamera\",\"Camera menu · quality and sound\":\"Menu della fotocamera · qualità e suono\",\"Close menu\":\"Chiudi menù\",\"View and window\":\"Vista e finestra\",\"Quality and image\":\"Qualità e immagine\",\"More actions\":\"Più azioni\",\"Currently main\":\"Attualmente principale\",\"Set as main\":\"Imposta come principale\",\"Pin live preview\":\"Aggiungi l'anteprima dal vivo\",\"Volume multiplier for this cam\":\"Moltiplicatore di volume per questa cam\",\"Reset individual volume to 100%\":\"Ripristina il volume individuale al 100%\",\"Individual volume\":\"Volume individuale\",\"Listen to this cam only\":\"Ascolta solo questa videocamera\",\"Set quality for this camera only\":\"Imposta la qualità solo per questa fotocamera\",\"Automatic (global/adaptive)\":\"Automatico (globale/adattivo)\",\" · available\":\"· disponibile\",\"Quality for this camera\":\"Qualità per questa fotocamera\",\"Hold for original\":\"Aspetta per l'originale\",\"Vivid\":\"Vivido\",\"Image adjustments\":\"Aggiustamenti dell'immagine\",\"More tools\":\"Più strumenti\",\"Interface\":\"Interfaccia\",\"Show top controls\":\"Mostra i controlli superiori\",\"Collapse top controls\":\"Comprimi i controlli superiori\",\"Show groups\":\"Mostra gruppi\",\"Windows\":\"Finestre\",\"Open workspace in new window\":\"Apri l'area di lavoro in una nuova finestra\",\"Control center\":\"Centro di controllo\",\"Choose the data to export\":\"Scegli i dati da esportare\",\"Preview the file and conflicts before applying it\":\"Visualizza l'anteprima del file e dei conflitti prima di applicarlo\",\"Help\":\"Aiuto\",\"Changes in this version\":\"Cambiamenti in questa versione\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Aree di lavoro, libreria di modelli, importazioni sicure, progettazione globale, pagine normali estese, Discover/dashboard ampliato e controlli di qualità/immagine del lettore.\",\"Muted\":\"Disattivato\",\"Performance limit reached\":\"Limite di prestazioni raggiunto\",\"Drag to resize main width\":\"Trascina per ridimensionare la larghezza principale\",\"Drag to resize main height\":\"Trascina per ridimensionare l'altezza principale\",\"Stable\":\"Stabile\",\"Unstable\":\"Instabile\",\"Refresh all cameras\":\"Aggiorna tutte le fotocamere\",\"Reconnect current models\":\"Ricollegare i modelli attuali\",\"Switch to grid view\":\"Passa alla visualizzazione griglia\",\"Switch to focus view\":\"Passa alla visualizzazione messa a fuoco\",\"Manage workspaces\":\"Gestire gli spazi di lavoro\",\"Create, duplicate, archive and device layouts\":\"Crea, duplica, archivia e layout dei dispositivi\",\"Open model library\":\"Apri la libreria dei modelli\",\"Notes, tags, ratings and bulk actions\":\"Note, tag, valutazioni e azioni collettive\",\"Design options\":\"Opzioni di progettazione\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Workstation multivista all-in-one per sale, registrazione, avvisi, screenshot, riconnessioni e strumenti dati.\",\"Username ↵\":\"Nome utente ↵\",\"Search rooms\":\"Cerca stanze\",\"Invalid username\":\"Nome utente non valido\",\"Hide offline\":\"Nascondi offline\",\"Hide private\":\"Nascondi privato\",\"Manual\":\"Manuale\",\"By status\":\"Per stato\",\"By name\":\"Per nome\",\"By added time\":\"Con l'aggiunta di tempo\",\"By watch time\":\"In base all'ora di visualizzazione\",\"By sessions\":\"Per sessioni\",\"By last watched\":\"Dall'ultimo guardato\",\"By first watched\":\"Per prima cosa guardato\",\"By viewer count\":\"Per numero di spettatori\",\"By last online\":\"Dall'ultimo online\",\"Advanced filters\":\"Filtri avanzati\",\"Watched before\":\"Visto prima\",\"Currently in view\":\"Attualmente in vista\",\"Include tag\":\"Includi etichetta\",\"Exclude tag\":\"Escludi etichetta\",\"Minimum watch time (minutes)\":\"Tempo di visualizzazione minimo (minuti)\",\"Watched within days\":\"Guardato in pochi giorni\",\"Refresh all\":\"Aggiorna tutto\",\"Online alerts\":\"Avvisi in linea\",\"Offline alerts\":\"Avvisi offline\",\"Desktop notification + card flash when a model goes online\":\"Notifica sul desktop + flash della scheda quando un modello va online\",\"Desktop notification + card flash when a model goes offline\":\"Notifica sul desktop + flash della scheda quando un modello va offline\",\"Dark mode\":\"Modalità oscura\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Utilizza un tema di interfaccia scuro. Lo streaming video e audio rimane invariato.\",\"Dark mode enabled\":\"Modalità oscura abilitata\",\"Light mode enabled\":\"Modalità luce abilitata\",\"Mute token/tip sounds\":\"Disattiva i suoni di token e mance\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Blocca solo i suoni di notifica di token e mance. L’audio della diretta rimane attivo.\",\"Token/tip sounds muted\":\"Suoni di token e mance disattivati\",\"Token/tip sounds enabled\":\"Suoni di token e mance attivati\",\"Clean\":\"Pulito\",\"Clean mode on\":\"Modalità pulizia attivata\",\"Exit clean mode\":\"Uscire dalla modalità pulita\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Nascondi temporaneamente tutti i controlli e le sovrapposizioni. Scorciatoia: Alt+P / Alt+C. Premi Esc per uscire.\",\"Window first\":\"Prima la finestra\",\"Window-first mode on\":\"Modalità finestra-prima attiva\",\"Exit window-first\":\"Esci prima dalla finestra\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Nascondi la shell dell'app e mantieni visibili le finestre della stanza. Scorciatoia: Alt+V.\",\"Show thumbnails\":\"Mostra miniature\",\"Hide thumbnails\":\"Nascondi miniature\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Mostra o nasconde la barra delle miniature in modalità Focus. Scorciatoia: Alt+T.\",\"Choose Focus rooms\":\"Scegli le stanze Focus\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Scegli le stanze per la modalità Focus. La prima stanza selezionata è la schermata principale; utilizzare ↑/↓ per impostare l'ordine delle finestre secondarie.\",\"Main screen\":\"Schermata principale\",\"Right window\":\"Finestra destra\",\"Bottom window\":\"Finestra inferiore\",\"Empty slot\":\"Slot vuoto\",\"Drag this model to a Focus slot\":\"Trascina questo modello in uno slot Focus\",\"Choose model for this slot\":\"Scegli il modello per questo slot\",\"Select a saved model for this exact Focus slot.\":\"Seleziona un modello salvato per questo esatto slot Focus.\",\"Choose model for this window\":\"Scegli il modello per questa finestra\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Seleziona un modello salvato per questa esatta finestra della griglia. I modelli esistenti rimangono salvati.\",\"Remove from this grid window\":\"Rimuovi da questa finestra della griglia\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Viene liberato solo questo slot. Il modello salvato rimane nell'area di lavoro.\",\"Show in grid window\":\"Mostra nella finestra della griglia\",\"Choose the exact active grid window for this model.\":\"Scegli l'esatta finestra della griglia attiva per questo modello.\",\"Current\":\"Corrente\",\"Model removed from this grid window.\":\"Modello rimosso da questa finestra della griglia.\",\"No saved models in this group.\":\"Nessun modello salvato in questo gruppo.\",\"Secondary window\":\"Finestra secondaria\",\"Use current page automatically\":\"Utilizza automaticamente la pagina corrente\",\"Clear custom selection\":\"Cancella la selezione personalizzata\",\"Apply Focus layout\":\"Applica il layout Focus\",\"No rooms are available in the current group and filter.\":\"Nessuna stanza è disponibile nel gruppo e filtro corrente.\",\"Fit: full image\":\"Vestibilità: immagine intera\",\"Fit: fill window\":\"Adatta: riempi la finestra\",\"Switch between full image and cropped fill.\":\"Passa dall'immagine intera al riempimento ritagliato.\",\"Playback settings\":\"Impostazioni di riproduzione\",\"Max stream quality\":\"Massima qualità del flusso\",\"Auto / no cap\":\"Auto/nessun limite\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Comando + zoom con rotellina\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Ingrandisci una scheda con Ctrl/Comando + rotellina del mouse, trascina mentre ingrandisci, fai doppio clic per reimpostare.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Uscire dalla modalità pulita. Scorciatoia: Alt+P / Alt+C o Esc.\",\"Clean · Alt+P/C / Esc\":\"Pulisci · Alt+P/C / Esc\",\"(synced from another tab)\":\"(sincronizzato da un'altra scheda)\",\"Type a username and press Enter. Shortcut: /\":\"Digita un nome utente e premi Invio. Scorciatoia: /\",\"Filter visible rooms by username\":\"Filtra le stanze visibili per nome utente\",\"Show / hide group sidebar\":\"Mostra/nascondi la barra laterale del gruppo\",\"Global volume. Per-room mute is still respected.\":\"Volume globale. Viene comunque rispettata la silenziosità per camera.\",\"Base tile size in grid view\":\"Dimensioni della tessera di base nella vista griglia\",\"Sort rooms in the current group\":\"Ordina le stanze nel gruppo corrente\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Altezza dello schermo principale in modalità Focus. Puoi anche trascinare il separatore.\",\"Main-screen aspect ratio in Focus mode\":\"Proporzioni della schermata principale in modalità Focus\",\"Thumbnail width in Focus mode\":\"Larghezza della miniatura in modalità Focus\",\"Show every saved room. The remove button deletes globally in this view.\":\"Mostra tutte le stanze salvate. Il pulsante Rimuovi elimina globalmente in questa visualizzazione.\",\"Create an isolated group\":\"Crea un gruppo isolato\",\"More tools and maintenance\":\"Più strumenti e manutenzione\",\"GROUPS\":\"GRUPPI\",\"All saved\":\"Tutto salvato\",\"Default\":\"Predefinito\",\"Online now\":\"In linea adesso\",\"New group name:\":\"Nuovo nome del gruppo:\",\"Rename\":\"Rinominare\",\"Rename to:\":\"Rinomina in:\",\"Delete group\":\"Elimina gruppo\",\"Live preview\":\"Anteprima dal vivo\",\"Loading preview…\":\"Caricamento anteprima...\",\"Private or away\":\"Privato o lontano\",\"Preview could not be loaded\":\"Impossibile caricare l'anteprima\",\"Toggle mute\":\"Disattiva l'audio\",\"Resume\":\"Riprendi\",\"Screenshot current frame\":\"Screenshot del fotogramma corrente\",\"Record current card\":\"Registra la carta corrente\",\"Stop recording\":\"Interrompi la registrazione\",\"Picture-in-Picture\":\"Immagine nell'immagine\",\"Remove from current group\":\"Rimuovi dal gruppo corrente\",\"Delete saved room\":\"Elimina la stanza salvata\",\"Mirror image\":\"Immagine speculare\",\"Flip image\":\"Capovolgi l'immagine\",\"Rotate left\":\"Ruota a sinistra\",\"Rotate right\":\"Ruota a destra\",\"Reset view\":\"Reimposta vista\",\"Copy username\":\"Copia nome utente\",\"Add favorite\":\"Aggiungi preferito\",\"Move to favorites only\":\"Sposta solo nei preferiti\",\"Copied\":\"Copiato\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Schermata non riuscita. Browser/CORS potrebbero bloccare la creazione di questo flusso.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"La registrazione è locale e salva questo video con l'audio corrispondente quando il browser lo espone. Usalo solo quando hai il permesso di salvare questo contenuto. Continuare?\",\"Recording started\":\"La registrazione è iniziata\",\"Recording is not supported for this video/browser\":\"La registrazione non è supportata per questo video/browser\",\"Recording segment saved\":\"Segmento di registrazione salvato\",\"Final recording segment saved\":\"Segmento di registrazione finale salvato\",\"Recording paused; waiting for the stream to return\":\"Registrazione in pausa; in attesa che il flusso ritorni\",\"Recording resumed\":\"La registrazione è ripresa\",\"Recording paused; click to stop\":\"Registrazione in pausa; fare clic per interrompere\",\"No recording data to save\":\"Nessun dato di registrazione da salvare\",\"Recording settings saved\":\"Impostazioni di registrazione salvate\",\"Segment length in minutes (1-180):\":\"Lunghezza del segmento in minuti (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Bitrate video in Mbps (0,5-20):\",\"Warn before leaving while recording\":\"Avvisa prima di partire durante la registrazione\",\"Recording is still active. Leave anyway?\":\"La registrazione è ancora attiva. Partire comunque?\",\"Recording center\":\"Centro di registrazione\",\"No active recordings\":\"Nessuna registrazione attiva\",\"Waiting for source\":\"In attesa della fonte\",\"Saved segments\":\"Segmenti salvati\",\"Duration\":\"Durata\",\"Bitrate\":\"Velocità in bit\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Formato: MP4 se supportato da questo browser; altrimenti il ​​fallback meglio supportato.\",\"Record current page\":\"Registra la pagina corrente\",\"Record current group\":\"Registra il gruppo corrente\",\"Record online rooms\":\"Registra stanze online\",\"Stop all recordings\":\"Interrompi tutte le registrazioni\",\"Show recording only\":\"Mostra solo la registrazione\",\"Show all rooms\":\"Mostra tutte le stanze\",\"Open current page rooms\":\"Apri le stanze della pagina corrente\",\"Move current page to group\":\"Sposta la pagina corrente nel gruppo\",\"Layout settings\":\"Impostazioni del layout\",\"Recording settings\":\"Impostazioni di registrazione\",\"Save settings\":\"Salva impostazioni\",\"Config backups\":\"Backup della configurazione\",\"No config backups found\":\"Nessun backup di configurazione trovato\",\"Online/offline history\":\"Cronologia online/offline\",\"Group rules\":\"Regole del gruppo\",\"Keep favorites first\":\"Mantieni prima i preferiti\",\"Temporary URL manager\":\"Gestore URL temporaneo\",\"Save current temporary URLs\":\"Salva gli URL temporanei correnti\",\"No temporary URLs\":\"Nessun URL temporaneo\",\"Share workspace\":\"Condividi lo spazio di lavoro\",\"Share current rooms and layout\":\"Condividi le stanze e il layout attuali\",\"Copy share link\":\"Copia il collegamento di condivisione\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Questo collegamento ripristina le stanze, i gruppi e le impostazioni di layout in un'altra scheda della workstation.\",\"Shared workspace imported\":\"Area di lavoro condivisa importata\",\"Shared workspace link is invalid\":\"Il collegamento all'area di lavoro condivisa non è valido\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Fare clic su un campo e premere la nuova scorciatoia. Backspace cancella quel campo.\",\"Reset shortcuts\":\"Reimposta le scorciatoie\",\"Focus add box\":\"Metti a fuoco la casella di aggiunta\",\"Grid view\":\"Visualizzazione griglia\",\"Focus view\":\"Vista focalizzata\",\"Clean mode\":\"Modalità pulita\",\"Window-first mode\":\"Modalità prima finestra\",\"Toggle thumbnails\":\"Attiva/disattiva le miniature\",\"Auto-detecting…\":\"Rilevamento automatico...\",\"No rooms in this group\":\"Nessuna stanza in questo gruppo\",\"Add a username at the top, switch group, or clear filters\":\"Aggiungi un nome utente in alto, cambia gruppo o cancella i filtri\",\"Open workstation\":\"Postazione di lavoro aperta\",\"Open here (replace page)\":\"Apri qui (sostituisci pagina)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Sostituire questa pagina con la workstation? Puoi tornare indietro tramite il browser.\",\"View saved list\":\"Visualizza l'elenco salvato\",\"Collapse dock\":\"Crollo del bacino\",\"No room detected\":\"Nessuna stanza rilevata\",\"Open here\":\"Apri qui\",\"Add room\":\"Aggiungi stanza\",\"Remove room\":\"Rimuovi stanza\",\"Record in workstation\":\"Registra nella workstation\",\"Screenshot video\":\"Schermata video\",\"Play / pause\":\"Riproduci/metti in pausa\",\"Mute / unmute\":\"Disattiva/attiva l'audio\",\"No playable video found on this page\":\"Nessun video riproducibile trovato in questa pagina\",\"Recording intent queued in workstation\":\"Intento di registrazione in coda nella workstation\",\"Added\":\"Aggiunto\",\"Already exists\":\"Esiste già\",\"Failed\":\"Fallito\",\"Add to workstation\":\"Aggiungi alla stazione di lavoro\",\"Already in workstation — click to remove\":\"Già nella workstation: fare clic per rimuovere\",\"Model online\":\"Modello in linea\",\"Model offline\":\"Modello offline\",\"Show fewer\":\"Mostra meno\",\"No measurable audio at the moment\":\"Nessun audio misurabile al momento\",\"Playback paused\":\"La riproduzione è stata messa in pausa\",\"Audio measurement unavailable or blocked\":\"Misurazione audio non disponibile o bloccata\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Autorizzazione di notifica negata. Il flash della scheda funziona ancora.\\n\\nPuoi abilitarlo nelle impostazioni del browser.\",\"Batch add\":\"Aggiunta batch\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Incolla i nomi utente, uno per riga o separati da spazi/virgole:\",\"Cancel\":\"Annulla\",\"Change the language of RoomGrid and the website\":\"Cambia la lingua di RoomGrid e del sito web\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Cambia la lingua di RoomGrid. Chaturbate rimane nella lingua corrente del sito Web perché non è disponibile alcuna lingua ufficiale del sito.\",\"About\":\"Circa\",\"Export config\":\"Esporta configurazione\",\"Export usernames.txt\":\"Esporta nomiutente.txt\",\"Copy usernames\":\"Copia nomi utente\",\"Unmute all\":\"Riattiva tutto\",\"⏸ Pause visible\":\"⏸ Pausa visibile\",\"Resume visible\":\"Curriculum vitae visibile\",\"Following\":\"Segue\",\"Sync followed models now\":\"Sincronizza i modelli seguiti ora\",\"✓ Auto-sync followed models\":\"✓ Sincronizzazione automatica dei modelli seguiti\",\"☐ Auto-sync followed models\":\"☐ Sincronizzazione automatica dei modelli seguiti\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importa i tuoi modelli Chaturbate attualmente seguiti nel gruppo Segui persistente. Solo quel gruppo è sincronizzato.\",\"Syncing followed models…\":\"Sincronizzazione dei modelli seguiti…\",\"Automatic Following sync disabled\":\"Sincronizzazione automatica del seguito disabilitata\",\"Automatic Following sync enabled\":\"Sincronizzazione automatica del seguito abilitata\",\"Following sync needs an active Chaturbate login\":\"La sincronizzazione successiva richiede un accesso Chaturbate attivo\",\"Following sync failed safely; saved workspace data was not changed\":\"La sincronizzazione successiva non è riuscita in modo sicuro; i dati dell'area di lavoro salvati non sono stati modificati\",\"Enable dark mode\":\"Abilita la modalità oscura\",\"Disable dark mode\":\"Disabilita la modalità oscura\",\"Status history\":\"Cronologia dello stato\",\"Temporary URLs\":\"URL temporanei\",\"Shortcut panel\":\"Pannello di scelta rapida\",\"Toggle video fit\":\"Attiva/disattiva l'adattamento del video\",\"Shortcuts / hints\":\"Scorciatoie/suggerimenti\",\"Ranking\":\"Classifica\",\"Model details\":\"Dettagli del modello\",\"My profile\":\"Il mio profilo\",\"Loading local statistics…\":\"Caricamento statistiche locali…\",\"No watch data yet. Statistics start with this version.\":\"Nessun dato sull'orologio ancora. Le statistiche iniziano con questa versione.\",\"Statistics are disabled.\":\"Le statistiche sono disabilitate.\",\"Enable statistics\":\"Abilita le statistiche\",\"Disable statistics\":\"Disattiva le statistiche\",\"Export statistics\":\"Esporta statistiche\",\"Delete all statistics\":\"Elimina tutte le statistiche\",\"Delete this model data\":\"Elimina questi dati del modello\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Eliminare tutte le statistiche sugli orologi raccolte localmente? Il tuo spazio di lavoro rimane intatto.\",\"Tracking since\":\"Monitoraggio da allora\",\"Models watched\":\"Le modelle guardavano\",\"Watch time\":\"Guarda il tempo\",\"Most watched tags\":\"Tag più visti\",\"Most watched hours\":\"Ore più viste\",\"Observed online\":\"Osservato in linea\",\"Public bio\":\"Biografia pubblica\",\"Public links\":\"Collegamenti pubblici\",\"No public profile data cached yet.\":\"Nessun dato del profilo pubblico ancora memorizzato nella cache.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Contano solo le carte visibili e attive. Le anteprime al passaggio del mouse non contano.\",\"Statistics imported\":\"Statistiche importate\",\"Statistics deleted\":\"Statistiche cancellate\",\"Statistics exported\":\"Statistiche esportate\",\"Local observation only\":\"Solo osservazione locale\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Non vengono raccolti messaggi privati, token, pagamenti, password o tracciamenti esterni.\",\"Not enough observations for a time estimate\":\"Osservazioni insufficienti per una stima del tempo\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Passa il mouse su qualsiasi pulsante/controllo per visualizzarne il suggerimento. Le scorciatoie comuni possono essere modificate nel pannello delle scorciatoie.\\n\\nImpostazioni predefinite:\\n/ Evidenzia l'immissione del nome utente\\nr Aggiorna tutto\\ng Visualizzazione griglia\\nf Visualizzazione messa a fuoco\\nAlt+P o Alt+C Modalità pulizia attiva/disattiva\\nAlt+Maiusc+C Centro di registrazione\\nAlt+Shift+R Registra la pagina corrente\\nEsc Esce dalla modalità pulizia/schermo intero\\nSpazio Mette in pausa/riprende la schermata principale nella vista Focus\\n←/→ o [/ ] Cambia la schermata principale in visualizzazione Focus\\nFare doppio clic sulla scheda a schermo intero\",\"Visible windows paused\":\"Le finestre visibili furono messe in pausa\",\"Visible windows resumed\":\"Le finestre visibili sono state ripristinate\",\"Import config\":\"Importa configurazione\",\"Repair saved data\":\"Ripara i dati salvati\",\"Saved data has been normalized.\":\"I dati salvati sono stati normalizzati.\",\"Clear all data\":\"Cancella tutti i dati\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Ciò cancellerà TUTTE le stanze, i gruppi e le impostazioni. Continuare?\",\"English\":\"Inglese\",\"Deutsch\":\"Tedesco\",\"Author\":\"Autore\",\"License\":\"Licenza\",\"If this script saves you time…\":\"Se questo script ti fa risparmiare tempo...\",\"Tip in ETH\":\"Suggerimento in ETH\",\"ETH address\":\"Indirizzo ETH\",\"Copy\":\"Copia\",\"HD\":\"HD\",\"Inner spacing\":\"Spaziatura interna\",\"Inspiration, not repetition\":\"Ispirazione, non ripetizione\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Permette di utilizzare, modificare e distribuire il software. Gli avvisi sul copyright e sulla licenza devono rimanere e il software viene fornito senza garanzia. Camclient è stato creato da Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Vive per poco tempo e già attira l'attività pubblica.\",\"Live stream audio activity\":\"Attività audio in live streaming\",\"Local watch\":\"Orologio locale\",\"Logo background\":\"Sfondo del logo\",\"Logo height\":\"Altezza del logo\",\"Logo width\":\"Larghezza del logo\",\"Loud\":\"Forte\",\"Measured even while output is muted\":\"Misurato anche mentre l'uscita è disattivata\",\"Micro\":\"Micro\",\"Middle-click or Ctrl+click opens a new tab\":\"Il clic con il tasto centrale o Ctrl+clic apre una nuova scheda\",\"Models you have visited locally and that are public again now.\":\"Modelli che hai visitato localmente e che ora sono di nuovo pubblici.\",\"Mono\":\"Mono\",\"Accent gradient\":\"Gradiente di accento\",\"Active filters\":\"Filtri attivi\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Aggiungi le tue immagini o utilizza il pulsante screenshot nel lettore. Gli screenshot possono entrare automaticamente nella galleria e possono essere utilizzati direttamente come copertina del modello.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Tutti i filtri della home page, il layout, l'anteprima e le impostazioni di impaginazione vengono archiviati nell'archivio delle impostazioni stabili e sopravvivono agli aggiornamenti degli script.\",\"All live models\":\"Tutti i modelli dal vivo\",\"All suggestions\":\"Tutti i suggerimenti\",\"Almost silent\":\"Quasi silenzioso\",\"Always active\":\"Sempre attivo\",\"AND\":\"E\",\"Aurora\":\"Aurora\",\"Automatic example\":\"Esempio automatico\",\"Automatic segments\":\"Segmenti automatici\",\"Automatically loading the complete catalog…\":\"Caricamento automatico del catalogo completo...\",\"Available recording data was saved.\":\"I dati di registrazione disponibili sono stati salvati.\",\"Back online\":\"Di nuovo in linea\",\"Continuing recording\":\"Continuando la registrazione\",\"current matches\":\"partite attuali\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Personalizza il nome, il marchio, lo slogan e il logo locale di Camclient. I loghi rimangono in questo browser.\",\"Discover something new\":\"Scopri qualcosa di nuovo\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Le corsie di raccomandazione distinte ti aiutano a trovare qualcosa di nuovo invece di ripetere il catalogo della home page.\",\"Do not enlarge\":\"Non ingrandire\",\"Eco\":\"Eco\",\"Enable meter\":\"Abilita contatore\",\"Fast starters\":\"Antipasti veloci\",\"Fill and crop\":\"Riempi e ritaglia\",\"Filter suggestions\":\"Filtra suggerimenti\",\"Fit completely\":\"Si adatta perfettamente\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Per la trasparenza, utilizza PNG, WebP o SVG sicuro e scegli Trasparente come sfondo del logo. JPEG non ha trasparenza. Stretch utilizza la larghezza e l'altezza scelte in modo indipendente.\",\"For you\":\"Per te\",\"gallery\":\"galleria\",\"Audio\":\"Audio\",\"Desktop\":\"Desktop\",\"Public live models with no local watch time in Camclient.\":\"Modelli live pubblici senza orario di visualizzazione locale in Camclient.\",\"Quiet\":\"Tranquillo\",\"Quota\":\"Quota\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Classificato in base al numero attuale di spettatori pubblici; i dati del catalogo non vengono archiviati in modo permanente.\",\"Recording is still active.\":\"La registrazione è ancora attiva.\",\"Recording quality\":\"Qualità di registrazione\",\"Recording stopped\":\"La registrazione è stata interrotta\",\"Recording unavailable\":\"Registrazione non disponibile\",\"Reset all\":\"Reimposta tutto\",\"RoomGrid Model library\":\"Libreria di modelli RoomGrid\",\"RoomGrid My user\":\"RoomGrid Il mio utente\",\"RoomGrid Statistics\":\"Statistiche RoomGrid\",\"Save to model gallery\":\"Salva nella galleria dei modelli\",\"Saved segment\":\"Segmento salvato\",\"Screenshot\":\"Schermata\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"In base ai preferiti, ai tag e all'attività di visualizzazione salvati localmente. Ogni scelta mostra la sua ragione.\",\"Black\":\"Nero\",\"Brave exposed no recordable video track.\":\"Brave non ha rivelato alcuna traccia video registrabile.\",\"Brave or the video source blocked frame capture.\":\"Brave o la sorgente video hanno bloccato l'acquisizione dei fotogrammi.\",\"Browser exposed no audio track\":\"Il browser non ha mostrato alcuna traccia audio\",\"Camclient as the real homepage\":\"Camclient come la vera home page\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient assegna automaticamente questi esempi trasparenti ai tag comuni. Utilizza il pulsante immagine su qualsiasi tag per scegliere un esempio, caricare il tuo PNG/WebP/SVG o nascondere l'immagine.\",\"Choose a Camclient example or upload your own transparent image.\":\"Scegli un esempio di Camclient o carica la tua immagine trasparente.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Scegli un PNG, JPEG, WebP o SVG sicuro di dimensioni inferiori a 1,1 MB.\",\"Choose artwork for tag\":\"Scegli la grafica per il tag\",\"Cinema\":\"Cinema\",\"Click to enable meter\":\"Fare clic per abilitare il contatore\",\"Close more actions\":\"Chiudi più azioni\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Colori, tipografia, spaziatura, angoli, ombre e sfondi si applicano immediatamente alle Impostazioni, all'area di lavoro, a Scopri e alla libreria dei modelli.\",\"Continue loading the complete catalog\":\"Prosegui caricando il catalogo completo\",\"New discoveries\":\"Nuove scoperte\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nuovo nel catalogo, appena visto da Camclient o appena iniziato lo streaming.\",\"No audio track\":\"Nessuna traccia audio\",\"No filters active · all matching live models are shown\":\"Nessun filtro attivo · vengono mostrati tutti i modelli live corrispondenti\",\"No recommendations match the active filters.\":\"Nessun consiglio corrisponde ai filtri attivi.\",\"No recording data\":\"Nessun dato di registrazione\",\"NOT\":\"NON\",\"Only new\":\"Solo nuovo\",\"Open Camclient homepage\":\"Apri la home page di Camclient\",\"OR\":\"O\",\"part\":\"parte\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Scelte personali, nuove scoperte, sale live popolari e modelli che non hai mai visitato, chiaramente separati dal catalogo completo della home page.\",\"Place\":\"Luogo\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP e SVG sicuri supportano la trasparenza. Le immagini vengono ridimensionate localmente a 144 × 144 pixel e salvate con le tue impostazioni.\",\"Popular now\":\"Popolare ora\",\"Privacy\":\"Privacy\",\"Camclient\":\"Camclient\",\"Remix\":\"Remix\",\"Screenshot failed\":\"Schermata non riuscita\",\"Screenshot format\":\"Formato schermata\",\"Screenshot resolution\":\"Risoluzione dello schermo\",\"Screenshot unavailable\":\"Schermata non disponibile\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Gli screenshot vengono scaricati e possono anche essere inseriti nella galleria dei modelli locali.\",\"segment(s)\":\"segmento(i)\",\"Show no image\":\"Non mostrare alcuna immagine\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Mostra l'attività nella traccia audio dello streaming indipendentemente dal fatto che l'output del lettore sia disattivato.\",\"Source ended\":\"La fonte è terminata\",\"Source resolution\":\"Risoluzione della sorgente\",\"Start recording\":\"Inizia a registrare\",\"Starter pick · popular now\":\"Scelta iniziale · popolare ora\",\"Stop and save\":\"Fermati e risparmia\",\"Stretch\":\"Allungare\",\"Tag artwork and emoji\":\"Tagga grafica ed emoji\",\"Tag artwork editor\":\"Tag editor di grafica\",\"The compressed image is still too large.\":\"L'immagine compressa è ancora troppo grande.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"L'intestazione mostra solo Home, Discover, Tag Explorer, Centro statistiche, Libreria modelli, Il mio utente e Impostazioni. Ogni area di configurazione è disponibile come sottopagina Impostazioni.\",\"The image could not be decoded.\":\"Impossibile decodificare l'immagine.\",\"The image could not be read.\":\"Impossibile leggere l'immagine.\",\"The image format is unsafe or unsupported.\":\"Il formato dell'immagine non è sicuro o non è supportato.\",\"The lanes respect your active search and filter settings.\":\"Le corsie rispettano le impostazioni di ricerca e filtro attive.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"La registrazione viene salvata solo su questo dispositivo. Continua solo se hai il permesso di salvare questo contenuto.\",\"The video has no usable frame yet.\":\"Il video non ha ancora un frame utilizzabile.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Non ci sono ancora dati sufficienti sulle preferenze locali, quindi queste sono scelte iniziali trasparenti basate sulla popolarità attuale.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Queste impostazioni controllano il layout della home page di Camclient, l'impaginazione, le informazioni sul modello, i filtri e le prestazioni.\",\"Transparent\":\"Trasparente\",\"Unified main navigation\":\"Navigazione principale unificata\",\"Unvisited\":\"Non visitato\",\"Upload own image\":\"Carica la tua immagine\",\"URL\":\"URL\",\"Use as model cover\":\"Utilizzare come copertina del modello\",\"Use the complete search, filters, and sorting for every current result.\":\"Utilizza la ricerca completa, i filtri e l'ordinamento per ogni risultato corrente.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Visitando la home page di Chaturbate si apre sempre la home page completa di Camclient. Ogni area principale rimane all'interno della stessa interfaccia personalizzata.\",\"Volume\":\"Volume\",\"Waiting for audio\":\"In attesa dell'audio\",\"What does the MIT license mean?\":\"Cosa significa la licenza MIT?\",\"White\":\"Bianco\",\"Your discovery mix\":\"Il tuo mix di scoperte\",\"中文\":\"中文\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Scopri le stanze pubbliche in diretta\",\"all groups\":\"tutti i gruppi\",\"at least one group\":\"almeno un gruppo\",\"no measurements yet\":\"nessuna misurazione disponibile\",\"Country, region or location\":\"Paese, regione o luogo\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Controlla l’ordine dei modelli, i filtri, il numero per pagina, le dimensioni delle schede e le informazioni visibili direttamente nella pagina iniziale originale di Chaturbate. Ogni modifica viene applicata subito e salvata in questo browser.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] fa\",\"[[ROOMGRID_VALUE_0]] preview\":\"Anteprima [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] profilo salvato\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Dettagli del modello per @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] modelli seguiti importati\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"Elementi My Collection riconosciuti: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"[[ROOMGRID_VALUE_0]] modelli visibili su [[ROOMGRID_VALUE_1]] attualmente caricati.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Filtro gruppo attivo: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] schede corrispondenti su [[ROOMGRID_VALUE_2]] ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Copertina personalizzata · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Copertina salvata localmente · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Immagini e copertine di esempio ([[ROOMGRID_VALUE_0]] salvate)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Memorizzato in modo permanente su questo dispositivo · ultimo [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Immagine personale · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Salvato [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Prima visualizzazione: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Ultima visualizzazione: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Ultima diretta: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Apparizioni: [[ROOMGRID_VALUE_0]]× · prima [[ROOMGRID_VALUE_1]] · ultima [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Orario tipico: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Dati tecnici: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] osservazioni live locali, al massimo una ogni 5 minuti\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] del tempo di visione precedente come stima prudente\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Rimuovi tag [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Gestisci gruppi personalizzati · assegnato a [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Eliminare il gruppo “[[ROOMGRID_VALUE_0]]”? I modelli resteranno nella libreria.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Gruppo “[[ROOMGRID_VALUE_0]]” eliminato\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] gruppi · selezione multipla possibile\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] tag salvati\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Rimuovi filtro [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] corrisponde\",\"[[ROOMGRID_VALUE_0]] filters active\":\"Filtri [[ROOMGRID_VALUE_0]] attivi\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Le informazioni pubbliche del profilo possono essere caricate tramite “Tutti i dettagli”.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Organizzato personalmente dal: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Catalogato automaticamente dal: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Ultima attività: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Apparizioni: [[ROOMGRID_VALUE_0]]× · ultima registrata [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Scoperti: [[ROOMGRID_VALUE_0]] · misurazioni qualità: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Informazioni profilo: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] archiviato\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] ripristinato\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Pagina [[ROOMGRID_VALUE_0]] di [[ROOMGRID_VALUE_1]] · modelli: [[ROOMGRID_VALUE_2]]\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] salvati\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] guardati\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] attivi · cresce automaticamente\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"modelli [[ROOMGRID_VALUE_0]] · sessioni [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] preferiti\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] modelli distinti\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] anteprime\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Anteprima/visualizzazione rapida dei modelli [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] giorni\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Serie di [[ROOMGRID_VALUE_0]] giorni\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] guardati ripetutamente\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] giorni di dati locali\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"media ponderata [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] in diretta ora\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modelli riconosciuti\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Non è stato possibile leggere tutto: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Giorno più attivo: [[ROOMGRID_VALUE_0]] con [[ROOMGRID_VALUE_1]]. Conservazione: [[ROOMGRID_VALUE_2]] giorni per gli eventi grezzi; i valori più vecchi possono restare come totali giornalieri.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sessioni\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] salvati localmente\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Impossibile caricare i dati locali: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] è online.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] è offline.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] non è online da un po’ di tempo.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] preferiti sono online insieme.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Il profilo pubblico di [[ROOMGRID_VALUE_0]] è cambiato.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"Modelli [[ROOMGRID_VALUE_0]] aggiornati\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Anteprima importazione statistiche (nessuna modifica):\\n[[ROOMGRID_VALUE_0]] modelli\\n[[ROOMGRID_VALUE_1]] eventi\\n[[ROOMGRID_VALUE_2]] profili pubblici\\n\\nContinuando verranno sostituite le statistiche esistenti.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Anteprima live di [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] aggiunto a MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Eliminare [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Importare [[ROOMGRID_VALUE_0]] viste e [[ROOMGRID_VALUE_1]] feed?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] stanze aggiunte all’area di lavoro\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modelli salvati\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] risultati corrispondenti\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] modelli dal vivo corrispondenti\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Condivide il n. [[ROOMGRID_VALUE_0]] con un preferito\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] immagini aggiunte; Impossibile elaborare [[ROOMGRID_VALUE_1]].\",\"[[ROOMGRID_VALUE_0]] images added.\":\"Immagini [[ROOMGRID_VALUE_0]] aggiunte.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] stanze\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] nomi utente validi\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Pagina [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], stanze: [[ROOMGRID_VALUE_2]]\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"URL temporanei: [[ROOMGRID_VALUE_0]] aggiunti, [[ROOMGRID_VALUE_1]] non riusciti\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Anteprima importazione statistiche: modelli [[ROOMGRID_VALUE_0]] ed eventi [[ROOMGRID_VALUE_1]]. Continuando unisci in modo sicuro i dati e crea prima un backup.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"I layout personalizzati supportano fino a [[ROOMGRID_VALUE_0]] stanze.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Scena “[[ROOMGRID_VALUE_0]]” salvata\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Scena “[[ROOMGRID_VALUE_0]]” caricata\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] flussi attivi · [[ROOMGRID_VALUE_1]] instabile · [[ROOMGRID_VALUE_2]] da guardare\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] flussi · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assegnato\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Eliminare la scena “[[ROOMGRID_VALUE_0]]”?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Salvato solo per [[ROOMGRID_VALUE_0]]: fino a [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Questa cam utilizza la qualità globale/adattativa[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] è stato sostituito da [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Riproduzione audio solo da [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Inserito nello spazio Focus [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Inserito nella finestra griglia [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Risoluzione [[ROOMGRID_VALUE_0]], stato [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Stato dello streaming [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modello/i\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Rimuovi filtro: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Corrisponde ai tuoi interessi: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catalogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] recuperato · [[ROOMGRID_VALUE_2]] modelli live\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modelli locali\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catalogo [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modelli dal vivo\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] stanze catalogo caricate\",\"Detected via: [[ROOMGRID_VALUE_0]]\":\"Rilevato tramite: [[ROOMGRID_VALUE_0]]\",\"Sources: [[ROOMGRID_VALUE_0]]\":\"Fonti: [[ROOMGRID_VALUE_0]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing replaces the current statistics.\":\"Anteprima importazione: [[ROOMGRID_VALUE_0]] modelli e [[ROOMGRID_VALUE_1]] eventi. Continuando verranno sostituite le statistiche attuali.\",\"[[ROOMGRID_VALUE_0]] ms startup · [[ROOMGRID_VALUE_1]] stalls · [[ROOMGRID_VALUE_2]]% dropped frames\":\"avvio [[ROOMGRID_VALUE_0]] ms · [[ROOMGRID_VALUE_1]] interruzioni · [[ROOMGRID_VALUE_2]]% fotogrammi persi\"},\"visible\":{\"Automatisch neu verbinden\":\"Riconnettersi automaticamente\",\"Bei verstecktem Tab pausieren\":\"Pausa mentre la scheda è nascosta\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Applica i dettagli dell'account Chaturbate esistente\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Ottimizza l'immagine · ampio zoom, posizione, filtro e rotazione\",\"Bildanpassung\":\"Regolazione dell'immagine\",\"Bildwerkzeuge einklappen\":\"Comprimi gli strumenti immagine\",\"Breite des Informationsbereichs\":\"Larghezza dell'area informazioni\",\"Broadcaster-Verbindung verwalten\":\"Gestisci la connessione dell'emittente\",\"Chat anzeigen\":\"Mostra chat\",\"Ctrl + Shift + X\":\"Ctrl+Maiusc+X\",\"Ctrl+K\":\"CTRL+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Ciò significa che non sei limitato ai dati da quando lo script è stato installato: RoomGrid può leggere il tuo elenco seguente esistente e le voci chiaramente riconoscibili da La mia raccolta. Nessun messaggio, pagamento, supporto o password viene copiato.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio contiene anche confronti di periodi di tempo, mappe di calore, canalizzazioni di scoperta, analisi dello spazio di lavoro e della qualità, nonché esportazioni di dati.\",\"Datenschutz & Export\":\"Protezione dei dati ed esportazione\",\"Deine Aktivität der letzten 14 Tage\":\"La tua attività negli ultimi 14 giorni\",\"Deine meistgesehenen Models\":\"I tuoi modelli più visti\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"La connessione corrente è attiva; Dopo la misurazione successiva, qui appare il percorso locale.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Questa visualizzazione della libreria è ancora vuota.\",\"Eigene Priorität\":\"La propria priorità\",\"Eigene Tags\":\"Tag propri\",\"Eigene Tags filtern\":\"Filtra i tuoi tag\",\"Eigenen Tag eingeben …\":\"Inserisci la tua giornata...\",\"Einstellungen\":\"Impostazioni\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Inserisci o \\\"Aggiungi tag\\\" prende il controllo della bozza. Viene salvato in modo permanente solo con \\\"Salva profilo\\\". I tag già utilizzati vengono suggeriti durante la digitazione.\",\"Ereignisse im Zeitraum\":\"Eventi del periodo\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Crea raccolte con nome libero, assegna modelli a più gruppi e visualizza uno o più gruppi insieme qui.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Per prima cosa crea il tuo gruppo sopra.\",\"Follower\":\"Seguaci\",\"GAMING\":\"GIOCO\",\"Gesamter Verlauf\":\"Intero corso\",\"Grid +\":\"Griglia+\",\"Größe der Bedienelemente\":\"Dimensione dei controlli\",\"Offizielle Statistik verbinden\":\"Collega le statistiche ufficiali\",\"Optionale offizielle Kontostatistik\":\"Statistiche ufficiali facoltative dell'account\",\"Persönlich organisiert\":\"Organizzato personalmente\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Lettore e connessione\",\"Player neu verbinden\":\"Ricollegare il lettore\",\"Player-Ecken\":\"Angoli del giocatore\",\"Player-Hintergrund\":\"Sfondo del giocatore\",\"Player-Höhe\":\"Altezza del giocatore\",\"Player-Seitenverhältnis\":\"Proporzioni del giocatore\",\"Position der Informationen\":\"Posizione delle informazioni\",\"Private Notiz\":\"Nota privata\",\"Private Notizen anzeigen\":\"Visualizza note private\",\"Puffergröße\":\"Dimensione del buffer\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"‹ Zurück\":\"‹ Indietro\",\"● 1.2k · #tag\":\"● 1.2k · #tag\",\"1 Jahr\":\"1 anno\",\"30 Tage\":\"30 giorni\",\"7 Tage\":\"7 giorni\",\"90 Tage\":\"90 giorni\",\"AA ×\":\"AA×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Mostra modelli simili\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Tutte le modifiche vengono salvate localmente e si applicano anche al modello successivo.\",\"Alle eigenen Tags\":\"Tutti i tag personalizzati\",\"Alle Gruppen\":\"Tutti i gruppi\",\"Alt+M / Alt+A\":\"Alt+M/Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Richiedi dati Chaturbate più vecchi\",\"Automatisch abspielen\":\"Riproduzione automatica\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Catturato automaticamente · completamente organizzato durante la modifica\",\"Model ist in allen gewählten Gruppen\":\"Il modello è in tutti i gruppi selezionati\",\"Model ist in mindestens einer gewählten Gruppe\":\"Il modello è in almeno un gruppo selezionato\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Apri La mia collezione\",\"Namen speichern\":\"Salva nome\",\"Nicht verbunden\":\"Non connesso\",\"Niedrige Live-Latenz\":\"Bassa latenza live\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Non ci sono ancora valori di utilizzo locale sufficienti.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Nessun gruppo nostro ancora. Inserisci un nome sopra e crea la tua prima collezione.\",\"Noch keine Model-Sehzeit erfasst.\":\"Nessun tempo di visione del modello ancora registrato.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Nessuna istantanea ufficiale dell'account ancora.\",\"Noch nicht eingelesen\":\"Non ancora letto\",\"Nur auf diesem Gerät\":\"Solo su questo dispositivo\",\"Öffentliche/erkannte Tags\":\"Tag pubblici/riconosciuti\",\"Öffentliches Profil anzeigen\":\"Visualizza il profilo pubblico\",\"Gruppe löschen\":\"Elimina gruppo\",\"Gruppen umbenennen oder löschen\":\"Rinominare o eliminare gruppi\",\"HD + Gaming\":\"Giochi HD+\",\"IndexedDB\":\"DB indicizzato\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Ogni tag salvato è automaticamente disponibile nella ricerca, nel completamento automatico e qui come filtro multiplo diretto.\",\"Keine Models passen zu diesen Filtern.\":\"Nessun modello corrisponde a questi filtri.\",\"Keine Priorität\":\"Non è una priorità\",\"Konto-Daten werden eingelesen …\":\"I dettagli dell'account vengono letti in...\",\"Lautstärke pro Model merken\":\"Notare il volume per modello\",\"Layout & sichtbare Bereiche\":\"Layout e aree visibili\",\"Letzte Messung\":\"Ultima misurazione\",\"LIVE\":\"VIVI\",\"lokal · für Suche und Filter\":\"locale · per ricerca e filtri\",\"Maximale Stream-Lautstärke\":\"Volume massimo del flusso\",\"Model besitzt alle gewählten Tags\":\"Il modello possiede tutti i tag selezionati\",\"Model besitzt mindestens einen gewählten Tag\":\"Il modello ha almeno un tag selezionato\",\"Verbunden\":\"Connesso\",\"Vollständige Modelstatistik\":\"Statistiche complete del modello\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Leggi subito i dati dell'account esistente\",\"Weiter ›\":\"Successivo ›\",\"Zufriedenheit\":\"Soddisfazione\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid non può indovinare i valori dell'account Chaturbate personale. Queste carte appaiono solo se hai collegato consapevolmente le statistiche ufficiali dell'emittente al tuo nome utente e token.\",\"RoomGrid Suite\":\"Suite RoomGrid\",\"Seitengröße\":\"Dimensioni della pagina\",\"SingleCam\":\"Camera singola\",\"SingleCam einstellen\":\"Configura SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Ripristina le impostazioni predefinite di SingleCam\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Non appena salvi il tuo tag per un modello, verrà visualizzato qui e nel completamento automatico.\",\"Status im Player anzeigen\":\"Mostra lo stato nel lettore\",\"Status-Aktualisierung\":\"Aggiornamento dello stato\",\"Stream immer stumm starten\":\"Avvia sempre lo streaming in modalità silenziosa\",\"Tag hinzufügen\":\"Aggiungi etichetta\",\"Tipps letzte Stunde\":\"Suggerimenti dell'ultima ora\",\"Token-Guthaben\":\"Saldo dei gettoni\",\"Ton & Ruhe\":\"Suono e calma\",\"Ungefähre Onlinezeiten\":\"Tempi online approssimativi\",\"Gruppen\":\"Gruppi\",\"Schließen\":\"Chiudi\",\"Startseite nach eigenen Gruppen filtern\":\"Filtra la home page in base ai tuoi gruppi\",\"RoomGrid Komfortzentrum\":\"Centro comfort RoomGrid\",\"Mein Nutzer\":\"Il mio profilo\",\"Mein Nutzer-Dashboard\":\"Dashboard del mio profilo\",\"Modellbibliothek\":\"Libreria modelli\",\"Große Modellbibliothek\":\"Grande libreria modelli\",\"Statistikzentrum\":\"Centro statistiche\",\"Sitzungen\":\"Sessioni\",\"Dock + Hover\":\"Dock + passaggio del mouse\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Locale e privato su questo dispositivo\",\"Sortierung\":\"Ordina per\",\"Meiste Sehzeit\":\"Più tempo di visione\",\"Zuletzt aktiv\":\"Attività recente\",\"Meiste Sitzungen\":\"Più sessioni\",\"Meiste Zuschauer\":\"Più spettatori\",\"Status\":\"Stato\",\"Alle Status\":\"Tutti gli stati\",\"Nur live\":\"Solo in diretta\",\"Nur Live\":\"Solo in diretta\",\"Offline\":\"Offline\",\"Unbekannt\":\"Sconosciuto\",\"Nur Bibliothek\":\"Solo libreria\",\"Pro Seite\":\"Per pagina\"}}","ru":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Переключение языка невозможно подготовить безопасно. Эта страница не была оставлена.\",\"Language\":\"Язык\",\"Default workspace\":\"Рабочая область по умолчанию\",\"Workspace\":\"Рабочая область\",\"New workspace\":\"Новое рабочее пространство\",\"copy\":\"копировать\",\"New group\":\"Новая группа\",\"Saved model\":\"Сохраненная модель\",\"Search models, pages, workspaces and commands…\":\"Поиск моделей, страниц, рабочих пространств и команд…\",\"Type to filter · Enter to run · Esc to close\":\"Введите текст для фильтрации · Введите для запуска · Esc для закрытия\",\"No matches\":\"Нет совпадений\",\"Currently active RoomGrid version\":\"Текущая активная версия RoomGrid\",\"years\":\"лет\",\"Live\":\"Живи\",\"Private\":\"Частный\",\"Away\":\"Отошёл\",\"Offline\":\"Оффлайн\",\"sessions\":\"сессии\",\"NEW\":\"НОВЫЙ\",\"Name, subject, tag or place\":\"Имя, тема, тег или место\",\"Website order\":\"Порядок сайта\",\"Most viewers\":\"Больше всего зрителей\",\"Fewest viewers\":\"Меньше всего зрителей\",\"Longest online\":\"Дольше всего в сети\",\"Recently online\":\"Недавно в сети\",\"Age ascending\":\"Возраст: по возрастанию\",\"Age descending\":\"Возраст: по убыванию\",\"New models first\":\"Сначала новые модели\",\"Saved first\":\"Сначала сохранённые\",\"Favorites first\":\"Сначала избранные\",\"Most watched\":\"Самые просматриваемые\",\"Recently watched\":\"Недавно просмотренные\",\"Random mix\":\"Случайное перемешивание\",\"All loaded\":\"Все загруженные\",\"Previous page\":\"Предыдущая страница\",\"Next page\":\"Следующая страница\",\"Reload complete live list\":\"Обновить полный список в реальном времени\",\"All homepage options\":\"Все настройки главной страницы\",\"Reset filters\":\"Сбросить фильтры\",\"Quick filter\":\"Быстрый фильтр\",\"Sort\":\"Сортировать\",\"Models per page\":\"Моделей на странице\",\"Load more results\":\"Загрузить больше результатов\",\"still to display\":\"еще показать\",\"continues automatically while scrolling\":\"продолжается автоматически во время прокрутки\",\"visible\":\"видно\",\"loaded\":\"загружено\",\"page\":\"страница\",\"active filters\":\"активных фильтров\",\"loading complete live list\":\"загрузка полного живого списка\",\"all matching live models found\":\"найдены все подходящие живые модели\",\"live list incomplete, retry\":\"текущий список неполный, повторите попытку\",\"Requests temporarily paused\":\"Запросы временно приостановлены\",\"Vault could not be unlocked\":\"Не удалось разблокировать хранилище\",\"Preview Dock\":\"Предварительный просмотр Dock\",\"Connecting…\":\"Подключение…\",\"Stream temporarily unavailable\":\"Трансляция временно недоступна\",\"Error\":\"Ошибка\",\"Buffering\":\"Буферизация\",\"Preview unavailable\":\"Предварительный просмотр недоступен\",\"Connect\":\"Подключиться\",\"Close\":\"Закрыть\",\"Opacity\":\"Непрозрачность\",\"Info\":\"Информация\",\"Age\":\"Возраст\",\"Gender\":\"Пол\",\"Location\":\"Местоположение\",\"Country\":\"Страна\",\"Last broadcast\":\"Последняя трансляция\",\"Fan club\":\"Фан-клуб\",\"Private show\":\"Частное шоу\",\"Spy show\":\"Шпионское шоу\",\"Model comparison list\":\"Список сравнения моделей\",\"Compare\":\"Сравнить\",\"Open model details\":\"Открыть сведения о модели\",\"Compare now\":\"Сравнить сейчас\",\"Clear\":\"Очистить\",\"Up to four models can be compared at once.\":\"Одновременно можно сравнивать до четырех моделей.\",\"Remove from comparison\":\"Удалить из сравнения\",\"Add to comparison\":\"Добавить к сравнению\",\"Total watched\":\"Всего просмотрено\",\"Sessions\":\"Сеансы\",\"Viewers\":\"Зрители\",\"Rating\":\"Рейтинг\",\"Online samples\":\"Онлайн образцы\",\"Loading quality data…\":\"Загрузка данных о качестве…\",\"samples\":\"образцы\",\"stalls\":\"киоски\",\"No quality measurements yet\":\"Качественных измерений пока нет\",\"Quality data unavailable\":\"Данные о качестве недоступны.\",\"Details\":\"Подробности\",\"Remove\":\"Удалить\",\"Model comparison\":\"Сравнение моделей\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Все значения основаны на общедоступной информации, вашей собственной организации или местных наблюдениях на этом устройстве. Отсутствующие значения никогда не угадываются.\",\"Shared tags\":\"Общие теги\",\"Loading public facts…\":\"Загрузка общедоступных фактов…\",\"Unknown status\":\"Неизвестный статус\",\"Last activity\":\"Последняя активность\",\"These statistics describe only your own local RoomGrid usage.\":\"Эта статистика описывает только ваше локальное использование RoomGrid.\",\"At a glance\":\"С первого взгляда\",\"Type\":\"Тип\",\"Appearances\":\"Появления\",\"Public facts\":\"Публичные факты\",\"No public biography available yet.\":\"Публичной биографии пока нет.\",\"Public link\":\"Публичная ссылка\",\"Public profile source\":\"Источник общедоступного профиля\",\"Profile updated\":\"Профиль обновлен\",\"Private note (local only)\":\"Личная заметка (только для местных жителей)\",\"Your tags\":\"Ваши теги\",\"No rating\":\"Нет рейтинга\",\"No priority\":\"Нет приоритета\",\"Private library data\":\"Данные частной библиотеки\",\"Private notes\":\"Личные заметки\",\"Priority\":\"Приоритет\",\"Favorite\":\"Любимый\",\"Watchlist\":\"Список наблюдения\",\"Archived\":\"В архиве\",\"Watch again\":\"Посмотрите еще раз\",\"Technical note\":\"Техническое примечание\",\"Technical\":\"Технический\",\"Save all changes\":\"Сохранить все изменения\",\"Loading local activity…\":\"Загрузка местных действий…\",\"Activity and playback quality\":\"Активность и качество воспроизведения\",\"Sun\":\"Солнце\",\"Mon\":\"Пн.\",\"Tue\":\"Вт\",\"Wed\":\"ср.\",\"Thu\":\"Чт\",\"Fri\":\"Пт\",\"Sat\":\"Суббота\",\"Quality samples\":\"Качественные образцы\",\"Average startup\":\"Средний стартап\",\"Stalls\":\"Киоски\",\"Reconnects\":\"Воссоединяет\",\"Dropped frames\":\"Пропущенные кадры\",\"Online observations\":\"Онлайн-наблюдения\",\"Frequently observed online times\":\"Часто наблюдаемое время онлайн\",\"Not enough online observations yet.\":\"Онлайн-наблюдений пока недостаточно.\",\"Watched\":\"Смотрел\",\"Preview\":\"Предварительный просмотр\",\"Quick view\":\"Быстрый просмотр\",\"Discovered\":\"Обнаружен\",\"Quality\":\"Качество\",\"Connection\":\"Подключение\",\"No local activity recorded yet.\":\"Никакой местной активности пока не зарегистрировано.\",\"Recent events\":\"Недавние события\",\"Activity data unavailable.\":\"Данные о деятельности недоступны.\",\"Similar models\":\"Похожие модели\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Рассчитывается на основе локальных/общедоступных тегов, страны и типа. Каждое предложение показывает причину, и данные не загружаются.\",\"shared tags\":\"общие теги\",\"same country\":\"та же страна\",\"same type\":\"тот же тип\",\"Not enough matching data yet.\":\"Пока недостаточно соответствующих данных.\",\"Overview\":\"Обзор\",\"Profile\":\"Профиль\",\"Organize\":\"Организовать\",\"Activity\":\"Деятельность\",\"Similar\":\"Похожие\",\"Model detail sections\":\"Разделы сведений о модели\",\"Previous model\":\"Предыдущая модель\",\"Next model\":\"Следующая модель\",\"Open room\":\"Открытая комната\",\"Pin preview\":\"Предварительный просмотр закрепления\",\"No public fields available\":\"Нет доступных общедоступных полей\",\"Public facts currently unavailable\":\"Публичные факты в настоящее время недоступны\",\"Session chat search\":\"Поиск сеансового чата\",\"Search this session only…\":\"Искать только в этом сеансе…\",\"New messages\":\"Новые сообщения\",\"Time for a short break.\":\"Время для небольшого перерыва.\",\"Your RoomGrid time budget for today has been reached.\":\"Ваш бюджет времени RoomGrid на сегодня исчерпан.\",\"RoomGrid action messages\":\"Сообщения о действиях RoomGrid\",\"This change is saved automatically.\":\"Это изменение сохраняется автоматически.\",\"Undo\":\"Отменить\",\"Change undone\":\"Изменение отменено\",\"Undo was not applied because newer changes already exist.\":\"Отмена не была применена, поскольку уже существуют новые изменения.\",\"Chaturbate live catalog\":\"Живой каталог Chaturbate\",\"RoomGrid Discover\":\"RoomGrid Откройте для себя\",\"Chaturbate Discover page\":\"Страница знакомства с Chaturbate\",\"Following list\":\"Следующий список\",\"My Collection\":\"Моя коллекция\",\"Chaturbate page\":\"Страница Чатурбата\",\"Local record\":\"Локальная запись\",\"Personally organized\":\"Лично организовано\",\"Local watch history\":\"Местная история просмотра\",\"Public profile\":\"Публичный профиль\",\"Local quality measurements\":\"Местные измерения качества\",\"Local model index\":\"Индекс локальной модели\",\"Current live signal\":\"Текущий живой сигнал\",\"Within the last hour\":\"В течение последнего часа\",\"Within the last 24 hours\":\"За последние 24 часа\",\"Within the last 30 days\":\"За последние 30 дней\",\"Historical record\":\"Исторический рекорд\",\"Time unknown\":\"Время неизвестно\",\"All\":\"Все\",\"Favorites\":\"Избранное\",\"Library\":\"Библиотека\",\"My Collection contained no reliably recognizable entries\":\"В моей коллекции не было достоверно узнаваемых записей.\",\"No group filter active · all models are shown.\":\"Ни один групповой фильтр не активен · показаны все модели.\",\"Clear group filter\":\"Очистить групповой фильтр\",\"Filter homepage by groups\":\"Фильтровать домашнюю страницу по группам\",\"Close group menu\":\"Закрыть меню группы\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Выберите одну или несколько своих групп. Карточки других моделей сразу скрываются на главной странице Chaturbate.\",\"Show all\":\"Показать все\",\"Models in this group\":\"Модели в этой группе\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Специальной группы пока нет. Создайте его в библиотеке моделей и назначьте туда модели.\",\"In any group\":\"В любой группе\",\"In all groups\":\"Во всех группах\",\"Manage groups\":\"Управление группами\",\"Create group\":\"Создать группу\",\"Custom groups for the homepage\":\"Пользовательские группы для главной страницы\",\"Main views\":\"Основные виды\",\"MultiCam workstation\":\"Рабочая станция MultiCam\",\"Camera grid, audio and layouts\":\"Сетка камеры, звук и макеты\",\"Discover\":\"Обзор\",\"Full catalog, filters and sorting\":\"Полный каталог, фильтры и сортировка\",\"Personal dashboard\":\"Персональный кабинет\",\"Favorites, activity and shortcuts\":\"Избранное, действия и ярлыки\",\"Tag explorer\":\"Обозреватель тегов\",\"Combinations, trends and related tags\":\"Комбинации, тенденции и связанные теги\",\"Library and statistics\":\"Библиотека и статистика\",\"Statistics Center\":\"Центр статистики\",\"Model comparison and local statistics\":\"Сравнение моделей и локальная статистика\",\"Model library\":\"Библиотека моделей\",\"Profiles, tags, groups and notes\":\"Профили, теги, группы и заметки\",\"My user\":\"Мой профиль\",\"Preferences, usage and account data\":\"Настройки, использование и данные учетной записи\",\"Analytics Studio\":\"Аналитическая студия\",\"Ranges, heatmaps and exports\":\"Диапазоны, тепловые карты и экспорт\",\"Settings and maintenance\":\"Настройки и обслуживание\",\"Customize homepage\":\"Настроить главную\",\"Cards, filters and visible details\":\"Карты, фильтры и видимые детали\",\"Appearance\":\"Внешний вид\",\"Colors, density and surfaces\":\"Цвета, плотность и поверхности\",\"Workspaces and groups\":\"Рабочие пространства и группы\",\"Layouts, templates and assignments\":\"Макеты, шаблоны и задания\",\"Data and backup\":\"Данные и резервное копирование\",\"Import, export and recovery\":\"Импорт, экспорт и восстановление\",\"Windows and performance\":\"Окна и производительность\",\"Resource profiles and preview limits\":\"Профили ресурсов и ограничения предварительного просмотра\",\"Privacy\":\"Конфиденциальность\",\"History, private session and local data\":\"История, приватный сеанс и локальные данные\",\"Diagnostics\":\"Диагностика\",\"Self-check, routes and recovery\":\"Самопроверка, маршруты и восстановление\",\"RoomGrid main menu\":\"Главное меню RoomGrid\",\"All pages, settings and tools in one place\":\"Все страницы, настройки и инструменты в одном месте\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Меню остается внутри видимого окна; все данные RoomGrid остаются локальными.\",\"RoomGrid pages\":\"Страницы RoomGrid\",\"Statistics\":\"Статистика\",\"Customize original homepage\":\"Настроить исходную главную страницу\",\"Homepage\":\"Главная страница\",\"Filter homepage by your own groups\":\"Фильтрация домашней страницы по вашим группам\",\"Groups\":\"Группы\",\"Activity trend\":\"Тенденция активности\",\"Link\":\"Ссылка\",\"Public profile text and links\":\"Текст общедоступного профиля и ссылки\",\"Last known public preview\":\"Последняя известная общедоступная предварительная версия\",\"Current public preview\":\"Текущая общедоступная предварительная версия\",\"Offline may only provide a placeholder\":\"Офлайн-режим может содержать только заполнитель.\",\"Example images and covers\":\"Примеры изображений и обложек\",\"Reading local image archive…\":\"Чтение локального архива изображений…\",\"Save new snapshot\":\"Сохранить новый снимок\",\"Saving public previews…\":\"Сохранение общедоступных предпросмотров…\",\"No saved image yet; current public previews are still shown.\":\"Еще нет сохраненного изображения; текущие общедоступные предварительные просмотры по-прежнему отображаются.\",\"Current public cover\":\"Текущая публичная обложка\",\"Current compact preview\":\"Текущая компактная предварительная версия\",\"Current live image\":\"Текущее живое изображение\",\"Room\":\"Комната\",\"More\":\"Подробнее\",\"Unwatch\":\"Отменить просмотр\",\"Data sources and freshness\":\"Источники данных и актуальность\",\"Data source\":\"Источник данных\",\"Full local data set\":\"Полный набор локальных данных\",\"Matching filters\":\"Соответствующие фильтры\",\"Shown on this page\":\"Показан на этой странице\",\"Latest live signals\":\"Последние живые сигналы\",\"With watch history\":\"С историей просмотра\",\"Public-profile coverage\":\"Публичное освещение\",\"Country coverage\":\"Охват страны\",\"Age coverage\":\"Возрастной охват\",\"Tag coverage\":\"Охват тегов\",\"Which data set is being searched?\":\"В каком наборе данных ведется поиск?\",\"Last local observation\":\"Последнее местное наблюдение\",\"No observation time yet\":\"Времени наблюдения еще нет\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Публичный = с текущих страниц Chaturbate или из живого каталога. Личное = сохранено, отмечено или сгруппировано вами. Локальный = сеансы, предварительные просмотры или значения качества, наблюдаемые только на этом устройстве. Неизвестные поля остаются неизвестными и никогда не угадываются.\",\"Local model statistics\":\"Статистика локальной модели\",\"Automatically recorded · personally organized\":\"Записывается автоматически · организовано лично\",\"Automatically recorded in the growing model catalog\":\"Автоматически записывается в каталог растущих моделей.\",\"Total\":\"Итого\",\"Avg. session\":\"Средний сеанс\",\"Active days\":\"Активные дни\",\"Share\":\"Поделиться\",\"Longest session\":\"Самый долгий сеанс\",\"Previews\":\"Предпросмотры\",\"Quick views\":\"Быстрые просмотры\",\"Data sources\":\"Источники данных\",\"Edit in library\":\"Редактировать в библиотеке\",\"No experience data yet\":\"Данных об опыте пока нет\",\"Online observations will be collected locally from now on.\":\"Отныне онлайн-наблюдения будут собираться локально.\",\"Open\":\"Открыть\",\"Good trend\":\"Хорошая тенденция\",\"Moderate trend\":\"Умеренная тенденция\",\"Initial trend\":\"Начальная тенденция\",\"First observation\":\"Первое наблюдение\",\"Estimated from watch time\":\"Расчетное время просмотра\",\"weekday open\":\"будний день открыт\",\"time open\":\"время открыто\",\"Preview image\":\"Изображение предпросмотра\",\"Biography / room subject\":\"Биография / тема комнаты\",\"Catalog state\":\"Состояние каталога\",\"Live status\":\"Статус эфира\",\"Viewer count\":\"Количество зрителей\",\"Country/region\":\"Страна/регион\",\"Total watch time\":\"Общее время просмотра\",\"Average session\":\"Средний сеанс\",\"First seen\":\"Впервые замечена\",\"Last seen\":\"Последний просмотр\",\"Last live\":\"Последний эфир\",\"Approximate online times\":\"Примерное время в сети\",\"Activity, sources and profile state\":\"Активность, источники и состояние профиля\",\"Example images\":\"Примеры изображений\",\"Own groups\":\"Свои группы\",\"Public/recognized tags\":\"Публичные/распознанные теги\",\"Notes, own tags and priority\":\"Заметки, свои теги и приоритет\",\"Local rating\":\"Локальная оценка\",\"Group assignment\":\"Назначение групп\",\"Room and preview actions\":\"Действия комнаты и предпросмотра\",\"Library display\":\"Отображение библиотеки\",\"Saved in this browser\":\"Сохранено в этом браузере\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Выберите размер карточек, число моделей в строке и на странице, а также точный набор данных на каждой карточке.\",\"Compact overview\":\"Компактный обзор\",\"Balanced\":\"Сбалансированный\",\"All details\":\"Все сведения\",\"Show all details\":\"Показать все сведения\",\"Core details only\":\"Только основные сведения\",\"Card zoom\":\"Масштаб карточек\",\"Visible information per model\":\"Видимые сведения о модели\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"При автоматических столбцах масштаб определяет, сколько моделей помещается рядом. Точное число столбцов имеет приоритет. Все настройки сохраняются в этом браузере.\",\"No custom tags yet\":\"Пользовательских тегов пока нет\",\"Name of the new group\":\"Название новой группы\",\"Models remain in the library.\":\"Модели остаются в библиотеке.\",\"Custom groups\":\"Пользовательские группы\",\"Create new group\":\"Создать новую группу\",\"Nothing selected — show all\":\"Ничего не выбрано — показать все\",\"Any\":\"Любой\",\"Women\":\"Женщины\",\"Men\":\"Мужчины\",\"Couples\":\"Пары\",\"Unknown\":\"Неизвестно\",\"Error/loading\":\"Ошибка/загрузка\",\"Every weekday\":\"Каждый будний день\",\"Sunday\":\"воскресенье\",\"Monday\":\"понедельник\",\"Tuesday\":\"вторник\",\"Wednesday\":\"среда\",\"Thursday\":\"Четверг\",\"Friday\":\"Пятница\",\"Saturday\":\"суббота\",\"Every time\":\"Каждый раз\",\"Advanced filters and sorting\":\"Расширенные фильтры и сортировка\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Комбинируйте столько фильтров, сколько захотите. Несколько полов, штатов, стран и тегов работают как множественный выбор. Все настройки сохраняются в этом браузере.\",\"Gender/type\":\"Пол/тип\",\"Countries/regions (multiple)\":\"Страны/регионы (несколько)\",\"Add country…\":\"Добавить страну…\",\"Public tags (multiple)\":\"Публичные теги (несколько)\",\"Add tag…\":\"Добавить тег…\",\"Public-tag matching\":\"Соответствие общедоступным тегам\",\"At least one tag\":\"Хотя бы один тег\",\"Every selected tag\":\"Каждый выбранный тег\",\"Catalog source\":\"Источник каталога\",\"All models\":\"Все модели\",\"Personally organized only\":\"Только лично организовано\",\"Automatically discovered only\":\"Только автоматически обнаруженные\",\"Profile data\":\"Данные профиля\",\"Loaded or missing\":\"Загружено или отсутствует\",\"Loaded only\":\"Только загружено\",\"Missing only\":\"Отсутствует только\",\"Picture/category\":\"Изображение/категория\",\"Neither HD nor gaming\":\"Ни HD, ни игры\",\"Watch history\":\"История просмотра\",\"Never watched\":\"Никогда не смотрел\",\"Own tags\":\"Собственные теги\",\"With or without\":\"С или без\",\"With tags only\":\"Только с тегами\",\"Without tags only\":\"Только без тегов\",\"At least one group\":\"Хотя бы одна группа\",\"No group\":\"Нет группы\",\"With notes only\":\"Только с примечаниями\",\"Without notes only\":\"Только без примечаний\",\"Numeric ranges and minimums\":\"Числовые диапазоны и минимумы\",\"Minimum age\":\"Минимальный возраст\",\"Maximum age\":\"Максимальный возраст\",\"Minimum viewers\":\"Минимальное количество зрителей\",\"Maximum viewers\":\"Максимальное количество зрителей\",\"Minimum followers\":\"Минимальное количество подписчиков\",\"Maximum followers\":\"Максимальное количество подписчиков\",\"Minimum total watch time\":\"Минимальное общее время просмотра\",\"Currently online for at least\":\"В настоящее время онлайн как минимум\",\"Minimum sessions\":\"Минимальное количество сеансов\",\"Minimum rating\":\"Минимальный рейтинг\",\"Minimum priority\":\"Минимальный приоритет\",\"Active within\":\"Активен внутри\",\"days\":\"дни\",\"Minimum appearances\":\"Минимум выступлений\",\"Minimum live observations\":\"Минимум живых наблюдений\",\"Filter by typical online time\":\"Фильтровать по обычному времени онлайн\",\"Weekday\":\"Будний день\",\"Hour (local time)\":\"Час (местное время)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Нулевые или пустые поля означают отсутствие ограничений. Неизвестный возраст исключается, пока активно возрастное ограничение. Фильтры времени используют время в реальном времени, наблюдаемое на этом устройстве, осторожно откатываясь назад, чтобы посмотреть время, когда это необходимо.\",\"Reset all library filters\":\"Сбросить все фильтры библиотеки\",\"Open live room\":\"Открытая живая комната\",\"Open room page\":\"Открыть страницу комнаты\",\"Status\":\"Статус\",\"Open model room in a new tab\":\"Открыть модельную комнату в новой вкладке\",\"Open the Chaturbate single-model view in a new tab\":\"Откройте представление одной модели Chaturbate в новой вкладке.\",\"Save profile\":\"Сохранить профиль\",\"Restore\":\"Восстановить\",\"Archive\":\"Архив\",\"Advanced statistics filters\":\"Расширенные фильтры статистики\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Каждый фильтр можно комбинировать; страны, теги, группы и типы поддерживают множественный выбор. Выбор сохраняется навсегда.\",\"Own groups (multiple)\":\"Собственные группы (несколько)\",\"Add group…\":\"Добавить группу…\",\"Tag matching\":\"Соответствие тегов\",\"At least one\":\"Хотя бы один\",\"All selected tags\":\"Все выбранные теги\",\"Group matching\":\"Групповое сопоставление\",\"All selected groups\":\"Все выбранные группы\",\"Cover/example image\":\"Обложка/пример изображения\",\"Known public image available\":\"Доступен известный общедоступный образ\",\"No known image\":\"Нет известного изображения\",\"Currently live\":\"В настоящее время в прямом эфире\",\"Maximum total watch time\":\"Максимальное общее время просмотра\",\"Maximum sessions\":\"Максимальное количество сеансов\",\"Minimum active days\":\"Минимальное количество активных дней\",\"Minimum previews\":\"Минимум превью\",\"Minimum quick views\":\"Минимум быстрых просмотров\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Нулевые или пустые поля означают отсутствие ограничений. Неизвестные значения никогда не угадываются; возрастные диапазоны исключают неизвестный возраст.\",\"Reset all statistics filters\":\"Сбросить все фильтры статистики\",\"Search name, tags or notes…\":\"Поиск по имени, тегам или заметкам…\",\"Search\":\"Поиск\",\"Sorting\":\"Сортировка\",\"Most watch time\":\"Наибольшее время просмотра\",\"Least watch time\":\"Наименьшее время просмотра\",\"Recently active\":\"Недавно активен\",\"Longest inactive\":\"Самый длинный неактивный\",\"Most sessions\":\"Большинство сессий\",\"Fewest sessions\":\"Наименьшее количество сеансов\",\"Longest average session\":\"Самая длинная средняя сессия\",\"Shortest average session\":\"Самая короткая средняя сессия\",\"Longest single session\":\"Самая длинная одиночная сессия\",\"Most active days\":\"Самые активные дни\",\"Highest watch share\":\"Самая высокая доля просмотров\",\"Most previews\":\"Большинство превью\",\"Most quick views\":\"Самые быстрые просмотры\",\"Most discoveries\":\"Большинство открытий\",\"Most followers\":\"Большинство подписчиков\",\"Youngest first\":\"Самый младший первый\",\"Oldest first\":\"Сначала самый старый\",\"Highest rating\":\"Высший рейтинг\",\"Highest priority\":\"Высший приоритет\",\"Most appearances\":\"Большинство выступлений\",\"Most live observations\":\"Самые живые наблюдения\",\"First observed\":\"Впервые замечено\",\"Country A–Z\":\"Страна А–Я\",\"Most public tags\":\"Большинство общедоступных тегов\",\"Most groups\":\"Большинство групп\",\"Best technical quality\":\"Лучшее техническое качество\",\"Cover/example images first\":\"Сначала обложки/примеры изображений\",\"All statuses\":\"Все статусы\",\"Live only\":\"Только жить\",\"Library only\":\"Только библиотека\",\"View\":\"Вид\",\"Active library\":\"Активная библиотека\",\"Live now\":\"Живи сейчас\",\"With notes\":\"С примечаниями\",\"Newest saved/discovered\":\"Новейшие сохраненные/обнаруженные\",\"Oldest saved/discovered\":\"Самый старый сохраненный/обнаруженный\",\"Lowest rating\":\"Самый низкий рейтинг\",\"Lowest priority\":\"Самый низкий приоритет\",\"Live longest now\":\"Живи дольше всех сейчас\",\"Age youngest first\":\"Сначала возраст самого младшего\",\"Age oldest first\":\"Возраст сначала самый старый\",\"Most recently live\":\"Совсем недавно в прямом эфире\",\"First discovered\":\"Впервые обнаружен\",\"Most own groups\":\"Большинство собственных групп\",\"Card size\":\"Размер карты\",\"Compact\":\"Компактный\",\"Comfortable\":\"Удобный\",\"Large\":\"Большой\",\"Cinema\":\"Кино\",\"Extra large\":\"Очень крупный\",\"Models per row\":\"Моделей в строке\",\"Automatic for window\":\"Автоматически по размеру окна\",\"Recorded models\":\"Записанные модели\",\"Total cam time\":\"Общее время камеры\",\"across all models\":\"во всех моделях\",\"Dock + hover\":\"Панель + наведение\",\"Last 30 days\":\"Последние 30 дней\",\"local events\":\"местные события\",\"Top model\":\"Топ-модель\",\"no watch time yet\":\"времени просмотра еще нет\",\"Deeper analysis\":\"Более глубокий анализ\",\"Open Analytics Studio\":\"Открыть студию аналитики\",\"Late night\":\"Поздняя ночь\",\"Morning\":\"Утро\",\"Daytime\":\"Дневное время\",\"Evening\":\"Вечер\",\"Gaming\":\"Игры\",\"Music & DJ\":\"Музыка и диджей\",\"Dance\":\"Танец\",\"Cosplay & anime\":\"Косплей и аниме\",\"Fitness & sports\":\"Фитнес и спорт\",\"Talk & social\":\"Обсуждение и общение\",\"Outdoor & travel\":\"Туризм и путешествия\",\"Roleplay\":\"Ролевая игра\",\"Performance\":\"Производительность\",\"My interest statistics\":\"Моя статистика интересов\",\"All local data\":\"Все локальные данные\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Рассчитывается на основе фактического местного времени просмотра, сеансов, предварительных просмотров, быстрых просмотров, избранного и известной на данный момент общедоступной информации о модели.\",\"Favorite tag\":\"Любимый тег\",\"not enough data\":\"недостаточно данных\",\"Favorite genre\":\"Любимый жанр\",\"from public tags\":\"из общедоступных тегов\",\"Most-watched age\":\"Самый просматриваемый возраст\",\"no age data\":\"нет данных о возрасте\",\"Most-watched country\":\"Самая просматриваемая страна\",\"no country data\":\"нет данных о стране\",\"Favorite model type\":\"Любимый тип модели\",\"no type data\":\"нет данных о типе\",\"Favorite viewing time\":\"Любимое время просмотра\",\"no time data\":\"нет данных о времени\",\"Top-three concentration\":\"Тройка лидеров по концентрации\",\"watch time on the top three models\":\"время просмотра трех лучших моделей\",\"Favorite affinity\":\"Любимое занятие\",\"watch time on current favorites\":\"время просмотра текущих избранных\",\"watch time on HD models\":\"время просмотра на HD-моделях\",\"Metadata coverage\":\"Охват метаданных\",\"age\":\"возраст\",\"country\":\"страна\",\"Tags, genres and browsing interests\":\"Теги, жанры и интересы при просмотре\",\"Tags by watch time\":\"Теги по времени просмотра\",\"Your ranking appears when watched models have public tags.\":\"Ваш рейтинг отображается, когда просматриваемые модели имеют общедоступные теги.\",\"Favorite genre worlds\":\"Любимые жанровые миры\",\"No genre world can yet be assigned from public tags.\":\"Ни один жанровый мир пока нельзя назначить из общедоступных тегов.\",\"Your own tags\":\"Ваши собственные теги\",\"Your saved custom tags are also evaluated here by watch time.\":\"Сохраненные вами пользовательские теги также оцениваются здесь по времени просмотра.\",\"Preview-interest tags\":\"Теги предварительного просмотра интересов\",\"Browsing interests appear when you use live previews or quick views.\":\"Интересы просмотра появляются, когда вы используете предварительный просмотр в реальном времени или быстрый просмотр.\",\"Most-previewed models\":\"Самые популярные модели\",\"No previews have been recorded in this period yet.\":\"За этот период еще не было записано ни одного превью.\",\"Age, origin and model type\":\"Возраст, происхождение и тип модели\",\"Age groups by watch time\":\"Возрастные группы по времени просмотра\",\"Not enough public age data is available.\":\"Недостаточно данных о возрасте населения.\",\"Countries by watch time\":\"Страны по времени просмотра\",\"Not enough public country data is available.\":\"Недостаточно общедоступных данных по стране.\",\"Most-watched places\":\"Самые просматриваемые места\",\"Not enough public location data is available.\":\"Недостаточно данных об общедоступных местоположениях.\",\"Model types by watch time\":\"Типы моделей по времени просмотра\",\"Not enough public model-type data is available.\":\"Недостаточно общедоступных данных о типах моделей.\",\"Viewing rhythm and favorite signals\":\"Ритм просмотра и сигналы избранного\",\"Most active weekdays\":\"Самые активные будни\",\"Not enough visit events are available.\":\"Недостаточно событий для посещения.\",\"Times of day\":\"Время суток\",\"Tags among favorites\":\"Теги среди избранных\",\"Preference signals appear when you mark models as favorites.\":\"Сигналы предпочтений появляются, когда вы отмечаете модели как избранные.\",\"Countries among favorites\":\"Страны среди фаворитов\",\"No known countries among favorites yet.\":\"Среди фаворитов пока нет известных стран.\",\"Age groups among favorites\":\"Возрастные группы в фаворитах\",\"No known ages among favorites yet.\":\"Возраст фаворитов пока неизвестен.\",\"Model types among favorites\":\"Типы моделей среди фаворитов\",\"No known model types among favorites yet.\":\"Среди избранных пока нет известных типов моделей.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Эти результаты не отправляются на сервер. В предпочтениях просмотра учитывается только фактическое время просмотра; превью и избранное отображаются отдельно. Отсутствие данных о возрасте, стране, местоположении или теге никогда не угадывается.\",\"Open Following on Chaturbate\":\"Откройте подписку на Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Полную историю просмотров или посещений невозможно получить задним числом, поскольку Chaturbate не предоставляет документированного API для личной истории. Вы можете запросить копию своих сохраненных личных данных через официальный экспорт данных Chaturbate. Если этот экспорт содержит полезные поля истории, RoomGrid может поддерживать безопасный импорт после проверки формата файла. Официальный API статистики предоставляет текущие значения вещательной компании; RoomGrid может создавать свою собственную историю только начиная с первого подключения.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Сравните каждую локально наблюдаемую, сохраненную и просматриваемую модель — с учетом времени просмотра, сеансов, активности, предварительного просмотра, качества и рейтинга.\",\"Large model library\":\"Большая библиотека моделей\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Ваши модели в виде удобных профилей с богатым изображением с актуальным статусом, заметками, тегами, рейтингами, приоритетом, группами и прямыми действиями.\",\"My user dashboard\":\"Моя панель пользователя\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Ваша личная локальная история использования, привычки, данные библиотеки и — только при намеренном подключении — данные официальной учетной записи вещательной компании.\",\"🔒 Local & private on this device\":\"🔒 Локально и конфиденциально на этом устройстве\",\"Preparing data…\":\"Подготовка данных…\",\"RoomGrid Comfort Center\":\"Центр управления RoomGrid\",\"inside the standard Chaturbate page\":\"на стандартной странице Chaturbate\",\"All RoomGrid pages and settings\":\"Все страницы и настройки RoomGrid\",\"Media paused; names and images are covered.\":\"СМИ сделали паузу; имена и изображения скрыты.\",\"Restore previous view\":\"Восстановить предыдущий вид\",\"Enter local PIN\":\"Введите местный PIN-код\",\"In-page notifications\":\"Уведомления на странице\",\"No new notifications\":\"Нет новых уведомлений\",\"Notifications are off\":\"Уведомления отключены\",\"RoomGrid notification\":\"Уведомление RoomGrid\",\"Dismiss as read\":\"Закрыть как прочитанное\",\"More unread notifications\":\"Больше непрочитанных уведомлений\",\"RoomGrid update\":\"Обновление RoomGrid\",\"A monitored source changed status.\":\"Контролируемый источник изменил статус.\",\"This is a local RoomGrid test notification.\":\"Это локальное тестовое уведомление RoomGrid.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Тест прошел успешно: уведомления могут появляться прямо на панели RoomGrid.\",\"Settings\":\"Настройки\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Рабочие области, библиотека, дизайн, данные, история, уведомления, конфиденциальность и производительность\",\"Workspaces\":\"Рабочие пространства\",\"Data\":\"Данные\",\"History & statistics\":\"История и статистика\",\"Broadcaster stats\":\"Статистика вещателей\",\"Notifications\":\"Уведомления\",\"Windows & performance\":\"Окна и производительность\",\"Optional tools\":\"Дополнительные инструменты\",\"Settings pages\":\"Страницы настроек\",\"RoomGrid control center\":\"Центр управления RoomGrid\",\"Any online model\":\"Любая онлайн-модель\",\"Workspace name\":\"Имя рабочей области\",\"Optional description\":\"Дополнительное описание\",\"Active\":\"Активный\",\"No description\":\"Нет описания\",\"Name\":\"Имя\",\"Description\":\"Описание\",\"Replace offline models\":\"Заменить офлайн-модели\",\"Replacement group\":\"Группа замены\",\"Delay seconds\":\"Задержка в секундах\",\"Save current state\":\"Сохранить текущее состояние\",\"New window\":\"Новое окно\",\"Export this workspace\":\"Экспортировать это рабочее пространство\",\"Duplicate\":\"Дублировать\",\"Workspace restored\":\"Рабочая область восстановлена\",\"Workspace archived\":\"Рабочая область заархивирована\",\"Delete permanently\":\"Удалить навсегда\",\"Permanently delete this workspace?\":\"Удалить эту рабочую область навсегда?\",\"Saved layout and panel sizes\":\"Сохраненная компоновка и размеры панелей.\",\"Apply\":\"Применить\",\"Layout template deleted\":\"Шаблон макета удален.\",\"Parent group\":\"Родительская группа\",\"None\":\"Нет\",\"Group description\":\"Описание группы\",\"Save current workspace\":\"Сохранить текущее рабочее пространство\",\"Save layout template\":\"Сохранить шаблон макета\",\"Template name\":\"Имя шаблона\",\"Layout templates\":\"Шаблоны макетов\",\"Device layout\":\"Компоновка устройства\",\"Groups and subgroups\":\"Группы и подгруппы\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"В каждом рабочем пространстве хранятся отдельные макеты рабочего стола и компактные макеты. Автоматическая замена отключена по умолчанию и заполняет автономные слоты только после того, как вы включите ее для этой рабочей области.\",\"No templates yet.\":\"Шаблонов пока нет.\",\"Group name\":\"Название группы\",\"New subgroup\":\"Новая подгруппа\",\"Enter parent group name\":\"Введите название родительской группы\",\"Subgroup name\":\"Название подгруппы\",\"Name, note or local tag\":\"Имя, примечание или локальный тег\",\"Choose group…\":\"Выбрать группу…\",\"Add selected models to group\":\"Добавить выбранные модели в группу\",\"Watch later\":\"Посмотреть позже\",\"Paused\":\"Приостановлено\",\"Collection name\":\"Название коллекции\",\"Tags, comma separated\":\"Теги, разделенные запятой\",\"Countries, comma separated\":\"Страны через запятую\",\"All types\":\"Все типы\",\"Rule name\":\"Название правила\",\"Required tags\":\"Обязательные теги\",\"Local alert\":\"Местное оповещение\",\"Add to favorites\":\"Добавить в избранное\",\"Add to group\":\"Добавить в группу\",\"Any tag\":\"Любой тег\",\"Any country\":\"Любая страна\",\"Pause\":\"Пауза\",\"Enable\":\"Включить\",\"Delete\":\"Удалить\",\"Automation rule deleted\":\"Правило автоматизации удалено.\",\"Select model\":\"Выберите модель\",\"Later\":\"Позже\",\"Never visited\":\"Никогда не посещал\",\"Last online\":\"Последний раз в сети\",\"Last visited\":\"Последнее посещение\",\"Local note\":\"Местное примечание\",\"Local tags, comma separated\":\"Локальные теги, разделенные запятой\",\"Remove later\":\"Удалить позже\",\"Hide today\":\"Скрыть сегодня\",\"Pause 7 days\":\"Пауза 7 дней\",\"Never suggest\":\"Никогда не предлагайте\",\"Select results\":\"Выберите результаты\",\"Clear selection\":\"Очистить выбор\",\"selected\":\"выбрано\",\"Add to watch later\":\"Добавить, чтобы посмотреть позже\",\"Permanently delete selected models?\":\"Удалить выбранные модели навсегда?\",\"Smart collections\":\"Умные коллекции\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Коллекции обновляются автоматически по мере роста локальных метаданных и статистики; модели не дублируются.\",\"Tags\":\"Теги\",\"Countries\":\"Страны\",\"Favorites only\":\"Только избранное\",\"Online only\":\"Только онлайн\",\"Save smart collection\":\"Сохранить умную коллекцию\",\"Delete current collection\":\"Удалить текущую коллекцию\",\"Delete this smart collection? Model data stays unchanged.\":\"Удалить эту умную коллекцию? Данные модели остаются неизменными.\",\"Rules & automation center\":\"Центр правил и автоматизации\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Если данные локальной модели совпадают, отобразите одно предупреждение, добавьте в избранное или добавьте модель в выбранную группу.\",\"Action\":\"Действие\",\"Target group\":\"Целевая группа\",\"Save rule\":\"Сохранить правило\",\"No automation rules yet.\":\"Правил автоматизации пока нет.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Имена пользователей дедуплицируются путем точного нормализованного совпадения. Поиск охватывает имена, заметки и локальные теги. Архивирование, удаление из просмотра и окончательное удаление остаются отдельными действиями.\",\"No matching models.\":\"Нет подходящих моделей.\",\"export\":\"экспорт\",\"Models and notes\":\"Модели и примечания\",\"This backup could not be read. Current data was not changed.\":\"Эту резервную копию невозможно прочитать. Текущие данные не были изменены.\",\"Import preview (nothing changed yet)\":\"Предварительный просмотр импорта (пока ничего не изменилось)\",\"conflicts\":\"конфликты\",\"Includes sensitive statistics\":\"Включает конфиденциальную статистику\",\"models\":\"модели\",\"events\":\"события\",\"quality samples\":\"качественные образцы\",\"Import statistics too\":\"Импортировать статистику тоже\",\"Import method\":\"Метод импорта\",\"Merge (recommended)\":\"Объединить (рекомендуется)\",\"Replace selected sections\":\"Заменить выбранные разделы\",\"Apply import\":\"Применить импорт\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Импорт завершен. Предыдущая конфигурация и статистика резервировались отдельно.\",\"The import could not be saved safely. The backup remains available.\":\"Импорт не удалось безопасно сохранить. Резервная копия остается доступной.\",\"Cancel preview\":\"Отменить предварительный просмотр\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Этот файл содержит данные модели, заметки или рабочей области и должен рассматриваться как конфиденциальный.\",\"This import is not marked sensitive.\":\"Этот импорт не помечен как конфиденциальный.\",\"Data version\":\"Версия данных\",\"Software version\":\"Версия программного обеспечения\",\"Revision\":\"Редакция\",\"Local models\":\"Локальные модели\",\"Selective export\":\"Выборочный экспорт\",\"Export selected\":\"Экспортировать выбранное\",\"Full export\":\"Полный экспорт\",\"Complete safety package\":\"Полный пакет безопасности\",\"Export statistics separately\":\"Экспортировать статистику отдельно\",\"Choose file and preview\":\"Выберите файл и просмотрите\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Полный пакет безопасности содержит конфигурацию и конфиденциальную статистику, но не содержит токен официальной статистики. Каждый импорт предварительно просматривается, и перед применением отдельно создается резервная копия конфигурации и статистики.\",\"Storage and backup center\":\"Центр хранения и резервного копирования\",\"Local storage\":\"Локальное хранилище\",\"Site storage\":\"Хранение сайта\",\"Available quota\":\"Доступная квота\",\"Internal statistics backups\":\"Резервное копирование внутренней статистики\",\"Configuration\":\"Конфигурация\",\"workspaces\":\"рабочие места\",\"LocalStorage + update vault\":\"LocalStorage + хранилище обновлений\",\"Statistics database\":\"База данных статистики\",\"Secrets\":\"Секреты\",\"The official statistics token is never included in normal or complete backups.\":\"Токен официальной статистики никогда не включается в обычные или полные резервные копии.\",\"Excluded\":\"Исключено\",\"Create configuration restore point\":\"Создать точку восстановления конфигурации\",\"Create statistics restore point\":\"Создать точку восстановления статистики\",\"Legacy cleanup\":\"Устаревшая очистка\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Устаревшие данные никогда не удаляются автоматически. Удалите старые ключи v6/v7 только после успешной проверки.\",\"Remove old v6/v7 copies\":\"Удалите старые копии v6/v7.\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Удалить только старые копии v6/v7? Текущие данные и резервные копии остаются.\",\"items removed\":\"элементы удалены\",\"Aurora\":\"Аврора\",\"Blue and teal, the RoomGrid signature\":\"Синий и бирюзовый — фирменный знак RoomGrid\",\"Ember\":\"Эмбер\",\"Warm orange and rose\":\"Теплый апельсин и роза\",\"Frost\":\"Мороз\",\"Crisp cyan and violet\":\"Яркий голубой и фиолетовый\",\"Custom\":\"Пользовательский\",\"Use the colors selected below\":\"Используйте цвета, выбранные ниже\",\"Name the current appearance\":\"Назовите текущий внешний вид\",\"My appearance\":\"Моя внешность\",\"Delete appearance profile\":\"Удалить профиль внешнего вида\",\"This appearance profile could not be imported.\":\"Этот профиль внешнего вида не удалось импортировать.\",\"Light\":\"Свет\",\"Dark\":\"Темный\",\"Export\":\"Экспорт\",\"Home\":\"Главная\",\"Listings and tags\":\"Списки и теги\",\"Model pages\":\"Страницы моделей\",\"Use global appearance\":\"Использовать глобальный внешний вид\",\"This logo format is unsafe or unsupported.\":\"Этот формат логотипа небезопасен или не поддерживается.\",\"The logo file could not be read.\":\"Не удалось прочитать файл логотипа.\",\"MultiCam tools\":\"Инструменты MultiCam\",\"Page\":\"Страница\",\"Panel\":\"Панель\",\"Card surface\":\"Поверхность карты\",\"Input\":\"Ввод\",\"Border\":\"Граница\",\"Text\":\"Текст\",\"Muted text\":\"Приглушенный текст\",\"Success\":\"Успех\",\"Warning\":\"Предупреждение\",\"Danger\":\"Опасность\",\"Info and focus\":\"Информация и фокус\",\"Data hub\":\"Центр данных\",\"Text / page\":\"Текст/страница\",\"Text / panel\":\"Текст/панель\",\"Muted text / panel\":\"Приглушенный текст/панель\",\"Accent text / accent\":\"Акцентный текст/акцент\",\"Desktop\":\"Рабочий стол\",\"Tablet\":\"Таблетка\",\"Mobile\":\"мобильный\",\"Brand and logo studio\":\"Студия бренда и логотипа\",\"Monogram\":\"Монограмма\",\"Tagline\":\"Слоган\",\"Brand display\":\"Дисплей бренда\",\"Full name\":\"Полное имя\",\"Uploaded logo\":\"Загруженный логотип\",\"Logo shape\":\"Форма логотипа\",\"Rounded\":\"Закругленный\",\"Circle\":\"Круг\",\"Square\":\"Площадь\",\"Pill\":\"Таблетка\",\"Page title\":\"Название страницы\",\"Website original\":\"Исходный вид сайта\",\"Brand plus original\":\"Бренд плюс оригинал\",\"Custom title\":\"Пользовательский заголовок\",\"Custom page title\":\"Пользовательский заголовок страницы\",\"Image fit\":\"Изображение соответствует\",\"Horizontal position\":\"Горизонтальное положение\",\"Vertical position\":\"Вертикальное положение\",\"Upload light-mode logo\":\"Загрузить логотип в светлом режиме\",\"Upload dark-mode logo\":\"Загрузить логотип в темном режиме\",\"Remove logos\":\"Удалить логотипы\",\"Show version\":\"Показать версию\",\"Use logo as tab icon on RoomGrid pages\":\"Использовать логотип в качестве значка вкладки на страницах RoomGrid\",\"Replace website logo in the original header (local only)\":\"Заменить логотип веб-сайта в исходном заголовке (только локально)\",\"Complete color system\":\"Полная система цвета\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Управляйте цветами страницы, панели, карточки, ввода, границы, текста и статуса в дополнение к акцентам.\",\"Derived automatically\":\"Получено автоматически\",\"Fully custom\":\"Полностью индивидуальный\",\"Editing dark palette\":\"Редактирование темной палитры\",\"Editing light palette\":\"Редактирование световой палитры\",\"Reset current palette\":\"Сбросить текущую палитру\",\"Layout editor\":\"Редактор макетов\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Установите расстояние между контентом, ширину карточки, заголовок и боковую панель одинаково на всех страницах RoomGrid.\",\"Minimum card width\":\"Минимальная ширина карты\",\"Section spacing\":\"Расстояние между секциями\",\"Content padding\":\"Заполнение контента\",\"Header height\":\"Высота заголовка\",\"Maximum columns\":\"Максимальное количество столбцов\",\"Automatic\":\"Автоматический\",\"Card aspect\":\"Аспект карты\",\"Sidebar position\":\"Положение боковой панели\",\"Left\":\"Левый\",\"Right\":\"Правильно\",\"Keep header visible while scrolling\":\"Держите заголовок видимым при прокрутке\",\"Live device preview\":\"Предварительный просмотр устройства в реальном времени\",\"Contrast and accessibility\":\"Контраст и доступность\",\"High-contrast mode\":\"Высококонтрастный режим\",\"Underline links\":\"Подчеркнуть ссылки\",\"Reduce transparency\":\"Уменьшить прозрачность\",\"Boost base text size\":\"Увеличить размер базового текста\",\"Focus ring width\":\"Ширина кольца фокусировки\",\"Minimum target size\":\"Минимальный размер цели\",\"Respect system reduced-motion setting\":\"Соблюдайте настройки системы замедления движения\",\"Apply accessible defaults\":\"Применить доступные значения по умолчанию\",\"Profiles by page and screen size\":\"Профили по страницам и размерам экрана\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Сохраните одно или несколько представлений, а затем назначьте их для каждого типа страницы для настольных компьютеров, планшетов и мобильных устройств. Профили переключаются автоматически при изменении размера экрана.\",\"Enable responsive design profiles\":\"Включить профили адаптивного дизайна\",\"Mobile maximum width\":\"Максимальная ширина мобильного устройства\",\"Tablet maximum width\":\"Максимальная ширина планшета\",\"RoomGrid design system\":\"Система дизайна RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Выбирайте направление бренда, ширину контента и плотность информации на карточке. Каждая страница следует сразу.\",\"Primary accent\":\"Основной акцент\",\"Secondary accent\":\"Вторичный акцент\",\"Content width\":\"Ширина контента\",\"Centered\":\"Центрировано\",\"Wide\":\"Широкий\",\"Full width\":\"Полная ширина\",\"Card style\":\"Стиль карты\",\"Visual\":\"Визуальный\",\"Data-rich\":\"Богатый данными\",\"Micro\":\"Микро\",\"Widget density\":\"Плотность виджетов\",\"Spacious\":\"Просторный\",\"Theme and surfaces\":\"Тема и поверхности\",\"Surface style\":\"Стиль поверхности\",\"Soft\":\"Мягкий\",\"Flat\":\"Квартира\",\"Glass\":\"Стекло\",\"Page background\":\"Фон страницы\",\"Solid\":\"Твердый\",\"Gradient\":\"Градиент\",\"Ambient glow\":\"Окружающее свечение\",\"Surface opacity\":\"Непрозрачность поверхности\",\"Custom accent color\":\"Пользовательский цвет акцента\",\"Sizing and motion\":\"Размер и движение\",\"Font style\":\"Стиль шрифта\",\"System\":\"Система\",\"Classic serif\":\"Классическая засечка\",\"Monospace\":\"Моноширинный\",\"Density\":\"Плотность\",\"Font scale\":\"Масштаб шрифта\",\"Control size\":\"Контрольный размер\",\"Corner radius\":\"Угловой радиус\",\"Card spacing\":\"Расстояние между картами\",\"Shadow strength\":\"Сила тени\",\"Sidebar width\":\"Ширина боковой панели\",\"Enable animations\":\"Включить анимацию\",\"Appearance profiles\":\"Профили внешнего вида\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Сохраните полный внешний вид в качестве многоразового профиля. Профили остаются доступными после обновления скрипта.\",\"Save current appearance\":\"Сохранить текущий внешний вид\",\"Import profile\":\"Импортировать профиль\",\"No profiles saved yet.\":\"Ни один профиль еще не сохранен.\",\"Instant preview\":\"Мгновенный предварительный просмотр\",\"Example card\":\"Пример карточки\",\"This is the combined result of the current visual settings.\":\"Это объединенный результат текущих визуальных настроек.\",\"Example action\":\"Пример действия\",\"Per-page design profiles\":\"Профили дизайна на странице\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Выберите другой сохраненный внешний вид для главной страницы, списков, страниц моделей, MultiCam, Discover и центра данных.\",\"Enable per-page design\":\"Включить постраничный дизайн\",\"Normal website pages\":\"Обычные страницы сайта\",\"Enable RoomGrid page design\":\"Включить дизайн страницы RoomGrid\",\"Tag and category pages\":\"Страницы тегов и категорий\",\"Cards\":\"Карты\",\"List\":\"Список\",\"Page density\":\"Плотность страницы\",\"Polish navigation\":\"Польская навигация\",\"Polish sidebars\":\"Польские боковые панели\",\"Hide recognized promo and merch areas\":\"Скрыть известные промо- и сувенирные зоны\",\"Compact footer\":\"Компактный нижний колонтитул\",\"Mark saved models\":\"Отметить сохраненные модели\",\"Mark favorites separately\":\"Отмечайте избранное отдельно\",\"Mark visited models\":\"Марк посещал модели\",\"Show card quick actions\":\"Показать быстрые действия на карточке\",\"Animated live previews on home and listing pages\":\"Анимированные превью в реальном времени на главной странице и страницах объявлений.\",\"Preview refresh rate\":\"Частота обновления предварительного просмотра\",\"Off\":\"Выкл.\",\"Update mode\":\"Режим обновления\",\"All visible cards simultaneously\":\"Все видимые карты одновременно\",\"Performance-bounded batches\":\"Пакеты с ограниченной производительностью\",\"Batch concurrency limit\":\"Ограничение пакетного параллелизма\",\"All visible\":\"Все видно\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Только видимые карты получают приглушенные обновления кадров JPEG; они сразу же приостанавливаются за кадром или на скрытой вкладке. Синхронный режим обновляет каждую видимую карту за одну волну; высокие скорости заметно увеличивают пропускную способность и загрузку процессора.\",\"Live video on hover\":\"Живое видео при наведении\",\"Play real-time video while hovering\":\"Воспроизведение видео в реальном времени при наведении курсора мыши\",\"Start delay\":\"Задержка запуска\",\"Immediately\":\"Немедленно\",\"Hover enlargement\":\"Увеличение при наведении\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Играет только карта под указателем, всегда без звука. Перемещение или скрытие вкладки немедленно останавливает поток.\",\"Live Preview Dock\":\"Док-станция для просмотра в реальном времени\",\"Enable low-load live previews\":\"Включить предварительный просмотр в реальном времени с низкой нагрузкой\",\"Concurrent previews\":\"Одновременный предварительный просмотр\",\"Window size\":\"Размер окна\",\"Small\":\"Маленький\",\"Medium\":\"Средний\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Предварительный просмотр начинается без звука и приостанавливается на скрытых вкладках. Аудио всегда требует явного щелчка.\",\"Quick info & chat focus\":\"Быстрая информация и фокус чата\",\"Enable model Quick View\":\"Включить быстрый просмотр модели\",\"Chat focus mode (opt-in)\":\"Режим фокусировки чата (по желанию)\",\"Show local timestamps\":\"Показывать местные временные метки\",\"System messages\":\"Системные сообщения\",\"Show\":\"Показать\",\"Dim\":\"Дим\",\"Hide\":\"Скрыть\",\"Chat filter strength\":\"Сила фильтра чата\",\"Low\":\"Низкий\",\"Strict\":\"Строгий\",\"Highlight words (comma separated)\":\"Выделить слова (через запятую)\",\"Muted words (comma separated)\":\"Приглушенные слова (через запятую)\",\"Collapse repeated messages\":\"Свернуть повторяющиеся сообщения\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Улучшения чата изменяют только текущее отображение. Они никогда не сохраняют чат, не отправляют сообщения и не дают чаевые автоматически.\",\"Shortcuts\":\"Ярлыки\",\"Command menu\":\"Меню команд\",\"Global model search\":\"Глобальный поиск модели\",\"Reset design defaults\":\"Сбросить настройки дизайна по умолчанию\",\"Optional modules\":\"Дополнительные модули\",\"Normal-page dock and card actions\":\"Закрепление на обычной странице и действия с карточками\",\"Real audio meters\":\"Настоящие аудиометры\",\"Media, app and contest directories\":\"Каталоги СМИ, приложений и конкурсов\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Переключатели модуля вступают в силу после перезагрузки страницы. Основные данные и модули резервного копирования остаются включенными, чтобы предотвратить потерю данных.\",\"Name the current filters and sorting\":\"Назовите текущие фильтры и сортировку\",\"My smart view\":\"Мой умный взгляд\",\"Delete smart view\":\"Удалить смарт-представление\",\"Smart view deleted\":\"Умный просмотр удален.\",\"tags\":\"теги\",\"countries\":\"страны\",\"Saved selection\":\"Сохраненный выбор\",\"Suggestions\":\"Предложения\",\"Selected\":\"Выбрано\",\"Website tags\":\"Теги веб-сайта\",\"Countries (multiple selection)\":\"Страны (множественный выбор)\",\"Select countries\":\"Выберите страны\",\"Search countries…\":\"Искать страны…\",\"Recognized in current models\":\"Признан в текущих моделях\",\"All countries\":\"Все страны\",\"Tags (multiple selection)\":\"Теги (множественный выбор)\",\"Select tags\":\"Выберите теги\",\"Search tags…\":\"Поиск по тегам…\",\"Room subject\":\"Тема комнаты\",\"Current online duration\":\"Текущая длительность эфира\",\"Local watch time and sessions\":\"Локальное время просмотра и сеансы\",\"HD, new, gaming and favorite badges\":\"Значки HD, новая модель, игры и избранное\",\"Homepage Studio\":\"Студия главной страницы\",\"Balanced preset\":\"Сбалансированный пресет\",\"Compact preset\":\"Компактный пресет\",\"Data-rich preset\":\"Информативный пресет\",\"Cinema preset\":\"Кинорежим\",\"Layout and pagination\":\"Макет и разбивка на страницы\",\"Show quick toolbar above models\":\"Показывать панель быстрых действий над моделями\",\"Cards per row\":\"Карточек в строке\",\"Image ratio\":\"Соотношение сторон\",\"Information display\":\"Отображение информации\",\"Detailed\":\"Подробно\",\"Overlay on image\":\"Поверх изображения\",\"Powerful sorting\":\"Расширенная сортировка\",\"Primary sort\":\"Первичная сортировка\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Сортировка меняет только порядок отображения загруженных карточек. Она не изменяет подписки или избранное в вашей учётной записи Chaturbate.\",\"Restore website order\":\"Восстановить порядок сайта\",\"Clear all filters\":\"Очистить все фильтры\",\"Search and range filters\":\"Поиск и диапазонные фильтры\",\"When several tags are selected\":\"Если выбрано несколько тегов\",\"Model must contain every tag\":\"Модель должна содержать все теги\",\"At least one tag matches\":\"Хотя бы один тег соответствует\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Фильтрация по странам показывает модели из любой выбранной страны. Собственные теги и признанные теги веб-сайта остаются доступными в списке, и их можно быстро найти с помощью поиска.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"После выбора тегов или стран RoomGrid выполняет поиск по всему текущему каталогу в реальном времени. На одной и той же странице результатов изначально отображается 120 совпадений, а при прокрутке загружается больше фрагментами, поэтому даже самые распространенные теги остаются отзывчивыми. Кнопка перезагрузки на панели инструментов главной страницы обновляет текущий список.\",\"Quick conditions\":\"Быстрые условия\",\"HD models only\":\"Только HD-модели\",\"New models only\":\"Только новые модели\",\"Gaming models only\":\"Только игровые модели\",\"Only saved in RoomGrid\":\"Только сохранённые в RoomGrid\",\"Only own favorites\":\"Только мои избранные\",\"Following only\":\"Только подписка\",\"Hide previously watched models\":\"Скрыть ранее просмотренные модели\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Страна, возраст и длительность эфира берутся из текущих публичных карточек или каталога комнат. Если Chaturbate не предоставляет поле, RoomGrid его не угадывает.\",\"Smart filter views\":\"Смарт-фильтры\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Сохраните текущие фильтры и сортировку как динамическое представление. Модели, которые появятся в сети позже и соответствуют правилам, появятся автоматически.\",\"Save current filter view\":\"Сохранить текущий вид фильтра\",\"No smart views saved yet.\":\"Умные представления пока не сохранены.\",\"What to show for each model\":\"Что показывать для каждой модели\",\"Import complete\":\"Импорт завершен\",\"Invalid file\":\"Неверный файл\",\"Range\":\"Диапазон\",\"Today\":\"Сегодня\",\"Week\":\"неделя\",\"Month\":\"Месяц\",\"Year\":\"Год\",\"Recording enabled\":\"Запись включена\",\"Private session (do not record)\":\"Частное занятие (не записывать)\",\"Retention days\":\"Дни хранения\",\"Usage time\":\"Время использования\",\"Summed camera time\":\"Суммарное время камеры\",\"Visits\":\"Посещения\",\"Known models\":\"Известные модели\",\"Weekly heatmap\":\"Еженедельная тепловая карта\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Агрегировано по дням недели и часам; темнее означает больше наблюдаемого времени.\",\"Sunday to Saturday, 24 hours per row\":\"С воскресенья по субботу, 24 часа в ряду\",\"Watch ranking\":\"Смотреть рейтинг\",\"little data\":\"мало данных\",\"No watch history yet.\":\"Истории просмотров пока нет.\",\"Workspace, filter and quality events\":\"Рабочее пространство, фильтрация и качество событий\",\"Recent timeline\":\"Недавняя временная шкала\",\"Export sensitive statistics\":\"Экспорт конфиденциальной статистики\",\"Import statistics\":\"Статистика импорта\",\"Delete selected range\":\"Удалить выбранный диапазон\",\"Delete local events in the selected range?\":\"Удалить локальные события в выбранном диапазоне?\",\"Clear all statistics\":\"Очистить всю статистику\",\"Permanently clear all local statistics?\":\"Удалить навсегда всю локальную статистику?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Экспорт статистики содержит названия моделей и временные метки, поэтому его следует рассматривать как конфиденциальный. Старые данные очищаются в соответствии с условиями хранения.\",\"new\":\"новый\",\"Under 1 minute\":\"Менее 1 минуты\",\"1–5 minutes\":\"1–5 минут\",\"5–15 minutes\":\"5–15 минут\",\"15–30 minutes\":\"15–30 минут\",\"30+ minutes\":\"30+ минут\",\"Local observations and your usage\":\"Местные наблюдения и ваше использование\",\"Public profile loaded on request\":\"Публичный профиль загружается по запросу\",\"Local RoomGrid interaction\":\"Локальное взаимодействие RoomGrid\",\"Local browser player\":\"Локальный браузерный плеер\",\"Optional official statistics API\":\"Дополнительный API официальной статистики\",\"Local daily aggregation\":\"Местное ежедневное агрегирование\",\"Local data\":\"Локальные данные\",\"Model summaries\":\"Обзор моделей\",\"Public profiles\":\"Публичные профили\",\"Usage events\":\"События использования\",\"Official account snapshots\":\"Официальные снимки аккаунта\",\"Daily rollups\":\"Ежедневные сводки\",\"Camera time\":\"Время камеры\",\"vs previous period\":\"по сравнению с предыдущим периодом\",\"Trends\":\"Тенденции\",\"Daily activity\":\"Ежедневная активность\",\"Playback quality\":\"Качество воспроизведения\",\"Privacy status\":\"Статус конфиденциальности\",\"Data provenance\":\"Происхождение данных\",\"Local RoomGrid usage\":\"Локальное использование RoomGrid\",\"7 days\":\"7 дней\",\"30 days\":\"30 дней\",\"90 days\":\"90 дней\",\"Not enough data yet.\":\"Пока недостаточно данных.\",\"Models revisited\":\"Пересмотр моделей\",\"Previews opened\":\"Предварительный просмотр открыт\",\"Quick Views\":\"Быстрый просмотр\",\"Samples\":\"Образцы\",\"Private session\":\"Частная сессия\",\"History paused\":\"История приостановлена\",\"Recording locally\":\"Запись локально\",\"Disabled\":\"Отключено\",\"No cloud sync or telemetry.\":\"Никакой облачной синхронизации и телеметрии.\",\"Raw events\":\"Необработанные события\",\"Only local observations and actions from this browser.\":\"Только локальные наблюдения и действия из этого браузера.\",\"Time\":\"Время\",\"Models\":\"Модели\",\"Online planner\":\"Онлайн-планировщик\",\"Data & privacy\":\"Данные и конфиденциальность\",\"Configure statistics dashboard\":\"Настроить панель статистики\",\"Preset\":\"Предустановка\",\"Minimal\":\"Минимальный\",\"Quality focus\":\"Фокус на качестве\",\"Privacy focus\":\"Конфиденциальность\",\"Reset dashboard\":\"Сбросить панель управления\",\"Size\":\"Размер\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Порядок, видимость и ширина сохраняются в настройках, безопасных для обновлений. Каждый виджет показывает свой источник данных.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Виджеты панели управления не отображаются. Откройте конфигурацию и выберите хотя бы один виджет.\",\"Median session\":\"Медианная сессия\",\"Daily usage trend\":\"Тенденция ежедневного использования\",\"Your trend appears here after you start using RoomGrid.\":\"Ваша тенденция появится здесь после того, как вы начнете использовать RoomGrid.\",\"Session-duration distribution\":\"Распределение продолжительности сеанса\",\"No session-duration data yet.\":\"Данных о продолжительности сеанса пока нет.\",\"Return rate\":\"Коэффициент возврата\",\"Total sessions\":\"Всего сеансов\",\"Longest observed\":\"Самый длинный наблюдаемый\",\"Model ranking\":\"Рейтинг моделей\",\"Select up to four models for a local comparison.\":\"Выберите до четырех моделей для локального сравнения.\",\"Quick View\":\"Быстрый просмотр\",\"No model data yet.\":\"Данных о модели пока нет.\",\"Models with time patterns\":\"Модели с временными закономерностями\",\"High confidence\":\"Высокая уверенность\",\"Moderate confidence\":\"Умеренная уверенность\",\"Planning horizon\":\"Горизонт планирования\",\"Likely next active times\":\"Вероятное время следующей активности\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Рассчитано на основе онлайн-выборок, наблюдаемых на местном уровне; это не гарантирует трансляцию. Уверенность повышается с увеличением количества данных.\",\"High\":\"Высокий\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Онлайн-наблюдений пока недостаточно. Планировщик автоматически появляется вместе с библиотекой.\",\"Discovery events\":\"События открытия\",\"Local interaction funnel\":\"Воронка локального взаимодействия\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Засчитываются только ваши действия на этом устройстве; содержимое чата и платежи никогда не отслеживаются.\",\"Workspace events\":\"События рабочей области\",\"Filter events\":\"Фильтровать события\",\"Tag events\":\"Отмечать события\",\"Top-four workspace comparison\":\"Сравнение четырех лучших рабочих пространств\",\"Workspace usage\":\"Использование рабочего пространства\",\"No workspace data yet.\":\"Данных о рабочем пространстве пока нет.\",\"Average active streams\":\"Среднее количество активных потоков\",\"Playback quality by model\":\"Качество воспроизведения по моделям\",\"No quality data yet.\":\"Данных о качестве пока нет.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Файл статистики недействителен или превышает безопасные пределы. Текущие данные не были изменены.\",\"Statistics import assistant · preview\":\"Помощник по импорту статистики · предварительный просмотр\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Пока ничего не изменилось. Выберите разделы для импорта и просмотра конфликтов.\",\"Safe merge (recommended)\":\"Безопасное слияние (рекомендуется)\",\"Import complete. The previous statistics were backed up automatically.\":\"Импорт завершен. Предыдущая статистика была скопирована автоматически.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"Импорт не может быть применен безопасно. Текущие данные и резервные копии остаются неизменными.\",\"File schema\":\"Схема файла\",\"Date range\":\"Диапазон дат\",\"Sensitive data\":\"Конфиденциальные данные\",\"Not marked sensitive\":\"Не отмечено как конфиденциальное\",\"Until manually resumed\":\"До возобновления вручную\",\"Not paused\":\"Не приостановлено\",\"Restore this statistics backup? The current state will be backed up first.\":\"Восстановить эту резервную копию статистики? Текущее состояние будет скопировано в первую очередь.\",\"Permanently delete this backup?\":\"Удалить эту резервную копию навсегда?\",\"Events\":\"События\",\"Estimated statistics storage\":\"Предполагаемый объем хранения статистики\",\"Recording, pauses and private sessions\":\"Запись, паузы и приватные сеансы\",\"Record usage statistics\":\"Запись статистики использования\",\"Pause 15 minutes\":\"Пауза 15 минут\",\"Pause 1 hour\":\"Пауза 1 час\",\"Pause 8 hours\":\"Пауза 8 часов\",\"Pause indefinitely\":\"Пауза на неопределенный срок\",\"Resume history\":\"История резюме\",\"History status\":\"Статус истории\",\"Private for 30 minutes\":\"Приват на 30 минут\",\"Private for 2 hours\":\"Приват на 2 часа\",\"Private until resumed\":\"Частное до возобновления\",\"End private session\":\"Завершить приватный сеанс\",\"Not active\":\"Не активен\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Частный сеанс также останавливает публичное наблюдение за моделью; обычная пауза истории останавливает личное использование и записи качества.\",\"Automatic deletion and retention\":\"Автоматическое удаление и сохранение\",\"Raw events (days)\":\"Необработанные события (дни)\",\"Quality samples (days)\":\"Качественные образцы (дни)\",\"Account snapshots (days)\":\"Снимки аккаунта (дни)\",\"Daily rollups (days)\":\"Ежедневные сводки (дни)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Необработанные события с истекшим сроком действия объединяются в ежедневные итоги перед автоматическим удалением. Снимки качества и аккаунта имеют свои ограничения.\",\"Statistics backup center\":\"Центр резервного копирования статистики\",\"Create internal backup now\":\"Создайте внутреннюю резервную копию сейчас\",\"Pseudonymized export\":\"Псевдонимизированный экспорт\",\"Print report\":\"Распечатать отчет\",\"Choose import file\":\"Выберите файл импорта\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Резервных копий внутренней статистики пока нет. После импорта, восстановления или создания вручную сохраняется до трех.\",\"Delete local statistics in this range? A backup will be created first.\":\"Удалить локальную статистику в этом диапазоне? Сначала будет создана резервная копия.\",\"Clear everything\":\"Очистить все\",\"Permanently clear all local statistics? A backup will be created first.\":\"Удалить навсегда всю локальную статистику? Сначала будет создана резервная копия.\",\"Data inventory and provenance\":\"Инвентаризация и происхождение данных\",\"Stored only in this browser\":\"Хранится только в этом браузере\",\"Coverage\":\"Покрытие\",\"Unknown or missing data is never inferred.\":\"Неизвестные или отсутствующие данные никогда не выводятся.\",\"Metric definitions & data quality\":\"Определения метрик и качество данных\",\"Accumulated locally while a RoomGrid page is active.\":\"Накапливается локально, пока страница RoomGrid активна.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Сумма всех наблюдаемых сеансов камеры; добавляются параллельные потоки.\",\"browser samples; no interpolation.\":\"образцы браузера; никакой интерполяции.\",\"Privacy boundary\":\"Граница конфиденциальности\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Никакой контент чата, платежи или медиа-файлы не сохраняются. Частные сеансы не добавляют событий.\",\"No history yet\":\"Пока нет истории\",\"Loading\":\"Загрузка\",\"Connected\":\"Подключено\",\"Locked\":\"Заблокировано\",\"Not connected\":\"Не подключено\",\"Username\":\"Имя пользователя\",\"24-character stats token\":\"24-значный токен статистики\",\"At least 8 characters\":\"Минимум 8 символов\",\"Secure official statistics connection\":\"Безопасное подключение к официальной статистике\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Необязательно. Токен никогда не входит в обычные настройки, журналы и экспорт. Режим сеанса сохраняет его только в памяти; зашифрованный режим использует изолированное локальное хранилище.\",\"Stats token\":\"Токен статистики\",\"Vault password\":\"Пароль хранилища\",\"Connect for this session\":\"Подключиться к этому сеансу\",\"Username or token is invalid.\":\"Имя пользователя или токен недействительны.\",\"Encrypt, save & connect\":\"Шифрование, сохранение и подключение\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Не удалось сохранить в зашифрованном виде. Проверьте токен, пароль и разрешения менеджера пользовательских сценариев.\",\"Get official token\":\"Получить официальный токен\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"Автоматическое обновление выполняется не чаще одного раза в пять минут. Этот модуль использует только официальный токен, который вы предоставляете намеренно.\",\"Account\":\"Аккаунт\",\"History snapshots\":\"Снимки истории\",\"Last refresh\":\"Последнее обновление\",\"Unlock local vault\":\"Разблокировать локальное хранилище\",\"Password\":\"Пароль\",\"Unlock\":\"Разблокировать\",\"Unlock failed.\":\"Разблокировать не удалось.\",\"Loading…\":\"Загрузка…\",\"Refresh now\":\"Обновить сейчас\",\"Auto-refresh every 5 minutes\":\"Автообновление каждые 5 минут\",\"Disconnect and delete token\":\"Отключить и удалить токен\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Удалить локальное соединение и зашифрованный токен? Историческая статистика остается.\",\"Broadcast status\":\"Статус трансляции\",\"Token balance\":\"Баланс токена\",\"Tips last hour\":\"Советы за последний час\",\"Followers\":\"Последователи\",\"Up votes\":\"Голосование вверх\",\"Down votes\":\"Голосование против\",\"Satisfaction\":\"Удовлетворение\",\"Registered viewers\":\"Зарегистрированные зрители\",\"Tokened viewers\":\"Токенизированные зрители\",\"Time online\":\"Время онлайн\",\"Follower trend\":\"Следующий тренд\",\"Tips trend\":\"Советы тренд\",\"Token-balance trend\":\"Тенденция баланса токенов\",\"Viewer trend\":\"Тенденция зрителей\",\"Account Health Center\":\"Центр состояния аккаунта\",\"Official My Collection\":\"Официальная моя коллекция\",\"Official support\":\"Официальная поддержка\",\"Export local statistics\":\"Экспорт локальной статистики\",\"Delete snapshot history\":\"Удалить историю снимков\",\"Delete the local official-statistics snapshots for this account?\":\"Удалить снимки местной официальной статистики для этого аккаунта?\",\"Last error\":\"Последняя ошибка\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Здесь находятся состояние токена, частота обновления, местная история и официальные ссылки. RoomGrid никогда не публикует, не отправляет сообщения и не выполняет транзакции автоматически.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Браузер не предоставил разрешение на уведомление. Уведомления на странице остаются активными.\",\"Notification profile\":\"Профиль уведомлений\",\"Go-live only\":\"Только в режиме реального времени\",\"Personalized\":\"Персонализированный\",\"Always (all events)\":\"Всегда (все события)\",\"Inactive days\":\"Неактивные дни\",\"Favorites, rating, priority and your tags decide relevance.\":\"Избранное, рейтинг, приоритет и ваши теги определяют релевантность.\",\"Includes go-live, offline and public-profile changes.\":\"Включает в себя изменения в режиме реального времени, в автономном режиме и в общедоступном профиле.\",\"Master switch\":\"Главный переключатель\",\"On\":\"Вкл.\",\"Off (default)\":\"Выкл. (по умолчанию)\",\"Monitored models\":\"Контролируемые модели\",\"Active tab\":\"Активная вкладка\",\"This tab\":\"Эта вкладка\",\"Another tab\":\"Другая вкладка\",\"Local notifications\":\"Локальные уведомления\",\"Notification delivery and display\":\"Доставка и отображение уведомлений\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Уведомления на странице используют свободную область верхней панели RoomGrid и не требуют разрешения браузера. Положение, продолжительность и плотность настраиваются.\",\"Delivery\":\"Доставка\",\"RoomGrid bar only\":\"Только панель RoomGrid\",\"Browser notifications only\":\"Только уведомления браузера\",\"RoomGrid bar and browser\":\"Панель RoomGrid и браузер\",\"No display, history only\":\"Нет дисплея, только история\",\"In-page position\":\"Позиция на странице\",\"Free top-bar area\":\"Бесплатная зона верхнего бара\",\"Floating top center\":\"Плавающий верхний центр\",\"Floating top right\":\"Плавающий вверху справа\",\"Display style\":\"Стиль отображения\",\"Compact single line\":\"Компактная однолинейная линия\",\"Display duration\":\"Продолжительность отображения\",\"Until dismissed\":\"До увольнения\",\"Visible at once\":\"Видно сразу\",\"Test notification\":\"Уведомление о тестировании\",\"General rules\":\"Общие правила\",\"Enable notifications\":\"Включить уведомления\",\"The browser did not grant notification permission.\":\"Браузер не предоставил разрешение на уведомление.\",\"Neutral text\":\"Нейтральный текст\",\"Quiet time starts\":\"Начало тихого времени\",\"Quiet time ends\":\"Время тишины заканчивается\",\"Cooldown (minutes)\":\"Время восстановления (минуты)\",\"Hourly limit\":\"Почасовой лимит\",\"Favorite threshold\":\"Любимый порог\",\"Open on click\":\"Открыть по клику\",\"Per model\":\"За модель\",\"Model\":\"Модель\",\"Library is empty.\":\"Библиотека пуста.\",\"Per group\":\"За группу\",\"Group\":\"Группа\",\"No groups.\":\"Никаких групп.\",\"Local notification list\":\"Локальный список уведомлений\",\"Mark all read\":\"Отметить все прочитанными\",\"Clear list\":\"Очистить список\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Проверка биографических данных только считывает статус и не запускает видео. Пользовательский скрипт может выполняться только тогда, когда открыта соответствующая вкладка сайта; несколько вкладок автоматически выбирают одну проверку.\",\"Shield status\":\"Статус щита\",\"Ready\":\"Готово\",\"Local PIN\":\"Местный PIN-код\",\"Set\":\"Установить\",\"Not set\":\"Не установлено\",\"Quick shield\":\"Быстрый щит\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Немедленно закрывает имена и изображения и приостанавливает/отключает звук всех медиафайлов. Восстановление возвращает предыдущее состояние носителя.\",\"Activate now\":\"Активировать сейчас\",\"Auto-shield after inactivity\":\"Автоматическая защита после бездействия\",\"Set or change local PIN\":\"Установить или изменить локальный PIN-код\",\"Leave empty to remove\":\"Оставьте пустым, чтобы удалить\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"ПИН-код представляет собой лишь локальный барьер доступа, а не шифрование, и не заменяет защиту учетной записи операционной системы.\",\"History controls\":\"Элементы управления историей\",\"Usage statistics\":\"Статистика использования\",\"Pause history 1 hour\":\"Приостановить историю на 1 час\",\"Clear statistics\":\"Очистить статистику\",\"Clear all local statistics? A backup will be created first.\":\"Очистить всю локальную статистику? Сначала будет создана резервная копия.\",\"Clear Discover settings\":\"Очистить настройки обнаружения\",\"Clear local Discover filters and feeds?\":\"Очистить местные фильтры и каналы Discover?\",\"Clear notification list\":\"Очистить список уведомлений\",\"Data boundary\":\"Граница данных\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Конфигурация, статистика просмотра, история уведомлений и настройки Discover хранятся локально в этом браузере. RoomGrid не содержит облачной синхронизации, загрузки учетной записи или телеметрии.\",\"Quota\":\"Квота\",\"Used\":\"Б/у\",\"Sensitivity & consent center\":\"Центр чувствительности и согласия\",\"Standard configuration\":\"Стандартная конфигурация\",\"Theme, layout, filters\":\"Тема, макет, фильтры\",\"Local\":\"Местный\",\"Model names, notes, timestamps, statistics\":\"Названия моделей, примечания, временные метки, статистика\",\"Warn on export\":\"Предупреждать об экспорте\",\"Secret\":\"Секрет\",\"Optional official statistics token\":\"Дополнительный жетон официальной статистики\",\"Session-only or encrypted vault\":\"Только сеансовое или зашифрованное хранилище\",\"Chat content\":\"Содержимое чата\",\"Never persisted\":\"Никогда не упорствовал\",\"Ephemeral\":\"Эфемерный\",\"Stream recording/screenshots\":\"Запись потока/скриншоты\",\"Not provided\":\"Не предусмотрено\",\"Policy guard\":\"Политическая охрана\",\"Performance profile\":\"Профиль производительности\",\"Playing streams\":\"Воспроизведение потоков\",\"Video elements\":\"Видеоэлементы\",\"Measured memory\":\"Измеренная память\",\"Unavailable in browser\":\"Недоступно в браузере\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Текущие измерения близки к настроенному пределу. Приостановите неиспользуемые камеры или выберите экономичный режим.\",\"Stream performance\":\"Производительность потока\",\"Eco\":\"Эко\",\"Power\":\"Мощность\",\"Concurrent stream limit\":\"Ограничение одновременного потока\",\"Adaptive quality\":\"Адаптивное качество\",\"Gradual startup\":\"Постепенный запуск\",\"Automatic performance mode\":\"Автоматический режим производительности\",\"Memory threshold\":\"Порог памяти\",\"Stream-count threshold\":\"Порог количества потоков\",\"Current auto state\":\"Текущее автоматическое состояние\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Неиспользованные превью остановлены. Новое видео остается на паузе на пределе, пока не освободится место. Высокие показатели начинаются постепенно; нет никакой гарантии «неограниченного количества камер».\",\"Multi-window workspaces\":\"Многооконные рабочие пространства\",\"Window name\":\"Имя окна\",\"Active and recent windows\":\"Активные и недавние окна\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"Библиотека моделей синхронизируется посредством локального слияния; каждое окно сохраняет свое рабочее пространство и настройки просмотра. Недавние ассоциации окон можно восстановить после перезапуска, если браузер это позволяет.\",\"Optional tools are disabled\":\"Дополнительные инструменты отключены\",\"Media, app and contest directories will not scan the current page.\":\"Каталоги мультимедиа, приложений и конкурсов не будут сканировать текущую страницу.\",\"Enable optional tools\":\"Включить дополнительные инструменты\",\"Local search\":\"Локальный поиск\",\"Search saved items and current page\":\"Поиск сохраненных элементов и текущей страницы\",\"Rescan current page\":\"Пересканировать текущую страницу\",\"Disable optional tools\":\"Отключить дополнительные инструменты\",\"Clear local side data\":\"Очистить локальные данные\",\"Clear app favorites, categories and media notes?\":\"Очистить избранное приложений, категории и медиа-заметки?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Эти модули организуют только общедоступные ссылки и текст, уже доступный на текущей странице. Они не копируют медиафайлы, не загружают их автоматически и не используют чат, токены или функции оплаты. Вход, проверка возраста, безопасность и формы оплаты остались нетронутыми.\",\"Photo and video areas\":\"Фото и видео зоны\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"На этой странице нет узнаваемой области средств массовой информации. Повторное сканирование на исходной странице с фотографиями или видео.\",\"Apps directory\":\"Каталог приложений\",\"Local category\":\"Местная категория\",\"Found on page\":\"Найдено на странице\",\"No recognizable app links are present on this page.\":\"На этой странице нет узнаваемых ссылок на приложения.\",\"Public contest results\":\"Результаты публичного конкурса\",\"No readable public contest table is present on this page.\":\"На этой странице нет читаемой публичной таблицы соревнований.\",\"Native account, help and broadcaster links\":\"Собственный аккаунт, помощь и ссылки на вещательные компании\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"На этой странице не обнаружено безопасных нативных ссылок; Пути оплаты и входа в систему намеренно фильтруются.\",\"Homepage filters reset\":\"Фильтры главной страницы сброшены\",\"Global appearance reset\":\"Глобальный сброс внешнего вида\",\"Version\":\"Версия\",\"Health score\":\"Оценка здоровья\",\"Page type\":\"Тип страницы\",\"Route generation\":\"Генерация маршрута\",\"Running modules\":\"Запуск модулей\",\"Local errors\":\"Локальные ошибки\",\"Repair center\":\"Ремонтный центр\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Резервная копия конфигурации создается перед каждым ремонтом. Эти действия не удаляют статистику и данные модели.\",\"Reset home filters\":\"Сбросить домашние фильтры\",\"Reset all home-page filters?\":\"Сбросить все фильтры главной страницы?\",\"Reset global appearance\":\"Сбросить глобальный внешний вид\",\"Reset the global appearance? Saved design profiles remain.\":\"Сбросить глобальный внешний вид? Сохраненные профили дизайна остаются.\",\"Normalize configuration\":\"Нормализовать конфигурацию\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Подтвердить и переписать текущую конфигурацию? Резервная копия создается в первую очередь.\",\"Close all previews\":\"Закрыть все предварительные просмотры\",\"Compatibility self-check\":\"Самопроверка совместимости\",\"Module and page status\":\"Статус модуля и страницы\",\"Page route test matrix\":\"Матрица тестирования маршрута страницы\",\"Current route\":\"Текущий маршрут\",\"Trigger\":\"Триггер\",\"Recent page transitions\":\"Недавние переходы страниц\",\"Capability layer\":\"Уровень возможностей\",\"Available\":\"Доступно\",\"Fallback\":\"Резервный вариант\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Если API отсутствует, возвращается только затронутый модуль. Повторяющиеся сбои в сети временно размыкают автоматический выключатель.\",\"Backup and recovery status\":\"Статус резервного копирования и восстановления\",\"Primary data\":\"Первичные данные\",\"Stable archive\":\"Стабильный архив\",\"Rolling backups\":\"Роллинг резервных копий\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"Самопроверка проверяет только читаемость и возможности браузера; он никогда не перезаписывает текущие данные автоматически. Для восстановления продолжает использоваться существующий подтвержденный поток резервного копирования.\",\"Sanitized local error log\":\"Обеззараженный локальный журнал ошибок\",\"No RoomGrid errors recorded.\":\"Ошибок RoomGrid не зарегистрировано.\",\"Run self-check again\":\"Запустите самопроверку еще раз\",\"Export diagnostics\":\"Экспорт диагностики\",\"Clear error log\":\"Очистить журнал ошибок\",\"Return to normal site\":\"Вернуться на обычный сайт\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"Диагностический экспорт не содержит рабочей области, названий моделей, примечаний и статистики. Параметры URL-запроса и возможные секретные значения удаляются из текста ошибки.\",\"Local history on\":\"Местная история на\",\"History off\":\"История отключена\",\"This settings page could not be loaded\":\"Не удалось загрузить страницу настроек.\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Ваши данные не были изменены. Попробуйте еще раз; если проблема остается, откройте Диагностику.\",\"Try again\":\"Попробуйте еще раз\",\"Open Diagnostics\":\"Открыть диагностику\",\"Public results\":\"Публичные результаты\",\"Public contest links\":\"Ссылки на публичные конкурсы\",\"Page content has not appeared yet\":\"Содержимое страницы еще не появилось\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid еще раз проверил страницу, но так и не смог найти область содержимого Chaturbate. Возможно, страница все еще загружается или сайт вернул пустое представление.\",\"Check again\":\"Проверьте еще раз\",\"Open RoomGrid Discover\":\"Откройте для себя RoomGrid\",\"Reload page\":\"Обновить страницу\",\"Hide notice\":\"Скрыть уведомление\",\"Open MultiCam\":\"Открыть МультиКам\",\"Open workstation in a new window\":\"Открыть рабочую станцию в новом окне\",\"Open Discover\":\"Открыть Обнаружить\",\"Statistics center\":\"Статистический центр\",\"Backup and import\":\"Резервное копирование и импорт\",\"Page and design options\":\"Варианты страницы и дизайна\",\"Hide immediately\":\"Скрыть немедленно\",\"All tools\":\"Все инструменты\",\"Search & commands\":\"Поиск и команды\",\"Page design\":\"Дизайн страницы\",\"Official collection\":\"Официальная коллекция\",\"Saved\":\"Сохранено\",\"Quick info\":\"Краткая информация\",\"Loading live frame\":\"Загрузка живого кадра\",\"Live still\":\"Живи еще\",\"Not live\":\"Не жить\",\"Pages\":\"Страницы\",\"Refresh\":\"Обновить\",\"Search…\":\"Искать…\",\"Tag Explorer\":\"Обозреватель тегов\",\"Refresh public catalog\":\"Обновить общедоступный каталог\",\"Online\":\"Онлайн\",\"Toggle favorite\":\"Переключить избранное\",\"Save\":\"Сохранить\",\"Remove favorite\":\"Удалить избранное\",\"Removed from library\":\"Удалено из библиотеки\",\"Saved to library\":\"Сохранено в библиотеке.\",\"Added to favorites\":\"Добавлено в избранное\",\"Removed from favorites\":\"Удалено из избранного\",\"Watching\":\"Смотрю\",\"Watch\":\"Смотреть\",\"Temporary watchlist\":\"Временный список наблюдения\",\"Removed from watchlist\":\"Удален из списка наблюдения\",\"Added to watchlist\":\"Добавлено в список наблюдения\",\"Choose group\":\"Выбрать группу\",\"Add to group…\":\"Добавить в группу…\",\"Group updated\":\"Группа обновлена\",\"Site room\":\"Комната сайта\",\"Select for bulk actions\":\"Выберите для массовых действий\",\"viewers\":\"зрители\",\"No public description\":\"Нет публичного описания\",\"Open in Tag Explorer\":\"Открыть в обозревателе тегов\",\"Feed name\":\"Название фида\",\"View name\":\"Посмотреть имя\",\"Name already exists\":\"Имя уже существует\",\"Saved-item limit reached\":\"Достигнут лимит сохраненных элементов\",\"Feed saved\":\"Лента сохранена.\",\"View saved\":\"Посмотреть сохраненные\",\"Applied\":\"Прикладной\",\"Backup exported\":\"Резервная копия экспортирована.\",\"Backup imported\":\"Резервная копия импортирована\",\"Invalid backup\":\"Неверная резервная копия\",\"New models\":\"Новые модели\",\"New since last visit\":\"Новое с момента последнего посещения\",\"New to me\":\"Новое для меня\",\"Returning\":\"Возвращение\",\"Stable online\":\"Стабильный онлайн\",\"Saved only\":\"Только сохранено\",\"Previously watched\":\"Ранее просмотренные\",\"Local collection\":\"Местная коллекция\",\"Hide visited today\":\"Скрыть посещенные сегодня\",\"No feed selected\":\"Фид не выбран\",\"No view selected\":\"Представление не выбрано\",\"Saved feeds\":\"Сохраненные каналы\",\"Saved views\":\"Сохраненные просмотры\",\"Save new\":\"Сохранить новый\",\"All public rooms\":\"Все общественные помещения\",\"Known HD\":\"Известный HD\",\"Random result\":\"Случайный результат\",\"Viewers ↓\":\"Зрители ↓\",\"Viewers ↑\":\"Зрители ↑\",\"Online duration ↓\":\"Продолжительность онлайн ↓\",\"Followers ↓\":\"Подписчики ↓\",\"Name A–Z\":\"Имя А–Я\",\"Name Z–A\":\"Имя Z–A\",\"New first\":\"Новое сначала\",\"Watch time ↓\":\"Время просмотра ↓\",\"Sessions ↓\":\"Сеансы ↓\",\"Last watched\":\"Последний просмотр\",\"Name, description, tag, place\":\"Имя, описание, тег, место\",\"Secondary sort\":\"Вторичная сортировка\",\"Shuffle again\":\"Перемешать еще раз\",\"All groups\":\"Все группы\",\"All tags (AND)\":\"Все теги (И)\",\"Any tag (OR)\":\"Любой тег (ИЛИ)\",\"Excluded tags (NOT)\":\"Исключенные теги (НЕ)\",\"Language, region, or country\":\"Язык, регион или страна\",\"Minimum online minutes\":\"Минимальное количество минут онлайн\",\"Maximum online minutes\":\"Максимальное количество минут онлайн\",\"Minimum local watch minutes\":\"Минимальное количество минут местного просмотра\",\"Advanced filters and Boolean tags\":\"Расширенные фильтры и логические теги\",\"Saved views and feeds\":\"Сохраненные просмотры и каналы\",\"Import\":\"Импорт\",\"Recent searches\":\"Недавние поиски\",\"Live previews and performance\":\"Живые превью и производительность\",\"Muted live preview for every visible card\":\"Отключенный предварительный просмотр в реальном времени для каждой видимой карты\",\"Refresh rate\":\"Частота обновления\",\"Real-time video while hovering\":\"Видео в реальном времени при наведении\",\"Hover delay\":\"Задержка при наведении\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Видимые карты используют кадры JPEG. При наведении только одна карточка переключается на приглушенное видео в реальном времени и полностью останавливается, когда указатель уходит. Высокая частота кадров значительно увеличивает пропускную способность и загрузку ЦП.\",\"saved\":\"сохранено\",\"previews\":\"превью\",\"Previews off\":\"Предварительный просмотр отключен\",\"No more feeds can be saved\":\"Больше фидов сохранить невозможно.\",\"Tag feed saved\":\"Фид тегов сохранен.\",\"Select tag\":\"Выберите тег\",\"Tag favorite\":\"Отметить избранное\",\"Block tag\":\"Блокировать тег\",\"Select tags first\":\"Сначала выберите теги\",\"Tag-group name\":\"Имя группы тегов\",\"Tag group saved\":\"Группа тегов сохранена.\",\"No gaming rooms in the current catalog\":\"В текущем каталоге нет игровых комнат\",\"All (AND)\":\"Все (И)\",\"Any (OR)\":\"Любой (ИЛИ)\",\"Exclude (NOT)\":\"Исключить (НЕ)\",\"Search tags\":\"Поиск по тегам\",\"Tag search\":\"Поиск по тегам\",\"Combination\":\"Комбинация\",\"Model sorting\":\"Сортировка моделей\",\"Show in Discover\":\"Показать в разделе Discover\",\"Save tag group\":\"Сохранить группу тегов\",\"Save tag feed\":\"Сохранить фид тегов\",\"No tag groups saved yet.\":\"Группы тегов пока не сохранены.\",\"Unknown region\":\"Неизвестный регион\",\"Unknown age\":\"Неизвестный возраст\",\"Public “New”\":\"Паблик «Новый»\",\"My new cams\":\"Мои новые камеры\",\"New-cams feed saved\":\"Лента новых камер сохранена.\",\"Save “My new cams” feed\":\"Сохранить ленту «Мои новые камеры»\",\"Create gaming workspace\":\"Создайте игровое рабочее пространство\",\"No matches for this combination.\":\"Нет совпадений для этой комбинации.\",\"Select tags to preview matching rooms here.\":\"Выберите теги, чтобы просмотреть подходящие комнаты здесь.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Ищите, добавляйте в избранное, блокируйте и объединяйте общедоступные теги. В трендах используются только изменения каталога, наблюдаемые на этом устройстве.\",\"Current tags\":\"Текущие теги\",\"Counts show current matches and change since the prior fetch.\":\"Подсчеты показывают текущие совпадения и изменения с момента предыдущей выборки.\",\"Results for selected tags\":\"Результаты по выбранным тегам\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Использует ту же сортировку и навигацию SingleCam, что и Discover.\",\"Categories, age, and regions\":\"Категории, возраст и регионы\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Показываются только явные общедоступные поля. Области, разделенные запятыми, объединяются с помощью OR; неизвестные значения остаются видимыми. Часовой пояс не угадывается.\",\"New and gaming\":\"Новое и игровое\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Местное первое появление, общедоступный флаг «Новый», временный список наблюдения и игровые темы остаются отдельными.\",\"Tag groups\":\"Группы тегов\",\"Save AND, OR, or NOT combinations.\":\"Сохраняйте комбинации И, ИЛИ или НЕ.\",\"Favorite tags\":\"Любимые теги\",\"Your personal shortcut list.\":\"Ваш личный список ярлыков.\",\"No favorites yet\":\"Пока нет избранных\",\"Related tags\":\"Связанные теги\",\"Tags co-occurring on the currently matching models.\":\"Теги, совпадающие с совпадающими в данный момент моделями.\",\"Personal tag history\":\"Личная история тегов\",\"Recently and frequently used on this device.\":\"Недавно и часто использовал на этом устройстве.\",\"Bulk target group\":\"Массовая целевая группа\",\"Selected models added to group\":\"Выбранные модели добавлены в группу\",\"New workspace name\":\"Новое имя рабочей области\",\"Created from a Discover selection\":\"Создано на основе выбора Discover\",\"Workspace created\":\"Рабочая область создана\",\"Save all\":\"Сохранить все\",\"Create workspace\":\"Создать рабочую область\",\"Loading public catalog…\":\"Загрузка общедоступного каталога…\",\"Loading the complete catalog in the background…\":\"Загрузка полного каталога в фоновом режиме…\",\"Updated\":\"Обновлено\",\"Public rooms\":\"Общественные помещения\",\"The catalog is loading.\":\"Каталог загружается.\",\"No matching rooms. Adjust search or filters.\":\"Нет подходящих номеров. Настройте поиск или фильтры.\",\"Previous\":\"Предыдущий\",\"Next\":\"Далее\",\"Move up\":\"Вверх\",\"Move down\":\"Двигаться вниз\",\"Number shown\":\"Показанный номер\",\"Widget width\":\"Ширина виджета\",\"Full\":\"Полный\",\"Nothing here yet.\":\"Здесь пока ничего.\",\"Newly online\":\"Недавно онлайн\",\"Recently visited\":\"Недавно посещенные\",\"Frequently visited\":\"Часто посещаемые\",\"Homepage appearance, pagination, and performance\":\"Внешний вид домашней страницы, нумерация страниц и производительность\",\"Information density\":\"Плотность информации\",\"Layout\":\"Макет\",\"Data rich\":\"Богатый объем данных\",\"Static\":\"Статический\",\"Show statistics summary\":\"Показать сводную статистику\",\"Show personal widgets\":\"Показать персональные виджеты\",\"Sections below the live catalog\":\"Разделы под живым каталогом\",\"Homepage live models\":\"Домашняя страница живые модели\",\"All live rooms in one place\":\"Все живые комнаты в одном месте\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Используйте тот же полный поиск, сортировку, группы, теги и фильтры локальной истории, что и в Discover.\",\"Refresh live catalog\":\"Обновить действующий каталог\",\"Homepage settings\":\"Настройки главной страницы\",\"Matches a favorite gaming theme\":\"Соответствует любимой игровой теме\",\"Saved models\":\"Сохраненные модели\",\"Online favorites\":\"Интернет-фавориты\",\"Local watch time\":\"Местное время просмотра\",\"Continue last MultiCam workspace\":\"Продолжить последнее рабочее пространство MultiCam\",\"Open full Discover page\":\"Открыть полную страницу Discover\",\"Online notifications\":\"Онлайн-уведомления\",\"on\":\"на\",\"off\":\"выкл.\",\"Offline notifications\":\"Офлайн-уведомления\",\"Feed\":\"Кормить\",\"Recent\":\"Недавние\",\"Frequent\":\"Частый\",\"Saved filter views\":\"Сохраненные представления фильтров\",\"Favorites currently public and online.\":\"Избранное в настоящее время общедоступно и онлайн.\",\"New models or public streams that started recently.\":\"Новые модели или публичные трансляции, которые начались недавно.\",\"Current results from the selected feed.\":\"Текущие результаты из выбранного канала.\",\"Suggestions with reasons\":\"Предложения с причинами\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Основано только на общих общедоступных тегах и темах, популярных на местном уровне; нет вывода об идентичности.\",\"Sorted by local last-visit time.\":\"Сортировка по местному времени последнего посещения.\",\"Sorted by local watch time.\":\"Сортировка по местному времени просмотра.\",\"Saved models never visited\":\"Сохраненные модели никогда не посещались\",\"Saved in the library without local watch time.\":\"Сохранено в библиотеке без местного времени просмотра.\",\"Draggable dashboard widgets\":\"Перетаскиваемые виджеты панели управления\",\"Your RoomGrid homepage\":\"Ваша домашняя страница RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Быстрый, настраиваемый каталог в реальном времени с SingleCam, MultiCam, полными фильтрами, сохраненными видами и вашими личными виджетами.\",\"Audio activity\":\"Аудио активность\",\"Auto quality\":\"Качество авто\",\"Stream interrupted; status checks continue.\":\"Трансляция прервана; Проверка статуса продолжается.\",\"This browser cannot play HLS.\":\"Этот браузер не может воспроизводить HLS.\",\"Connecting to live stream…\":\"Подключение к прямой трансляции…\",\"The room is not public right now.\":\"Комната сейчас не является публичной.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"В настоящее время офлайн. Альтернативы тому же фильтру показаны ниже.\",\"Status temporarily unavailable; retrying automatically.\":\"Статус временно недоступен; повторная попытка автоматически.\",\"Loading public profile…\":\"Загрузка общедоступного профиля…\",\"Profile temporarily unavailable.\":\"Профиль временно недоступен.\",\"No cached profile.\":\"Нет кэшированного профиля.\",\"Public profile changed\":\"Публичный профиль изменен\",\"No public bio.\":\"Никакой публичной биографии.\",\"Source\":\"Источник\",\"Observed\":\"Наблюдается\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Показаны только ссылки, опубликованные самостоятельно в общедоступном профиле. Совпадающее имя пользователя само по себе не является доказательством личности.\",\"Profile information\":\"Информация профиля\",\"Preparing…\":\"Подготовка…\",\"Play\":\"Играть\",\"Unmute\":\"Включить звук\",\"Mute\":\"Отключить звук\",\"Volume\":\"Объем\",\"Contain\":\"Содержать\",\"Cover\":\"Обложка\",\"Brightness\":\"Яркость\",\"Contrast\":\"Контраст\",\"Saturation\":\"Насыщенность\",\"Hue\":\"Хюэ\",\"Sepia\":\"сепия\",\"Grayscale\":\"Оттенки серого\",\"Invert\":\"Инвертировать\",\"Blur\":\"Размытие\",\"Image zoom\":\"Увеличение изображения\",\"Recording saved\":\"Запись сохранена.\",\"Screenshot saved\":\"Скриншот сохранен.\",\"Recording\":\"Запись\",\"Mirror\":\"Зеркало\",\"Flip vertically\":\"Перевернуть вертикально\",\"Rotate\":\"Поворот\",\"Show edited\":\"Показать отредактированные\",\"Show original\":\"Показать оригинал\",\"Reset image\":\"Сбросить изображение\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Удерживайте Ctrl и прокрутите, чтобы увеличить масштаб; перетащите изображение при увеличении. Дважды щелкните для сброса.\",\"Zoom out\":\"Уменьшить масштаб\",\"Zoom in\":\"Увеличить масштаб\",\"Reset zoom and position\":\"Сбросить масштаб и положение\",\"Player window\":\"Окно проигрывателя\",\"Fullscreen\":\"Полноэкранный режим\",\"Private notes, memories, or things to check next time (stored locally)\":\"Личные заметки, воспоминания или вещи, которые стоит проверить в следующий раз (хранятся локально)\",\"Autosave is on\":\"Автосохранение включено\",\"Enter your own tag…\":\"Введите свой тег…\",\"Saved at\":\"Сохранено в\",\"Local details saved\":\"Местные данные сохранены.\",\"Remove tag\":\"Удалить тег\",\"No personal tags yet.\":\"Персональных тегов пока нет.\",\"Saving…\":\"Сохранение…\",\"Create your first group under Groups.\":\"Создайте свою первую группу в разделе «Группы».\",\"Added to MultiCam\":\"Добавлено в Мультикам.\",\"Edit private details\":\"Редактировать личные данные\",\"Trans\":\"Транс\",\"Region\":\"Регион\",\"Current online time\":\"Текущее онлайн-время\",\"Stream attributes\":\"Атрибуты потока\",\"New\":\"Новый\",\"Yes\":\"Да\",\"No / unknown\":\"Нет / неизвестно\",\"Last observed\":\"Последнее наблюдение\",\"Local visits\":\"Местные посещения\",\"First watched\":\"Впервые посмотрел\",\"No public description.\":\"Нет публичного описания.\",\"No public tags available.\":\"Нет доступных общедоступных тегов.\",\"Quick-add your own tag…\":\"Быстро добавьте свой собственный тег…\",\"Public tags\":\"Публичные теги\",\"Add\":\"Добавить\",\"Private details\":\"Частные данные\",\"Everything stays in this browser and survives updates.\":\"Все остается в этом браузере и сохраняется при обновлениях.\",\"Private notebook\":\"Личный блокнот\",\"Keep on watchlist\":\"Продолжайте в списке наблюдения\",\"Group assignment (multiple)\":\"Групповое задание (несколько)\",\"Save now\":\"Сохранить сейчас\",\"Chat loads only when this section is opened.\":\"Чат загружается только при открытии этого раздела.\",\"Official chat\":\"Официальный чат\",\"Official chat connected.\":\"Официальный чат подключен.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Чат загружен; текущую структуру страницы невозможно полностью изолировать.\",\"Chat loaded.\":\"Чат загружен.\",\"Loading official chat…\":\"Загрузка официального чата…\",\"Official chat room\":\"Официальный чат\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Загружается только при необходимости; RoomGrid никогда не сохраняет и не отправляет автоматически сообщения чата.\",\"Reload chat\":\"Перезагрузить чат\",\"Open official room in a new window\":\"Открыть официальную комнату в новом окне\",\"Hide chat\":\"Скрыть чат\",\"Live observations\":\"Живые наблюдения\",\"Trend confidence\":\"Уверенность в тренде\",\"Known time slots\":\"Известные временные интервалы\",\"Typical peak\":\"Типичный пик\",\"Watched in 7 days\":\"Просмотрено за 7 дней\",\"Watched in 30 days\":\"Просмотрено за 30 дней\",\"Current streak\":\"Текущая серия\",\"Typical weekly online times\":\"Типичное еженедельное время онлайн\",\"Online times will be learned on this device from now on.\":\"Теперь время онлайн будет узнаваться на этом устройстве.\",\"Based on observations on this device; missing information is never guessed.\":\"На основе наблюдений за этим устройством; недостающая информация никогда не угадывается.\",\"Activity by weekday\":\"Активность по дням недели\",\"Most likely online times\":\"Скорее всего время онлайн\",\"Online-time map\":\"Карта онлайн-времени\",\"Public covers and locally saved example images.\":\"Публичные обложки и локально сохраненные примеры изображений.\",\"Import quality\":\"Импортное качество\",\"Original file\":\"Исходный файл\",\"Very high (up to 4K)\":\"Очень высокое (до 4K)\",\"High (up to 1440p)\":\"Высокое (до 1440p)\",\"Space-saving (up to 900p)\":\"Экономия места (до 900p)\",\"Add images from computer\":\"Добавить изображения с компьютера\",\"Use website cover\":\"Используйте обложку сайта\",\"Use first image as cover\":\"Использовать первое изображение в качестве обложки\",\"Covers and local gallery\":\"Обложки и местная галерея\",\"Updating images…\":\"Обновление изображений…\",\"Loading images…\":\"Загрузка изображений…\",\"Own image\":\"Собственный имидж\",\"Example image\":\"Пример изображения\",\"Current cover\":\"Текущая обложка\",\"Alternative preview\":\"Альтернативный предварительный просмотр\",\"Current public image\":\"Текущий публичный имидж\",\"Saving cover…\":\"Сохранение обложки…\",\"Set as cover\":\"Установить в качестве обложки\",\"Delete this image from the local gallery?\":\"Удалить это изображение из локальной галереи?\",\"No images yet.\":\"Пока нет изображений.\",\"saved locally\":\"сохранено локально\",\"own images\":\"собственные изображения\",\"Update public previews\":\"Обновить общедоступные предварительные версии\",\"Processing your images…\":\"Обработка изображений…\",\"ALL PAGE SOUNDS OFF\":\"ВСЕ СТРАНИЦЫ ВЫКЛЮЧЕНЫ\",\"PAGE SOUNDS ALLOWED\":\"ЗВУКИ СТРАНИЦЫ РАЗРЕШЕНЫ\",\"Chat/token sounds OFF\":\"Звук чата/жетона ВЫКЛ.\",\"Chat/token sounds ON\":\"Звук чата/токенов включен\",\"Reconnecting the player with the new settings…\":\"Повторное подключение плеера с новыми настройками…\",\"Turn off chat, token and tip sounds completely\":\"Полностью отключите звуки чата, жетонов и подсказок.\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Блокирует звуки страницы из аудиоэлементов и веб-аудио. Потоковое аудио по-прежнему контролируется отдельно с помощью параметра «Звук включен».\",\"No alternatives available.\":\"Альтернатив нет.\",\"Similar models and offline alternatives\":\"Похожие модели и офлайн-альтернативы\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Приоритет по общим тегам; никакой идентификации или вывода о внешней учетной записи.\",\"Chat\":\"Чат\",\"Notes & organization\":\"Заметки и организация\",\"Images\":\"Изображения\",\"SingleCam sections\":\"Секции SingleCam\",\"Back to original results\":\"Вернуться к исходным результатам\",\"Private workspace\":\"Личное рабочее пространство\",\"Chat on/off\":\"Чат вкл/выкл\",\"Hide similar models\":\"Скрыть похожие модели\",\"Show similar models\":\"Показать похожие модели\",\"RoomGrid Home\":\"КомнатаСетка Главная\",\"RoomGrid Tags\":\"Теги RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Публичный каталог недоступен; показ местной библиотеки.\",\"Drag to resize sidebar\":\"Перетащите, чтобы изменить размер боковой панели\",\"The workspace could not be saved safely. Export a backup now.\":\"Не удалось безопасно сохранить рабочую область. Экспортируйте резервную копию прямо сейчас.\",\"Open menu or restore controls\":\"Открыть меню или восстановить элементы управления\",\"Controls restored\":\"Управление восстановлено\",\"Menu\":\"Меню\",\"Show toolbar\":\"Показать панель инструментов\",\"Tools\":\"Инструменты\",\"Show sidebar\":\"Показать боковую панель\",\"Restore controls\":\"Восстановить элементы управления\",\"Show toolbar, groups, and menus\":\"Показать панель инструментов, группы и меню\",\"Open more tools\":\"Открыть больше инструментов\",\"all watch states\":\"все состояния просмотра\",\"Watch status\":\"Статус просмотра\",\"0 means no viewer limit.\":\"0 означает отсутствие ограничения по количеству зрителей.\",\"Import temporary URLs; they disappear after refresh\":\"Импортировать временные URL-адреса; они исчезают после обновления\",\"URL\":\"URL-адрес\",\"Filter current group\":\"Фильтровать текущую группу\",\"All status\":\"Все статусы\",\"Hide offline/private\":\"Скрыть офлайн/лично\",\"Collapse toolbar\":\"Свернуть панель инструментов\",\"Windows per page\":\"Окна на страницу\",\"Custom grid\":\"Пользовательская сетка\",\"Choose MultiCam audio routing\":\"Выберите маршрутизацию звука MultiCam\",\"Audio: manual\":\"Аудио: руководство\",\"Audio: solo cam\":\"Аудио: соло-камера\",\"Audio: follow focus\":\"Аудио: следовать фокусу\",\"Auto\":\"Авто\",\"Userscript version\":\"Версия пользовательского скрипта\",\"Open Discover and dashboard\":\"Откройте Discover и панель управления\",\"Global search and quick commands · Ctrl+K\":\"Глобальный поиск и быстрые команды · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Состояние потока, бюджет качества, звук, сцены и замены\",\"Control\":\"Контроль\",\"Rooms\":\"Комнаты\",\"Filter\":\"Фильтр\",\"Playback\":\"Воспроизведение\",\"Stabilizing live preview…\":\"Стабилизация предварительного просмотра в реальном времени…\",\"Recovering live preview…\":\"Восстановление предварительного просмотра…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"Предварительный просмотр в реальном времени ненадолго приостанавливается, чтобы предотвратить цикл перезагрузки. Повторите попытку через некоторое время.\",\"Collapse groups\":\"Свернуть группы\",\"Expand/collapse users\":\"Развернуть/свернуть пользователей\",\"Output volume; no live signal level is available for this stream\":\"Выходной объем; для этого потока не доступен уровень живого сигнала\",\"OFF\":\"ВЫКЛ.\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Вставьте URL-адреса. Поддерживаются страницы комнат, m3u8, mp4/webm. Обычные страницы будут проанализированы с максимальной эффективностью.\",\"Actual resolution, connection and buffer\":\"Реальное разрешение, подключение и буфер\",\"Drag to reorder / move group\":\"Перетащите, чтобы изменить порядок/переместить группу\",\"All time\":\"Все время\",\"Click a model for details.\":\"Нажмите на модель, чтобы узнать подробности.\",\"Most watched groups\":\"Самые просматриваемые группы\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Поля учетной записи, такие как дата присоединения, отображаются только в том случае, если Chaturbate ясно отображает их на одной странице. Скрипт не придумывает значения.\",\"Back\":\"Назад\",\"Statistics import failed: \":\"Не удалось импортировать статистику:\",\"Recent saves\":\"Недавние сохранения\",\"Rooms per page\":\"Комнат на странице\",\"Custom rows\":\"Пользовательские строки\",\"Custom columns\":\"Пользовательские столбцы\",\"Main width\":\"Основная ширина\",\"Main height\":\"Основная высота\",\"Modules\":\"Модули\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Модули запускаются только на соответствующих типах страниц. При сохранении перезагружается текущая страница.\",\"Tools on normal pages\":\"Инструменты на обычных страницах\",\"True audio-level meters\":\"Настоящие измерители уровня звука\",\"MultiCam control center\":\"Центр управления МультиКам\",\"No active streams yet\":\"Активных трансляций пока нет\",\"Stream health\":\"Здоровье потока\",\"Budget off\":\"Бюджет выключен\",\"Economy · 2160p total\":\"Эконом · Всего 2160p\",\"Balanced · 4320p total\":\"Сбалансированный · Всего 4320p\",\"High · 6480p total\":\"Высокое · Всего 6480p\",\"Ultra · 8640p total\":\"Ультра · всего 8640p\",\"Quality budget is off; global and per-camera limits still apply.\":\"Бюджет качества отключен; глобальные ограничения и ограничения на каждую камеру по-прежнему применяются.\",\"Quality budget\":\"Бюджет качества\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Распределяет качество по активным камерам. Явное качество каждой камеры всегда побеждает.\",\"Give the focused cam double weight\":\"Придайте сфокусированному кулачку двойной вес\",\"Manual per camera\":\"Руководство для каждой камеры\",\"Solo: one camera only\":\"Соло: только одна камера\",\"Follow focused camera\":\"Следуйте за сфокусированной камерой\",\"Audio management\":\"Управление аудио\",\"Routing never overwrites saved mute states or individual volume.\":\"Маршрутизация никогда не перезаписывает сохраненные состояния отключения звука или индивидуальную громкость.\",\"Mute all\":\"Отключить все звуки\",\"Restore saved audio\":\"Восстановить сохраненный звук\",\"Scene name\":\"Название сцены\",\"Focus\":\"Фокус\",\"Grid\":\"Сетка\",\"cams\":\"камеры\",\"Load\":\"Загрузить\",\"No scenes saved for this workspace yet.\":\"Для этого рабочего пространства пока не сохранено ни одной сцены.\",\"Scenes\":\"Сцены\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Сохраните текущие слоты, макет и маршрутизацию звука для восстановления одним щелчком мыши.\",\"Save current view as scene\":\"Сохранить текущий вид как сцену\",\"All saved online models\":\"Все сохраненные онлайн-модели\",\"Replacement models\":\"Модели на замену\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Предпочтительные модели опробуются в порядке списка; только после этого используются онлайн-модели из выбранной группы. Ручные задания всегда выигрывают.\",\"Replace offline slots automatically\":\"Автоматическая замена офлайн-слотов\",\"Delay (seconds)\":\"Задержка (секунды)\",\"Candidate group\":\"Группа кандидатов\",\"Preferred replacements (multiple)\":\"Предпочтительные замены (несколько)\",\"Replacement rule saved\":\"Правило замены сохранено.\",\"Save replacement rule\":\"Сохранить правило замены\",\"Resolve duplicate shortcuts first:\":\"Сначала устраните повторяющиеся ярлыки:\",\"Name, note or own tag…\":\"Имя, заметка или собственный тег…\",\"Camera menu\":\"Меню камеры\",\"Camera menu · quality and sound\":\"Меню камеры · качество и звук\",\"Close menu\":\"Закрыть меню\",\"View and window\":\"Вид и окно\",\"Audio\":\"Аудио\",\"Quality and image\":\"Качество и имидж\",\"More actions\":\"Дополнительные действия\",\"Currently main\":\"На данный момент основной\",\"Set as main\":\"Сделать основным\",\"Pin live preview\":\"Закрепить предварительный просмотр в реальном времени\",\"Volume multiplier for this cam\":\"Множитель громкости для этой камеры\",\"Reset individual volume to 100%\":\"Сброс индивидуальной громкости до 100 %.\",\"Individual volume\":\"Индивидуальный объем\",\"Listen to this cam only\":\"Слушайте только эту камеру\",\"Set quality for this camera only\":\"Установить качество только для этой камеры\",\"Automatic (global/adaptive)\":\"Автоматический (глобальный/адаптивный)\",\" · available\":\"· доступен\",\"Quality for this camera\":\"Качество для этой камеры\",\"Hold for original\":\"Держите для оригинала\",\"Vivid\":\"Яркий\",\"Mono\":\"Моно\",\"Image adjustments\":\"Настройки изображения\",\"More tools\":\"Дополнительные инструменты\",\"Interface\":\"Интерфейс\",\"Show top controls\":\"Показать верхние элементы управления\",\"Collapse top controls\":\"Свернуть верхние элементы управления\",\"Show groups\":\"Показать группы\",\"Windows\":\"Окна\",\"Open workspace in new window\":\"Открыть рабочее пространство в новом окне\",\"Control center\":\"Центр управления\",\"Choose the data to export\":\"Выберите данные для экспорта\",\"Preview the file and conflicts before applying it\":\"Предварительный просмотр файла и конфликтов перед его применением.\",\"Help\":\"Помощь\",\"Changes in this version\":\"Изменения в этой версии\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Рабочие области, библиотека моделей, безопасный импорт, глобальный дизайн, расширенные обычные страницы, расширенное обнаружение/панель мониторинга и элементы управления качеством/изображением проигрывателя.\",\"Muted\":\"Без звука\",\"Performance limit reached\":\"Достигнут предел производительности\",\"Drag to resize main width\":\"Перетащите, чтобы изменить размер основной ширины\",\"Drag to resize main height\":\"Перетащите, чтобы изменить размер основной высоты\",\"Stable\":\"Стабильный\",\"Unstable\":\"Нестабильный\",\"Refresh all cameras\":\"Обновить все камеры\",\"Reconnect current models\":\"Повторно подключить текущие модели\",\"Switch to grid view\":\"Переключиться на вид сетки\",\"Switch to focus view\":\"Переключиться в режим фокусировки\",\"Manage workspaces\":\"Управление рабочими пространствами\",\"Create, duplicate, archive and device layouts\":\"Создание, дублирование, архивирование и макеты устройств\",\"Open model library\":\"Открыть библиотеку моделей\",\"Notes, tags, ratings and bulk actions\":\"Заметки, теги, рейтинги и массовые действия\",\"Design options\":\"Варианты дизайна\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Универсальная рабочая станция с несколькими представлениями для комнат, записи, оповещений, снимков экрана, повторных подключений и инструментов обработки данных.\",\"Username ↵\":\"Имя пользователя ↵\",\"Search rooms\":\"Поиск номеров\",\"Invalid username\":\"Неверное имя пользователя\",\"Hide offline\":\"Скрыть офлайн\",\"Hide private\":\"Скрыть личное\",\"Manual\":\"Руководство\",\"By status\":\"По статусу\",\"By name\":\"По имени\",\"By added time\":\"По добавленному времени\",\"By watch time\":\"По времени просмотра\",\"By sessions\":\"По сессиям\",\"By last watched\":\"По последнему просмотру\",\"By first watched\":\"По первому просмотру\",\"By viewer count\":\"По количеству зрителей\",\"By last online\":\"По последнему онлайну\",\"Advanced filters\":\"Расширенные фильтры\",\"Watched before\":\"Смотрел раньше\",\"Currently in view\":\"В настоящее время на виду\",\"Include tag\":\"Включить тег\",\"Exclude tag\":\"Исключить тег\",\"Minimum watch time (minutes)\":\"Минимальное время просмотра (минуты)\",\"Watched within days\":\"Просмотрено в течение нескольких дней\",\"Refresh all\":\"Обновить все\",\"Online alerts\":\"Онлайн-оповещения\",\"Offline alerts\":\"Офлайн-оповещения\",\"Desktop notification + card flash when a model goes online\":\"Уведомление на рабочем столе + вспышка карты, когда модель выходит в интернет\",\"Desktop notification + card flash when a model goes offline\":\"Уведомление на рабочем столе + мигание карточки, когда модель выходит из сети\",\"Dark mode\":\"Темный режим\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Используйте темную тему интерфейса. Потоковое видео и аудио не изменяются.\",\"Dark mode enabled\":\"Темный режим включен\",\"Light mode enabled\":\"Светлый режим включен\",\"Mute token/tip sounds\":\"Отключить звуки токенов и чаевых\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Блокирует только звуки уведомлений о токенах и чаевых. Звук трансляции остаётся включённым.\",\"Token/tip sounds muted\":\"Звуки токенов и чаевых отключены\",\"Token/tip sounds enabled\":\"Звуки токенов и чаевых включены\",\"Clean\":\"Чистый\",\"Clean mode on\":\"Режим очистки включен\",\"Exit clean mode\":\"Выйти из режима очистки\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Временно скройте все элементы управления и наложения. Ярлык: Alt+P / Alt+C. Нажмите Esc, чтобы выйти.\",\"Window first\":\"Окно сначала\",\"Window-first mode on\":\"Режим «сначала окно» включен\",\"Exit window-first\":\"Выход из окна первым\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Скройте оболочку приложения и оставьте окна комнаты видимыми. Ярлык: Alt+V.\",\"Show thumbnails\":\"Показать миниатюры\",\"Hide thumbnails\":\"Скрыть миниатюры\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Показать или скрыть полосу миниатюр в режиме фокусировки. Ярлык: Альт+Т.\",\"Choose Focus rooms\":\"Выбирайте комнаты Фокус\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Выберите комнаты для режима Фокус. Первая выбранная комната — это главный экран; используйте ↑/↓, чтобы установить порядок дополнительных окон.\",\"Main screen\":\"Главный экран\",\"Right window\":\"Правое окно\",\"Bottom window\":\"Нижнее окно\",\"Empty slot\":\"Пустой слот\",\"Drag this model to a Focus slot\":\"Перетащите эту модель в слот Focus.\",\"Choose model for this slot\":\"Выберите модель для этого слота\",\"Select a saved model for this exact Focus slot.\":\"Выберите сохраненную модель именно для этого слота Focus.\",\"Choose model for this window\":\"Выберите модель для этого окна\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Выберите сохраненную модель именно для этого окна сетки. Существующие модели остаются сохраненными.\",\"Remove from this grid window\":\"Удалить из этого окна сетки\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Очищен только этот слот. Сохраненная модель останется в вашем рабочем пространстве.\",\"Show in grid window\":\"Показать в окне сетки\",\"Choose the exact active grid window for this model.\":\"Выберите точное активное окно сетки для этой модели.\",\"Current\":\"Текущий\",\"Model removed from this grid window.\":\"Модель удалена из этого окна сетки.\",\"No saved models in this group.\":\"В этой группе нет сохраненных моделей.\",\"Secondary window\":\"Вторичное окно\",\"Use current page automatically\":\"Использовать текущую страницу автоматически\",\"Clear custom selection\":\"Очистить индивидуальный выбор\",\"Apply Focus layout\":\"Применить макет фокуса\",\"No rooms are available in the current group and filter.\":\"В текущей группе и фильтре нет свободных номеров.\",\"Fit: full image\":\"Подходит: полное изображение\",\"Fit: fill window\":\"По размеру: заполнить окно\",\"Switch between full image and cropped fill.\":\"Переключение между полным изображением и обрезанной заливкой.\",\"Playback settings\":\"Настройки воспроизведения\",\"Max stream quality\":\"Максимальное качество потока\",\"Auto / no cap\":\"Авто / без крышки\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + колесико масштабирования\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Увеличьте карту с помощью Ctrl/Command + колесика мыши, перетащите при увеличении, дважды щелкните для сброса.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Выйдите из режима очистки. Ярлык: Alt+P / Alt+C или Esc.\",\"Clean · Alt+P/C / Esc\":\"Очистить · Alt+P/C / Esc\",\"(synced from another tab)\":\"(синхронизировано с другой вкладки)\",\"Type a username and press Enter. Shortcut: /\":\"Введите имя пользователя и нажмите Enter. Ярлык: /\",\"Filter visible rooms by username\":\"Фильтровать видимые комнаты по имени пользователя\",\"Show / hide group sidebar\":\"Показать/скрыть боковую панель группы\",\"Global volume. Per-room mute is still respected.\":\"Глобальный объем. Отключение звука в каждой комнате по-прежнему пользуется уважением.\",\"Base tile size in grid view\":\"Размер базовой плитки в виде сетки\",\"Sort rooms in the current group\":\"Сортировать помещения в текущей группе\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Высота главного экрана в режиме фокусировки. Вы также можете перетащить разделитель.\",\"Main-screen aspect ratio in Focus mode\":\"Соотношение сторон главного экрана в режиме фокусировки\",\"Thumbnail width in Focus mode\":\"Ширина миниатюр в режиме фокусировки\",\"Show every saved room. The remove button deletes globally in this view.\":\"Показать каждую сохраненную комнату. Кнопка «Удалить» удаляет глобально в этом представлении.\",\"Create an isolated group\":\"Создать изолированную группу\",\"More tools and maintenance\":\"Дополнительные инструменты и обслуживание\",\"GROUPS\":\"ГРУППЫ\",\"All saved\":\"Все сохранено\",\"Default\":\"По умолчанию\",\"Online now\":\"Сейчас онлайн\",\"New group name:\":\"Новое название группы:\",\"Rename\":\"Переименовать\",\"Rename to:\":\"Переименуйте в:\",\"Delete group\":\"Удалить группу\",\"Live preview\":\"Предварительный просмотр в реальном времени\",\"Loading preview…\":\"Загрузка предварительного просмотра…\",\"Private or away\":\"Частное или выездное\",\"Preview could not be loaded\":\"Не удалось загрузить предварительный просмотр.\",\"Toggle mute\":\"Отключить звук\",\"Resume\":\"Резюме\",\"Screenshot current frame\":\"Скриншот текущего кадра\",\"Record current card\":\"Записать текущую карту\",\"Stop recording\":\"Остановить запись\",\"Picture-in-Picture\":\"Картинка в картинке\",\"Remove from current group\":\"Удалить из текущей группы\",\"Delete saved room\":\"Удалить сохраненную комнату\",\"Mirror image\":\"Зеркальное изображение\",\"Flip image\":\"Перевернуть изображение\",\"Rotate left\":\"Повернуть влево\",\"Rotate right\":\"Повернуть вправо\",\"Reset view\":\"Сбросить вид\",\"Copy username\":\"Скопировать имя пользователя\",\"Add favorite\":\"Добавить в избранное\",\"Move to favorites only\":\"Переместить только в избранное\",\"Copied\":\"Скопировано\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Скриншот не удался. Браузер/CORS может заблокировать отрисовку этого потока.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"Запись является локальной и сохраняет это видео с соответствующим звуком, когда браузер отображает его. Используйте его только в том случае, если у вас есть разрешение на сохранение этого контента. Продолжать?\",\"Recording started\":\"Запись началась\",\"Recording is not supported for this video/browser\":\"Запись не поддерживается для этого видео/браузера\",\"Recording segment saved\":\"Сегмент записи сохранен.\",\"Final recording segment saved\":\"Последний сегмент записи сохранен.\",\"Recording paused; waiting for the stream to return\":\"Запись приостановлена; жду возобновления потока\",\"Recording resumed\":\"Запись возобновлена\",\"Recording paused; click to stop\":\"Запись приостановлена; нажмите, чтобы остановить\",\"No recording data to save\":\"Нет данных записи для сохранения\",\"Recording settings saved\":\"Настройки записи сохранены.\",\"Segment length in minutes (1-180):\":\"Длина сегмента в минутах (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Битрейт видео в Мбит/с (0,5-20):\",\"Warn before leaving while recording\":\"Предупреждать перед уходом во время записи\",\"Recording is still active. Leave anyway?\":\"Запись все еще активна. Все равно уйти?\",\"Recording center\":\"Центр записи\",\"No active recordings\":\"Нет активных записей\",\"Waiting for source\":\"Ожидание источника\",\"Saved segments\":\"Сохраненные сегменты\",\"Duration\":\"Продолжительность\",\"Bitrate\":\"Битрейт\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Формат: MP4, если он поддерживается этим браузером; в противном случае лучший поддерживаемый запасной вариант.\",\"Record current page\":\"Записать текущую страницу\",\"Record current group\":\"Записать текущую группу\",\"Record online rooms\":\"Запись онлайн-комнат\",\"Stop all recordings\":\"Остановить все записи\",\"Show recording only\":\"Показать только запись\",\"Show all rooms\":\"Показать все номера\",\"Open current page rooms\":\"Открыть комнаты текущей страницы\",\"Move current page to group\":\"Переместить текущую страницу в группу\",\"Layout settings\":\"Настройки макета\",\"Recording settings\":\"Настройки записи\",\"Save settings\":\"Сохранить настройки\",\"Config backups\":\"Резервные копии конфигурации\",\"No config backups found\":\"Резервные копии конфигурации не найдены\",\"Online/offline history\":\"Онлайн/оффлайн история\",\"Group rules\":\"Правила группы\",\"Keep favorites first\":\"Держите избранное в первую очередь\",\"Temporary URL manager\":\"Временный менеджер URL-адресов\",\"Save current temporary URLs\":\"Сохранить текущие временные URL-адреса\",\"No temporary URLs\":\"Нет временных URL-адресов\",\"Share workspace\":\"Поделиться рабочим пространством\",\"Share current rooms and layout\":\"Поделитесь текущими комнатами и планировкой\",\"Copy share link\":\"Скопировать ссылку «Поделиться»\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Эта ссылка восстанавливает настройки помещений, групп и планировки на другой вкладке рабочей станции.\",\"Shared workspace imported\":\"Общая рабочая область импортирована.\",\"Shared workspace link is invalid\":\"Ссылка на общую рабочую область недействительна.\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Щелкните поле и нажмите новый ярлык. Backspace очищает это поле.\",\"Reset shortcuts\":\"Сбросить ярлыки\",\"Focus add box\":\"Фокус добавить поле\",\"Grid view\":\"Представление в виде сетки\",\"Focus view\":\"Вид в фокусе\",\"Clean mode\":\"Чистый режим\",\"Window-first mode\":\"Режим «сначала окно»\",\"Toggle thumbnails\":\"Переключить миниатюры\",\"Auto-detecting…\":\"Автоматическое обнаружение…\",\"No rooms in this group\":\"В этой группе нет номеров\",\"Add a username at the top, switch group, or clear filters\":\"Добавьте имя пользователя вверху, переключите группу или очистите фильтры.\",\"Open workstation\":\"Открытая рабочая станция\",\"Open here (replace page)\":\"Открыть здесь (заменить страницу)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Заменить эту страницу рабочей станцией? Вернуться назад можно через браузер.\",\"View saved list\":\"Посмотреть сохраненный список\",\"Collapse dock\":\"Свернуть док-станцию\",\"No room detected\":\"Комната не обнаружена\",\"Open here\":\"Открыть здесь\",\"Add room\":\"Добавить комнату\",\"Remove room\":\"Удалить комнату\",\"Record in workstation\":\"Запись на рабочем месте\",\"Screenshot video\":\"Скриншот видео\",\"Play / pause\":\"Воспроизведение / пауза\",\"Mute / unmute\":\"Отключить/включить звук\",\"No playable video found on this page\":\"На этой странице не найдено воспроизводимое видео\",\"Recording intent queued in workstation\":\"Намерение записи поставлено в очередь на рабочей станции\",\"Added\":\"Добавлено\",\"Already exists\":\"Уже существует\",\"Failed\":\"Не удалось\",\"Add to workstation\":\"Добавить на рабочую станцию\",\"Already in workstation — click to remove\":\"Уже на рабочей станции — нажмите, чтобы удалить\",\"Model online\":\"Модель онлайн\",\"Model offline\":\"Модель офлайн\",\"Show fewer\":\"Показать меньше\",\"No measurable audio at the moment\":\"На данный момент нет измеримого звука\",\"Playback paused\":\"Воспроизведение приостановлено\",\"Audio measurement unavailable or blocked\":\"Измерение звука недоступно или заблокировано\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Разрешение на уведомление отклонено. Флеш-карта все еще работает.\\n\\nВы можете включить его в настройках браузера.\",\"Batch add\":\"Пакетное добавление\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Вставьте имена пользователей по одному в строке или через пробелы/запятые:\",\"Cancel\":\"Отмена\",\"Change the language of RoomGrid and the website\":\"Измените язык RoomGrid и веб-сайта.\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Измените язык RoomGrid. Chaturbate остается на текущем языке веб-сайта, поскольку официальный язык сайта недоступен.\",\"About\":\"О\",\"Export config\":\"Экспортировать конфигурацию\",\"Export usernames.txt\":\"Экспортировать имена пользователей.txt\",\"Copy usernames\":\"Копировать имена пользователей\",\"Unmute all\":\"Включить все звуки\",\"⏸ Pause visible\":\"⏸ Видна пауза\",\"Resume visible\":\"Резюме видно\",\"Following\":\"Следование\",\"Sync followed models now\":\"Синхронизировать модели, на которых вы подписаны, сейчас\",\"✓ Auto-sync followed models\":\"✓ Автосинхронизация отслеживаемых моделей\",\"☐ Auto-sync followed models\":\"☐ Автосинхронизация отслеживаемых моделей\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Импортируйте модели Chaturbate, на которых вы сейчас отслеживаете, в постоянную группу «Подписчики». Синхронизируется только эта группа.\",\"Syncing followed models…\":\"Синхронизация отслеживаемых моделей…\",\"Automatic Following sync disabled\":\"Автоматическая синхронизация подписок отключена\",\"Automatic Following sync enabled\":\"Автоматическая синхронизация подписок включена\",\"Following sync needs an active Chaturbate login\":\"Для последующей синхронизации требуется активный вход в Chaturbate.\",\"Following sync failed safely; saved workspace data was not changed\":\"Последующая синхронизация благополучно завершилась неудачно; сохраненные данные рабочей области не были изменены\",\"Enable dark mode\":\"Включить темный режим\",\"Disable dark mode\":\"Отключить темный режим\",\"Status history\":\"История статусов\",\"Temporary URLs\":\"Временные URL-адреса\",\"Shortcut panel\":\"Панель ярлыков\",\"Toggle video fit\":\"Переключить подгонку видео\",\"Shortcuts / hints\":\"Ярлыки/подсказки\",\"Ranking\":\"Рейтинг\",\"Model details\":\"Детали модели\",\"My profile\":\"Мой профиль\",\"Loading local statistics…\":\"Загрузка местной статистики…\",\"No watch data yet. Statistics start with this version.\":\"Данных о часах пока нет. Статистика начинается с этой версии.\",\"Statistics are disabled.\":\"Статистика отключена.\",\"Enable statistics\":\"Включить статистику\",\"Disable statistics\":\"Отключить статистику\",\"Export statistics\":\"Экспортная статистика\",\"Delete all statistics\":\"Удалить всю статистику\",\"Delete this model data\":\"Удалить данные этой модели\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Удалить всю локально собранную статистику просмотра? Ваше рабочее пространство останется нетронутым.\",\"Tracking since\":\"Отслеживание с\",\"Models watched\":\"Модели смотрели\",\"Watch time\":\"Время просмотра\",\"Most watched tags\":\"Самые просматриваемые теги\",\"Most watched hours\":\"Самые просматриваемые часы\",\"Observed online\":\"Наблюдается онлайн\",\"Public bio\":\"Публичная биография\",\"Public links\":\"Публичные ссылки\",\"No public profile data cached yet.\":\"Данные общедоступного профиля пока не кэшированы.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Учитываются только видимые, активно играющие карты. Превью при наведении не учитываются.\",\"Statistics imported\":\"Статистика импортирована\",\"Statistics deleted\":\"Статистика удалена\",\"Statistics exported\":\"Статистика экспортирована\",\"Local observation only\":\"Только местное наблюдение\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Никакие личные сообщения, токены, платежи, пароли или внешнее отслеживание не собираются.\",\"Not enough observations for a time estimate\":\"Недостаточно наблюдений для оценки времени\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Наведите курсор на любую кнопку/элемент управления, чтобы увидеть подсказку. Общие ярлыки можно редактировать на панели ярлыков.\\n\\nПо умолчанию:\\n/ Фокус на вводе имени пользователя\\nр Обновить все\\ng Представление в виде сетки\\nf Режим фокусировки\\nAlt+P или Alt+C Режим очистки вкл./выкл.\\nAlt+Shift+C Центр записи\\nAlt+Shift+R Записать текущую страницу\\nEsc Выход из режима очистки/полноэкранного режима\\nПробел Пауза/возобновление главного экрана в режиме фокусировки\\n←/→ или [/ ] Переключение главного экрана в режиме фокусировки\\nДвойной щелчок по карточке в полноэкранном режиме\",\"Visible windows paused\":\"Видимые окна приостановлены\",\"Visible windows resumed\":\"Видимые окна возобновлены\",\"Import config\":\"Импортировать конфигурацию\",\"Repair saved data\":\"Восстановить сохраненные данные\",\"Saved data has been normalized.\":\"Сохраненные данные нормализованы.\",\"Clear all data\":\"Очистить все данные\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Это сотрет ВСЕ комнаты, группы и настройки. Продолжать?\",\"English\":\"английский\",\"Deutsch\":\"немецкий\",\"Author\":\"Автор\",\"License\":\"Лицензия\",\"If this script saves you time…\":\"Если этот сценарий сэкономит вам время…\",\"Tip in ETH\":\"Чаевые в ETH\",\"ETH address\":\"ETH-адрес\",\"Copy\":\"Копировать\",\"Live stream audio activity\":\"Аудиоактивность в прямом эфире\",\"Local watch\":\"Местные часы\",\"Logo background\":\"Фон логотипа\",\"Logo height\":\"Высота логотипа\",\"Logo width\":\"Ширина логотипа\",\"Loud\":\"Громко\",\"Measured even while output is muted\":\"Измеряется даже при отключенном звуке\",\"Middle-click or Ctrl+click opens a new tab\":\"Щелчок средней кнопкой мыши или Ctrl+щелчок открывает новую вкладку.\",\"Models you have visited locally and that are public again now.\":\"Модели, которые вы посетили локально и которые теперь снова общедоступны.\",\"New discoveries\":\"Новые открытия\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Новое в каталоге, недавно просмотренное Camclient или только что начавшееся вещание.\",\"No audio track\":\"Нет звуковой дорожки\",\"No filters active · all matching live models are shown\":\"Фильтры не активны · показаны все подходящие живые модели\",\"No recommendations match the active filters.\":\"Нет рекомендаций, соответствующих активным фильтрам.\",\"No recording data\":\"Нет данных записи\",\"NOT\":\"НЕ\",\"Accent gradient\":\"Акцентный градиент\",\"Active filters\":\"Активные фильтры\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Добавляйте свои изображения или воспользуйтесь кнопкой скриншота в плеере. Снимки экрана могут автоматически попадать в галерею и использоваться непосредственно в качестве обложки модели.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Все фильтры домашней страницы, настройки макета, предварительного просмотра и разбиения на страницы хранятся в стабильном хранилище настроек и сохраняются при обновлении сценариев.\",\"All live models\":\"Все живые модели\",\"All suggestions\":\"Все предложения\",\"Almost silent\":\"Почти бесшумно\",\"Always active\":\"Всегда активен\",\"AND\":\"И\",\"Automatic example\":\"Автоматический пример\",\"Automatic segments\":\"Автоматические сегменты\",\"Automatically loading the complete catalog…\":\"Автоматическая загрузка полного каталога…\",\"Available recording data was saved.\":\"Доступные данные записи были сохранены.\",\"Back online\":\"Вернуться в онлайн\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"На основе вашего локально сохраненного избранного, тегов и активности просмотров. Каждый выбор показывает свою причину.\",\"Black\":\"Черный\",\"Discover something new\":\"Откройте для себя что-то новое\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Четкие полосы рекомендаций помогут вам найти что-то новое вместо повторения каталога на главной странице.\",\"Do not enlarge\":\"Не увеличивать\",\"Enable meter\":\"Включить счетчик\",\"Fast starters\":\"Быстрые стартеры\",\"Fill and crop\":\"Заполнить и обрезать\",\"Filter suggestions\":\"Фильтровать предложения\",\"Fit completely\":\"Подходит полностью\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Для прозрачности используйте PNG, WebP или безопасный SVG и выберите «Прозрачный» в качестве фона логотипа. JPEG не имеет прозрачности. Stretch использует выбранную ширину и высоту независимо.\",\"For you\":\"Для тебя\",\"gallery\":\"галерея\",\"HD\":\"HD\",\"Inner spacing\":\"Внутреннее расстояние\",\"Inspiration, not repetition\":\"Вдохновение, а не повторение\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Он разрешает использование, изменение и распространение программного обеспечения. Уведомления об авторских правах и лицензии должны сохраняться, а программное обеспечение поставляется без гарантии. Camclient создан Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Живу недолго и уже привлекаю общественную активность.\",\"Brave exposed no recordable video track.\":\"Brave не предоставил записываемую видеодорожку.\",\"Brave or the video source blocked frame capture.\":\"Brave или видеоисточник заблокировали захват кадра.\",\"Browser exposed no audio track\":\"Браузер не отображает звуковую дорожку\",\"Camclient\":\"Камклиент\",\"Camclient as the real homepage\":\"Camclient как настоящая домашняя страница\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient автоматически присваивает эти прозрачные примеры общим тегам. Используйте кнопку изображения на любом теге, чтобы выбрать пример, загрузить собственный PNG/WebP/SVG или скрыть изображение.\",\"Choose a Camclient example or upload your own transparent image.\":\"Выберите пример Camclient или загрузите собственное прозрачное изображение.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Выберите PNG, JPEG, WebP или безопасный SVG размером менее 1,1 МБ.\",\"Choose artwork for tag\":\"Выберите иллюстрацию для тега\",\"Click to enable meter\":\"Нажмите, чтобы включить счетчик\",\"Close more actions\":\"Закрыть больше действий\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Цвета, типографика, интервалы, углы, тени и фон немедленно применяются к настройкам, рабочей области, Discover и библиотеке моделей.\",\"Continue loading the complete catalog\":\"Продолжить загрузку полного каталога\",\"Continuing recording\":\"Продолжаем запись\",\"current matches\":\"текущие матчи\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Настройте имя, марку, слоган и локальный логотип Camclient. Логотипы остаются в этом браузере.\",\"Reset all\":\"Сбросить все\",\"RoomGrid Model library\":\"Библиотека моделей RoomGrid\",\"RoomGrid My user\":\"RoomGrid Мой пользователь\",\"RoomGrid Statistics\":\"Статистика RoomGrid\",\"Save to model gallery\":\"Сохранить в галерею моделей\",\"Saved segment\":\"Сохраненный сегмент\",\"Screenshot\":\"Скриншот\",\"Screenshot failed\":\"Снимок экрана не выполнен.\",\"Screenshot format\":\"Формат скриншота\",\"Screenshot resolution\":\"Разрешение скриншота\",\"Screenshot unavailable\":\"Скриншот недоступен\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Скриншоты загружаются и также могут быть добавлены в локальную галерею моделей.\",\"segment(s)\":\"сегмент(ы)\",\"Show no image\":\"Не показывать изображение\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Показывает активность аудиодорожки потока независимо от того, отключен ли звук на выходе проигрывателя.\",\"Source ended\":\"Источник закончился\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Эти настройки управляют макетом домашней страницы Camclient, нумерацией страниц, информацией о модели, фильтрами и производительностью.\",\"Transparent\":\"Прозрачный\",\"Unified main navigation\":\"Единая основная навигация\",\"Unvisited\":\"Непосещенный\",\"Upload own image\":\"Загрузить собственное изображение\",\"Use as model cover\":\"Использовать в качестве обложки модели\",\"Use the complete search, filters, and sorting for every current result.\":\"Используйте полный поиск, фильтры и сортировку для каждого текущего результата.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"При посещении домашней страницы Chaturbate всегда открывается полная домашняя страница Camclient. Каждая основная область остается внутри одного и того же пользовательского интерфейса.\",\"Waiting for audio\":\"Ожидание звука\",\"What does the MIT license mean?\":\"Что означает лицензия MIT?\",\"White\":\"Белый\",\"Your discovery mix\":\"Ваш микс открытий\",\"中文\":\"中文\",\"Only new\":\"Только новый\",\"Open Camclient homepage\":\"Открыть домашнюю страницу Camclient\",\"OR\":\"ИЛИ\",\"part\":\"часть\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Персональный выбор, новые открытия, популярные концертные залы и модели, которые вы никогда не посещали, — все это четко отделено от полного каталога на главной странице.\",\"Place\":\"Место\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP и безопасный SVG поддерживают прозрачность. Размер изображений изменяется локально до 144 × 144 пикселей и сохраняется с вашими настройками.\",\"Popular now\":\"Популярно сейчас\",\"Public live models with no local watch time in Camclient.\":\"Публичные живые модели без местного времени просмотра в Camclient.\",\"Quiet\":\"Тихий\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Ранжируется по текущему количеству зрителей; данные каталога не хранятся постоянно.\",\"Recording is still active.\":\"Запись все еще активна.\",\"Recording quality\":\"Качество записи\",\"Recording stopped\":\"Запись остановлена\",\"Recording unavailable\":\"Запись недоступна\",\"Remix\":\"Ремикс\",\"Source resolution\":\"Исходное разрешение\",\"Start recording\":\"Начать запись\",\"Starter pick · popular now\":\"Стартовый выбор · сейчас популярен\",\"Stop and save\":\"Остановись и сохрани\",\"Stretch\":\"Растянуть\",\"Tag artwork and emoji\":\"Отмечайте изображения и смайлы\",\"Tag artwork editor\":\"Редактор тегов\",\"The compressed image is still too large.\":\"Сжатое изображение все еще слишком велико.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"В заголовке отображаются только «Домой», «Обнаружение», «Обозреватель тегов», «Центр статистики», «Библиотека моделей», «Мой пользователь» и «Настройки». Каждая область конфигурации доступна в виде подстраницы настроек.\",\"The image could not be decoded.\":\"Изображение не удалось декодировать.\",\"The image could not be read.\":\"Изображение невозможно прочитать.\",\"The image format is unsafe or unsupported.\":\"Формат изображения небезопасен или не поддерживается.\",\"The lanes respect your active search and filter settings.\":\"Полосы учитывают ваш активный поиск и настройки фильтра.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"Запись сохраняется только на этом устройстве. Продолжайте, только если у вас есть разрешение на сохранение этого контента.\",\"The video has no usable frame yet.\":\"В видео пока нет пригодного кадра.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Данных о местных предпочтениях пока недостаточно, поэтому это прозрачные стартовые выборы, основанные на текущей популярности.\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Откройте для себя публичные трансляции\",\"all groups\":\"все группы\",\"at least one group\":\"хотя бы одна группа\",\"no measurements yet\":\"измерений пока нет\",\"Country, region or location\":\"Страна, регион или место\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Управляйте порядком моделей, фильтрами, количеством на странице, размером карточек и видимой информацией прямо на исходной главной странице Chaturbate. Все изменения применяются сразу и сохраняются в этом браузере.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] назад\",\"[[ROOMGRID_VALUE_0]] preview\":\"Предпросмотр [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"Профиль @[[ROOMGRID_VALUE_0]] сохранен\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Подробности модели для @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"Импортировано отслеживаемых моделей: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"Распознано элементов My Collection: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"Видно моделей: [[ROOMGRID_VALUE_0]] из [[ROOMGRID_VALUE_1]] загруженных.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Активный фильтр групп: [[ROOMGRID_VALUE_0]] · подходит карточек: [[ROOMGRID_VALUE_1]] из [[ROOMGRID_VALUE_2]] ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Специальная обложка · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Локально сохраненная обложка · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Примеры изображений и обложек ([[ROOMGRID_VALUE_0]] сохранены)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Постоянно хранится на этом устройстве · последняя версия [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Собственное изображение · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Сохранено [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Впервые замечено: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Последнее наблюдение: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Последний эфир: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Появления: [[ROOMGRID_VALUE_0]]× · первое [[ROOMGRID_VALUE_1]] · последнее [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Обычное время: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Технические данные: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"Локальных наблюдений эфира: [[ROOMGRID_VALUE_0]], не чаще одного каждые 5 минут\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] от прежнего времени просмотра как осторожная оценка\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Удалить тег [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Управление своими группами · назначено: [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Удалить группу «[[ROOMGRID_VALUE_0]]»? Модели останутся в библиотеке.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Группа «[[ROOMGRID_VALUE_0]]» удалена.\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"Групп: [[ROOMGRID_VALUE_0]] · можно выбрать несколько\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"Сохранено тегов: [[ROOMGRID_VALUE_0]]\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Удалить фильтр [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] совпадений\",\"[[ROOMGRID_VALUE_0]] filters active\":\"Фильтры [[ROOMGRID_VALUE_0]] активны\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Публичные данные профиля можно загрузить через «Все сведения».\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Личная организация с: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Автоматически в каталоге с: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Последняя активность: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Появления: [[ROOMGRID_VALUE_0]]× · последнее записано [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Обнаружено: [[ROOMGRID_VALUE_0]] · измерений качества: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Данные профиля: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] в архиве\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] восстановлено\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Страница [[ROOMGRID_VALUE_0]] из [[ROOMGRID_VALUE_1]] · моделей: [[ROOMGRID_VALUE_2]]\",\"[[ROOMGRID_VALUE_0]] saved\":\"Сохранено: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] watched\":\"Просмотрено: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"Активных: [[ROOMGRID_VALUE_0]] · пополняется автоматически\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] моделей · [[ROOMGRID_VALUE_1]] сеансов\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] избранное\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] отдельные модели\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] превью\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Предварительный просмотр/быстрый просмотр моделей [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] дн.\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Серия: [[ROOMGRID_VALUE_0]] дн.\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"Повторно просмотрено: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] дн. местных данных\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"средневзвешенное значение [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"Сейчас в эфире: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"Распознано моделей: [[ROOMGRID_VALUE_0]]\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Не все данные удалось прочитать: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Самый активный день: [[ROOMGRID_VALUE_0]], событий: [[ROOMGRID_VALUE_1]]. Хранение исходных событий: [[ROOMGRID_VALUE_2]] дн.; более старые значения могут оставаться в суточных итогах.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"Сеансов: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"Сохранено локально: [[ROOMGRID_VALUE_0]]\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Не удалось загрузить локальные данные: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] в сети.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] не в сети.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] давно не был(а) замечен(а) в сети.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"Избранных одновременно в сети: [[ROOMGRID_VALUE_0]].\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Публичный профиль [[ROOMGRID_VALUE_0]] изменился.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"Обновлено [[ROOMGRID_VALUE_0]] моделей\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Предпросмотр импорта статистики (пока без изменений):\\nМоделей: [[ROOMGRID_VALUE_0]]\\nСобытий: [[ROOMGRID_VALUE_1]]\\nПубличных профилей: [[ROOMGRID_VALUE_2]]\\n\\nПродолжение заменит существующую статистику.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Предпросмотр трансляции [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] добавлен(а) в MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Удалить [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Импортировать представления ([[ROOMGRID_VALUE_0]]) и ленты ([[ROOMGRID_VALUE_1]])?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"Комнат добавлено в рабочую область: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] models saved\":\"Сохранено моделей: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matching results\":\"Подходящих результатов: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] соответствие живым моделям\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Совпадает №[[ROOMGRID_VALUE_0]] с избранным\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"Добавлено [[ROOMGRID_VALUE_0]] изображений; [[ROOMGRID_VALUE_1]] не удалось обработать.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"Добавлено [[ROOMGRID_VALUE_0]] изображений.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"Комнат: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"Допустимых имён пользователей: [[ROOMGRID_VALUE_0]]\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Страница [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], комнат: [[ROOMGRID_VALUE_2]]\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"Временные URL: добавлено — [[ROOMGRID_VALUE_0]], ошибок — [[ROOMGRID_VALUE_1]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Предварительный просмотр статистики: модели [[ROOMGRID_VALUE_0]] и события [[ROOMGRID_VALUE_1]]. Продолжая безопасно объединять данные и сначала создавать резервную копию.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Пользовательские макеты поддерживают до [[ROOMGRID_VALUE_0]] комнат.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Сцена «[[ROOMGRID_VALUE_0]]» сохранена.\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Сцена «[[ROOMGRID_VALUE_0]]» загружена.\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] активных трансляций · [[ROOMGRID_VALUE_1]] нестабильно · [[ROOMGRID_VALUE_2]] для просмотра\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] потоков · назначено [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Удалить сцену «[[ROOMGRID_VALUE_0]]»?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Сохраняется только для [[ROOMGRID_VALUE_0]]: до [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Эта камера использует глобальное/адаптивное качество[[ROOMGRID_VALUE_0]].\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] заменён на [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Воспроизведение звука только с [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Размещено в позиции фокуса [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Размещено в окне сетки [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Разрешение [[ROOMGRID_VALUE_0]], состояние [[ROOMGRID_VALUE_1]], буфер [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Состояние потока [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"Моделей: [[ROOMGRID_VALUE_0]]\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Удалить фильтр: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Соответствует вашим интересам: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Каталог [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] получен · [[ROOMGRID_VALUE_2]] живые модели\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] локальные модели\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Каталог [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] живых моделей\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"Каталог номеров [[ROOMGRID_VALUE_0]] загружен\",\"Detected via: [[ROOMGRID_VALUE_0]]\":\"Обнаружено через: [[ROOMGRID_VALUE_0]]\",\"Sources: [[ROOMGRID_VALUE_0]]\":\"Источники: [[ROOMGRID_VALUE_0]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing replaces the current statistics.\":\"Предпросмотр импорта: моделей — [[ROOMGRID_VALUE_0]], событий — [[ROOMGRID_VALUE_1]]. Продолжение заменит текущую статистику.\",\"[[ROOMGRID_VALUE_0]] ms startup · [[ROOMGRID_VALUE_1]] stalls · [[ROOMGRID_VALUE_2]]% dropped frames\":\"запуск [[ROOMGRID_VALUE_0]] мс · прерываний: [[ROOMGRID_VALUE_1]] · потеря кадров: [[ROOMGRID_VALUE_2]] %\"},\"visible\":{\"‹ Zurück\":\"‹ Назад\",\"● 1.2k · #tag\":\"● 1,2 тыс. · #tag\",\"1 Jahr\":\"1 год\",\"30 Tage\":\"30 дней\",\"7 Tage\":\"7 дней\",\"90 Tage\":\"90 дней\",\"AA ×\":\"АА ×\",\"AA ✓\":\"АА ✓\",\"Ähnliche Models anzeigen\":\"Показать похожие модели\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Все изменения сохраняются локально и также применяются к следующей модели.\",\"Alle eigenen Tags\":\"Все пользовательские теги\",\"Alle Gruppen\":\"Все группы\",\"Alt+M / Alt+A\":\"Альт+М / Альт+А\",\"Ältere Chaturbate-Daten anfordern\":\"Запросить старые данные Chaturbate\",\"Automatisch abspielen\":\"Автозапуск\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Автоматически захватывается · полностью организуется при редактировании\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"Текущее соединение активно; После следующего измерения здесь появится местный курс.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Это представление библиотеки все еще пусто.\",\"Eigene Priorität\":\"Собственный приоритет\",\"Eigene Tags\":\"Собственные теги\",\"Eigene Tags filtern\":\"Фильтруйте свои собственные теги\",\"Eigenen Tag eingeben …\":\"Введите свой день...\",\"Einstellungen\":\"Настройки\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Ввод или «Добавить тег» возьмет на себя черновик. Он сохраняется навсегда только с помощью «Сохранить профиль». Уже использованные теги предлагаются по мере ввода.\",\"Ereignisse im Zeitraum\":\"События за период\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Создавайте коллекции со свободными именами, назначайте модели нескольким группам и отображайте здесь одну или несколько групп вместе.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Сначала создайте свою собственную группу выше.\",\"Follower\":\"Последователи\",\"GAMING\":\"ИГРОВОЙ\",\"Gesamter Verlauf\":\"Весь курс\",\"Grid +\":\"Сетка+\",\"Größe der Bedienelemente\":\"Размер элементов управления\",\"Model ist in allen gewählten Gruppen\":\"Модель есть во всех выбранных группах\",\"Model ist in mindestens einer gewählten Gruppe\":\"Модель находится хотя бы в одной выбранной группе.\",\"MultiCam\":\"Мультикам\",\"MultiCam +\":\"МультиКам+\",\"My Collection öffnen\":\"Открыть мою коллекцию\",\"Namen speichern\":\"Сохранить имя\",\"Nicht verbunden\":\"Не подключено\",\"Niedrige Live-Latenz\":\"Низкая задержка в реальном времени\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"До сих пор нет достаточных значений локального использования.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Своей группы пока нет. Введите имя выше и создайте свою первую коллекцию.\",\"Noch keine Model-Sehzeit erfasst.\":\"Время просмотра модели пока не зафиксировано.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Официальных снимков аккаунта пока нет.\",\"Noch nicht eingelesen\":\"Еще не читал\",\"Nur auf diesem Gerät\":\"Только на этом устройстве\",\"Öffentliche/erkannte Tags\":\"Публичные/признанные теги\",\"Öffentliches Profil anzeigen\":\"Посмотреть общедоступный профиль\",\"Offizielle Statistik verbinden\":\"Подключите официальную статистику\",\"Optionale offizielle Kontostatistik\":\"Дополнительная официальная статистика аккаунта\",\"Persönlich organisiert\":\"Лично организовано\",\"PiP\":\"Пип\",\"Player & Verbindung\":\"Плеер и подключение\",\"Player neu verbinden\":\"Переподключить плеер\",\"Player-Ecken\":\"Углы игрока\",\"Player-Hintergrund\":\"История игрока\",\"Player-Höhe\":\"Высота игрока\",\"Player-Seitenverhältnis\":\"Соотношение сторон плеера\",\"Position der Informationen\":\"Расположение информации\",\"Private Notiz\":\"Личная заметка\",\"Private Notizen anzeigen\":\"Просмотр личных заметок\",\"Puffergröße\":\"Размер буфера\",\"RG\":\"РГ\",\"RoomGrid\":\"RoomGrid\",\"Verbunden\":\"Подключено\",\"Vollständige Modelstatistik\":\"Полная статистика модели\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Прочитайте данные существующего аккаунта прямо сейчас\",\"Weiter ›\":\"Далее ›\",\"Zufriedenheit\":\"Удовлетворение\",\"Gruppe löschen\":\"Удалить группу\",\"Gruppen umbenennen oder löschen\":\"Переименуйте или удалите группы\",\"HD + Gaming\":\"HD+ игры\",\"IndexedDB\":\"ИндекседБД\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Каждый сохраненный тег автоматически доступен в поиске, в автозаполнении и здесь в виде прямого множественного фильтра.\",\"Keine Models passen zu diesen Filtern.\":\"Ни одна модель не соответствует этим фильтрам.\",\"Keine Priorität\":\"Не приоритет\",\"Konto-Daten werden eingelesen …\":\"Детали счета считываются в...\",\"Lautstärke pro Model merken\":\"Обратите внимание на объем каждой модели\",\"Layout & sichtbare Bereiche\":\"Планировка и видимые области\",\"Letzte Messung\":\"Последнее измерение\",\"LIVE\":\"ЖИТЬ\",\"lokal · für Suche und Filter\":\"локальный · для поиска и фильтров\",\"Maximale Stream-Lautstärke\":\"Максимальная громкость потока\",\"Model besitzt alle gewählten Tags\":\"Модель владеет всеми выбранными тегами\",\"Model besitzt mindestens einen gewählten Tag\":\"В модели есть хотя бы один выбранный тег.\",\"Automatisch neu verbinden\":\"Автоматическое повторное подключение\",\"Bei verstecktem Tab pausieren\":\"Пауза, пока вкладка скрыта\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Применить существующие данные учетной записи Chaturbate\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Оптимизация изображения · большой масштаб, положение, фильтр и вращение\",\"Bildanpassung\":\"Настройка изображения\",\"Bildwerkzeuge einklappen\":\"Инструменты свернуть изображения\",\"Breite des Informationsbereichs\":\"Ширина информационной области\",\"Broadcaster-Verbindung verwalten\":\"Управление подключением вещательной компании\",\"Chat anzeigen\":\"Показать чат\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+К\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Это означает, что вы не ограничены данными с момента установки сценария: RoomGrid может читать существующий список подписчиков и четко узнаваемые записи из «Моей коллекции». Никакие сообщения, платежи, медиафайлы или пароли не копируются.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio также содержит сравнения периодов времени, тепловые карты, воронки обнаружения, анализ рабочего пространства и качества, а также экспорт данных.\",\"Datenschutz & Export\":\"Защита и экспорт данных\",\"Deine Aktivität der letzten 14 Tage\":\"Ваша активность за последние 14 дней\",\"Deine meistgesehenen Models\":\"Ваши самые просматриваемые модели\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid не может угадать персональные значения учетной записи Chaturbate. Эти карточки появляются только в том случае, если вы сознательно связали официальную статистику вещательной компании с вашим собственным именем пользователя и токеном.\",\"RoomGrid Suite\":\"Люкс RoomGrid\",\"Seitengröße\":\"Размер страницы\",\"SingleCam\":\"Одиночная камера\",\"SingleCam einstellen\":\"Настроить SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Восстановить настройки SingleCam по умолчанию\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Как только вы сохраните собственный тег для модели, он появится здесь и в автозаполнении.\",\"Status im Player anzeigen\":\"Показать статус в плеере\",\"Status-Aktualisierung\":\"Обновление статуса\",\"Stream immer stumm starten\":\"Всегда запускайте трансляцию без звука\",\"Tag hinzufügen\":\"Добавить тег\",\"Tipps letzte Stunde\":\"Советы за последний час\",\"Token-Guthaben\":\"Баланс токена\",\"Ton & Ruhe\":\"Звук и спокойствие\",\"Ungefähre Onlinezeiten\":\"Примерное время онлайн\",\"Gruppen\":\"Группы\",\"Schließen\":\"Закрыть\",\"Startseite nach eigenen Gruppen filtern\":\"Фильтровать главную страницу по своим группам\",\"RoomGrid Komfortzentrum\":\"Центр управления RoomGrid\",\"Mein Nutzer\":\"Мой профиль\",\"Mein Nutzer-Dashboard\":\"Панель моего профиля\",\"Modellbibliothek\":\"Библиотека моделей\",\"Große Modellbibliothek\":\"Большая библиотека моделей\",\"Statistikzentrum\":\"Центр статистики\",\"Sitzungen\":\"Сеансы\",\"Dock + Hover\":\"Панель + наведение\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Локально и конфиденциально на этом устройстве\",\"Sortierung\":\"Сортировка\",\"Meiste Sehzeit\":\"Больше всего просмотров\",\"Zuletzt aktiv\":\"Недавняя активность\",\"Meiste Sitzungen\":\"Больше всего сеансов\",\"Meiste Zuschauer\":\"Больше всего зрителей\",\"Status\":\"Статус\",\"Alle Status\":\"Все статусы\",\"Nur live\":\"Только в эфире\",\"Nur Live\":\"Только в эфире\",\"Offline\":\"Не в сети\",\"Unbekannt\":\"Неизвестно\",\"Nur Bibliothek\":\"Только библиотека\",\"Pro Seite\":\"На странице\"}}","ja":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"言語スイッチを安全に準備できませんでした。このページは残されていませんでした。\",\"Language\":\"言語\",\"Default workspace\":\"デフォルトのワークスペース\",\"Workspace\":\"ワークスペース\",\"New workspace\":\"新しいワークスペース\",\"copy\":\"コピー\",\"New group\":\"新しいグループ\",\"Saved model\":\"保存されたモデル\",\"Search models, pages, workspaces and commands…\":\"モデル、ページ、ワークスペース、コマンドを検索…\",\"Type to filter · Enter to run · Esc to close\":\"フィルタするには入力します · 実行するには Enter · 閉じるには Esc\",\"No matches\":\"一致しません\",\"Currently active RoomGrid version\":\"現在アクティブな RoomGrid バージョン\",\"years\":\"歳\",\"Live\":\"ライブ\",\"Private\":\"プライベート\",\"Away\":\"離席中\",\"Offline\":\"オフライン\",\"sessions\":\"セッション\",\"NEW\":\"新しい\",\"Name, subject, tag or place\":\"名前、テーマ、タグ、場所\",\"Website order\":\"サイトの順序\",\"Most viewers\":\"視聴者数が多い順\",\"Fewest viewers\":\"視聴者数が少ない順\",\"Longest online\":\"オンライン時間が長い順\",\"Recently online\":\"最近オンライン\",\"Age ascending\":\"年齢の低い順\",\"Age descending\":\"年齢の高い順\",\"New models first\":\"新しいモデルを優先\",\"Saved first\":\"保存済みを優先\",\"Favorites first\":\"お気に入りを優先\",\"Most watched\":\"最も視聴されている\",\"Recently watched\":\"最近見た順\",\"Random mix\":\"ランダム表示\",\"All loaded\":\"読み込んだすべて\",\"Previous page\":\"前のページへ\",\"Next page\":\"次のページ\",\"Reload complete live list\":\"完全なライブリストをリロードする\",\"All homepage options\":\"ホームページの全設定\",\"Reset filters\":\"フィルターをリセットする\",\"Quick filter\":\"クイックフィルター\",\"Sort\":\"並べ替え\",\"Models per page\":\"1ページあたりのモデル数\",\"Load more results\":\"さらに結果を読み込む\",\"still to display\":\"まだ表示中\",\"continues automatically while scrolling\":\"スクロール中は自動的に続きます\",\"visible\":\"表示中\",\"loaded\":\"読込済み\",\"page\":\"ページ\",\"active filters\":\"有効なフィルター\",\"loading complete live list\":\"完全なライブリストをロードしています\",\"all matching live models found\":\"一致するライブモデルがすべて見つかりました\",\"live list incomplete, retry\":\"ライブリストが不完全です、再試行してください\",\"Requests temporarily paused\":\"リクエストは一時停止されています\",\"Vault could not be unlocked\":\"金庫のロックを解除できませんでした\",\"Preview Dock\":\"プレビュードック\",\"Connecting…\":\"接続中…\",\"Stream temporarily unavailable\":\"ストリームが一時的に利用できなくなりました\",\"Error\":\"エラー\",\"Buffering\":\"バッファリング\",\"Preview unavailable\":\"プレビューは利用できません\",\"Connect\":\"接続する\",\"Close\":\"閉じる\",\"Opacity\":\"不透明度\",\"Info\":\"情報\",\"Age\":\"年齢\",\"Gender\":\"性別\",\"Location\":\"場所\",\"Country\":\"国\",\"Last broadcast\":\"前回の放送\",\"Fan club\":\"ファンクラブ\",\"Private show\":\"プライベートショー\",\"Spy show\":\"スパイショー\",\"Model comparison list\":\"機種比較表\",\"Compare\":\"比較する\",\"Open model details\":\"モデルの詳細を開く\",\"Compare now\":\"今すぐ比較してください\",\"Clear\":\"クリア\",\"Up to four models can be compared at once.\":\"最大 4 つのモデルを一度に比較できます。\",\"Remove from comparison\":\"比較から削除\",\"Add to comparison\":\"比較に追加\",\"Total watched\":\"総視聴数\",\"Sessions\":\"セッション\",\"Viewers\":\"視聴者\",\"Rating\":\"評価\",\"Online samples\":\"オンラインサンプル\",\"Loading quality data…\":\"高品質のデータをロードしています…\",\"samples\":\"サンプル\",\"stalls\":\"屋台\",\"No quality measurements yet\":\"品質測定はまだ行われていません\",\"Quality data unavailable\":\"高品質のデータが利用できない\",\"Details\":\"詳細\",\"Remove\":\"削除\",\"Model comparison\":\"機種比較\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"すべての値は、公開情報、独自の組織、またはこのデバイスに関するローカルな観察から得られます。欠損値が推測されることはありません。\",\"Shared tags\":\"共有タグ\",\"Loading public facts…\":\"公開事実を読み込み中…\",\"Unknown status\":\"不明なステータス\",\"Last activity\":\"最後のアクティビティ\",\"These statistics describe only your own local RoomGrid usage.\":\"これらの統計は、ユーザー自身のローカル RoomGrid の使用状況のみを示します。\",\"At a glance\":\"一目でわかる\",\"Type\":\"種類\",\"Appearances\":\"出演\",\"Public facts\":\"公的な事実\",\"No public biography available yet.\":\"まだ公開されている経歴はありません。\",\"Public link\":\"パブリックリンク\",\"Public profile source\":\"公開プロフィールのソース\",\"Profile updated\":\"プロフィールを更新しました\",\"Private note (local only)\":\"プライベートメモ (ローカルのみ)\",\"Your tags\":\"あなたのタグ\",\"No rating\":\"評価なし\",\"No priority\":\"優先順位なし\",\"Private library data\":\"私設図書館のデータ\",\"Private notes\":\"個人的なメモ\",\"Priority\":\"優先順位\",\"Favorite\":\"お気に入り\",\"Watchlist\":\"ウォッチリスト\",\"Archived\":\"アーカイブ済み\",\"Watch again\":\"もう一度見る\",\"Technical note\":\"テクニカルノート\",\"Technical\":\"技術的な\",\"Save all changes\":\"すべての変更を保存する\",\"Loading local activity…\":\"ローカル アクティビティを読み込んでいます…\",\"Activity and playback quality\":\"アクティビティと再生の品質\",\"Sun\":\"太陽\",\"Mon\":\"月\",\"Tue\":\"火\",\"Wed\":\"水\",\"Thu\":\"木\",\"Fri\":\"金\",\"Sat\":\"土\",\"Quality samples\":\"高品質のサンプル\",\"Average startup\":\"平均的な起動\",\"Stalls\":\"屋台\",\"Reconnects\":\"再接続します\",\"Dropped frames\":\"ドロップされたフレーム\",\"Online observations\":\"オンライン観察\",\"Frequently observed online times\":\"よく観察されるオンライン時間\",\"Not enough online observations yet.\":\"オンラインでの観察はまだ十分ではありません。\",\"Watched\":\"見た\",\"Preview\":\"プレビュー\",\"Quick view\":\"クイックビュー\",\"Discovered\":\"発見されました\",\"Quality\":\"品質\",\"Connection\":\"接続\",\"No local activity recorded yet.\":\"ローカルアクティビティはまだ記録されていません。\",\"Recent events\":\"最近の出来事\",\"Activity data unavailable.\":\"活動データは利用できません。\",\"Similar models\":\"類似モデル\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"ローカル/パブリック タグ、国、タイプから計算されます。すべての提案にはその理由が示されており、データはアップロードされません。\",\"shared tags\":\"共有タグ\",\"same country\":\"同じ国\",\"same type\":\"同じタイプ\",\"Not enough matching data yet.\":\"マッチングデータがまだ十分ではありません。\",\"Overview\":\"概要\",\"Profile\":\"プロフィール\",\"Organize\":\"整理する\",\"Activity\":\"アクティビティ\",\"Similar\":\"同様の\",\"Model detail sections\":\"モデルの詳細セクション\",\"Previous model\":\"従来モデル\",\"Next model\":\"次期モデル\",\"Open room\":\"オープンルーム\",\"Pin preview\":\"ピンプレビュー\",\"No public fields available\":\"利用可能なパブリックフィールドはありません\",\"Public facts currently unavailable\":\"現在公開されている事実は入手できません\",\"Session chat search\":\"セッションチャット検索\",\"Search this session only…\":\"このセッションのみを検索…\",\"New messages\":\"新しいメッセージ\",\"Time for a short break.\":\"ちょっとした休憩の時間です。\",\"Your RoomGrid time budget for today has been reached.\":\"今日の RoomGrid 時間の予算に達しました。\",\"RoomGrid action messages\":\"RoomGrid アクション メッセージ\",\"This change is saved automatically.\":\"この変更は自動的に保存されます。\",\"Undo\":\"元に戻す\",\"Change undone\":\"変更を元に戻す\",\"Undo was not applied because newer changes already exist.\":\"新しい変更がすでに存在するため、元に戻すは適用されませんでした。\",\"Chaturbate live catalog\":\"Chaturbateライブカタログ\",\"RoomGrid Discover\":\"ルームグリッドディスカバー\",\"Chaturbate Discover page\":\"Chaturbate の発見ページ\",\"Following list\":\"次のリスト\",\"My Collection\":\"私のコレクション\",\"Chaturbate page\":\"チャットページ\",\"Local record\":\"ローカルレコード\",\"Personally organized\":\"個人的に組織された\",\"Local watch history\":\"ローカルの視聴履歴\",\"Public profile\":\"公開プロフィール\",\"Local quality measurements\":\"ローカル品質測定\",\"Local model index\":\"ローカルモデルインデックス\",\"Current live signal\":\"現在のライブ信号\",\"Within the last hour\":\"過去 1 時間以内\",\"Within the last 24 hours\":\"過去 24 時間以内\",\"Within the last 30 days\":\"過去 30 日以内\",\"Historical record\":\"歴史的記録\",\"Time unknown\":\"時間不明\",\"All\":\"すべて\",\"Favorites\":\"お気に入り\",\"Library\":\"図書館\",\"My Collection contained no reliably recognizable entries\":\"私のコレクションには、確実に認識できるエントリが含まれていませんでした\",\"No group filter active · all models are shown.\":\"アクティブなグループ フィルターはありません。すべてのモデルが表示されます。\",\"Clear group filter\":\"グループフィルターをクリア\",\"Filter homepage by groups\":\"グループごとにホームページをフィルタリングする\",\"Close group menu\":\"グループメニューを閉じる\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"1 つ以上の独自のグループを選択します。他のモデルのカードは、Chaturbate ホームページ上ですぐに非表示になります。\",\"Show all\":\"すべて表示\",\"Models in this group\":\"このグループのモデル\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"カスタム グループはまだありません。モデル ライブラリにモデルを作成し、そこにモデルを割り当てます。\",\"In any group\":\"どのグループでも\",\"In all groups\":\"すべてのグループで\",\"Manage groups\":\"グループの管理\",\"Create group\":\"グループの作成\",\"Custom groups for the homepage\":\"ホームページのカスタム グループ\",\"Main views\":\"主な見解\",\"MultiCam workstation\":\"マルチカムワークステーション\",\"Camera grid, audio and layouts\":\"カメラグリッド、オーディオ、レイアウト\",\"Discover\":\"見つける\",\"Full catalog, filters and sorting\":\"完全なカタログ、フィルター、並べ替え\",\"Personal dashboard\":\"個人用ダッシュボード\",\"Favorites, activity and shortcuts\":\"お気に入り、アクティビティ、ショートカット\",\"Tag explorer\":\"タグエクスプローラー\",\"Combinations, trends and related tags\":\"組み合わせ、トレンド、関連タグ\",\"Library and statistics\":\"図書館と統計\",\"Statistics Center\":\"統計センター\",\"Model comparison and local statistics\":\"モデルの比較とローカル統計\",\"Model library\":\"モデルライブラリ\",\"Profiles, tags, groups and notes\":\"プロフィール、タグ、グループ、メモ\",\"My user\":\"マイプロフィール\",\"Preferences, usage and account data\":\"設定、使用状況、アカウントデータ\",\"Analytics Studio\":\"分析スタジオ\",\"Ranges, heatmaps and exports\":\"範囲、ヒートマップ、エクスポート\",\"Settings and maintenance\":\"設定とメンテナンス\",\"Customize homepage\":\"ホームページをカスタマイズ\",\"Cards, filters and visible details\":\"カード、フィルター、および目に見える詳細\",\"Appearance\":\"外観\",\"Colors, density and surfaces\":\"色、密度、表面\",\"Workspaces and groups\":\"ワークスペースとグループ\",\"Layouts, templates and assignments\":\"レイアウト、テンプレート、割り当て\",\"Data and backup\":\"データとバックアップ\",\"Import, export and recovery\":\"インポート、エクスポート、およびリカバリ\",\"Windows and performance\":\"ウィンドウとパフォーマンス\",\"Resource profiles and preview limits\":\"リソースプロファイルとプレビュー制限\",\"Privacy\":\"プライバシー\",\"History, private session and local data\":\"履歴、プライベートセッション、ローカルデータ\",\"Diagnostics\":\"診断\",\"Self-check, routes and recovery\":\"セルフチェック、ルートとリカバリ\",\"RoomGrid main menu\":\"ルームグリッドのメインメニュー\",\"All pages, settings and tools in one place\":\"すべてのページ、設定、ツールを 1 か所に\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"メニューは表示ウィンドウ内に表示されます。すべての RoomGrid データはローカルのままです。\",\"RoomGrid pages\":\"ルームグリッドページ\",\"Statistics\":\"統計\",\"Customize original homepage\":\"元のホームページをカスタマイズ\",\"Homepage\":\"ホームページ\",\"Filter homepage by your own groups\":\"独自のグループでホームページをフィルタリングする\",\"Groups\":\"グループ\",\"Activity trend\":\"活動傾向\",\"Link\":\"リンク\",\"Public profile text and links\":\"公開プロフィールのテキストとリンク\",\"Last known public preview\":\"既知の最後のパブリック プレビュー\",\"Current public preview\":\"現在のパブリック プレビュー\",\"Offline may only provide a placeholder\":\"オフラインではプレースホルダーのみが提供される場合があります\",\"Example images and covers\":\"画像とカバーの例\",\"Reading local image archive…\":\"ローカル画像アーカイブを読み取り中…\",\"Save new snapshot\":\"新しいスナップショットを保存する\",\"Saving public previews…\":\"パブリック プレビューを保存しています…\",\"No saved image yet; current public previews are still shown.\":\"画像はまだ保存されていません。現在のパブリック プレビューは引き続き表示されます。\",\"Current public cover\":\"現在の公開表紙\",\"Current compact preview\":\"現在のコンパクト プレビュー\",\"Current live image\":\"現在のライブ画像\",\"Room\":\"ルーム\",\"More\":\"もっと見る\",\"Unwatch\":\"ウォッチしない\",\"Data sources and freshness\":\"データソースと鮮度\",\"Data source\":\"データソース\",\"Full local data set\":\"完全なローカル データ セット\",\"Matching filters\":\"マッチングフィルター\",\"Shown on this page\":\"このページに表示されている\",\"Latest live signals\":\"最新のライブ信号\",\"With watch history\":\"視聴履歴あり\",\"Public-profile coverage\":\"公開プロフィールの報道\",\"Country coverage\":\"対象国\",\"Age coverage\":\"対象年齢\",\"Tag coverage\":\"タグの適用範囲\",\"Which data set is being searched?\":\"どのデータセットが検索されていますか?\",\"Last local observation\":\"最後の現地観察\",\"No observation time yet\":\"まだ観察時間はありません\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"パブリック = 現在の Chaturbate ページまたはライブ カタログから。個人 = あなたによって保存、タグ付け、またはグループ化されます。ローカル = このデバイスでのみ観察されるセッション、プレビュー、または品質値。未知のフィールドは未知のままであり、推測されることはありません。\",\"Local model statistics\":\"ローカルモデル統計\",\"Automatically recorded · personally organized\":\"自動記録・個人整理\",\"Automatically recorded in the growing model catalog\":\"成長するモデルカタログに自動的に記録される\",\"Total\":\"合計\",\"Avg. session\":\"平均セッション\",\"Active days\":\"アクティブ日数\",\"Share\":\"シェアする\",\"Longest session\":\"最長セッション\",\"Previews\":\"プレビュー\",\"Quick views\":\"クイック表示\",\"Data sources\":\"データソース\",\"Edit in library\":\"ライブラリで編集する\",\"No experience data yet\":\"まだ経験データがありません\",\"Online observations will be collected locally from now on.\":\"今後はオンライン観測を現地で収集する。\",\"Open\":\"開く\",\"Good trend\":\"良い傾向\",\"Moderate trend\":\"緩やかな傾向\",\"Initial trend\":\"初期傾向\",\"First observation\":\"最初の観察\",\"Estimated from watch time\":\"総再生時間から推定\",\"weekday open\":\"平日営業\",\"time open\":\"開いている時間\",\"Preview image\":\"プレビュー画像\",\"Biography / room subject\":\"プロフィール/ルームのテーマ\",\"Catalog state\":\"カタログ状態\",\"Live status\":\"ライブ状態\",\"Viewer count\":\"視聴者数\",\"Country/region\":\"国・地域\",\"Total watch time\":\"合計視聴時間\",\"Average session\":\"平均セッション\",\"First seen\":\"初回表示\",\"Last seen\":\"最終表示\",\"Last live\":\"最終ライブ\",\"Approximate online times\":\"おおよそのオンライン時間\",\"Activity, sources and profile state\":\"アクティビティ、情報源、プロフィール状態\",\"Example images\":\"サンプル画像\",\"Own groups\":\"自分のグループ\",\"Public/recognized tags\":\"公開/認識済みタグ\",\"Notes, own tags and priority\":\"メモ、自分のタグ、優先度\",\"Local rating\":\"ローカル評価\",\"Group assignment\":\"グループ割り当て\",\"Room and preview actions\":\"ルームとプレビューの操作\",\"Library display\":\"ライブラリ表示\",\"Saved in this browser\":\"このブラウザに保存\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"カードサイズ、1行と1ページあたりのモデル数、各カードに表示する詳細情報を選べます。\",\"Compact overview\":\"コンパクト表示\",\"Balanced\":\"バランス\",\"All details\":\"すべての詳細\",\"Show all details\":\"すべての詳細を表示\",\"Core details only\":\"基本情報のみ\",\"Card zoom\":\"カードの拡大率\",\"Visible information per model\":\"モデルごとの表示情報\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"自動列ではカードの拡大率によって横に並ぶモデル数が決まります。固定列数を選ぶとそちらが優先されます。設定はこのブラウザに保存されます。\",\"No custom tags yet\":\"カスタムタグはまだありません\",\"Name of the new group\":\"新しいグループの名前\",\"Models remain in the library.\":\"モデルはライブラリに残ります。\",\"Custom groups\":\"カスタムグループ\",\"Create new group\":\"新しいグループを作成する\",\"Nothing selected — show all\":\"何も選択されていません — すべて表示\",\"Any\":\"どれでも\",\"Women\":\"女性\",\"Men\":\"男性\",\"Couples\":\"カップル\",\"Unknown\":\"不明\",\"Error/loading\":\"エラー/読み込み中\",\"Every weekday\":\"平日毎日\",\"Sunday\":\"日曜日\",\"Monday\":\"月曜日\",\"Tuesday\":\"火曜日\",\"Wednesday\":\"水曜日\",\"Thursday\":\"木曜日\",\"Friday\":\"金曜日\",\"Saturday\":\"土曜日\",\"Every time\":\"毎回\",\"Advanced filters and sorting\":\"高度なフィルターと並べ替え\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"フィルターを好きなだけ組み合わせます。複数の性別、州、国、タグは複数選択として機能します。すべての設定はこのブラウザに保存されたままになります。\",\"Gender/type\":\"性別・タイプ\",\"Countries/regions (multiple)\":\"国・地域(複数)\",\"Add country…\":\"国を追加…\",\"Public tags (multiple)\":\"パブリックタグ(複数)\",\"Add tag…\":\"タグを追加…\",\"Public-tag matching\":\"パブリックタグマッチング\",\"At least one tag\":\"少なくとも 1 つのタグ\",\"Every selected tag\":\"選択したすべてのタグ\",\"Catalog source\":\"カタログソース\",\"All models\":\"全モデル\",\"Personally organized only\":\"個人的に組織したもののみ\",\"Automatically discovered only\":\"自動検出のみ\",\"Profile data\":\"プロフィールデータ\",\"Loaded or missing\":\"ロードされているか、見つからない\",\"Loaded only\":\"ロードのみ\",\"Missing only\":\"欠落のみ\",\"Picture/category\":\"写真/カテゴリー\",\"Neither HD nor gaming\":\"HDでもゲームでもない\",\"Watch history\":\"視聴履歴\",\"Never watched\":\"見たことない\",\"Own tags\":\"独自のタグ\",\"With or without\":\"有無にかかわらず\",\"With tags only\":\"タグのみ付き\",\"Without tags only\":\"タグなしのみ\",\"At least one group\":\"少なくとも 1 つのグループ\",\"No group\":\"グループなし\",\"With notes only\":\"メモのみで\",\"Without notes only\":\"メモなしのみ\",\"Numeric ranges and minimums\":\"数値範囲と最小値\",\"Minimum age\":\"最低年齢\",\"Maximum age\":\"最大年齢\",\"Minimum viewers\":\"最小視聴者数\",\"Maximum viewers\":\"最大視聴者数\",\"Minimum followers\":\"最小フォロワー数\",\"Maximum followers\":\"最大フォロワー数\",\"Minimum total watch time\":\"最小総視聴時間\",\"Currently online for at least\":\"現在少なくともオンラインです\",\"Minimum sessions\":\"最小セッション数\",\"Minimum rating\":\"最低評価\",\"Minimum priority\":\"最低優先度\",\"Active within\":\"内で活動中\",\"days\":\"日\",\"Minimum appearances\":\"最低出演回数\",\"Minimum live observations\":\"最小限のライブ観測\",\"Filter by typical online time\":\"一般的なオンライン時間でフィルタリングする\",\"Weekday\":\"平日\",\"Hour (local time)\":\"時 (現地時間)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"ゼロまたは空のフィールドは制限がないことを意味します。年齢制限が有効な場合、不明な年齢は除外されます。時間フィルターは、このデバイスで観察されたライブ時間を使用し、必要に応じて時間を監視するために慎重にフォールバックします。\",\"Reset all library filters\":\"すべてのライブラリフィルターをリセットします\",\"Open live room\":\"オープンなライブルーム\",\"Open room page\":\"ルームページを開く\",\"Status\":\"ステータス\",\"Open model room in a new tab\":\"新しいタブでモデルルームを開く\",\"Open the Chaturbate single-model view in a new tab\":\"新しいタブで Chaturbate の単一モデル ビューを開きます\",\"Save profile\":\"プロファイルの保存\",\"Restore\":\"復元\",\"Archive\":\"アーカイブ\",\"Advanced statistics filters\":\"高度な統計フィルター\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"すべてのフィルターは組み合わせることができます。国、タグ、グループ、タイプは複数選択をサポートしています。選択肢は永久に保存されます。\",\"Own groups (multiple)\":\"自身のグループ(複数)\",\"Add group…\":\"グループを追加…\",\"Tag matching\":\"タグマッチング\",\"At least one\":\"少なくとも 1 つ\",\"All selected tags\":\"選択されたすべてのタグ\",\"Group matching\":\"グループマッチング\",\"All selected groups\":\"選択されたすべてのグループ\",\"Cover/example image\":\"表紙/サンプル画像\",\"Known public image available\":\"既知のパブリックイメージが利用可能\",\"No known image\":\"知られていない画像\",\"Currently live\":\"現在ライブ中\",\"Maximum total watch time\":\"最大総視聴時間\",\"Maximum sessions\":\"最大セッション数\",\"Minimum active days\":\"最小アクティブ日数\",\"Minimum previews\":\"最小限のプレビュー\",\"Minimum quick views\":\"最小限のクイックビュー\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"ゼロまたは空のフィールドは制限がないことを意味します。未知の値は決して推測されません。年齢範囲には不明な年齢は含まれません。\",\"Reset all statistics filters\":\"すべての統計フィルターをリセットします\",\"Search name, tags or notes…\":\"名前、タグ、メモを検索…\",\"Search\":\"検索\",\"Sorting\":\"仕分け\",\"Most watch time\":\"最多視聴時間\",\"Least watch time\":\"最低総視聴時間\",\"Recently active\":\"最近活動中\",\"Longest inactive\":\"最長非アクティブ\",\"Most sessions\":\"ほとんどのセッション\",\"Fewest sessions\":\"最も少ないセッション\",\"Longest average session\":\"最長の平均セッション\",\"Shortest average session\":\"最短の平均セッション\",\"Longest single session\":\"最長の単一セッション\",\"Most active days\":\"最も活動的な日\",\"Highest watch share\":\"最高の視聴シェア\",\"Most previews\":\"ほとんどのプレビュー\",\"Most quick views\":\"最も簡単なビュー\",\"Most discoveries\":\"ほとんどの発見\",\"Most followers\":\"最も多くのフォロワー\",\"Youngest first\":\"最年少から順\",\"Oldest first\":\"古い順\",\"Highest rating\":\"最高評価\",\"Highest priority\":\"最高の優先度\",\"Most appearances\":\"最多出演\",\"Most live observations\":\"最も多くのライブ観測\",\"First observed\":\"最初に観察された\",\"Country A–Z\":\"A ~ Z の国\",\"Most public tags\":\"最も公開されているタグ\",\"Most groups\":\"ほとんどのグループ\",\"Best technical quality\":\"最高の技術品質\",\"Cover/example images first\":\"表紙/サンプル画像を最初に\",\"All statuses\":\"すべてのステータス\",\"Live only\":\"ライブのみ\",\"Library only\":\"図書館のみ\",\"View\":\"表示\",\"Active library\":\"アクティブなライブラリ\",\"Live now\":\"今を生きる\",\"With notes\":\"メモ付き\",\"Newest saved/discovered\":\"最新の保存/検出\",\"Oldest saved/discovered\":\"保存/検出された最も古いもの\",\"Lowest rating\":\"最低評価\",\"Lowest priority\":\"最も低い優先度\",\"Live longest now\":\"今が一番長生きする\",\"Age youngest first\":\"年齢は若い順\",\"Age oldest first\":\"年齢が古い順\",\"Most recently live\":\"最近のライブ\",\"First discovered\":\"最初に発見された\",\"Most own groups\":\"ほとんどの独自グループ\",\"Card size\":\"カードサイズ\",\"Compact\":\"コンパクト\",\"Comfortable\":\"快適\",\"Large\":\"大\",\"Cinema\":\"映画\",\"Extra large\":\"特大\",\"Models per row\":\"1行あたりのモデル数\",\"Automatic for window\":\"ウィンドウに合わせて自動\",\"Recorded models\":\"収録モデル\",\"Total cam time\":\"合計カム時間\",\"across all models\":\"すべてのモデルにわたって\",\"Dock + hover\":\"ドック+ホバー\",\"Last 30 days\":\"過去 30 日間\",\"local events\":\"地元のイベント\",\"Top model\":\"最上位モデル\",\"no watch time yet\":\"まだ視聴時間がありません\",\"Deeper analysis\":\"より深い分析\",\"Open Analytics Studio\":\"分析スタジオを開く\",\"Late night\":\"深夜\",\"Morning\":\"朝\",\"Daytime\":\"昼間\",\"Evening\":\"夕方\",\"Gaming\":\"ゲーム\",\"Music & DJ\":\"音楽とDJ\",\"Dance\":\"ダンス\",\"Cosplay & anime\":\"コスプレ&アニメ\",\"Fitness & sports\":\"フィットネス&スポーツ\",\"Talk & social\":\"トーク&ソーシャル\",\"Outdoor & travel\":\"アウトドア&トラベル\",\"Roleplay\":\"ロールプレイ\",\"Performance\":\"パフォーマンス\",\"My interest statistics\":\"興味・嗜好の統計\",\"All local data\":\"すべてのローカル データ\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"実際のローカル総再生時間、セッション、プレビュー、クイック ビュー、お気に入り、現在既知の公開モデル情報から計算されます。\",\"Favorite tag\":\"お気に入りタグ\",\"not enough data\":\"データが不十分です\",\"Favorite genre\":\"お気に入りジャンル\",\"from public tags\":\"パブリックタグから\",\"Most-watched age\":\"最も視聴した年齢\",\"no age data\":\"年齢データなし\",\"Most-watched country\":\"最も視聴した国\",\"no country data\":\"国のデータがない\",\"Favorite model type\":\"好みのモデルタイプ\",\"no type data\":\"型データがありません\",\"Favorite viewing time\":\"好みの視聴時間\",\"no time data\":\"時間データがありません\",\"Top-three concentration\":\"トップ3の集中力\",\"watch time on the top three models\":\"上位 3 モデルの総再生時間\",\"Favorite affinity\":\"お気に入りの相性\",\"watch time on current favorites\":\"現在のお気に入りの総再生時間\",\"watch time on HD models\":\"HD モデルの総再生時間\",\"Metadata coverage\":\"メタデータの範囲\",\"age\":\"年齢\",\"country\":\"国\",\"Tags, genres and browsing interests\":\"タグ、ジャンル、閲覧の興味\",\"Tags by watch time\":\"総再生時間ごとのタグ付け\",\"Your ranking appears when watched models have public tags.\":\"注目モデルに公開タグがある場合、ランキングが表示されます。\",\"Favorite genre worlds\":\"好きなジャンルの世界\",\"No genre world can yet be assigned from public tags.\":\"まだパブリック タグからジャンル ワールドを割り当てることはできません。\",\"Your own tags\":\"独自のタグ\",\"Your saved custom tags are also evaluated here by watch time.\":\"保存したカスタムタグもここで総再生時間によって評価されます。\",\"Preview-interest tags\":\"プレビュー関心タグ\",\"Browsing interests appear when you use live previews or quick views.\":\"ライブ プレビューまたはクイック ビューを使用すると、興味のある閲覧が表示されます。\",\"Most-previewed models\":\"最もプレビューされているモデル\",\"No previews have been recorded in this period yet.\":\"この期間にはまだプレビューが記録されていません。\",\"Age, origin and model type\":\"年齢、出身地、モデルタイプ\",\"Age groups by watch time\":\"総再生時間別の年齢層\",\"Not enough public age data is available.\":\"公開されている年齢データは十分ではありません。\",\"Countries by watch time\":\"総再生時間別の国\",\"Not enough public country data is available.\":\"十分な公開国データが入手できません。\",\"Most-watched places\":\"最も注目されている場所\",\"Not enough public location data is available.\":\"十分な公開位置データが利用できません。\",\"Model types by watch time\":\"総再生時間ごとのモデルの種類\",\"Not enough public model-type data is available.\":\"公開されているモデルタイプのデータが十分にありません。\",\"Viewing rhythm and favorite signals\":\"視聴リズムとお気に入り傾向\",\"Most active weekdays\":\"最もアクティブな平日\",\"Not enough visit events are available.\":\"訪問イベントが十分にありません。\",\"Times of day\":\"時間帯\",\"Tags among favorites\":\"お気に入りのタグ\",\"Preference signals appear when you mark models as favorites.\":\"モデルをお気に入りとしてマークすると、優先シグナルが表示されます。\",\"Countries among favorites\":\"お気に入りの国\",\"No known countries among favorites yet.\":\"お気に入りの国にはまだ知られていません。\",\"Age groups among favorites\":\"お気に入りの年齢層\",\"No known ages among favorites yet.\":\"お気に入りの年齢はまだ不明です。\",\"Model types among favorites\":\"お気に入りのモデルタイプ\",\"No known model types among favorites yet.\":\"お気に入りのモデルタイプはまだ知られていません。\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"これらの結果はサーバーに送信されません。実際の視聴時間のみが視聴設定にカウントされます。プレビューとお気に入りは別々に表示されます。年齢、国、場所、またはタグのデータが欠落していても、推測されることはありません。\",\"Open Following on Chaturbate\":\"Chaturbate でフォローを開く\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Chaturbate は文書化された個人履歴 API を提供していないため、完全な総再生時間や訪問履歴を遡って取得することはできません。 Chaturbate の公式データ エクスポートを通じて、保存されている個人データのコピーをリクエストできます。そのエクスポートに使用可能な履歴フィールドが含まれている場合、RoomGrid はファイル形式を検証した後、安全なインポートをサポートできます。公式の統計 API は現在のブロードキャスターの値を提供します。 RoomGrid は、最初の接続以降のみ独自の履歴を構築できます。\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"ローカルで観察、保存、監視されたすべてのモデルを、総再生時間、セッション、アクティビティ、プレビュー、品質、ランキングと比較します。\",\"Large model library\":\"大規模なモデル ライブラリ\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"モデルは、ライブ ステータス、メモ、タグ、評価、優先度、グループ、直接アクションを備えた画像が豊富で便利なプロファイルとして表示されます。\",\"My user dashboard\":\"私のユーザーダッシュボード\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"あなたのプライベートなローカル利用履歴、習慣、図書館データ、そして(意図的に接続した場合のみ)公式放送局アカウントのデータ。\",\"🔒 Local & private on this device\":\"🔒 このデバイス内だけで安全に保存\",\"Preparing data…\":\"データを準備中…\",\"RoomGrid Comfort Center\":\"RoomGridコントロールセンター\",\"inside the standard Chaturbate page\":\"通常のChaturbateページ内\",\"All RoomGrid pages and settings\":\"RoomGrid のすべてのページと設定\",\"Media paused; names and images are covered.\":\"メディアは一時停止しました。名前と画像がカバーされています。\",\"Restore previous view\":\"以前のビューを復元する\",\"Enter local PIN\":\"ローカルPINを入力してください\",\"In-page notifications\":\"ページ内通知\",\"No new notifications\":\"新しい通知はありません\",\"Notifications are off\":\"通知はオフになっています\",\"RoomGrid notification\":\"ルームグリッド通知\",\"Dismiss as read\":\"既読として閉じる\",\"More unread notifications\":\"未読の通知が増える\",\"RoomGrid update\":\"ルームグリッドのアップデート\",\"A monitored source changed status.\":\"監視対象ソースのステータスが変更されました。\",\"This is a local RoomGrid test notification.\":\"これはローカル RoomGrid テスト通知です。\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"テストが成功しました: 通知は RoomGrid バーに直接表示されます。\",\"Settings\":\"設定\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"ワークスペース、ライブラリ、デザイン、データ、履歴、通知、プライバシー、パフォーマンス\",\"Workspaces\":\"ワークスペース\",\"Data\":\"データ\",\"History & statistics\":\"歴史と統計\",\"Broadcaster stats\":\"放送局の統計\",\"Notifications\":\"通知\",\"Windows & performance\":\"ウィンドウとパフォーマンス\",\"Optional tools\":\"オプションのツール\",\"Settings pages\":\"設定ページ\",\"RoomGrid control center\":\"ルームグリッド コントロール センター\",\"Any online model\":\"あらゆるオンラインモデル\",\"Workspace name\":\"ワークスペース名\",\"Optional description\":\"オプションの説明\",\"Active\":\"アクティブ\",\"No description\":\"説明なし\",\"Name\":\"名前\",\"Description\":\"説明\",\"Replace offline models\":\"オフラインモデルを置き換える\",\"Replacement group\":\"代替グループ\",\"Delay seconds\":\"遅延秒数\",\"Save current state\":\"現在の状態を保存する\",\"New window\":\"新しいウィンドウ\",\"Export this workspace\":\"このワークスペースをエクスポートする\",\"Duplicate\":\"重複\",\"Workspace restored\":\"ワークスペースが復元されました\",\"Workspace archived\":\"ワークスペースがアーカイブされました\",\"Delete permanently\":\"完全に削除\",\"Permanently delete this workspace?\":\"このワークスペースを完全に削除しますか?\",\"Saved layout and panel sizes\":\"保存されたレイアウトとパネルのサイズ\",\"Apply\":\"申し込む\",\"Layout template deleted\":\"レイアウトテンプレートが削除されました\",\"Parent group\":\"親グループ\",\"None\":\"なし\",\"Group description\":\"グループの説明\",\"Save current workspace\":\"現在のワークスペースを保存する\",\"Save layout template\":\"レイアウトテンプレートを保存する\",\"Template name\":\"テンプレート名\",\"Layout templates\":\"レイアウトテンプレート\",\"Device layout\":\"デバイスのレイアウト\",\"Groups and subgroups\":\"グループとサブグループ\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"各ワークスペースには、個別のデスクトップ レイアウトとコンパクト レイアウトが保存されます。自動置換はデフォルトではオフになっており、そのワークスペースで有効にした後でのみオフライン スロットが埋められます。\",\"No templates yet.\":\"テンプレートはまだありません。\",\"Group name\":\"グループ名\",\"New subgroup\":\"新しいサブグループ\",\"Enter parent group name\":\"親グループ名を入力してください\",\"Subgroup name\":\"サブグループ名\",\"Name, note or local tag\":\"名前、メモ、またはローカルタグ\",\"Choose group…\":\"グループを選択してください…\",\"Add selected models to group\":\"選択したモデルをグループに追加\",\"Watch later\":\"後で見る\",\"Paused\":\"一時停止中\",\"Collection name\":\"コレクション名\",\"Tags, comma separated\":\"タグ、カンマ区切り\",\"Countries, comma separated\":\"国、カンマ区切り\",\"All types\":\"全種類\",\"Rule name\":\"ルール名\",\"Required tags\":\"必須タグ\",\"Local alert\":\"ローカルアラート\",\"Add to favorites\":\"お気に入りに追加\",\"Add to group\":\"グループに追加\",\"Any tag\":\"任意のタグ\",\"Any country\":\"どの国でも\",\"Pause\":\"一時停止\",\"Enable\":\"有効にする\",\"Delete\":\"削除\",\"Automation rule deleted\":\"自動化ルールが削除されました\",\"Select model\":\"モデルを選択してください\",\"Later\":\"後で\",\"Never visited\":\"訪れたことがない\",\"Last online\":\"最後にオンラインで\",\"Last visited\":\"最後に訪問した\",\"Local note\":\"ローカルメモ\",\"Local tags, comma separated\":\"ローカルタグ、カンマ区切り\",\"Remove later\":\"後で削除する\",\"Hide today\":\"今日は隠す\",\"Pause 7 days\":\"7日間一時停止する\",\"Never suggest\":\"決して提案しないでください\",\"Select results\":\"結果の選択\",\"Clear selection\":\"選択をクリア\",\"selected\":\"選択された\",\"Add to watch later\":\"後で見るために追加する\",\"Permanently delete selected models?\":\"選択したモデルを完全に削除しますか?\",\"Smart collections\":\"スマートコレクション\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"ローカルのメタデータと統計が増加すると、コレクションは自動的に更新されます。モデルは複製されません。\",\"Tags\":\"タグ\",\"Countries\":\"国\",\"Favorites only\":\"お気に入りのみ\",\"Online only\":\"オンラインのみ\",\"Save smart collection\":\"スマートコレクションを保存する\",\"Delete current collection\":\"現在のコレクションを削除する\",\"Delete this smart collection? Model data stays unchanged.\":\"このスマート コレクションを削除しますか?モデルデータは変更されません。\",\"Rules & automation center\":\"ルールとオートメーションセンター\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"ローカル モデル データが一致する場合、アラートを 1 つ表示するか、お気に入りを追加するか、モデルを選択したグループに追加します。\",\"Action\":\"アクション\",\"Target group\":\"対象者\",\"Save rule\":\"ルールの保存\",\"No automation rules yet.\":\"自動化ルールはまだありません。\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"ユーザー名は、完全に正規化された一致によって重複排除されます。検索には名前、メモ、ローカル タグが含まれます。アーカイブ、ビューからの削除、および完全な削除は別個のアクションのままです。\",\"No matching models.\":\"一致するモデルがありません。\",\"export\":\"輸出\",\"Models and notes\":\"モデルとメモ\",\"This backup could not be read. Current data was not changed.\":\"このバックアップを読み取れませんでした。現在のデータは変更されていません。\",\"Import preview (nothing changed yet)\":\"インポートプレビュー (まだ何も変更されていません)\",\"conflicts\":\"衝突\",\"Includes sensitive statistics\":\"機密性の高い統計が含まれます\",\"models\":\"モデル\",\"events\":\"イベント\",\"quality samples\":\"高品質のサンプル\",\"Import statistics too\":\"統計もインポートします\",\"Import method\":\"インポート方法\",\"Merge (recommended)\":\"マージ (推奨)\",\"Replace selected sections\":\"選択したセクションを置換する\",\"Apply import\":\"インポートを適用する\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"インポートが完了しました。以前の構成と統計は個別にバックアップされました。\",\"The import could not be saved safely. The backup remains available.\":\"インポートを安全に保存できませんでした。バックアップは引き続き利用可能です。\",\"Cancel preview\":\"プレビューをキャンセルする\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"このファイルにはモデル、メモ、またはワークスペース データが含まれているため、機密として扱う必要があります。\",\"This import is not marked sensitive.\":\"このインポートは機密としてマークされていません。\",\"Data version\":\"データバージョン\",\"Software version\":\"ソフトウェアバージョン\",\"Revision\":\"改訂\",\"Local models\":\"ローカルモデル\",\"Selective export\":\"選択的エクスポート\",\"Export selected\":\"選択したエクスポート\",\"Full export\":\"完全なエクスポート\",\"Complete safety package\":\"完全な安全パッケージ\",\"Export statistics separately\":\"統計を個別にエクスポートする\",\"Choose file and preview\":\"ファイルを選択してプレビューする\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"完全な安全パッケージには構成と機密統計が含まれていますが、公式統計トークンは含まれていません。すべてのインポートはプレビューされ、適用する前に構成と統計を個別にバックアップします。\",\"Storage and backup center\":\"ストレージおよびバックアップセンター\",\"Local storage\":\"ローカルストレージ\",\"Site storage\":\"サイトストレージ\",\"Available quota\":\"利用可能な割り当て量\",\"Internal statistics backups\":\"内部統計のバックアップ\",\"Configuration\":\"構成\",\"workspaces\":\"ワークスペース\",\"LocalStorage + update vault\":\"LocalStorage + ボールトの更新\",\"Statistics database\":\"統計データベース\",\"Secrets\":\"秘密\",\"The official statistics token is never included in normal or complete backups.\":\"公式統計トークンは、通常のバックアップまたは完全なバックアップには決して含まれません。\",\"Excluded\":\"除外される\",\"Create configuration restore point\":\"構成復元ポイントを作成する\",\"Create statistics restore point\":\"統計復元ポイントを作成する\",\"Legacy cleanup\":\"レガシーのクリーンアップ\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"レガシーデータは自動的に削除されることはありません。古い v6/v7 キーは、レビューが成功した後にのみ削除してください。\",\"Remove old v6/v7 copies\":\"古い v6/v7 コピーを削除する\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"古い v6/v7 コピーのみを削除しますか?現在のデータとバックアップは残ります。\",\"items removed\":\"削除された項目\",\"Aurora\":\"オーロラ\",\"Blue and teal, the RoomGrid signature\":\"ブルーとティール、RoomGridの特徴\",\"Ember\":\"残り火\",\"Warm orange and rose\":\"温かみのあるオレンジとローズ\",\"Frost\":\"フロスト\",\"Crisp cyan and violet\":\"鮮やかなシアンとバイオレット\",\"Custom\":\"カスタム\",\"Use the colors selected below\":\"以下で選択した色を使用します\",\"Name the current appearance\":\"現在の外観に名前を付けます\",\"My appearance\":\"私の姿\",\"Delete appearance profile\":\"外観プロファイルを削除する\",\"This appearance profile could not be imported.\":\"この外観プロファイルはインポートできませんでした。\",\"Light\":\"ライト\",\"Dark\":\"暗い\",\"Export\":\"エクスポート\",\"Home\":\"ホーム\",\"Listings and tags\":\"リストとタグ\",\"Model pages\":\"モデルページ\",\"Use global appearance\":\"グローバルな外観を使用する\",\"This logo format is unsafe or unsupported.\":\"このロゴ形式は安全でないか、サポートされていません。\",\"The logo file could not be read.\":\"ロゴファイルを読み取れませんでした。\",\"MultiCam tools\":\"マルチカムツール\",\"Page\":\"ページ\",\"Panel\":\"パネル\",\"Card surface\":\"カード表面\",\"Input\":\"入力\",\"Border\":\"境界線\",\"Text\":\"テキスト\",\"Muted text\":\"ミュートされたテキスト\",\"Success\":\"成功\",\"Warning\":\"警告\",\"Danger\":\"危険\",\"Info and focus\":\"情報と焦点\",\"Data hub\":\"データハブ\",\"Text / page\":\"テキスト/ページ\",\"Text / panel\":\"テキスト/パネル\",\"Muted text / panel\":\"ミュートされたテキスト/パネル\",\"Accent text / accent\":\"アクセントテキスト/アクセント\",\"Desktop\":\"デスクトップ\",\"Tablet\":\"タブレット\",\"Mobile\":\"モバイル\",\"Brand and logo studio\":\"ブランドとロゴのスタジオ\",\"Monogram\":\"モノグラム\",\"Tagline\":\"キャッチフレーズ\",\"Brand display\":\"ブランド表示\",\"Full name\":\"フルネーム\",\"Uploaded logo\":\"アップロードされたロゴ\",\"Logo shape\":\"ロゴの形状\",\"Rounded\":\"丸い\",\"Circle\":\"サークル\",\"Square\":\"正方形\",\"Pill\":\"錠剤\",\"Page title\":\"ページタイトル\",\"Website original\":\"サイト標準\",\"Brand plus original\":\"ブランドプラスオリジナル\",\"Custom title\":\"カスタムタイトル\",\"Custom page title\":\"カスタムページタイトル\",\"Image fit\":\"画像のフィット\",\"Horizontal position\":\"水平位置\",\"Vertical position\":\"縦位置\",\"Upload light-mode logo\":\"ライトモードのロゴをアップロードする\",\"Upload dark-mode logo\":\"ダークモードロゴをアップロードする\",\"Remove logos\":\"ロゴを削除する\",\"Show version\":\"バージョンを表示\",\"Use logo as tab icon on RoomGrid pages\":\"RoomGrid ページのタブ アイコンとしてロゴを使用する\",\"Replace website logo in the original header (local only)\":\"元のヘッダーの Web サイトのロゴを置き換えます (ローカルのみ)\",\"Complete color system\":\"完全なカラーシステム\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"アクセントに加えて、ページ、パネル、カード、入力、境界線、テキスト、ステータスの色をコントロールします。\",\"Derived automatically\":\"自動的に派生\",\"Fully custom\":\"フルカスタム\",\"Editing dark palette\":\"ダークパレットの編集\",\"Editing light palette\":\"ライトパレットの編集\",\"Reset current palette\":\"現在のパレットをリセット\",\"Layout editor\":\"レイアウトエディター\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"RoomGrid ページ全体でコンテンツの間隔、カードの幅、ヘッダー、サイドバーを一貫して設定します。\",\"Minimum card width\":\"最小カード幅\",\"Section spacing\":\"セクションの間隔\",\"Content padding\":\"コンテンツのパディング\",\"Header height\":\"ヘッダーの高さ\",\"Maximum columns\":\"最大列数\",\"Automatic\":\"自動\",\"Card aspect\":\"カードの側面\",\"Sidebar position\":\"サイドバーの位置\",\"Left\":\"左\",\"Right\":\"右\",\"Keep header visible while scrolling\":\"スクロール中にヘッダーを表示したままにする\",\"Live device preview\":\"ライブデバイスプレビュー\",\"Contrast and accessibility\":\"コントラストとアクセシビリティ\",\"High-contrast mode\":\"ハイコントラストモード\",\"Underline links\":\"リンクに下線を引く\",\"Reduce transparency\":\"透明度を下げる\",\"Boost base text size\":\"基本テキストサイズを大きくする\",\"Focus ring width\":\"フォーカスリング幅\",\"Minimum target size\":\"最小ターゲットサイズ\",\"Respect system reduced-motion setting\":\"システムのモーション低減設定を尊重する\",\"Apply accessible defaults\":\"アクセス可能なデフォルトを適用する\",\"Profiles by page and screen size\":\"ページと画面サイズごとのプロファイル\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"1 つ以上の外観を保存し、デスクトップ、タブレット、モバイルのページ タイプごとに割り当てます。画面サイズが変わるとプロファイルが自動的に切り替わります。\",\"Enable responsive design profiles\":\"レスポンシブデザインプロファイルを有効にする\",\"Mobile maximum width\":\"モバイルの最大幅\",\"Tablet maximum width\":\"タブレットの最大幅\",\"RoomGrid design system\":\"RoomGrid デザイン システム\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"ブランドの方向性、コンテンツの幅、カード情報の密度を選択します。すべてのページがすぐに続きます。\",\"Primary accent\":\"主アクセント\",\"Secondary accent\":\"第二アクセント\",\"Content width\":\"コンテンツの幅\",\"Centered\":\"中央揃え\",\"Wide\":\"ワイド\",\"Full width\":\"全幅\",\"Card style\":\"カードスタイル\",\"Visual\":\"ビジュアル\",\"Data-rich\":\"豊富なデータ\",\"Micro\":\"マイクロ\",\"Widget density\":\"ウィジェットの密度\",\"Spacious\":\"広々とした\",\"Theme and surfaces\":\"テーマとサーフェス\",\"Surface style\":\"表面のスタイル\",\"Soft\":\"ソフト\",\"Flat\":\"フラット\",\"Glass\":\"ガラス\",\"Page background\":\"ページの背景\",\"Solid\":\"固体\",\"Gradient\":\"グラデーション\",\"Ambient glow\":\"アンビエントグロー\",\"Surface opacity\":\"表面の不透明度\",\"Custom accent color\":\"カスタムアクセントカラー\",\"Sizing and motion\":\"サイズと動き\",\"Font style\":\"フォントスタイル\",\"System\":\"システム\",\"Classic serif\":\"クラシックなセリフ\",\"Monospace\":\"等空間\",\"Density\":\"密度\",\"Font scale\":\"フォントスケール\",\"Control size\":\"コントロールサイズ\",\"Corner radius\":\"コーナー半径\",\"Card spacing\":\"カードの間隔\",\"Shadow strength\":\"影の強さ\",\"Sidebar width\":\"サイドバーの幅\",\"Enable animations\":\"アニメーションを有効にする\",\"Appearance profiles\":\"外観プロフィール\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"完全な外観を再利用可能なプロファイルとして保存します。プロファイルは、スクリプトの更新後も引き続き使用できます。\",\"Save current appearance\":\"現在の外観を保存する\",\"Import profile\":\"プロファイルのインポート\",\"No profiles saved yet.\":\"プロファイルはまだ保存されていません。\",\"Instant preview\":\"インスタントプレビュー\",\"Example card\":\"カード例\",\"This is the combined result of the current visual settings.\":\"これは、現在のビジュアル設定を組み合わせた結果です。\",\"Example action\":\"アクション例\",\"Per-page design profiles\":\"ページごとのデザインプロファイル\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"ホーム、リスト、モデル ページ、MultiCam、Discover、およびデータ ハブに別の保存済み外観を選択します。\",\"Enable per-page design\":\"ページごとのデザインを有効にする\",\"Normal website pages\":\"通常のウェブサイトのページ\",\"Enable RoomGrid page design\":\"RoomGrid ページのデザインを有効にする\",\"Tag and category pages\":\"タグページとカテゴリーページ\",\"Cards\":\"カード\",\"List\":\"リスト\",\"Page density\":\"ページ密度\",\"Polish navigation\":\"ポーランド語のナビゲーション\",\"Polish sidebars\":\"ポーランド語のサイドバー\",\"Hide recognized promo and merch areas\":\"認識されたプロモーションおよび商品エリアを非表示にする\",\"Compact footer\":\"コンパクトフッター\",\"Mark saved models\":\"保存されたモデルにマークを付ける\",\"Mark favorites separately\":\"お気に入りを個別にマークする\",\"Mark visited models\":\"訪問したモデルにマークを付ける\",\"Show card quick actions\":\"カードのクイックアクションを表示\",\"Animated live previews on home and listing pages\":\"ホームページとリストページのアニメーションライブプレビュー\",\"Preview refresh rate\":\"プレビューのリフレッシュ レート\",\"Off\":\"オフ\",\"Update mode\":\"アップデートモード\",\"All visible cards simultaneously\":\"表示されているすべてのカードを同時に\",\"Performance-bounded batches\":\"パフォーマンスに制限のあるバッチ\",\"Batch concurrency limit\":\"バッチ同時実行制限\",\"All visible\":\"すべて表示されます\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"表示されているカードのみがミュートされた JPEG フレーム更新を受け取ります。画面外または非表示のタブですぐに一時停止します。同期モードでは、表示されているすべてのカードが 1 つのウェーブで更新されます。レートが高いと、帯域幅と CPU 使用率が著しく増加します。\",\"Live video on hover\":\"ホバー時のライブビデオ\",\"Play real-time video while hovering\":\"ホバリング中にリアルタイムビデオを再生する\",\"Start delay\":\"開始遅延\",\"Immediately\":\"すぐに\",\"Hover enlargement\":\"ホバー拡大\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"ポインターの下にあるカードのみが再生され、常にミュートされます。タブを離れるか非表示にすると、ストリームがすぐに停止します。\",\"Live Preview Dock\":\"ライブプレビュードック\",\"Enable low-load live previews\":\"低負荷のライブ プレビューを有効にする\",\"Concurrent previews\":\"同時プレビュー\",\"Window size\":\"ウィンドウサイズ\",\"Small\":\"小\",\"Medium\":\"中\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"プレビューはミュートで開始され、非表示のタブで一時停止します。オーディオには常に明示的なクリックが必要です。\",\"Quick info & chat focus\":\"クイック情報とチャット中心\",\"Enable model Quick View\":\"モデルのクイックビューを有効にする\",\"Chat focus mode (opt-in)\":\"チャット フォーカス モード (オプトイン)\",\"Show local timestamps\":\"ローカルタイムスタンプを表示する\",\"System messages\":\"システムメッセージ\",\"Show\":\"表示する\",\"Dim\":\"薄暗い\",\"Hide\":\"隠す\",\"Chat filter strength\":\"チャットフィルターの強度\",\"Low\":\"低い\",\"Strict\":\"厳格\",\"Highlight words (comma separated)\":\"単語を強調表示します (カンマ区切り)\",\"Muted words (comma separated)\":\"ミュートされた単語 (カンマ区切り)\",\"Collapse repeated messages\":\"繰り返されるメッセージを折りたたむ\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"チャットの機能強化では、現在の表示のみが変更されます。チャットを保存したり、メッセージを送信したり、自動的にチップを送ったりすることはありません。\",\"Shortcuts\":\"ショートカット\",\"Command menu\":\"コマンドメニュー\",\"Global model search\":\"グローバルモデル検索\",\"Reset design defaults\":\"デザインのデフォルトをリセットする\",\"Optional modules\":\"オプションモジュール\",\"Normal-page dock and card actions\":\"通常ページのドックとカードのアクション\",\"Real audio meters\":\"リアルオーディオメーター\",\"Media, app and contest directories\":\"メディア、アプリ、コンテストのディレクトリ\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"モジュールのスイッチは、ページをリロードした後に完全に有効になります。データ損失を防ぐために、コア データとバックアップ モジュールは有効なままになります。\",\"Name the current filters and sorting\":\"現在のフィルターと並べ替えに名前を付けます\",\"My smart view\":\"私の賢明な見方\",\"Delete smart view\":\"スマートビューを削除する\",\"Smart view deleted\":\"スマートビューが削除されました\",\"tags\":\"タグ\",\"countries\":\"国\",\"Saved selection\":\"保存された選択範囲\",\"Suggestions\":\"提案\",\"Selected\":\"選択済み\",\"Website tags\":\"ウェブサイトのタグ\",\"Countries (multiple selection)\":\"国 (複数選択)\",\"Select countries\":\"国を選択してください\",\"Search countries…\":\"国を検索…\",\"Recognized in current models\":\"現行モデルでも認識\",\"All countries\":\"すべての国\",\"Tags (multiple selection)\":\"タグ (複数選択)\",\"Select tags\":\"タグの選択\",\"Search tags…\":\"タグを検索…\",\"Room subject\":\"ルームのテーマ\",\"Current online duration\":\"現在のオンライン時間\",\"Local watch time and sessions\":\"ローカル視聴時間とセッション\",\"HD, new, gaming and favorite badges\":\"HD・新人・ゲーム・お気に入りのバッジ\",\"Homepage Studio\":\"ホームページスタジオ\",\"Balanced preset\":\"バランス設定\",\"Compact preset\":\"コンパクト設定\",\"Data-rich preset\":\"情報重視設定\",\"Cinema preset\":\"シネマ設定\",\"Layout and pagination\":\"レイアウトとページ分割\",\"Show quick toolbar above models\":\"モデルの上にクイックツールバーを表示\",\"Cards per row\":\"1行あたりのカード数\",\"Image ratio\":\"画像比率\",\"Information display\":\"情報表示\",\"Detailed\":\"詳細\",\"Overlay on image\":\"画像上に重ねる\",\"Powerful sorting\":\"高度な並べ替え\",\"Primary sort\":\"一次ソート\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"並べ替えで変わるのは読み込み済みモデルカードの表示順だけです。Chaturbateアカウントのフォローやお気に入りは変更されません。\",\"Restore website order\":\"サイトの順序に戻す\",\"Clear all filters\":\"すべてのフィルターを解除\",\"Search and range filters\":\"検索と範囲フィルター\",\"When several tags are selected\":\"複数のタグを選択した場合\",\"Model must contain every tag\":\"モデルにはすべてのタグが含まれている必要があります\",\"At least one tag matches\":\"少なくとも 1 つのタグが一致します\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"国フィルタリングでは、選択した国のモデルが表示されます。独自のタグと認識された Web サイトのタグはリスト内で利用可能なままであり、検索ですぐに見つけることができます。\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"タグまたは国を選択すると、RoomGrid は現在のライブ カタログ全体を検索します。同じ結果ページには、最初は 120 件の一致が表示され、スクロール中にさらに多くのものが分割されて読み込まれるため、非常に一般的なタグでも応答性が維持されます。ホームページ ツールバーのリロード ボタンを使用すると、ライブ リストが更新されます。\",\"Quick conditions\":\"クイック条件\",\"HD models only\":\"HDモデルのみ\",\"New models only\":\"新しいモデルのみ\",\"Gaming models only\":\"ゲーム配信モデルのみ\",\"Only saved in RoomGrid\":\"RoomGridに保存済みのみ\",\"Only own favorites\":\"自分のお気に入りのみ\",\"Following only\":\"フォローのみ\",\"Hide previously watched models\":\"以前見たモデルを非表示\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"国、年齢、オンライン時間は、現在の公開カードまたは公開ルーム一覧から取得します。Chaturbateが提供しない項目をRoomGridが推測することはありません。\",\"Smart filter views\":\"スマートフィルタービュー\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"現在のフィルターと並べ替えを動的ビューとして保存します。後でオンラインになり、ルールに一致するモデルが自動的に表示されます。\",\"Save current filter view\":\"現在のフィルタービューを保存する\",\"No smart views saved yet.\":\"スマート ビューはまだ保存されていません。\",\"What to show for each model\":\"モデルごとに表示する内容\",\"Import complete\":\"インポートが完了しました\",\"Invalid file\":\"無効なファイル\",\"Range\":\"範囲\",\"Today\":\"今日\",\"Week\":\"週\",\"Month\":\"月\",\"Year\":\"年\",\"Recording enabled\":\"録音可能\",\"Private session (do not record)\":\"プライベートセッション(録音不可)\",\"Retention days\":\"保存日数\",\"Usage time\":\"利用時間\",\"Summed camera time\":\"合計カメラ時間\",\"Visits\":\"訪問\",\"Known models\":\"既知のモデル\",\"Weekly heatmap\":\"毎週のヒートマップ\",\"Aggregated by weekday and hour; darker means more observed time.\":\"曜日と時間ごとに集計されます。暗いほど観察時間が長いことを意味します。\",\"Sunday to Saturday, 24 hours per row\":\"日曜日から土曜日まで、1 行あたり 24 時間\",\"Watch ranking\":\"ウォッチランキング\",\"little data\":\"少ないデータ\",\"No watch history yet.\":\"まだ視聴履歴がありません。\",\"Workspace, filter and quality events\":\"ワークスペース、フィルター、品質イベント\",\"Recent timeline\":\"最近のタイムライン\",\"Export sensitive statistics\":\"機密統計をエクスポートする\",\"Import statistics\":\"統計のインポート\",\"Delete selected range\":\"選択範囲を削除\",\"Delete local events in the selected range?\":\"選択した範囲のローカル イベントを削除しますか?\",\"Clear all statistics\":\"すべての統計をクリアする\",\"Permanently clear all local statistics?\":\"すべてのローカル統計を永久に消去しますか?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"統計エクスポートにはモデル名とタイムスタンプが含まれているため、機密情報として扱う必要があります。古いデータは保持期間に従って消去されます。\",\"new\":\"新しい\",\"Under 1 minute\":\"1分未満\",\"1–5 minutes\":\"1 ~ 5 分\",\"5–15 minutes\":\"5~15分\",\"15–30 minutes\":\"15~30分\",\"30+ minutes\":\"30分以上\",\"Local observations and your usage\":\"現地での観察と使い方\",\"Public profile loaded on request\":\"リクエストに応じてロードされる公開プロファイル\",\"Local RoomGrid interaction\":\"ローカルルームグリッドインタラクション\",\"Local browser player\":\"ローカルブラウザプレーヤー\",\"Optional official statistics API\":\"オプションの公式統計 API\",\"Local daily aggregation\":\"ローカルの日次集計\",\"Local data\":\"ローカルデータ\",\"Model summaries\":\"モデルの概要\",\"Public profiles\":\"公開プロフィール\",\"Usage events\":\"使用イベント\",\"Official account snapshots\":\"公式アカウントのスナップショット\",\"Daily rollups\":\"毎日のロールアップ\",\"Camera time\":\"カメラ時間\",\"vs previous period\":\"前期との比較\",\"Trends\":\"トレンド\",\"Daily activity\":\"日々の活動\",\"Playback quality\":\"再生品質\",\"Privacy status\":\"プライバシーステータス\",\"Data provenance\":\"データの出所\",\"Local RoomGrid usage\":\"ローカルルームグリッドの使用法\",\"7 days\":\"7日間\",\"30 days\":\"30日\",\"90 days\":\"90日\",\"Not enough data yet.\":\"まだ十分なデータがありません。\",\"Models revisited\":\"モデルの再検討\",\"Previews opened\":\"プレビューが開かれました\",\"Quick Views\":\"クイックビュー\",\"Samples\":\"サンプル\",\"Private session\":\"プライベートセッション\",\"History paused\":\"歴史は一時停止しました\",\"Recording locally\":\"ローカルで録音する\",\"Disabled\":\"障害者\",\"No cloud sync or telemetry.\":\"クラウド同期やテレメトリはありません。\",\"Raw events\":\"生のイベント\",\"Only local observations and actions from this browser.\":\"このブラウザからのローカルの観察とアクションのみ。\",\"Time\":\"時間\",\"Models\":\"モデル\",\"Online planner\":\"オンラインプランナー\",\"Data & privacy\":\"データとプライバシー\",\"Configure statistics dashboard\":\"統計ダッシュボードの構成\",\"Preset\":\"プリセット\",\"Minimal\":\"最小限\",\"Quality focus\":\"品質重視\",\"Privacy focus\":\"プライバシー重視\",\"Reset dashboard\":\"ダッシュボードをリセットする\",\"Size\":\"サイズ\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"順序、可視性、および幅は更新セーフ設定に保存されます。すべてのウィジェットにはデータ ソースが表示されます。\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"ダッシュボード ウィジェットは表示されません。設定を開き、少なくとも 1 つのウィジェットを選択します。\",\"Median session\":\"セッション中央値\",\"Daily usage trend\":\"日々の使用量の傾向\",\"Your trend appears here after you start using RoomGrid.\":\"RoomGrid の使用を開始すると、ここに傾向が表示されます。\",\"Session-duration distribution\":\"セッション期間の分布\",\"No session-duration data yet.\":\"セッション期間データはまだありません。\",\"Return rate\":\"返品率\",\"Total sessions\":\"合計セッション数\",\"Longest observed\":\"最も長く観測された\",\"Model ranking\":\"モデルランキング\",\"Select up to four models for a local comparison.\":\"ローカル比較には最大 4 つのモデルを選択します。\",\"Quick View\":\"クイックビュー\",\"No model data yet.\":\"まだモデルデータがありません。\",\"Models with time patterns\":\"時間パターンを備えたモデル\",\"High confidence\":\"高い信頼性\",\"Moderate confidence\":\"適度な自信\",\"Planning horizon\":\"計画期間\",\"Likely next active times\":\"次にアクティブになる可能性が高い時間帯\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"ローカルで観察されたオンラインサンプルから計算。放送を保証するものではありません。データが増えるほど信頼性が高まります。\",\"High\":\"高\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"オンラインでの観察はまだ十分ではありません。プランナーはライブラリとともに自動的に表示されます。\",\"Discovery events\":\"ディスカバリーイベント\",\"Local interaction funnel\":\"ローカルインタラクションファネル\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"このデバイス上でのアクションのみがカウントされます。チャットの内容と支払いは決して追跡されません。\",\"Workspace events\":\"ワークスペースイベント\",\"Filter events\":\"イベントをフィルタリングする\",\"Tag events\":\"イベントにタグを付ける\",\"Top-four workspace comparison\":\"上位 4 つのワークスペースの比較\",\"Workspace usage\":\"ワークスペースの使用法\",\"No workspace data yet.\":\"ワークスペース データがまだありません。\",\"Average active streams\":\"平均アクティブストリーム\",\"Playback quality by model\":\"モデル別の再生品質\",\"No quality data yet.\":\"質の高いデータはまだありません。\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"統計ファイルが無効であるか、安全制限を超えています。現在のデータは変更されていません。\",\"Statistics import assistant · preview\":\"統計インポートアシスタント・プレビュー\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"まだ何も変わっていません。インポートするセクションを選択し、競合を確認します。\",\"Safe merge (recommended)\":\"安全なマージ (推奨)\",\"Import complete. The previous statistics were backed up automatically.\":\"インポートが完了しました。以前の統計は自動的にバックアップされました。\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"インポートを安全に適用できませんでした。現在のデータとバックアップは変更されません。\",\"File schema\":\"ファイルスキーマ\",\"Date range\":\"日付範囲\",\"Sensitive data\":\"機密データ\",\"Not marked sensitive\":\"機密扱いとしてマークされていない\",\"Until manually resumed\":\"手動で再開するまで\",\"Not paused\":\"一時停止されていません\",\"Restore this statistics backup? The current state will be backed up first.\":\"この統計バックアップを復元しますか?最初に現在の状態がバックアップされます。\",\"Permanently delete this backup?\":\"このバックアップを完全に削除しますか?\",\"Events\":\"イベント\",\"Estimated statistics storage\":\"推定統計ストレージ\",\"Recording, pauses and private sessions\":\"録音、一時停止、プライベートセッション\",\"Record usage statistics\":\"使用状況の統計を記録する\",\"Pause 15 minutes\":\"15分間一時停止します\",\"Pause 1 hour\":\"1時間停止\",\"Pause 8 hours\":\"8時間停止\",\"Pause indefinitely\":\"無期限に一時停止する\",\"Resume history\":\"履歴を再開する\",\"History status\":\"履歴ステータス\",\"Private for 30 minutes\":\"30分間のプライベート\",\"Private for 2 hours\":\"2時間のプライベートタイム\",\"Private until resumed\":\"再開するまで非公開\",\"End private session\":\"プライベートセッションを終了する\",\"Not active\":\"アクティブではありません\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"プライベート セッションでは、パブリック モデルの観察も停止します。通常の履歴の一時停止により、個人的な使用と品質記録が停止されます。\",\"Automatic deletion and retention\":\"自動削除と保存\",\"Raw events (days)\":\"未処理のイベント (日)\",\"Quality samples (days)\":\"高品質のサンプル (日)\",\"Account snapshots (days)\":\"アカウントのスナップショット (日)\",\"Daily rollups (days)\":\"日次ロールアップ (日)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"期限切れの生イベントは、自動削除される前に毎日の合計にまとめられます。品質スナップショットとアカウント スナップショットには独自の制限が使用されます。\",\"Statistics backup center\":\"統計バックアップセンター\",\"Create internal backup now\":\"今すぐ内部バックアップを作成します\",\"Pseudonymized export\":\"仮名化されたエクスポート\",\"Print report\":\"レポートの印刷\",\"Choose import file\":\"インポートファイルを選択\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"内部統計のバックアップはまだありません。インポート、復元、または手動作成後には、最大 3 つが保持されます。\",\"Delete local statistics in this range? A backup will be created first.\":\"この範囲のローカル統計を削除しますか?最初にバックアップが作成されます。\",\"Clear everything\":\"すべてクリア\",\"Permanently clear all local statistics? A backup will be created first.\":\"すべてのローカル統計を永久に消去しますか?最初にバックアップが作成されます。\",\"Data inventory and provenance\":\"データの目録と出所\",\"Stored only in this browser\":\"このブラウザにのみ保存されます\",\"Coverage\":\"適用範囲\",\"Unknown or missing data is never inferred.\":\"不明なデータまたは欠落したデータは決して推測されません。\",\"Metric definitions & data quality\":\"メトリクスの定義とデータ品質\",\"Accumulated locally while a RoomGrid page is active.\":\"RoomGrid ページがアクティブである間、ローカルに蓄積されます。\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"観察されたすべてのカメラ セッションの合計。同時ストリームが追加されます。\",\"browser samples; no interpolation.\":\"ブラウザのサンプル。補間はありません。\",\"Privacy boundary\":\"プライバシーの境界\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"チャットのコンテンツ、支払い、メディア ファイルは保存されません。プライベート セッションではイベントは追加されません。\",\"No history yet\":\"まだ履歴がありません\",\"Loading\":\"読み込み中\",\"Connected\":\"接続済み\",\"Locked\":\"ロックされています\",\"Not connected\":\"接続されていません\",\"Username\":\"ユーザー名\",\"24-character stats token\":\"24 文字の統計トークン\",\"At least 8 characters\":\"少なくとも 8 文字\",\"Secure official statistics connection\":\"安全な公式統計接続\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"オプション。トークンが通常の設定、ログ、エクスポートに入力されることはありません。セッションモードではメモリ内にのみ保持されます。暗号化モードでは、分離されたローカル ストレージが使用されます。\",\"Stats token\":\"統計トークン\",\"Vault password\":\"ボールトのパスワード\",\"Connect for this session\":\"このセッションのために接続します\",\"Username or token is invalid.\":\"ユーザー名またはトークンが無効です。\",\"Encrypt, save & connect\":\"暗号化、保存、接続\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"暗号化して保存できませんでした。トークン、パスワード、および userscript-manager 権限を確認してください。\",\"Get official token\":\"公式トークンを取得する\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"自動更新は最大 5 分に 1 回実行されます。このモジュールは、意図的に提供された公式トークンのみを使用します。\",\"Account\":\"アカウント\",\"History snapshots\":\"履歴スナップショット\",\"Last refresh\":\"最終更新\",\"Unlock local vault\":\"ローカル金庫のロックを解除する\",\"Password\":\"パスワード\",\"Unlock\":\"ロックを解除する\",\"Unlock failed.\":\"ロック解除に失敗しました。\",\"Loading…\":\"読み込み中…\",\"Refresh now\":\"今すぐ更新してください\",\"Auto-refresh every 5 minutes\":\"5分ごとに自動更新\",\"Disconnect and delete token\":\"トークンを切断して削除する\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"ローカル接続と暗号化されたトークンを削除しますか?過去の統計が残っています。\",\"Broadcast status\":\"放送状況\",\"Token balance\":\"トークン残高\",\"Tips last hour\":\"最後の 1 時間のヒント\",\"Followers\":\"フォロワー\",\"Up votes\":\"賛成票\",\"Down votes\":\"反対票\",\"Satisfaction\":\"満足\",\"Registered viewers\":\"登録視聴者\",\"Tokened viewers\":\"トークン化された視聴者\",\"Time online\":\"オンライン時間\",\"Follower trend\":\"フォロワーの傾向\",\"Tips trend\":\"ヒントの傾向\",\"Token-balance trend\":\"トークン残高の傾向\",\"Viewer trend\":\"視聴者の傾向\",\"Account Health Center\":\"アカウントヘルスセンター\",\"Official My Collection\":\"公式マイコレクション\",\"Official support\":\"公式サポート\",\"Export local statistics\":\"ローカル統計をエクスポートする\",\"Delete snapshot history\":\"スナップショット履歴の削除\",\"Delete the local official-statistics snapshots for this account?\":\"このアカウントのローカル公式統計スナップショットを削除しますか?\",\"Last error\":\"最後のエラー\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"トークンの状態、更新頻度、ローカル履歴、公式リンクがここにあります。 RoomGrid は、トランザクションを自動的に公開したり、メッセージを送信したり、実行したりすることはありません。\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"ブラウザは通知の許可を与えませんでした。ページ内通知はアクティブのままです。\",\"Notification profile\":\"通知プロファイル\",\"Go-live only\":\"ゴーライブのみ\",\"Personalized\":\"パーソナライズされた\",\"Always (all events)\":\"常に (すべてのイベント)\",\"Inactive days\":\"非アクティブな日\",\"Favorites, rating, priority and your tags decide relevance.\":\"お気に入り、評価、優先度、タグによって関連性が決まります。\",\"Includes go-live, offline and public-profile changes.\":\"ゴーライブ、オフライン、パブリック プロファイルの変更が含まれます。\",\"Master switch\":\"マスタースイッチ\",\"On\":\"オン\",\"Off (default)\":\"オフ (デフォルト)\",\"Monitored models\":\"監視対象モデル\",\"Active tab\":\"アクティブなタブ\",\"This tab\":\"このタブ\",\"Another tab\":\"別のタブ\",\"Local notifications\":\"ローカル通知\",\"Notification delivery and display\":\"通知の配信と表示\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"ページ内通知は RoomGrid の上部バーの空き領域を使用するため、ブラウザの許可は必要ありません。位置、継続時間、密度は設定可能です。\",\"Delivery\":\"配送\",\"RoomGrid bar only\":\"ルームグリッドバーのみ\",\"Browser notifications only\":\"ブラウザ通知のみ\",\"RoomGrid bar and browser\":\"RoomGrid バーとブラウザ\",\"No display, history only\":\"表示なし、履歴のみ\",\"In-page position\":\"ページ内の位置\",\"Free top-bar area\":\"無料のトップバーエリア\",\"Floating top center\":\"フローティングトップセンター\",\"Floating top right\":\"右上にフローティング\",\"Display style\":\"表示スタイル\",\"Compact single line\":\"コンパクトな単線\",\"Display duration\":\"表示時間\",\"Until dismissed\":\"解雇されるまで\",\"Visible at once\":\"すぐに見える\",\"Test notification\":\"テスト通知\",\"General rules\":\"一般規則\",\"Enable notifications\":\"通知を有効にする\",\"The browser did not grant notification permission.\":\"ブラウザは通知の許可を与えませんでした。\",\"Neutral text\":\"中立的なテキスト\",\"Quiet time starts\":\"静かな時間が始まります\",\"Quiet time ends\":\"静かな時間が終わる\",\"Cooldown (minutes)\":\"クールダウン (分)\",\"Hourly limit\":\"時間制限\",\"Favorite threshold\":\"お気に入りのしきい値\",\"Open on click\":\"クリックで開く\",\"Per model\":\"モデルごと\",\"Model\":\"モデル\",\"Library is empty.\":\"ライブラリが空です。\",\"Per group\":\"グループごと\",\"Group\":\"グループ\",\"No groups.\":\"グループはありません。\",\"Local notification list\":\"ローカル通知リスト\",\"Mark all read\":\"すべて既読としてマークする\",\"Clear list\":\"リストをクリアする\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"バックグラウンドでは読み取りステータスのみがチェックされ、ビデオは開始されません。ユーザースクリプトは、関連サイトのタブが開いている間のみ実行できます。複数のタブでは自動的に 1 つのチェッカーが選択されます。\",\"Shield status\":\"シールドステータス\",\"Ready\":\"準備完了\",\"Local PIN\":\"ローカルPIN\",\"Set\":\"セット\",\"Not set\":\"未設定\",\"Quick shield\":\"クイックシールド\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"すぐに名前と画像をカバーし、すべてのメディアを一時停止/ミュートします。復元すると、以前のメディアの状態が戻ります。\",\"Activate now\":\"今すぐアクティブ化する\",\"Auto-shield after inactivity\":\"非アクティブ後の自動シールド\",\"Set or change local PIN\":\"ローカルPINを設定または変更する\",\"Leave empty to remove\":\"削除するには空のままにしておきます\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"PIN はローカル アクセスの障壁にすぎず、暗号化ではなく、オペレーティング システムのアカウント保護に代わるものではありません。\",\"History controls\":\"履歴コントロール\",\"Usage statistics\":\"使用状況の統計\",\"Pause history 1 hour\":\"一時停止履歴 1 時間\",\"Clear statistics\":\"統計を明確にする\",\"Clear all local statistics? A backup will be created first.\":\"ローカル統計をすべてクリアしますか?最初にバックアップが作成されます。\",\"Clear Discover settings\":\"検出設定をクリアします\",\"Clear local Discover filters and feeds?\":\"ローカルの Discover フィルターとフィードをクリアしますか?\",\"Clear notification list\":\"通知リストをクリアする\",\"Data boundary\":\"データ境界\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"構成、監視統計、通知履歴、Discover 設定は、このブラウザーにローカルに保存されます。 RoomGrid には、クラウド同期、アカウントのアップロード、テレメトリは含まれません。\",\"Quota\":\"割り当て\",\"Used\":\"中古品\",\"Sensitivity & consent center\":\"機密性および同意センター\",\"Standard configuration\":\"標準構成\",\"Theme, layout, filters\":\"テーマ、レイアウト、フィルター\",\"Local\":\"ローカル\",\"Model names, notes, timestamps, statistics\":\"モデル名、メモ、タイムスタンプ、統計情報\",\"Warn on export\":\"輸出時に警告する\",\"Secret\":\"秘密\",\"Optional official statistics token\":\"オプションの公式統計トークン\",\"Session-only or encrypted vault\":\"セッション専用または暗号化されたボールト\",\"Chat content\":\"チャット内容\",\"Never persisted\":\"決して持続しなかった\",\"Ephemeral\":\"一時的な\",\"Stream recording/screenshots\":\"ストリーム録画/スクリーンショット\",\"Not provided\":\"提供されません\",\"Policy guard\":\"ポリシーガード\",\"Performance profile\":\"パフォーマンスプロファイル\",\"Playing streams\":\"ストリームの再生\",\"Video elements\":\"ビデオ要素\",\"Measured memory\":\"メモリの測定値\",\"Unavailable in browser\":\"ブラウザでは利用できません\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"現在の測定値は設定された制限に近づいています。使用していないカメラを一時停止するか、エコモードを選択してください。\",\"Stream performance\":\"ストリームパフォーマンス\",\"Eco\":\"エコ\",\"Power\":\"パワー\",\"Concurrent stream limit\":\"同時ストリーム制限\",\"Adaptive quality\":\"適応性のある品質\",\"Gradual startup\":\"段階的な起動\",\"Automatic performance mode\":\"自動演奏モード\",\"Memory threshold\":\"メモリしきい値\",\"Stream-count threshold\":\"ストリーム数のしきい値\",\"Current auto state\":\"現在の自動状態\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"未使用のプレビューは停止されます。容量が利用可能になるまで、新しいビデオは制限で一時停止されたままになります。カウントが高くなると徐々に始まります。 「無制限のカメラ」という保証はありません。\",\"Multi-window workspaces\":\"マルチウィンドウのワークスペース\",\"Window name\":\"ウィンドウ名\",\"Active and recent windows\":\"アクティブなウィンドウと最近使用したウィンドウ\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"モデル ライブラリはローカル マージを通じて同期されます。各ウィンドウは独自のワークスペースとビュー設定を保持します。ブラウザで許可されている場合は、再起動後に最近のウィンドウの関連付けを復元できます。\",\"Optional tools are disabled\":\"オプションのツールが無効になっています\",\"Media, app and contest directories will not scan the current page.\":\"メディア、アプリ、コンテストのディレクトリは現在のページをスキャンしません。\",\"Enable optional tools\":\"オプションのツールを有効にする\",\"Local search\":\"ローカル検索\",\"Search saved items and current page\":\"保存されたアイテムと現在のページを検索する\",\"Rescan current page\":\"現在のページを再スキャンする\",\"Disable optional tools\":\"オプションのツールを無効にする\",\"Clear local side data\":\"ローカル側のデータをクリアする\",\"Clear app favorites, categories and media notes?\":\"アプリのお気に入り、カテゴリ、メディアメモをクリアしますか?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"これらのモジュールは、現在のページですでにアクセス可能なパブリック リンクとテキストのみを整理します。メディアのコピー、自動ダウンロード、チャット、トークン、支払い機能の操作は行いません。ログイン、年齢認証、セキュリティ、支払いフォームはそのまま残ります。\",\"Photo and video areas\":\"写真とビデオのエリア\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"このページには、認識できるパブリック メディア領域は存在しません。ネイティブの写真またはビデオ ページを表示しているときに再スキャンします。\",\"Apps directory\":\"アプリディレクトリ\",\"Local category\":\"ローカルカテゴリー\",\"Found on page\":\"ページで見つかりました\",\"No recognizable app links are present on this page.\":\"このページには、認識できるアプリのリンクはありません。\",\"Public contest results\":\"公開コンテストの結果\",\"No readable public contest table is present on this page.\":\"このページには、読み取り可能な公開コンテストの表はありません。\",\"Native account, help and broadcaster links\":\"ネイティブアカウント、ヘルプ、ブロードキャスターリンク\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"このページには安全なネイティブ リンクが見つかりませんでした。支払いとログインのパスは意図的にフィルタリングされます。\",\"Homepage filters reset\":\"ホームページフィルターのリセット\",\"Global appearance reset\":\"グローバルな外観のリセット\",\"Version\":\"バージョン\",\"Health score\":\"健康スコア\",\"Page type\":\"ページの種類\",\"Route generation\":\"ルート生成\",\"Running modules\":\"モジュールの実行\",\"Local errors\":\"ローカルエラー\",\"Repair center\":\"修理センター\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"構成のバックアップは修復の前に必ず作成されます。統計およびモデル データは、これらのアクションによって削除されません。\",\"Reset home filters\":\"ホームフィルターをリセットする\",\"Reset all home-page filters?\":\"すべてのホームページフィルターをリセットしますか?\",\"Reset global appearance\":\"グローバルな外観をリセット\",\"Reset the global appearance? Saved design profiles remain.\":\"グローバルの外観をリセットしますか?保存されたデザインプロファイルは残ります。\",\"Normalize configuration\":\"構成を正規化する\",\"Validate and rewrite the current configuration? A backup is created first.\":\"現在の構成を検証して書き換えますか?最初にバックアップが作成されます。\",\"Close all previews\":\"すべてのプレビューを閉じる\",\"Compatibility self-check\":\"互換性セルフチェック\",\"Module and page status\":\"モジュールとページのステータス\",\"Page route test matrix\":\"ページルートテストマトリックス\",\"Current route\":\"現在のルート\",\"Trigger\":\"トリガー\",\"Recent page transitions\":\"最近のページ遷移\",\"Capability layer\":\"機能層\",\"Available\":\"利用可能\",\"Fallback\":\"フォールバック\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"API が欠落している場合、影響を受けるモジュールのみがフォールバックします。ネットワーク障害が繰り返されると、一時的にサーキット ブレーカーが開きます。\",\"Backup and recovery status\":\"バックアップとリカバリのステータス\",\"Primary data\":\"一次データ\",\"Stable archive\":\"安定したアーカイブ\",\"Rolling backups\":\"ローリングバックアップ\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"セルフチェックでは、可読性とブラウザの機能のみが検証されます。現在のデータが自動的に上書きされることはありません。リカバリでは、既存の確認済みバックアップ フローが引き続き使用されます。\",\"Sanitized local error log\":\"サニタイズされたローカル エラー ログ\",\"No RoomGrid errors recorded.\":\"RoomGrid エラーは記録されませんでした。\",\"Run self-check again\":\"再度セルフチェックを実行してください\",\"Export diagnostics\":\"診断のエクスポート\",\"Clear error log\":\"エラーログをクリアする\",\"Return to normal site\":\"通常サイトに戻る\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"診断エクスポートには、ワークスペース、モデル名、メモ、統計情報は含まれません。 URL クエリ パラメーターと考えられるシークレット値は、エラー テキストから削除されます。\",\"Local history on\":\"地元の歴史\",\"History off\":\"履歴オフ\",\"This settings page could not be loaded\":\"この設定ページを読み込めませんでした\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"データは変更されませんでした。もう一度やり直してください;問題が解決しない場合は、「診断」を開きます。\",\"Try again\":\"もう一度試してください\",\"Open Diagnostics\":\"オープン診断\",\"Public results\":\"公開結果\",\"Public contest links\":\"公開コンテストのリンク\",\"Page content has not appeared yet\":\"ページのコンテンツはまだ表示されていません\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid はページを再度チェックしましたが、Chaturbate のコンテンツ エリアがまだ見つかりません。ページがまだ読み込み中であるか、サイトが空のビューを返した可能性があります。\",\"Check again\":\"もう一度確認してください\",\"Open RoomGrid Discover\":\"オープンルームグリッドディスカバー\",\"Reload page\":\"ページをリロードする\",\"Hide notice\":\"通知を非表示にする\",\"Open MultiCam\":\"マルチカムを開く\",\"Open workstation in a new window\":\"新しいウィンドウでワークステーションを開く\",\"Open Discover\":\"オープンディスカバー\",\"Statistics center\":\"統計センター\",\"Backup and import\":\"バックアップとインポート\",\"Page and design options\":\"ページとデザインのオプション\",\"Hide immediately\":\"すぐに隠れる\",\"All tools\":\"すべてのツール\",\"Search & commands\":\"検索とコマンド\",\"Page design\":\"ページデザイン\",\"Official collection\":\"公式コレクション\",\"Saved\":\"保存されました\",\"Quick info\":\"クイックインフォメーション\",\"Loading live frame\":\"ライブフレームをロードしています\",\"Live still\":\"まだ生きてください\",\"Not live\":\"ライブではありません\",\"Pages\":\"ページ\",\"Refresh\":\"リフレッシュ\",\"Search…\":\"検索…\",\"Tag Explorer\":\"タグエクスプローラー\",\"Refresh public catalog\":\"公開カタログを更新する\",\"Online\":\"オンライン\",\"Toggle favorite\":\"お気に入りを切り替え\",\"Save\":\"保存\",\"Remove favorite\":\"お気に入りを削除\",\"Removed from library\":\"ライブラリから削除されました\",\"Saved to library\":\"ライブラリに保存されました\",\"Added to favorites\":\"お気に入りに追加されました\",\"Removed from favorites\":\"お気に入りから削除されました\",\"Watching\":\"見てる\",\"Watch\":\"見る\",\"Temporary watchlist\":\"一時的なウォッチリスト\",\"Removed from watchlist\":\"ウォッチリストから削除されました\",\"Added to watchlist\":\"ウォッチリストに追加されました\",\"Choose group\":\"グループを選択してください\",\"Add to group…\":\"グループに追加…\",\"Group updated\":\"グループが更新されました\",\"Site room\":\"現場室\",\"Select for bulk actions\":\"一括操作の場合に選択します\",\"viewers\":\"視聴者\",\"No public description\":\"公開された説明はありません\",\"Open in Tag Explorer\":\"タグエクスプローラーで開く\",\"Feed name\":\"フィード名\",\"View name\":\"ビュー名\",\"Name already exists\":\"名前はすでに存在します\",\"Saved-item limit reached\":\"保存アイテムの制限に達しました\",\"Feed saved\":\"フィードが保存されました\",\"View saved\":\"保存されたビュー\",\"Applied\":\"適用済み\",\"Backup exported\":\"バックアップがエクスポートされました\",\"Backup imported\":\"インポートされたバックアップ\",\"Invalid backup\":\"無効なバックアップ\",\"New models\":\"新モデル\",\"New since last visit\":\"前回の訪問以来の新規\",\"New to me\":\"私にとって初めてのこと\",\"Returning\":\"戻る\",\"Stable online\":\"安定したオンライン\",\"Saved only\":\"保存のみ\",\"Previously watched\":\"以前に視聴した\",\"Local collection\":\"地元のコレクション\",\"Hide visited today\":\"今日はヒデさんにご来店いただきました\",\"No feed selected\":\"フィードが選択されていません\",\"No view selected\":\"ビューが選択されていません\",\"Saved feeds\":\"保存されたフィード\",\"Saved views\":\"保存されたビュー\",\"Save new\":\"新規保存\",\"All public rooms\":\"すべてのパブリックルーム\",\"Known HD\":\"既知の HD\",\"Random result\":\"ランダムな結果\",\"Viewers ↓\":\"視聴者↓\",\"Viewers ↑\":\"視聴者 ↑\",\"Online duration ↓\":\"オンライン期間↓\",\"Followers ↓\":\"フォロワー↓\",\"Name A–Z\":\"名前 A ~ Z\",\"Name Z–A\":\"名前 Z-A\",\"New first\":\"新しい順\",\"Watch time ↓\":\"視聴時間↓\",\"Sessions ↓\":\"セッション↓\",\"Last watched\":\"最後に見たもの\",\"Name, description, tag, place\":\"名前、説明、タグ、場所\",\"Secondary sort\":\"二次ソート\",\"Shuffle again\":\"もう一度シャッフルする\",\"All groups\":\"すべてのグループ\",\"All tags (AND)\":\"すべてのタグ (AND)\",\"Any tag (OR)\":\"任意のタグ (OR)\",\"Excluded tags (NOT)\":\"除外タグ (NOT)\",\"Language, region, or country\":\"言語、地域、または国\",\"Minimum online minutes\":\"最小オンライン時間(分)\",\"Maximum online minutes\":\"オンラインの最大時間数\",\"Minimum local watch minutes\":\"ローカル総再生時間の最小値\",\"Advanced filters and Boolean tags\":\"高度なフィルターとブール型タグ\",\"Saved views and feeds\":\"保存されたビューとフィード\",\"Import\":\"インポート\",\"Recent searches\":\"最近の検索\",\"Live previews and performance\":\"ライブプレビューとパフォーマンス\",\"Muted live preview for every visible card\":\"表示されているすべてのカードのミュートされたライブ プレビュー\",\"Refresh rate\":\"リフレッシュレート\",\"Real-time video while hovering\":\"ホバリング中のリアルタイムビデオ\",\"Hover delay\":\"ホバー遅延\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"可視カードは JPEG フレームを使用します。ホバーすると、1 枚のカードだけがミュートされたリアルタイム ビデオに切り替わり、ポインターが離れると完全に停止します。フレーム レートが高いと、帯域幅と CPU 使用率が大幅に増加します。\",\"saved\":\"保存されました\",\"previews\":\"プレビュー\",\"Previews off\":\"プレビューをオフにする\",\"No more feeds can be saved\":\"これ以上フィードを保存できません\",\"Tag feed saved\":\"タグフィードが保存されました\",\"Select tag\":\"タグの選択\",\"Tag favorite\":\"お気に入りにタグを付ける\",\"Block tag\":\"ブロックタグ\",\"Select tags first\":\"最初にタグを選択してください\",\"Tag-group name\":\"タググループ名\",\"Tag group saved\":\"タググループが保存されました\",\"No gaming rooms in the current catalog\":\"現在のカタログにはゲームルームはありません\",\"All (AND)\":\"すべて (AND)\",\"Any (OR)\":\"任意 (または)\",\"Exclude (NOT)\":\"除外する(しない)\",\"Search tags\":\"タグを検索\",\"Tag search\":\"タグ検索\",\"Combination\":\"組み合わせ\",\"Model sorting\":\"モデルのソート\",\"Show in Discover\":\"ディスカバーで表示\",\"Save tag group\":\"タググループを保存する\",\"Save tag feed\":\"タグフィードを保存する\",\"No tag groups saved yet.\":\"タグ グループはまだ保存されていません。\",\"Unknown region\":\"未知の領域\",\"Unknown age\":\"年齢不詳\",\"Public “New”\":\"公共の「新しい」\",\"My new cams\":\"私の新しいカメラ\",\"New-cams feed saved\":\"新しいカメラのフィードが保存されました\",\"Save “My new cams” feed\":\"「私の新しいカメラ」フィードを保存する\",\"Create gaming workspace\":\"ゲームワークスペースを作成する\",\"No matches for this combination.\":\"この組み合わせに一致するものはありません。\",\"Select tags to preview matching rooms here.\":\"ここでタグを選択して、一致する部屋をプレビューします。\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"公開タグを検索、お気に入り、ブロック、組み合わせます。トレンドでは、このデバイスで観察されたカタログの変更のみが使用されます。\",\"Current tags\":\"現在のタグ\",\"Counts show current matches and change since the prior fetch.\":\"カウントは、現在の一致と前回のフェッチ以降の変化を示します。\",\"Results for selected tags\":\"選択したタグの結果\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Discover と同じ並べ替えと SingleCam ナビゲーションを使用します。\",\"Categories, age, and regions\":\"カテゴリー、年齢、地域\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"明示的なパブリック フィールドのみが表示されます。カンマで区切られた領域は OR で結合されます。不明な値は表示されたままになります。タイムゾーンは推測されません。\",\"New and gaming\":\"新しいゲームとゲーム\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"ローカル初見、公開の「新しい」フラグ、一時的なウォッチリスト、およびゲームのテーマは分離されたままです。\",\"Tag groups\":\"タググループ\",\"Save AND, OR, or NOT combinations.\":\"AND、OR、または NOT の組み合わせを保存します。\",\"Favorite tags\":\"お気に入りのタグ\",\"Your personal shortcut list.\":\"あなただけのショートカットリスト。\",\"No favorites yet\":\"まだお気に入りはありません\",\"Related tags\":\"関連タグ\",\"Tags co-occurring on the currently matching models.\":\"現在一致しているモデル上で同時に発生するタグ。\",\"Personal tag history\":\"個人タグ履歴\",\"Recently and frequently used on this device.\":\"このデバイスで最近、頻繁に使用されています。\",\"Bulk target group\":\"一括ターゲットグループ\",\"Selected models added to group\":\"選択したモデルがグループに追加されました\",\"New workspace name\":\"新しいワークスペース名\",\"Created from a Discover selection\":\"Discover の選択から作成\",\"Workspace created\":\"ワークスペースが作成されました\",\"Save all\":\"すべて保存\",\"Create workspace\":\"ワークスペースの作成\",\"Loading public catalog…\":\"公開カタログを読み込み中…\",\"Loading the complete catalog in the background…\":\"完全なカタログをバックグラウンドでロードしています…\",\"Updated\":\"更新されました\",\"Public rooms\":\"パブリックルーム\",\"The catalog is loading.\":\"カタログを読み込んでいます。\",\"No matching rooms. Adjust search or filters.\":\"一致する部屋はありません。検索またはフィルターを調整します。\",\"Previous\":\"前へ\",\"Next\":\"次へ\",\"Move up\":\"上に移動\",\"Move down\":\"下に移動\",\"Number shown\":\"表示されている番号\",\"Widget width\":\"ウィジェットの幅\",\"Full\":\"フル\",\"Nothing here yet.\":\"ここにはまだ何もありません。\",\"Newly online\":\"新たにオンライン化\",\"Recently visited\":\"最近訪問した\",\"Frequently visited\":\"よく訪れる\",\"Homepage appearance, pagination, and performance\":\"ホームページの外観、ページネーション、パフォーマンス\",\"Information density\":\"情報密度\",\"Layout\":\"レイアウト\",\"Data rich\":\"豊富なデータ\",\"Static\":\"静的\",\"Show statistics summary\":\"統計の概要を表示\",\"Show personal widgets\":\"個人用ウィジェットを表示する\",\"Sections below the live catalog\":\"ライブカタログの下のセクション\",\"Homepage live models\":\"ホームページのライブモデル\",\"All live rooms in one place\":\"すべてのライブルームを 1 か所に\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Discover と同じ完全な検索、並べ替え、グループ、タグ、およびローカル履歴フィルターを使用します。\",\"Refresh live catalog\":\"ライブカタログを更新\",\"Homepage settings\":\"ホームページの設定\",\"Matches a favorite gaming theme\":\"お気に入りのゲームテーマと一致\",\"Saved models\":\"保存されたモデル\",\"Online favorites\":\"オンラインのお気に入り\",\"Local watch time\":\"ローカル総再生時間\",\"Continue last MultiCam workspace\":\"最後の MultiCam ワークスペースを続行\",\"Open full Discover page\":\"Discover ページ全体を開く\",\"Online notifications\":\"オンライン通知\",\"on\":\"に\",\"off\":\"オフ\",\"Offline notifications\":\"オフライン通知\",\"Feed\":\"飼料\",\"Recent\":\"最近の\",\"Frequent\":\"頻繁に\",\"Saved filter views\":\"保存されたフィルタービュー\",\"Favorites currently public and online.\":\"現在公開されているオンラインのお気に入り。\",\"New models or public streams that started recently.\":\"最近開始された新しいモデルまたは公開ストリーム。\",\"Current results from the selected feed.\":\"選択したフィードからの現在の結果。\",\"Suggestions with reasons\":\"理由付きの提案\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"共有された公開タグとローカルで人気のテーマのみに基づいています。アイデンティティの推論はありません。\",\"Sorted by local last-visit time.\":\"現地の最終訪問時間順に並べ替えられます。\",\"Sorted by local watch time.\":\"現地の総再生時間順に並べています。\",\"Saved models never visited\":\"保存されたモデルはアクセスされませんでした\",\"Saved in the library without local watch time.\":\"ローカル視聴時間なしでライブラリに保存されました。\",\"Draggable dashboard widgets\":\"ドラッグ可能なダッシュボード ウィジェット\",\"Your RoomGrid homepage\":\"あなたの RoomGrid ホームページ\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"SingleCam、MultiCam、完全なフィルター、保存されたビュー、個人用ウィジェットを備えた高速でカスタマイズ可能なライブ カタログ。\",\"Audio activity\":\"音声アクティビティ\",\"Auto quality\":\"自動品質\",\"Stream interrupted; status checks continue.\":\"ストリームが中断されました。ステータスチェックは続行されます。\",\"This browser cannot play HLS.\":\"このブラウザでは HLS を再生できません。\",\"Connecting to live stream…\":\"ライブストリームに接続中…\",\"The room is not public right now.\":\"現在この部屋は公開されていません。\",\"Currently offline. Alternatives from the same filter are shown below.\":\"現在オフラインです。同じフィルターの代替を以下に示します。\",\"Status temporarily unavailable; retrying automatically.\":\"ステータスは一時的に利用できません。自動的に再試行します。\",\"Loading public profile…\":\"公開プロフィールを読み込み中…\",\"Profile temporarily unavailable.\":\"プロファイルは一時的に利用できません。\",\"No cached profile.\":\"キャッシュされたプロファイルはありません。\",\"Public profile changed\":\"公開プロフィールが変更されました\",\"No public bio.\":\"公開プロフィールはありません。\",\"Source\":\"ソース\",\"Observed\":\"観察された\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"公開プロフィールで自己公開されたリンクのみが表示されます。ユーザー名が一致するだけでは身元の証明にはなりません。\",\"Profile information\":\"プロフィール情報\",\"Preparing…\":\"準備中…\",\"Play\":\"遊ぶ\",\"Unmute\":\"ミュートを解除する\",\"Mute\":\"ミュート\",\"Volume\":\"ボリューム\",\"Contain\":\"含む\",\"Cover\":\"カバー\",\"Brightness\":\"明るさ\",\"Contrast\":\"コントラスト\",\"Saturation\":\"彩度\",\"Hue\":\"色相\",\"Sepia\":\"セピア\",\"Grayscale\":\"グレースケール\",\"Invert\":\"反転\",\"Blur\":\"ぼかし\",\"Image zoom\":\"画像のズーム\",\"Recording saved\":\"録音が保存されました\",\"Screenshot saved\":\"スクリーンショットが保存されました\",\"Recording\":\"録音\",\"Mirror\":\"鏡\",\"Flip vertically\":\"垂直方向に反転\",\"Rotate\":\"回転\",\"Show edited\":\"編集済みを表示\",\"Show original\":\"オリジナルを表示\",\"Reset image\":\"画像をリセット\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Ctrl を押したままスクロールしてズームします。ズーム時に画像をドラッグします。ダブルクリックしてリセットします。\",\"Zoom out\":\"ズームアウト\",\"Zoom in\":\"ズームイン\",\"Reset zoom and position\":\"ズームと位置をリセットする\",\"Player window\":\"プレーヤーウィンドウ\",\"Fullscreen\":\"フルスクリーン\",\"Private notes, memories, or things to check next time (stored locally)\":\"個人的なメモ、思い出、または次回確認するもの (ローカルに保存)\",\"Autosave is on\":\"自動保存がオンになっています\",\"Enter your own tag…\":\"独自のタグを入力してください…\",\"Saved at\":\"保存場所\",\"Local details saved\":\"ローカルの詳細が保存されました\",\"Remove tag\":\"タグを削除する\",\"No personal tags yet.\":\"個人タグはまだありません。\",\"Saving…\":\"保存中…\",\"Create your first group under Groups.\":\"「グループ」の下に最初のグループを作成します。\",\"Added to MultiCam\":\"マルチカムに追加されました\",\"Edit private details\":\"プライベートの詳細を編集する\",\"Trans\":\"トランス\",\"Region\":\"地域\",\"Current online time\":\"現在のオンライン時間\",\"Stream attributes\":\"ストリーム属性\",\"New\":\"新しい\",\"Yes\":\"はい\",\"No / unknown\":\"いいえ/不明\",\"Last observed\":\"最後に観測された\",\"Local visits\":\"現地訪問\",\"First watched\":\"初めて見た\",\"No public description.\":\"公開説明はありません。\",\"No public tags available.\":\"利用可能な公開タグはありません。\",\"Quick-add your own tag…\":\"独自のタグを簡単に追加…\",\"Public tags\":\"パブリックタグ\",\"Add\":\"追加\",\"Private details\":\"個人情報の詳細\",\"Everything stays in this browser and survives updates.\":\"すべてがこのブラウザー内に残り、更新されても存続します。\",\"Private notebook\":\"私用ノート\",\"Keep on watchlist\":\"ウォッチリストに入れ続ける\",\"Group assignment (multiple)\":\"グループ割り当て(複数)\",\"Save now\":\"今すぐ保存\",\"Chat loads only when this section is opened.\":\"チャットは、このセクションを開いたときにのみ読み込まれます。\",\"Official chat\":\"公式チャット\",\"Official chat connected.\":\"公式チャットがつながりました。\",\"Chat loaded; the current page structure could not be fully isolated.\":\"チャットがロードされました。現在のページ構造を完全に分離できませんでした。\",\"Chat loaded.\":\"チャットが読み込まれました。\",\"Loading official chat…\":\"公式チャットを読み込み中…\",\"Official chat room\":\"公式チャットルーム\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"必要な場合にのみロードされます。 RoomGrid はチャット メッセージを保存したり、自動的に送信したりすることはありません。\",\"Reload chat\":\"チャットをリロードする\",\"Open official room in a new window\":\"オフィシャルルームを新しいウィンドウで開く\",\"Hide chat\":\"チャットを非表示にする\",\"Live observations\":\"ライブ観測\",\"Trend confidence\":\"トレンドの信頼度\",\"Known time slots\":\"既知のタイムスロット\",\"Typical peak\":\"典型的なピーク\",\"Watched in 7 days\":\"7日間で視聴しました\",\"Watched in 30 days\":\"30日以内に視聴しました\",\"Current streak\":\"現在のストリーク\",\"Typical weekly online times\":\"通常の毎週のオンライン時間\",\"Online times will be learned on this device from now on.\":\"今後、オンライン時間はこのデバイスで学習されます。\",\"Based on observations on this device; missing information is never guessed.\":\"このデバイスの観察に基づいています。欠落した情報が推測されることはありません。\",\"Activity by weekday\":\"平日のアクティビティ\",\"Most likely online times\":\"最も可能性の高いオンライン時間\",\"Online-time map\":\"オンライン時のマップ\",\"Public covers and locally saved example images.\":\"公開されているカバーとローカルに保存されたサンプル画像。\",\"Import quality\":\"輸入品質\",\"Original file\":\"元のファイル\",\"Very high (up to 4K)\":\"非常に高い (最大 4K)\",\"High (up to 1440p)\":\"高 (最大 1440p)\",\"Space-saving (up to 900p)\":\"省スペース (最大 900p)\",\"Add images from computer\":\"パソコンから画像を追加する\",\"Use website cover\":\"ウェブサイトのカバーを使用する\",\"Use first image as cover\":\"最初の画像をカバーとして使用する\",\"Covers and local gallery\":\"表紙と地元のギャラリー\",\"Updating images…\":\"画像を更新中…\",\"Loading images…\":\"画像を読み込み中…\",\"Own image\":\"自分のイメージ\",\"Example image\":\"画像例\",\"Current cover\":\"現在のカバー\",\"Alternative preview\":\"代替プレビュー\",\"Current public image\":\"現在のパブリックイメージ\",\"Saving cover…\":\"カバーを保存中…\",\"Set as cover\":\"カバーとして設定\",\"Delete this image from the local gallery?\":\"この画像をローカル ギャラリーから削除しますか?\",\"No images yet.\":\"まだ画像がありません。\",\"saved locally\":\"ローカルに保存された\",\"own images\":\"自分のイメージ\",\"Update public previews\":\"パブリック プレビューを更新する\",\"Processing your images…\":\"画像を処理しています…\",\"ALL PAGE SOUNDS OFF\":\"すべてのページの音がオフになる\",\"PAGE SOUNDS ALLOWED\":\"ページサウンドは許可されています\",\"Chat/token sounds OFF\":\"チャット/トークン音オフ\",\"Chat/token sounds ON\":\"チャット/トークン音オン\",\"Reconnecting the player with the new settings…\":\"新しい設定でプレーヤーを再接続しています…\",\"Turn off chat, token and tip sounds completely\":\"チャット、トークン、チップの音を完全にオフにする\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"オーディオ要素と Web オーディオからのページ サウンドをブロックします。ストリームオーディオは、「サウンドオン」を通じて個別に制御できます。\",\"No alternatives available.\":\"代替手段はありません。\",\"Similar models and offline alternatives\":\"同様のモデルとオフラインの代替品\",\"Prioritized by shared tags; no identity or external-account inference.\":\"共有タグによって優先順位が付けられます。 ID や外部アカウントの推論はありません。\",\"Chat\":\"チャット\",\"Notes & organization\":\"メモと整理\",\"Images\":\"画像\",\"SingleCam sections\":\"シングルカムセクション\",\"Back to original results\":\"元の結果に戻る\",\"Private workspace\":\"プライベートワークスペース\",\"Chat on/off\":\"チャットのオン/オフ\",\"Hide similar models\":\"類似モデルを非表示にする\",\"Show similar models\":\"類似モデルを表示\",\"RoomGrid Home\":\"ルームグリッドホーム\",\"RoomGrid Tags\":\"ルームグリッド Tags\",\"Public catalog unavailable; showing the local library.\":\"公開カタログは利用できません。地元の図書館を紹介しています。\",\"Drag to resize sidebar\":\"ドラッグしてサイドバーのサイズを変更します\",\"The workspace could not be saved safely. Export a backup now.\":\"ワークスペースを安全に保存できませんでした。今すぐバックアップをエクスポートしてください。\",\"Open menu or restore controls\":\"メニューを開くかコントロールを復元する\",\"Controls restored\":\"コントロールが復元されました\",\"Menu\":\"メニュー\",\"Show toolbar\":\"ツールバーを表示\",\"Tools\":\"ツール\",\"Show sidebar\":\"サイドバーを表示\",\"Restore controls\":\"コントロールを復元する\",\"Show toolbar, groups, and menus\":\"ツールバー、グループ、メニューを表示する\",\"Open more tools\":\"さらにツールを開く\",\"all watch states\":\"すべての監視状態\",\"Watch status\":\"監視ステータス\",\"0 means no viewer limit.\":\"0 は視聴者制限がないことを意味します。\",\"Import temporary URLs; they disappear after refresh\":\"一時的な URL をインポートします。更新すると消えます\",\"Filter current group\":\"現在のグループをフィルタリングする\",\"All status\":\"すべてのステータス\",\"Hide offline/private\":\"オフライン/プライベートを非表示にする\",\"Collapse toolbar\":\"ツールバーを折りたたむ\",\"Windows per page\":\"ページごとのウィンドウ\",\"Custom grid\":\"カスタムグリッド\",\"Choose MultiCam audio routing\":\"MultiCamオーディオルーティングを選択してください\",\"Audio: manual\":\"オーディオ: マニュアル\",\"Audio: solo cam\":\"オーディオ: ソロカム\",\"Audio: follow focus\":\"オーディオ: フォーカスに従う\",\"Auto\":\"自動\",\"Userscript version\":\"ユーザースクリプトのバージョン\",\"Open Discover and dashboard\":\"Discover とダッシュボードを開く\",\"Global search and quick commands · Ctrl+K\":\"グローバル検索とクイックコマンド · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"ストリームの健全性、品質予算、オーディオ、シーン、置換\",\"Control\":\"制御\",\"Rooms\":\"ルーム\",\"Filter\":\"フィルター\",\"Playback\":\"再生\",\"Stabilizing live preview…\":\"ライブ プレビューを安定化させています…\",\"Recovering live preview…\":\"ライブ プレビューを回復しています…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"ライブ プレビューは、リロード ループを防ぐために一時停止されます。しばらくしてからもう一度お試しください。\",\"Collapse groups\":\"グループを折りたたむ\",\"Expand/collapse users\":\"ユーザーを展開/折りたたむ\",\"Output volume; no live signal level is available for this stream\":\"出力ボリューム。このストリームでは利用可能なライブ信号レベルがありません\",\"OFF\":\"オフ\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"URLを貼り付けます。ルームページ、m3u8、mp4/webm がサポートされています。通常のページはベストエフォートで解析されます。\",\"Actual resolution, connection and buffer\":\"実際の解像度、接続、バッファ\",\"Drag to reorder / move group\":\"ドラッグしてグループを並べ替え/移動します\",\"All time\":\"ずっと\",\"Click a model for details.\":\"詳細については、モデルをクリックしてください。\",\"Most watched groups\":\"最も視聴されているグループ\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"参加日などのアカウントフィールドは、Chaturbate が同じページに明確に公開している場合にのみ表示されます。スクリプトは値を作成しません。\",\"Back\":\"戻る\",\"Statistics import failed: \":\"統計のインポートに失敗しました:\",\"Recent saves\":\"最近の保存\",\"Rooms per page\":\"ページあたりの部屋数\",\"Custom rows\":\"カスタム行\",\"Custom columns\":\"カスタム列\",\"Main width\":\"メイン幅\",\"Main height\":\"主な高さ\",\"Modules\":\"モジュール\",\"Modules start only on matching page types. Saving reloads the current page.\":\"モジュールは、一致するページ タイプでのみ開始されます。保存すると、現在のページがリロードされます。\",\"Tools on normal pages\":\"通常ページのツール\",\"True audio-level meters\":\"真のオーディオレベルメーター\",\"MultiCam control center\":\"マルチカムコントロールセンター\",\"No active streams yet\":\"アクティブなストリームはまだありません\",\"Stream health\":\"ストリームの健全性\",\"Budget off\":\"予算をオフにする\",\"Economy · 2160p total\":\"エコノミー・合計2160p\",\"Balanced · 4320p total\":\"バランス済み・合計4320p\",\"High · 6480p total\":\"高・合計6480p\",\"Ultra · 8640p total\":\"ウルトラ・合計8640p\",\"Quality budget is off; global and per-camera limits still apply.\":\"品質予算はオフです。グローバル制限とカメラごとの制限は引き続き適用されます。\",\"Quality budget\":\"品質予算\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"アクティブなカム全体に品質を分散します。明示的なカメラごとの品質が常に優先されます。\",\"Give the focused cam double weight\":\"フォーカスされたカムに 2 倍の重みを与える\",\"Manual per camera\":\"カメラごとのマニュアル\",\"Solo: one camera only\":\"ソロ: カメラ 1 台のみ\",\"Follow focused camera\":\"焦点を合わせたカメラを追跡する\",\"Audio management\":\"オーディオ管理\",\"Routing never overwrites saved mute states or individual volume.\":\"ルーティングによって、保存されたミュート状態や個々のボリュームが上書きされることはありません。\",\"Mute all\":\"すべてミュート\",\"Restore saved audio\":\"保存したオーディオを復元する\",\"Scene name\":\"シーン名\",\"Focus\":\"フォーカス\",\"Grid\":\"グリッド\",\"cams\":\"カム\",\"Load\":\"ロード\",\"No scenes saved for this workspace yet.\":\"このワークスペースにはまだシーンが保存されていません。\",\"Scenes\":\"シーン\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"現在のスロット、レイアウト、オーディオ ルーティングを保存して、ワンクリックで復元できます。\",\"Save current view as scene\":\"現在のビューをシーンとして保存\",\"All saved online models\":\"保存されたすべてのオンライン モデル\",\"Replacement models\":\"代替機種\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"優先モデルはリストの順序で試行されます。その場合にのみ、選択したグループのオンライン モデルが使用されます。手動割り当てが常に優先されます。\",\"Replace offline slots automatically\":\"オフラインスロットを自動的に置き換える\",\"Delay (seconds)\":\"遅延 (秒)\",\"Candidate group\":\"候補者グループ\",\"Preferred replacements (multiple)\":\"優先代替品 (複数)\",\"Replacement rule saved\":\"置換ルールが保存されました\",\"Save replacement rule\":\"置換ルールの保存\",\"Resolve duplicate shortcuts first:\":\"まず重複したショートカットを解決します。\",\"Name, note or own tag…\":\"名前、メモ、または独自のタグ…\",\"Camera menu\":\"カメラメニュー\",\"Camera menu · quality and sound\":\"カメラメニュー・画質とサウンド\",\"Close menu\":\"メニューを閉じる\",\"View and window\":\"景色と窓\",\"Audio\":\"オーディオ\",\"Quality and image\":\"品質とイメージ\",\"More actions\":\"さらなるアクション\",\"Currently main\":\"現在のメイン\",\"Set as main\":\"メインに設定\",\"Pin live preview\":\"ライブプレビューをピン留めする\",\"Volume multiplier for this cam\":\"このカムのボリューム乗数\",\"Reset individual volume to 100%\":\"個別の音量を 100% にリセット\",\"Individual volume\":\"個別ボリューム\",\"Listen to this cam only\":\"このカメラのみを聴く\",\"Set quality for this camera only\":\"このカメラのみの品質を設定する\",\"Automatic (global/adaptive)\":\"自動 (グローバル/適応)\",\" · available\":\"· 利用可能\",\"Quality for this camera\":\"このカメラの品質\",\"Hold for original\":\"オリジナルを保持する\",\"Vivid\":\"鮮やかな\",\"Mono\":\"モノラル\",\"Image adjustments\":\"画像調整\",\"More tools\":\"その他のツール\",\"Interface\":\"インターフェース\",\"Show top controls\":\"トップコントロールを表示\",\"Collapse top controls\":\"上部のコントロールを折りたたむ\",\"Show groups\":\"グループを表示\",\"Windows\":\"窓\",\"Open workspace in new window\":\"新しいウィンドウでワークスペースを開く\",\"Control center\":\"コントロールセンター\",\"Choose the data to export\":\"エクスポートするデータを選択してください\",\"Preview the file and conflicts before applying it\":\"適用する前にファイルと競合をプレビューする\",\"Help\":\"ヘルプ\",\"Changes in this version\":\"このバージョンでの変更点\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"ワークスペース、モデル ライブラリ、安全なインポート、グローバル デザイン、拡張された通常ページ、拡張された Discover/ダッシュボード、およびプレーヤーの品質/画像コントロール。\",\"Muted\":\"ミュート\",\"Performance limit reached\":\"パフォーマンスの限界に達しました\",\"Drag to resize main width\":\"ドラッグしてメインの幅を変更します\",\"Drag to resize main height\":\"ドラッグしてメインの高さを変更します\",\"Stable\":\"安定した\",\"Unstable\":\"不安定\",\"Refresh all cameras\":\"すべてのカメラを更新します\",\"Reconnect current models\":\"現在のモデルを再接続する\",\"Switch to grid view\":\"グリッドビューに切り替える\",\"Switch to focus view\":\"フォーカスビューに切り替える\",\"Manage workspaces\":\"ワークスペースの管理\",\"Create, duplicate, archive and device layouts\":\"レイアウトの作成、複製、アーカイブ、デバイスレイアウト\",\"Open model library\":\"モデルライブラリを開く\",\"Notes, tags, ratings and bulk actions\":\"メモ、タグ、評価、一括操作\",\"Design options\":\"デザインオプション\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"ルーム、記録、アラート、スクリーンショット、再接続、データ ツールを備えたオールインワンのマルチビュー ワークステーション。\",\"Username ↵\":\"ユーザー名 ↵\",\"Search rooms\":\"部屋を検索する\",\"Invalid username\":\"無効なユーザー名\",\"Hide offline\":\"オフラインで非表示にする\",\"Hide private\":\"プライベートを隠す\",\"Manual\":\"マニュアル\",\"By status\":\"ステータス別\",\"By name\":\"名前で\",\"By added time\":\"追加時間までに\",\"By watch time\":\"視聴時間別\",\"By sessions\":\"セッションごと\",\"By last watched\":\"最後に視聴した日までに\",\"By first watched\":\"初めて見た人によって\",\"By viewer count\":\"視聴者数別\",\"By last online\":\"最後にオンラインになった時点で\",\"Advanced filters\":\"高度なフィルター\",\"Watched before\":\"前に見た\",\"Currently in view\":\"現在表示中\",\"Include tag\":\"タグを含める\",\"Exclude tag\":\"タグを除外する\",\"Minimum watch time (minutes)\":\"最小視聴時間(分)\",\"Watched within days\":\"数日以内に視聴した\",\"Refresh all\":\"すべて更新\",\"Online alerts\":\"オンラインアラート\",\"Offline alerts\":\"オフラインアラート\",\"Desktop notification + card flash when a model goes online\":\"モデルがオンラインになるとデスクトップ通知 + カードがフラッシュされます\",\"Desktop notification + card flash when a model goes offline\":\"モデルがオフラインになったときのデスクトップ通知 + カードのフラッシュ\",\"Dark mode\":\"ダークモード\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"ダークインターフェイステーマを使用します。ストリームのビデオとオーディオは変更されません。\",\"Dark mode enabled\":\"ダークモード有効\",\"Light mode enabled\":\"ライトモード有効\",\"Mute token/tip sounds\":\"トークンとチップの音を消す\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"トークンとチップの通知音だけを消します。配信音声はそのままです。\",\"Token/tip sounds muted\":\"トークンとチップの音をミュートしました\",\"Token/tip sounds enabled\":\"トークンとチップの音を有効にしました\",\"Clean\":\"クリーン\",\"Clean mode on\":\"クリーンモードオン\",\"Exit clean mode\":\"クリーンモードを終了する\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"すべてのコントロールとオーバーレイを一時的に非表示にします。ショートカット: Alt+P / Alt+C。 Esc キーを押して終了します。\",\"Window first\":\"ウィンドウファースト\",\"Window-first mode on\":\"ウィンドウファーストモードがオン\",\"Exit window-first\":\"ウィンドウを最初に終了する\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"アプリ シェルを非表示にし、ルーム ウィンドウを表示したままにします。ショートカット: Alt+V。\",\"Show thumbnails\":\"サムネイルを表示する\",\"Hide thumbnails\":\"サムネイルを非表示にする\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"フォーカス モードでサムネイル レールを表示または非表示にします。ショートカット: Alt+T。\",\"Choose Focus rooms\":\"フォーカスルームを選択してください\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"フォーカス モードの部屋を選択します。最初に選択した部屋がメイン画面です。 ↑/↓ を使用して 2 番目のウィンドウの順序を設定します。\",\"Main screen\":\"メイン画面\",\"Right window\":\"右の窓\",\"Bottom window\":\"下の窓\",\"Empty slot\":\"空のスロット\",\"Drag this model to a Focus slot\":\"このモデルをフォーカス スロットにドラッグします\",\"Choose model for this slot\":\"このスロットのモデルを選択してください\",\"Select a saved model for this exact Focus slot.\":\"この正確なフォーカス スロット用に保存されたモデルを選択します。\",\"Choose model for this window\":\"このウィンドウのモデルを選択してください\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"このグリッド ウィンドウに保存されているモデルを選択します。既存のモデルは保存されたままになります。\",\"Remove from this grid window\":\"このグリッド ウィンドウから削除します\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"このスロットのみがクリアされます。保存されたモデルはワークスペースに残ります。\",\"Show in grid window\":\"グリッドウィンドウに表示\",\"Choose the exact active grid window for this model.\":\"このモデルに適したアクティブなグリッド ウィンドウを選択します。\",\"Current\":\"現在\",\"Model removed from this grid window.\":\"このグリッド ウィンドウからモデルが削除されました。\",\"No saved models in this group.\":\"このグループには保存されたモデルがありません。\",\"Secondary window\":\"セカンダリウィンドウ\",\"Use current page automatically\":\"現在のページを自動的に使用する\",\"Clear custom selection\":\"カスタム選択をクリア\",\"Apply Focus layout\":\"フォーカス レイアウトを適用する\",\"No rooms are available in the current group and filter.\":\"現在のグループとフィルターには利用可能な部屋がありません。\",\"Fit: full image\":\"フィット: フル画像\",\"Fit: fill window\":\"フィット: ウィンドウを埋める\",\"Switch between full image and cropped fill.\":\"フルイメージとトリミングされた塗りつぶしを切り替えます。\",\"Playback settings\":\"再生設定\",\"Max stream quality\":\"最高のストリーム品質\",\"Auto / no cap\":\"オート/キャップなし\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + ホイールズーム\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Ctrl/Command + マウス ホイールを使用してカードをズームし、ズーム中にドラッグし、ダブルクリックしてリセットします。\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"クリーンモードを終了します。ショートカット: Alt+P / Alt+C または Esc。\",\"Clean · Alt+P/C / Esc\":\"クリーン · Alt+P/C / Esc\",\"(synced from another tab)\":\"(別のタブから同期)\",\"Type a username and press Enter. Shortcut: /\":\"ユーザー名を入力して Enter キーを押します。ショートカット: /\",\"Filter visible rooms by username\":\"表示されているルームをユーザー名でフィルタリングする\",\"Show / hide group sidebar\":\"グループサイドバーの表示/非表示\",\"Global volume. Per-room mute is still respected.\":\"グローバルボリューム。部屋ごとのミュートは引き続き尊重されます。\",\"Base tile size in grid view\":\"グリッドビューのベースタイルのサイズ\",\"Sort rooms in the current group\":\"現在のグループ内のルームを並べ替えます\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"フォーカス モードでのメイン画面の高さ。セパレータをドラッグすることもできます。\",\"Main-screen aspect ratio in Focus mode\":\"フォーカスモードでのメイン画面のアスペクト比\",\"Thumbnail width in Focus mode\":\"フォーカスモードでのサムネイルの幅\",\"Show every saved room. The remove button deletes globally in this view.\":\"保存されているすべての部屋を表示します。削除ボタンは、このビュー内でグローバルに削除します。\",\"Create an isolated group\":\"隔離されたグループを作成する\",\"More tools and maintenance\":\"追加のツールとメンテナンス\",\"GROUPS\":\"グループ\",\"All saved\":\"すべて保存されました\",\"Default\":\"デフォルト\",\"Online now\":\"現在オンライン中\",\"New group name:\":\"新しいグループ名:\",\"Rename\":\"名前の変更\",\"Rename to:\":\"名前を次のように変更します:\",\"Delete group\":\"グループの削除\",\"Live preview\":\"ライブプレビュー\",\"Loading preview…\":\"プレビューを読み込み中…\",\"Private or away\":\"プライベートまたは外出中\",\"Preview could not be loaded\":\"プレビューを読み込めませんでした\",\"Toggle mute\":\"ミュートの切り替え\",\"Resume\":\"再開\",\"Screenshot current frame\":\"現在のフレームのスクリーンショット\",\"Record current card\":\"現在のカードを記録する\",\"Stop recording\":\"録音を停止する\",\"Picture-in-Picture\":\"ピクチャー・イン・ピクチャー\",\"Remove from current group\":\"現在のグループから削除\",\"Delete saved room\":\"保存したルームを削除する\",\"Mirror image\":\"鏡像\",\"Flip image\":\"画像を反転\",\"Rotate left\":\"左に回転\",\"Rotate right\":\"右回転\",\"Reset view\":\"ビューをリセット\",\"Copy username\":\"ユーザー名をコピーする\",\"Add favorite\":\"お気に入りを追加\",\"Move to favorites only\":\"お気に入りのみに移動\",\"Copied\":\"コピーされました\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"スクリーンショットに失敗しました。ブラウザ/CORS がこのストリームの描画をブロックする可能性があります。\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"録画はローカルで行われ、ブラウザーがこのビデオを公開すると、一致するオーディオとともにこのビデオが保存されます。このコンテンツを保存する許可がある場合にのみ使用してください。続く?\",\"Recording started\":\"録音開始\",\"Recording is not supported for this video/browser\":\"このビデオ/ブラウザでは録画がサポートされていません\",\"Recording segment saved\":\"録画セグメントが保存されました\",\"Final recording segment saved\":\"最終的な録画セグメントが保存されました\",\"Recording paused; waiting for the stream to return\":\"録音が一時停止されました。ストリームが戻るのを待っています\",\"Recording resumed\":\"録音再開\",\"Recording paused; click to stop\":\"録音が一時停止されました。クリックして停止します\",\"No recording data to save\":\"保存する録画データがありません\",\"Recording settings saved\":\"録音設定が保存されました\",\"Segment length in minutes (1-180):\":\"分単位のセグメントの長さ (1 ~ 180):\",\"Video bitrate in Mbps (0.5-20):\":\"ビデオ ビットレート (Mbps) (0.5 ~ 20):\",\"Warn before leaving while recording\":\"録音中に離れる前に警告する\",\"Recording is still active. Leave anyway?\":\"記録はまだアクティブです。とにかく出発しますか?\",\"Recording center\":\"レコーディングセンター\",\"No active recordings\":\"アクティブな録音はありません\",\"Waiting for source\":\"ソースを待っています\",\"Saved segments\":\"保存されたセグメント\",\"Duration\":\"期間\",\"Bitrate\":\"ビットレート\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"形式: MP4 (このブラウザでサポートされている場合)。それ以外の場合は、最もサポートされているフォールバックです。\",\"Record current page\":\"現在のページを記録する\",\"Record current group\":\"現在のグループを記録する\",\"Record online rooms\":\"オンラインルームを記録する\",\"Stop all recordings\":\"すべての録音を停止する\",\"Show recording only\":\"録画のみを表示\",\"Show all rooms\":\"すべての部屋を表示する\",\"Open current page rooms\":\"現在のページのルームを開く\",\"Move current page to group\":\"現在のページをグループに移動\",\"Layout settings\":\"レイアウト設定\",\"Recording settings\":\"録音設定\",\"Save settings\":\"設定を保存する\",\"Config backups\":\"構成のバックアップ\",\"No config backups found\":\"構成バックアップが見つかりません\",\"Online/offline history\":\"オンライン/オフライン履歴\",\"Group rules\":\"グループルール\",\"Keep favorites first\":\"最初にお気に入りを保存します\",\"Temporary URL manager\":\"一時 URL マネージャー\",\"Save current temporary URLs\":\"現在の一時 URL を保存する\",\"No temporary URLs\":\"一時的な URL はありません\",\"Share workspace\":\"ワークスペースを共有する\",\"Share current rooms and layout\":\"現在の部屋とレイアウトを共有する\",\"Copy share link\":\"共有リンクをコピーする\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"このリンクにより、ルーム、グループ、レイアウト設定が別のワークステーション タブに復元されます。\",\"Shared workspace imported\":\"共有ワークスペースがインポートされました\",\"Shared workspace link is invalid\":\"共有ワークスペースのリンクが無効です\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"フィールドをクリックして、新しいショートカットを押します。バックスペースキーを押すとそのフィールドがクリアされます。\",\"Reset shortcuts\":\"ショートカットをリセットする\",\"Focus add box\":\"フォーカス追加ボックス\",\"Grid view\":\"グリッドビュー\",\"Focus view\":\"フォーカスビュー\",\"Clean mode\":\"クリーンモード\",\"Window-first mode\":\"ウィンドウファーストモード\",\"Toggle thumbnails\":\"サムネイルの切り替え\",\"Auto-detecting…\":\"自動検出中…\",\"No rooms in this group\":\"このグループには部屋がありません\",\"Add a username at the top, switch group, or clear filters\":\"上部にユーザー名を追加するか、グループを切り替えるか、フィルターをクリアします\",\"Open workstation\":\"オープンワークステーション\",\"Open here (replace page)\":\"ここを開きます(ページを置き換えます)\",\"Replace this page with the workstation? You can go back via the browser.\":\"このページをワークステーションに置き換えますか?ブラウザから戻ることができます。\",\"View saved list\":\"保存されたリストを表示する\",\"Collapse dock\":\"ドックを折りたたむ\",\"No room detected\":\"部屋が検出されませんでした\",\"Open here\":\"ここを開きます\",\"Add room\":\"部屋を追加する\",\"Remove room\":\"部屋を削除する\",\"Record in workstation\":\"ワークステーションでの記録\",\"Screenshot video\":\"スクリーンショットビデオ\",\"Play / pause\":\"再生/一時停止\",\"Mute / unmute\":\"ミュート/ミュート解除\",\"No playable video found on this page\":\"このページには再生可能なビデオが見つかりません\",\"Recording intent queued in workstation\":\"ワークステーションでキューに入れられた録音インテント\",\"Added\":\"追加されました\",\"Already exists\":\"すでに存在します\",\"Failed\":\"失敗しました\",\"Add to workstation\":\"ワークステーションに追加\",\"Already in workstation — click to remove\":\"すでにワークステーション内にあります - クリックして削除します\",\"Model online\":\"オンラインでモデルを作成する\",\"Model offline\":\"オフラインでモデルを作成する\",\"Show fewer\":\"表示数を減らす\",\"No measurable audio at the moment\":\"現時点では測定可能な音声はありません\",\"Playback paused\":\"再生が一時停止されました\",\"Audio measurement unavailable or blocked\":\"音声測定が利用できないかブロックされています\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"通知の許可が拒否されました。カードフラッシュはまだ動作します。\\n\\nブラウザの設定で有効にすることができます。\",\"Batch add\":\"一括追加\",\"Paste usernames, one per line or separated by spaces/commas:\":\"ユーザー名を 1 行に 1 つずつ、またはスペース/カンマで区切って貼り付けます。\",\"Cancel\":\"キャンセル\",\"Change the language of RoomGrid and the website\":\"RoomGridとWebサイトの言語を変更する\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"RoomGrid の言語を変更します。公式サイト言語が利用できないため、Chaturbate は現在の Web サイト言語のままです。\",\"About\":\"について\",\"Export config\":\"設定のエクスポート\",\"Export usernames.txt\":\"ユーザー名.txtをエクスポートする\",\"Copy usernames\":\"ユーザー名をコピーする\",\"Unmute all\":\"すべてのミュートを解除する\",\"⏸ Pause visible\":\"⏸ 一時停止が表示されます\",\"Resume visible\":\"履歴書が表示されます\",\"Following\":\"フォロー中\",\"Sync followed models now\":\"フォローされているモデルを今すぐ同期します\",\"✓ Auto-sync followed models\":\"✓ フォローされたモデルを自動同期\",\"☐ Auto-sync followed models\":\"☐ フォローされたモデルを自動同期します\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"現在フォローしている Chaturbate モデルを永続的なフォロー グループにインポートします。そのグループのみが同期されます。\",\"Syncing followed models…\":\"フォローされているモデルを同期しています…\",\"Automatic Following sync disabled\":\"自動フォロー同期が無効になっています\",\"Automatic Following sync enabled\":\"自動フォロー同期が有効になりました\",\"Following sync needs an active Chaturbate login\":\"次の同期にはアクティブな Chaturbate ログインが必要です\",\"Following sync failed safely; saved workspace data was not changed\":\"次の同期は安全に失敗しました。保存されたワークスペース データは変更されませんでした\",\"Enable dark mode\":\"ダークモードを有効にする\",\"Disable dark mode\":\"ダークモードを無効にする\",\"Status history\":\"ステータス履歴\",\"Temporary URLs\":\"一時的な URL\",\"Shortcut panel\":\"ショートカットパネル\",\"Toggle video fit\":\"ビデオフィットを切り替え\",\"Shortcuts / hints\":\"ショートカット/ヒント\",\"Ranking\":\"ランキング\",\"Model details\":\"モデル詳細\",\"My profile\":\"私のプロフィール\",\"Loading local statistics…\":\"ローカル統計をロードしています…\",\"No watch data yet. Statistics start with this version.\":\"まだ時計データがありません。統計はこのバージョンから始まります。\",\"Statistics are disabled.\":\"統計は無効になっています。\",\"Enable statistics\":\"統計を有効にする\",\"Disable statistics\":\"統計を無効にする\",\"Export statistics\":\"統計のエクスポート\",\"Delete all statistics\":\"すべての統計を削除する\",\"Delete this model data\":\"このモデルデータを削除します\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"ローカルで収集された監視統計をすべて削除しますか?ワークスペースはそのまま残ります。\",\"Tracking since\":\"以降の追跡\",\"Models watched\":\"注目のモデル\",\"Watch time\":\"視聴時間\",\"Most watched tags\":\"よく見られているタグ\",\"Most watched hours\":\"最も視聴された時間帯\",\"Observed online\":\"オンラインで観察\",\"Public bio\":\"公開略歴\",\"Public links\":\"パブリックリンク\",\"No public profile data cached yet.\":\"パブリック プロファイル データはまだキャッシュされていません。\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"表示され、アクティブにプレイされているカードのみがカウントされます。ホバープレビューはカウントされません。\",\"Statistics imported\":\"インポートされた統計\",\"Statistics deleted\":\"統計が削除されました\",\"Statistics exported\":\"エクスポートされた統計\",\"Local observation only\":\"現地観察のみ\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"プライベートメッセージ、トークン、支払い、パスワード、外部追跡は収集されません。\",\"Not enough observations for a time estimate\":\"時間を見積もるには十分な観測がありません\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"任意のボタン/コントロールにマウスを置くと、そのヒントが表示されます。共通のショートカットはショートカット パネルで編集できます。\\n\\nデフォルト:\\n/ ユーザー名の入力に焦点を当てる\\nr すべて更新\\ng グリッドビュー\\nf フォーカスビュー\\nAlt+P または Alt+C クリーン モードのオン/オフ\\nAlt+Shift+C 録音センター\\nAlt+Shift+R 現在のページを記録する\\nEsc クリーンモードを終了/全画面表示\\nスペース フォーカスビューのメイン画面を一時停止/再開します\\n←/→ または [/ ] フォーカスビューのメイン画面を切り替えます\\nカードをダブルクリック 全画面表示\",\"Visible windows paused\":\"表示されているウィンドウが一時停止されました\",\"Visible windows resumed\":\"表示ウィンドウが再開されました\",\"Import config\":\"構成をインポートする\",\"Repair saved data\":\"セーブデータを修復する\",\"Saved data has been normalized.\":\"セーブデータは正規化されました。\",\"Clear all data\":\"すべてのデータをクリア\",\"This will erase ALL rooms, groups and settings. Continue?\":\"これにより、すべてのルーム、グループ、設定が消去されます。続く?\",\"中文\":\"中国語\",\"English\":\"英語\",\"Deutsch\":\"ドイツ語\",\"Author\":\"著者\",\"License\":\"ライセンス\",\"If this script saves you time…\":\"このスクリプトで時間を節約できれば…\",\"Tip in ETH\":\"ETHのチップ\",\"ETH address\":\"ETHアドレス\",\"Copy\":\"コピー\",\"Brave exposed no recordable video track.\":\"Brave は記録可能なビデオ トラックを公開しませんでした。\",\"Brave or the video source blocked frame capture.\":\"Brave またはビデオ ソースがフレーム キャプチャをブロックしました。\",\"Browser exposed no audio track\":\"ブラウザでオーディオ トラックが公開されませんでした\",\"Camclient\":\"カムクライアント\",\"Camclient as the real homepage\":\"実際のホームページとしての Camclient\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient は、これらの透明なサンプルを共通のタグに自動的に割り当てます。タグの画像ボタンを使用してサンプルを選択し、独自の PNG/WebP/SVG をアップロードするか、画像を非表示にします。\",\"Choose a Camclient example or upload your own transparent image.\":\"Camclient のサンプルを選択するか、独自の透明な画像をアップロードします。\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"1.1 MB 未満の PNG、JPEG、WebP、または安全な SVG を選択してください。\",\"Choose artwork for tag\":\"タグに使用するアートワークを選択してください\",\"Click to enable meter\":\"クリックしてメーターを有効にします\",\"Close more actions\":\"さらにアクションを閉じる\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"色、タイポグラフィー、間隔、コーナー、影、および背景は、設定、ワークスペース、Discover、およびモデル ライブラリにすぐに適用されます。\",\"Continue loading the complete catalog\":\"完全なカタログのロードを続行します\",\"Continuing recording\":\"録音を継続する\",\"current matches\":\"現在の試合\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Camclient の名前、マーク、キャッチフレーズ、ローカル ロゴをカスタマイズします。ロゴはこのブラウザ内に残ります。\",\"Accent gradient\":\"アクセントグラデーション\",\"Active filters\":\"アクティブフィルター\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"独自の画像を追加するか、プレーヤーのスクリーンショット ボタンを使用します。スクリーンショットは自動的にギャラリーに追加され、モデルのカバーとして直接使用できます。\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"すべてのホームページ フィルター、レイアウト、プレビュー、ページネーション設定は安定した設定ボールトに保存され、スクリプトの更新が存続します。\",\"All live models\":\"すべてのライブモデル\",\"All suggestions\":\"すべての提案\",\"Almost silent\":\"ほぼ無音\",\"Always active\":\"常にアクティブ\",\"AND\":\"そして\",\"Automatic example\":\"自動の例\",\"Automatic segments\":\"自動セグメント\",\"Automatically loading the complete catalog…\":\"完全なカタログを自動的にロードしています…\",\"Available recording data was saved.\":\"利用可能な録画データが保存されました。\",\"Back online\":\"オンラインに戻る\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"ローカルに保存されたお気に入り、タグ、および表示アクティビティに基づきます。すべての選択にはその理由が示されています。\",\"Black\":\"ブラック\",\"Discover something new\":\"新しいものを発見する\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"独特のおすすめレーンは、ホームページのカタログを繰り返す代わりに、新しいものを見つけるのに役立ちます。\",\"Do not enlarge\":\"拡大しないでください\",\"Enable meter\":\"メーターを有効にする\",\"Fast starters\":\"ファストスターター\",\"Fill and crop\":\"塗りつぶしとトリミング\",\"Filter suggestions\":\"フィルタ提案\",\"Fit completely\":\"完全にフィット\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"透明度を設定するには、PNG、WebP、または安全な SVG を使用し、ロゴの背景として「透明」を選択します。 JPEGには透明度がありません。ストレッチは、選択された幅と高さを独立して使用します。\",\"For you\":\"あなたのために\",\"gallery\":\"ギャラリー\",\"HD\":\"HD\",\"Inner spacing\":\"内部間隔\",\"Inspiration, not repetition\":\"繰り返しではなく、インスピレーション\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"ソフトウェアの使用、変更、配布が許可されます。著作権およびライセンス通知はそのままにしておく必要があり、ソフトウェアには保証がありません。 Camclient は Emberhost によって作成されます。\",\"Live for a short time and already drawing public activity.\":\"短期間しか住んでいませんが、すでに大衆の活動を描いています。\",\"Only new\":\"新品のみ\",\"Open Camclient homepage\":\"Camclient ホームページを開く\",\"OR\":\"または\",\"part\":\"部分\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"個人的なおすすめ、新しい発見、人気のライブルーム、訪れたことのないモデルなど、ホームページの完全なカタログとは明確に区別されています。\",\"Place\":\"場所\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG、WebP、安全な SVG は透明性をサポートします。画像はローカルで 144 × 144 ピクセルにサイズ変更され、設定とともに保存されます。\",\"Popular now\":\"今人気の\",\"Public live models with no local watch time in Camclient.\":\"Camclient でのローカル総再生時間のない公開ライブ モデル。\",\"Quiet\":\"静か\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"現在の公開視聴者数によってランク付けされます。カタログ データは永続的に保存されるわけではありません。\",\"Recording is still active.\":\"記録はまだアクティブです。\",\"Recording quality\":\"録音品質\",\"Recording stopped\":\"録音停止\",\"Recording unavailable\":\"録音不可\",\"Remix\":\"リミックス\",\"Live stream audio activity\":\"ライブ ストリームのオーディオ アクティビティ\",\"Local watch\":\"ローカルウォッチ\",\"Logo background\":\"ロゴの背景\",\"Logo height\":\"ロゴの高さ\",\"Logo width\":\"ロゴの幅\",\"Loud\":\"大声で\",\"Measured even while output is muted\":\"出力をミュートした状態でも測定可能\",\"Middle-click or Ctrl+click opens a new tab\":\"中クリックまたは Ctrl キーを押しながらクリックすると、新しいタブが開きます\",\"Models you have visited locally and that are public again now.\":\"あなたがローカルで訪問し、現在再び公開されているモデル。\",\"New discoveries\":\"新しい発見\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"カタログに初めて入った人、Camclient で新しく見た人、またはストリーミングを始めたばかりの人。\",\"No audio track\":\"オーディオトラックがありません\",\"No filters active · all matching live models are shown\":\"アクティブなフィルターはありません。一致するライブ モデルがすべて表示されます。\",\"No recommendations match the active filters.\":\"アクティブなフィルターに一致する推奨事項はありません。\",\"No recording data\":\"録画データがありません\",\"NOT\":\"そうではない\",\"Reset all\":\"すべてリセット\",\"RoomGrid Model library\":\"RoomGrid モデル ライブラリ\",\"RoomGrid My user\":\"RoomGrid 私のユーザー\",\"RoomGrid Statistics\":\"ルームグリッド統計\",\"Save to model gallery\":\"モデルギャラリーに保存\",\"Saved segment\":\"保存されたセグメント\",\"Screenshot\":\"スクリーンショット\",\"Screenshot failed\":\"スクリーンショットが失敗しました\",\"Screenshot format\":\"スクリーンショットの形式\",\"Screenshot resolution\":\"スクリーンショットの解像度\",\"Screenshot unavailable\":\"スクリーンショットは利用できません\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"スクリーンショットがダウンロードされ、ローカル モデル ギャラリーにアクセスすることもできます。\",\"segment(s)\":\"セグメント\",\"Show no image\":\"画像を表示しない\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"プレーヤー出力がミュートされているかどうかに関係なく、ストリーム オーディオ トラック内のアクティビティを表示します。\",\"Source ended\":\"ソースが終了しました\",\"Source resolution\":\"ソース解像度\",\"Start recording\":\"録音を開始する\",\"Starter pick · popular now\":\"スターターピック・今人気\",\"Stop and save\":\"停止して保存する\",\"Stretch\":\"ストレッチ\",\"Tag artwork and emoji\":\"アートワークと絵文字にタグを付ける\",\"Tag artwork editor\":\"タグアートワークエディター\",\"The compressed image is still too large.\":\"圧縮された画像はまだ大きすぎます。\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"ヘッダーには、ホーム、ディスカバー、タグ エクスプローラー、統計センター、モデル ライブラリ、マイ ユーザー、および設定のみが表示されます。すべての構成領域は、設定サブページとして利用できます。\",\"The image could not be decoded.\":\"画像をデコードできませんでした。\",\"The image could not be read.\":\"画像を読み取れませんでした。\",\"The image format is unsafe or unsupported.\":\"画像形式が安全でないか、サポートされていません。\",\"The lanes respect your active search and filter settings.\":\"レーンはアクティブな検索とフィルターの設定を尊重します。\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"録音はこのデバイスにのみ保存されます。このコンテンツを保存する権限がある場合にのみ続行してください。\",\"The video has no usable frame yet.\":\"ビデオにはまだ使用可能なフレームがありません。\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"地域の嗜好データがまだ十分ではないため、これらは現在の人気に基づいた透明性のあるスターター ピックです。\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"これらの設定は、Camclient ホームページのレイアウト、ページネーション、モデル情報、フィルター、パフォーマンスを制御します。\",\"Transparent\":\"透明\",\"Unified main navigation\":\"統合されたメインナビゲーション\",\"Unvisited\":\"未訪問\",\"Upload own image\":\"自分の画像をアップロードする\",\"URL\":\"URL\",\"Use as model cover\":\"モデルカバーとして使用\",\"Use the complete search, filters, and sorting for every current result.\":\"現在のすべての結果に対して完全な検索、フィルター、並べ替えを使用します。\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Chaturbate ホームページにアクセスすると、常に完全な Camclient ホームページが開きます。すべてのメイン領域は同じカスタム インターフェイス内に留まります。\",\"Waiting for audio\":\"オーディオを待っています\",\"What does the MIT license mean?\":\"MITライセンスとはどういう意味ですか?\",\"White\":\"ホワイト\",\"Your discovery mix\":\"あなたの発見ミックス\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"公開ライブ配信を見つける\",\"all groups\":\"すべてのグループ\",\"at least one group\":\"1つ以上のグループ\",\"no measurements yet\":\"測定データはまだありません\",\"Country, region or location\":\"国、地域、場所\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Chaturbate本来のホームページ上で、モデルの順序、フィルター、1ページあたりの件数、カードサイズ、表示情報を直接調整できます。変更はすぐに適用され、このブラウザに保存されます。\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]]前\",\"[[ROOMGRID_VALUE_0]] preview\":\"[[ROOMGRID_VALUE_0]]のプレビュー\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] プロフィールを保存しました\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"@[[ROOMGRID_VALUE_0]] のモデルの詳細\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"フォロー中のモデルを[[ROOMGRID_VALUE_0]]件読み込みました\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"My Collectionの認識項目: [[ROOMGRID_VALUE_0]]件\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"現在読み込まれている[[ROOMGRID_VALUE_1]]モデルのうち[[ROOMGRID_VALUE_0]]モデルを表示中。\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"有効なグループフィルター: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_2]]枚中[[ROOMGRID_VALUE_1]]枚が一致([[ROOMGRID_VALUE_3]])。\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"カスタムカバー・[[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"ローカルに保存されたカバー · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"画像とカバーの例 ([[ROOMGRID_VALUE_0]] が保存されました)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"このデバイスに永続的に保存されます · 最新の [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"自分の画像 · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"[[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] を保存しました\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"初回確認: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"最終確認: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"最終ライブ: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"表示回数: [[ROOMGRID_VALUE_0]]回 · 初回 [[ROOMGRID_VALUE_1]] · 最新 [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"典型的な時間: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"技術情報: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"ローカルライブ観測[[ROOMGRID_VALUE_0]]件(5分につき最大1回)\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"以前の視聴時間の[[ROOMGRID_VALUE_0]]を控えめな推定値として使用\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"タグ[[ROOMGRID_VALUE_0]]を削除\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"カスタムグループを管理 · 割り当て: [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"グループ「[[ROOMGRID_VALUE_0]]」を削除しますか?モデルはライブラリに残ります。\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"グループ「[[ROOMGRID_VALUE_0]]」が削除されました\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"グループ[[ROOMGRID_VALUE_0]]件 · 複数選択可能\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"保存済みタグ[[ROOMGRID_VALUE_0]]件\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"フィルター [[ROOMGRID_VALUE_0]] を削除します\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] が一致\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] フィルターがアクティブです\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · 公開プロフィール情報は「すべての詳細」から読み込めます。\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"個人整理開始: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"自動登録開始: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"最終アクティビティ: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"表示回数: [[ROOMGRID_VALUE_0]]回 · 最終記録 [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"検出: [[ROOMGRID_VALUE_0]] · 品質測定: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"プロフィール情報: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] がアーカイブされました\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] が復元されました\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"[[ROOMGRID_VALUE_1]]ページ中[[ROOMGRID_VALUE_0]]ページ · [[ROOMGRID_VALUE_2]]モデル\",\"[[ROOMGRID_VALUE_0]] saved\":\"保存済み: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] watched\":\"視聴済み: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"アクティブ[[ROOMGRID_VALUE_0]]件 · 自動的に増加\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] モデル · [[ROOMGRID_VALUE_1]] セッション\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] 件のお気に入り\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] の個別のモデル\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] プレビュー\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"[[ROOMGRID_VALUE_0]] モデル全体のプレビュー/クイック ビュー\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]]日\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"[[ROOMGRID_VALUE_0]]日連続\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"複数回視聴: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] 日分のローカル データ\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"加重平均 [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"現在ライブ: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"認識したモデル[[ROOMGRID_VALUE_0]]件\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"一部を読み取れませんでした: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"最もアクティブな日: [[ROOMGRID_VALUE_0]]([[ROOMGRID_VALUE_1]])。生イベントの保存期間は[[ROOMGRID_VALUE_2]]日で、それ以前は日別合計として残る場合があります。\",\"[[ROOMGRID_VALUE_0]] sessions\":\"セッション[[ROOMGRID_VALUE_0]]件\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"ローカル保存: [[ROOMGRID_VALUE_0]]\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"ローカルデータを読み込めませんでした: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]]はオンラインです。\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]]はオフラインです。\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]]はしばらくオンラインで確認されていません。\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"お気に入り[[ROOMGRID_VALUE_0]]件が同時にオンラインです。\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"[[ROOMGRID_VALUE_0]]の公開プロフィールが変更されました。\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] モデルが更新されました\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"統計インポートのプレビュー(まだ変更されていません):\\nモデル [[ROOMGRID_VALUE_0]]件\\nイベント [[ROOMGRID_VALUE_1]]件\\n公開プロフィール [[ROOMGRID_VALUE_2]]件\\n\\n続行すると既存の統計が置き換えられます。\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]]のライブプレビュー\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]]をMultiCamに追加しました\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"[[ROOMGRID_VALUE_0]]を削除しますか?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"[[ROOMGRID_VALUE_0]]件の表示と[[ROOMGRID_VALUE_1]]件のフィードを読み込みますか?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"ワークスペースに[[ROOMGRID_VALUE_0]]ルームを追加しました\",\"[[ROOMGRID_VALUE_0]] models saved\":\"保存済みモデル[[ROOMGRID_VALUE_0]]件\",\"[[ROOMGRID_VALUE_0]] matching results\":\"一致する結果[[ROOMGRID_VALUE_0]]件\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] 一致するライブ モデル\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"お気に入りと#[[ROOMGRID_VALUE_0]]が一致\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] 画像が追加されました。 [[ROOMGRID_VALUE_1]] を処理できませんでした。\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] 画像が追加されました。\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]]ルーム\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"有効なユーザー名[[ROOMGRID_VALUE_0]]件\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"ページ [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]]、[[ROOMGRID_VALUE_2]]ルーム\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"一時URL: [[ROOMGRID_VALUE_0]]件追加、[[ROOMGRID_VALUE_1]]件失敗\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"統計インポートのプレビュー: [[ROOMGRID_VALUE_0]] モデルと [[ROOMGRID_VALUE_1]] イベント。続行すると、データが安全にマージされ、最初にバックアップが作成されます。\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"カスタムレイアウトは最大[[ROOMGRID_VALUE_0]]ルームに対応します。\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"シーン「[[ROOMGRID_VALUE_0]]」を保存しました\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"シーン「[[ROOMGRID_VALUE_0]]」がロードされました\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] アクティブなストリーム · [[ROOMGRID_VALUE_1]] 不安定 · [[ROOMGRID_VALUE_2]] 監視対象\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] ストリーム · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p が割り当てられました\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"シーン「[[ROOMGRID_VALUE_0]]」を削除しますか?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"[[ROOMGRID_VALUE_0]] に対してのみ保存: 最大 [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"このカメラはグローバル/アダプティブ品質[[ROOMGRID_VALUE_0]]を使用しています\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]]を[[ROOMGRID_VALUE_1]]に置き換えました\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] からのみオーディオを再生しています\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"フォーカス枠[[ROOMGRID_VALUE_0]]に配置しました\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"グリッドウィンドウ[[ROOMGRID_VALUE_0]]に配置しました\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"解像度 [[ROOMGRID_VALUE_0]]、状態 [[ROOMGRID_VALUE_1]]、バッファ [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"ストリームの健全性 [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"モデル[[ROOMGRID_VALUE_0]]件\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"フィルターを削除: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"あなたの興味に合ったもの: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"カタログ [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] フェッチ済み · [[ROOMGRID_VALUE_2]] ライブ モデル\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] ローカル モデル\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"カタログ [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] ライブ モデル\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] カタログ ルームがロードされました\",\"Detected via: [[ROOMGRID_VALUE_0]]\":\"検出元: [[ROOMGRID_VALUE_0]]\",\"Sources: [[ROOMGRID_VALUE_0]]\":\"ソース: [[ROOMGRID_VALUE_0]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing replaces the current statistics.\":\"統計インポートのプレビュー: モデル[[ROOMGRID_VALUE_0]]件、イベント[[ROOMGRID_VALUE_1]]件。続行すると現在の統計が置き換えられます。\",\"[[ROOMGRID_VALUE_0]] ms startup · [[ROOMGRID_VALUE_1]] stalls · [[ROOMGRID_VALUE_2]]% dropped frames\":\"起動[[ROOMGRID_VALUE_0]] ms · 停止[[ROOMGRID_VALUE_1]]回 · ドロップフレーム[[ROOMGRID_VALUE_2]]%\"},\"visible\":{\"Gruppe löschen\":\"グループの削除\",\"Gruppen umbenennen oder löschen\":\"グループの名前変更または削除\",\"HD + Gaming\":\"HD+ ゲーム\",\"IndexedDB\":\"インデックス付きDB\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"保存されたすべてのタグは、検索、オートコンプリート、および直接複数フィルターとして自動的に使用できます。\",\"Keine Models passen zu diesen Filtern.\":\"これらのフィルターに一致するモデルはありません。\",\"Keine Priorität\":\"優先事項ではありません\",\"Konto-Daten werden eingelesen …\":\"アカウントの詳細を読み取っています...\",\"Lautstärke pro Model merken\":\"モデルごとの音量\",\"Layout & sichtbare Bereiche\":\"レイアウトと表示領域\",\"Letzte Messung\":\"前回の測定\",\"LIVE\":\"ライブ\",\"lokal · für Suche und Filter\":\"ローカル · 検索とフィルター用\",\"Maximale Stream-Lautstärke\":\"最大ストリーム量\",\"Model besitzt alle gewählten Tags\":\"モデルは選択されたすべてのタグを所有します\",\"Model besitzt mindestens einen gewählten Tag\":\"モデルには少なくとも 1 つの選択されたタグがあります\",\"Automatisch neu verbinden\":\"自動的に再接続する\",\"Bei verstecktem Tab pausieren\":\"タブを非表示にしたまま一時停止する\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"既存の Chaturbate アカウントの詳細を適用する\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"画像の最適化 · 大きなズーム、位置、フィルター、回転\",\"Bildanpassung\":\"画像調整\",\"Bildwerkzeuge einklappen\":\"画像ツールを折りたたむ\",\"Breite des Informationsbereichs\":\"情報エリアの幅\",\"Broadcaster-Verbindung verwalten\":\"ブロードキャスター接続の管理\",\"Chat anzeigen\":\"チャットを表示する\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"これは、スクリプトがインストールされて以来、データに制限されていないことを意味します。RoomGrid は、既存のフォロー リストと、マイ コレクションから明確に認識できるエントリを読み取ることができます。メッセージ、支払い、メディア、パスワードはコピーされません。\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio には、期間比較、ヒートマップ、発見ファネル、ワークスペースおよび品質分析、データ エクスポートも含まれています。\",\"Datenschutz & Export\":\"データ保護とエクスポート\",\"Deine Aktivität der letzten 14 Tage\":\"過去 14 日間のアクティビティ\",\"Deine meistgesehenen Models\":\"最も閲覧されているモデル\",\"Model ist in allen gewählten Gruppen\":\"モデルは選択されたすべてのグループに含まれています\",\"Model ist in mindestens einer gewählten Gruppe\":\"モデルは少なくとも 1 つの選択されたグループに属しています\",\"MultiCam\":\"マルチカム\",\"MultiCam +\":\"マルチカム+\",\"My Collection öffnen\":\"私のコレクションを開く\",\"Namen speichern\":\"名前を保存する\",\"Nicht verbunden\":\"接続されていません\",\"Niedrige Live-Latenz\":\"ライブレイテンシが低い\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"ローカル使用量の値はまだ十分ではありません。\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"私たち自身のグループはまだありません。上に名前を入力して、最初のコレクションを作成します。\",\"Noch keine Model-Sehzeit erfasst.\":\"モデルの表示時間はまだ記録されていません。\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"公式アカウントのスナップショットはまだありません。\",\"Noch nicht eingelesen\":\"まだ読んでいません\",\"Nur auf diesem Gerät\":\"このデバイスのみ\",\"Öffentliche/erkannte Tags\":\"パブリック/認識されたタグ\",\"Öffentliches Profil anzeigen\":\"公開プロフィールを表示\",\"Offizielle Statistik verbinden\":\"公式統計を接続する\",\"Optionale offizielle Kontostatistik\":\"オプションの公式アカウント統計\",\"Persönlich organisiert\":\"個人的に組織された\",\"PiP\":\"ピップ\",\"Player & Verbindung\":\"プレーヤーと接続\",\"Player neu verbinden\":\"プレーヤーを再接続する\",\"Player-Ecken\":\"プレイヤーコーナー\",\"Player-Hintergrund\":\"選手背景\",\"Player-Höhe\":\"選手の身長\",\"Player-Seitenverhältnis\":\"プレーヤーのアスペクト比\",\"Position der Informationen\":\"情報の所在\",\"Private Notiz\":\"私的なメモ\",\"Private Notizen anzeigen\":\"プライベートノートを表示する\",\"Puffergröße\":\"バッファサイズ\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"現在の接続はアクティブです。次回の計測後、ここにローカルコースが表示されます。\",\"Diese Bibliotheksansicht ist noch leer.\":\"このライブラリ ビューはまだ空です。\",\"Eigene Priorität\":\"自分自身の優先順位\",\"Eigene Tags\":\"独自のタグ\",\"Eigene Tags filtern\":\"独自のタグをフィルタリングする\",\"Eigenen Tag eingeben …\":\"自分の一日を入力してください...\",\"Einstellungen\":\"設定\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"入力するか「タグを追加」すると下書きが引き継がれます。 「プロファイルを保存」でのみ永続的に保存されます。入力すると、すでに使用されているタグが提案されます。\",\"Ereignisse im Zeitraum\":\"期間中のイベント\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"自由に名前を付けたコレクションを作成し、モデルを複数のグループに割り当て、1 つまたは複数のグループをここにまとめて表示します。\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"まず、上で独自のグループを作成します。\",\"Follower\":\"フォロワー\",\"GAMING\":\"ゲーム\",\"Gesamter Verlauf\":\"コース全体\",\"Grid +\":\"グリッド+\",\"Größe der Bedienelemente\":\"コントロールのサイズ\",\"‹ Zurück\":\"« 戻る\",\"● 1.2k · #tag\":\"● 1.2k · #タグ\",\"1 Jahr\":\"1年\",\"30 Tage\":\"30日\",\"7 Tage\":\"7日間\",\"90 Tage\":\"90日\",\"AA ×\":\"AA×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"類似モデルを表示\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"すべての変更はローカルに保存され、次のモデルにも適用されます。\",\"Alle eigenen Tags\":\"すべてのカスタムタグ\",\"Alle Gruppen\":\"すべてのグループ\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"古い Chaturbate データをリクエストする\",\"Automatisch abspielen\":\"自動再生\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"自動的にキャプチャされ、編集時に完全に整理されます\",\"Verbunden\":\"接続済み\",\"Vollständige Modelstatistik\":\"完全なモデル統計\",\"Vorhandene Konto-Daten jetzt einlesen\":\"既存のアカウントデータを今すぐ読み込みます\",\"Weiter ›\":\"次へ ›\",\"Zufriedenheit\":\"満足\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid は個人の Chaturbate アカウントの値を推測できません。これらのカードは、公式放送局の統計を意識的に自分のユーザー名とトークンにリンクした場合にのみ表示されます。\",\"RoomGrid Suite\":\"ルームグリッドスイート\",\"Seitengröße\":\"ページサイズ\",\"SingleCam\":\"シングルカム\",\"SingleCam einstellen\":\"SingleCamを設定\",\"SingleCam Pro\":\"シングルカムプロ\",\"SingleCam-Standard wiederherstellen\":\"SingleCam をデフォルトに戻す\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"モデルの独自のタグを保存するとすぐに、こことオートコンプリートに表示されます。\",\"Status im Player anzeigen\":\"プレーヤーにステータスを表示\",\"Status-Aktualisierung\":\"ステータスの更新\",\"Stream immer stumm starten\":\"常にサイレントにストリームを開始する\",\"Tag hinzufügen\":\"タグを追加\",\"Tipps letzte Stunde\":\"最後の 1 時間のヒント\",\"Token-Guthaben\":\"トークン残高\",\"Ton & Ruhe\":\"音と静けさ\",\"Ungefähre Onlinezeiten\":\"オンライン時間の目安\",\"Gruppen\":\"グループ\",\"Schließen\":\"閉じる\",\"Startseite nach eigenen Gruppen filtern\":\"自分のグループでホームページを絞り込む\",\"RoomGrid Komfortzentrum\":\"RoomGridコントロールセンター\",\"Mein Nutzer\":\"マイプロフィール\",\"Mein Nutzer-Dashboard\":\"マイプロフィールダッシュボード\",\"Modellbibliothek\":\"モデルライブラリ\",\"Große Modellbibliothek\":\"大きなモデルライブラリ\",\"Statistikzentrum\":\"統計センター\",\"Sitzungen\":\"セッション\",\"Dock + Hover\":\"ドック+ホバー\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 このデバイス内だけで安全に保存\",\"Sortierung\":\"並べ替え\",\"Meiste Sehzeit\":\"視聴時間が長い順\",\"Zuletzt aktiv\":\"最近のアクティビティ\",\"Meiste Sitzungen\":\"セッション数が多い順\",\"Meiste Zuschauer\":\"視聴者数が多い順\",\"Status\":\"ステータス\",\"Alle Status\":\"すべてのステータス\",\"Nur live\":\"ライブのみ\",\"Nur Live\":\"ライブのみ\",\"Offline\":\"オフライン\",\"Unbekannt\":\"不明\",\"Nur Bibliothek\":\"ライブラリのみ\",\"Pro Seite\":\"1ページあたり\"}}","pl":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Nie można było bezpiecznie przygotować zmiany języka. Ta strona nie została opuszczona.\",\"Language\":\"Język\",\"Default workspace\":\"Domyślny obszar roboczy\",\"Workspace\":\"Obszar roboczy\",\"New workspace\":\"Nowy obszar roboczy\",\"copy\":\"kopia\",\"New group\":\"Nowa grupa\",\"Saved model\":\"Zapisany model\",\"Search models, pages, workspaces and commands…\":\"Przeszukuj modele, strony, obszary robocze i polecenia…\",\"Type to filter · Enter to run · Esc to close\":\"Wpisz, aby filtrować · Enter, aby uruchomić · Esc, aby zamknąć\",\"No matches\":\"Brak dopasowań\",\"Currently active RoomGrid version\":\"Aktualnie aktywna wersja RoomGrid\",\"years\":\"lat\",\"Live\":\"Na żywo\",\"Private\":\"Prywatny\",\"Away\":\"Nieobecny\",\"Offline\":\"Nieaktywny\",\"sessions\":\"sesje\",\"NEW\":\"NOWOŚĆ\",\"Name, subject, tag or place\":\"Nazwa, temat, tag lub miejsce\",\"Website order\":\"Kolejność witryny\",\"Most viewers\":\"Najwięcej widzów\",\"Fewest viewers\":\"Najmniej widzów\",\"Longest online\":\"Najdłużej online\",\"Recently online\":\"Ostatnio online\",\"Age ascending\":\"Wiek rosnąco\",\"Age descending\":\"Wiek malejąco\",\"New models first\":\"Najpierw nowe modele\",\"Saved first\":\"Najpierw zapisane\",\"Favorites first\":\"Najpierw ulubione\",\"Most watched\":\"Najczęściej oglądane\",\"Recently watched\":\"Ostatnio oglądane\",\"Random mix\":\"Losowa kolejność\",\"All loaded\":\"Wszystkie wczytane\",\"Previous page\":\"Poprzednia strona\",\"Next page\":\"Następna strona\",\"Reload complete live list\":\"Załaduj ponownie pełną listę na żywo\",\"All homepage options\":\"Wszystkie opcje strony głównej\",\"Reset filters\":\"Zresetuj filtry\",\"Quick filter\":\"Szybki filtr\",\"Sort\":\"Sortuj\",\"Models per page\":\"Modele na stronę\",\"Load more results\":\"Załaduj więcej wyników\",\"still to display\":\"jeszcze do wyświetlenia\",\"continues automatically while scrolling\":\"jest kontynuowany automatycznie podczas przewijania\",\"visible\":\"widoczne\",\"loaded\":\"wczytano\",\"page\":\"strona\",\"active filters\":\"aktywne filtry\",\"loading complete live list\":\"ładowanie pełnej listy na żywo\",\"all matching live models found\":\"znaleziono wszystkie pasujące modele na żywo\",\"live list incomplete, retry\":\"lista na żywo jest niekompletna, spróbuj ponownie\",\"Requests temporarily paused\":\"Żądania tymczasowo wstrzymane\",\"Vault could not be unlocked\":\"Nie można odblokować skarbca\",\"Preview Dock\":\"Podgląd Doku\",\"Connecting…\":\"Łączę…\",\"Stream temporarily unavailable\":\"Transmisja chwilowo niedostępna\",\"Error\":\"Błąd\",\"Buffering\":\"Buforowanie\",\"Preview unavailable\":\"Podgląd niedostępny\",\"Connect\":\"Połącz\",\"Close\":\"Zamknij\",\"Opacity\":\"Nieprzezroczystość\",\"Info\":\"Informacje\",\"Age\":\"Wiek\",\"Gender\":\"Płeć\",\"Location\":\"Lokalizacja\",\"Country\":\"Kraj\",\"Last broadcast\":\"Ostatnia transmisja\",\"Fan club\":\"Klub fanów\",\"Private show\":\"Prywatny pokaz\",\"Spy show\":\"Spektakl szpiegowski\",\"Model comparison list\":\"Lista porównawcza modeli\",\"Compare\":\"Porównaj\",\"Open model details\":\"Otwórz szczegóły modelu\",\"Compare now\":\"Porównaj teraz\",\"Clear\":\"Jasne\",\"Up to four models can be compared at once.\":\"Jednocześnie można porównać maksymalnie cztery modele.\",\"Remove from comparison\":\"Usuń z porównania\",\"Add to comparison\":\"Dodaj do porównania\",\"Total watched\":\"Razem obejrzeliśmy\",\"Sessions\":\"Sesje\",\"Viewers\":\"Widzowie\",\"Rating\":\"Ocena\",\"Online samples\":\"Próbki w Internecie\",\"Loading quality data…\":\"Ładuję dane dotyczące jakości…\",\"samples\":\"próbki\",\"stalls\":\"stragany\",\"No quality measurements yet\":\"Nie ma jeszcze żadnych pomiarów jakości\",\"Quality data unavailable\":\"Dane dotyczące jakości niedostępne\",\"Details\":\"Szczegóły\",\"Remove\":\"Usuń\",\"Model comparison\":\"Porównanie modeli\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Każda wartość pochodzi z informacji publicznych, Twojej własnej organizacji lub lokalnych obserwacji na tym urządzeniu. Brakujące wartości nigdy nie są odgadywane.\",\"Shared tags\":\"Udostępnione tagi\",\"Loading public facts…\":\"Ładowanie faktów publicznych…\",\"Unknown status\":\"Nieznany stan\",\"Last activity\":\"Ostatnia aktywność\",\"These statistics describe only your own local RoomGrid usage.\":\"Te statystyki opisują tylko Twoje lokalne wykorzystanie RoomGrid.\",\"At a glance\":\"W skrócie\",\"Type\":\"Typ\",\"Appearances\":\"Występy\",\"Public facts\":\"Fakty publiczne\",\"No public biography available yet.\":\"Nie ma jeszcze dostępnej publicznej biografii.\",\"Public link\":\"Link publiczny\",\"Public profile source\":\"Źródło profilu publicznego\",\"Profile updated\":\"Profil zaktualizowany\",\"Private note (local only)\":\"Prywatna notatka (tylko lokalnie)\",\"Your tags\":\"Twoje tagi\",\"No rating\":\"Brak oceny\",\"No priority\":\"Bez priorytetu\",\"Private library data\":\"Dane biblioteki prywatnej\",\"Private notes\":\"Prywatne notatki\",\"Priority\":\"Priorytet\",\"Favorite\":\"Ulubiony\",\"Watchlist\":\"Lista obserwowanych\",\"Archived\":\"Zarchiwizowane\",\"Watch again\":\"Obejrzyj jeszcze raz\",\"Technical note\":\"Uwaga techniczna\",\"Technical\":\"Techniczne\",\"Save all changes\":\"Zapisz wszystkie zmiany\",\"Loading local activity…\":\"Ładowanie aktywności lokalnej…\",\"Activity and playback quality\":\"Jakość aktywności i odtwarzania\",\"Sun\":\"niedziela\",\"Mon\":\"pon\",\"Tue\":\"wt\",\"Wed\":\"śr\",\"Thu\":\"czw\",\"Fri\":\"piątek\",\"Sat\":\"sob\",\"Quality samples\":\"Próbki jakości\",\"Average startup\":\"Przeciętny start\",\"Stalls\":\"Stragany\",\"Reconnects\":\"Łączy się ponownie\",\"Dropped frames\":\"Opuszczone ramki\",\"Online observations\":\"Obserwacje w Internecie\",\"Frequently observed online times\":\"Często obserwowane czasy online\",\"Not enough online observations yet.\":\"Za mało jeszcze obserwacji online.\",\"Watched\":\"Obejrzałem\",\"Preview\":\"Podgląd\",\"Quick view\":\"Szybki podgląd\",\"Discovered\":\"Odkryto\",\"Quality\":\"Jakość\",\"Connection\":\"Połączenie\",\"No local activity recorded yet.\":\"Nie zarejestrowano jeszcze żadnej aktywności lokalnej.\",\"Recent events\":\"Ostatnie wydarzenia\",\"Activity data unavailable.\":\"Dane o aktywności są niedostępne.\",\"Similar models\":\"Podobne modele\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Obliczane na podstawie tagów lokalnych/publicznych, kraju i typu. Każda sugestia pokazuje swoją przyczynę i żadne dane nie są przesyłane.\",\"shared tags\":\"wspólne tagi\",\"same country\":\"ten sam kraj\",\"same type\":\"ten sam typ\",\"Not enough matching data yet.\":\"Jeszcze za mało pasujących danych.\",\"Overview\":\"Przegląd\",\"Profile\":\"Profil\",\"Organize\":\"Organizuj\",\"Activity\":\"Aktywność\",\"Similar\":\"Podobne\",\"Model detail sections\":\"Sekcje szczegółów modelu\",\"Previous model\":\"Poprzedni model\",\"Next model\":\"Następny model\",\"Open room\":\"Otwarty pokój\",\"Pin preview\":\"Podgląd przypięcia\",\"No public fields available\":\"Brak dostępnych pól publicznych\",\"Public facts currently unavailable\":\"Fakty publiczne obecnie niedostępne\",\"Session chat search\":\"Wyszukiwanie czatu sesyjnego\",\"Search this session only…\":\"Przeszukaj tylko tę sesję…\",\"New messages\":\"Nowe wiadomości\",\"Time for a short break.\":\"Czas na krótką przerwę.\",\"Your RoomGrid time budget for today has been reached.\":\"Twój budżet czasowy RoomGrid na dzisiaj został wyczerpany.\",\"RoomGrid action messages\":\"Komunikaty akcji RoomGrid\",\"This change is saved automatically.\":\"Ta zmiana zostanie zapisana automatycznie.\",\"Undo\":\"Cofnij\",\"Change undone\":\"Zmiana cofnięta\",\"Undo was not applied because newer changes already exist.\":\"Nie zastosowano opcji cofnięcia, ponieważ istnieją już nowsze zmiany.\",\"Chaturbate live catalog\":\"Katalog na żywo Chaturbate\",\"RoomGrid Discover\":\"Odkryj RoomGrid\",\"Chaturbate Discover page\":\"Chaturbate Odkryj stronę\",\"Following list\":\"Poniższa lista\",\"My Collection\":\"Moja kolekcja\",\"Chaturbate page\":\"Strona Chaturbate\",\"Local record\":\"Lokalny rekord\",\"Personally organized\":\"Osobiście zorganizowane\",\"Local watch history\":\"Lokalna historia oglądania\",\"Public profile\":\"Profil publiczny\",\"Local quality measurements\":\"Lokalne pomiary jakości\",\"Local model index\":\"Indeks modelu lokalnego\",\"Current live signal\":\"Aktualny sygnał na żywo\",\"Within the last hour\":\"W ciągu ostatniej godziny\",\"Within the last 24 hours\":\"W ciągu ostatnich 24 godzin\",\"Within the last 30 days\":\"W ciągu ostatnich 30 dni\",\"Historical record\":\"Rekord historyczny\",\"Time unknown\":\"Czas nieznany\",\"All\":\"Wszystko\",\"Favorites\":\"Ulubione\",\"Library\":\"Biblioteka\",\"My Collection contained no reliably recognizable entries\":\"Moja kolekcja nie zawierała żadnych rozpoznawalnych wpisów\",\"No group filter active · all models are shown.\":\"Żaden filtr grupowy nie jest aktywny · pokazane są wszystkie modele.\",\"Clear group filter\":\"Wyczyść filtr grupy\",\"Filter homepage by groups\":\"Filtruj stronę główną według grup\",\"Close group menu\":\"Zamknij menu grupy\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Wybierz jedną lub więcej własnych grup. Inne karty modeli są natychmiast ukryte na stronie głównej Chaturbate.\",\"Show all\":\"Pokaż wszystko\",\"Models in this group\":\"Modele w tej grupie\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Nie ma jeszcze grupy niestandardowej. Utwórz go w bibliotece modeli i przypisz tam modele.\",\"In any group\":\"W dowolnej grupie\",\"In all groups\":\"We wszystkich grupach\",\"Manage groups\":\"Zarządzaj grupami\",\"Create group\":\"Utwórz grupę\",\"Custom groups for the homepage\":\"Niestandardowe grupy dla strony głównej\",\"Main views\":\"Główne widoki\",\"MultiCam workstation\":\"Stacja robocza MultiCam\",\"Camera grid, audio and layouts\":\"Siatka kamer, dźwięk i układy\",\"Discover\":\"Odkrywaj\",\"Full catalog, filters and sorting\":\"Pełny katalog, filtry i sortowanie\",\"Personal dashboard\":\"Panel osobisty\",\"Favorites, activity and shortcuts\":\"Ulubione, aktywność i skróty\",\"Tag explorer\":\"Eksplorator tagów\",\"Combinations, trends and related tags\":\"Kombinacje, trendy i powiązane tagi\",\"Library and statistics\":\"Biblioteka i statystyka\",\"Statistics Center\":\"Centrum statystyk\",\"Model comparison and local statistics\":\"Porównanie modeli i statystyki lokalne\",\"Model library\":\"Biblioteka modeli\",\"Profiles, tags, groups and notes\":\"Profile, tagi, grupy i notatki\",\"My user\":\"Mój profil\",\"Preferences, usage and account data\":\"Preferencje, użytkowanie i dane konta\",\"Analytics Studio\":\"Studio analityczne\",\"Ranges, heatmaps and exports\":\"Zasięgi, mapy cieplne i eksport\",\"Settings and maintenance\":\"Ustawienia i konserwacja\",\"Customize homepage\":\"Dostosuj stronę główną\",\"Cards, filters and visible details\":\"Karty, filtry i widoczne szczegóły\",\"Appearance\":\"Wygląd\",\"Colors, density and surfaces\":\"Kolory, gęstość i powierzchnie\",\"Workspaces and groups\":\"Obszary robocze i grupy\",\"Layouts, templates and assignments\":\"Układy, szablony i zadania\",\"Data and backup\":\"Dane i kopie zapasowe\",\"Import, export and recovery\":\"Import, eksport i odzyskiwanie\",\"Windows and performance\":\"Windows i wydajność\",\"Resource profiles and preview limits\":\"Profile zasobów i limity podglądu\",\"Privacy\":\"Prywatność\",\"History, private session and local data\":\"Historia, sesja prywatna i dane lokalne\",\"Diagnostics\":\"Diagnostyka\",\"Self-check, routes and recovery\":\"Samokontrola, trasy i odzyskiwanie\",\"RoomGrid main menu\":\"Menu główne RoomGrid\",\"All pages, settings and tools in one place\":\"Wszystkie strony, ustawienia i narzędzia w jednym miejscu\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Menu pozostaje w widocznym oknie; wszystkie dane RoomGrid pozostają lokalne.\",\"RoomGrid pages\":\"strony RoomGrid\",\"Statistics\":\"Statystyki\",\"Customize original homepage\":\"Dostosuj oryginalną stronę główną\",\"Homepage\":\"Strona główna\",\"Filter homepage by your own groups\":\"Filtruj stronę główną według własnych grup\",\"Groups\":\"Grupy\",\"Activity trend\":\"Trend aktywności\",\"Link\":\"Połączyć\",\"Public profile text and links\":\"Tekst i linki do profilu publicznego\",\"Last known public preview\":\"Ostatnia znana publiczna wersja zapoznawcza\",\"Current public preview\":\"Aktualny publiczny podgląd\",\"Offline may only provide a placeholder\":\"Tryb offline może zapewnić jedynie symbol zastępczy\",\"Example images and covers\":\"Przykładowe zdjęcia i okładki\",\"Reading local image archive…\":\"Czytanie lokalnego archiwum obrazów…\",\"Save new snapshot\":\"Zapisz nową migawkę\",\"Saving public previews…\":\"Zapisywanie publicznych podglądów…\",\"No saved image yet; current public previews are still shown.\":\"Nie zapisano jeszcze obrazu; bieżące publiczne podglądy są nadal wyświetlane.\",\"Current public cover\":\"Aktualna okładka publiczna\",\"Current compact preview\":\"Aktualny podgląd kompaktowy\",\"Current live image\":\"Aktualny obraz na żywo\",\"Room\":\"Pokój\",\"More\":\"Więcej\",\"Unwatch\":\"Przestań oglądać\",\"Data sources and freshness\":\"Źródła danych i świeżość\",\"Data source\":\"Źródło danych\",\"Full local data set\":\"Pełny zestaw danych lokalnych\",\"Matching filters\":\"Pasujące filtry\",\"Shown on this page\":\"Pokazane na tej stronie\",\"Latest live signals\":\"Najnowsze sygnały na żywo\",\"With watch history\":\"Z historią oglądania\",\"Public-profile coverage\":\"Relacja w profilu publicznym\",\"Country coverage\":\"Zasięg kraju\",\"Age coverage\":\"Zakres wieku\",\"Tag coverage\":\"Zasięg tagu\",\"Which data set is being searched?\":\"Który zbiór danych jest przeszukiwany?\",\"Last local observation\":\"Ostatnia lokalna obserwacja\",\"No observation time yet\":\"Nie ma jeszcze czasu na obserwację\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Publiczny = z bieżących stron Chaturbate lub katalogu na żywo. Osobiste = zapisane, oznaczone lub pogrupowane przez Ciebie. Lokalne = sesje, podglądy lub wartości jakości obserwowane tylko na tym urządzeniu. Nieznane pola pozostają nieznane i nigdy się ich nie odgadnie.\",\"Local model statistics\":\"Statystyki modelu lokalnego\",\"Automatically recorded · personally organized\":\"Nagrywane automatycznie · organizowane osobiście\",\"Automatically recorded in the growing model catalog\":\"Automatycznie rejestrowane w rosnącym katalogu modeli\",\"Total\":\"Razem\",\"Avg. session\":\"Średnia sesja\",\"Active days\":\"Aktywne dni\",\"Share\":\"Udostępnij\",\"Longest session\":\"Najdłuższa sesja\",\"Previews\":\"Podglądy\",\"Quick views\":\"Szybkie podglądy\",\"Data sources\":\"Źródła danych\",\"Edit in library\":\"Edytuj w bibliotece\",\"No experience data yet\":\"Brak danych na temat doświadczeń\",\"Online observations will be collected locally from now on.\":\"Od teraz obserwacje online będą zbierane lokalnie.\",\"Open\":\"Otwórz\",\"Good trend\":\"Dobry trend\",\"Moderate trend\":\"Umiarkowany trend\",\"Initial trend\":\"Początkowy trend\",\"First observation\":\"Pierwsza obserwacja\",\"Estimated from watch time\":\"Szacowane na podstawie czasu oglądania\",\"weekday open\":\"otwarte w dni powszednie\",\"time open\":\"czas otwarty\",\"Preview image\":\"Obraz podglądu\",\"Biography / room subject\":\"Biografia / temat pokoju\",\"Catalog state\":\"Stan katalogu\",\"Live status\":\"Status transmisji\",\"Viewer count\":\"Liczba widzów\",\"Country/region\":\"Kraj/region\",\"Total watch time\":\"Łączny czas oglądania\",\"Average session\":\"Średnia sesja\",\"First seen\":\"Pierwsze wyświetlenie\",\"Last seen\":\"Ostatnie wyświetlenie\",\"Last live\":\"Ostatnia transmisja\",\"Approximate online times\":\"Przybliżone godziny online\",\"Activity, sources and profile state\":\"Aktywność, źródła i stan profilu\",\"Example images\":\"Przykładowe obrazy\",\"Own groups\":\"Własne grupy\",\"Public/recognized tags\":\"Publiczne/rozpoznane tagi\",\"Notes, own tags and priority\":\"Notatki, własne tagi i priorytet\",\"Local rating\":\"Ocena lokalna\",\"Group assignment\":\"Przypisanie do grup\",\"Room and preview actions\":\"Działania pokoju i podglądu\",\"Library display\":\"Wygląd biblioteki\",\"Saved in this browser\":\"Zapisano w tej przeglądarce\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Wybierz rozmiar kart, liczbę modeli w wierszu i na stronie oraz dokładne informacje wyświetlane na każdej karcie.\",\"Compact overview\":\"Widok kompaktowy\",\"Balanced\":\"Zrównoważony\",\"All details\":\"Wszystkie szczegóły\",\"Show all details\":\"Pokaż wszystkie szczegóły\",\"Core details only\":\"Tylko najważniejsze informacje\",\"Card zoom\":\"Powiększenie kart\",\"Visible information per model\":\"Widoczne informacje o modelu\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Przy automatycznych kolumnach powiększenie określa liczbę modeli obok siebie. Dokładna liczba kolumn ma pierwszeństwo. Wszystkie ustawienia są zapisywane w tej przeglądarce.\",\"No custom tags yet\":\"Nie ma jeszcze żadnych tagów niestandardowych\",\"Name of the new group\":\"Nazwa nowej grupy\",\"Models remain in the library.\":\"Modele pozostają w bibliotece.\",\"Custom groups\":\"Grupy niestandardowe\",\"Create new group\":\"Utwórz nową grupę\",\"Nothing selected — show all\":\"Nic nie wybrano — pokaż wszystko\",\"Any\":\"Dowolny\",\"Women\":\"Kobiety\",\"Men\":\"Mężczyźni\",\"Couples\":\"Pary\",\"Unknown\":\"Nieznany\",\"Error/loading\":\"Błąd/ładowanie\",\"Every weekday\":\"Każdy dzień powszedni\",\"Sunday\":\"niedziela\",\"Monday\":\"poniedziałek\",\"Tuesday\":\"wtorek\",\"Wednesday\":\"środa\",\"Thursday\":\"czwartek\",\"Friday\":\"piątek\",\"Saturday\":\"sobota\",\"Every time\":\"Za każdym razem\",\"Advanced filters and sorting\":\"Zaawansowane filtry i sortowanie\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Połącz dowolną liczbę filtrów. Wiele płci, stanów, krajów i tagów działa jako wybór wielokrotny. Każde ustawienie pozostaje zapisane w tej przeglądarce.\",\"Gender/type\":\"Płeć/typ\",\"Countries/regions (multiple)\":\"Kraje/regiony (wiele)\",\"Add country…\":\"Dodaj kraj…\",\"Public tags (multiple)\":\"Tagi publiczne (wiele)\",\"Add tag…\":\"Dodaj tag…\",\"Public-tag matching\":\"Dopasowywanie tagów publicznych\",\"At least one tag\":\"Przynajmniej jeden tag\",\"Every selected tag\":\"Każdy wybrany tag\",\"Catalog source\":\"Źródło katalogu\",\"All models\":\"Wszystkie modele\",\"Personally organized only\":\"Tylko organizowane osobiście\",\"Automatically discovered only\":\"Tylko automatycznie wykryte\",\"Profile data\":\"Dane profilu\",\"Loaded or missing\":\"Załadowano lub brak\",\"Loaded only\":\"Tylko załadowane\",\"Missing only\":\"Brakuje tylko\",\"Picture/category\":\"Zdjęcie/kategoria\",\"Neither HD nor gaming\":\"Ani HD, ani gry\",\"Watch history\":\"Oglądaj historię\",\"Never watched\":\"Nigdy nie oglądałem\",\"Own tags\":\"Własne tagi\",\"With or without\":\"Z lub bez\",\"With tags only\":\"Tylko z tagami\",\"Without tags only\":\"Tylko bez tagów\",\"At least one group\":\"Przynajmniej jedna grupa\",\"No group\":\"Brak grupy\",\"With notes only\":\"Tylko z notatkami\",\"Without notes only\":\"Tylko bez notatek\",\"Numeric ranges and minimums\":\"Zakresy liczbowe i minima\",\"Minimum age\":\"Minimalny wiek\",\"Maximum age\":\"Maksymalny wiek\",\"Minimum viewers\":\"Minimalna liczba widzów\",\"Maximum viewers\":\"Maksymalna liczba widzów\",\"Minimum followers\":\"Minimalna liczba obserwujących\",\"Maximum followers\":\"Maksymalna liczba obserwujących\",\"Minimum total watch time\":\"Minimalny łączny czas oglądania\",\"Currently online for at least\":\"Obecnie online przez co najmniej\",\"Minimum sessions\":\"Minimalne sesje\",\"Minimum rating\":\"Minimalna ocena\",\"Minimum priority\":\"Minimalny priorytet\",\"Active within\":\"Aktywny wewnątrz\",\"days\":\"dni\",\"Minimum appearances\":\"Minimalne występy\",\"Minimum live observations\":\"Minimalne obserwacje na żywo\",\"Filter by typical online time\":\"Filtruj według typowego czasu online\",\"Weekday\":\"Dzień powszedni\",\"Hour (local time)\":\"Godzina (czas lokalny)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Pola zerowe lub puste oznaczają brak limitu. Nieznany wiek jest wykluczony, jeśli limit wieku jest aktywny. Filtry czasu wykorzystują czasy na żywo obserwowane na tym urządzeniu i w razie potrzeby ostrożnie cofają się, aby obejrzeć czasy.\",\"Reset all library filters\":\"Zresetuj wszystkie filtry biblioteczne\",\"Open live room\":\"Otwórz pokój na żywo\",\"Open room page\":\"Otwórz stronę pokoju\",\"Open model room in a new tab\":\"Otwórz pokój modelowy w nowej karcie\",\"Open the Chaturbate single-model view in a new tab\":\"Otwórz widok pojedynczego modelu Chaturbate w nowej karcie\",\"Save profile\":\"Zapisz profil\",\"Restore\":\"Przywróć\",\"Archive\":\"Archiwum\",\"Advanced statistics filters\":\"Zaawansowane filtry statystyczne\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Każdy filtr można łączyć; kraje, tagi, grupy i typy obsługują wielokrotny wybór. Wybory są zapisywane na stałe.\",\"Own groups (multiple)\":\"Grupy własne (wiele)\",\"Add group…\":\"Dodaj grupę…\",\"Tag matching\":\"Dopasowanie tagów\",\"At least one\":\"Przynajmniej jeden\",\"All selected tags\":\"Wszystkie wybrane tagi\",\"Group matching\":\"Dopasowanie grupowe\",\"All selected groups\":\"Wszystkie wybrane grupy\",\"Cover/example image\":\"Okładka/przykładowe zdjęcie\",\"Known public image available\":\"Dostępny znany publiczny wizerunek\",\"No known image\":\"Brak znanego obrazu\",\"Currently live\":\"Obecnie na żywo\",\"Maximum total watch time\":\"Maksymalny łączny czas oglądania\",\"Maximum sessions\":\"Maksymalna liczba sesji\",\"Minimum active days\":\"Minimalna liczba dni aktywnych\",\"Minimum previews\":\"Minimalne podglądy\",\"Minimum quick views\":\"Minimalne szybkie wyświetlenia\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Pola zerowe lub puste oznaczają brak limitu. Nieznanych wartości nigdy się nie odgaduje; przedziały wiekowe wykluczają nieznany wiek.\",\"Reset all statistics filters\":\"Zresetuj wszystkie filtry statystyk\",\"Search name, tags or notes…\":\"Wyszukaj nazwę, znaczniki lub notatki…\",\"Search\":\"Szukaj\",\"Sorting\":\"Sortowanie\",\"Most watch time\":\"Większość czasu oglądania\",\"Least watch time\":\"Najkrótszy czas oglądania\",\"Recently active\":\"Ostatnio aktywny\",\"Longest inactive\":\"Najdłużej nieaktywny\",\"Most sessions\":\"Większość sesji\",\"Fewest sessions\":\"Najmniej sesji\",\"Longest average session\":\"Najdłuższa średnia sesja\",\"Shortest average session\":\"Najkrótsza średnia sesja\",\"Longest single session\":\"Najdłuższa pojedyncza sesja\",\"Most active days\":\"Najbardziej aktywne dni\",\"Highest watch share\":\"Najwyższy udział w oglądalności\",\"Most previews\":\"Większość podglądów\",\"Most quick views\":\"Najszybsze wyświetlenia\",\"Most discoveries\":\"Większość odkryć\",\"Most followers\":\"Większość obserwujących\",\"Youngest first\":\"Najpierw najmłodszy\",\"Oldest first\":\"Najpierw najstarszy\",\"Highest rating\":\"Najwyższa ocena\",\"Highest priority\":\"Najwyższy priorytet\",\"Most appearances\":\"Większość występów\",\"Most live observations\":\"Większość obserwacji na żywo\",\"First observed\":\"Pierwszy zaobserwowany\",\"Country A–Z\":\"Kraj A–Z\",\"Most public tags\":\"Większość tagów publicznych\",\"Most groups\":\"Większość grup\",\"Best technical quality\":\"Najlepsza jakość techniczna\",\"Cover/example images first\":\"Najpierw okładki/przykładowe zdjęcia\",\"All statuses\":\"Wszystkie statusy\",\"Live only\":\"Tylko na żywo\",\"Library only\":\"Tylko biblioteka\",\"View\":\"Widok\",\"Active library\":\"Aktywna biblioteka\",\"Live now\":\"Żyj teraz\",\"With notes\":\"Z notatkami\",\"Newest saved/discovered\":\"Najnowsze zapisane/odkryte\",\"Oldest saved/discovered\":\"Najstarszy zapisany/odkryty\",\"Lowest rating\":\"Najniższa ocena\",\"Lowest priority\":\"Najniższy priorytet\",\"Live longest now\":\"Żyj teraz najdłużej\",\"Age youngest first\":\"Najpierw najmłodszy wiek\",\"Age oldest first\":\"Najpierw najstarszy\",\"Most recently live\":\"Ostatnio na żywo\",\"First discovered\":\"Najpierw odkryto\",\"Most own groups\":\"Większość ma własne grupy\",\"Card size\":\"Rozmiar karty\",\"Compact\":\"Kompaktowy\",\"Comfortable\":\"Wygodne\",\"Large\":\"Duży\",\"Cinema\":\"Kino\",\"Extra large\":\"Bardzo duże\",\"Models per row\":\"Modele w wierszu\",\"Automatic for window\":\"Automatycznie dla okna\",\"Recorded models\":\"Nagrane modele\",\"Total cam time\":\"Całkowity czas kamery\",\"across all models\":\"we wszystkich modelach\",\"Dock + hover\":\"Dokowanie + najechanie\",\"Last 30 days\":\"Ostatnie 30 dni\",\"local events\":\"wydarzenia lokalne\",\"Top model\":\"Najlepszy model\",\"no watch time yet\":\"nie ma jeszcze czasu oglądania\",\"Deeper analysis\":\"Głębsza analiza\",\"Open Analytics Studio\":\"Otwórz Studio Analytics\",\"Late night\":\"Późna noc\",\"Morning\":\"Rano\",\"Daytime\":\"Dzień\",\"Evening\":\"Wieczór\",\"Gaming\":\"Gry\",\"Music & DJ\":\"Muzyka i DJ\",\"Dance\":\"Taniec\",\"Cosplay & anime\":\"Cosplay i anime\",\"Fitness & sports\":\"Fitness i sport\",\"Talk & social\":\"Rozmowy i kontakty towarzyskie\",\"Outdoor & travel\":\"Na świeżym powietrzu i w podróży\",\"Roleplay\":\"Odgrywanie ról\",\"Performance\":\"Wydajność\",\"My interest statistics\":\"Moje statystyki zainteresowań\",\"All local data\":\"Wszystkie dane lokalne\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Obliczane na podstawie rzeczywistego lokalnego czasu oglądania, sesji, podglądów, szybkich wyświetleń, ulubionych i aktualnie znanych informacji o modelach publicznych.\",\"Favorite tag\":\"Ulubiony tag\",\"not enough data\":\"za mało danych\",\"Favorite genre\":\"Ulubiony gatunek\",\"from public tags\":\"z tagów publicznych\",\"Most-watched age\":\"Najczęściej oglądany wiek\",\"no age data\":\"brak danych o wieku\",\"Most-watched country\":\"Najczęściej oglądany kraj\",\"no country data\":\"brak danych krajowych\",\"Favorite model type\":\"Ulubiony typ modelu\",\"no type data\":\"brak danych typu\",\"Favorite viewing time\":\"Ulubiona pora oglądania\",\"no time data\":\"brak danych czasowych\",\"Top-three concentration\":\"Stężenie w pierwszej trójce\",\"watch time on the top three models\":\"czas oglądania trzech najlepszych modelek\",\"Favorite affinity\":\"Ulubione powinowactwo\",\"watch time on current favorites\":\"czas oglądania na bieżących ulubionych\",\"watch time on HD models\":\"czas oglądania na modelach HD\",\"Metadata coverage\":\"Zasięg metadanych\",\"age\":\"wiek\",\"country\":\"kraj\",\"Tags, genres and browsing interests\":\"Tagi, gatunki i zainteresowania podczas przeglądania\",\"Tags by watch time\":\"Tagi według czasu oglądania\",\"Your ranking appears when watched models have public tags.\":\"Twój ranking pojawia się, gdy obserwowane modele mają publiczne tagi.\",\"Favorite genre worlds\":\"Ulubione światy gatunkowe\",\"No genre world can yet be assigned from public tags.\":\"Do tagów publicznych nie można jeszcze przypisać żadnego świata gatunków.\",\"Your own tags\":\"Twoje własne tagi\",\"Your saved custom tags are also evaluated here by watch time.\":\"Twoje zapisane tagi niestandardowe są tutaj również oceniane według czasu oglądania.\",\"Preview-interest tags\":\"Podgląd tagów zainteresowań\",\"Browsing interests appear when you use live previews or quick views.\":\"Zainteresowania przeglądaniem pojawiają się, gdy korzystasz z podglądów na żywo lub szybkich widoków.\",\"Most-previewed models\":\"Najczęściej oglądane modele\",\"No previews have been recorded in this period yet.\":\"W tym okresie nie zarejestrowano jeszcze żadnych zapowiedzi.\",\"Age, origin and model type\":\"Wiek, pochodzenie i typ modelu\",\"Age groups by watch time\":\"Grupy wiekowe według czasu oglądania\",\"Not enough public age data is available.\":\"Nie ma wystarczających publicznych danych dotyczących wieku.\",\"Countries by watch time\":\"Kraje według czasu oglądania\",\"Not enough public country data is available.\":\"Dostępnych jest za mało publicznych danych o krajach.\",\"Most-watched places\":\"Najczęściej oglądane miejsca\",\"Not enough public location data is available.\":\"Nie ma wystarczających danych o lokalizacji publicznej.\",\"Model types by watch time\":\"Typy modeli według czasu oglądania\",\"Not enough public model-type data is available.\":\"Nie ma wystarczającej ilości danych dotyczących typu modelu publicznego.\",\"Viewing rhythm and favorite signals\":\"Rytm oglądania i sygnały ulubionych\",\"Most active weekdays\":\"Najbardziej aktywne dni powszednie\",\"Not enough visit events are available.\":\"Nie ma wystarczającej liczby wydarzeń związanych z wizytami.\",\"Times of day\":\"Pory dnia\",\"Tags among favorites\":\"Tagi wśród ulubionych\",\"Preference signals appear when you mark models as favorites.\":\"Sygnały preferencji pojawiają się, gdy oznaczysz modele jako ulubione.\",\"Countries among favorites\":\"Kraje w gronie ulubionych\",\"No known countries among favorites yet.\":\"Nie ma jeszcze znanych krajów wśród ulubionych.\",\"Age groups among favorites\":\"Grupy wiekowe wśród ulubionych\",\"No known ages among favorites yet.\":\"Nie ma jeszcze znanego wieku wśród ulubionych.\",\"Model types among favorites\":\"Typy modeli wśród ulubionych\",\"No known model types among favorites yet.\":\"Nie ma jeszcze znanych typów modeli wśród ulubionych.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Wyniki te nie są wysyłane na serwer. Do preferencji oglądania wliczany jest tylko rzeczywisty czas oglądania; podglądy i ulubione są wyświetlane oddzielnie. Brak danych dotyczących wieku, kraju, lokalizacji lub znacznika nigdy nie jest zgadywany.\",\"Open Following on Chaturbate\":\"Otwórz obserwowanie na Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Nie można odzyskać z mocą wsteczną pełnej historii czasu oglądania ani odwiedzin, ponieważ Chaturbate nie zapewnia udokumentowanego interfejsu API historii osobistej. Możesz poprosić o kopię przechowywanych danych osobowych poprzez oficjalny eksport danych Chaturbate. Jeśli ten eksport zawiera użyteczne pola historii, RoomGrid może obsłużyć bezpieczny import po sprawdzeniu formatu pliku. Oficjalny interfejs Stats API dostarcza aktualnych wartości nadawcy; RoomGrid może budować własną historię dopiero od pierwszego połączenia.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Porównuj każdy lokalnie obserwowany, zapisany i oglądany model — pod względem czasu oglądania, sesji, aktywności, podglądów, jakości i rankingów.\",\"Large model library\":\"Duża biblioteka modeli\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Twoje modele w postaci bogatych w obrazy, wygodnych profili ze statusem na żywo, notatkami, tagami, ocenami, priorytetami, grupami i akcjami bezpośrednimi.\",\"My user dashboard\":\"Mój panel użytkownika\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Twoja prywatna historia lokalnego użytkowania, nawyki, dane biblioteczne i – tylko w przypadku celowego połączenia – dane oficjalnych kont nadawcy.\",\"🔒 Local & private on this device\":\"🔒 Lokalnie i prywatnie na tym urządzeniu\",\"Preparing data…\":\"Przygotowywanie danych…\",\"RoomGrid Comfort Center\":\"Centrum sterowania RoomGrid\",\"inside the standard Chaturbate page\":\"na standardowej stronie Chaturbate\",\"All RoomGrid pages and settings\":\"Wszystkie strony i ustawienia RoomGrid\",\"Media paused; names and images are covered.\":\"Multimedia wstrzymane; nazwy i obrazy są zakryte.\",\"Restore previous view\":\"Przywróć poprzedni widok\",\"Enter local PIN\":\"Wprowadź lokalny PIN\",\"In-page notifications\":\"Powiadomienia na stronie\",\"No new notifications\":\"Brak nowych powiadomień\",\"Notifications are off\":\"Powiadomienia są wyłączone\",\"RoomGrid notification\":\"Powiadomienie RoomGrid\",\"Dismiss as read\":\"Odrzuć jako przeczytane\",\"More unread notifications\":\"Więcej nieprzeczytanych powiadomień\",\"RoomGrid update\":\"Aktualizacja RoomGrid\",\"A monitored source changed status.\":\"Monitorowane źródło zmieniło status.\",\"This is a local RoomGrid test notification.\":\"To jest lokalne powiadomienie o teście RoomGrid.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Test udany: powiadomienia mogą pojawiać się bezpośrednio na pasku RoomGrid.\",\"Settings\":\"Ustawienia\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Obszary robocze, biblioteka, projekt, dane, historia, powiadomienia, prywatność i wydajność\",\"Workspaces\":\"Obszary robocze\",\"Data\":\"Dane\",\"History & statistics\":\"Historia i statystyki\",\"Broadcaster stats\":\"Statystyki nadawcy\",\"Notifications\":\"Powiadomienia\",\"Windows & performance\":\"Windows i wydajność\",\"Optional tools\":\"Opcjonalne narzędzia\",\"Settings pages\":\"Strony ustawień\",\"RoomGrid control center\":\"Centrum sterowania RoomGrid\",\"Any online model\":\"Dowolny model online\",\"Workspace name\":\"Nazwa obszaru roboczego\",\"Optional description\":\"Opcjonalny opis\",\"Active\":\"Aktywny\",\"No description\":\"Brak opisu\",\"Name\":\"Imię\",\"Description\":\"Opis\",\"Replace offline models\":\"Wymień modele offline\",\"Replacement group\":\"Grupa zastępcza\",\"Delay seconds\":\"Opóźnij sekundy\",\"Save current state\":\"Zapisz bieżący stan\",\"New window\":\"Nowe okno\",\"Export this workspace\":\"Wyeksportuj ten obszar roboczy\",\"Duplicate\":\"Duplikat\",\"Workspace restored\":\"Obszar roboczy został przywrócony\",\"Workspace archived\":\"Obszar roboczy zarchiwizowany\",\"Delete permanently\":\"Usuń trwale\",\"Permanently delete this workspace?\":\"Trwale usunąć ten obszar roboczy?\",\"Saved layout and panel sizes\":\"Zapisany układ i rozmiary paneli\",\"Apply\":\"Zastosuj\",\"Layout template deleted\":\"Szablon układu został usunięty\",\"Parent group\":\"Grupa rodziców\",\"None\":\"Żadne\",\"Group description\":\"Opis grupy\",\"Save current workspace\":\"Zapisz bieżący obszar roboczy\",\"Save layout template\":\"Zapisz szablon układu\",\"Template name\":\"Nazwa szablonu\",\"Layout templates\":\"Szablony układów\",\"Device layout\":\"Układ urządzenia\",\"Groups and subgroups\":\"Grupy i podgrupy\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Każdy obszar roboczy przechowuje oddzielne układy pulpitu i układy kompaktowe. Automatyczne zastępowanie jest domyślnie wyłączone i wypełnia miejsca offline dopiero po włączeniu tej funkcji dla tego obszaru roboczego.\",\"No templates yet.\":\"Nie ma jeszcze żadnych szablonów.\",\"Group name\":\"Nazwa grupy\",\"New subgroup\":\"Nowa podgrupa\",\"Enter parent group name\":\"Wprowadź nazwę grupy nadrzędnej\",\"Subgroup name\":\"Nazwa podgrupy\",\"Name, note or local tag\":\"Nazwa, notatka lub tag lokalny\",\"Choose group…\":\"Wybierz grupę…\",\"Add selected models to group\":\"Dodaj wybrane modele do grupy\",\"Watch later\":\"Obejrzyj później\",\"Paused\":\"Wstrzymano\",\"Collection name\":\"Nazwa kolekcji\",\"Tags, comma separated\":\"Tagi, oddzielone przecinkami\",\"Countries, comma separated\":\"Kraje, oddzielone przecinkami\",\"All types\":\"Wszystkie typy\",\"Rule name\":\"Nazwa reguły\",\"Required tags\":\"Wymagane tagi\",\"Local alert\":\"Alarm lokalny\",\"Add to favorites\":\"Dodaj do ulubionych\",\"Add to group\":\"Dodaj do grupy\",\"Any tag\":\"Dowolny znacznik\",\"Any country\":\"Dowolny kraj\",\"Pause\":\"Pauza\",\"Enable\":\"Włącz\",\"Delete\":\"Usuń\",\"Automation rule deleted\":\"Reguła automatyzacji została usunięta\",\"Select model\":\"Wybierz model\",\"Later\":\"Później\",\"Never visited\":\"Nigdy nie odwiedzane\",\"Last online\":\"Ostatni w Internecie\",\"Last visited\":\"Ostatnio odwiedzane\",\"Local note\":\"Uwaga lokalna\",\"Local tags, comma separated\":\"Tagi lokalne, oddzielone przecinkami\",\"Remove later\":\"Usuń później\",\"Hide today\":\"Ukryj się dzisiaj\",\"Pause 7 days\":\"Wstrzymaj 7 dni\",\"Never suggest\":\"Nigdy nie sugeruj\",\"Select results\":\"Wybierz wyniki\",\"Clear selection\":\"Wyczyść wybór\",\"selected\":\"wybrane\",\"Add to watch later\":\"Dodaj do obejrzenia później\",\"Permanently delete selected models?\":\"Trwale usunąć wybrane modele?\",\"Smart collections\":\"Inteligentne kolekcje\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Kolekcje aktualizują się automatycznie wraz ze wzrostem lokalnych metadanych i statystyk; modele nie są powielane.\",\"Tags\":\"Tagi\",\"Countries\":\"Kraje\",\"Favorites only\":\"Tylko ulubione\",\"Online only\":\"Tylko online\",\"Save smart collection\":\"Zapisz inteligentną kolekcję\",\"Delete current collection\":\"Usuń bieżącą kolekcję\",\"Delete this smart collection? Model data stays unchanged.\":\"Usunąć tę inteligentną kolekcję? Dane modelu pozostają niezmienione.\",\"Rules & automation center\":\"Centrum reguł i automatyzacji\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Gdy dane modelu lokalnego są zgodne, wyświetl jeden alert, dodaj ulubiony lub dodaj model do wybranej grupy.\",\"Action\":\"Akcja\",\"Target group\":\"Grupa docelowa\",\"Save rule\":\"Zapisz regułę\",\"No automation rules yet.\":\"Nie ma jeszcze reguł automatyzacji.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Nazwy użytkowników są deduplikowane poprzez dokładne znormalizowane dopasowanie. Wyszukiwanie obejmuje nazwy, notatki i lokalne znaczniki. Archiwizacja, usunięcie z widoku i trwałe usunięcie to odrębne działania.\",\"No matching models.\":\"Brak pasujących modeli.\",\"export\":\"eksport\",\"Models and notes\":\"Modele i notatki\",\"This backup could not be read. Current data was not changed.\":\"Nie można odczytać tej kopii zapasowej. Aktualne dane nie uległy zmianie.\",\"Import preview (nothing changed yet)\":\"Podgląd importu (jeszcze nic się nie zmieniło)\",\"conflicts\":\"konflikty\",\"Includes sensitive statistics\":\"Zawiera wrażliwe statystyki\",\"models\":\"modele\",\"events\":\"wydarzenia\",\"quality samples\":\"próbki jakości\",\"Import statistics too\":\"Importuj także statystyki\",\"Import method\":\"Metoda importu\",\"Merge (recommended)\":\"Scal (zalecane)\",\"Replace selected sections\":\"Zamień wybrane sekcje\",\"Apply import\":\"Zastosuj import\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Import zakończony. Kopia zapasowa poprzedniej konfiguracji i statystyk została utworzona osobno.\",\"The import could not be saved safely. The backup remains available.\":\"Nie udało się bezpiecznie zapisać importu. Kopia zapasowa pozostaje dostępna.\",\"Cancel preview\":\"Anuluj podgląd\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Ten plik zawiera dane modelu, notatki lub obszaru roboczego i należy go traktować jako poufny.\",\"This import is not marked sensitive.\":\"Ten import nie jest oznaczony jako poufny.\",\"Data version\":\"Wersja danych\",\"Software version\":\"Wersja oprogramowania\",\"Revision\":\"Wersja\",\"Local models\":\"Modele lokalne\",\"Selective export\":\"Eksport selektywny\",\"Export selected\":\"Wyeksportuj wybrane\",\"Full export\":\"Pełny eksport\",\"Complete safety package\":\"Kompletny pakiet bezpieczeństwa\",\"Export statistics separately\":\"Eksportuj statystyki osobno\",\"Choose file and preview\":\"Wybierz plik i podgląd\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Kompletny pakiet bezpieczeństwa zawiera konfigurację i wrażliwe statystyki, ale nigdy oficjalny token statystyk. Każdy import jest przeglądany i przed zastosowaniem tworzy kopię zapasową konfiguracji i statystyk.\",\"Storage and backup center\":\"Centrum przechowywania i tworzenia kopii zapasowych\",\"Local storage\":\"Magazyn lokalny\",\"Site storage\":\"Przechowywanie witryny\",\"Available quota\":\"Dostępny limit\",\"Internal statistics backups\":\"Wewnętrzne kopie zapasowe statystyk\",\"Configuration\":\"Konfiguracja\",\"workspaces\":\"obszary robocze\",\"LocalStorage + update vault\":\"LocalStorage + skarbiec aktualizacji\",\"Statistics database\":\"Baza danych statystycznych\",\"Secrets\":\"Sekrety\",\"The official statistics token is never included in normal or complete backups.\":\"Token oficjalnych statystyk nigdy nie jest uwzględniany w normalnych lub pełnych kopiach zapasowych.\",\"Excluded\":\"wykluczone\",\"Create configuration restore point\":\"Utwórz punkt przywracania konfiguracji\",\"Create statistics restore point\":\"Utwórz punkt przywracania statystyk\",\"Legacy cleanup\":\"Sprzątanie dziedzictwa\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Starsze dane nigdy nie są usuwane automatycznie. Usuń stare klucze v6/v7 dopiero po pomyślnym sprawdzeniu.\",\"Remove old v6/v7 copies\":\"Usuń stare kopie v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Usunąć tylko stare kopie v6/v7? Aktualne dane i kopie zapasowe pozostają.\",\"items removed\":\"elementy usunięte\",\"Aurora\":\"Zorza\",\"Blue and teal, the RoomGrid signature\":\"Niebieski i turkusowy, sygnatura RoomGrid\",\"Ember\":\"Żar\",\"Warm orange and rose\":\"Ciepły pomarańcz i róż\",\"Frost\":\"Mróz\",\"Crisp cyan and violet\":\"Wyraźny cyjan i fiolet\",\"Custom\":\"Niestandardowe\",\"Use the colors selected below\":\"Użyj kolorów wybranych poniżej\",\"Name the current appearance\":\"Nazwij obecny wygląd\",\"My appearance\":\"Mój wygląd\",\"Delete appearance profile\":\"Usuń profil wyglądu\",\"This appearance profile could not be imported.\":\"Nie można zaimportować tego profilu wyglądu.\",\"Light\":\"Światło\",\"Dark\":\"Ciemny\",\"Export\":\"Eksportuj\",\"Home\":\"Dom\",\"Listings and tags\":\"Listy i tagi\",\"Model pages\":\"Strony modelowe\",\"Use global appearance\":\"Użyj wyglądu globalnego\",\"This logo format is unsafe or unsupported.\":\"Ten format logo jest niebezpieczny lub nieobsługiwany.\",\"The logo file could not be read.\":\"Nie można odczytać pliku logo.\",\"MultiCam tools\":\"Narzędzia MultiCam\",\"Page\":\"Strona\",\"Panel\":\"Panelu\",\"Card surface\":\"Powierzchnia karty\",\"Input\":\"Wejście\",\"Border\":\"Granica\",\"Text\":\"Tekst\",\"Muted text\":\"Wyciszony tekst\",\"Success\":\"Sukces\",\"Warning\":\"Ostrzeżenie\",\"Danger\":\"Niebezpieczeństwo\",\"Info and focus\":\"Informacje i skupienie\",\"Data hub\":\"Centrum danych\",\"Text / page\":\"Tekst / strona\",\"Text / panel\":\"Tekst / panel\",\"Muted text / panel\":\"Wyciszony tekst/panel\",\"Accent text / accent\":\"Tekst akcentujący / akcent\",\"Desktop\":\"Pulpit\",\"Tablet\":\"Tabletka\",\"Mobile\":\"Mobilny\",\"Brand and logo studio\":\"Studio marki i logo\",\"Tagline\":\"Hasło\",\"Brand display\":\"Wyświetlacz marki\",\"Full name\":\"Pełne imię i nazwisko\",\"Uploaded logo\":\"Przesłane logo\",\"Logo shape\":\"Kształt logo\",\"Rounded\":\"Zaokrąglone\",\"Circle\":\"Okrąg\",\"Square\":\"Kwadrat\",\"Pill\":\"Pigułka\",\"Page title\":\"Tytuł strony\",\"Website original\":\"Oryginał witryny\",\"Brand plus original\":\"Marka plus oryginał\",\"Custom title\":\"Tytuł niestandardowy\",\"Custom page title\":\"Niestandardowy tytuł strony\",\"Image fit\":\"Dopasowanie obrazu\",\"Horizontal position\":\"Pozycja pozioma\",\"Vertical position\":\"Pozycja pionowa\",\"Upload light-mode logo\":\"Prześlij logo trybu jasnego\",\"Upload dark-mode logo\":\"Prześlij logo trybu ciemnego\",\"Remove logos\":\"Usuń logo\",\"Show version\":\"Pokaż wersję\",\"Use logo as tab icon on RoomGrid pages\":\"Użyj logo jako ikony karty na stronach RoomGrid\",\"Replace website logo in the original header (local only)\":\"Zamień logo witryny w oryginalnym nagłówku (tylko lokalnie)\",\"Complete color system\":\"Kompletny system kolorów\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Oprócz akcentów kontroluj kolory strony, panelu, karty, danych wejściowych, obramowania, tekstu i stanu.\",\"Derived automatically\":\"Wyprowadzane automatycznie\",\"Fully custom\":\"W pełni niestandardowe\",\"Editing dark palette\":\"Edycja ciemnej palety\",\"Editing light palette\":\"Edycja palety świateł\",\"Reset current palette\":\"Zresetuj bieżącą paletę\",\"Layout editor\":\"Edytor układu\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Ustaw spójne odstępy między treściami, szerokość karty, nagłówek i pasek boczny na stronach RoomGrid.\",\"Minimum card width\":\"Minimalna szerokość karty\",\"Section spacing\":\"Rozstaw sekcji\",\"Content padding\":\"Dopełnienie treści\",\"Header height\":\"Wysokość nagłówka\",\"Maximum columns\":\"Maksymalna liczba kolumn\",\"Automatic\":\"Automatyczny\",\"Card aspect\":\"Aspekt karty\",\"Sidebar position\":\"Pozycja paska bocznego\",\"Left\":\"Lewy\",\"Right\":\"Jasne\",\"Keep header visible while scrolling\":\"Zachowaj widoczność nagłówka podczas przewijania\",\"Live device preview\":\"Podgląd urządzenia na żywo\",\"Contrast and accessibility\":\"Kontrast i dostępność\",\"High-contrast mode\":\"Tryb wysokiego kontrastu\",\"Underline links\":\"Podkreśl linki\",\"Reduce transparency\":\"Zmniejsz przezroczystość\",\"Boost base text size\":\"Zwiększ podstawowy rozmiar tekstu\",\"Focus ring width\":\"Szerokość pierścienia ostrości\",\"Minimum target size\":\"Minimalny rozmiar celu\",\"Respect system reduced-motion setting\":\"Przestrzegaj ustawień systemu o zmniejszonym ruchu\",\"Apply accessible defaults\":\"Zastosuj dostępne ustawienia domyślne\",\"Profiles by page and screen size\":\"Profile według strony i rozmiaru ekranu\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Zapisz jeden lub więcej wyglądów, a następnie przypisz je do typu strony na komputerze, tablecie i telefonie komórkowym. Profile przełączają się automatycznie po zmianie rozmiaru ekranu.\",\"Enable responsive design profiles\":\"Włącz responsywne profile projektowe\",\"Mobile maximum width\":\"Maksymalna szerokość mobilna\",\"Tablet maximum width\":\"Maksymalna szerokość tabletu\",\"RoomGrid design system\":\"System projektowania RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Wybierz kierunek marki, szerokość treści i gęstość informacji na karcie. Każda strona następuje natychmiast.\",\"Primary accent\":\"Podstawowy akcent\",\"Secondary accent\":\"Akcent wtórny\",\"Content width\":\"Szerokość treści\",\"Centered\":\"Wyśrodkowany\",\"Wide\":\"Szeroki\",\"Full width\":\"Pełna szerokość\",\"Card style\":\"Styl karty\",\"Visual\":\"Wizualny\",\"Data-rich\":\"Bogate w dane\",\"Micro\":\"Mikro\",\"Widget density\":\"Gęstość widgetów\",\"Spacious\":\"Przestronny\",\"Theme and surfaces\":\"Motyw i powierzchnie\",\"Surface style\":\"Styl powierzchni\",\"Soft\":\"Miękkie\",\"Flat\":\"Mieszkanie\",\"Glass\":\"Szkło\",\"Page background\":\"Tło strony\",\"Solid\":\"Solidne\",\"Gradient\":\"Gradientowe\",\"Ambient glow\":\"Blask otoczenia\",\"Surface opacity\":\"Nieprzezroczystość powierzchni\",\"Custom accent color\":\"Niestandardowy kolor akcentu\",\"Sizing and motion\":\"Rozmiar i ruch\",\"Font style\":\"Styl czcionki\",\"System\":\"Systemu\",\"Classic serif\":\"Klasyczny szeryf\",\"Monospace\":\"Monoprzestrzeń\",\"Density\":\"Gęstość\",\"Font scale\":\"Skala czcionki\",\"Control size\":\"Rozmiar kontrolny\",\"Corner radius\":\"Promień narożnika\",\"Card spacing\":\"Rozstaw kart\",\"Shadow strength\":\"Siła cienia\",\"Sidebar width\":\"Szerokość paska bocznego\",\"Enable animations\":\"Włącz animacje\",\"Appearance profiles\":\"Profile wyglądu\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Zapisz cały wygląd jako profil wielokrotnego użytku. Profile pozostają dostępne po aktualizacji skryptu.\",\"Save current appearance\":\"Zapisz aktualny wygląd\",\"Import profile\":\"Importuj profil\",\"No profiles saved yet.\":\"Nie zapisano jeszcze żadnych profili.\",\"Instant preview\":\"Natychmiastowy podgląd\",\"Example card\":\"Przykładowa karta\",\"This is the combined result of the current visual settings.\":\"To łączny wynik bieżących ustawień wizualnych.\",\"Example action\":\"Przykładowa akcja\",\"Per-page design profiles\":\"Profile projektowe na stronie\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Wybierz inny zapisany wygląd strony głównej, ofert, stron modeli, MultiCam, Discover i centrum danych.\",\"Enable per-page design\":\"Włącz projektowanie na stronie\",\"Normal website pages\":\"Normalne strony internetowe\",\"Enable RoomGrid page design\":\"Włącz projektowanie strony RoomGrid\",\"Tag and category pages\":\"Strony tagów i kategorii\",\"Cards\":\"Karty\",\"List\":\"Lista\",\"Page density\":\"Gęstość strony\",\"Polish navigation\":\"Polska nawigacja\",\"Polish sidebars\":\"Polskie sidebary\",\"Hide recognized promo and merch areas\":\"Ukryj rozpoznane obszary promocji i gadżetów\",\"Compact footer\":\"Kompaktowa stopka\",\"Mark saved models\":\"Zaznacz zapisane modele\",\"Mark favorites separately\":\"Oznacz ulubione osobno\",\"Mark visited models\":\"Marek odwiedził modele\",\"Show card quick actions\":\"Pokaż szybkie działania na karcie\",\"Animated live previews on home and listing pages\":\"Animowane podglądy na żywo na stronach głównych i stronach aukcji\",\"Preview refresh rate\":\"Podgląd częstotliwości odświeżania\",\"Off\":\"Wyłączone\",\"Update mode\":\"Tryb aktualizacji\",\"All visible cards simultaneously\":\"Wszystkie widoczne karty jednocześnie\",\"Performance-bounded batches\":\"Partie o ograniczonej wydajności\",\"Batch concurrency limit\":\"Limit współbieżności partii\",\"All visible\":\"Wszystko widoczne\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Tylko widoczne karty otrzymują wyciszone aktualizacje klatek JPEG; zatrzymują się natychmiast poza ekranem lub w ukrytej karcie. Tryb zsynchronizowany odświeża każdą widoczną kartę w jednej fali; wysokie szybkości zauważalnie zwiększają przepustowość i wykorzystanie procesora.\",\"Live video on hover\":\"Wideo na żywo po najechaniu myszką\",\"Play real-time video while hovering\":\"Odtwarzaj wideo w czasie rzeczywistym po najechaniu myszką\",\"Start delay\":\"Opóźnienie rozpoczęcia\",\"Immediately\":\"Natychmiast\",\"Hover enlargement\":\"Najedź na powiększenie\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Gra tylko karta pod wskaźnikiem, zawsze wyciszona. Odsunięcie lub ukrycie karty natychmiast zatrzymuje transmisję.\",\"Live Preview Dock\":\"Stacja dokująca z podglądem na żywo\",\"Enable low-load live previews\":\"Włącz podglądy na żywo przy niskim obciążeniu\",\"Concurrent previews\":\"Równoczesne podglądy\",\"Window size\":\"Rozmiar okna\",\"Small\":\"Mały\",\"Medium\":\"Średni\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Podglądy zaczynają być wyciszone i wstrzymują się w ukrytych kartach. Dźwięk zawsze wymaga wyraźnego kliknięcia.\",\"Quick info & chat focus\":\"Szybkie informacje i skupienie na czacie\",\"Enable model Quick View\":\"Włącz szybki podgląd modelu\",\"Chat focus mode (opt-in)\":\"Tryb skupienia na czacie (opcja)\",\"Show local timestamps\":\"Pokaż lokalne znaczniki czasu\",\"System messages\":\"Komunikaty systemowe\",\"Show\":\"Pokaż\",\"Dim\":\"Przyciemnienie\",\"Hide\":\"Ukryj\",\"Chat filter strength\":\"Siła filtra czatu\",\"Low\":\"Niski\",\"Strict\":\"Surowe\",\"Highlight words (comma separated)\":\"Zaznacz słowa (oddzielone przecinkami)\",\"Muted words (comma separated)\":\"Wyciszone słowa (oddzielone przecinkami)\",\"Collapse repeated messages\":\"Zwiń powtarzające się wiadomości\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Ulepszenia czatu zmieniają tylko bieżący wyświetlacz. Nigdy nie przechowują czatów, nie wysyłają wiadomości ani nie przekazują automatycznie napiwków.\",\"Shortcuts\":\"Skróty\",\"Command menu\":\"Menu poleceń\",\"Global model search\":\"Globalne wyszukiwanie modeli\",\"Reset design defaults\":\"Zresetuj domyślne ustawienia projektu\",\"Optional modules\":\"Opcjonalne moduły\",\"Normal-page dock and card actions\":\"Dok i działania kart na normalnej stronie\",\"Real audio meters\":\"Prawdziwe mierniki dźwięku\",\"Media, app and contest directories\":\"Katalogi multimediów, aplikacji i konkursów\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Przełączniki modułów zaczynają działać w pełni po ponownym załadowaniu strony. Podstawowe moduły danych i kopie zapasowe pozostają włączone, aby zapobiec utracie danych.\",\"Name the current filters and sorting\":\"Nazwij bieżące filtry i sortowanie\",\"My smart view\":\"Mój mądry pogląd\",\"Delete smart view\":\"Usuń inteligentny widok\",\"Smart view deleted\":\"Inteligentny widok został usunięty\",\"tags\":\"tagi\",\"countries\":\"kraje\",\"Saved selection\":\"Zapisano wybór\",\"Suggestions\":\"Sugestie\",\"Selected\":\"Wybrane\",\"Website tags\":\"Tagi witryny\",\"Countries (multiple selection)\":\"Kraje (wielokrotny wybór)\",\"Select countries\":\"Wybierz kraje\",\"Search countries…\":\"Wyszukaj kraje…\",\"Recognized in current models\":\"Uznany w obecnych modelach\",\"All countries\":\"Wszystkie kraje\",\"Tags (multiple selection)\":\"Tagi (wielokrotny wybór)\",\"Select tags\":\"Wybierz tagi\",\"Search tags…\":\"Wyszukaj tagi…\",\"Room subject\":\"Temat pokoju\",\"Current online duration\":\"Bieżący czas online\",\"Local watch time and sessions\":\"Lokalny czas oglądania i sesje\",\"HD, new, gaming and favorite badges\":\"Odznaki HD, nowość, gaming i ulubione\",\"Homepage Studio\":\"Studio strony głównej\",\"Balanced preset\":\"Ustawienie zrównoważone\",\"Compact preset\":\"Ustawienie kompaktowe\",\"Data-rich preset\":\"Ustawienie bogate w dane\",\"Cinema preset\":\"Ustawienie kinowe\",\"Layout and pagination\":\"Układ i stronicowanie\",\"Show quick toolbar above models\":\"Pokaż szybki pasek nad modelami\",\"Cards per row\":\"Karty w wierszu\",\"Image ratio\":\"Proporcje obrazu\",\"Information display\":\"Wyświetlanie informacji\",\"Detailed\":\"Szczegółowo\",\"Overlay on image\":\"Nakładka na obrazie\",\"Powerful sorting\":\"Zaawansowane sortowanie\",\"Primary sort\":\"Sortowanie podstawowe\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Sortowanie zmienia tylko kolejność wyświetlania wczytanych kart modeli. Nie zmienia obserwowanych ani ulubionych na koncie Chaturbate.\",\"Restore website order\":\"Przywróć kolejność witryny\",\"Clear all filters\":\"Wyczyść wszystkie filtry\",\"Search and range filters\":\"Wyszukiwanie i filtry zakresu\",\"When several tags are selected\":\"Gdy wybranych jest kilka tagów\",\"Model must contain every tag\":\"Model musi zawierać każdy tag\",\"At least one tag matches\":\"Co najmniej jeden tag pasuje\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Filtrowanie krajów pokazuje modele z dowolnego wybranego kraju. Własne tagi i rozpoznane tagi witryn internetowych pozostają dostępne na liście i można je szybko znaleźć za pomocą wyszukiwania.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Po wybraniu tagów lub krajów RoomGrid przeszukuje cały bieżący katalog na żywo. Ta sama strona wyników początkowo wyświetla 120 dopasowań, a podczas przewijania ładuje ich więcej fragmentami, więc nawet bardzo popularne tagi pozostają responsywne. Przycisk ponownego ładowania na pasku narzędzi strony głównej odświeża listę na żywo.\",\"Quick conditions\":\"Szybkie warunki\",\"HD models only\":\"Tylko modele HD\",\"New models only\":\"Tylko nowe modele\",\"Gaming models only\":\"Tylko modele gamingowe\",\"Only saved in RoomGrid\":\"Tylko zapisane w RoomGrid\",\"Only own favorites\":\"Tylko własne ulubione\",\"Following only\":\"Tylko podążanie\",\"Hide previously watched models\":\"Ukryj wcześniej oglądane modele\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Kraj, wiek i czas online pochodzą z bieżących publicznych kart lub publicznego katalogu pokoi. Jeśli Chaturbate nie udostępnia danego pola, RoomGrid go nie zgaduje.\",\"Smart filter views\":\"Inteligentne widoki filtrów\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Zapisz bieżące filtry i sortowanie jako widok dynamiczny. Modele, które pojawią się w Internecie później i będą zgodne z zasadami, pojawią się automatycznie.\",\"Save current filter view\":\"Zapisz bieżący widok filtra\",\"No smart views saved yet.\":\"Nie zapisano jeszcze żadnych inteligentnych widoków.\",\"What to show for each model\":\"Co wyświetlać dla każdego modelu\",\"Import complete\":\"Import zakończony\",\"Invalid file\":\"Nieprawidłowy plik\",\"Range\":\"Zasięg\",\"Today\":\"Dzisiaj\",\"Week\":\"Tydzień\",\"Month\":\"Miesiąc\",\"Year\":\"Rok\",\"Recording enabled\":\"Nagrywanie włączone\",\"Private session (do not record)\":\"Sesja prywatna (nie nagrywaj)\",\"Retention days\":\"Dni przetrzymywania\",\"Usage time\":\"Czas użytkowania\",\"Summed camera time\":\"Sumaryczny czas pracy kamery\",\"Visits\":\"Wizyty\",\"Known models\":\"Znane modele\",\"Weekly heatmap\":\"Tygodniowa mapa cieplna\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Zagregowane według dnia tygodnia i godziny; ciemniejszy oznacza dłuższy obserwowany czas.\",\"Sunday to Saturday, 24 hours per row\":\"Od niedzieli do soboty, 24 godziny na rząd\",\"Watch ranking\":\"Obejrzyj ranking\",\"little data\":\"mało danych\",\"No watch history yet.\":\"Nie ma jeszcze historii oglądania.\",\"Workspace, filter and quality events\":\"Obszar roboczy, filtr i zdarzenia dotyczące jakości\",\"Recent timeline\":\"Najnowsza kalendarium\",\"Export sensitive statistics\":\"Eksportuj poufne statystyki\",\"Import statistics\":\"Importuj statystyki\",\"Delete selected range\":\"Usuń wybrany zakres\",\"Delete local events in the selected range?\":\"Usunąć wydarzenia lokalne w wybranym zakresie?\",\"Clear all statistics\":\"Wyczyść wszystkie statystyki\",\"Permanently clear all local statistics?\":\"Trwale wyczyścić wszystkie statystyki lokalne?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Eksporty statystyk zawierają nazwy modeli i znaczniki czasu, dlatego należy je traktować jako poufne. Stare dane są czyszczone zgodnie z retencją.\",\"new\":\"nowy\",\"Under 1 minute\":\"Poniżej 1 minuty\",\"1–5 minutes\":\"1–5 minut\",\"5–15 minutes\":\"5–15 minut\",\"15–30 minutes\":\"15–30 minut\",\"30+ minutes\":\"ponad 30 minut\",\"Local observations and your usage\":\"Lokalne obserwacje i Twoje wykorzystanie\",\"Public profile loaded on request\":\"Profil publiczny ładowany na żądanie\",\"Local RoomGrid interaction\":\"Lokalna interakcja RoomGrid\",\"Local browser player\":\"Lokalny odtwarzacz przeglądarkowy\",\"Optional official statistics API\":\"Opcjonalne API oficjalnych statystyk\",\"Local daily aggregation\":\"Lokalna agregacja dzienna\",\"Local data\":\"Dane lokalne\",\"Model summaries\":\"Podsumowanie modeli\",\"Public profiles\":\"Profile publiczne\",\"Usage events\":\"Zdarzenia użytkowe\",\"Official account snapshots\":\"Oficjalne migawki konta\",\"Daily rollups\":\"Codzienne rollupy\",\"Camera time\":\"Czas na kamerę\",\"vs previous period\":\"w stosunku do poprzedniego okresu\",\"Trends\":\"Trendy\",\"Daily activity\":\"Codzienna aktywność\",\"Playback quality\":\"Jakość odtwarzania\",\"Privacy status\":\"Stan prywatności\",\"Data provenance\":\"Pochodzenie danych\",\"Local RoomGrid usage\":\"Lokalne użycie RoomGrid\",\"7 days\":\"7 dni\",\"30 days\":\"30 dni\",\"90 days\":\"90 dni\",\"Not enough data yet.\":\"Jeszcze za mało danych.\",\"Models revisited\":\"Ponowna wizyta w modelach\",\"Previews opened\":\"Podgląd został otwarty\",\"Quick Views\":\"Szybkie widoki\",\"Samples\":\"Próbki\",\"Private session\":\"Prywatna sesja\",\"History paused\":\"Historia się zatrzymała\",\"Recording locally\":\"Nagrywanie lokalnie\",\"Disabled\":\"Niepełnosprawny\",\"No cloud sync or telemetry.\":\"Brak synchronizacji w chmurze i telemetrii.\",\"Raw events\":\"Surowe wydarzenia\",\"Only local observations and actions from this browser.\":\"Tylko lokalne obserwacje i działania z tej przeglądarki.\",\"Time\":\"Czas\",\"Models\":\"Modele\",\"Online planner\":\"Planista online\",\"Data & privacy\":\"Dane i prywatność\",\"Configure statistics dashboard\":\"Skonfiguruj panel statystyk\",\"Preset\":\"Wstępnie ustawione\",\"Minimal\":\"Minimalne\",\"Quality focus\":\"Koncentracja na jakości\",\"Privacy focus\":\"Koncentracja na prywatności\",\"Reset dashboard\":\"Zresetuj pulpit nawigacyjny\",\"Size\":\"Rozmiar\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Porządek, widoczność i szerokość są przechowywane w ustawieniach zapewniających możliwość aktualizacji. Każdy widget pokazuje swoje źródło danych.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Żadne widżety pulpitu nawigacyjnego nie są widoczne. Otwórz konfigurację i wybierz przynajmniej jeden widget.\",\"Median session\":\"Mediana sesji\",\"Daily usage trend\":\"Dzienny trend użytkowania\",\"Your trend appears here after you start using RoomGrid.\":\"Twój trend pojawi się tutaj po rozpoczęciu korzystania z RoomGrid.\",\"Session-duration distribution\":\"Rozkład czasu trwania sesji\",\"No session-duration data yet.\":\"Brak jeszcze danych o czasie trwania sesji.\",\"Return rate\":\"Stopa zwrotu\",\"Total sessions\":\"Suma sesji\",\"Longest observed\":\"Najdłużej obserwowany\",\"Model ranking\":\"Ranking modeli\",\"Select up to four models for a local comparison.\":\"Wybierz maksymalnie cztery modele do lokalnego porównania.\",\"Quick View\":\"Szybki podgląd\",\"No model data yet.\":\"Nie ma jeszcze danych modelu.\",\"Models with time patterns\":\"Modele z wzorcami czasowymi\",\"High confidence\":\"Wysoka pewność\",\"Moderate confidence\":\"Umiarkowana pewność siebie\",\"Planning horizon\":\"Horyzont planowania\",\"Likely next active times\":\"Prawdopodobne następne aktywne czasy\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Obliczono na podstawie lokalnie zaobserwowanych próbek w Internecie; nie gwarantuje to transmisji. Zaufanie wzrasta wraz z większą ilością danych.\",\"High\":\"Wysoka\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Za mało jeszcze obserwacji online. Planer pojawia się automatycznie wraz z biblioteką.\",\"Discovery events\":\"Wydarzenia odkrywcze\",\"Local interaction funnel\":\"Lokalny lejek interakcji\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Liczone są tylko Twoje działania na tym urządzeniu; treść czatu i płatności nigdy nie są śledzone.\",\"Workspace events\":\"Wydarzenia w obszarze roboczym\",\"Filter events\":\"Filtruj wydarzenia\",\"Tag events\":\"Taguj wydarzenia\",\"Top-four workspace comparison\":\"Porównanie czterech najlepszych obszarów roboczych\",\"Workspace usage\":\"Wykorzystanie przestrzeni roboczej\",\"No workspace data yet.\":\"Brak jeszcze danych obszaru roboczego.\",\"Average active streams\":\"Średnio aktywne strumienie\",\"Playback quality by model\":\"Jakość odtwarzania według modelu\",\"No quality data yet.\":\"Brak jeszcze danych dotyczących jakości.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Plik statystyk jest nieprawidłowy lub przekracza bezpieczne limity. Aktualne dane nie uległy zmianie.\",\"Statistics import assistant · preview\":\"Asystent importu statystyk · podgląd\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Nic się jeszcze nie zmieniło. Wybierz sekcje do zaimportowania i przejrzenia konfliktów.\",\"Safe merge (recommended)\":\"Bezpieczne scalanie (zalecane)\",\"Import complete. The previous statistics were backed up automatically.\":\"Import zakończony. Kopia zapasowa poprzednich statystyk została utworzona automatycznie.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"Nie można bezpiecznie zastosować importu. Bieżące dane i kopie zapasowe pozostają niezmienione.\",\"File schema\":\"Schemat pliku\",\"Date range\":\"Zakres dat\",\"Sensitive data\":\"Wrażliwe dane\",\"Not marked sensitive\":\"Nieoznaczone jako wrażliwe\",\"Until manually resumed\":\"Do czasu ręcznego wznowienia\",\"Not paused\":\"Nie wstrzymano\",\"Restore this statistics backup? The current state will be backed up first.\":\"Przywrócić tę kopię zapasową statystyk? W pierwszej kolejności zostanie utworzona kopia zapasowa bieżącego stanu.\",\"Permanently delete this backup?\":\"Trwale usunąć tę kopię zapasową?\",\"Events\":\"Wydarzenia\",\"Estimated statistics storage\":\"Szacowane przechowywanie statystyk\",\"Recording, pauses and private sessions\":\"Nagrywanie, pauzy i sesje prywatne\",\"Record usage statistics\":\"Rejestruj statystyki użytkowania\",\"Pause 15 minutes\":\"Zatrzymaj się na 15 minut\",\"Pause 1 hour\":\"Wstrzymaj 1 godzinę\",\"Pause 8 hours\":\"Przerwa na 8 godzin\",\"Pause indefinitely\":\"Wstrzymaj na czas nieokreślony\",\"Resume history\":\"Wznów historię\",\"History status\":\"Stan historii\",\"Private for 30 minutes\":\"Prywatnie przez 30 minut\",\"Private for 2 hours\":\"Prywatnie na 2 godziny\",\"Private until resumed\":\"Prywatne do wznowienia\",\"End private session\":\"Zakończ sesję prywatną\",\"Not active\":\"Nieaktywny\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Sesja prywatna wstrzymuje także obserwacje modelu publicznego; normalna przerwa w historii zatrzymuje zapisy dotyczące osobistego użytkowania i jakości.\",\"Automatic deletion and retention\":\"Automatyczne usuwanie i przechowywanie\",\"Raw events (days)\":\"Surowe zdarzenia (dni)\",\"Quality samples (days)\":\"Próbki jakościowe (dni)\",\"Account snapshots (days)\":\"Migawki konta (dni)\",\"Daily rollups (days)\":\"Codzienne rollupy (dni)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Wygasłe nieprzetworzone zdarzenia są uwzględniane w sumach dziennych przed automatycznym usunięciem. Migawki jakości i konta korzystają z własnych limitów.\",\"Statistics backup center\":\"Centrum tworzenia kopii zapasowych statystyk\",\"Create internal backup now\":\"Utwórz teraz wewnętrzną kopię zapasową\",\"Pseudonymized export\":\"Pseudonimizowany eksport\",\"Print report\":\"Wydrukuj raport\",\"Choose import file\":\"Wybierz plik importu\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Nie ma jeszcze kopii zapasowych statystyk wewnętrznych. Po zaimportowaniu, przywróceniu lub ręcznym utworzeniu przechowywane są maksymalnie trzy.\",\"Delete local statistics in this range? A backup will be created first.\":\"Usunąć statystyki lokalne w tym zakresie? Najpierw zostanie utworzona kopia zapasowa.\",\"Clear everything\":\"Wyczyść wszystko\",\"Permanently clear all local statistics? A backup will be created first.\":\"Trwale wyczyścić wszystkie statystyki lokalne? Najpierw zostanie utworzona kopia zapasowa.\",\"Data inventory and provenance\":\"Inwentaryzacja i pochodzenie danych\",\"Stored only in this browser\":\"Przechowywane tylko w tej przeglądarce\",\"Coverage\":\"Zasięg\",\"Unknown or missing data is never inferred.\":\"Nieznane lub brakujące dane nigdy nie są wnioskowane.\",\"Metric definitions & data quality\":\"Definicje metryk i jakość danych\",\"Accumulated locally while a RoomGrid page is active.\":\"Naliczane lokalnie, gdy strona RoomGrid jest aktywna.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Suma wszystkich zaobserwowanych sesji kamery; dodawane są równoczesne strumienie.\",\"browser samples; no interpolation.\":\"próbki przeglądarki; brak interpolacji.\",\"Privacy boundary\":\"Granica prywatności\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Żadna treść czatu, płatności ani pliki multimedialne nie są zapisywane. Sesje prywatne nie dodają żadnych wydarzeń.\",\"No history yet\":\"Nie ma jeszcze historii\",\"Loading\":\"Ładowanie\",\"Connected\":\"Połączono\",\"Locked\":\"Zablokowane\",\"Not connected\":\"Nie podłączony\",\"Username\":\"Nazwa użytkownika\",\"24-character stats token\":\"24-znakowy token statystyk\",\"At least 8 characters\":\"Co najmniej 8 znaków\",\"Secure official statistics connection\":\"Bezpieczne połączenie z oficjalnymi statystykami\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Opcjonalne. Token nigdy nie wchodzi do normalnych ustawień, dzienników ani eksportów. Tryb sesji przechowuje go tylko w pamięci; tryb szyfrowany wykorzystuje izolowaną pamięć lokalną.\",\"Stats token\":\"Token statystyk\",\"Vault password\":\"Hasło do skarbca\",\"Connect for this session\":\"Połącz się w tej sesji\",\"Username or token is invalid.\":\"Nazwa użytkownika lub token jest nieprawidłowy.\",\"Encrypt, save & connect\":\"Szyfruj, zapisuj i łącz\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Nie można zapisać zaszyfrowanego. Sprawdź uprawnienia tokena, hasła i menedżera skryptów użytkownika.\",\"Get official token\":\"Zdobądź oficjalny token\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"Automatyczne odświeżanie odbywa się najwyżej raz na pięć minut. Ten moduł wykorzystuje wyłącznie oficjalny token, który podałeś celowo.\",\"Account\":\"Konto\",\"History snapshots\":\"Migawki historii\",\"Last refresh\":\"Ostatnie odświeżenie\",\"Unlock local vault\":\"Odblokuj skarbiec lokalny\",\"Password\":\"Hasło\",\"Unlock\":\"Odblokuj\",\"Unlock failed.\":\"Odblokowanie nie powiodło się.\",\"Loading…\":\"Ładowanie…\",\"Refresh now\":\"Odśwież teraz\",\"Auto-refresh every 5 minutes\":\"Automatyczne odświeżanie co 5 minut\",\"Disconnect and delete token\":\"Odłącz i usuń token\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Usunąć połączenie lokalne i zaszyfrowany token? Statystyki historyczne pozostają.\",\"Broadcast status\":\"Stan transmisji\",\"Token balance\":\"Bilans tokenów\",\"Tips last hour\":\"Wskazówki z ostatniej godziny\",\"Followers\":\"Obserwujący\",\"Up votes\":\"Głosy w górę\",\"Down votes\":\"Głosy w dół\",\"Satisfaction\":\"Satysfakcja\",\"Registered viewers\":\"Zarejestrowani widzowie\",\"Tokened viewers\":\"Tokenowani widzowie\",\"Time online\":\"Czas w Internecie\",\"Follower trend\":\"Trend podążający\",\"Tips trend\":\"Trend na porady\",\"Token-balance trend\":\"Trend równowagi tokenów\",\"Viewer trend\":\"Trend widza\",\"Account Health Center\":\"Centrum stanu konta\",\"Official My Collection\":\"Oficjalna moja kolekcja\",\"Official support\":\"Oficjalne wsparcie\",\"Export local statistics\":\"Eksportuj statystyki lokalne\",\"Delete snapshot history\":\"Usuń historię migawek\",\"Delete the local official-statistics snapshots for this account?\":\"Usunąć migawki lokalnych oficjalnych statystyk dla tego konta?\",\"Last error\":\"Ostatni błąd\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Stan tokena, częstotliwość odświeżania, lokalna historia i oficjalne linki znajdują się tutaj. RoomGrid nigdy nie publikuje, nie wysyła wiadomości ani nie przeprowadza transakcji automatycznie.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Przeglądarka nie udzieliła pozwolenia na powiadomienia. Powiadomienia na stronie pozostają aktywne.\",\"Notification profile\":\"Profil powiadomień\",\"Go-live only\":\"Tylko na żywo\",\"Personalized\":\"Spersonalizowane\",\"Always (all events)\":\"Zawsze (wszystkie wydarzenia)\",\"Inactive days\":\"Dni nieaktywne\",\"Favorites, rating, priority and your tags decide relevance.\":\"Ulubione, ocena, priorytet i Twoje tagi decydują o trafności.\",\"Includes go-live, offline and public-profile changes.\":\"Obejmuje zmiany na żywo, offline i w profilu publicznym.\",\"Master switch\":\"Przełącznik główny\",\"On\":\"Włączone\",\"Off (default)\":\"Wyłączone (domyślnie)\",\"Monitored models\":\"Monitorowane modele\",\"Active tab\":\"Aktywna zakładka\",\"This tab\":\"Ta zakładka\",\"Another tab\":\"Kolejna zakładka\",\"Local notifications\":\"Powiadomienia lokalne\",\"Notification delivery and display\":\"Dostarczanie i wyświetlanie powiadomień\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Powiadomienia na stronie wykorzystują wolny obszar na górnym pasku RoomGrid i nie wymagają pozwolenia przeglądarki. Pozycję, czas trwania i gęstość można konfigurować.\",\"Delivery\":\"Dostawa\",\"RoomGrid bar only\":\"Tylko pasek RoomGrid\",\"Browser notifications only\":\"Tylko powiadomienia przeglądarki\",\"RoomGrid bar and browser\":\"Pasek RoomGrid i przeglądarka\",\"No display, history only\":\"Brak wyświetlania, tylko historia\",\"In-page position\":\"Pozycja na stronie\",\"Free top-bar area\":\"Bezpłatny obszar w górnym barze\",\"Floating top center\":\"Pływający górny środek\",\"Floating top right\":\"Pływające w prawym górnym rogu\",\"Display style\":\"Styl wyświetlania\",\"Compact single line\":\"Kompaktowa pojedyncza linia\",\"Display duration\":\"Czas wyświetlania\",\"Until dismissed\":\"Do czasu zwolnienia\",\"Visible at once\":\"Widoczne od razu\",\"Test notification\":\"Powiadomienie testowe\",\"General rules\":\"Ogólne zasady\",\"Enable notifications\":\"Włącz powiadomienia\",\"The browser did not grant notification permission.\":\"Przeglądarka nie udzieliła pozwolenia na powiadomienia.\",\"Neutral text\":\"Tekst neutralny\",\"Quiet time starts\":\"Zaczyna się spokojny czas\",\"Quiet time ends\":\"Spokojny czas dobiega końca\",\"Cooldown (minutes)\":\"Czas odnowienia (minuty)\",\"Hourly limit\":\"Limit godzinowy\",\"Favorite threshold\":\"Ulubiony próg\",\"Open on click\":\"Otwórz po kliknięciu\",\"Per model\":\"Według modelu\",\"Model\":\"Modelka\",\"Library is empty.\":\"Biblioteka jest pusta.\",\"Per group\":\"Na grupę\",\"Group\":\"Grupa\",\"No groups.\":\"Brak grup.\",\"Local notification list\":\"Lokalna lista powiadomień\",\"Mark all read\":\"Zaznacz wszystkie jako przeczytane\",\"Clear list\":\"Wyczyść listę\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Tło sprawdza tylko status odczytu i nie uruchamia wideo. Skrypt użytkownika może działać tylko wtedy, gdy otwarta jest karta powiązanej witryny; wiele zakładek automatycznie wybiera jeden kontroler.\",\"Shield status\":\"Stan tarczy\",\"Ready\":\"Gotowy\",\"Local PIN\":\"Lokalny PIN\",\"Set\":\"Zestaw\",\"Not set\":\"Nie ustawiono\",\"Quick shield\":\"Szybka tarcza\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Natychmiast zakrywa nazwy i obrazy oraz wstrzymuje/wycisza wszystkie multimedia. Przywracanie przywraca poprzedni stan nośnika.\",\"Activate now\":\"Aktywuj teraz\",\"Auto-shield after inactivity\":\"Automatyczna osłona po bezczynności\",\"Set or change local PIN\":\"Ustaw lub zmień lokalny kod PIN\",\"Leave empty to remove\":\"Pozostaw puste, aby je usunąć\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"PIN stanowi jedynie lokalną barierę dostępu, a nie szyfrowanie i nie zastępuje ochrony konta w systemie operacyjnym.\",\"History controls\":\"Kontrola historii\",\"Usage statistics\":\"Statystyki użytkowania\",\"Pause history 1 hour\":\"Wstrzymaj historię na 1 godzinę\",\"Clear statistics\":\"Jasne statystyki\",\"Clear all local statistics? A backup will be created first.\":\"Wyczyścić wszystkie statystyki lokalne? Najpierw zostanie utworzona kopia zapasowa.\",\"Clear Discover settings\":\"Wyczyść ustawienia Discover\",\"Clear local Discover filters and feeds?\":\"Wyczyścić lokalne filtry i kanały Discover?\",\"Clear notification list\":\"Wyczyść listę powiadomień\",\"Data boundary\":\"Granica danych\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Konfiguracja, statystyki oglądania, historia powiadomień i preferencje Discover są przechowywane lokalnie w tej przeglądarce. RoomGrid nie zawiera synchronizacji w chmurze, przesyłania konta ani telemetrii.\",\"Quota\":\"Limit\",\"Used\":\"Używany\",\"Sensitivity & consent center\":\"Centrum wrażliwości i zgody\",\"Standard configuration\":\"Konfiguracja standardowa\",\"Theme, layout, filters\":\"Motyw, układ, filtry\",\"Local\":\"Lokalny\",\"Model names, notes, timestamps, statistics\":\"Nazwy modeli, uwagi, znaczniki czasu, statystyki\",\"Warn on export\":\"Ostrzegaj o eksporcie\",\"Secret\":\"Sekret\",\"Optional official statistics token\":\"Opcjonalny token oficjalnych statystyk\",\"Session-only or encrypted vault\":\"Skarbiec sesyjny lub zaszyfrowany\",\"Chat content\":\"Treść czatu\",\"Never persisted\":\"Nigdy się nie utrzymywał\",\"Ephemeral\":\"Efemeryczne\",\"Stream recording/screenshots\":\"Nagrywanie strumieniowe/zrzuty ekranu\",\"Not provided\":\"Nie podano\",\"Policy guard\":\"Strażnik polityki\",\"Performance profile\":\"Profil wydajności\",\"Playing streams\":\"Odtwarzanie strumieni\",\"Video elements\":\"Elementy wideo\",\"Measured memory\":\"Zmierzona pamięć\",\"Unavailable in browser\":\"Niedostępne w przeglądarce\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Bieżące pomiary są blisko skonfigurowanego limitu. Wstrzymaj nieużywane kamery lub wybierz tryb Eco.\",\"Stream performance\":\"Wydajność strumieniowa\",\"Eco\":\"Eko\",\"Power\":\"Moc\",\"Concurrent stream limit\":\"Limit jednoczesnego strumienia\",\"Adaptive quality\":\"Jakość adaptacyjna\",\"Gradual startup\":\"Stopniowe uruchamianie\",\"Automatic performance mode\":\"Automatyczny tryb wydajności\",\"Memory threshold\":\"Próg pamięci\",\"Stream-count threshold\":\"Próg licznika strumieni\",\"Current auto state\":\"Aktualny stan automatyczny\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Nieużywane podglądy są zatrzymywane. Nowy film wideo pozostaje wstrzymany na limicie do momentu udostępnienia miejsca. Wysokie liczby zaczynają się stopniowo; nie ma gwarancji „nieograniczonej liczby kamer”.\",\"Multi-window workspaces\":\"Przestrzenie robocze z wieloma oknami\",\"Window name\":\"Nazwa okna\",\"Active and recent windows\":\"Aktywne i ostatnie okna\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"Biblioteka modeli jest synchronizowana poprzez scalanie lokalne; każde okno ma swój własny obszar roboczy i ustawienia widoku. Ostatnie skojarzenia okien można przywrócić po ponownym uruchomieniu, jeśli przeglądarka na to pozwala.\",\"Optional tools are disabled\":\"Narzędzia opcjonalne są wyłączone\",\"Media, app and contest directories will not scan the current page.\":\"Katalogi multimediów, aplikacji i konkursów nie będą skanować bieżącej strony.\",\"Enable optional tools\":\"Włącz narzędzia opcjonalne\",\"Local search\":\"Wyszukiwanie lokalne\",\"Search saved items and current page\":\"Wyszukaj zapisane elementy i bieżącą stronę\",\"Rescan current page\":\"Przeskanuj ponownie bieżącą stronę\",\"Disable optional tools\":\"Wyłącz narzędzia opcjonalne\",\"Clear local side data\":\"Wyczyść lokalne dane boczne\",\"Clear app favorites, categories and media notes?\":\"Wyczyścić ulubione aplikacje, kategorie i notatki multimedialne?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Moduły te organizują jedynie publiczne linki i tekst, który jest już dostępny na bieżącej stronie. Nie kopiują multimediów, nie pobierają ich automatycznie, nie obsługują czatu, tokenów ani funkcji płatności. Logowanie, weryfikacja wieku, bezpieczeństwo i formy płatności pozostają niezmienione.\",\"Photo and video areas\":\"Obszary zdjęć i filmów\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Na tej stronie nie ma rozpoznawalnego obszaru mediów publicznych. Zeskanuj ponownie na natywnej stronie ze zdjęciami lub filmami.\",\"Apps directory\":\"Katalog aplikacji\",\"Local category\":\"Kategoria lokalna\",\"Found on page\":\"Znaleziono na stronie\",\"No recognizable app links are present on this page.\":\"Na tej stronie nie ma rozpoznawalnych linków do aplikacji.\",\"Public contest results\":\"Wyniki konkursu ogólnodostępnego\",\"No readable public contest table is present on this page.\":\"Na tej stronie nie ma czytelnej publicznej tabeli konkursowej.\",\"Native account, help and broadcaster links\":\"Konto natywne, pomoc i linki do nadawcy\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Na tej stronie nie znaleziono bezpiecznych linków natywnych; ścieżki płatności i logowania są celowo filtrowane.\",\"Homepage filters reset\":\"Reset filtrów strony głównej\",\"Global appearance reset\":\"Globalny reset wyglądu\",\"Version\":\"Wersja\",\"Health score\":\"Wynik zdrowia\",\"Page type\":\"Typ strony\",\"Route generation\":\"Generowanie tras\",\"Running modules\":\"Uruchamianie modułów\",\"Local errors\":\"Lokalne błędy\",\"Repair center\":\"Centrum napraw\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Przed każdą naprawą tworzona jest kopia zapasowa konfiguracji. Statystyki i dane modelu nie są usuwane w wyniku tych działań.\",\"Reset home filters\":\"Zresetuj filtry domowe\",\"Reset all home-page filters?\":\"Zresetować wszystkie filtry strony głównej?\",\"Reset global appearance\":\"Zresetuj wygląd globalny\",\"Reset the global appearance? Saved design profiles remain.\":\"Zresetować wygląd globalny? Zapisane profile projektów pozostają.\",\"Normalize configuration\":\"Normalizuj konfigurację\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Zatwierdzić i przepisać bieżącą konfigurację? Najpierw tworzona jest kopia zapasowa.\",\"Close all previews\":\"Zamknij wszystkie podglądy\",\"Compatibility self-check\":\"Samodzielna kontrola kompatybilności\",\"Module and page status\":\"Stan modułu i strony\",\"Page route test matrix\":\"Macierz testu trasy strony\",\"Current route\":\"Aktualna trasa\",\"Trigger\":\"Wyzwalacz\",\"Recent page transitions\":\"Ostatnie przejścia stron\",\"Capability layer\":\"Warstwa możliwości\",\"Available\":\"Dostępne\",\"Fallback\":\"Powrót\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"W przypadku braku interfejsu API przywracany jest tylko moduł, którego dotyczy problem. Powtarzające się awarie sieci tymczasowo otwierają wyłącznik automatyczny.\",\"Backup and recovery status\":\"Stan tworzenia kopii zapasowych i odzyskiwania\",\"Primary data\":\"Dane pierwotne\",\"Stable archive\":\"Stabilne archiwum\",\"Rolling backups\":\"Kroczące kopie zapasowe\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"Samokontrola sprawdza jedynie czytelność i możliwości przeglądarki; nigdy nie zastępuje automatycznie bieżących danych. Odzyskiwanie w dalszym ciągu korzysta z istniejącego, potwierdzonego przepływu kopii zapasowych.\",\"Sanitized local error log\":\"Oczyszczony lokalny dziennik błędów\",\"No RoomGrid errors recorded.\":\"Nie zarejestrowano żadnych błędów RoomGrid.\",\"Run self-check again\":\"Uruchom ponownie samosprawdzenie\",\"Export diagnostics\":\"Diagnostyka eksportowa\",\"Clear error log\":\"Wyczyść dziennik błędów\",\"Return to normal site\":\"Wróć do normalnej witryny\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"Eksport diagnostyczny nie zawiera obszaru roboczego, nazw modeli, notatek ani statystyk. Parametry zapytania adresu URL i możliwe wartości tajne zostały usunięte z tekstu błędu.\",\"Local history on\":\"Historia lokalna włączona\",\"History off\":\"Historia wyłączona\",\"This settings page could not be loaded\":\"Nie można załadować tej strony ustawień\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Twoje dane nie zostały zmienione. Spróbuj ponownie; jeśli problem będzie się powtarzał, otwórz Diagnostykę.\",\"Try again\":\"Spróbuj ponownie\",\"Open Diagnostics\":\"Otwórz diagnostykę\",\"Public results\":\"Wyniki publiczne\",\"Public contest links\":\"Linki do konkursów publicznych\",\"Page content has not appeared yet\":\"Treść strony jeszcze się nie pojawiła\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid ponownie sprawdził stronę, ale nadal nie może znaleźć obszaru zawartości Chaturbate. Strona może nadal się ładować lub witryna zwróciła pusty widok.\",\"Check again\":\"Sprawdź ponownie\",\"Open RoomGrid Discover\":\"Otwórz RoomGrid Odkryj\",\"Reload page\":\"Załaduj ponownie stronę\",\"Hide notice\":\"Ukryj powiadomienie\",\"Open MultiCam\":\"Otwórz MultiCam\",\"Open workstation in a new window\":\"Otwórz stację roboczą w nowym oknie\",\"Open Discover\":\"Otwórz Odkryj\",\"Statistics center\":\"Centrum statystyczne\",\"Backup and import\":\"Kopia zapasowa i import\",\"Page and design options\":\"Opcje strony i projektu\",\"Hide immediately\":\"Ukryj się natychmiast\",\"All tools\":\"Wszystkie narzędzia\",\"Search & commands\":\"Wyszukiwanie i polecenia\",\"Page design\":\"Projekt strony\",\"Official collection\":\"Oficjalna kolekcja\",\"Saved\":\"Zapisano\",\"Quick info\":\"Szybkie informacje\",\"Loading live frame\":\"Ładowanie klatki na żywo\",\"Live still\":\"Żyj spokojnie\",\"Not live\":\"Nie na żywo\",\"Pages\":\"Strony\",\"Refresh\":\"Odśwież\",\"Search…\":\"Szukaj…\",\"Tag Explorer\":\"Eksplorator tagów\",\"Refresh public catalog\":\"Odśwież katalog publiczny\",\"Online\":\"W Internecie\",\"Toggle favorite\":\"Przełącz ulubione\",\"Save\":\"Zapisz\",\"Remove favorite\":\"Usuń ulubione\",\"Removed from library\":\"Usunięto z biblioteki\",\"Saved to library\":\"Zapisano w bibliotece\",\"Added to favorites\":\"Dodano do ulubionych\",\"Removed from favorites\":\"Usunięto z ulubionych\",\"Watching\":\"Oglądanie\",\"Watch\":\"Oglądaj\",\"Temporary watchlist\":\"Tymczasowa lista obserwowanych\",\"Removed from watchlist\":\"Usunięto z listy obserwowanych\",\"Added to watchlist\":\"Dodano do listy obserwowanych\",\"Choose group\":\"Wybierz grupę\",\"Add to group…\":\"Dodaj do grupy…\",\"Group updated\":\"Grupa zaktualizowana\",\"Site room\":\"Pokój terenowy\",\"Select for bulk actions\":\"Wybierz, aby wykonać działania zbiorcze\",\"viewers\":\"widzów\",\"No public description\":\"Brak publicznego opisu\",\"Open in Tag Explorer\":\"Otwórz w Eksploratorze tagów\",\"Feed name\":\"Nazwa kanału\",\"View name\":\"Wyświetl nazwę\",\"Name already exists\":\"Nazwa już istnieje\",\"Saved-item limit reached\":\"Osiągnięto limit zapisanych elementów\",\"Feed saved\":\"Kanał został zapisany\",\"View saved\":\"Zobacz zapisane\",\"Applied\":\"Zastosowano\",\"Backup exported\":\"Kopia zapasowa wyeksportowana\",\"Backup imported\":\"Kopia zapasowa zaimportowana\",\"Invalid backup\":\"Nieprawidłowa kopia zapasowa\",\"New models\":\"Nowe modele\",\"New since last visit\":\"Nowy od ostatniej wizyty\",\"New to me\":\"Nowe dla mnie\",\"Returning\":\"Powrót\",\"Stable online\":\"Stabilny w Internecie\",\"Saved only\":\"Tylko zapisane\",\"Previously watched\":\"Poprzednio oglądane\",\"Local collection\":\"Lokalna kolekcja\",\"Hide visited today\":\"Ukryj odwiedzone dzisiaj\",\"No feed selected\":\"Nie wybrano kanału\",\"No view selected\":\"Nie wybrano widoku\",\"Saved feeds\":\"Zapisane kanały\",\"Saved views\":\"Zapisane widoki\",\"Save new\":\"Zapisz nowy\",\"All public rooms\":\"Wszystkie pomieszczenia ogólnodostępne\",\"Known HD\":\"Znane HD\",\"Random result\":\"Losowy wynik\",\"Viewers ↓\":\"Widzowie ↓\",\"Viewers ↑\":\"Widzowie ↑\",\"Online duration ↓\":\"Czas trwania online ↓\",\"Followers ↓\":\"Obserwujący ↓\",\"Name A–Z\":\"Nazwa A–Z\",\"Name Z–A\":\"Imię Z–A\",\"New first\":\"Najpierw nowe\",\"Watch time ↓\":\"Czas oglądania ↓\",\"Sessions ↓\":\"Sesje ↓\",\"Last watched\":\"Ostatnio oglądane\",\"Name, description, tag, place\":\"Nazwa, opis, tag, miejsce\",\"Secondary sort\":\"Sortowanie wtórne\",\"Shuffle again\":\"Ponownie przetasuj\",\"All groups\":\"Wszystkie grupy\",\"All tags (AND)\":\"Wszystkie tagi (ORAZ)\",\"Any tag (OR)\":\"Dowolny tag (LUB)\",\"Excluded tags (NOT)\":\"Wykluczone tagi (NIE)\",\"Language, region, or country\":\"Język, region lub kraj\",\"Minimum online minutes\":\"Minimalna liczba minut online\",\"Maximum online minutes\":\"Maksymalna liczba minut online\",\"Minimum local watch minutes\":\"Minimalna liczba minut oglądania lokalnego\",\"Advanced filters and Boolean tags\":\"Zaawansowane filtry i znaczniki logiczne\",\"Saved views and feeds\":\"Zapisane widoki i kanały\",\"Import\":\"Importuj\",\"Recent searches\":\"Ostatnie wyszukiwania\",\"Live previews and performance\":\"Podglądy na żywo i występy\",\"Muted live preview for every visible card\":\"Wyciszony podgląd na żywo dla każdej widocznej karty\",\"Refresh rate\":\"Częstotliwość odświeżania\",\"Real-time video while hovering\":\"Wideo w czasie rzeczywistym podczas unoszenia się\",\"Hover delay\":\"Opóźnienie zawisu\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Widoczne karty korzystają z ramek JPEG. Po najechaniu myszką tylko jedna karta przełącza się na wyciszone wideo w czasie rzeczywistym i zatrzymuje się całkowicie, gdy wskaźnik opuści. Wysoka liczba klatek na sekundę znacznie zwiększa przepustowość i wykorzystanie procesora.\",\"saved\":\"zapisane\",\"previews\":\"podglądy\",\"Previews off\":\"Podgląd wyłączony\",\"No more feeds can be saved\":\"Nie można zapisać więcej kanałów\",\"Tag feed saved\":\"Kanał tagów został zapisany\",\"Select tag\":\"Wybierz znacznik\",\"Tag favorite\":\"Oznacz ulubione\",\"Block tag\":\"Zablokuj znacznik\",\"Select tags first\":\"Najpierw wybierz tagi\",\"Tag-group name\":\"Nazwa grupy tagów\",\"Tag group saved\":\"Grupa tagów została zapisana\",\"No gaming rooms in the current catalog\":\"Brak pokoi gier w aktualnym katalogu\",\"All (AND)\":\"Wszystko (ORAZ)\",\"Any (OR)\":\"Dowolny (LUB)\",\"Exclude (NOT)\":\"Wyklucz (NIE)\",\"Search tags\":\"Szukaj tagów\",\"Tag search\":\"Wyszukiwanie tagów\",\"Combination\":\"Kombinacja\",\"Model sorting\":\"Sortowanie modeli\",\"Show in Discover\":\"Pokaż w Discover\",\"Save tag group\":\"Zapisz grupę tagów\",\"Save tag feed\":\"Zapisz kanał tagów\",\"No tag groups saved yet.\":\"Nie zapisano jeszcze żadnych grup tagów.\",\"Unknown region\":\"Nieznany region\",\"Unknown age\":\"Nieznany wiek\",\"Public “New”\":\"Publiczne „Nowe”\",\"My new cams\":\"Moje nowe kamery\",\"New-cams feed saved\":\"Zapisano kanał nowych kamer\",\"Save “My new cams” feed\":\"Zapisz kanał „Moje nowe kamery”.\",\"Create gaming workspace\":\"Utwórz przestrzeń roboczą do gier\",\"No matches for this combination.\":\"Brak dopasowań dla tej kombinacji.\",\"Select tags to preview matching rooms here.\":\"Wybierz tagi, aby wyświetlić podgląd pasujących pokoi tutaj.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Wyszukuj, dodawaj do ulubionych, blokuj i łącz tagi publiczne. Trendy wykorzystują wyłącznie zmiany katalogowe zaobserwowane na tym urządzeniu.\",\"Current tags\":\"Aktualne tagi\",\"Counts show current matches and change since the prior fetch.\":\"Liczniki pokazują bieżące dopasowania i zmieniają się od poprzedniego pobrania.\",\"Results for selected tags\":\"Wyniki dla wybranych tagów\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Używa tego samego sortowania i nawigacji SingleCam co Discover.\",\"Categories, age, and regions\":\"Kategorie, wiek i regiony\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Wyświetlane są tylko jawne pola publiczne. Regiony oddzielone przecinkami łączą się z OR; nieznane wartości pozostają widoczne. Żadna strefa czasowa nie jest zgadywana.\",\"New and gaming\":\"Nowość i gra\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Lokalnie widziane po raz pierwszy, publiczna flaga „Nowa”, tymczasowa lista obserwowanych i motywy gier pozostają odrębne.\",\"Tag groups\":\"Grupy tagów\",\"Save AND, OR, or NOT combinations.\":\"Zapisz kombinacje ORAZ, LUB lub NIE.\",\"Favorite tags\":\"Ulubione tagi\",\"Your personal shortcut list.\":\"Twoja osobista lista skrótów.\",\"No favorites yet\":\"Nie ma jeszcze ulubionych\",\"Related tags\":\"Powiązane tagi\",\"Tags co-occurring on the currently matching models.\":\"Tagi współwystępujące w aktualnie pasujących modelach.\",\"Personal tag history\":\"Historia tagów osobistych\",\"Recently and frequently used on this device.\":\"Ostatnio i często używany na tym urządzeniu.\",\"Bulk target group\":\"Masowa grupa docelowa\",\"Selected models added to group\":\"Wybrane modele dodane do grupy\",\"New workspace name\":\"Nowa nazwa obszaru roboczego\",\"Created from a Discover selection\":\"Utworzono na podstawie wyboru Discover\",\"Workspace created\":\"Utworzono obszar roboczy\",\"Save all\":\"Zapisz wszystko\",\"Create workspace\":\"Utwórz obszar roboczy\",\"Loading public catalog…\":\"Ładowanie katalogu publicznego…\",\"Loading the complete catalog in the background…\":\"Ładowanie całego katalogu w tle…\",\"Updated\":\"Zaktualizowano\",\"Public rooms\":\"Pokoje publiczne\",\"The catalog is loading.\":\"Katalog się ładuje.\",\"No matching rooms. Adjust search or filters.\":\"Brak pasujących pokoi. Dostosuj wyszukiwanie lub filtry.\",\"Previous\":\"Poprzedni\",\"Next\":\"Następny\",\"Move up\":\"Przesuń się w górę\",\"Move down\":\"Przesuń w dół\",\"Number shown\":\"Pokazany numer\",\"Widget width\":\"Szerokość widgetu\",\"Full\":\"Pełny\",\"Nothing here yet.\":\"Jeszcze nic tu nie ma.\",\"Newly online\":\"Nowo w Internecie\",\"Recently visited\":\"Ostatnio odwiedziłem\",\"Frequently visited\":\"Często odwiedzane\",\"Homepage appearance, pagination, and performance\":\"Wygląd strony głównej, paginacja i wydajność\",\"Information density\":\"Gęstość informacji\",\"Layout\":\"Układ\",\"Data rich\":\"Bogate w dane\",\"Static\":\"Statyczny\",\"Show statistics summary\":\"Pokaż podsumowanie statystyk\",\"Show personal widgets\":\"Pokaż osobiste widżety\",\"Sections below the live catalog\":\"Sekcje poniżej aktualnego katalogu\",\"Homepage live models\":\"Modele na żywo na stronie głównej\",\"All live rooms in one place\":\"Wszystkie live roomy w jednym miejscu\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Korzystaj z tych samych filtrów pełnego wyszukiwania, sortowania, grup, tagów i historii lokalnej, co w Discover.\",\"Refresh live catalog\":\"Odśwież katalog na żywo\",\"Homepage settings\":\"Ustawienia strony głównej\",\"Matches a favorite gaming theme\":\"Pasuje do ulubionego motywu gier\",\"Saved models\":\"Zapisane modele\",\"Online favorites\":\"Ulubione w Internecie\",\"Local watch time\":\"Lokalny czas oglądania\",\"Continue last MultiCam workspace\":\"Kontynuuj ostatni obszar roboczy MultiCam\",\"Open full Discover page\":\"Otwórz pełną stronę Discover\",\"Online notifications\":\"Powiadomienia internetowe\",\"on\":\"na\",\"off\":\"wyłączone\",\"Offline notifications\":\"Powiadomienia offline\",\"Feed\":\"Karmić\",\"Recent\":\"Najnowsze\",\"Frequent\":\"Częste\",\"Saved filter views\":\"Zapisane widoki filtrów\",\"Favorites currently public and online.\":\"Ulubione są obecnie publiczne i online.\",\"New models or public streams that started recently.\":\"Nowe modele lub publiczne transmisje, które rozpoczęły się niedawno.\",\"Current results from the selected feed.\":\"Aktualne wyniki z wybranego kanału.\",\"Suggestions with reasons\":\"Sugestie z uzasadnieniem\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Opiera się wyłącznie na udostępnionych tagach publicznych i lokalnie preferowanych motywach; żadnego wnioskowania o tożsamości.\",\"Sorted by local last-visit time.\":\"Posortowane według lokalnego czasu ostatniej wizyty.\",\"Sorted by local watch time.\":\"Posortowane według lokalnego czasu oglądania.\",\"Saved models never visited\":\"Zapisane modele nigdy nie odwiedzane\",\"Saved in the library without local watch time.\":\"Zapisano w bibliotece bez lokalnego czasu oglądania.\",\"Draggable dashboard widgets\":\"Przeciągalne widżety pulpitu nawigacyjnego\",\"Your RoomGrid homepage\":\"Twoja strona główna RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Szybki, konfigurowalny katalog na żywo z SingleCam, MultiCam, kompletnymi filtrami, zapisanymi widokami i osobistymi widżetami.\",\"Audio activity\":\"Aktywność dźwiękowa\",\"Auto quality\":\"Jakość automatyczna\",\"Stream interrupted; status checks continue.\":\"Strumień został przerwany; sprawdzanie statusu jest kontynuowane.\",\"This browser cannot play HLS.\":\"Ta przeglądarka nie może odtwarzać plików HLS.\",\"Connecting to live stream…\":\"Łączę z transmisją na żywo…\",\"The room is not public right now.\":\"Pokój nie jest obecnie publiczny.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Aktualnie nieaktywny. Poniżej przedstawiono alternatywy z tego samego filtra.\",\"Status temporarily unavailable; retrying automatically.\":\"Stan chwilowo niedostępny; ponawianie próby automatycznie.\",\"Loading public profile…\":\"Ładowanie profilu publicznego…\",\"Profile temporarily unavailable.\":\"Profil chwilowo niedostępny.\",\"No cached profile.\":\"Brak profilu w pamięci podręcznej.\",\"Public profile changed\":\"Zmieniono profil publiczny\",\"No public bio.\":\"Brak publicznej biografii.\",\"Source\":\"Źródło\",\"Observed\":\"Zaobserwowane\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Wyświetlane są wyłącznie linki opublikowane samodzielnie w profilu publicznym. Sama pasująca nazwa użytkownika nie stanowi dowodu tożsamości.\",\"Profile information\":\"Informacje o profilu\",\"Preparing…\":\"Przygotowanie…\",\"Play\":\"Zagraj\",\"Unmute\":\"Wyłącz wyciszenie\",\"Mute\":\"Wycisz\",\"Volume\":\"Objętość\",\"Contain\":\"Zawierać\",\"Cover\":\"Okładka\",\"Brightness\":\"Jasność\",\"Contrast\":\"Kontrast\",\"Saturation\":\"Nasycenie\",\"Hue\":\"Odcień\",\"Grayscale\":\"Skala szarości\",\"Invert\":\"Odwróć\",\"Blur\":\"Rozmycie\",\"Image zoom\":\"Powiększanie obrazu\",\"Recording saved\":\"Nagranie zapisane\",\"Screenshot saved\":\"Zrzut ekranu został zapisany\",\"Recording\":\"Nagrywanie\",\"Mirror\":\"Lustro\",\"Flip vertically\":\"Odwróć w pionie\",\"Rotate\":\"Obróć\",\"Show edited\":\"Pokaż edytowane\",\"Show original\":\"Pokaż oryginał\",\"Reset image\":\"Zresetuj obraz\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Przytrzymaj Ctrl i przewiń, aby powiększyć; przeciągnij obraz po powiększeniu. Kliknij dwukrotnie, aby zresetować.\",\"Zoom out\":\"Pomniejsz\",\"Zoom in\":\"Powiększ\",\"Reset zoom and position\":\"Zresetuj powiększenie i pozycję\",\"Player window\":\"Okno odtwarzacza\",\"Fullscreen\":\"Pełny ekran\",\"Private notes, memories, or things to check next time (stored locally)\":\"Prywatne notatki, wspomnienia lub rzeczy do sprawdzenia następnym razem (przechowywane lokalnie)\",\"Autosave is on\":\"Automatyczne zapisywanie jest włączone\",\"Enter your own tag…\":\"Wpisz swój własny tag…\",\"Saved at\":\"Zapisano o godz\",\"Local details saved\":\"Dane lokalne zostały zapisane\",\"Remove tag\":\"Usuń znacznik\",\"No personal tags yet.\":\"Nie ma jeszcze tagów osobistych.\",\"Saving…\":\"Zapisuję…\",\"Create your first group under Groups.\":\"Utwórz pierwszą grupę w obszarze Grupy.\",\"Added to MultiCam\":\"Dodano do MultiCam\",\"Edit private details\":\"Edytuj dane prywatne\",\"Trans\":\"Przeł\",\"Current online time\":\"Aktualny czas online\",\"Stream attributes\":\"Atrybuty strumienia\",\"New\":\"Nowy\",\"Yes\":\"Tak\",\"No / unknown\":\"Nie / nieznane\",\"Last observed\":\"Ostatnio zaobserwowano\",\"Local visits\":\"Wizyty lokalne\",\"First watched\":\"Najpierw obejrzałem\",\"No public description.\":\"Brak publicznego opisu.\",\"No public tags available.\":\"Brak dostępnych tagów publicznych.\",\"Quick-add your own tag…\":\"Szybko dodaj swój własny tag…\",\"Public tags\":\"Tagi publiczne\",\"Add\":\"Dodaj\",\"Private details\":\"Prywatne szczegóły\",\"Everything stays in this browser and survives updates.\":\"Wszystko pozostaje w tej przeglądarce i przetrwa aktualizacje.\",\"Private notebook\":\"Prywatny notatnik\",\"Keep on watchlist\":\"Zachowaj na liście obserwowanych\",\"Group assignment (multiple)\":\"Przypisanie grupowe (wielokrotne)\",\"Save now\":\"Zapisz teraz\",\"Chat loads only when this section is opened.\":\"Czat ładuje się tylko wtedy, gdy ta sekcja jest otwarta.\",\"Official chat\":\"Oficjalny czat\",\"Official chat connected.\":\"Oficjalny czat podłączony.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Czat załadowany; nie udało się w pełni wyodrębnić aktualnej struktury strony.\",\"Chat loaded.\":\"Czat załadowany.\",\"Loading official chat…\":\"Ładowanie oficjalnego czatu…\",\"Official chat room\":\"Oficjalny czat\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Ładowane tylko w razie potrzeby; RoomGrid nigdy nie przechowuje ani nie wysyła automatycznie wiadomości czatu.\",\"Reload chat\":\"Załaduj ponownie czat\",\"Open official room in a new window\":\"Otwórz oficjalny pokój w nowym oknie\",\"Hide chat\":\"Ukryj czat\",\"Live observations\":\"Obserwacje na żywo\",\"Trend confidence\":\"Pewność trendu\",\"Known time slots\":\"Znane przedziały czasowe\",\"Typical peak\":\"Typowy szczyt\",\"Watched in 7 days\":\"Obejrzyj w 7 dni\",\"Watched in 30 days\":\"Obejrzyj w 30 dni\",\"Current streak\":\"Aktualna passa\",\"Typical weekly online times\":\"Typowe tygodniowe czasy online\",\"Online times will be learned on this device from now on.\":\"Od teraz czasy online będą się uczyć na tym urządzeniu.\",\"Based on observations on this device; missing information is never guessed.\":\"Na podstawie obserwacji tego urządzenia; brakujących informacji nigdy się nie odgadnie.\",\"Activity by weekday\":\"Aktywność według dni powszednich\",\"Most likely online times\":\"Najprawdopodobniej godziny online\",\"Online-time map\":\"Mapa czasu online\",\"Public covers and locally saved example images.\":\"Okładki publiczne i przykładowe obrazy zapisane lokalnie.\",\"Import quality\":\"Jakość importu\",\"Original file\":\"Oryginalny plik\",\"Very high (up to 4K)\":\"Bardzo wysoka (do 4K)\",\"High (up to 1440p)\":\"Wysoka (do 1440p)\",\"Space-saving (up to 900p)\":\"Oszczędność miejsca (do 900p)\",\"Add images from computer\":\"Dodaj obrazy z komputera\",\"Use website cover\":\"Użyj okładki witryny\",\"Use first image as cover\":\"Użyj pierwszego obrazu jako okładki\",\"Covers and local gallery\":\"Okładki i galeria lokalna\",\"Updating images…\":\"Aktualizuję obrazy…\",\"Loading images…\":\"Ładowanie obrazów…\",\"Own image\":\"Własny obraz\",\"Example image\":\"Przykładowy obraz\",\"Current cover\":\"Aktualna okładka\",\"Alternative preview\":\"Alternatywny podgląd\",\"Current public image\":\"Aktualny wizerunek publiczny\",\"Saving cover…\":\"Zapisuję okładkę…\",\"Set as cover\":\"Ustaw jako okładkę\",\"Delete this image from the local gallery?\":\"Usunąć ten obraz z lokalnej galerii?\",\"No images yet.\":\"Nie ma jeszcze żadnych obrazów.\",\"saved locally\":\"zapisane lokalnie\",\"own images\":\"własne obrazy\",\"Update public previews\":\"Zaktualizuj publiczne podglądy\",\"Processing your images…\":\"Przetwarzam Twoje obrazy…\",\"ALL PAGE SOUNDS OFF\":\"WSZYSTKIE STRONY WYŁĄCZĄ DŹWIĘKI\",\"PAGE SOUNDS ALLOWED\":\"DŹWIĘKI STRONY DOZWOLONE\",\"Chat/token sounds OFF\":\"Dźwięki czatu/tokena WYŁĄCZONE\",\"Chat/token sounds ON\":\"Dźwięki czatu/tokena WŁĄCZONE\",\"Reconnecting the player with the new settings…\":\"Ponowne łączenie odtwarzacza z nowymi ustawieniami…\",\"Turn off chat, token and tip sounds completely\":\"Całkowicie wyłącz dźwięki czatu, tokenów i napiwków\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Blokuje dźwięki stron z elementów audio i Web Audio. Strumieniowe przesyłanie dźwięku można oddzielnie kontrolować za pomocą opcji „Włącz dźwięk”.\",\"No alternatives available.\":\"Brak dostępnych alternatyw.\",\"Similar models and offline alternatives\":\"Podobne modele i alternatywy offline\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Priorytet według udostępnionych tagów; brak wnioskowania o tożsamości lub koncie zewnętrznym.\",\"Chat\":\"Czat\",\"Notes & organization\":\"Notatki i organizacja\",\"Images\":\"Obrazy\",\"SingleCam sections\":\"Sekcje SingleCam\",\"Back to original results\":\"Powrót do oryginalnych wyników\",\"Private workspace\":\"Prywatna przestrzeń do pracy\",\"Chat on/off\":\"Czat włączony/wyłączony\",\"Hide similar models\":\"Ukryj podobne modele\",\"Show similar models\":\"Pokaż podobne modele\",\"RoomGrid Home\":\"Strona główna RoomGrid\",\"RoomGrid Tags\":\"Tagi RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Katalog publiczny niedostępny; pokazując lokalną bibliotekę.\",\"Drag to resize sidebar\":\"Przeciągnij, aby zmienić rozmiar paska bocznego\",\"The workspace could not be saved safely. Export a backup now.\":\"Nie udało się bezpiecznie zapisać obszaru roboczego. Wyeksportuj teraz kopię zapasową.\",\"Open menu or restore controls\":\"Otwórz menu lub przywróć elementy sterujące\",\"Controls restored\":\"Przywrócono kontrolę\",\"Show toolbar\":\"Pokaż pasek narzędzi\",\"Tools\":\"Narzędzia\",\"Show sidebar\":\"Pokaż pasek boczny\",\"Restore controls\":\"Przywróć kontrolę\",\"Show toolbar, groups, and menus\":\"Pokaż pasek narzędzi, grupy i menu\",\"Open more tools\":\"Otwórz więcej narzędzi\",\"all watch states\":\"wszystkie stany zegarka\",\"Watch status\":\"Oglądaj stan\",\"0 means no viewer limit.\":\"0 oznacza brak limitu widzów.\",\"Import temporary URLs; they disappear after refresh\":\"Importuj tymczasowe adresy URL; znikają po odświeżeniu\",\"URL\":\"Adres URL\",\"Filter current group\":\"Filtruj bieżącą grupę\",\"All status\":\"Wszystkie stany\",\"Hide offline/private\":\"Ukryj offline/prywatny\",\"Collapse toolbar\":\"Zwiń pasek narzędzi\",\"Windows per page\":\"Okna na stronę\",\"Custom grid\":\"Niestandardowa siatka\",\"Choose MultiCam audio routing\":\"Wybierz routing audio MultiCam\",\"Audio: manual\":\"Dźwięk: ręczny\",\"Audio: solo cam\":\"Dźwięk: kamera solowa\",\"Audio: follow focus\":\"Dźwięk: podążaj za ostrością\",\"Auto\":\"Automat\",\"Userscript version\":\"Wersja skryptu użytkownika\",\"Open Discover and dashboard\":\"Otwórz Discover i pulpit nawigacyjny\",\"Global search and quick commands · Ctrl+K\":\"Wyszukiwanie globalne i szybkie polecenia · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Kondycja strumieniowa, budżet jakości, dźwięk, sceny i zamienniki\",\"Control\":\"Kontrola\",\"Rooms\":\"Pokoje\",\"Filter\":\"Filtruj\",\"Playback\":\"Odtwarzanie\",\"Stabilizing live preview…\":\"Stabilizuję podgląd na żywo…\",\"Recovering live preview…\":\"Odzyskiwanie podglądu na żywo…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"Podgląd na żywo jest na chwilę wstrzymywany, aby zapobiec pętli ponownego ładowania. Spróbuj ponownie wkrótce.\",\"Collapse groups\":\"Zwiń grupy\",\"Expand/collapse users\":\"Rozwiń/zwiń użytkowników\",\"Output volume; no live signal level is available for this stream\":\"głośność wyjściowa; dla tej transmisji nie jest dostępny poziom sygnału na żywo\",\"OFF\":\"WYŁ\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Wklej adresy URL. Obsługiwane są strony pokojów, m3u8, mp4/webm. Zwykłe strony zostaną przeanalizowane z największą starannością.\",\"Actual resolution, connection and buffer\":\"Rzeczywista rozdzielczość, połączenie i bufor\",\"Drag to reorder / move group\":\"Przeciągnij, aby zmienić kolejność/przenieść grupę\",\"All time\":\"Cały czas\",\"Click a model for details.\":\"Kliknij model, aby uzyskać szczegółowe informacje.\",\"Most watched groups\":\"Najczęściej oglądane grupy\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Pola konta, takie jak data dołączenia, są wyświetlane tylko wtedy, gdy Chaturbate wyraźnie je eksponuje na tej samej stronie. Skrypt nie wymyśla wartości.\",\"Back\":\"Powrót\",\"Statistics import failed: \":\"Import statystyk nie powiódł się:\",\"Recent saves\":\"Ostatnie zapisy\",\"Rooms per page\":\"Pokoje na stronę\",\"Custom rows\":\"Niestandardowe wiersze\",\"Custom columns\":\"Niestandardowe kolumny\",\"Main width\":\"Główna szerokość\",\"Main height\":\"Główna wysokość\",\"Modules\":\"Moduły\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Moduły rozpoczynają się tylko na pasujących typach stron. Zapisanie powoduje ponowne załadowanie bieżącej strony.\",\"Tools on normal pages\":\"Narzędzia na normalnych stronach\",\"True audio-level meters\":\"Prawdziwe mierniki poziomu dźwięku\",\"MultiCam control center\":\"Centrum sterowania MultiCamem\",\"No active streams yet\":\"Nie ma jeszcze aktywnych transmisji\",\"Stream health\":\"Zdrowie strumieniowe\",\"Budget off\":\"Budżet wyłączony\",\"Economy · 2160p total\":\"Ekonomiczna · Łącznie 2160p\",\"Balanced · 4320p total\":\"Zrównoważony · Łącznie 4320p\",\"High · 6480p total\":\"Wysoka · Łącznie 6480p\",\"Ultra · 8640p total\":\"Ultra · Łącznie 8640p\",\"Quality budget is off; global and per-camera limits still apply.\":\"Budżet jakościowy jest wyłączony; Limity globalne i limity dotyczące kamery nadal obowiązują.\",\"Quality budget\":\"Budżet jakości\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Rozdziela jakość pomiędzy aktywne kamery. Wyraźna jakość obrazu zawsze wygrywa.\",\"Give the focused cam double weight\":\"Daj skupionej kamerze podwójną wagę\",\"Manual per camera\":\"Instrukcja dla każdej kamery\",\"Solo: one camera only\":\"Solo: tylko jedna kamera\",\"Follow focused camera\":\"Podążaj za skupioną kamerą\",\"Audio management\":\"Zarządzanie dźwiękiem\",\"Routing never overwrites saved mute states or individual volume.\":\"Funkcja routingu nigdy nie zastępuje zapisanych stanów wyciszenia ani indywidualnej głośności.\",\"Mute all\":\"Wycisz wszystko\",\"Restore saved audio\":\"Przywróć zapisany dźwięk\",\"Scene name\":\"Nazwa sceny\",\"Focus\":\"Skup się\",\"Grid\":\"Siatka\",\"cams\":\"kamery\",\"Load\":\"Załaduj\",\"No scenes saved for this workspace yet.\":\"Nie zapisano jeszcze żadnych scen dla tego obszaru roboczego.\",\"Scenes\":\"Sceny\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Zapisz bieżące gniazda, układ i routing audio, aby przywrócić je jednym kliknięciem.\",\"Save current view as scene\":\"Zapisz bieżący widok jako scenę\",\"All saved online models\":\"Wszystkie zapisane modele online\",\"Replacement models\":\"Modele zamienne\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Preferowane modele są wypróbowywane w kolejności listy; dopiero wtedy wykorzystywane są modele online z wybranej grupy. Ręczne przypisania zawsze wygrywają.\",\"Replace offline slots automatically\":\"Automatycznie zastępuj sloty offline\",\"Delay (seconds)\":\"Opóźnienie (sekundy)\",\"Candidate group\":\"Grupa kandydatów\",\"Preferred replacements (multiple)\":\"Preferowane zamienniki (wiele)\",\"Replacement rule saved\":\"Reguła zastępcza została zapisana\",\"Save replacement rule\":\"Zapisz regułę zastępczą\",\"Resolve duplicate shortcuts first:\":\"Najpierw rozwiąż zduplikowane skróty:\",\"Name, note or own tag…\":\"Imię, notatka lub własny tag…\",\"Camera menu\":\"Menu aparatu\",\"Camera menu · quality and sound\":\"Menu aparatu · jakość i dźwięk\",\"Close menu\":\"Zamknij menu\",\"View and window\":\"Widok i okno\",\"Audio\":\"Dźwięk\",\"Quality and image\":\"Jakość i wizerunek\",\"More actions\":\"Więcej akcji\",\"Currently main\":\"Obecnie główny\",\"Set as main\":\"Ustaw jako główne\",\"Pin live preview\":\"Przypnij podgląd na żywo\",\"Volume multiplier for this cam\":\"Mnożnik głośności dla tej kamery\",\"Reset individual volume to 100%\":\"Zresetuj indywidualną głośność do 100%\",\"Individual volume\":\"Indywidualna objętość\",\"Listen to this cam only\":\"Słuchaj tylko tej kamery\",\"Set quality for this camera only\":\"Ustaw jakość tylko dla tego aparatu\",\"Automatic (global/adaptive)\":\"Automatyczny (globalny/adaptacyjny)\",\" · available\":\"· dostępny\",\"Quality for this camera\":\"Jakość jak na ten aparat\",\"Hold for original\":\"Trzymaj za oryginał\",\"Vivid\":\"Żywy\",\"Image adjustments\":\"Korekty obrazu\",\"More tools\":\"Więcej narzędzi\",\"Interface\":\"Interfejs\",\"Show top controls\":\"Pokaż górne elementy sterujące\",\"Collapse top controls\":\"Zwiń górne elementy sterujące\",\"Show groups\":\"Pokaż grupy\",\"Windows\":\"Okna\",\"Open workspace in new window\":\"Otwórz obszar roboczy w nowym oknie\",\"Control center\":\"Centrum kontroli\",\"Choose the data to export\":\"Wybierz dane do eksportu\",\"Preview the file and conflicts before applying it\":\"Wyświetl podgląd pliku i konfliktów przed jego zastosowaniem\",\"Help\":\"Pomoc\",\"Changes in this version\":\"Zmiany w tej wersji\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Obszary robocze, biblioteka modeli, bezpieczne importy, globalny projekt, rozszerzone normalne strony, rozszerzony Discover/pulpit nawigacyjny i kontrola jakości odtwarzacza/obrazu.\",\"Muted\":\"Wyciszony\",\"Performance limit reached\":\"Osiągnięto limit wydajności\",\"Drag to resize main width\":\"Przeciągnij, aby zmienić szerokość główną\",\"Drag to resize main height\":\"Przeciągnij, aby zmienić rozmiar głównej wysokości\",\"Stable\":\"Stabilny\",\"Unstable\":\"Niestabilny\",\"Refresh all cameras\":\"Odśwież wszystkie kamery\",\"Reconnect current models\":\"Podłącz ponownie aktualne modele\",\"Switch to grid view\":\"Przełącz na widok siatki\",\"Switch to focus view\":\"Przełącz na widok skupiony\",\"Manage workspaces\":\"Zarządzaj obszarami roboczymi\",\"Create, duplicate, archive and device layouts\":\"Twórz, duplikuj, archiwizuj i układy urządzeń\",\"Open model library\":\"Otwórz bibliotekę modeli\",\"Notes, tags, ratings and bulk actions\":\"Notatki, tagi, oceny i akcje zbiorcze\",\"Design options\":\"Opcje projektowania\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Uniwersalna stacja robocza z wieloma widokami do obsługi pomieszczeń, nagrywania, alertów, zrzutów ekranu, ponownego łączenia i narzędzi do transmisji danych.\",\"Username ↵\":\"Nazwa użytkownika ↵\",\"Search rooms\":\"Wyszukaj pokoje\",\"Invalid username\":\"Nieprawidłowa nazwa użytkownika\",\"Hide offline\":\"Ukryj offline\",\"Hide private\":\"Ukryj prywatne\",\"Manual\":\"Instrukcja\",\"By status\":\"Według stanu\",\"By name\":\"Po imieniu\",\"By added time\":\"Według doliczonego czasu\",\"By watch time\":\"Według czasu oglądania\",\"By sessions\":\"Przez sesje\",\"By last watched\":\"Do ostatniego oglądania\",\"By first watched\":\"Najpierw obejrzałem\",\"By viewer count\":\"Według liczby widzów\",\"By last online\":\"Do ostatniego online\",\"Advanced filters\":\"Zaawansowane filtry\",\"Watched before\":\"Oglądane wcześniej\",\"Currently in view\":\"Obecnie w widoku\",\"Include tag\":\"Dołącz tag\",\"Exclude tag\":\"Wyklucz tag\",\"Minimum watch time (minutes)\":\"Minimalny czas oglądania (minuty)\",\"Watched within days\":\"Obejrzyj w ciągu kilku dni\",\"Refresh all\":\"Odśwież wszystko\",\"Online alerts\":\"Alerty internetowe\",\"Offline alerts\":\"Alerty offline\",\"Desktop notification + card flash when a model goes online\":\"Powiadomienie na pulpicie + flashowanie karty, gdy modelka przejdzie do trybu online\",\"Desktop notification + card flash when a model goes offline\":\"Powiadomienie na pulpicie + flashowanie karty, gdy model przechodzi w tryb offline\",\"Dark mode\":\"Tryb ciemny\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Użyj ciemnego motywu interfejsu. Przesyłanie strumieniowe wideo i audio pozostaje niezmienione.\",\"Dark mode enabled\":\"Włączono tryb ciemny\",\"Light mode enabled\":\"Włączono tryb oświetlenia\",\"Mute token/tip sounds\":\"Wycisz dźwięki tokenów i napiwków\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Blokuje tylko dźwięki powiadomień o tokenach i napiwkach. Dźwięk transmisji pozostaje włączony.\",\"Token/tip sounds muted\":\"Dźwięki tokenów i napiwków wyciszone\",\"Token/tip sounds enabled\":\"Dźwięki tokenów i napiwków włączone\",\"Clean\":\"Czysty\",\"Clean mode on\":\"Włączony tryb czyszczenia\",\"Exit clean mode\":\"Wyjdź z trybu czyszczenia\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Tymczasowo ukryj wszystkie elementy sterujące i nakładki. Skrót: Alt+P / Alt+C. Naciśnij Esc, aby wyjść.\",\"Window first\":\"Najpierw okno\",\"Window-first mode on\":\"Tryb „Najpierw okno” włączony\",\"Exit window-first\":\"Wyjdź najpierw przez okno\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Ukryj powłokę aplikacji i zachowaj widoczność okien pokoju. Skrót: Alt+V.\",\"Show thumbnails\":\"Pokaż miniatury\",\"Hide thumbnails\":\"Ukryj miniatury\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Pokaż lub ukryj szynę miniatur w trybie ostrości. Skrót: Alt+T.\",\"Choose Focus rooms\":\"Wybierz pokoje Focus\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Wybierz pokoje dla trybu Fokus. Pierwszy wybrany pokój jest ekranem głównym; użyj ↑/↓, aby ustawić kolejność okien wtórnych.\",\"Main screen\":\"Główny ekran\",\"Right window\":\"Prawe okno\",\"Bottom window\":\"Dolne okno\",\"Empty slot\":\"Puste gniazdo\",\"Drag this model to a Focus slot\":\"Przeciągnij ten model do gniazda Fokus\",\"Choose model for this slot\":\"Wybierz model dla tego gniazda\",\"Select a saved model for this exact Focus slot.\":\"Wybierz zapisany model dla tego dokładnie gniazda Focus.\",\"Choose model for this window\":\"Wybierz model dla tego okna\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Wybierz zapisany model dla tego dokładnego okna siatki. Istniejące modele pozostają zapisane.\",\"Remove from this grid window\":\"Usuń z tego okna siatki\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Tylko to miejsce jest wyczyszczone. Zapisany model pozostaje w Twoim obszarze roboczym.\",\"Show in grid window\":\"Pokaż w oknie siatki\",\"Choose the exact active grid window for this model.\":\"Wybierz dokładnie aktywne okno siatki dla tego modelu.\",\"Current\":\"Aktualny\",\"Model removed from this grid window.\":\"Model został usunięty z tego okna siatki.\",\"No saved models in this group.\":\"Brak zapisanych modeli w tej grupie.\",\"Secondary window\":\"Okno wtórne\",\"Use current page automatically\":\"Użyj bieżącej strony automatycznie\",\"Clear custom selection\":\"Wyczyść wybór niestandardowy\",\"Apply Focus layout\":\"Zastosuj układ Fokusa\",\"No rooms are available in the current group and filter.\":\"W bieżącej grupie i filtrze nie ma dostępnych pokoi.\",\"Fit: full image\":\"Dopasowanie: pełny obraz\",\"Fit: fill window\":\"Dopasowanie: wypełnij okno\",\"Switch between full image and cropped fill.\":\"Przełączaj między pełnym obrazem a przyciętym wypełnieniem.\",\"Playback settings\":\"Ustawienia odtwarzania\",\"Max stream quality\":\"Maksymalna jakość strumienia\",\"Auto / no cap\":\"Automatyczny / bez czapki\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + powiększenie koła\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Powiększ kartę za pomocą Ctrl/Command + kółko myszy, przeciągnij podczas powiększenia, kliknij dwukrotnie, aby zresetować.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Wyjdź z trybu czyszczenia. Skrót: Alt+P / Alt+C lub Esc.\",\"Clean · Alt+P/C / Esc\":\"Wyczyść · Alt+P/C / Esc\",\"(synced from another tab)\":\"(synchronizowane z innej karty)\",\"Type a username and press Enter. Shortcut: /\":\"Wpisz nazwę użytkownika i naciśnij Enter. Skrót: /\",\"Filter visible rooms by username\":\"Filtruj widoczne pokoje według nazwy użytkownika\",\"Show / hide group sidebar\":\"Pokaż/ukryj pasek boczny grupy\",\"Global volume. Per-room mute is still respected.\":\"Globalny wolumen. Wyciszenie poszczególnych pomieszczeń jest nadal przestrzegane.\",\"Base tile size in grid view\":\"Podstawowy rozmiar kafelka w widoku siatki\",\"Sort rooms in the current group\":\"Sortuj pokoje w bieżącej grupie\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Wysokość ekranu głównego w trybie ostrości. Możesz także przeciągnąć separator.\",\"Main-screen aspect ratio in Focus mode\":\"Proporcje ekranu głównego w trybie ostrości\",\"Thumbnail width in Focus mode\":\"Szerokość miniatury w trybie ostrości\",\"Show every saved room. The remove button deletes globally in this view.\":\"Pokaż każdy zapisany pokój. Przycisk usuwania usuwa globalnie w tym widoku.\",\"Create an isolated group\":\"Utwórz izolowaną grupę\",\"More tools and maintenance\":\"Więcej narzędzi i konserwacji\",\"GROUPS\":\"GRUPY\",\"All saved\":\"Wszystko zapisane\",\"Default\":\"Domyślne\",\"Online now\":\"Teraz online\",\"New group name:\":\"Nowa nazwa grupy:\",\"Rename\":\"Zmień nazwę\",\"Rename to:\":\"Zmień nazwę na:\",\"Delete group\":\"Usuń grupę\",\"Live preview\":\"Podgląd na żywo\",\"Loading preview…\":\"Ładowanie podglądu…\",\"Private or away\":\"Prywatnie lub poza domem\",\"Preview could not be loaded\":\"Nie można załadować podglądu\",\"Toggle mute\":\"Przełącz wyciszenie\",\"Resume\":\"Wznów\",\"Screenshot current frame\":\"Zrzut ekranu bieżącej klatki\",\"Record current card\":\"Zapisz aktualną kartę\",\"Stop recording\":\"Zatrzymaj nagrywanie\",\"Picture-in-Picture\":\"Obraz w obrazie\",\"Remove from current group\":\"Usuń z bieżącej grupy\",\"Delete saved room\":\"Usuń zapisany pokój\",\"Mirror image\":\"Odbicie lustrzane\",\"Flip image\":\"Odwróć obraz\",\"Rotate left\":\"Obróć w lewo\",\"Rotate right\":\"Obróć w prawo\",\"Reset view\":\"Zresetuj widok\",\"Copy username\":\"Skopiuj nazwę użytkownika\",\"Add favorite\":\"Dodaj ulubione\",\"Move to favorites only\":\"Przejdź tylko do ulubionych\",\"Copied\":\"Skopiowano\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Zrzut ekranu nie powiódł się. Przeglądarka/CORS może blokować rysowanie tego strumienia.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"Nagrywanie odbywa się lokalnie i zapisuje ten film z pasującym dźwiękiem, gdy przeglądarka go odsłoni. Używaj go tylko wtedy, gdy masz pozwolenie na zapisanie tej zawartości. Kontynuować?\",\"Recording started\":\"Rozpoczęło się nagrywanie\",\"Recording is not supported for this video/browser\":\"Nagrywanie nie jest obsługiwane w przypadku tego filmu/przeglądarki\",\"Recording segment saved\":\"Segment nagrania został zapisany\",\"Final recording segment saved\":\"Zapisano końcowy fragment nagrania\",\"Recording paused; waiting for the stream to return\":\"Nagrywanie wstrzymane; czekam na powrót streama\",\"Recording resumed\":\"Nagrywanie wznowione\",\"Recording paused; click to stop\":\"Nagrywanie wstrzymane; kliknij, aby zatrzymać\",\"No recording data to save\":\"Brak danych nagrywania do zapisania\",\"Recording settings saved\":\"Ustawienia nagrywania zostały zapisane\",\"Segment length in minutes (1-180):\":\"Długość segmentu w minutach (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Szybkość transmisji wideo w Mb/s (0,5–20):\",\"Warn before leaving while recording\":\"Ostrzegaj przed opuszczeniem obiektu podczas nagrywania\",\"Recording is still active. Leave anyway?\":\"Nagrywanie jest nadal aktywne. Mimo to wyjść?\",\"Recording center\":\"Centrum nagrań\",\"No active recordings\":\"Brak aktywnych nagrań\",\"Waiting for source\":\"Czekam na źródło\",\"Saved segments\":\"Zapisane segmenty\",\"Duration\":\"Czas trwania\",\"Bitrate\":\"Szybkość transmisji\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Format: MP4, jeśli jest obsługiwany przez tę przeglądarkę; w przeciwnym razie najlepiej obsługiwany rezerwowy.\",\"Record current page\":\"Nagraj bieżącą stronę\",\"Record current group\":\"Nagraj bieżącą grupę\",\"Record online rooms\":\"Nagrywaj pokoje online\",\"Stop all recordings\":\"Zatrzymaj wszystkie nagrania\",\"Show recording only\":\"Pokaż tylko nagranie\",\"Show all rooms\":\"Pokaż wszystkie pokoje\",\"Open current page rooms\":\"Otwórz pokoje bieżącej strony\",\"Move current page to group\":\"Przenieś bieżącą stronę do grupy\",\"Layout settings\":\"Ustawienia układu\",\"Recording settings\":\"Ustawienia nagrywania\",\"Save settings\":\"Zapisz ustawienia\",\"Config backups\":\"Skonfiguruj kopie zapasowe\",\"No config backups found\":\"Nie znaleziono kopii zapasowych konfiguracji\",\"Online/offline history\":\"Historia online/offline\",\"Group rules\":\"Zasady grupy\",\"Keep favorites first\":\"Najpierw zachowaj ulubione\",\"Temporary URL manager\":\"Tymczasowy menedżer adresów URL\",\"Save current temporary URLs\":\"Zapisz bieżące tymczasowe adresy URL\",\"No temporary URLs\":\"Brak tymczasowych adresów URL\",\"Share workspace\":\"Udostępnij przestrzeń roboczą\",\"Share current rooms and layout\":\"Udostępnij aktualne pokoje i układ\",\"Copy share link\":\"Skopiuj link do udostępniania\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"To łącze przywraca pokoje, grupy i ustawienia układu na innej karcie stacji roboczej.\",\"Shared workspace imported\":\"Zaimportowano udostępniony obszar roboczy\",\"Shared workspace link is invalid\":\"Link do udostępnionego obszaru roboczego jest nieprawidłowy\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Kliknij pole i naciśnij nowy skrót. Backspace czyści to pole.\",\"Reset shortcuts\":\"Zresetuj skróty\",\"Focus add box\":\"Pole dodawania fokusu\",\"Grid view\":\"Widok siatki\",\"Focus view\":\"Widok ostrości\",\"Clean mode\":\"Tryb czyszczenia\",\"Window-first mode\":\"Tryb pierwszego okna\",\"Toggle thumbnails\":\"Przełącz miniatury\",\"Auto-detecting…\":\"Automatyczne wykrywanie…\",\"No rooms in this group\":\"Brak pokoi w tej grupie\",\"Add a username at the top, switch group, or clear filters\":\"Dodaj nazwę użytkownika u góry, przełącz grupę lub wyczyść filtry\",\"Open workstation\":\"Otwórz stację roboczą\",\"Open here (replace page)\":\"Otwórz tutaj (zamień stronę)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Zamienić tę stronę na stację roboczą? Możesz wrócić przez przeglądarkę.\",\"View saved list\":\"Zobacz zapisaną listę\",\"Collapse dock\":\"Zwiń dok\",\"No room detected\":\"Nie wykryto pokoju\",\"Open here\":\"Otwórz tutaj\",\"Add room\":\"Dodaj pokój\",\"Remove room\":\"Usuń pokój\",\"Record in workstation\":\"Nagraj na stacji roboczej\",\"Screenshot video\":\"Zrzut ekranu wideo\",\"Play / pause\":\"Odtwórz / pauza\",\"Mute / unmute\":\"Wycisz / wyłącz wyciszenie\",\"No playable video found on this page\":\"Na tej stronie nie znaleziono wideo, które można odtworzyć\",\"Recording intent queued in workstation\":\"Zamiar nagrywania ustawiony w kolejce na stacji roboczej\",\"Added\":\"Dodano\",\"Already exists\":\"Już istnieje\",\"Failed\":\"Nie udało się\",\"Add to workstation\":\"Dodaj do stacji roboczej\",\"Already in workstation — click to remove\":\"Już na stacji roboczej — kliknij, aby usunąć\",\"Model online\":\"Modelka w Internecie\",\"Model offline\":\"Modeluj offline\",\"Show fewer\":\"Pokaż mniej\",\"No measurable audio at the moment\":\"W tej chwili brak mierzalnego dźwięku\",\"Playback paused\":\"Odtwarzanie wstrzymane\",\"Audio measurement unavailable or blocked\":\"Pomiar dźwięku niedostępny lub zablokowany\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Odmowa pozwolenia na powiadomienie. Flash karty nadal działa.\\n\\nMożesz ją włączyć w ustawieniach przeglądarki.\",\"Batch add\":\"Dodaj partię\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Wklej nazwy użytkowników, po jednej w wierszu lub oddzielone spacjami/przecinkami:\",\"Cancel\":\"Anuluj\",\"Change the language of RoomGrid and the website\":\"Zmień język RoomGrid i strony internetowej\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Zmień język RoomGrid. Chaturbate pozostaje w obecnym języku witryny, ponieważ nie jest dostępny żaden oficjalny język witryny.\",\"About\":\"O\",\"Export config\":\"Eksportuj konfigurację\",\"Export usernames.txt\":\"Eksportuj nazwy użytkowników.txt\",\"Copy usernames\":\"Skopiuj nazwy użytkowników\",\"Unmute all\":\"Wyłącz wyciszenie wszystkich\",\"⏸ Pause visible\":\"⏸Pauza widoczna\",\"Resume visible\":\"CV widoczne\",\"Following\":\"Śledzenie\",\"Sync followed models now\":\"Synchronizuj teraz obserwowane modele\",\"✓ Auto-sync followed models\":\"✓ Automatyczna synchronizacja obserwowanych modeli\",\"☐ Auto-sync followed models\":\"☐ Automatycznie synchronizuj obserwowane modele\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Zaimportuj aktualnie obserwowane modele Chaturbate do trwałej grupy Obserwowane. Synchronizowana jest tylko ta grupa.\",\"Syncing followed models…\":\"Synchronizowanie obserwowanych modeli…\",\"Automatic Following sync disabled\":\"Automatyczna synchronizacja śledzenia wyłączona\",\"Automatic Following sync enabled\":\"Włączono automatyczną synchronizację śledzenia\",\"Following sync needs an active Chaturbate login\":\"Następująca synchronizacja wymaga aktywnego logowania do Chaturbate\",\"Following sync failed safely; saved workspace data was not changed\":\"Następująca synchronizacja nie powiodła się bezpiecznie; zapisane dane obszaru roboczego nie zostały zmienione\",\"Enable dark mode\":\"Włącz tryb ciemny\",\"Disable dark mode\":\"Wyłącz tryb ciemny\",\"Status history\":\"Historia stanu\",\"Temporary URLs\":\"Tymczasowe adresy URL\",\"Shortcut panel\":\"Panel skrótów\",\"Toggle video fit\":\"Przełącz dopasowanie wideo\",\"Shortcuts / hints\":\"Skróty / podpowiedzi\",\"Ranking\":\"Zaszeregowanie\",\"Model details\":\"Szczegóły modelu\",\"My profile\":\"Mój profil\",\"Loading local statistics…\":\"Ładowanie statystyk lokalnych…\",\"No watch data yet. Statistics start with this version.\":\"Nie ma jeszcze danych zegarka. Statystyki zaczynają się od tej wersji.\",\"Statistics are disabled.\":\"Statystyki są wyłączone.\",\"Enable statistics\":\"Włącz statystyki\",\"Disable statistics\":\"Wyłącz statystyki\",\"Export statistics\":\"Eksportuj statystyki\",\"Delete all statistics\":\"Usuń wszystkie statystyki\",\"Delete this model data\":\"Usuń dane tego modelu\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Usunąć wszystkie zebrane lokalnie statystyki oglądania? Twoje miejsce pracy pozostaje nietknięte.\",\"Tracking since\":\"Śledzenie od\",\"Models watched\":\"Modelki oglądały\",\"Watch time\":\"Czas oglądania\",\"Most watched tags\":\"Najczęściej oglądane tagi\",\"Most watched hours\":\"Najczęściej oglądane godziny\",\"Observed online\":\"Zaobserwowane w Internecie\",\"Public bio\":\"Biografia publiczna\",\"Public links\":\"Linki publiczne\",\"No public profile data cached yet.\":\"Żadne dane profilu publicznego nie zostały jeszcze zapisane w pamięci podręcznej.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Liczą się tylko widoczne, aktywnie grające karty. Podglądy po najechaniu myszką nie liczą się.\",\"Statistics imported\":\"Statystyki zaimportowane\",\"Statistics deleted\":\"Statystyki usunięte\",\"Statistics exported\":\"Statystyki wyeksportowane\",\"Local observation only\":\"Tylko obserwacja lokalna\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Nie są gromadzone żadne prywatne wiadomości, tokeny, płatności, hasła ani zewnętrzne śledzenie.\",\"Not enough observations for a time estimate\":\"Za mało obserwacji, aby oszacować czas\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Najedź kursorem na dowolny przycisk/kontrolkę, aby zobaczyć wskazówkę. Typowe skróty można edytować w panelu skrótów.\\n\\nWartości domyślne:\\n/ Skoncentruj się na wprowadzaniu nazwy użytkownika\\nr Odśwież wszystko\\ng Widok siatki\\nf Widok ostrości\\nWłączanie/wyłączanie trybu czyszczenia Alt+P lub Alt+C\\nAlt+Shift+C Centrum nagrań\\nAlt+Shift+R Nagraj bieżącą stronę\\nEsc Wyjdź z trybu czyszczenia / pełnego ekranu\\nSpacja Wstrzymaj/wznów ekran główny w widoku Fokus\\n←/→ lub [/ ] Przełącz ekran główny w widoku Fokus\\nKliknij dwukrotnie kartę Pełny ekran\",\"Visible windows paused\":\"Widoczne okna zostały wstrzymane\",\"Visible windows resumed\":\"Widoczne okna wznowione\",\"Import config\":\"Importuj konfigurację\",\"Repair saved data\":\"Napraw zapisane dane\",\"Saved data has been normalized.\":\"Zapisane dane zostały znormalizowane.\",\"Clear all data\":\"Wyczyść wszystkie dane\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Spowoduje to usunięcie WSZYSTKICH pokoi, grup i ustawień. Kontynuować?\",\"English\":\"Angielski\",\"Deutsch\":\"Niemiecki\",\"Author\":\"Autor\",\"License\":\"Licencja\",\"If this script saves you time…\":\"Jeśli ten skrypt pozwoli Ci zaoszczędzić czas…\",\"Tip in ETH\":\"Wskazówka w ETH\",\"ETH address\":\"Adres ETH\",\"Copy\":\"Kopiuj\",\"Accent gradient\":\"Gradient akcentu\",\"Active filters\":\"Aktywne filtry\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Dodaj własne obrazy lub użyj przycisku zrzutu ekranu w odtwarzaczu. Zrzuty ekranu mogą automatycznie trafiać do galerii i mogą być użyte bezpośrednio jako okładka modelu.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Wszystkie filtry strony głównej, ustawienia układu, podglądu i paginacji są przechowywane w stabilnym skarbcu ustawień i przetrwają aktualizacje skryptu.\",\"All live models\":\"Wszystkie żywe modele\",\"All suggestions\":\"Wszystkie sugestie\",\"Almost silent\":\"Prawie cicho\",\"Always active\":\"Zawsze aktywny\",\"AND\":\"ORAZ\",\"Automatic example\":\"Automatyczny przykład\",\"Automatic segments\":\"Segmenty automatyczne\",\"Automatically loading the complete catalog…\":\"Automatyczne ładowanie całego katalogu…\",\"Available recording data was saved.\":\"Dostępne dane nagrywania zostały zapisane.\",\"Back online\":\"Powrót do Internetu\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Na podstawie zapisanych lokalnie ulubionych, tagów i aktywności związanej z przeglądaniem. Każdy wybór pokazuje swój powód.\",\"Black\":\"Czarny\",\"Live stream audio activity\":\"Aktywność audio w transmisji na żywo\",\"Local watch\":\"Lokalny zegarek\",\"Logo background\":\"Tło logo\",\"Logo height\":\"Wysokość logo\",\"Logo width\":\"Szerokość logo\",\"Loud\":\"Głośno\",\"Measured even while output is muted\":\"Mierzone nawet przy wyciszonym wyjściu\",\"Middle-click or Ctrl+click opens a new tab\":\"Kliknięcie środkowym przyciskiem lub kliknięcie klawiszem Ctrl i kliknięcie otwiera nową kartę\",\"Models you have visited locally and that are public again now.\":\"Modele, które odwiedziłeś lokalnie i które są teraz ponownie publiczne.\",\"Mono\":\"Mono\",\"New discoveries\":\"Nowe odkrycia\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nowy w katalogu, nowo oglądany przez Camclient lub dopiero rozpoczynający transmisję strumieniową.\",\"No audio track\":\"Brak ścieżki dźwiękowej\",\"No filters active · all matching live models are shown\":\"Brak aktywnych filtrów · pokazane są wszystkie pasujące modele na żywo\",\"Discover something new\":\"Odkryj coś nowego\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Wyraźne ścieżki rekomendacji pomogą Ci znaleźć coś nowego zamiast powtarzać katalog na stronie głównej.\",\"Do not enlarge\":\"Nie powiększaj\",\"Enable meter\":\"Włącz miernik\",\"Fast starters\":\"Szybkie startery\",\"Fill and crop\":\"Wypełnij i przytnij\",\"Filter suggestions\":\"Filtruj sugestie\",\"Fit completely\":\"Dopasuj całkowicie\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Aby uzyskać przezroczystość, użyj PNG, WebP lub bezpiecznego SVG i wybierz Przezroczysty jako tło logo. JPEG nie ma przezroczystości. Rozciągnięcie niezależnie wykorzystuje wybraną szerokość i wysokość.\",\"For you\":\"Dla ciebie\",\"gallery\":\"galeria\",\"HD\":\"HD\",\"Inner spacing\":\"Rozstaw wewnętrzny\",\"Inspiration, not repetition\":\"Inspiracja, a nie powtarzanie\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Umożliwia używanie, modyfikowanie i rozpowszechnianie oprogramowania. Informacje o prawach autorskich i licencjach muszą pozostać, a oprogramowanie jest dostarczane bez gwarancji. Camclient jest autorstwa Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Żyję krótko i już przyciągam aktywność publiczną.\",\"Brave exposed no recordable video track.\":\"Brave nie ujawnił ścieżki wideo, którą można nagrać.\",\"Brave or the video source blocked frame capture.\":\"Brave lub źródło wideo zablokowało przechwytywanie klatek.\",\"Browser exposed no audio track\":\"Przeglądarka nie ujawniła żadnej ścieżki dźwiękowej\",\"Camclient\":\"Klient Cam\",\"Camclient as the real homepage\":\"Camclient jako prawdziwa strona główna\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient automatycznie przypisuje te przejrzyste przykłady do popularnych tagów. Użyj przycisku obrazu na dowolnym tagu, aby wybrać przykład, przesłać własny plik PNG/WebP/SVG lub ukryć obraz.\",\"Choose a Camclient example or upload your own transparent image.\":\"Wybierz przykładowy klient Camclient lub prześlij własny przezroczysty obraz.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Wybierz plik PNG, JPEG, WebP lub bezpieczny plik SVG mniejszy niż 1,1 MB.\",\"Choose artwork for tag\":\"Wybierz grafikę dla tagu\",\"Click to enable meter\":\"Kliknij, aby włączyć licznik\",\"Close more actions\":\"Zamknij więcej działań\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Kolory, typografia, odstępy, narożniki, cienie i tła mają natychmiastowe zastosowanie w Ustawieniach, obszarze roboczym, Odkryj i bibliotece modeli.\",\"Continue loading the complete catalog\":\"Kontynuuj ładowanie całego katalogu\",\"Continuing recording\":\"Kontynuacja nagrywania\",\"current matches\":\"aktualne mecze\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Dostosuj nazwę, znak, slogan i lokalne logo Camclient. Logo pozostaje w tej przeglądarce.\",\"Menu\":\"Menu\",\"Monogram\":\"Monogram\",\"No recommendations match the active filters.\":\"Żadne rekomendacje nie pasują do aktywnych filtrów.\",\"No recording data\":\"Brak danych nagrywania\",\"NOT\":\"NIE\",\"Only new\":\"Tylko nowy\",\"Open Camclient homepage\":\"Otwórz stronę główną Camclient\",\"OR\":\"LUB\",\"part\":\"część\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Osobiste typy, nowe odkrycia, popularne pokoje na żywo i modele, których nigdy nie odwiedziłeś — wyraźnie oddzielone od pełnego katalogu na stronie głównej.\",\"Place\":\"Miejsce\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP i bezpieczny SVG obsługują przezroczystość. Rozmiar obrazów jest zmieniany lokalnie do 144 × 144 pikseli i zapisywany z ustawieniami użytkownika.\",\"Popular now\":\"Popularne teraz\",\"Public live models with no local watch time in Camclient.\":\"Publiczne modele na żywo bez lokalnego czasu oglądania w Camclient.\",\"Quiet\":\"Cicho\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Ranking według aktualnej liczby widzów; dane katalogowe nie są przechowywane trwale.\",\"Recording is still active.\":\"Nagrywanie jest nadal aktywne.\",\"Recording quality\":\"Jakość nagrywania\",\"The image could not be read.\":\"Nie można odczytać obrazu.\",\"The image format is unsafe or unsupported.\":\"Format obrazu jest niebezpieczny lub nieobsługiwany.\",\"The lanes respect your active search and filter settings.\":\"Pasy uwzględniają Twoje aktywne ustawienia wyszukiwania i filtrów.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"Nagranie jest zapisywane tylko na tym urządzeniu. Kontynuuj tylko wtedy, gdy masz pozwolenie na zapisanie tej zawartości.\",\"The video has no usable frame yet.\":\"Film nie ma jeszcze użytecznej ramki.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Nie ma jeszcze wystarczających danych o preferencjach lokalnych, dlatego są to przejrzyste typy początkowe oparte na aktualnej popularności.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Te ustawienia kontrolują układ strony głównej Camclient, paginację, informacje o modelu, filtry i wydajność.\",\"Transparent\":\"Przezroczysty\",\"Unified main navigation\":\"Ujednolicona nawigacja główna\",\"Unvisited\":\"Nieodwiedzone\",\"Upload own image\":\"Prześlij własny obraz\",\"Use as model cover\":\"Użyj jako okładki modelu\",\"Use the complete search, filters, and sorting for every current result.\":\"Skorzystaj z pełnego wyszukiwania, filtrów i sortowania dla każdego bieżącego wyniku.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Odwiedzenie strony głównej Chaturbate zawsze powoduje otwarcie pełnej strony głównej Camclient. Każdy główny obszar pozostaje w tym samym niestandardowym interfejsie.\",\"Waiting for audio\":\"Czekam na dźwięk\",\"What does the MIT license mean?\":\"Co oznacza licencja MIT?\",\"Recording stopped\":\"Nagrywanie zatrzymane\",\"Recording unavailable\":\"Nagranie niedostępne\",\"Remix\":\"Remiks\",\"Reset all\":\"Zresetuj wszystko\",\"RoomGrid Model library\":\"Biblioteka modeli RoomGrid\",\"RoomGrid My user\":\"RoomGrid Mój użytkownik\",\"RoomGrid Statistics\":\"Statystyki RoomGrid\",\"Save to model gallery\":\"Zapisz w galerii modeli\",\"Saved segment\":\"Zapisany segment\",\"Screenshot\":\"Zrzut ekranu\",\"Screenshot failed\":\"Zrzut ekranu nie powiódł się\",\"Screenshot format\":\"Format zrzutu ekranu\",\"Screenshot resolution\":\"Rozdzielczość zrzutu ekranu\",\"Screenshot unavailable\":\"Zrzut ekranu niedostępny\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Zrzuty ekranu są pobierane i mogą również wejść do lokalnej galerii modeli.\",\"Region\":\"Region\",\"segment(s)\":\"segment(y)\",\"Show no image\":\"Nie pokazuj żadnego obrazu\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Pokazuje aktywność na ścieżce audio strumienia niezależnie od tego, czy wyjście odtwarzacza jest wyciszone.\",\"Source ended\":\"Źródło się skończyło\",\"Source resolution\":\"Rozdzielczość źródła\",\"Start recording\":\"Rozpocznij nagrywanie\",\"Starter pick · popular now\":\"Wybór startowy · teraz popularne\",\"Status\":\"Status\",\"Stop and save\":\"Zatrzymaj się i zapisz\",\"Stretch\":\"Rozciągnij\",\"Tag artwork and emoji\":\"Oznacz grafikę i emoji\",\"Tag artwork editor\":\"Edytor grafiki tagów\",\"The compressed image is still too large.\":\"Skompresowany obraz jest nadal za duży.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"Nagłówek pokazuje tylko Strona główna, Odkryj, Eksplorator tagów, Centrum statystyk, Biblioteka modeli, Mój użytkownik i Ustawienia. Każdy obszar konfiguracji dostępny jest w formie podstrony Ustawienia.\",\"The image could not be decoded.\":\"Nie udało się rozszyfrować obrazu.\",\"White\":\"Biały\",\"Your discovery mix\":\"Twoja mieszanka odkryć\",\"中文\":\"中文\",\"Sepia\":\"Sepia\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Odkrywaj publiczne transmisje na żywo\",\"all groups\":\"wszystkie grupy\",\"at least one group\":\"co najmniej jedna grupa\",\"no measurements yet\":\"brak pomiarów\",\"Country, region or location\":\"Kraj, region lub lokalizacja\",\"Control model order, filters, page size, card size and visible information directly on the original Chaturbate homepage. Every change applies immediately and is saved in this browser.\":\"Steruj kolejnością modeli, filtrami, liczbą na stronie, rozmiarem kart i widocznymi informacjami bezpośrednio na oryginalnej stronie głównej Chaturbate. Każda zmiana jest stosowana od razu i zapisywana w tej przeglądarce.\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] temu\",\"[[ROOMGRID_VALUE_0]] preview\":\"Podgląd [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"Profil @[[ROOMGRID_VALUE_0]] został zapisany\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Szczegóły modelu dla @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"Zaimportowane obserwowane modele: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"Rozpoznane elementy My Collection: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"Widoczne modele: [[ROOMGRID_VALUE_0]] z [[ROOMGRID_VALUE_1]] obecnie wczytanych.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Aktywny filtr grup: [[ROOMGRID_VALUE_0]] · pasuje [[ROOMGRID_VALUE_1]] z [[ROOMGRID_VALUE_2]] widocznych kart ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Okładka niestandardowa · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Okładka zapisana lokalnie · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Przykładowe obrazy i okładki (zapisano [[ROOMGRID_VALUE_0]])\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Trwale zapisane na tym urządzeniu · najnowszy [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Własny wizerunek · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Zapisano [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Pierwsza obserwacja: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Ostatnia obserwacja: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Ostatnio na żywo: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Wystąpienia: [[ROOMGRID_VALUE_0]]× · pierwsze [[ROOMGRID_VALUE_1]] · ostatnie [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Typowy czas: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Dane techniczne: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"Lokalne obserwacje na żywo: [[ROOMGRID_VALUE_0]], najwyżej jedna co 5 minut\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] wcześniejszego czasu oglądania jako ostrożny szacunek\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Usuń tag [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Zarządzaj własnymi grupami · przypisano do: [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Usunąć grupę „[[ROOMGRID_VALUE_0]]”? Modele pozostaną w bibliotece.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Grupa „[[ROOMGRID_VALUE_0]]” została usunięta\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"Grupy: [[ROOMGRID_VALUE_0]] · możliwy wybór wielokrotny\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"Zapisane tagi: [[ROOMGRID_VALUE_0]]\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Usuń filtr [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] pasuje\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtry aktywne\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Publiczne informacje profilowe można wczytać przez „Wszystkie szczegóły”.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Własna organizacja od: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Automatycznie skatalogowano od: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Ostatnia aktywność: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Wystąpienia: [[ROOMGRID_VALUE_0]]× · ostatnio zapisano [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Odkryto: [[ROOMGRID_VALUE_0]] · pomiary jakości: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Informacje profilowe: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] zarchiwizowane\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] przywrócony\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Strona [[ROOMGRID_VALUE_0]] z [[ROOMGRID_VALUE_1]] · modele: [[ROOMGRID_VALUE_2]]\",\"[[ROOMGRID_VALUE_0]] saved\":\"Zapisano: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] watched\":\"Obejrzano: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"Aktywne: [[ROOMGRID_VALUE_0]] · rośnie automatycznie\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"Modele [[ROOMGRID_VALUE_0]] · Sesje [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] ulubione\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] różne modele\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] podglądy\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Podgląd/szybki podgląd modeli [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] dni\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Seria: [[ROOMGRID_VALUE_0]] dni\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"Oglądane wielokrotnie: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] dni danych lokalnych\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"średnia ważona [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"Teraz na żywo: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"Rozpoznane modele: [[ROOMGRID_VALUE_0]]\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Nie udało się odczytać wszystkiego: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Najaktywniejszy dzień: [[ROOMGRID_VALUE_0]], zdarzenia: [[ROOMGRID_VALUE_1]]. Surowe zdarzenia są przechowywane przez [[ROOMGRID_VALUE_2]] dni; starsze wartości mogą pozostać jako sumy dzienne.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"Sesje: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"Zapisano lokalnie: [[ROOMGRID_VALUE_0]]\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Nie udało się wczytać danych lokalnych: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] jest online.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] jest offline.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] od pewnego czasu nie był(a) widziany(-a) online.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"Ulubieni online jednocześnie: [[ROOMGRID_VALUE_0]].\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Publiczny profil użytkownika [[ROOMGRID_VALUE_0]] zmienił się.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"Zaktualizowano modele [[ROOMGRID_VALUE_0]]\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Podgląd importu statystyk (jeszcze bez zmian):\\nModele: [[ROOMGRID_VALUE_0]]\\nZdarzenia: [[ROOMGRID_VALUE_1]]\\nProfile publiczne: [[ROOMGRID_VALUE_2]]\\n\\nKontynuacja zastąpi istniejące statystyki.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Podgląd na żywo: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"Dodano [[ROOMGRID_VALUE_0]] do MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Usunąć [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Zaimportować widoki ([[ROOMGRID_VALUE_0]]) i kanały ([[ROOMGRID_VALUE_1]])?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"Pokoje dodane do obszaru roboczego: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] models saved\":\"Zapisane modele: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matching results\":\"Pasujące wyniki: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] pasujące modele na żywo\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Wspólna cecha nr [[ROOMGRID_VALUE_0]] z ulubionym\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"Dodano obrazy [[ROOMGRID_VALUE_0]]; Nie można przetworzyć [[ROOMGRID_VALUE_1]].\",\"[[ROOMGRID_VALUE_0]] images added.\":\"Dodano obrazy [[ROOMGRID_VALUE_0]].\",\"[[ROOMGRID_VALUE_0]] rooms\":\"Pokoje: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"Prawidłowe nazwy użytkownika: [[ROOMGRID_VALUE_0]]\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Strona [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], pokoje: [[ROOMGRID_VALUE_2]]\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"Tymczasowe adresy URL: dodano [[ROOMGRID_VALUE_0]], niepowodzenia [[ROOMGRID_VALUE_1]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Podgląd importu statystyk: modele [[ROOMGRID_VALUE_0]] i zdarzenia [[ROOMGRID_VALUE_1]]. Kontynuacja bezpiecznie scala dane i najpierw tworzy kopię zapasową.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Układy niestandardowe obsługują do [[ROOMGRID_VALUE_0]] pokoi.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Scena „[[ROOMGRID_VALUE_0]]” została zapisana\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Załadowano scenę „[[ROOMGRID_VALUE_0]]”.\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] aktywne strumienie · [[ROOMGRID_VALUE_1]] niestabilny · [[ROOMGRID_VALUE_2]] do oglądania\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] strumienie · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p przypisane\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Usunąć scenę „[[ROOMGRID_VALUE_0]]”?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Zapisano tylko dla [[ROOMGRID_VALUE_0]]: do [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Ta kamera wykorzystuje jakość globalną/adaptacyjną[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] zastąpiono przez [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Odtwarzanie dźwięku tylko z [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Umieszczono w polu Focus [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Umieszczono w oknie siatki [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Rozdzielczość [[ROOMGRID_VALUE_0]], stan [[ROOMGRID_VALUE_1]], bufor [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Kondycja strumienia [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"Modele: [[ROOMGRID_VALUE_0]]\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Usuń filtr: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Pasuje do Twoich zainteresowań: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Pobrano katalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modele na żywo\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modele lokalne\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Katalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modele na żywo\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"Załadowano [[ROOMGRID_VALUE_0]] pomieszczenia katalogu\",\"Detected via: [[ROOMGRID_VALUE_0]]\":\"Wykryto przez: [[ROOMGRID_VALUE_0]]\",\"Sources: [[ROOMGRID_VALUE_0]]\":\"Źródła: [[ROOMGRID_VALUE_0]]\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing replaces the current statistics.\":\"Podgląd importu: modele — [[ROOMGRID_VALUE_0]], zdarzenia — [[ROOMGRID_VALUE_1]]. Kontynuacja zastąpi bieżące statystyki.\",\"[[ROOMGRID_VALUE_0]] ms startup · [[ROOMGRID_VALUE_1]] stalls · [[ROOMGRID_VALUE_2]]% dropped frames\":\"start [[ROOMGRID_VALUE_0]] ms · zatrzymania: [[ROOMGRID_VALUE_1]] · utracone klatki: [[ROOMGRID_VALUE_2]]%\"},\"visible\":{\"‹ Zurück\":\"‹ Powrót\",\"● 1.2k · #tag\":\"● 1,2 tys. · #tag\",\"1 Jahr\":\"1 rok\",\"30 Tage\":\"30 dni\",\"7 Tage\":\"7 dni\",\"90 Tage\":\"90 dni\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AAA ✓\",\"Ähnliche Models anzeigen\":\"Pokaż podobne modele\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Wszystkie zmiany są zapisywane lokalnie i dotyczą także następnego modelu.\",\"Alle eigenen Tags\":\"Wszystkie niestandardowe tagi\",\"Alle Gruppen\":\"Wszystkie grupy\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Poproś o starsze dane Chaturbate\",\"Automatisch abspielen\":\"Autoodtwarzanie\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Automatycznie przechwycone · w pełni uporządkowane podczas edycji\",\"Automatisch neu verbinden\":\"Automatycznie połącz ponownie\",\"Bei verstecktem Tab pausieren\":\"Wstrzymaj, gdy karta jest ukryta\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Zastosuj istniejące dane konta Chaturbate\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Optymalizacja obrazu · duży zoom, pozycja, filtr i obrót\",\"Bildanpassung\":\"Regulacja obrazu\",\"Bildwerkzeuge einklappen\":\"Zwiń narzędzia obrazu\",\"Breite des Informationsbereichs\":\"Szerokość obszaru informacyjnego\",\"Broadcaster-Verbindung verwalten\":\"Zarządzaj połączeniem nadawcy\",\"Chat anzeigen\":\"Pokaż czat\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Oznacza to, że od chwili zainstalowania skryptu nie jesteś ograniczony danymi: RoomGrid może odczytać istniejącą listę poniżej i wyraźnie rozpoznawalne wpisy z Mojej kolekcji. Żadne wiadomości, płatności, multimedia ani hasła nie są kopiowane.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio zawiera także porównania okresów, mapy cieplne, odkrywanie ścieżek, analizy przestrzeni roboczej i jakości, a także eksport danych.\",\"Datenschutz & Export\":\"Ochrona danych i eksport\",\"Deine Aktivität der letzten 14 Tage\":\"Twoja aktywność w ciągu ostatnich 14 dni\",\"Deine meistgesehenen Models\":\"Twoje najczęściej oglądane modele\",\"Gruppe löschen\":\"Usuń grupę\",\"Gruppen umbenennen oder löschen\":\"Zmień nazwę lub usuń grupy\",\"HD + Gaming\":\"Granie w HD+\",\"IndexedDB\":\"Indeksowana baza danych\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Każdy zapisany tag jest automatycznie dostępny w wyszukiwaniu, przy autouzupełnianiu i tutaj jako bezpośredni filtr wielokrotny.\",\"Keine Models passen zu diesen Filtern.\":\"Żaden model nie pasuje do tych filtrów.\",\"Keine Priorität\":\"Nie jest to priorytet\",\"Konto-Daten werden eingelesen …\":\"Dane konta są odczytywane w...\",\"Lautstärke pro Model merken\":\"Zanotuj głośność według modelu\",\"Layout & sichtbare Bereiche\":\"Układ i widoczne obszary\",\"Letzte Messung\":\"Ostatni pomiar\",\"LIVE\":\"NA ŻYWO\",\"lokal · für Suche und Filter\":\"lokalny · do wyszukiwania i filtrów\",\"Maximale Stream-Lautstärke\":\"Maksymalna głośność strumienia\",\"Model besitzt alle gewählten Tags\":\"Model jest właścicielem wszystkich wybranych tagów\",\"Model besitzt mindestens einen gewählten Tag\":\"Model ma co najmniej jeden wybrany tag\",\"Model ist in allen gewählten Gruppen\":\"Model znajduje się we wszystkich wybranych grupach\",\"Model ist in mindestens einer gewählten Gruppe\":\"Model znajduje się w co najmniej jednej wybranej grupie\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Otwórz Moją kolekcję\",\"Namen speichern\":\"Zapisz nazwę\",\"Nicht verbunden\":\"Nie podłączony\",\"Niedrige Live-Latenz\":\"Niskie opóźnienie na żywo\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Nadal nie ma wystarczających lokalnych wartości użytkowych.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Nie mamy jeszcze własnej grupy. Wpisz nazwę powyżej i stwórz swoją pierwszą kolekcję.\",\"Noch keine Model-Sehzeit erfasst.\":\"Nie zarejestrowano jeszcze czasu oglądania modelu.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Nie ma jeszcze oficjalnych migawek kont.\",\"Noch nicht eingelesen\":\"Jeszcze nie przeczytane\",\"Nur auf diesem Gerät\":\"Tylko na tym urządzeniu\",\"Öffentliche/erkannte Tags\":\"Tagi publiczne/rozpoznane\",\"Öffentliches Profil anzeigen\":\"Wyświetl profil publiczny\",\"Offizielle Statistik verbinden\":\"Połącz oficjalne statystyki\",\"Optionale offizielle Kontostatistik\":\"Opcjonalne oficjalne statystyki konta\",\"Persönlich organisiert\":\"Osobiście zorganizowane\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Gracz i połączenie\",\"Player neu verbinden\":\"Podłącz ponownie odtwarzacz\",\"Player-Ecken\":\"Kąciki graczy\",\"Player-Hintergrund\":\"Tło gracza\",\"Player-Höhe\":\"Wzrost gracza\",\"Player-Seitenverhältnis\":\"Proporcje gracza\",\"Position der Informationen\":\"Lokalizacja informacji\",\"Private Notiz\":\"Prywatna notatka\",\"Private Notizen anzeigen\":\"Wyświetl prywatne notatki\",\"Puffergröße\":\"Rozmiar bufora\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid nie może odgadnąć wartości osobistego konta Chaturbate. Karty te pojawiają się tylko wtedy, gdy świadomie powiązałeś oficjalne statystyki nadawcy z własną nazwą użytkownika i tokenem.\",\"RoomGrid Suite\":\"Apartament RoomGrid\",\"Seitengröße\":\"Rozmiar strony\",\"SingleCam\":\"Pojedyncza kamera\",\"SingleCam einstellen\":\"Skonfiguruj SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Przywróć ustawienia domyślne SingleCam\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Gdy tylko zapiszesz własny tag dla modelu, pojawi się on tutaj oraz w autouzupełnianiu.\",\"Status im Player anzeigen\":\"Pokaż status w odtwarzaczu\",\"Status-Aktualisierung\":\"Aktualizacja statusu\",\"Stream immer stumm starten\":\"Zawsze rozpoczynaj transmisję po cichu\",\"Tag hinzufügen\":\"Dodaj tag\",\"Tipps letzte Stunde\":\"Wskazówki z ostatniej godziny\",\"Token-Guthaben\":\"Bilans tokenów\",\"Ton & Ruhe\":\"Dźwięk i spokój\",\"Ungefähre Onlinezeiten\":\"Przybliżone czasy online\",\"Verbunden\":\"Połączono\",\"Vollständige Modelstatistik\":\"Pełne statystyki modelu\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Wczytaj już teraz istniejące dane konta\",\"Weiter ›\":\"Dalej ›\",\"Zufriedenheit\":\"Satysfakcja\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"Bieżące połączenie jest aktywne; Po kolejnym pomiarze w tym miejscu pojawi się kurs lokalny.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Ten widok biblioteki jest nadal pusty.\",\"Eigene Priorität\":\"Własny priorytet\",\"Eigene Tags\":\"Własne tagi\",\"Eigene Tags filtern\":\"Filtruj własne tagi\",\"Eigenen Tag eingeben …\":\"Wprowadź swój własny dzień...\",\"Einstellungen\":\"Ustawienia\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter lub „Dodaj tag” przejmuje wersję roboczą. Jest on zapisywany na stałe tylko poprzez opcję „Zapisz profil”. Już używane tagi są sugerowane podczas pisania.\",\"Ereignisse im Zeitraum\":\"Wydarzenia w tym okresie\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Twórz kolekcje o dowolnych nazwach, przypisuj modele do wielu grup i wyświetlaj tutaj jedną lub więcej grup.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Najpierw utwórz własną grupę powyżej.\",\"Follower\":\"Obserwujący\",\"GAMING\":\"GRA\",\"Gesamter Verlauf\":\"Cały kurs\",\"Grid +\":\"Siatka+\",\"Größe der Bedienelemente\":\"Rozmiar elementów sterujących\",\"Gruppen\":\"Grupy\",\"Schließen\":\"Zamknij\",\"Startseite nach eigenen Gruppen filtern\":\"Filtruj stronę główną według własnych grup\",\"RoomGrid Komfortzentrum\":\"Centrum sterowania RoomGrid\",\"Mein Nutzer\":\"Mój profil\",\"Mein Nutzer-Dashboard\":\"Panel mojego profilu\",\"Modellbibliothek\":\"Biblioteka modeli\",\"Große Modellbibliothek\":\"Duża biblioteka modeli\",\"Statistikzentrum\":\"Centrum statystyk\",\"Sitzungen\":\"Sesje\",\"Dock + Hover\":\"Dokowanie + najechanie\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Lokalnie i prywatnie na tym urządzeniu\",\"Sortierung\":\"Sortuj według\",\"Meiste Sehzeit\":\"Najdłuższy czas oglądania\",\"Zuletzt aktiv\":\"Ostatnia aktywność\",\"Meiste Sitzungen\":\"Najwięcej sesji\",\"Meiste Zuschauer\":\"Najwięcej widzów\",\"Status\":\"Status\",\"Alle Status\":\"Wszystkie statusy\",\"Nur live\":\"Tylko na żywo\",\"Nur Live\":\"Tylko na żywo\",\"Offline\":\"Offline\",\"Unbekannt\":\"Nieznany\",\"Nur Bibliothek\":\"Tylko biblioteka\",\"Pro Seite\":\"Na stronę\"}}","ro":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Comutarea de limbă nu a putut fi pregătită în siguranță. Această pagină nu a fost lăsată.\",\"Language\":\"Limba\",\"Default workspace\":\"Spațiu de lucru implicit\",\"Workspace\":\"Spațiul de lucru\",\"New workspace\":\"Nou spațiu de lucru\",\"copy\":\"copie\",\"New group\":\"Grup nou\",\"Saved model\":\"Model salvat\",\"Search models, pages, workspaces and commands…\":\"Căutați modele, pagini, spații de lucru și comenzi...\",\"Type to filter · Enter to run · Esc to close\":\"Tastați pentru a filtra · Enter pentru a rula · Esc pentru a închide\",\"No matches\":\"Fără meciuri\",\"Currently active RoomGrid version\":\"Versiunea RoomGrid activă în prezent\",\"years\":\"ani\",\"Live\":\"În direct\",\"Private\":\"Privat\",\"Away\":\"Departe\",\"sessions\":\"sesiuni\",\"NEW\":\"NOU\",\"Name, subject, tag or place\":\"Nume, subiect, etichetă sau loc\",\"Website order\":\"Comanda pe site\",\"Most viewers\":\"Majoritatea telespectatorilor\",\"Fewest viewers\":\"Cei mai puțini telespectatori\",\"Longest online\":\"Cel mai lung online\",\"Recently online\":\"Recent online\",\"Age ascending\":\"Vârsta crescătoare\",\"Age descending\":\"Vârsta descrescătoare\",\"New models first\":\"Modele noi mai întâi\",\"Saved first\":\"Salvat mai întâi\",\"Favorites first\":\"În primul rând, preferatele\",\"Most watched\":\"Cele mai vizionate\",\"Recently watched\":\"Vizionat recent\",\"Random mix\":\"Mix aleatoriu\",\"All loaded\":\"Toate încărcate\",\"Previous page\":\"Pagina anterioară\",\"Next page\":\"Pagina următoare\",\"Reload complete live list\":\"Reîncărcați lista completă live\",\"All homepage options\":\"Toate opțiunile paginii de pornire\",\"Reset filters\":\"Resetați filtrele\",\"Quick filter\":\"Filtru rapid\",\"Sort\":\"Sortează\",\"Models per page\":\"Modele pe pagină\",\"Load more results\":\"Încărcați mai multe rezultate\",\"still to display\":\"încă de afișat\",\"continues automatically while scrolling\":\"continuă automat în timpul derulării\",\"visible\":\"vizibile\",\"loaded\":\"încărcat\",\"page\":\"pagina\",\"active filters\":\"filtre active\",\"loading complete live list\":\"se încarcă lista live completă\",\"all matching live models found\":\"S-au găsit toate modelele live care se potrivesc\",\"live list incomplete, retry\":\"lista live incompletă, reîncercați\",\"Requests temporarily paused\":\"Solicitările au fost temporar întrerupte\",\"Vault could not be unlocked\":\"Seiful nu a putut fi deblocat\",\"Preview Dock\":\"Previzualizare Dock\",\"Connecting…\":\"Se conectează…\",\"Stream temporarily unavailable\":\"Stream nu este disponibil temporar\",\"Error\":\"Eroare\",\"Buffering\":\"Se încarcă\",\"Preview unavailable\":\"Previzualizare indisponibilă\",\"Connect\":\"Conectați-vă\",\"Close\":\"Închide\",\"Opacity\":\"Opacitate\",\"Info\":\"Informații\",\"Age\":\"Vârsta\",\"Gender\":\"Gen\",\"Location\":\"Locație\",\"Country\":\"Țara\",\"Last broadcast\":\"Ultima transmisie\",\"Fan club\":\"Club de fani\",\"Private show\":\"Spectacol privat\",\"Spy show\":\"Spectacol de spionaj\",\"Model comparison list\":\"Lista de comparare a modelelor\",\"Compare\":\"Comparați\",\"Open model details\":\"Deschideți detaliile modelului\",\"Compare now\":\"Compara acum\",\"Clear\":\"Clar\",\"Up to four models can be compared at once.\":\"Pot fi comparate până la patru modele simultan.\",\"Remove from comparison\":\"Eliminați din comparație\",\"Add to comparison\":\"Adăugați la comparație\",\"Total watched\":\"Total vizionat\",\"Sessions\":\"Sesiuni\",\"Viewers\":\"Spectatori\",\"Rating\":\"Evaluare\",\"Online samples\":\"Mostre online\",\"Loading quality data…\":\"Se încarcă date de calitate...\",\"samples\":\"mostre\",\"stalls\":\"tarabe\",\"No quality measurements yet\":\"Nu există încă măsurători de calitate\",\"Quality data unavailable\":\"Date de calitate indisponibile\",\"Details\":\"Detalii\",\"Remove\":\"Eliminați\",\"Model comparison\":\"Comparație model\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Fiecare valoare provine din informațiile publice, din propria organizație sau din observațiile locale pe acest dispozitiv. Valorile lipsă nu sunt niciodată ghicite.\",\"Shared tags\":\"Etichete partajate\",\"Loading public facts…\":\"Se încarcă fapte publice...\",\"Unknown status\":\"Stare necunoscută\",\"Last activity\":\"Ultima activitate\",\"These statistics describe only your own local RoomGrid usage.\":\"Aceste statistici descriu doar propria dvs. utilizare locală a RoomGrid.\",\"At a glance\":\"Dintr-o privire\",\"Type\":\"Tip\",\"Appearances\":\"Apariții\",\"Public facts\":\"Fapte publice\",\"No public biography available yet.\":\"Nu există încă o biografie publică disponibilă.\",\"Public link\":\"Link public\",\"Public profile source\":\"Sursa de profil public\",\"Profile updated\":\"Profil actualizat\",\"Private note (local only)\":\"Notă privată (numai local)\",\"Your tags\":\"Etichetele tale\",\"No rating\":\"Nicio evaluare\",\"No priority\":\"Nicio prioritate\",\"Private library data\":\"Datele bibliotecii private\",\"Private notes\":\"Note private\",\"Priority\":\"Prioritate\",\"Favorite\":\"Favorit\",\"Watchlist\":\"Lista de urmărire\",\"Archived\":\"Arhivat\",\"Watch again\":\"Privește din nou\",\"Technical note\":\"Notă tehnică\",\"Technical\":\"Tehnic\",\"Save all changes\":\"Salvați toate modificările\",\"Loading local activity…\":\"Se încarcă activitatea locală...\",\"Activity and playback quality\":\"Activitatea și calitatea redării\",\"Sun\":\"soare\",\"Mon\":\"Lun\",\"Tue\":\"mar\",\"Wed\":\"mier\",\"Thu\":\"joi\",\"Fri\":\"vineri\",\"Sat\":\"sat\",\"Quality samples\":\"Mostre de calitate\",\"Average startup\":\"Pornire medie\",\"Stalls\":\"Tarabele\",\"Reconnects\":\"Reconectează\",\"Dropped frames\":\"Cadre scăpate\",\"Online observations\":\"Observații online\",\"Frequently observed online times\":\"Ori online observate frecvent\",\"Not enough online observations yet.\":\"Încă nu sunt suficiente observații online.\",\"Watched\":\"Privit\",\"Preview\":\"Previzualizare\",\"Quick view\":\"Vizualizare rapidă\",\"Discovered\":\"Descoperit\",\"Quality\":\"Calitate\",\"Connection\":\"Conexiune\",\"No local activity recorded yet.\":\"Nu s-a înregistrat încă nicio activitate locală.\",\"Recent events\":\"Evenimente recente\",\"Activity data unavailable.\":\"Datele despre activitate nu sunt disponibile.\",\"Similar models\":\"Modele similare\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Calculat din etichete locale/publice, țară și tip. Fiecare sugestie își arată motivul și nu sunt încărcate date.\",\"shared tags\":\"etichete partajate\",\"same country\":\"aceeași țară\",\"same type\":\"acelasi tip\",\"Not enough matching data yet.\":\"Nu sunt suficiente date de potrivire încă.\",\"Overview\":\"Prezentare generală\",\"Profile\":\"Profil\",\"Organize\":\"Organizați\",\"Activity\":\"Activitate\",\"Model detail sections\":\"Secțiuni de detalii model\",\"Previous model\":\"Modelul anterior\",\"Next model\":\"Următorul model\",\"Open room\":\"Cameră deschisă\",\"Pin preview\":\"Previzualizare fixare\",\"No public fields available\":\"Nu există câmpuri publice disponibile\",\"Public facts currently unavailable\":\"Fapte publice momentan indisponibile\",\"Session chat search\":\"Căutare prin chat de sesiune\",\"Search this session only…\":\"Căutați numai în această sesiune...\",\"New messages\":\"Mesaje noi\",\"Time for a short break.\":\"E timpul pentru o scurtă pauză.\",\"Your RoomGrid time budget for today has been reached.\":\"Bugetul dvs. de timp RoomGrid pentru astăzi a fost atins.\",\"RoomGrid action messages\":\"Mesaje de acțiune RoomGrid\",\"This change is saved automatically.\":\"Această modificare este salvată automat.\",\"Undo\":\"Anulați\",\"Change undone\":\"Modificare anulată\",\"Undo was not applied because newer changes already exist.\":\"Anularea nu a fost aplicată deoarece există deja modificări mai noi.\",\"Chaturbate live catalog\":\"Catalog live Chaturbate\",\"RoomGrid Discover\":\"RoomGrid Descoperiți\",\"Chaturbate Discover page\":\"Pagina Descoperire Chaturbate\",\"Following list\":\"Următoarea listă\",\"My Collection\":\"Colecția mea\",\"Chaturbate page\":\"Pagina Chaturbate\",\"Local record\":\"Record local\",\"Personally organized\":\"Organizat personal\",\"Local watch history\":\"Istoricul vizionărilor locale\",\"Public profile\":\"Profil public\",\"Local quality measurements\":\"Măsurătorile locale ale calității\",\"Local model index\":\"Indexul modelului local\",\"Current live signal\":\"Semnal curent actual\",\"Within the last hour\":\"În ultima oră\",\"Within the last 24 hours\":\"În ultimele 24 de ore\",\"Within the last 30 days\":\"În ultimele 30 de zile\",\"Historical record\":\"Record istoric\",\"Time unknown\":\"Timp necunoscut\",\"All\":\"Toate\",\"Favorites\":\"Favorite\",\"Library\":\"Biblioteca\",\"My Collection contained no reliably recognizable entries\":\"Colecția mea nu conținea intrări care să poată fi recunoscute în mod fiabil\",\"No group filter active · all models are shown.\":\"Niciun filtru de grup activ · toate modelele sunt afișate.\",\"Clear group filter\":\"Șterge filtrul de grup\",\"Filter homepage by groups\":\"Filtrați pagina de pornire după grupuri\",\"Close group menu\":\"Închide meniul de grup\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Alegeți unul sau mai multe dintre propriile grupuri. Alte modele de carduri sunt ascunse imediat pe pagina de pornire Chaturbate.\",\"Show all\":\"Arată tot\",\"Models in this group\":\"Modele din acest grup\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Nu există încă un grup personalizat. Creați unul în biblioteca de modele și atribuiți modele acolo.\",\"In any group\":\"În orice grup\",\"In all groups\":\"În toate grupurile\",\"Manage groups\":\"Gestionați grupuri\",\"Create group\":\"Creați grup\",\"Custom groups for the homepage\":\"Grupuri personalizate pentru pagina de pornire\",\"Main views\":\"Vederi principale\",\"MultiCam workstation\":\"Statie de lucru MultiCam\",\"Camera grid, audio and layouts\":\"Grila camerei, sunet și machete\",\"Discover\":\"Descoperă\",\"Full catalog, filters and sorting\":\"Catalog complet, filtre și sortare\",\"Personal dashboard\":\"Tabloul de bord personal\",\"Favorites, activity and shortcuts\":\"Favorite, activitate și comenzi rapide\",\"Tag explorer\":\"Explorator de etichete\",\"Combinations, trends and related tags\":\"Combinații, tendințe și etichete aferente\",\"Library and statistics\":\"Bibliotecă și statistică\",\"Statistics Center\":\"Centrul de statistici\",\"Model comparison and local statistics\":\"Comparație de modele și statistici locale\",\"Model library\":\"Biblioteca de modele\",\"Profiles, tags, groups and notes\":\"Profiluri, etichete, grupuri și note\",\"My user\":\"Profilul meu\",\"Preferences, usage and account data\":\"Preferințe, utilizare și datele contului\",\"Analytics Studio\":\"Studio de analiză\",\"Ranges, heatmaps and exports\":\"Intervale, hărți termice și exporturi\",\"Settings and maintenance\":\"Setări și întreținere\",\"Customize homepage\":\"Personalizați pagina de pornire\",\"Cards, filters and visible details\":\"Carduri, filtre și detalii vizibile\",\"Appearance\":\"Aspectul\",\"Colors, density and surfaces\":\"Culori, densitate și suprafețe\",\"Workspaces and groups\":\"Spații de lucru și grupuri\",\"Layouts, templates and assignments\":\"Aspecte, șabloane și sarcini\",\"Data and backup\":\"Date și backup\",\"Import, export and recovery\":\"Import, export și recuperare\",\"Windows and performance\":\"Windows și performanță\",\"Resource profiles and preview limits\":\"Profiluri de resurse și limite de previzualizare\",\"Privacy\":\"Confidențialitate\",\"History, private session and local data\":\"Istoric, sesiune privată și date locale\",\"Diagnostics\":\"Diagnosticare\",\"Self-check, routes and recovery\":\"Autoverificare, trasee și recuperare\",\"RoomGrid main menu\":\"meniul principal RoomGrid\",\"All pages, settings and tools in one place\":\"Toate paginile, setările și instrumentele într-un singur loc\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Meniul rămâne în fereastra vizibilă; toate datele RoomGrid rămân locale.\",\"RoomGrid pages\":\"Pagini RoomGrid\",\"Statistics\":\"Statistici\",\"Customize original homepage\":\"Personalizați pagina de pornire originală\",\"Homepage\":\"Pagina de pornire\",\"Filter homepage by your own groups\":\"Filtrați pagina de pornire după propriile grupuri\",\"Groups\":\"Grupuri\",\"Activity trend\":\"Tendință de activitate\",\"Link\":\"Legătură\",\"Public profile text and links\":\"Text de profil public și linkuri\",\"Last known public preview\":\"Ultima previzualizare publică cunoscută\",\"Current public preview\":\"Previzualizare publică curentă\",\"Offline may only provide a placeholder\":\"Offline poate oferi doar un substituent\",\"Example images and covers\":\"Exemple de imagini și coperți\",\"Reading local image archive…\":\"Se citește arhiva locală de imagini...\",\"Save new snapshot\":\"Salvați un nou instantaneu\",\"Saving public previews…\":\"Se salvează previzualizările publice...\",\"No saved image yet; current public previews are still shown.\":\"Nicio imagine salvată încă; previzualizările publice curente sunt încă afișate.\",\"Current public cover\":\"Acoperire publică curentă\",\"Current compact preview\":\"Previzualizare compactă curentă\",\"Current live image\":\"Imagine live actuală\",\"Room\":\"Cameră\",\"More\":\"Mai mult\",\"Unwatch\":\"Nu mai urmări\",\"Data sources and freshness\":\"Surse de date și prospețime\",\"Data source\":\"Sursa datelor\",\"Full local data set\":\"Set complet de date locale\",\"Matching filters\":\"Filtre potrivite\",\"Shown on this page\":\"Afișat pe această pagină\",\"Latest live signals\":\"Cele mai recente semnale live\",\"With watch history\":\"Cu istoricul vizionărilor\",\"Public-profile coverage\":\"Acoperire de profil public\",\"Country coverage\":\"Acoperire pe țară\",\"Age coverage\":\"Acoperire de vârstă\",\"Tag coverage\":\"Acoperirea etichetelor\",\"Which data set is being searched?\":\"Ce set de date este căutat?\",\"Last local observation\":\"Ultima observație locală\",\"No observation time yet\":\"Încă nu există timp de observație\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Public = din paginile actuale Chaturbate sau din catalogul live. Personal = salvat, etichetat sau grupat de dvs. Local = sesiuni, previzualizări sau valori de calitate observate numai pe acest dispozitiv. Câmpurile necunoscute rămân necunoscute și nu sunt niciodată ghicite.\",\"Local model statistics\":\"Statistica modelului local\",\"Automatically recorded · personally organized\":\"Înregistrat automat · organizat personal\",\"Automatically recorded in the growing model catalog\":\"Înregistrat automat în catalogul de modele în creștere\",\"Avg. session\":\"Mediu sesiune\",\"Active days\":\"Zile active\",\"Share\":\"Distribuie\",\"Longest session\":\"Cea mai lungă sesiune\",\"Previews\":\"Previzualizări\",\"Quick views\":\"Vizualizări rapide\",\"Data sources\":\"Surse de date\",\"Edit in library\":\"Editați în bibliotecă\",\"No experience data yet\":\"Nu există date despre experiență încă\",\"Online observations will be collected locally from now on.\":\"Observațiile online vor fi colectate local de acum înainte.\",\"Open\":\"Deschide\",\"Good trend\":\"Trend bun\",\"Moderate trend\":\"Tendință moderată\",\"Initial trend\":\"Tendință inițială\",\"First observation\":\"Prima observatie\",\"Estimated from watch time\":\"Estimată din timpul de vizionare\",\"weekday open\":\"deschis în zilele lucrătoare\",\"time open\":\"timp deschis\",\"Preview image\":\"Imagine de previzualizare\",\"Biography / room subject\":\"Biografie / subiect de cameră\",\"Catalog state\":\"Starea catalogului\",\"Live status\":\"Stare live\",\"Viewer count\":\"Numărul de spectatori\",\"Country/region\":\"Țara/regiune\",\"Total watch time\":\"Timp total de vizionare\",\"Average session\":\"Sesiune medie\",\"First seen\":\"Văzut pentru prima dată\",\"Last seen\":\"Văzut ultima dată\",\"Last live\":\"Ultimul live\",\"Approximate online times\":\"Timp aproximativ online\",\"Activity, sources and profile state\":\"Activitate, surse și starea profilului\",\"Example images\":\"Exemple de imagini\",\"Own groups\":\"Grupuri proprii\",\"Public/recognized tags\":\"Etichete publice/recunoscute\",\"Notes, own tags and priority\":\"Note, etichete proprii și prioritate\",\"Local rating\":\"Evaluare locală\",\"Group assignment\":\"Misiunea de grup\",\"Room and preview actions\":\"Acțiuni de cameră și previzualizare\",\"Library display\":\"Afișarea bibliotecii\",\"Saved in this browser\":\"Salvat în acest browser\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Alegeți dimensiunea cardului, modelele pe rând, dimensiunea paginii și exact ce detalii arată fiecare card.\",\"Compact overview\":\"Prezentare generală compactă\",\"Balanced\":\"Echilibrat\",\"All details\":\"Toate detaliile\",\"Show all details\":\"Afișați toate detaliile\",\"Core details only\":\"Doar detalii de bază\",\"Card zoom\":\"Zoom card\",\"Visible information per model\":\"Informații vizibile pentru fiecare model\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Cu coloane automate, zoom-ul cardului controlează câte modele se potrivesc unul lângă celălalt. Un număr exact de coloane îl înlocuiește. Toate opțiunile rămân salvate în acest browser.\",\"No custom tags yet\":\"Nu există încă etichete personalizate\",\"Name of the new group\":\"Numele noului grup\",\"Models remain in the library.\":\"Modelele rămân în bibliotecă.\",\"Custom groups\":\"Grupuri personalizate\",\"Create new group\":\"Creați un grup nou\",\"Nothing selected — show all\":\"Nimic selectat — arată-le pe toate\",\"Any\":\"Oricare\",\"Women\":\"Femei\",\"Men\":\"Bărbați\",\"Couples\":\"Cupluri\",\"Unknown\":\"Necunoscut\",\"Error/loading\":\"Eroare/încărcare\",\"Every weekday\":\"În fiecare zi a săptămânii\",\"Sunday\":\"duminica\",\"Monday\":\"luni\",\"Tuesday\":\"marți\",\"Wednesday\":\"miercuri\",\"Thursday\":\"Joi\",\"Friday\":\"vineri\",\"Saturday\":\"Sambata\",\"Every time\":\"De fiecare dată\",\"Advanced filters and sorting\":\"Filtre avansate și sortare\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combinați câte filtre doriți. Mai multe genuri, state, țări și etichete funcționează ca selecții multiple. Fiecare setare rămâne salvată în acest browser.\",\"Gender/type\":\"Gen/tip\",\"Countries/regions (multiple)\":\"Țări/regiuni (mai multe)\",\"Add country…\":\"Adăugați țara...\",\"Public tags (multiple)\":\"Etichete publice (mai multe)\",\"Add tag…\":\"Adăugați etichetă...\",\"Public-tag matching\":\"Potrivirea etichetelor publice\",\"At least one tag\":\"Cel puțin o etichetă\",\"Every selected tag\":\"Fiecare etichetă selectată\",\"Catalog source\":\"Sursa catalogului\",\"All models\":\"Toate modelele\",\"Personally organized only\":\"Doar organizat personal\",\"Automatically discovered only\":\"Descoperit automat numai\",\"Profile data\":\"Date de profil\",\"Loaded or missing\":\"Încărcat sau lipsă\",\"Loaded only\":\"Numai încărcat\",\"Missing only\":\"Lipsesc doar\",\"Picture/category\":\"Poza/categorie\",\"Neither HD nor gaming\":\"Nici HD, nici jocuri\",\"Watch history\":\"Vizionați istoricul\",\"Never watched\":\"N-am privit niciodată\",\"Own tags\":\"Etichete proprii\",\"With or without\":\"Cu sau fără\",\"With tags only\":\"Doar cu etichete\",\"Without tags only\":\"Doar fără etichete\",\"At least one group\":\"Cel puțin un grup\",\"No group\":\"Fără grup\",\"With notes only\":\"Doar cu note\",\"Without notes only\":\"Doar fără note\",\"Numeric ranges and minimums\":\"Intervalele numerice și minimele\",\"Minimum age\":\"Varsta minima\",\"Maximum age\":\"Varsta maxima\",\"Minimum viewers\":\"Telespectatori minim\",\"Maximum viewers\":\"Numărul maxim de spectatori\",\"Minimum followers\":\"Adepți minim\",\"Maximum followers\":\"Numărul maxim de urmăritori\",\"Minimum total watch time\":\"Timp total minim de vizionare\",\"Currently online for at least\":\"Momentan online de cel puțin\",\"Minimum sessions\":\"Sesiuni minime\",\"Minimum rating\":\"Rating minim\",\"Minimum priority\":\"Prioritate minima\",\"Active within\":\"Activ în interior\",\"days\":\"zile\",\"Minimum appearances\":\"Apariții minime\",\"Minimum live observations\":\"Observații în direct minime\",\"Filter by typical online time\":\"Filtrați după timpul obișnuit online\",\"Weekday\":\"Ziua săptămânii\",\"Hour (local time)\":\"Ora (ora locală)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Câmpurile zero sau goale înseamnă că nu există limită. Vârstele necunoscute sunt excluse în timp ce este activă o limită de vârstă. Filtrele de timp folosesc timpii în direct observați pe acest dispozitiv, revenind cu atenție pentru a viziona timpii atunci când este necesar.\",\"Reset all library filters\":\"Resetați toate filtrele bibliotecii\",\"Open live room\":\"Deschis camera live\",\"Open room page\":\"Deschide pagina camerei\",\"Status\":\"Stare\",\"Open model room in a new tab\":\"Deschideți camera model într-o filă nouă\",\"Open the Chaturbate single-model view in a new tab\":\"Deschideți vizualizarea Chaturbate cu un singur model într-o filă nouă\",\"Save profile\":\"Salvați profilul\",\"Restore\":\"Restaurați\",\"Archive\":\"Arhiva\",\"Advanced statistics filters\":\"Filtre avansate de statistici\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Fiecare filtru poate fi combinat; țările, etichetele, grupurile și tipurile acceptă selecția multiplă. Opțiunile sunt salvate permanent.\",\"Own groups (multiple)\":\"Grupuri proprii (mai multe)\",\"Add group…\":\"Adăugați grup...\",\"Tag matching\":\"Potrivirea etichetelor\",\"At least one\":\"Cel puțin unul\",\"All selected tags\":\"Toate etichetele selectate\",\"Group matching\":\"Potrivire de grup\",\"All selected groups\":\"Toate grupurile selectate\",\"Cover/example image\":\"Imagine de copertă/exemplu\",\"Known public image available\":\"Imagine publică cunoscută disponibilă\",\"No known image\":\"Nicio imagine cunoscută\",\"Currently live\":\"Momentan live\",\"Maximum total watch time\":\"Durata totală maximă de vizionare\",\"Maximum sessions\":\"Sesiuni maxime\",\"Minimum active days\":\"Minim zile active\",\"Minimum previews\":\"Previzualizări minime\",\"Minimum quick views\":\"Vizualizări rapide minime\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Câmpurile zero sau goale înseamnă că nu există limită. Valorile necunoscute nu sunt niciodată ghicite; intervalele de vârstă exclud vârstele necunoscute.\",\"Reset all statistics filters\":\"Resetați toate filtrele de statistici\",\"Search name, tags or notes…\":\"Căutați nume, etichete sau note...\",\"Search\":\"Caută\",\"Sorting\":\"Sortarea\",\"Most watch time\":\"Cea mai mare durată de vizionare\",\"Least watch time\":\"Cel mai mic timp de vizionare\",\"Recently active\":\"Activ recent\",\"Longest inactive\":\"Cel mai mult timp inactiv\",\"Most sessions\":\"Cele mai multe sesiuni\",\"Fewest sessions\":\"Cele mai puține sesiuni\",\"Longest average session\":\"Cea mai lungă sesiune medie\",\"Shortest average session\":\"Cea mai scurtă sesiune medie\",\"Longest single session\":\"Cea mai lungă sesiune singură\",\"Most active days\":\"Cele mai active zile\",\"Highest watch share\":\"Cea mai mare cotă de ceas\",\"Most previews\":\"Cele mai multe previzualizări\",\"Most quick views\":\"Cele mai rapide vizualizări\",\"Most discoveries\":\"Cele mai multe descoperiri\",\"Most followers\":\"Majoritatea adepților\",\"Youngest first\":\"Cel mai tânăr mai întâi\",\"Oldest first\":\"Cel mai vechi mai întâi\",\"Highest rating\":\"Cel mai mare rating\",\"Highest priority\":\"Cea mai mare prioritate\",\"Most appearances\":\"Cele mai multe apariții\",\"Most live observations\":\"Cele mai multe observații în direct\",\"First observed\":\"Prima observată\",\"Country A–Z\":\"Țara A–Z\",\"Most public tags\":\"Cele mai multe etichete publice\",\"Most groups\":\"Majoritatea grupurilor\",\"Best technical quality\":\"Cea mai bună calitate tehnică\",\"Cover/example images first\":\"Mai întâi copertă/exemplu de imagini\",\"All statuses\":\"Toate stările\",\"Live only\":\"Doar live\",\"Library only\":\"Doar bibliotecă\",\"View\":\"Vizualizare\",\"Active library\":\"Bibliotecă activă\",\"Live now\":\"Traieste acum\",\"With notes\":\"Cu note\",\"Newest saved/discovered\":\"Cel mai nou salvat/descoperit\",\"Oldest saved/discovered\":\"Cel mai vechi salvat/descoperit\",\"Lowest rating\":\"Evaluarea cea mai scăzută\",\"Lowest priority\":\"Prioritatea cea mai scăzută\",\"Live longest now\":\"Traieste cel mai mult acum\",\"Age youngest first\":\"Mai întâi vârsta cel mai mic\",\"Age oldest first\":\"Cel mai în vârstă mai întâi\",\"Most recently live\":\"Cel mai recent live\",\"First discovered\":\"Mai întâi descoperit\",\"Most own groups\":\"Majoritatea grupurilor proprii\",\"Card size\":\"Dimensiunea cardului\",\"Comfortable\":\"Confortabil\",\"Large\":\"Mare\",\"Cinema\":\"Cinematograf\",\"Extra large\":\"Foarte mare\",\"Models per row\":\"Modele pe rând\",\"Automatic for window\":\"Automat pentru fereastra\",\"Recorded models\":\"Modele înregistrate\",\"Total cam time\":\"Timp total al camerei\",\"across all models\":\"la toate modelele\",\"Dock + hover\":\"Panou + trecere cu mouse-ul\",\"Last 30 days\":\"Ultimele 30 de zile\",\"local events\":\"evenimente locale\",\"Top model\":\"Model de top\",\"no watch time yet\":\"încă nu există timp de vizionare\",\"Deeper analysis\":\"Analiză mai profundă\",\"Open Analytics Studio\":\"Deschideți Analytics Studio\",\"Late night\":\"Noaptea târziu\",\"Morning\":\"dimineata\",\"Daytime\":\"În timpul zilei\",\"Evening\":\"seara\",\"Gaming\":\"Jocuri\",\"Music & DJ\":\"Muzică și DJ\",\"Dance\":\"Dans\",\"Cosplay & anime\":\"Cosplay și anime\",\"Fitness & sports\":\"Fitness și sport\",\"Talk & social\":\"Discuție și socializare\",\"Outdoor & travel\":\"În aer liber și călătorii\",\"Roleplay\":\"Joc de rol\",\"Performance\":\"Performanță\",\"My interest statistics\":\"Statisticile mele de interes\",\"All local data\":\"Toate datele locale\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Calculat din timpul real de vizionare local, sesiuni, previzualizări, vizualizări rapide, favorite și informații despre modelul public cunoscute în prezent.\",\"Favorite tag\":\"Etichetă preferată\",\"not enough data\":\"nu sunt suficiente date\",\"Favorite genre\":\"Genul preferat\",\"from public tags\":\"din etichetele publice\",\"Most-watched age\":\"Vârsta cea mai urmărită\",\"no age data\":\"fara date de varsta\",\"Most-watched country\":\"Țara cea mai urmărită\",\"no country data\":\"fără date de țară\",\"Favorite model type\":\"Tipul de model preferat\",\"no type data\":\"fără date de tip\",\"Favorite viewing time\":\"Timp de vizionare preferat\",\"no time data\":\"fără date de timp\",\"Top-three concentration\":\"Primele trei concentrare\",\"watch time on the top three models\":\"timpul de vizionare pe primele trei modele\",\"Favorite affinity\":\"Afinitate preferată\",\"watch time on current favorites\":\"timpul de vizionare la favoritele curente\",\"watch time on HD models\":\"timpul de vizionare pe modelele HD\",\"Metadata coverage\":\"Acoperirea metadatelor\",\"age\":\"vârsta\",\"country\":\"tara\",\"Tags, genres and browsing interests\":\"Etichete, genuri și interese de navigare\",\"Tags by watch time\":\"Etichete în funcție de timpul de vizionare\",\"Your ranking appears when watched models have public tags.\":\"Clasamentul tău apare atunci când modelele urmărite au etichete publice.\",\"Favorite genre worlds\":\"Lumile genurilor preferate\",\"No genre world can yet be assigned from public tags.\":\"Nicio lume de gen nu poate fi încă atribuită din etichetele publice.\",\"Your own tags\":\"Etichetele tale proprii\",\"Your saved custom tags are also evaluated here by watch time.\":\"Etichetele dvs. personalizate salvate sunt, de asemenea, evaluate aici în funcție de durata de vizionare.\",\"Preview-interest tags\":\"Previzualizare-etichete de interes\",\"Browsing interests appear when you use live previews or quick views.\":\"Interesele de navigare apar atunci când utilizați previzualizări live sau vizualizări rapide.\",\"Most-previewed models\":\"Cele mai previzualizate modele\",\"No previews have been recorded in this period yet.\":\"Nu au fost înregistrate încă previzualizări în această perioadă.\",\"Age, origin and model type\":\"Vârstă, origine și tip de model\",\"Age groups by watch time\":\"Grupe de vârstă în funcție de timpul de vizionare\",\"Not enough public age data is available.\":\"Nu sunt disponibile suficiente date publice privind vârsta.\",\"Countries by watch time\":\"Țări după timpul de vizionare\",\"Not enough public country data is available.\":\"Nu sunt disponibile suficiente date publice despre țară.\",\"Most-watched places\":\"Cele mai vizionate locuri\",\"Not enough public location data is available.\":\"Nu sunt disponibile suficiente date privind locația publică.\",\"Model types by watch time\":\"Tipuri de modele în funcție de timpul de vizionare\",\"Not enough public model-type data is available.\":\"Nu sunt disponibile suficiente date publice de tip model.\",\"Viewing rhythm and favorite signals\":\"Ritmul de vizionare și semnalele de preferință\",\"Most active weekdays\":\"Cele mai active zile ale săptămânii\",\"Not enough visit events are available.\":\"Nu sunt disponibile suficiente evenimente de vizită.\",\"Times of day\":\"Momente ale zilei\",\"Tags among favorites\":\"Etichete printre favorite\",\"Preference signals appear when you mark models as favorites.\":\"Semnalele de preferință apar atunci când marcați modelele ca favorite.\",\"Countries among favorites\":\"Țările printre favorite\",\"No known countries among favorites yet.\":\"Încă nu se cunoaște țări printre favorite.\",\"Age groups among favorites\":\"Grupe de vârstă printre favorite\",\"No known ages among favorites yet.\":\"Nu se cunosc încă vârste printre favorite.\",\"Model types among favorites\":\"Tipurile de modele printre favorite\",\"No known model types among favorites yet.\":\"Încă nu sunt cunoscute tipuri de model printre favorite.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Aceste rezultate nu sunt trimise la un server. Doar timpul real de vizionare contează pentru preferințele de vizionare; previzualizările și favoritele sunt afișate separat. Datele lipsă de vârstă, țară, locație sau etichete nu sunt niciodată ghicite.\",\"Open Following on Chaturbate\":\"Deschide Urmărire pe Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Un istoric complet al duratei de vizionare sau al vizitelor nu poate fi preluat retroactiv, deoarece Chaturbate nu oferă un API pentru istoricul personal documentat. Puteți solicita o copie a datelor dvs. personale stocate prin exportul de date oficial al Chaturbate. Dacă exportul respectiv conține câmpuri de istoric utilizabile, RoomGrid poate accepta un import sigur după validarea formatului de fișier. API-ul oficial Stats furnizează valorile actuale ale radiodifuzorilor; RoomGrid își poate construi propria istorie numai de la prima conexiune încolo.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Comparați fiecare model observat, salvat și vizionat local, cu durata de vizionare, sesiuni, activitate, previzualizări, calitate și clasamente.\",\"Large model library\":\"Biblioteca mare de modele\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Modelele dvs. ca profiluri convenabile, bogate în imagini, cu stare live, note, etichete, evaluări, prioritate, grupuri și acțiuni directe.\",\"My user dashboard\":\"Tabloul meu de bord pentru utilizatori\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Istoricul dvs. privat de utilizare locală, obiceiurile, datele din bibliotecă și, numai atunci când sunt conectate în mod deliberat, datele contului oficial de radiodifuzor.\",\"🔒 Local & private on this device\":\"🔒 Local și privat pe acest dispozitiv\",\"Preparing data…\":\"Se pregătesc datele...\",\"RoomGrid Comfort Center\":\"Centrul de control RoomGrid\",\"inside the standard Chaturbate page\":\"în pagina Chaturbate standard\",\"All RoomGrid pages and settings\":\"Toate paginile și setările RoomGrid\",\"Media paused; names and images are covered.\":\"Media întreruptă; numele și imaginile sunt acoperite.\",\"Restore previous view\":\"Restabiliți vizualizarea anterioară\",\"Enter local PIN\":\"Introduceți codul PIN local\",\"In-page notifications\":\"Notificări în pagină\",\"No new notifications\":\"Fără notificări noi\",\"Notifications are off\":\"Notificările sunt dezactivate\",\"RoomGrid notification\":\"Notificare RoomGrid\",\"Dismiss as read\":\"Închideți ca citit\",\"More unread notifications\":\"Mai multe notificări necitite\",\"RoomGrid update\":\"Actualizare RoomGrid\",\"A monitored source changed status.\":\"O sursă monitorizată și-a schimbat starea.\",\"This is a local RoomGrid test notification.\":\"Aceasta este o notificare locală de testare RoomGrid.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Testare reușită: notificările pot apărea direct în bara RoomGrid.\",\"Settings\":\"Setări\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Spații de lucru, bibliotecă, design, date, istoric, notificări, confidențialitate și performanță\",\"Workspaces\":\"Spații de lucru\",\"Data\":\"Date\",\"History & statistics\":\"Istorie și statistici\",\"Broadcaster stats\":\"Statisticile radiodifuzorului\",\"Notifications\":\"Notificări\",\"Windows & performance\":\"Windows și performanță\",\"Optional tools\":\"Instrumente opționale\",\"Settings pages\":\"Pagini de setări\",\"RoomGrid control center\":\"Centru de control RoomGrid\",\"Any online model\":\"Orice model online\",\"Workspace name\":\"Numele spațiului de lucru\",\"Optional description\":\"Descriere opțională\",\"Active\":\"Activ\",\"No description\":\"Fără descriere\",\"Name\":\"Nume\",\"Description\":\"Descriere\",\"Replace offline models\":\"Înlocuiți modelele offline\",\"Replacement group\":\"Grup de înlocuire\",\"Delay seconds\":\"Întârziere secunde\",\"Save current state\":\"Salvați starea curentă\",\"New window\":\"Fereastra noua\",\"Export this workspace\":\"Exportați acest spațiu de lucru\",\"Duplicate\":\"Duplicat\",\"Workspace restored\":\"Spațiul de lucru a fost restaurat\",\"Workspace archived\":\"Spațiul de lucru arhivat\",\"Delete permanently\":\"Ștergeți definitiv\",\"Permanently delete this workspace?\":\"Ștergeți definitiv acest spațiu de lucru?\",\"Saved layout and panel sizes\":\"Aspect și dimensiunile panourilor salvate\",\"Apply\":\"Aplicați\",\"Layout template deleted\":\"Șablonul de aspect a fost șters\",\"Parent group\":\"Grupul de părinți\",\"None\":\"Niciuna\",\"Group description\":\"Descrierea grupului\",\"Save current workspace\":\"Salvați spațiul de lucru curent\",\"Save layout template\":\"Salvați șablonul de aspect\",\"Template name\":\"Numele șablonului\",\"Layout templates\":\"Șabloane de aspect\",\"Device layout\":\"Aspectul dispozitivului\",\"Groups and subgroups\":\"Grupuri și subgrupuri\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Fiecare spațiu de lucru stochează layout-uri desktop și compacte separate. Înlocuirea automată este dezactivată în mod implicit și umple spațiile offline numai după ce o activați pentru spațiul de lucru respectiv.\",\"No templates yet.\":\"Încă nu există șabloane.\",\"Group name\":\"Numele grupului\",\"New subgroup\":\"Subgrup nou\",\"Enter parent group name\":\"Introduceți numele grupului părinte\",\"Subgroup name\":\"Numele subgrupului\",\"Name, note or local tag\":\"Nume, notă sau etichetă locală\",\"Choose group…\":\"Alegeți grupul...\",\"Add selected models to group\":\"Adăugați modelele selectate la grup\",\"Watch later\":\"Urmăriți mai târziu\",\"Paused\":\"Întrerupt\",\"Collection name\":\"Numele colecției\",\"Tags, comma separated\":\"Etichete, separate prin virgulă\",\"Countries, comma separated\":\"Țări, separate prin virgulă\",\"All types\":\"Toate tipurile\",\"Rule name\":\"Numele regulii\",\"Required tags\":\"Etichete obligatorii\",\"Local alert\":\"Alertă locală\",\"Add to favorites\":\"Adăugați la favorite\",\"Add to group\":\"Adăugați la grup\",\"Any tag\":\"Orice etichetă\",\"Any country\":\"Orice tara\",\"Pause\":\"Pauză\",\"Enable\":\"Activați\",\"Delete\":\"Ștergeți\",\"Automation rule deleted\":\"Regula de automatizare a fost ștearsă\",\"Select model\":\"Selectați modelul\",\"Later\":\"mai târziu\",\"Never visited\":\"Niciodată vizitat\",\"Last online\":\"Ultima online\",\"Last visited\":\"Ultima vizitare\",\"Local note\":\"Notă locală\",\"Local tags, comma separated\":\"Etichete locale, separate prin virgulă\",\"Remove later\":\"Eliminați mai târziu\",\"Hide today\":\"Ascunde-te azi\",\"Pause 7 days\":\"Pauza de 7 zile\",\"Never suggest\":\"Nu sugerați niciodată\",\"Select results\":\"Selectați rezultatele\",\"Clear selection\":\"Ștergeți selecția\",\"selected\":\"selectat\",\"Add to watch later\":\"Adaugă pentru a viziona mai târziu\",\"Permanently delete selected models?\":\"Ștergeți definitiv modelele selectate?\",\"Smart collections\":\"Colecții inteligente\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Colecțiile se actualizează automat pe măsură ce metadatele și statisticile locale cresc; modelele nu sunt duplicate.\",\"Tags\":\"Etichete\",\"Countries\":\"Țări\",\"Favorites only\":\"Doar favorite\",\"Online only\":\"Numai online\",\"Save smart collection\":\"Salvați colecția inteligentă\",\"Delete current collection\":\"Ștergeți colecția curentă\",\"Delete this smart collection? Model data stays unchanged.\":\"Ștergeți această colecție inteligentă? Datele modelului rămân neschimbate.\",\"Rules & automation center\":\"Reguli și centru de automatizare\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Când datele modelului local se potrivesc, afișați o alertă, adăugați un favorit sau adăugați modelul la un grup ales.\",\"Action\":\"Acțiune\",\"Target group\":\"Grup țintă\",\"Save rule\":\"Salvați regula\",\"No automation rules yet.\":\"Încă nu există reguli de automatizare.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Numele de utilizator sunt deduplicate prin potrivire normalizată exactă. Căutarea acoperă nume, note și etichete locale. Arhivarea, eliminarea dintr-o vizualizare și ștergerea definitivă rămân acțiuni separate.\",\"No matching models.\":\"Nu se potrivesc modele.\",\"Models and notes\":\"Modele și note\",\"This backup could not be read. Current data was not changed.\":\"Această copie de rezervă nu a putut fi citită. Datele curente nu au fost modificate.\",\"Import preview (nothing changed yet)\":\"Previzualizare import (nu s-a schimbat încă)\",\"conflicts\":\"conflicte\",\"Includes sensitive statistics\":\"Include statistici sensibile\",\"models\":\"modele\",\"events\":\"evenimente\",\"quality samples\":\"mostre de calitate\",\"Import statistics too\":\"Importă și statistici\",\"Import method\":\"Metoda de import\",\"Merge (recommended)\":\"Îmbinare (recomandat)\",\"Replace selected sections\":\"Înlocuiți secțiunile selectate\",\"Apply import\":\"Aplicați importul\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Import finalizat. Configurația și statisticile anterioare au fost salvate separat.\",\"The import could not be saved safely. The backup remains available.\":\"Importul nu a putut fi salvat în siguranță. Backup-ul rămâne disponibil.\",\"Cancel preview\":\"Anulează previzualizarea\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Acest fișier conține date despre model, notă sau spațiu de lucru și ar trebui tratat ca fiind sensibil.\",\"This import is not marked sensitive.\":\"Acest import nu este marcat ca fiind sensibil.\",\"Data version\":\"Versiunea datelor\",\"Software version\":\"Versiune software\",\"Revision\":\"Revizuire\",\"Local models\":\"Modele locale\",\"Selective export\":\"Export selectiv\",\"Export selected\":\"Export selectat\",\"Full export\":\"Export complet\",\"Complete safety package\":\"Pachet complet de siguranță\",\"Export statistics separately\":\"Exportați statisticile separat\",\"Choose file and preview\":\"Alegeți fișierul și previzualizați\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Pachetul complet de siguranță conține statistici de configurare și sensibile, dar niciodată simbolul de statistici oficiale. Fiecare import este previzualizat și face backup separat pentru configurație și statistici înainte de aplicare.\",\"Storage and backup center\":\"Centru de stocare și backup\",\"Local storage\":\"Stocare locală\",\"Site storage\":\"Stocare pe site\",\"Available quota\":\"Cotă disponibilă\",\"Internal statistics backups\":\"Copiere de rezervă interne ale statisticilor\",\"Configuration\":\"Configurare\",\"workspaces\":\"spatii de lucru\",\"LocalStorage + update vault\":\"LocalStorage + seif de actualizare\",\"Statistics database\":\"Baza de date statistice\",\"Secrets\":\"Secrete\",\"The official statistics token is never included in normal or complete backups.\":\"Indicatorul de statistică oficial nu este niciodată inclus în backup-urile normale sau complete.\",\"Excluded\":\"Exclus\",\"Create configuration restore point\":\"Creați un punct de restaurare a configurației\",\"Create statistics restore point\":\"Creați un punct de restaurare a statisticilor\",\"Legacy cleanup\":\"Curățare moștenire\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Datele moștenite nu sunt niciodată șterse automat. Scoateți cheile vechi v6/v7 numai după o examinare cu succes.\",\"Remove old v6/v7 copies\":\"Eliminați vechile copii v6/v7\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Eliminați numai copii vechi v6/v7? Rămân datele actuale și backup-urile.\",\"items removed\":\"articolele eliminate\",\"Blue and teal, the RoomGrid signature\":\"Albastru și albastru, semnătura RoomGrid\",\"Warm orange and rose\":\"Portocală și trandafir cald\",\"Frost\":\"Îngheț\",\"Crisp cyan and violet\":\"Cyan și violet crocante\",\"Custom\":\"Personalizat\",\"Use the colors selected below\":\"Utilizați culorile selectate mai jos\",\"Name the current appearance\":\"Denumiți aspectul curent\",\"My appearance\":\"Aspectul meu\",\"Delete appearance profile\":\"Ștergeți profilul aspectului\",\"This appearance profile could not be imported.\":\"Acest profil de aspect nu a putut fi importat.\",\"Light\":\"Lumină\",\"Dark\":\"Întuneric\",\"Export\":\"Exportați\",\"Home\":\"Acasă\",\"Listings and tags\":\"Listări și etichete\",\"Model pages\":\"Pagini model\",\"Use global appearance\":\"Utilizați aspectul global\",\"This logo format is unsafe or unsupported.\":\"Acest format de logo este nesigur sau neacceptat.\",\"The logo file could not be read.\":\"Fișierul siglă nu a putut fi citit.\",\"MultiCam tools\":\"Instrumente MultiCam\",\"Page\":\"Pagina\",\"Panel\":\"Panoul\",\"Card surface\":\"Suprafața cardului\",\"Input\":\"Intrare\",\"Border\":\"Frontieră\",\"Muted text\":\"Text dezactivat\",\"Success\":\"Succes\",\"Warning\":\"Avertisment\",\"Danger\":\"Pericol\",\"Info and focus\":\"Informații și concentrare\",\"Data hub\":\"Hub de date\",\"Text / page\":\"Text / pagină\",\"Text / panel\":\"Text / panou\",\"Muted text / panel\":\"Text/panou dezactivat\",\"Accent text / accent\":\"Text de accent / accent\",\"Tablet\":\"Tabletă\",\"Mobile\":\"Mobil\",\"Brand and logo studio\":\"Studio de marcă și logo\",\"Monogram\":\"Monogramă\",\"Tagline\":\"Slogan\",\"Brand display\":\"Afișarea mărcii\",\"Full name\":\"Nume complet\",\"Uploaded logo\":\"Logo-ul încărcat\",\"Logo shape\":\"Forma logo-ului\",\"Rounded\":\"rotunjite\",\"Circle\":\"Cercul\",\"Square\":\"pătrat\",\"Pill\":\"pilula\",\"Page title\":\"Titlul paginii\",\"Website original\":\"Site original\",\"Brand plus original\":\"Marca plus original\",\"Custom title\":\"Titlu personalizat\",\"Custom page title\":\"Titlul paginii personalizat\",\"Image fit\":\"Se potrivește imaginea\",\"Horizontal position\":\"Poziție orizontală\",\"Vertical position\":\"Poziție verticală\",\"Upload light-mode logo\":\"Încărcați sigla pentru modul de lumină\",\"Upload dark-mode logo\":\"Încărcați sigla în modul întunecat\",\"Remove logos\":\"Eliminați siglele\",\"Show version\":\"Afișează versiunea\",\"Use logo as tab icon on RoomGrid pages\":\"Folosiți sigla ca pictogramă de filă pe paginile RoomGrid\",\"Replace website logo in the original header (local only)\":\"Înlocuiți sigla site-ului în antetul original (numai local)\",\"Complete color system\":\"Sistem complet de culoare\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Pagina de control, panou, card, intrare, chenar, text și culori de stare, pe lângă accente.\",\"Derived automatically\":\"Derivat automat\",\"Fully custom\":\"Complet personalizat\",\"Editing dark palette\":\"Editarea paletei întunecate\",\"Editing light palette\":\"Editarea paletei de lumini\",\"Reset current palette\":\"Resetați paleta curentă\",\"Layout editor\":\"Editor de aspect\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Setați spațierea conținutului, lățimea cardului, antetul și bara laterală în mod constant pe paginile RoomGrid.\",\"Minimum card width\":\"Lățimea minimă a cardului\",\"Section spacing\":\"Spațierea secțiunilor\",\"Content padding\":\"Umplutură de conținut\",\"Header height\":\"Înălțimea antetului\",\"Maximum columns\":\"Numărul maxim de coloane\",\"Automatic\":\"automată\",\"Card aspect\":\"Aspect card\",\"Sidebar position\":\"Poziția barei laterale\",\"Left\":\"Stânga\",\"Right\":\"Corect\",\"Keep header visible while scrolling\":\"Păstrați antetul vizibil în timp ce derulați\",\"Live device preview\":\"Previzualizare live a dispozitivului\",\"Contrast and accessibility\":\"Contrastul și accesibilitatea\",\"High-contrast mode\":\"Modul cu contrast ridicat\",\"Underline links\":\"Subliniați linkurile\",\"Reduce transparency\":\"Reduceți transparența\",\"Boost base text size\":\"Creșteți dimensiunea textului de bază\",\"Focus ring width\":\"Lățimea inelului de focalizare\",\"Minimum target size\":\"Dimensiunea minimă a țintei\",\"Respect system reduced-motion setting\":\"Respectați setarea sistemului de mișcare redusă\",\"Apply accessible defaults\":\"Aplicați setările implicite accesibile\",\"Profiles by page and screen size\":\"Profiluri după pagină și dimensiunea ecranului\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Salvați una sau mai multe apariții, apoi atribuiți-le pentru fiecare tip de pagină pentru desktop, tabletă și mobil. Profilurile se schimbă automat când dimensiunea ecranului se modifică.\",\"Enable responsive design profiles\":\"Activați profilurile de design receptiv\",\"Mobile maximum width\":\"Lățimea maximă mobilă\",\"Tablet maximum width\":\"Lățimea maximă a tabletei\",\"RoomGrid design system\":\"Sistem de proiectare RoomGrid\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Alegeți direcția mărcii, lățimea conținutului și densitatea informațiilor cardului. Fiecare pagină urmează imediat.\",\"Primary accent\":\"Accent primar\",\"Secondary accent\":\"Accent secundar\",\"Content width\":\"Lățimea conținutului\",\"Centered\":\"Centrat\",\"Wide\":\"Lat\",\"Full width\":\"Latimea intreaga\",\"Card style\":\"Stilul cardului\",\"Visual\":\"vizual\",\"Data-rich\":\"Bogat în date\",\"Widget density\":\"Densitatea widgetului\",\"Spacious\":\"Spațios\",\"Theme and surfaces\":\"Temă și suprafețe\",\"Surface style\":\"Stil de suprafață\",\"Soft\":\"Moale\",\"Flat\":\"plat\",\"Glass\":\"Sticlă\",\"Page background\":\"Fundalul paginii\",\"Ambient glow\":\"Strălucire ambientală\",\"Surface opacity\":\"Opacitatea suprafeței\",\"Custom accent color\":\"Culoare de accent personalizată\",\"Sizing and motion\":\"Dimensiunea și mișcarea\",\"Font style\":\"Stilul fontului\",\"System\":\"Sistem\",\"Classic serif\":\"Serif clasic\",\"Monospace\":\"Monospațiu\",\"Density\":\"Densitatea\",\"Font scale\":\"Scara fontului\",\"Control size\":\"Dimensiunea controlului\",\"Corner radius\":\"Raza colțului\",\"Card spacing\":\"Spațierea cardurilor\",\"Shadow strength\":\"Puterea umbrei\",\"Sidebar width\":\"Lățimea barei laterale\",\"Enable animations\":\"Activați animațiile\",\"Appearance profiles\":\"Profiluri de aspect\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Salvați aspectul complet ca profil reutilizabil. Profilurile rămân disponibile după actualizările scripturilor.\",\"Save current appearance\":\"Salvați aspectul curent\",\"Import profile\":\"Importați profilul\",\"No profiles saved yet.\":\"Niciun profil nu a fost salvat încă.\",\"Instant preview\":\"Previzualizare instantanee\",\"Example card\":\"Exemplu de card\",\"This is the combined result of the current visual settings.\":\"Acesta este rezultatul combinat al setărilor vizuale curente.\",\"Example action\":\"Exemplu de acțiune\",\"Per-page design profiles\":\"Profiluri de design pe pagină\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Alegeți un alt aspect salvat pentru Acasă, listări, pagini de model, MultiCam, Discover și hub de date.\",\"Enable per-page design\":\"Activați designul pe pagină\",\"Normal website pages\":\"Pagini de site obișnuite\",\"Enable RoomGrid page design\":\"Activați designul paginii RoomGrid\",\"Tag and category pages\":\"Pagini de etichete și categorii\",\"Cards\":\"Carduri\",\"List\":\"Lista\",\"Page density\":\"Densitatea paginii\",\"Polish navigation\":\"Navigație poloneză\",\"Polish sidebars\":\"Bare laterale poloneze\",\"Hide recognized promo and merch areas\":\"Ascundeți zonele de promovare și produse recunoscute\",\"Compact footer\":\"Subsol compact\",\"Mark saved models\":\"Marcați modelele salvate\",\"Mark favorites separately\":\"Marcați preferatele separat\",\"Mark visited models\":\"Mark a vizitat modele\",\"Show card quick actions\":\"Afișați acțiuni rapide pe card\",\"Animated live previews on home and listing pages\":\"Previzualizări live animate pe paginile de pornire și de listare\",\"Preview refresh rate\":\"Previzualizează rata de reîmprospătare\",\"Update mode\":\"Mod actualizare\",\"All visible cards simultaneously\":\"Toate cărțile vizibile simultan\",\"Performance-bounded batches\":\"Loturi limitate de performanță\",\"Batch concurrency limit\":\"Limită de concurență în lot\",\"All visible\":\"Toate vizibile\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Numai cardurile vizibile primesc actualizări ale cadrelor JPEG dezactivate; se întrerup imediat în afara ecranului sau într-o filă ascunsă. Modul sincronizat reîmprospătează fiecare card vizibil într-un singur val; ratele ridicate cresc considerabil lățimea de bandă și utilizarea procesorului.\",\"Live video on hover\":\"Video live la trecerea cursorului\",\"Play real-time video while hovering\":\"Redați videoclipuri în timp real în timp ce treceți cu mouse-ul\",\"Start delay\":\"Întârziere de pornire\",\"Immediately\":\"Imediat\",\"Hover enlargement\":\"Mărire cu mouse-ul\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Se joacă doar cartea de sub indicator, întotdeauna dezactivată. Îndepărtarea sau ascunderea filei oprește imediat fluxul.\",\"Live Preview Dock\":\"Panou de previzualizare live\",\"Enable low-load live previews\":\"Activați previzualizările live cu încărcare redusă\",\"Concurrent previews\":\"Previzualizări simultane\",\"Window size\":\"Dimensiunea ferestrei\",\"Small\":\"Mic\",\"Medium\":\"Mediu\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Previzualizările încep dezactivate și se întrerup în filele ascunse. Audio necesită întotdeauna un clic explicit.\",\"Quick info & chat focus\":\"Informații rapide și focalizare pe chat\",\"Enable model Quick View\":\"Activați Vizualizare rapidă a modelului\",\"Chat focus mode (opt-in)\":\"Modul de focalizare pe chat (înscriere)\",\"Show local timestamps\":\"Afișați marcajele orare locale\",\"System messages\":\"Mesaje de sistem\",\"Show\":\"Arată\",\"Hide\":\"Ascunde\",\"Chat filter strength\":\"Puterea filtrului de chat\",\"Low\":\"Scăzut\",\"Highlight words (comma separated)\":\"Evidențiați cuvinte (separate prin virgulă)\",\"Muted words (comma separated)\":\"Cuvinte dezactivate (separate prin virgulă)\",\"Collapse repeated messages\":\"Restrângeți mesajele repetate\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Îmbunătățirile pentru chat schimbă doar afișarea curentă. Nu stochează niciodată chatul, nu trimit mesaje sau bacșișuri automat.\",\"Shortcuts\":\"Comenzi rapide\",\"Command menu\":\"Meniul de comandă\",\"Global model search\":\"Căutare globală de modele\",\"Reset design defaults\":\"Resetați setările implicite de design\",\"Optional modules\":\"Module optionale\",\"Normal-page dock and card actions\":\"Acțiuni normale de andocare a paginilor și carduri\",\"Real audio meters\":\"Contoare audio reale\",\"Media, app and contest directories\":\"Directoare media, aplicații și concursuri\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Comutatoarele modulului au efect complet după reîncărcarea paginii. Datele de bază și modulele de rezervă rămân activate pentru a preveni pierderea datelor.\",\"Name the current filters and sorting\":\"Denumiți filtrele curente și sortarea\",\"My smart view\":\"Vederea mea inteligentă\",\"Delete smart view\":\"Ștergeți vizualizarea inteligentă\",\"Smart view deleted\":\"Vizualizarea inteligentă a fost ștearsă\",\"tags\":\"etichete\",\"countries\":\"ţări\",\"Saved selection\":\"Selecția salvată\",\"Suggestions\":\"Sugestii\",\"Selected\":\"Selectat\",\"Website tags\":\"Etichete de site\",\"Countries (multiple selection)\":\"Țări (selecție multiplă)\",\"Select countries\":\"Selectați țări\",\"Search countries…\":\"Căutați țări...\",\"Recognized in current models\":\"Recunoscut în modelele actuale\",\"All countries\":\"Toate țările\",\"Tags (multiple selection)\":\"Etichete (selecție multiplă)\",\"Select tags\":\"Selectați etichete\",\"Search tags…\":\"Etichete de căutare...\",\"Room subject\":\"Subiectul camerei\",\"Current online duration\":\"Durata curentă online\",\"Local watch time and sessions\":\"Ora locală de vizionare și sesiuni\",\"HD, new, gaming and favorite badges\":\"HD, insigne noi, pentru jocuri și preferate\",\"Homepage Studio\":\"Studio pentru pagina principală\",\"Balanced preset\":\"Presetare echilibrată\",\"Compact preset\":\"Presetat compact\",\"Data-rich preset\":\"Presetare bogată în date\",\"Cinema preset\":\"Cinema presetat\",\"Layout and pagination\":\"Aspect și paginare\",\"Show quick toolbar above models\":\"Afișați bara de instrumente rapidă deasupra modelelor\",\"Cards per row\":\"Cărți pe rând\",\"Image ratio\":\"Raportul imaginii\",\"Information display\":\"Afișaj informații\",\"Detailed\":\"Detaliat\",\"Overlay on image\":\"Suprapunere pe imagine\",\"Powerful sorting\":\"Sortare puternică\",\"Primary sort\":\"Sortare primară\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Sortarea modifică numai ordinea de afișare a cardurilor model încărcate. Nu schimbă urmările sau favoritele din contul tău Chaturbate.\",\"Restore website order\":\"Restabiliți ordinea site-ului\",\"Clear all filters\":\"Ștergeți toate filtrele\",\"Search and range filters\":\"Filtre de căutare și intervale\",\"When several tags are selected\":\"Când sunt selectate mai multe etichete\",\"Model must contain every tag\":\"Modelul trebuie să conțină fiecare etichetă\",\"At least one tag matches\":\"Cel puțin o etichetă se potrivește\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Filtrarea pe țară afișează modele din orice țară selectată. Etichetele proprii și etichetele site-ului web recunoscute rămân disponibile în listă și pot fi găsite rapid cu ajutorul căutării.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"După selectarea etichetelor sau a țărilor, RoomGrid caută în întregul catalog live actual. Aceeași pagină de rezultate arată inițial 120 de potriviri și se încarcă mai multe în bucăți în timpul derulării, astfel încât chiar și etichetele foarte comune rămân receptive. Butonul de reîncărcare din bara de instrumente a paginii de pornire reîmprospătează lista live.\",\"Quick conditions\":\"Condiții rapide\",\"HD models only\":\"Numai modele HD\",\"New models only\":\"Doar modele noi\",\"Gaming models only\":\"Numai modele de jocuri\",\"Only saved in RoomGrid\":\"Salvat doar în RoomGrid\",\"Only own favorites\":\"Doar propriile favorite\",\"Following only\":\"Urmează doar\",\"Hide previously watched models\":\"Ascunde modelele urmărite anterior\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Țara, vârsta și durata online provin din cardurile publice curente sau din directorul sălilor publice. Dacă Chaturbate nu oferă un câmp, RoomGrid nu îl ghicește.\",\"Smart filter views\":\"Vizualizări inteligente cu filtru\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Salvați filtrele curente și sortarea ca vizualizare dinamică. Modelele care vin online mai târziu și se potrivesc cu regulile apar automat.\",\"Save current filter view\":\"Salvați vizualizarea curentă a filtrului\",\"No smart views saved yet.\":\"Nu au fost salvate încă vizualizări inteligente.\",\"What to show for each model\":\"Ce să arăți pentru fiecare model\",\"Import complete\":\"Import finalizat\",\"Invalid file\":\"Fișier nevalid\",\"Range\":\"Gama\",\"Today\":\"Astăzi\",\"Week\":\"Săptămâna\",\"Month\":\"luna\",\"Year\":\"Anul\",\"Recording enabled\":\"Înregistrare activată\",\"Private session (do not record)\":\"Sesiune privată (nu înregistrați)\",\"Retention days\":\"Zile de reținere\",\"Usage time\":\"Timp de utilizare\",\"Summed camera time\":\"Durata însumată a camerei\",\"Visits\":\"Vizite\",\"Known models\":\"Modele cunoscute\",\"Weekly heatmap\":\"Harta termică săptămânală\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Agregat după zi și oră; mai întunecat înseamnă mai mult timp observat.\",\"Sunday to Saturday, 24 hours per row\":\"Duminică până sâmbătă, 24 de ore pe rând\",\"Watch ranking\":\"Clasamentul ceasului\",\"little data\":\"putine date\",\"No watch history yet.\":\"Încă nu există istoric de vizionări.\",\"Workspace, filter and quality events\":\"Spațiu de lucru, filtre și evenimente de calitate\",\"Recent timeline\":\"Cronologie recentă\",\"Export sensitive statistics\":\"Exportați statistici sensibile\",\"Import statistics\":\"Importați statistici\",\"Delete selected range\":\"Ștergeți intervalul selectat\",\"Delete local events in the selected range?\":\"Ștergeți evenimentele locale din intervalul selectat?\",\"Clear all statistics\":\"Ștergeți toate statisticile\",\"Permanently clear all local statistics?\":\"Ștergeți definitiv toate statisticile locale?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Exporturile de statistici conțin nume de model și marcaje de timp și ar trebui tratate ca fiind sensibile. Datele vechi sunt curățate conform reținerii.\",\"new\":\"nou\",\"Under 1 minute\":\"Sub 1 minut\",\"1–5 minutes\":\"1–5 minute\",\"5–15 minutes\":\"5–15 minute\",\"15–30 minutes\":\"15–30 de minute\",\"30+ minutes\":\"30+ minute\",\"Local observations and your usage\":\"Observații locale și utilizarea dvs\",\"Public profile loaded on request\":\"Profil public încărcat la cerere\",\"Local RoomGrid interaction\":\"Interacțiune locală RoomGrid\",\"Local browser player\":\"Player de browser local\",\"Optional official statistics API\":\"API-ul pentru statistici oficiale opționale\",\"Local daily aggregation\":\"Agregare zilnică locală\",\"Local data\":\"Date locale\",\"Model summaries\":\"Rezumate model\",\"Public profiles\":\"Profiluri publice\",\"Usage events\":\"Evenimente de utilizare\",\"Official account snapshots\":\"Instantanee oficiale ale contului\",\"Daily rollups\":\"Rollup-uri zilnice\",\"Camera time\":\"Timpul camerei\",\"vs previous period\":\"față de perioada anterioară\",\"Trends\":\"Tendințe\",\"Daily activity\":\"Activitate zilnică\",\"Playback quality\":\"Calitatea redării\",\"Privacy status\":\"Starea de confidențialitate\",\"Data provenance\":\"Proveniența datelor\",\"Local RoomGrid usage\":\"Utilizare locală RoomGrid\",\"7 days\":\"7 zile\",\"30 days\":\"30 de zile\",\"90 days\":\"90 de zile\",\"Not enough data yet.\":\"Nu sunt suficiente date încă.\",\"Models revisited\":\"Modele revăzute\",\"Previews opened\":\"S-au deschis previzualizările\",\"Quick Views\":\"Vizualizări rapide\",\"Samples\":\"Mostre\",\"Private session\":\"Sesiune privată\",\"History paused\":\"Istoria sa oprit\",\"Recording locally\":\"Înregistrare local\",\"Disabled\":\"Dezactivat\",\"No cloud sync or telemetry.\":\"Fără sincronizare în cloud sau telemetrie.\",\"Raw events\":\"Evenimente brute\",\"Only local observations and actions from this browser.\":\"Doar observații și acțiuni locale din acest browser.\",\"Time\":\"timpul\",\"Models\":\"Modele\",\"Online planner\":\"Planificator online\",\"Data & privacy\":\"Date și confidențialitate\",\"Configure statistics dashboard\":\"Configurați tabloul de bord cu statistici\",\"Preset\":\"Presetat\",\"Minimal\":\"Minim\",\"Quality focus\":\"Concentrare asupra calității\",\"Privacy focus\":\"Concentrare asupra confidențialității\",\"Reset dashboard\":\"Resetați tabloul de bord\",\"Size\":\"Dimensiune\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Ordinea, vizibilitatea și lățimea sunt stocate în setări sigure pentru actualizare. Fiecare widget își arată sursa de date.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Nu este vizibil niciun widget din tabloul de bord. Deschideți configurația și selectați cel puțin un widget.\",\"Median session\":\"Sesiune mediană\",\"Daily usage trend\":\"Tendință de utilizare zilnică\",\"Your trend appears here after you start using RoomGrid.\":\"Tendința dvs. apare aici după ce începeți să utilizați RoomGrid.\",\"Session-duration distribution\":\"Distribuția sesiunii-durată\",\"No session-duration data yet.\":\"Încă nu există date despre durata sesiunii.\",\"Return rate\":\"Rata de retur\",\"Total sessions\":\"Total sesiuni\",\"Longest observed\":\"Cel mai mult observat\",\"Model ranking\":\"Clasamentul modelului\",\"Select up to four models for a local comparison.\":\"Selectați până la patru modele pentru o comparație locală.\",\"Quick View\":\"Vizualizare rapidă\",\"No model data yet.\":\"Nu există date despre model încă.\",\"Models with time patterns\":\"Modele cu modele de timp\",\"High confidence\":\"Încredere ridicată\",\"Moderate confidence\":\"Încredere moderată\",\"Planning horizon\":\"Orizontul de planificare\",\"Likely next active times\":\"Probabil următoarele perioade active\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Calculat din mostre online observate local; nu garantează o emisiune. Încrederea se îmbunătățește cu mai multe date.\",\"High\":\"Înalt\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Încă nu sunt suficiente observații online. Planificatorul apare automat împreună cu biblioteca.\",\"Discovery events\":\"Evenimente de descoperire\",\"Local interaction funnel\":\"Pâlnie de interacțiune locală\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Sunt luate în considerare doar acțiunile dvs. pe acest dispozitiv; conținutul de chat și plățile nu sunt niciodată urmărite.\",\"Workspace events\":\"Evenimente în spațiul de lucru\",\"Filter events\":\"Filtrați evenimentele\",\"Tag events\":\"Etichetați evenimente\",\"Top-four workspace comparison\":\"Comparație cu primele patru spații de lucru\",\"Workspace usage\":\"Utilizarea spațiului de lucru\",\"No workspace data yet.\":\"Încă nu există date despre spațiul de lucru.\",\"Average active streams\":\"Fluxuri active medii\",\"Playback quality by model\":\"Calitatea redării după model\",\"No quality data yet.\":\"Nu există date de calitate încă.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Fișierul cu statistici este invalid sau depășește limitele de siguranță. Datele curente nu au fost modificate.\",\"Statistics import assistant · preview\":\"Asistent pentru import statistici · previzualizare\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Nimic nu s-a schimbat încă. Selectați secțiunile pentru a importa și a examina conflictele.\",\"Safe merge (recommended)\":\"Îmbinare sigură (recomandat)\",\"Import complete. The previous statistics were backed up automatically.\":\"Import finalizat. Statisticile anterioare au fost salvate automat.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"Importul nu a putut fi aplicat în siguranță. Datele actuale și backup-urile rămân neschimbate.\",\"File schema\":\"Schema de fișiere\",\"Date range\":\"Interval de date\",\"Sensitive data\":\"Date sensibile\",\"Not marked sensitive\":\"Nu este marcat ca sensibil\",\"Until manually resumed\":\"Până la reluarea manuală\",\"Not paused\":\"Nu întrerupt\",\"Restore this statistics backup? The current state will be backed up first.\":\"Restabiliți această copie de rezervă a statisticilor? Se va face backup mai întâi pentru starea actuală.\",\"Permanently delete this backup?\":\"Ștergeți definitiv această copie de rezervă?\",\"Events\":\"Evenimente\",\"Estimated statistics storage\":\"Stocarea estimată a statisticilor\",\"Recording, pauses and private sessions\":\"Înregistrare, pauze și sesiuni private\",\"Record usage statistics\":\"Înregistrați statistici de utilizare\",\"Pause 15 minutes\":\"Pauza de 15 minute\",\"Pause 1 hour\":\"Pauză 1 oră\",\"Pause 8 hours\":\"Pauza de 8 ore\",\"Pause indefinitely\":\"Întrerupeți la nesfârșit\",\"Resume history\":\"Reluați istoricul\",\"History status\":\"Stare istorică\",\"Private for 30 minutes\":\"Privat timp de 30 de minute\",\"Private for 2 hours\":\"Privat timp de 2 ore\",\"Private until resumed\":\"Privat până la reluare\",\"End private session\":\"Încheiați sesiunea privată\",\"Not active\":\"Nu este activ\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"O sesiune privată oprește, de asemenea, observațiile de model public; o pauză normală a istoricului oprește utilizarea personală și înregistrările de calitate.\",\"Automatic deletion and retention\":\"Ștergerea și păstrarea automată\",\"Raw events (days)\":\"Evenimente brute (zile)\",\"Quality samples (days)\":\"Probe de calitate (zile)\",\"Account snapshots (days)\":\"Instantanee ale contului (zile)\",\"Daily rollups (days)\":\"Sumar zilnic (zile)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Evenimentele brute expirate sunt transformate în totaluri zilnice înainte de ștergerea automată. Calitatea și instantaneele contului își folosesc propriile limite.\",\"Statistics backup center\":\"Centru de backup pentru statistici\",\"Create internal backup now\":\"Creați o copie de rezervă internă acum\",\"Pseudonymized export\":\"Export pseudonimizat\",\"Print report\":\"Imprimați raportul\",\"Choose import file\":\"Alegeți fișierul de import\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Încă nu există copii de rezervă ale statisticilor interne. Până la trei sunt păstrate după importuri, restaurări sau crearea manuală.\",\"Delete local statistics in this range? A backup will be created first.\":\"Ștergeți statisticile locale din acest interval? Mai întâi va fi creată o copie de rezervă.\",\"Clear everything\":\"Șterge totul\",\"Permanently clear all local statistics? A backup will be created first.\":\"Ștergeți definitiv toate statisticile locale? Mai întâi va fi creată o copie de rezervă.\",\"Data inventory and provenance\":\"Inventarul datelor și proveniența\",\"Stored only in this browser\":\"Stocat numai în acest browser\",\"Coverage\":\"Acoperire\",\"Unknown or missing data is never inferred.\":\"Datele necunoscute sau lipsă nu sunt niciodată deduse.\",\"Metric definitions & data quality\":\"Definiții de metrică și calitatea datelor\",\"Accumulated locally while a RoomGrid page is active.\":\"Acumulat local în timp ce o pagină RoomGrid este activă.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Suma tuturor sesiunilor de cameră observate; sunt adăugate fluxuri concurente.\",\"browser samples; no interpolation.\":\"mostre de browser; fara interpolare.\",\"Privacy boundary\":\"Limită de confidențialitate\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Nu sunt salvate conținut de chat, plăți sau fișiere media. Sesiunile private nu adaugă evenimente.\",\"No history yet\":\"Nicio istorie încă\",\"Loading\":\"Încărcare\",\"Connected\":\"Conectat\",\"Locked\":\"Blocat\",\"Not connected\":\"Nu este conectat\",\"Username\":\"Nume de utilizator\",\"24-character stats token\":\"Jeton de statistici cu 24 de caractere\",\"At least 8 characters\":\"Cel puțin 8 caractere\",\"Secure official statistics connection\":\"Conexiune securizată la statistici oficiale\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Opțional. Tokenul nu intră niciodată în setările normale, jurnalele sau exporturile. Modul sesiune îl păstrează doar în memorie; modul criptat utilizează stocarea locală izolată.\",\"Stats token\":\"Indicator de statistici\",\"Vault password\":\"Parola seifului\",\"Connect for this session\":\"Conectați-vă pentru această sesiune\",\"Username or token is invalid.\":\"Numele de utilizator sau simbolul este nevalid.\",\"Encrypt, save & connect\":\"Criptați, salvați și conectați-vă\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Nu s-a putut salva criptat. Verificați tokenul, parola și permisiunile userscript-manager.\",\"Get official token\":\"Obțineți un token oficial\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"Reîmprospătarea automată rulează cel mult o dată la cinci minute. Acest modul folosește doar un simbol oficial pe care îl furnizați în mod deliberat.\",\"Account\":\"Cont\",\"History snapshots\":\"Instantanee ale istoriei\",\"Last refresh\":\"Ultima reîmprospătare\",\"Unlock local vault\":\"Deblocați seiful local\",\"Password\":\"Parolă\",\"Unlock\":\"Deblocați\",\"Unlock failed.\":\"Deblocarea eșuată.\",\"Loading…\":\"Se încarcă…\",\"Refresh now\":\"Actualizează acum\",\"Auto-refresh every 5 minutes\":\"Reîmprospătare automată la fiecare 5 minute\",\"Disconnect and delete token\":\"Deconectați și ștergeți tokenul\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Ștergeți conexiunea locală și simbolul criptat? Statisticile istorice rămân.\",\"Broadcast status\":\"Starea difuzării\",\"Token balance\":\"Bilanțul de simboluri\",\"Tips last hour\":\"Sfaturi ultima ora\",\"Followers\":\"Urmaritori\",\"Up votes\":\"Voturi în sus\",\"Down votes\":\"Voturi în jos\",\"Satisfaction\":\"Satisfacția\",\"Registered viewers\":\"Spectatori înregistrați\",\"Tokened viewers\":\"Spectatori cu simboluri\",\"Time online\":\"Timp online\",\"Follower trend\":\"Tendință de urmăritori\",\"Tips trend\":\"Trend sfaturi\",\"Token-balance trend\":\"Tendința balanței simbolurilor\",\"Viewer trend\":\"Tendința spectatorilor\",\"Account Health Center\":\"Centrul de sănătate a contului\",\"Official My Collection\":\"Colecția mea oficială\",\"Official support\":\"Sprijin oficial\",\"Export local statistics\":\"Exportați statistici locale\",\"Delete snapshot history\":\"Ștergeți istoricul instantaneelor\",\"Delete the local official-statistics snapshots for this account?\":\"Ștergeți instantaneele de statistici oficiale locale pentru acest cont?\",\"Last error\":\"Ultima eroare\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Starea simbolului, cadența de reîmprospătare, istoria locală și link-urile oficiale live aici. RoomGrid nu publică, trimite mesaje și nu efectuează tranzacții automat.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Browserul nu a acordat permisiunea de notificare. Notificările în pagină rămân active.\",\"Notification profile\":\"Profil de notificare\",\"Go-live only\":\"Doar live\",\"Personalized\":\"Personalizat\",\"Always (all events)\":\"Întotdeauna (toate evenimentele)\",\"Inactive days\":\"Zile inactive\",\"Favorites, rating, priority and your tags decide relevance.\":\"Favoritele, evaluarea, prioritatea și etichetele dvs. decid relevanța.\",\"Includes go-live, offline and public-profile changes.\":\"Include modificări de lansare, offline și de profil public.\",\"Master switch\":\"Comutator principal\",\"On\":\"Pornit\",\"Off (default)\":\"Dezactivat (implicit)\",\"Monitored models\":\"Modele monitorizate\",\"Active tab\":\"Fila activă\",\"This tab\":\"Această filă\",\"Another tab\":\"O altă filă\",\"Local notifications\":\"Notificări locale\",\"Notification delivery and display\":\"Livrarea și afișarea notificărilor\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Notificările în pagină folosesc zona liberă din bara de sus RoomGrid și nu au nevoie de permisiunea browserului. Poziția, durata și densitatea sunt configurabile.\",\"Delivery\":\"Livrare\",\"RoomGrid bar only\":\"Numai barul RoomGrid\",\"Browser notifications only\":\"Doar notificări de browser\",\"RoomGrid bar and browser\":\"Bara RoomGrid și browser\",\"No display, history only\":\"Fără afișare, doar istoric\",\"In-page position\":\"Poziția în pagină\",\"Free top-bar area\":\"Zona de top-bar gratuită\",\"Floating top center\":\"Plutitor de sus în centru\",\"Floating top right\":\"Plutitor, sus dreapta\",\"Display style\":\"Stilul de afișare\",\"Compact single line\":\"Compact o singură linie\",\"Display duration\":\"Durata de afișare\",\"Until dismissed\":\"Până la demitere\",\"Visible at once\":\"Vizibil deodată\",\"Test notification\":\"Notificare de testare\",\"General rules\":\"Reguli generale\",\"Enable notifications\":\"Activați notificările\",\"The browser did not grant notification permission.\":\"Browserul nu a acordat permisiunea de notificare.\",\"Neutral text\":\"Text neutru\",\"Quiet time starts\":\"Începe timpul de liniște\",\"Quiet time ends\":\"Timpul de liniște se termină\",\"Cooldown (minutes)\":\"Cooldown (minute)\",\"Hourly limit\":\"Limită orară\",\"Favorite threshold\":\"Pragul preferat\",\"Open on click\":\"Deschide la clic\",\"Per model\":\"Pe model\",\"Library is empty.\":\"Biblioteca este goală.\",\"Per group\":\"Pe grup\",\"Group\":\"grup\",\"No groups.\":\"Fără grupuri.\",\"Local notification list\":\"Lista locală de notificări\",\"Mark all read\":\"Marcați toate citite\",\"Clear list\":\"Ștergeți lista\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Fondul verifică starea doar citire și nu începe niciun videoclip. Un userscript poate rula numai în timp ce o filă de site asociată este deschisă; mai multe file aleg automat o singură verificare.\",\"Shield status\":\"Stare scut\",\"Ready\":\"Gata\",\"Local PIN\":\"PIN local\",\"Set\":\"Setați\",\"Not set\":\"Nu setat\",\"Quick shield\":\"Scut rapid\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Acoperă imediat numele și imaginile și întrerupe/dezactivează toate mediile. Restaurarea returnează starea media anterioară.\",\"Activate now\":\"Activați acum\",\"Auto-shield after inactivity\":\"Auto-scut după inactivitate\",\"Set or change local PIN\":\"Setați sau modificați codul PIN local\",\"Leave empty to remove\":\"Lăsați gol pentru a elimina\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"PIN-ul este doar o barieră de acces local, nu o criptare și nu înlocuiește protecția contului sistemului de operare.\",\"History controls\":\"Controale istorice\",\"Usage statistics\":\"Statistici de utilizare\",\"Pause history 1 hour\":\"Întrerupeți istoricul 1 oră\",\"Clear statistics\":\"Statistici clare\",\"Clear all local statistics? A backup will be created first.\":\"Ștergeți toate statisticile locale? Mai întâi va fi creată o copie de rezervă.\",\"Clear Discover settings\":\"Ștergeți setările Discover\",\"Clear local Discover filters and feeds?\":\"Ștergeți filtrele și feedurile Discover locale?\",\"Clear notification list\":\"Ștergeți lista de notificări\",\"Data boundary\":\"Limita datelor\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Configurația, statisticile de vizionare, istoricul notificărilor și preferințele Discover sunt stocate local în acest browser. RoomGrid nu conține sincronizare în cloud, încărcare de cont sau telemetrie.\",\"Quota\":\"Cotă\",\"Used\":\"Folosit\",\"Sensitivity & consent center\":\"Centru de sensibilitate și consimțământ\",\"Standard configuration\":\"Configurație standard\",\"Theme, layout, filters\":\"Temă, aspect, filtre\",\"Model names, notes, timestamps, statistics\":\"Nume modele, note, marcaje temporale, statistici\",\"Warn on export\":\"Avertizare la export\",\"Optional official statistics token\":\"Indicativ statistic oficial opțional\",\"Session-only or encrypted vault\":\"Seif numai pentru sesiune sau criptat\",\"Chat content\":\"Conținut de chat\",\"Never persisted\":\"Nu a persistat niciodată\",\"Ephemeral\":\"Efemer\",\"Stream recording/screenshots\":\"Înregistrare/capturi de ecran în flux\",\"Not provided\":\"Nu este furnizat\",\"Policy guard\":\"Gardianul politicii\",\"Performance profile\":\"Profil de performanță\",\"Playing streams\":\"Redarea fluxurilor\",\"Video elements\":\"Elemente video\",\"Measured memory\":\"Memoria măsurată\",\"Unavailable in browser\":\"Indisponibil în browser\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Măsurătorile curente sunt aproape de limita configurată. Întrerupeți camerele nefolosite sau alegeți modul Eco.\",\"Stream performance\":\"Performanță în flux\",\"Power\":\"Putere\",\"Concurrent stream limit\":\"Limită de flux simultan\",\"Adaptive quality\":\"Calitate adaptivă\",\"Gradual startup\":\"Pornire treptată\",\"Automatic performance mode\":\"Modul de performanță automat\",\"Memory threshold\":\"Pragul de memorie\",\"Stream-count threshold\":\"Pragul de numărare a fluxurilor\",\"Current auto state\":\"Stare auto actuală\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Previzualizările neutilizate sunt oprite. Videoclipul nou rămâne întrerupt la limită până când capacitatea este disponibilă. Numărările mari încep treptat; nu există nicio garanție pentru „camere nelimitate”.\",\"Multi-window workspaces\":\"Spații de lucru cu mai multe ferestre\",\"Window name\":\"Numele ferestrei\",\"Active and recent windows\":\"Ferestre active și recente\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"Biblioteca de modele este sincronizată prin fuziune locală; fiecare fereastră își păstrează propriul spațiu de lucru și setări de vizualizare. Asociațiile recente ale ferestrelor pot fi restaurate după repornire acolo unde browserul permite acest lucru.\",\"Optional tools are disabled\":\"Instrumentele opționale sunt dezactivate\",\"Media, app and contest directories will not scan the current page.\":\"Directoarele media, aplicațiile și concursurile nu vor scana pagina curentă.\",\"Enable optional tools\":\"Activați instrumentele opționale\",\"Local search\":\"Căutare locală\",\"Search saved items and current page\":\"Căutați articolele salvate și pagina curentă\",\"Rescan current page\":\"Rescanați pagina curentă\",\"Disable optional tools\":\"Dezactivați instrumentele opționale\",\"Clear local side data\":\"Ștergeți datele secundare locale\",\"Clear app favorites, categories and media notes?\":\"Ștergeți aplicațiile favorite, categoriile și notele media?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Aceste module organizează doar linkuri publice și text deja accesibil pe pagina curentă. Nu copiază media, nu descarcă automat și nu operează funcții de chat, token sau de plată. Autentificarea, verificarea vârstei, securitatea și formularele de plată rămân neatinse.\",\"Photo and video areas\":\"Zone foto și video\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Nu este prezentă nicio zonă media publică recunoscută pe această pagină. Scanați din nou în timp ce vă aflați pe o pagină nativă de fotografie sau video.\",\"Apps directory\":\"Directorul de aplicații\",\"Local category\":\"Categoria locală\",\"Found on page\":\"Găsit pe pagină\",\"No recognizable app links are present on this page.\":\"Nu sunt prezente linkuri de aplicații recunoscute pe această pagină.\",\"Public contest results\":\"Rezultatele concursului public\",\"No readable public contest table is present on this page.\":\"Pe această pagină nu este prezent un tabel public de concurs lizibil.\",\"Native account, help and broadcaster links\":\"Cont nativ, ajutor și link-uri pentru difuzor\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Nu au fost găsite linkuri native sigure pe această pagină; căile de plată și de conectare sunt filtrate în mod deliberat.\",\"Homepage filters reset\":\"Filtrele paginii de pornire sunt resetate\",\"Global appearance reset\":\"Resetarea aspectului global\",\"Version\":\"Versiune\",\"Health score\":\"Scorul de sănătate\",\"Page type\":\"Tipul paginii\",\"Route generation\":\"Generarea rutei\",\"Running modules\":\"Rularea modulelor\",\"Local errors\":\"Erori locale\",\"Repair center\":\"Centru de reparatii\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"O copie de rezervă a configurației este creată înainte de fiecare reparație. Statisticile și datele modelului nu sunt șterse prin aceste acțiuni.\",\"Reset home filters\":\"Resetați filtrele de acasă\",\"Reset all home-page filters?\":\"Resetați toate filtrele paginii de pornire?\",\"Reset global appearance\":\"Resetați aspectul global\",\"Reset the global appearance? Saved design profiles remain.\":\"Resetați aspectul global? Profilurile de design salvate rămân.\",\"Normalize configuration\":\"Normalizați configurația\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Validați și rescrieți configurația curentă? Mai întâi este creată o copie de rezervă.\",\"Close all previews\":\"Închideți toate previzualizările\",\"Compatibility self-check\":\"Autoverificarea compatibilităţii\",\"Module and page status\":\"Starea modulului și a paginii\",\"Page route test matrix\":\"Matricea de testare a traseului paginii\",\"Current route\":\"Ruta curentă\",\"Recent page transitions\":\"Tranziții recente de pagină\",\"Capability layer\":\"Stratul de capacitate\",\"Available\":\"Disponibil\",\"Fallback\":\"Revenire\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Când lipsește un API, doar modulul afectat revine. Eșecurile repetate ale rețelei deschid temporar un întrerupător.\",\"Backup and recovery status\":\"Stare de backup și recuperare\",\"Primary data\":\"Date primare\",\"Stable archive\":\"Arhivă stabilă\",\"Rolling backups\":\"Backup-uri rulante\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"Autoverificarea verifică doar lizibilitatea și capacitățile browserului; nu suprascrie niciodată automat datele curente. Recuperarea continuă să utilizeze fluxul de backup confirmat existent.\",\"Sanitized local error log\":\"Jurnal de erori local igienizat\",\"No RoomGrid errors recorded.\":\"Nu au fost înregistrate erori RoomGrid.\",\"Run self-check again\":\"Rulați din nou autoverificarea\",\"Export diagnostics\":\"Export de diagnosticare\",\"Clear error log\":\"Ștergeți jurnalul de erori\",\"Return to normal site\":\"Reveniți la site-ul normal\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"Exportul de diagnosticare nu conține spațiu de lucru, nume de model, note sau statistici. Parametrii de interogare URL și posibilele valori secrete sunt eliminate din textul de eroare.\",\"Local history on\":\"Istoria locală activată\",\"History off\":\"Istoricul oprit\",\"This settings page could not be loaded\":\"Această pagină de setări nu a putut fi încărcată\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Datele dvs. nu au fost modificate. Încearcă din nou; dacă problema rămâne, deschideți Diagnosticare.\",\"Try again\":\"Încercați din nou\",\"Open Diagnostics\":\"Deschideți Diagnosticare\",\"Public results\":\"Rezultate publice\",\"Public contest links\":\"Linkuri pentru concursuri publice\",\"Page content has not appeared yet\":\"Conținutul paginii nu a apărut încă\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid a verificat din nou pagina, dar tot nu poate găsi zona de conținut a lui Chaturbate. Este posibil ca pagina să se încarce în continuare sau site-ul a returnat o vizualizare goală.\",\"Check again\":\"Verificați din nou\",\"Open RoomGrid Discover\":\"Deschide RoomGrid Discover\",\"Reload page\":\"Reîncărcați pagina\",\"Hide notice\":\"Ascundeți notificarea\",\"Open MultiCam\":\"Deschideți MultiCam\",\"Open workstation in a new window\":\"Deschideți stația de lucru într-o fereastră nouă\",\"Open Discover\":\"Deschideți Discover\",\"Statistics center\":\"Centrul de statistică\",\"Backup and import\":\"Backup și import\",\"Page and design options\":\"Opțiuni de pagină și design\",\"Hide immediately\":\"Ascunde-te imediat\",\"All tools\":\"Toate instrumentele\",\"Search & commands\":\"Căutare și comenzi\",\"Page design\":\"Designul paginii\",\"Official collection\":\"Colecție oficială\",\"Saved\":\"Salvat\",\"Quick info\":\"Informații rapide\",\"Loading live frame\":\"Se încarcă cadrul live\",\"Live still\":\"Trăiește în continuare\",\"Not live\":\"Nu în direct\",\"Pages\":\"Pagini\",\"Refresh\":\"Reîmprospătați\",\"Search…\":\"Cauta...\",\"Tag Explorer\":\"Explorator de etichete\",\"Refresh public catalog\":\"Actualizează catalogul public\",\"Toggle favorite\":\"Comutați favorit\",\"Save\":\"Salvați\",\"Remove favorite\":\"Eliminați preferatul\",\"Removed from library\":\"Eliminat din bibliotecă\",\"Saved to library\":\"Salvat în bibliotecă\",\"Added to favorites\":\"Adăugat la favorite\",\"Removed from favorites\":\"Eliminat din favorite\",\"Watching\":\"Privind\",\"Watch\":\"Privește\",\"Temporary watchlist\":\"Lista temporară de urmărire\",\"Removed from watchlist\":\"Eliminat din lista de urmărire\",\"Added to watchlist\":\"Adăugat la lista de urmărire\",\"Choose group\":\"Alegeți grupul\",\"Add to group…\":\"Adăugați la grup...\",\"Group updated\":\"Grupul a fost actualizat\",\"Site room\":\"Cameră de șantier\",\"Select for bulk actions\":\"Selectați pentru acțiuni în bloc\",\"viewers\":\"telespectatori\",\"No public description\":\"Fără descriere publică\",\"Open in Tag Explorer\":\"Deschideți în Tag Explorer\",\"Feed name\":\"Numele feedului\",\"View name\":\"Vedeți numele\",\"Name already exists\":\"Numele există deja\",\"Saved-item limit reached\":\"Limita de articole salvate a fost atinsă\",\"Feed saved\":\"Feed salvat\",\"View saved\":\"Vizualizare salvată\",\"Applied\":\"Aplicat\",\"Backup exported\":\"Backup exportat\",\"Backup imported\":\"Backup importat\",\"Invalid backup\":\"Backup nevalid\",\"New models\":\"Modele noi\",\"New since last visit\":\"Nou de la ultima vizită\",\"New to me\":\"Nou pentru mine\",\"Returning\":\"Întorcându-se\",\"Stable online\":\"Stabil online\",\"Saved only\":\"Doar salvat\",\"Previously watched\":\"Vizionat anterior\",\"Local collection\":\"Colecție locală\",\"Hide visited today\":\"Hide vizitat astăzi\",\"No feed selected\":\"Niciun feed selectat\",\"No view selected\":\"Nicio vizualizare selectată\",\"Saved feeds\":\"Fluxuri salvate\",\"Saved views\":\"Vizualizări salvate\",\"Save new\":\"Salvați nou\",\"All public rooms\":\"Toate camerele publice\",\"Known HD\":\"HD cunoscut\",\"Random result\":\"Rezultat aleatoriu\",\"Viewers ↓\":\"Spectatori ↓\",\"Viewers ↑\":\"Spectatori ↑\",\"Online duration ↓\":\"Durata online ↓\",\"Followers ↓\":\"Urmaritori ↓\",\"Name A–Z\":\"Numele A–Z\",\"Name Z–A\":\"Numele Z–A\",\"New first\":\"Mai întâi nou\",\"Watch time ↓\":\"Timp de vizionare ↓\",\"Sessions ↓\":\"Sesiuni ↓\",\"Last watched\":\"Ultima vizionare\",\"Name, description, tag, place\":\"Nume, descriere, etichetă, loc\",\"Secondary sort\":\"Sortare secundară\",\"Shuffle again\":\"Amestecă din nou\",\"All groups\":\"Toate grupurile\",\"All tags (AND)\":\"Toate etichetele (ȘI)\",\"Any tag (OR)\":\"Orice etichetă (SAU)\",\"Excluded tags (NOT)\":\"Etichete excluse (NU)\",\"Language, region, or country\":\"Limbă, regiune sau țară\",\"Minimum online minutes\":\"Minute online minime\",\"Maximum online minutes\":\"Numărul maxim de minute online\",\"Minimum local watch minutes\":\"Minute de vizionare locale minime\",\"Advanced filters and Boolean tags\":\"Filtre avansate și etichete booleene\",\"Saved views and feeds\":\"Vizualizări și fluxuri salvate\",\"Recent searches\":\"Căutări recente\",\"Live previews and performance\":\"Previzualizări live și performanță\",\"Muted live preview for every visible card\":\"Previzualizare live dezactivată pentru fiecare card vizibil\",\"Refresh rate\":\"Rata de reîmprospătare\",\"Real-time video while hovering\":\"Videoclip în timp real în timp ce treceți cu mouse-ul\",\"Hover delay\":\"Întârzierea trecerii cu mouse-ul\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Cardurile vizibile folosesc cadre JPEG. La trecerea cu mouse-ul, doar un card comută la sunetul video în timp real și se oprește complet când indicatorul pleacă. Ratele ridicate de cadre cresc semnificativ lățimea de bandă și utilizarea procesorului.\",\"saved\":\"salvat\",\"previews\":\"previzualizări\",\"Previews off\":\"Previzualizările dezactivate\",\"No more feeds can be saved\":\"Nu mai pot fi salvate fluxuri\",\"Tag feed saved\":\"Feedul de etichete a fost salvat\",\"Select tag\":\"Selectați eticheta\",\"Tag favorite\":\"Etichetați favorit\",\"Block tag\":\"Eticheta de blocare\",\"Select tags first\":\"Selectați mai întâi etichetele\",\"Tag-group name\":\"Numele grupului de etichete\",\"Tag group saved\":\"Grupul de etichete a fost salvat\",\"No gaming rooms in the current catalog\":\"Nu există camere de jocuri în catalogul actual\",\"All (AND)\":\"Toate (ȘI)\",\"Any (OR)\":\"Orice (SAU)\",\"Exclude (NOT)\":\"Excludeți (NU)\",\"Search tags\":\"Căutați etichete\",\"Tag search\":\"Căutare de etichete\",\"Combination\":\"Combinație\",\"Model sorting\":\"Sortarea modelelor\",\"Show in Discover\":\"Afișați în Discover\",\"Save tag group\":\"Salvați grupul de etichete\",\"Save tag feed\":\"Salvați feedul de etichete\",\"No tag groups saved yet.\":\"Nu s-a salvat încă niciun grup de etichete.\",\"Unknown region\":\"Regiune necunoscută\",\"Unknown age\":\"Vârsta necunoscută\",\"Public “New”\":\"Public „Nou”\",\"My new cams\":\"Noile mele camere\",\"New-cams feed saved\":\"Fluxul camerelor noi a fost salvat\",\"Save “My new cams” feed\":\"Salvați feedul „Noile mele camere”.\",\"Create gaming workspace\":\"Creați spațiu de lucru pentru jocuri\",\"No matches for this combination.\":\"Nu există potriviri pentru această combinație.\",\"Select tags to preview matching rooms here.\":\"Selectați etichetele pentru a previzualiza sălile care se potrivesc aici.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Căutați, favorizați, blocați și combinați etichete publice. Trends utilizează numai modificările de catalog observate pe acest dispozitiv.\",\"Current tags\":\"Etichete curente\",\"Counts show current matches and change since the prior fetch.\":\"Numărările arată potrivirile curente și se modifică de la preluarea anterioară.\",\"Results for selected tags\":\"Rezultate pentru etichetele selectate\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Utilizează aceeași sortare și navigare SingleCam ca Discover.\",\"Categories, age, and regions\":\"Categorii, vârstă și regiuni\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Sunt afișate numai câmpurile publice explicite. Regiunile separate prin virgulă se combină cu OR; valorile necunoscute rămân vizibile. Niciun fus orar nu este ghicit.\",\"New and gaming\":\"Nou și jocuri\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Primăria locală, steagul public „Nou”, lista temporară de urmărire și temele de jocuri rămân separate.\",\"Tag groups\":\"Grupuri de etichete\",\"Save AND, OR, or NOT combinations.\":\"Salvați combinațiile ȘI, SAU sau NU.\",\"Favorite tags\":\"Etichete preferate\",\"Your personal shortcut list.\":\"Lista dvs. personală de comenzi rapide.\",\"No favorites yet\":\"Încă nu sunt favorite\",\"Related tags\":\"Etichete înrudite\",\"Tags co-occurring on the currently matching models.\":\"Etichete care apar concomitent pe modelele care se potrivesc în prezent.\",\"Personal tag history\":\"Istoricul etichetelor personale\",\"Recently and frequently used on this device.\":\"Utilizat recent și frecvent pe acest dispozitiv.\",\"Bulk target group\":\"Grup țintă în vrac\",\"Selected models added to group\":\"Modele selectate adăugate la grup\",\"New workspace name\":\"Nume nou spațiu de lucru\",\"Created from a Discover selection\":\"Creat dintr-o selecție Discover\",\"Workspace created\":\"Spațiul de lucru creat\",\"Save all\":\"Salvează totul\",\"Create workspace\":\"Creați spațiu de lucru\",\"Loading public catalog…\":\"Se încarcă catalogul public...\",\"Loading the complete catalog in the background…\":\"Se încarcă catalogul complet în fundal...\",\"Updated\":\"Actualizat\",\"Public rooms\":\"Camere publice\",\"The catalog is loading.\":\"Catalogul se încarcă.\",\"No matching rooms. Adjust search or filters.\":\"Nu există camere care se potrivesc. Ajustați căutarea sau filtrele.\",\"Previous\":\"Anterior\",\"Next\":\"În continuare\",\"Move up\":\"Mișcă-te în sus\",\"Move down\":\"Deplasați-vă în jos\",\"Number shown\":\"Numărul afișat\",\"Widget width\":\"Lățimea widgetului\",\"Full\":\"plin\",\"Nothing here yet.\":\"Nimic aici încă.\",\"Newly online\":\"Nou online\",\"Recently visited\":\"Vizitat recent\",\"Frequently visited\":\"Vizitat frecvent\",\"Homepage appearance, pagination, and performance\":\"Aspectul paginii de pornire, paginarea și performanța\",\"Information density\":\"Densitatea informației\",\"Layout\":\"Aspect\",\"Data rich\":\"Bogat în date\",\"Show statistics summary\":\"Afișează rezumatul statisticilor\",\"Show personal widgets\":\"Afișați widget-uri personale\",\"Sections below the live catalog\":\"Secțiuni de sub catalogul live\",\"Homepage live models\":\"Modele live pe pagina de pornire\",\"All live rooms in one place\":\"Toate camerele vii într-un singur loc\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Utilizați aceleași filtre complete de căutare, sortare, grupuri, etichete și istoric local ca și Discover.\",\"Refresh live catalog\":\"Actualizează catalogul live\",\"Homepage settings\":\"Setări pentru pagina de pornire\",\"Matches a favorite gaming theme\":\"Se potrivește cu o temă de joc preferată\",\"Saved models\":\"Modele salvate\",\"Online favorites\":\"Favorite online\",\"Local watch time\":\"Ora locală de vizionare\",\"Continue last MultiCam workspace\":\"Continuați ultimul spațiu de lucru MultiCam\",\"Open full Discover page\":\"Deschideți pagina Descoperire completă\",\"Online notifications\":\"Notificări online\",\"on\":\"pe\",\"off\":\"oprit\",\"Offline notifications\":\"Notificări offline\",\"Feed\":\"Hrăniți\",\"Frequent\":\"Frecvent\",\"Saved filter views\":\"Vizualizări de filtru salvate\",\"Favorites currently public and online.\":\"Favorite în prezent publice și online.\",\"New models or public streams that started recently.\":\"Modele noi sau fluxuri publice care au început recent.\",\"Current results from the selected feed.\":\"Rezultatele curente din feedul selectat.\",\"Suggestions with reasons\":\"Sugestii cu motive\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Bazat numai pe etichete publice partajate și pe teme preferate la nivel local; nicio inferență de identitate.\",\"Sorted by local last-visit time.\":\"Sortate după ora locală a ultimei vizite.\",\"Sorted by local watch time.\":\"Sortate după ora locală de vizionare.\",\"Saved models never visited\":\"Modele salvate nu au fost niciodată vizitate\",\"Saved in the library without local watch time.\":\"Salvat în bibliotecă fără timp local de vizionare.\",\"Draggable dashboard widgets\":\"Widgeturi de tablou de bord care pot fi glisate\",\"Your RoomGrid homepage\":\"Pagina dvs. de pornire RoomGrid\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Un catalog live rapid, personalizabil, cu SingleCam, MultiCam, filtre complete, vizualizări salvate și widget-uri personale.\",\"Audio activity\":\"Activitate audio\",\"Auto quality\":\"Calitate auto\",\"Stream interrupted; status checks continue.\":\"Fluxul întrerupt; verificările de stare continuă.\",\"This browser cannot play HLS.\":\"Acest browser nu poate juca HLS.\",\"Connecting to live stream…\":\"Se conectează la fluxul live...\",\"The room is not public right now.\":\"Sala nu este publică acum.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Momentan offline. Alternativele de la același filtru sunt prezentate mai jos.\",\"Status temporarily unavailable; retrying automatically.\":\"Stare indisponibilă temporar; reîncercând automat.\",\"Loading public profile…\":\"Se încarcă profilul public...\",\"Profile temporarily unavailable.\":\"Profil indisponibil temporar.\",\"No cached profile.\":\"Niciun profil memorat în cache.\",\"Public profile changed\":\"Profilul public a fost schimbat\",\"No public bio.\":\"Fără biografie publică.\",\"Source\":\"Sursa\",\"Observed\":\"Observat\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Sunt afișate numai linkurile autopublicate pe profilul public. Numai un nume de utilizator care se potrivește nu este dovada identității.\",\"Profile information\":\"Informații de profil\",\"Preparing…\":\"Se pregătesc…\",\"Play\":\"Joacă\",\"Unmute\":\"Activați sunetul\",\"Mute\":\"Mut\",\"Volume\":\"Volumul\",\"Contain\":\"Conține\",\"Cover\":\"Acoperire\",\"Brightness\":\"Luminozitate\",\"Saturation\":\"Saturație\",\"Grayscale\":\"Tonuri de gri\",\"Invert\":\"inversează\",\"Blur\":\"Estompare\",\"Image zoom\":\"Zoom imagine\",\"Recording saved\":\"Înregistrare salvată\",\"Screenshot saved\":\"Captură de ecran salvată\",\"Recording\":\"Înregistrare\",\"Mirror\":\"Oglindă\",\"Flip vertically\":\"Întoarceți vertical\",\"Rotate\":\"Rotiți\",\"Show edited\":\"Afișează editat\",\"Show original\":\"Afișați originalul\",\"Reset image\":\"Resetați imaginea\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Țineți apăsat Ctrl și derulați pentru a mări; trageți imaginea când este mărită. Faceți dublu clic pentru a reseta.\",\"Zoom out\":\"Micșorați\",\"Zoom in\":\"Măriți\",\"Reset zoom and position\":\"Resetați zoomul și poziția\",\"Player window\":\"Fereastra playerului\",\"Fullscreen\":\"Ecran complet\",\"Private notes, memories, or things to check next time (stored locally)\":\"Note private, amintiri sau lucruri de verificat data viitoare (stocate local)\",\"Autosave is on\":\"Salvarea automată este activată\",\"Enter your own tag…\":\"Introduceți propria etichetă...\",\"Saved at\":\"Salvat la\",\"Local details saved\":\"Detaliile locale au fost salvate\",\"Remove tag\":\"Eliminați eticheta\",\"No personal tags yet.\":\"Nu există încă etichete personale.\",\"Saving…\":\"Se salvează…\",\"Create your first group under Groups.\":\"Creați primul grup în Grupuri.\",\"Added to MultiCam\":\"Adăugat la MultiCam\",\"Edit private details\":\"Editați detaliile private\",\"Region\":\"Regiunea\",\"Current online time\":\"Ora curentă online\",\"Stream attributes\":\"Atributele fluxului\",\"New\":\"Nou\",\"Yes\":\"Da\",\"No / unknown\":\"Nu / necunoscut\",\"Last observed\":\"Ultimul observat\",\"Local visits\":\"Vizite locale\",\"First watched\":\"Prima dată vizionat\",\"No public description.\":\"Fără descriere publică.\",\"No public tags available.\":\"Nu sunt disponibile etichete publice.\",\"Quick-add your own tag…\":\"Adăugați rapid propria etichetă...\",\"Public tags\":\"Etichete publice\",\"Add\":\"Adăugați\",\"Private details\":\"Detalii private\",\"Everything stays in this browser and survives updates.\":\"Totul rămâne în acest browser și supraviețuiește actualizărilor.\",\"Private notebook\":\"Caiet privat\",\"Keep on watchlist\":\"Păstrați pe lista de urmărire\",\"Group assignment (multiple)\":\"Atribuire de grup (multiplu)\",\"Save now\":\"Salvați acum\",\"Chat loads only when this section is opened.\":\"Chatul se încarcă numai când această secțiune este deschisă.\",\"Official chat\":\"Chat oficial\",\"Official chat connected.\":\"Chat oficial conectat.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Chat încărcat; structura actuală a paginii nu a putut fi complet izolată.\",\"Chat loaded.\":\"Chat încărcat.\",\"Loading official chat…\":\"Se încarcă chatul oficial...\",\"Official chat room\":\"Cameră oficială de chat\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Se incarca numai cand este nevoie; RoomGrid nu stochează sau trimite automat mesaje de chat.\",\"Reload chat\":\"Reîncarcă chatul\",\"Open official room in a new window\":\"Deschide camera oficială într-o fereastră nouă\",\"Hide chat\":\"Ascunde chatul\",\"Live observations\":\"Observații în direct\",\"Trend confidence\":\"Încrederea în tendințe\",\"Known time slots\":\"Fase orare cunoscute\",\"Typical peak\":\"Vârful tipic\",\"Watched in 7 days\":\"Vizionat in 7 zile\",\"Watched in 30 days\":\"Vizionat in 30 de zile\",\"Current streak\":\"Serie actuală\",\"Typical weekly online times\":\"Timp online săptămânal obișnuit\",\"Online times will be learned on this device from now on.\":\"Orele online vor fi învățate pe acest dispozitiv de acum înainte.\",\"Based on observations on this device; missing information is never guessed.\":\"Pe baza observațiilor la acest dispozitiv; informațiile lipsă nu sunt niciodată ghicite.\",\"Activity by weekday\":\"Activitate în ziua săptămânii\",\"Most likely online times\":\"Cel mai probabil ori online\",\"Online-time map\":\"Harta timpului online\",\"Public covers and locally saved example images.\":\"Coperți publice și exemple de imagini salvate local.\",\"Import quality\":\"Calitate de import\",\"Original file\":\"Dosarul original\",\"Very high (up to 4K)\":\"Foarte ridicat (până la 4K)\",\"High (up to 1440p)\":\"Ridicat (până la 1440p)\",\"Space-saving (up to 900p)\":\"Economie de spațiu (până la 900p)\",\"Add images from computer\":\"Adăugați imagini de pe computer\",\"Use website cover\":\"Utilizați coperta site-ului\",\"Use first image as cover\":\"Utilizați prima imagine ca acoperire\",\"Covers and local gallery\":\"Coperți și galerie locală\",\"Updating images…\":\"Se actualizează imaginile...\",\"Loading images…\":\"Se încarcă imaginile...\",\"Own image\":\"Imagine proprie\",\"Example image\":\"Imagine exemplu\",\"Current cover\":\"Coperta curentă\",\"Alternative preview\":\"Previzualizare alternativă\",\"Current public image\":\"Imaginea publică curentă\",\"Saving cover…\":\"Se salvează coperta…\",\"Set as cover\":\"Setați ca acoperire\",\"Delete this image from the local gallery?\":\"Ștergeți această imagine din galeria locală?\",\"No images yet.\":\"Nicio imagine încă.\",\"saved locally\":\"salvat local\",\"own images\":\"propriile imagini\",\"Update public previews\":\"Actualizați previzualizările publice\",\"Processing your images…\":\"Procesarea imaginilor dvs....\",\"ALL PAGE SOUNDS OFF\":\"TOATA PAGINA SUNET OPRIT\",\"PAGE SOUNDS ALLOWED\":\"SUNETE DE PAGINĂ PERMISE\",\"Chat/token sounds OFF\":\"Sunete de chat/token OFF\",\"Chat/token sounds ON\":\"Sunete de chat/token ACTIVATE\",\"Reconnecting the player with the new settings…\":\"Se reconecta playerul cu noile setări...\",\"Turn off chat, token and tip sounds completely\":\"Opriți complet sunetele de chat, token și ponturi\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Blochează sunetele paginii de la elementele audio și Web Audio. Fluxul audio rămâne controlabil separat prin „Sound on”.\",\"No alternatives available.\":\"Nu există alternative disponibile.\",\"Similar models and offline alternatives\":\"Modele similare și alternative offline\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Prioritizate prin etichete partajate; nicio identitate sau inferență din contul extern.\",\"Notes & organization\":\"Note și organizare\",\"Images\":\"Imagini\",\"SingleCam sections\":\"Secțiuni SingleCam\",\"Back to original results\":\"Înapoi la rezultatele originale\",\"Private workspace\":\"Spațiu de lucru privat\",\"Chat on/off\":\"Chat activat/dezactivat\",\"Hide similar models\":\"Ascunde modele similare\",\"Show similar models\":\"Afișați modele similare\",\"RoomGrid Home\":\"RoomGrid Acasă\",\"RoomGrid Tags\":\"Etichete RoomGrid\",\"Public catalog unavailable; showing the local library.\":\"Catalog public indisponibil; arătând biblioteca locală.\",\"Drag to resize sidebar\":\"Trageți pentru a redimensiona bara laterală\",\"The workspace could not be saved safely. Export a backup now.\":\"Spațiul de lucru nu a putut fi salvat în siguranță. Exportați acum o copie de rezervă.\",\"Open menu or restore controls\":\"Deschide meniul sau restabilește comenzile\",\"Controls restored\":\"Controalele restaurate\",\"Menu\":\"Meniu\",\"Show toolbar\":\"Afișați bara de instrumente\",\"Tools\":\"Instrumente\",\"Show sidebar\":\"Afișează bara laterală\",\"Restore controls\":\"Restabiliți controalele\",\"Show toolbar, groups, and menus\":\"Afișați bara de instrumente, grupurile și meniurile\",\"Open more tools\":\"Deschideți mai multe instrumente\",\"all watch states\":\"toate stările de ceas\",\"Watch status\":\"Starea ceasului\",\"0 means no viewer limit.\":\"0 înseamnă că nu există limită de spectatori.\",\"Import temporary URLs; they disappear after refresh\":\"Importați adrese URL temporare; ele dispar după reîmprospătare\",\"Filter current group\":\"Filtrați grupul curent\",\"All status\":\"Toate statutul\",\"Hide offline/private\":\"Ascunde offline/privat\",\"Collapse toolbar\":\"Restrângeți bara de instrumente\",\"Windows per page\":\"Windows pe pagină\",\"Custom grid\":\"Grilă personalizată\",\"Choose MultiCam audio routing\":\"Alegeți rutarea audio MultiCam\",\"Audio: manual\":\"Audio: control manual\",\"Audio: solo cam\":\"Audio: camera solo\",\"Audio: follow focus\":\"Audio: urmăriți focalizarea\",\"Auto\":\"Automat\",\"Userscript version\":\"Versiunea Userscript\",\"Open Discover and dashboard\":\"Deschideți Discover și tabloul de bord\",\"Global search and quick commands · Ctrl+K\":\"Căutare globală și comenzi rapide · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Transmiteți în flux sănătate, buget de calitate, sunet, scene și înlocuitori\",\"Control\":\"Controla\",\"Rooms\":\"Camere\",\"Filter\":\"Filtru\",\"Playback\":\"Redare\",\"Stabilizing live preview…\":\"Se stabilizează previzualizarea live...\",\"Recovering live preview…\":\"Se recuperează previzualizarea live...\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"Previzualizarea live este întreruptă pentru scurt timp pentru a preveni o buclă de reîncărcare. Încercați din nou în scurt timp.\",\"Collapse groups\":\"Restrângeți grupurile\",\"Expand/collapse users\":\"Extindeți/restrângeți utilizatorii\",\"Output volume; no live signal level is available for this stream\":\"Volumul de ieșire; nici un nivel de semnal live nu este disponibil pentru acest flux\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Lipiți adrese URL. Sunt acceptate paginile de cameră, m3u8, mp4/webm. Paginile normale vor fi analizate cu cel mai bun efort.\",\"Actual resolution, connection and buffer\":\"Rezoluție reală, conexiune și tampon\",\"Drag to reorder / move group\":\"Trageți pentru a reordona/muta grupul\",\"All time\":\"Tot timpul\",\"Click a model for details.\":\"Faceți clic pe un model pentru detalii.\",\"Most watched groups\":\"Cele mai vizionate grupuri\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Câmpurile contului, cum ar fi data aderării, sunt afișate numai atunci când Chaturbate le expune clar pe aceeași pagină. Scenariul nu inventează valori.\",\"Back\":\"Înapoi\",\"Statistics import failed: \":\"Importul statisticilor a eșuat:\",\"Recent saves\":\"Salvări recente\",\"Rooms per page\":\"Camere pe pagină\",\"Custom rows\":\"Rânduri personalizate\",\"Custom columns\":\"Coloane personalizate\",\"Main width\":\"Lățimea principală\",\"Main height\":\"Înălțimea principală\",\"Modules\":\"Module\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Modulele încep numai pe tipurile de pagini care se potrivesc. Salvarea reîncarcă pagina curentă.\",\"Tools on normal pages\":\"Instrumente pe pagini normale\",\"True audio-level meters\":\"Contoare de nivel audio adevărate\",\"MultiCam control center\":\"Centru de control MultiCam\",\"No active streams yet\":\"Încă nu există fluxuri active\",\"Stream health\":\"Sănătate în flux\",\"Budget off\":\"Buget scos\",\"Economy · 2160p total\":\"Economie · 2160p total\",\"Balanced · 4320p total\":\"Echilibrat · 4320p total\",\"High · 6480p total\":\"Ridicat · 6480p total\",\"Ultra · 8640p total\":\"Ultra · 8640p în total\",\"Quality budget is off; global and per-camera limits still apply.\":\"Bugetul de calitate este oprit; limitele globale și per cameră încă se aplică.\",\"Quality budget\":\"Buget de calitate\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Distribuie calitatea între camerele active. O calitate explicită per cameră câștigă întotdeauna.\",\"Give the focused cam double weight\":\"Dați camei focalizate greutate dublă\",\"Manual per camera\":\"Manual pentru fiecare cameră\",\"Solo: one camera only\":\"Solo: o singură cameră\",\"Follow focused camera\":\"Urmăriți camera focalizată\",\"Audio management\":\"Management audio\",\"Routing never overwrites saved mute states or individual volume.\":\"Rutarea nu suprascrie niciodată stările de sunet salvate sau volumul individual.\",\"Mute all\":\"Dezactivați totul\",\"Restore saved audio\":\"Restaurați sunetul salvat\",\"Scene name\":\"Numele scenei\",\"Focus\":\"Concentrează-te\",\"Grid\":\"Grilă\",\"cams\":\"came\",\"Load\":\"Încărcare\",\"No scenes saved for this workspace yet.\":\"Nu s-a salvat încă nicio scenă pentru acest spațiu de lucru.\",\"Scenes\":\"Scene\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Salvați sloturile curente, aspectul și rutarea audio pentru restaurarea cu un singur clic.\",\"Save current view as scene\":\"Salvați vizualizarea curentă ca scenă\",\"All saved online models\":\"Toate modelele online salvate\",\"Replacement models\":\"Modele de schimb\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Modelele preferate sunt încercate în ordinea listei; numai atunci sunt folosite modele online din grupul selectat. Misiunile manuale câștigă întotdeauna.\",\"Replace offline slots automatically\":\"Înlocuiți automat sloturile offline\",\"Delay (seconds)\":\"Întârziere (secunde)\",\"Candidate group\":\"Grupul de candidat\",\"Preferred replacements (multiple)\":\"Înlocuitori preferati (multiple)\",\"Replacement rule saved\":\"Regula de înlocuire a fost salvată\",\"Save replacement rule\":\"Salvați regula de înlocuire\",\"Resolve duplicate shortcuts first:\":\"Rezolvați mai întâi comenzile rapide duplicate:\",\"Name, note or own tag…\":\"Nume, notă sau etichetă proprie...\",\"Camera menu\":\"Meniul camerei\",\"Camera menu · quality and sound\":\"Meniul camerei · calitate și sunet\",\"Close menu\":\"Închide meniul\",\"View and window\":\"Vedere și fereastră\",\"Quality and image\":\"Calitate si imagine\",\"More actions\":\"Mai multe acțiuni\",\"Currently main\":\"Momentan principal\",\"Set as main\":\"Setați ca principal\",\"Pin live preview\":\"Fixați previzualizarea live\",\"Volume multiplier for this cam\":\"Multiplicator de volum pentru această cameră\",\"Reset individual volume to 100%\":\"Resetați volumul individual la 100%\",\"Individual volume\":\"Volumul individual\",\"Listen to this cam only\":\"Ascultă doar această cameră\",\"Set quality for this camera only\":\"Setați calitatea numai pentru această cameră\",\"Automatic (global/adaptive)\":\"Automat (global/adaptativ)\",\" · available\":\"· disponibil\",\"Quality for this camera\":\"Calitate pentru această cameră\",\"Hold for original\":\"Țineți pentru original\",\"Vivid\":\"Vioi\",\"Image adjustments\":\"Ajustări de imagine\",\"More tools\":\"Mai multe instrumente\",\"Interface\":\"Interfață\",\"Show top controls\":\"Afișează comenzile de sus\",\"Collapse top controls\":\"Restrânge comenzile de sus\",\"Show groups\":\"Afișați grupuri\",\"Open workspace in new window\":\"Deschide spațiul de lucru într-o fereastră nouă\",\"Control center\":\"Centru de control\",\"Choose the data to export\":\"Alegeți datele de exportat\",\"Preview the file and conflicts before applying it\":\"Previzualizează fișierul și conflictele înainte de a-l aplica\",\"Help\":\"Ajutor\",\"Changes in this version\":\"Modificări în această versiune\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Spații de lucru, bibliotecă de modele, importuri sigure, design global, pagini normale extinse, Descoperire/tablou de bord extins și controale de calitate/imagine a playerului.\",\"Muted\":\"Dezactivat\",\"Performance limit reached\":\"Limita de performanță a fost atinsă\",\"Drag to resize main width\":\"Trageți pentru a redimensiona lățimea principală\",\"Drag to resize main height\":\"Trageți pentru a redimensiona înălțimea principală\",\"Stable\":\"Stabil\",\"Unstable\":\"Instabil\",\"Refresh all cameras\":\"Actualizează toate camerele\",\"Reconnect current models\":\"Reconectați modelele actuale\",\"Switch to grid view\":\"Comutați la vizualizarea grilă\",\"Switch to focus view\":\"Comutați la vizualizarea focalizării\",\"Manage workspaces\":\"Gestionați spațiile de lucru\",\"Create, duplicate, archive and device layouts\":\"Creați, duplicați, arhivați și machete dispozitive\",\"Open model library\":\"Deschideți biblioteca de modele\",\"Notes, tags, ratings and bulk actions\":\"Note, etichete, evaluări și acțiuni în bloc\",\"Design options\":\"Opțiuni de proiectare\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Stație de lucru multifuncțională pentru camere, înregistrare, alerte, capturi de ecran, reconectari și instrumente de date.\",\"Username ↵\":\"Nume de utilizator ↵\",\"Search rooms\":\"Căutați camere\",\"Invalid username\":\"Nume de utilizator invalid\",\"Hide offline\":\"Ascunde offline\",\"Hide private\":\"Ascunde privat\",\"By status\":\"După statut\",\"By name\":\"După nume\",\"By added time\":\"Cu timpul adăugat\",\"By watch time\":\"După timpul de vizionare\",\"By sessions\":\"Pe sesiuni\",\"By last watched\":\"După ultima vizionare\",\"By first watched\":\"Prin prima vedere\",\"By viewer count\":\"După numărul de spectatori\",\"By last online\":\"Până la ultimul online\",\"Advanced filters\":\"Filtre avansate\",\"Watched before\":\"Privit înainte\",\"Currently in view\":\"Momentan la vedere\",\"Include tag\":\"Includeți eticheta\",\"Exclude tag\":\"Excludeți eticheta\",\"Minimum watch time (minutes)\":\"Timp minim de vizionare (minute)\",\"Watched within days\":\"Vizionat în câteva zile\",\"Refresh all\":\"Reîmprospătați totul\",\"Online alerts\":\"Alerte online\",\"Offline alerts\":\"Alerte offline\",\"Desktop notification + card flash when a model goes online\":\"Notificare desktop + card flash atunci când un model este online\",\"Desktop notification + card flash when a model goes offline\":\"Notificare desktop + card flash când un model este offline\",\"Dark mode\":\"Modul întunecat\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Utilizați o temă de interfață întunecată. Fluxul video și audio sunt neschimbate.\",\"Dark mode enabled\":\"Modul întunecat activat\",\"Light mode enabled\":\"Modul de lumină activat\",\"Mute token/tip sounds\":\"Dezactivați sunetele indicative/sfaturi\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Blochează doar sunetele de notificare pentru sfaturi Chaturbate. Streamul audio rămâne activat.\",\"Token/tip sounds muted\":\"Sunetele simbolului/sfatului sunt dezactivate\",\"Token/tip sounds enabled\":\"Sunete cu simboluri/bacșișuri activate\",\"Clean\":\"Curat\",\"Clean mode on\":\"Modul Curățare activat\",\"Exit clean mode\":\"Ieși din modul curățare\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Ascundeți temporar toate comenzile și suprapunerile. Comandă rapidă: Alt+P / Alt+C. Apăsați Esc pentru a ieși.\",\"Window first\":\"Mai întâi fereastra\",\"Window-first mode on\":\"Modul fereastra mai întâi activat\",\"Exit window-first\":\"Ieșiți mai întâi din fereastră\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Ascundeți shell-ul aplicației și păstrați ferestrele camerei vizibile. Comandă rapidă: Alt+V.\",\"Show thumbnails\":\"Afișați miniaturile\",\"Hide thumbnails\":\"Ascunde miniaturile\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Afișați sau ascundeți șina pentru miniaturi în modul Focus. Comandă rapidă: Alt+T.\",\"Choose Focus rooms\":\"Alege camere Focus\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Alegeți camerele pentru modul Focus. Prima cameră selectată este ecranul principal; utilizați ↑/↓ pentru a seta ordinea ferestrelor secundare.\",\"Main screen\":\"Ecranul principal\",\"Right window\":\"Fereastra din dreapta\",\"Bottom window\":\"Fereastra de jos\",\"Empty slot\":\"Slot gol\",\"Drag this model to a Focus slot\":\"Trageți acest model într-un slot Focus\",\"Choose model for this slot\":\"Alegeți modelul pentru acest slot\",\"Select a saved model for this exact Focus slot.\":\"Selectați un model salvat pentru acest slot Focus exact.\",\"Choose model for this window\":\"Alegeți modelul pentru această fereastră\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Selectați un model salvat pentru această fereastră grilă exactă. Modelele existente rămân salvate.\",\"Remove from this grid window\":\"Eliminați din această fereastră grilă\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Doar acest slot este golit. Modelul salvat rămâne în spațiul dvs. de lucru.\",\"Show in grid window\":\"Afișați în fereastra grilă\",\"Choose the exact active grid window for this model.\":\"Alegeți fereastra de grilă activă exactă pentru acest model.\",\"Current\":\"Curent\",\"Model removed from this grid window.\":\"Model eliminat din această fereastră grilă.\",\"No saved models in this group.\":\"Nu există modele salvate în acest grup.\",\"Secondary window\":\"Fereastra secundara\",\"Use current page automatically\":\"Utilizați pagina curentă automat\",\"Clear custom selection\":\"Ștergeți selecția personalizată\",\"Apply Focus layout\":\"Aplicați aspectul Focus\",\"No rooms are available in the current group and filter.\":\"Nu sunt disponibile camere în grupul și filtrul actual.\",\"Fit: full image\":\"Potrivire: imagine completă\",\"Fit: fill window\":\"Fit: umple fereastra\",\"Switch between full image and cropped fill.\":\"Comutați între imaginea completă și umplerea decupată.\",\"Playback settings\":\"Setări de redare\",\"Max stream quality\":\"Calitate maximă a fluxului\",\"Auto / no cap\":\"Auto / fără limită\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Comandă + roată zoom\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Măriți un card cu Ctrl/Command + rotița mouse-ului, trageți în timp ce este mărit, faceți dublu clic pentru a reseta.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Ieși din modul curățare. Comandă rapidă: Alt+P / Alt+C sau Esc.\",\"Clean · Alt+P/C / Esc\":\"Curățare · Alt+P/C / Esc\",\"(synced from another tab)\":\"(sincronizat dintr-o altă filă)\",\"Type a username and press Enter. Shortcut: /\":\"Introduceți un nume de utilizator și apăsați Enter. Comandă rapidă: /\",\"Filter visible rooms by username\":\"Filtrați camerele vizibile după numele de utilizator\",\"Show / hide group sidebar\":\"Afișează / ascunde bara laterală a grupului\",\"Global volume. Per-room mute is still respected.\":\"Volumul global. Sunetul per cameră este încă respectat.\",\"Base tile size in grid view\":\"Dimensiunea plăcii de bază în vizualizarea grilă\",\"Sort rooms in the current group\":\"Sortați camerele din grupul curent\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Înălțimea ecranului principal în modul Focus. De asemenea, puteți trage separatorul.\",\"Main-screen aspect ratio in Focus mode\":\"Raportul de aspect al ecranului principal în modul Focus\",\"Thumbnail width in Focus mode\":\"Lățimea miniaturii în modul Focus\",\"Show every saved room. The remove button deletes globally in this view.\":\"Afișați fiecare cameră salvată. Butonul de eliminare se șterge global în această vizualizare.\",\"Create an isolated group\":\"Creați un grup izolat\",\"More tools and maintenance\":\"Mai multe instrumente și întreținere\",\"GROUPS\":\"GRUPURI\",\"All saved\":\"Toate salvate\",\"Default\":\"Implicit\",\"Online now\":\"Online acum\",\"New group name:\":\"Nou nume de grup:\",\"Rename\":\"Redenumiți\",\"Rename to:\":\"Redenumiți în:\",\"Delete group\":\"Șterge grupul\",\"Live preview\":\"Previzualizare live\",\"Loading preview…\":\"Se încarcă previzualizarea...\",\"Private or away\":\"Privat sau departe\",\"Preview could not be loaded\":\"Previzualizarea nu a putut fi încărcată\",\"Toggle mute\":\"Comutați dezactivarea sunetului\",\"Resume\":\"Reluați\",\"Screenshot current frame\":\"Captură de ecran cadru curent\",\"Record current card\":\"Înregistrați cardul curent\",\"Stop recording\":\"Opriți înregistrarea\",\"Remove from current group\":\"Eliminați din grupul curent\",\"Delete saved room\":\"Șterge camera salvată\",\"Mirror image\":\"Imagine în oglindă\",\"Flip image\":\"Întoarce imaginea\",\"Rotate left\":\"Rotiți la stânga\",\"Rotate right\":\"Rotiți la dreapta\",\"Reset view\":\"Resetați vizualizarea\",\"Copy username\":\"Copiați numele de utilizator\",\"Add favorite\":\"Adăugați favorit\",\"Move to favorites only\":\"Mutați numai la favorite\",\"Copied\":\"Copiat\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Captura de ecran a eșuat. Browserul/CORS poate bloca desenarea acestui flux.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"Înregistrarea este locală și salvează acest videoclip cu sunetul corespunzător atunci când browserul îl expune. Folosiți-l numai atunci când aveți permisiunea de a salva acest conținut. Continua?\",\"Recording started\":\"Înregistrarea a început\",\"Recording is not supported for this video/browser\":\"Înregistrarea nu este acceptată pentru acest videoclip/browser\",\"Recording segment saved\":\"Segment de înregistrare salvat\",\"Final recording segment saved\":\"Segmentul final de înregistrare a fost salvat\",\"Recording paused; waiting for the stream to return\":\"Înregistrare întreruptă; aşteptând să se întoarcă pârâul\",\"Recording resumed\":\"Înregistrarea a fost reluată\",\"Recording paused; click to stop\":\"Înregistrare întreruptă; faceți clic pentru a opri\",\"No recording data to save\":\"Nu există date de înregistrare de salvat\",\"Recording settings saved\":\"Setările de înregistrare au fost salvate\",\"Segment length in minutes (1-180):\":\"Lungimea segmentului în minute (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Rata de biți video în Mbps (0,5-20):\",\"Warn before leaving while recording\":\"Avertizați înainte de a pleca în timpul înregistrării\",\"Recording is still active. Leave anyway?\":\"Înregistrarea este încă activă. Pleacă oricum?\",\"Recording center\":\"Centru de înregistrare\",\"No active recordings\":\"Nu există înregistrări active\",\"Waiting for source\":\"Se așteaptă sursa\",\"Saved segments\":\"Segmente salvate\",\"Duration\":\"Durata\",\"Bitrate\":\"Rata de biți\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Format: MP4 atunci când este acceptat de acest browser; altfel, cel mai bine suportat fallback.\",\"Record current page\":\"Înregistrați pagina curentă\",\"Record current group\":\"Înregistrați grupul curent\",\"Record online rooms\":\"Înregistrați camerele online\",\"Stop all recordings\":\"Opriți toate înregistrările\",\"Show recording only\":\"Afișați numai înregistrarea\",\"Show all rooms\":\"Afișați toate camerele\",\"Open current page rooms\":\"Deschide sălile paginii curente\",\"Move current page to group\":\"Mutați pagina curentă în grup\",\"Layout settings\":\"Setări de aspect\",\"Recording settings\":\"Setări de înregistrare\",\"Save settings\":\"Salvați setările\",\"Config backups\":\"Configurare copii de rezervă\",\"No config backups found\":\"Nu s-au găsit copii de rezervă de configurare\",\"Online/offline history\":\"Istoricul online/offline\",\"Group rules\":\"Regulile de grup\",\"Keep favorites first\":\"Păstrați mai întâi favoritele\",\"Temporary URL manager\":\"Manager de adrese URL temporare\",\"Save current temporary URLs\":\"Salvați adresele URL temporare curente\",\"No temporary URLs\":\"Nu există adrese URL temporare\",\"Share workspace\":\"Partajați spațiul de lucru\",\"Share current rooms and layout\":\"Partajați camerele și aspectul actual\",\"Copy share link\":\"Copiați linkul de distribuire\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Acest link restaurează setările pentru camere, grupuri și aspect într-o altă filă de stație de lucru.\",\"Shared workspace imported\":\"A fost importat spațiu de lucru comun\",\"Shared workspace link is invalid\":\"Linkul pentru spațiul de lucru partajat este nevalid\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Faceți clic pe un câmp și apăsați pe noua comandă rapidă. Backspace șterge acel câmp.\",\"Reset shortcuts\":\"Resetați comenzile rapide\",\"Focus add box\":\"Focalizează caseta de adăugare\",\"Grid view\":\"Vizualizare grilă\",\"Focus view\":\"Vedere focalizată\",\"Clean mode\":\"Modul curat\",\"Window-first mode\":\"Modul fereastra mai întâi\",\"Toggle thumbnails\":\"Comutați miniaturile\",\"Auto-detecting…\":\"Detectare automată...\",\"No rooms in this group\":\"Nu există camere în acest grup\",\"Add a username at the top, switch group, or clear filters\":\"Adăugați un nume de utilizator în partea de sus, schimbați grupul sau ștergeți filtrele\",\"Open workstation\":\"Deschideți stația de lucru\",\"Open here (replace page)\":\"Deschide aici (înlocuiește pagina)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Înlocuiți această pagină cu stația de lucru? Vă puteți întoarce prin browser.\",\"View saved list\":\"Vizualizați lista salvată\",\"Collapse dock\":\"Colaps docul\",\"No room detected\":\"Nu a fost detectată nicio cameră\",\"Open here\":\"Deschide aici\",\"Add room\":\"Adăugați cameră\",\"Remove room\":\"Eliminați camera\",\"Record in workstation\":\"Înregistrați în stația de lucru\",\"Screenshot video\":\"Captură de ecran video\",\"Play / pause\":\"Redare / pauză\",\"Mute / unmute\":\"Dezactivați / dezactivați sunetul\",\"No playable video found on this page\":\"Nu s-a găsit niciun videoclip redabil pe această pagină\",\"Recording intent queued in workstation\":\"Intenția de înregistrare a fost pusă în coadă în stația de lucru\",\"Added\":\"Adăugat\",\"Already exists\":\"Deja există\",\"Failed\":\"A eșuat\",\"Add to workstation\":\"Adăugați la stația de lucru\",\"Already in workstation — click to remove\":\"Deja în stația de lucru - faceți clic pentru a elimina\",\"Model online\":\"Modelul este online\",\"Model offline\":\"Modelul este offline\",\"Show fewer\":\"Arată mai puține\",\"No measurable audio at the moment\":\"Nu există sunet măsurabil în acest moment\",\"Playback paused\":\"Redarea a fost întreruptă\",\"Audio measurement unavailable or blocked\":\"Măsurarea audio nu este disponibilă sau este blocată\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Permisiunea de notificare a fost refuzată. Blițul cardului încă funcționează.\\n\\nÎl puteți activa în setările browserului.\",\"Batch add\":\"Adăugarea lotului\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Inserați numele de utilizator, unul pe linie sau separate prin spații/virgule:\",\"Cancel\":\"Anulează\",\"Change the language of RoomGrid and the website\":\"Schimbați limba RoomGrid și a site-ului web\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Schimbați limba RoomGrid. Chaturbate rămâne în limba actuală a site-ului, deoarece nu este disponibilă nicio limbă oficială a site-ului.\",\"About\":\"Despre\",\"Export config\":\"Exportă configurația\",\"Export usernames.txt\":\"Exportați nume de utilizator.txt\",\"Copy usernames\":\"Copiați numele de utilizator\",\"Unmute all\":\"Dezactivați sunetul pe toate\",\"⏸ Pause visible\":\"⏸ Pauză vizibilă\",\"Resume visible\":\"Reluarea vizibilă\",\"Following\":\"Urmând\",\"Sync followed models now\":\"Sincronizați modelele urmărite acum\",\"✓ Auto-sync followed models\":\"✓ Sincronizare automată a modelelor urmate\",\"☐ Auto-sync followed models\":\"☐ Sincronizarea automată a modelelor urmate\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importați modelele Chaturbate urmărite în prezent în grupul de urmărire persistent. Doar acel grup este sincronizat.\",\"Syncing followed models…\":\"Se sincronizează modelele urmărite...\",\"Automatic Following sync disabled\":\"Sincronizarea automată a urmăririi este dezactivată\",\"Automatic Following sync enabled\":\"Sincronizarea automată a urmăririi este activată\",\"Following sync needs an active Chaturbate login\":\"Următoarea sincronizare necesită o conectare activă la Chaturbate\",\"Following sync failed safely; saved workspace data was not changed\":\"Sincronizarea următoare a eșuat în siguranță; datele salvate ale spațiului de lucru nu au fost modificate\",\"Enable dark mode\":\"Activați modul întunecat\",\"Disable dark mode\":\"Dezactivați modul întunecat\",\"Status history\":\"Istoricul stării\",\"Temporary URLs\":\"URL-uri temporare\",\"Shortcut panel\":\"Panoul de comenzi rapide\",\"Toggle video fit\":\"Comutați încadrarea video\",\"Shortcuts / hints\":\"Comenzi rapide / sugestii\",\"Ranking\":\"Clasament\",\"Model details\":\"Detalii model\",\"My profile\":\"Profilul meu\",\"Loading local statistics…\":\"Se încarcă statisticile locale...\",\"No watch data yet. Statistics start with this version.\":\"Nu există date de ceas încă. Statisticile încep cu această versiune.\",\"Statistics are disabled.\":\"Statisticile sunt dezactivate.\",\"Enable statistics\":\"Activați statisticile\",\"Disable statistics\":\"Dezactivați statisticile\",\"Export statistics\":\"Export statistici\",\"Delete all statistics\":\"Ștergeți toate statisticile\",\"Delete this model data\":\"Ștergeți datele acestui model\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Ștergeți toate statisticile privind ceasurile colectate local? Spațiul tău de lucru rămâne neatins.\",\"Tracking since\":\"Urmărirea de atunci\",\"Models watched\":\"Modele vizionate\",\"Watch time\":\"Timp de vizionare\",\"Most watched tags\":\"Cele mai vizionate etichete\",\"Most watched hours\":\"Cele mai vizionate ore\",\"Observed online\":\"Observat online\",\"Public bio\":\"Biografie publică\",\"Public links\":\"Linkuri publice\",\"No public profile data cached yet.\":\"Nu au fost încă stocate în cache date publice de profil.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Contează doar cărțile vizibile, cu joc activ. Previzualizările cu mouse-ul nu sunt luate în considerare.\",\"Statistics imported\":\"Statistici importate\",\"Statistics deleted\":\"Statisticile au fost șterse\",\"Statistics exported\":\"Statistici exportate\",\"Local observation only\":\"Doar observație locală\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Nu sunt colectate mesaje private, jetoane, plăți, parole sau urmărire externă.\",\"Not enough observations for a time estimate\":\"Nu sunt suficiente observații pentru o estimare a timpului\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Treceți cu mouse-ul pe orice buton/comandă pentru a vedea indiciu. Comenzile rapide comune pot fi editate în panoul de comenzi rapide.\\n\\nValori implicite:\\n/ Focalizează introducerea numelui de utilizator\\nr Reîmprospătați toate\\ng Vizualizare grilă\\nf Vedere focalizată\\nAlt+P sau Alt+C Mod Curățare activat/dezactivat\\nAlt+Shift+C Centru de înregistrare\\nAlt+Shift+R Înregistrați pagina curentă\\nEsc Ieșiți din modul curat / ecran complet\\nSpațiu Întrerupeți/reluați ecranul principal în vizualizarea Focus\\n←/→ sau [/ ] Comutați ecranul principal în vizualizarea Focus\\nFaceți dublu clic pe card Fullscreen\",\"Visible windows paused\":\"Ferestrele vizibile au fost întrerupte\",\"Visible windows resumed\":\"Ferestrele vizibile au fost reluate\",\"Import config\":\"Importă configurația\",\"Repair saved data\":\"Reparați datele salvate\",\"Saved data has been normalized.\":\"Datele salvate au fost normalizate.\",\"Clear all data\":\"Ștergeți toate datele\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Aceasta va șterge TOATE camerele, grupurile și setările. Continua?\",\"English\":\"engleză\",\"Deutsch\":\"Germană\",\"Author\":\"autor\",\"License\":\"Licență\",\"If this script saves you time…\":\"Dacă acest script vă economisește timp...\",\"Tip in ETH\":\"Sfat în ETH\",\"ETH address\":\"adresa ETH\",\"Copy\":\"Copiere\",\"Continue loading the complete catalog\":\"Continuați să încărcați catalogul complet\",\"Continuing recording\":\"Înregistrarea continuă\",\"current matches\":\"meciurile curente\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Personalizați numele Camclient, marcajul, sloganul și sigla locală. Siglele rămân în acest browser.\",\"Dim\":\"Dim\",\"Discover something new\":\"Descoperă ceva nou\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Benzile de recomandare distincte vă ajută să găsiți ceva nou în loc să repetați catalogul paginii de pornire.\",\"Do not enlarge\":\"Nu măriți\",\"Eco\":\"Eco\",\"Ember\":\"Ember\",\"Enable meter\":\"Activați contorul\",\"Fast starters\":\"Începători rapid\",\"Fill and crop\":\"Umpleți și tăiați\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Pe baza preferințelor, etichetelor și activității de vizualizare salvate local. Fiecare alegere își arată motivul.\",\"Black\":\"negru\",\"Brave exposed no recordable video track.\":\"Brave nu a expus nicio piesă video care poate fi înregistrată.\",\"Brave or the video source blocked frame capture.\":\"Brave sau sursa video a blocat captarea cadrului.\",\"Browser exposed no audio track\":\"Browserul nu a afișat nicio pistă audio\",\"Camclient as the real homepage\":\"Camclient ca adevărată pagină de pornire\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient atribuie automat aceste exemple transparente etichetelor comune. Utilizați butonul de imagine de pe orice etichetă pentru a alege un exemplu, pentru a încărca propriul PNG/WebP/SVG sau pentru a ascunde imaginea.\",\"Choose a Camclient example or upload your own transparent image.\":\"Alegeți un exemplu Camclient sau încărcați propria imagine transparentă.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Alegeți un PNG, JPEG, WebP sau SVG sigur mai mic de 1,1 MB.\",\"Choose artwork for tag\":\"Alegeți opera de artă pentru etichetă\",\"Click to enable meter\":\"Faceți clic pentru a activa contorul\",\"Close more actions\":\"Închideți mai multe acțiuni\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Culorile, tipografia, spațierea, colțurile, umbrele și fundalul se aplică imediat la Setări, spațiul de lucru, Discover și biblioteca de modele.\",\"Contrast\":\"Contrast\",\"export\":\"export\",\"Chat\":\"Chat\",\"Desktop\":\"Desktop\",\"Compact\":\"Compact\",\"Camclient\":\"Camclient\",\"Filter suggestions\":\"Filtrați sugestiile\",\"Fit completely\":\"Se potrivește complet\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Pentru transparență, utilizați PNG, WebP sau SVG sigur și alegeți Transparent ca fundal pentru logo. JPEG nu are transparență. Stretch folosește independent lățimea și înălțimea alese.\",\"For you\":\"Pentru tine\",\"gallery\":\"galerie\",\"HD\":\"HD\",\"Hue\":\"Hue\",\"Inner spacing\":\"Spațiere interioară\",\"Inspiration, not repetition\":\"Inspirație, nu repetiție\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Acesta permite utilizarea, modificarea și distribuirea software-ului. Notificările privind drepturile de autor și licența trebuie să rămână, iar software-ul vine fără garanție. Camclient este creat de Emberhost.\",\"Live for a short time and already drawing public activity.\":\"Trăiește pentru o perioadă scurtă de timp și atrage deja activitate publică.\",\"Live stream audio activity\":\"Activitate audio în flux live\",\"Local watch\":\"Ceas local\",\"Accent gradient\":\"Graent de accent\",\"Active filters\":\"Filtre active\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Adăugați propriile imagini sau utilizați butonul de captură de ecran din player. Capturile de ecran pot intra automat în galerie și pot fi folosite direct ca coperta modelului.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Toate filtrele paginii de pornire, aspectul, previzualizarea și setările de paginare sunt stocate în seiful de setări stabil și supraviețuiesc actualizărilor de script.\",\"All live models\":\"Toate modelele live\",\"All suggestions\":\"Toate sugestiile\",\"Almost silent\":\"Aproape tăcut\",\"Always active\":\"Întotdeauna activ\",\"AND\":\"ȘI\",\"Aurora\":\"Aurora\",\"Automatic example\":\"Exemplu automat\",\"Automatic segments\":\"Segmente automate\",\"Automatically loading the complete catalog…\":\"Se încarcă automat catalogul complet...\",\"Available recording data was saved.\":\"Datele de înregistrare disponibile au fost salvate.\",\"Back online\":\"Înapoi online\",\"Local\":\"Local\",\"Import\":\"Import\",\"Gradient\":\"Gradient\",\"Audio\":\"Audio\",\"Logo background\":\"Fundal logo\",\"Logo height\":\"Înălțimea logo-ului\",\"Logo width\":\"Lățimea logo-ului\",\"Loud\":\"tare\",\"Measured even while output is muted\":\"Măsurat chiar și atunci când ieșirea este dezactivată\",\"Micro\":\"Micro\",\"Middle-click or Ctrl+click opens a new tab\":\"Faceți clic cu butonul din mijloc sau Ctrl+clic deschide o filă nouă\",\"Models you have visited locally and that are public again now.\":\"Modele pe care le-ați vizitat local și care sunt din nou publice acum.\",\"Mono\":\"Mono\",\"New discoveries\":\"Noi descoperiri\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nou în catalog, recent văzut de Camclient sau tocmai a început să difuzeze.\",\"No audio track\":\"Fără pistă audio\",\"No filters active · all matching live models are shown\":\"Nu există filtre active · sunt afișate toate modelele live care se potrivesc\",\"No recommendations match the active filters.\":\"Nicio recomandare nu se potrivește cu filtrele active.\",\"Screenshot format\":\"Format captură de ecran\",\"Screenshot resolution\":\"Rezoluția capturii de ecran\",\"Screenshot unavailable\":\"Captură de ecran indisponibilă\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Capturile de ecran sunt descărcate și pot intra și în galeria locală de modele.\",\"segment(s)\":\"segment(e)\",\"Show no image\":\"Nu afișa nicio imagine\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Afișează activitatea în pista audio din flux, indiferent dacă ieșirea playerului este dezactivată.\",\"Source ended\":\"Sursa s-a încheiat\",\"Source resolution\":\"Rezoluție sursă\",\"Start recording\":\"Începeți înregistrarea\",\"Starter pick · popular now\":\"Alegere pentru început · populară acum\",\"No recording data\":\"Nu există date de înregistrare\",\"NOT\":\"NU\",\"Off\":\"Off\",\"OFF\":\"OFF\",\"Offline\":\"Offline\",\"Only new\":\"Numai nou\",\"Open Camclient homepage\":\"Deschideți pagina de pornire Camclient\",\"OR\":\"SAU\",\"part\":\"parte\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Alegeri personale, noi descoperiri, camere live populare și modele pe care nu le-ați vizitat niciodată — clar separate de catalogul complet al paginii de pornire.\",\"Picture-in-Picture\":\"Picture-in-Picture\",\"Place\":\"Locul\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP și SVG sigur acceptă transparența. Imaginile sunt redimensionate local la 144 × 144 pixeli și salvate cu setările dvs.\",\"Popular now\":\"Popular acum\",\"Public live models with no local watch time in Camclient.\":\"Modele publice live fără oră locală de vizionare în Camclient.\",\"Quiet\":\"Liniste\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Clasat după numărul curent de spectatori publici; datele de catalog nu sunt stocate permanent.\",\"Recording is still active.\":\"Înregistrarea este încă activă.\",\"Recording quality\":\"Calitatea inregistrarii\",\"Recording stopped\":\"Înregistrarea sa oprit\",\"Recording unavailable\":\"Înregistrare indisponibilă\",\"Reset all\":\"Resetați toate\",\"RoomGrid Model library\":\"Biblioteca de modele RoomGrid\",\"RoomGrid My user\":\"RoomGrid Utilizatorul meu\",\"RoomGrid Statistics\":\"Statistici RoomGrid\",\"Save to model gallery\":\"Salvați în galeria de modele\",\"Saved segment\":\"Segment salvat\",\"Screenshot\":\"Captură de ecran\",\"Screenshot failed\":\"Captura de ecran a eșuat\",\"Recent\":\"Recent\",\"Secret\":\"Secret\",\"Solid\":\"Solid\",\"Online\":\"Online\",\"Similar\":\"Similar\",\"Model\":\"Model\",\"Sepia\":\"Sepia\",\"Remix\":\"Remix\",\"Manual\":\"Manual\",\"Static\":\"Static\",\"中文\":\"中文\",\"Stop and save\":\"Oprește-te și salvează\",\"Stretch\":\"Întindere\",\"Tag artwork and emoji\":\"Etichetați opera de artă și emoji\",\"Tag artwork editor\":\"Etichetați editorul de lucrări de artă\",\"The compressed image is still too large.\":\"Imaginea comprimată este încă prea mare.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"Antetul afișează doar Acasă, Discover, Tag Explorer, Statistics Center, Model Library, My User și Settings. Fiecare zonă de configurare este disponibilă ca subpagină Setări.\",\"The image could not be decoded.\":\"Imaginea nu a putut fi decodificată.\",\"The image could not be read.\":\"Imaginea nu a putut fi citită.\",\"The image format is unsafe or unsupported.\":\"Formatul imaginii este nesigur sau neacceptat.\",\"The lanes respect your active search and filter settings.\":\"Benzile respectă setările dvs. active de căutare și filtrare.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"Înregistrarea este salvată numai pe acest dispozitiv. Continuați numai dacă aveți permisiunea de a salva acest conținut.\",\"The video has no usable frame yet.\":\"Videoclipul nu are încă un cadru utilizabil.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Nu există încă suficiente date despre preferințele locale, așa că acestea sunt alegeri transparente de pornire bazate pe popularitatea actuală.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Aceste setări controlează aspectul paginii de pornire Camclient, paginarea, informațiile despre model, filtrele și performanța.\",\"Total\":\"Total\",\"Unified main navigation\":\"Navigare principală unificată\",\"Unvisited\":\"Nevizitat\",\"Upload own image\":\"Încărcați propria imagine\",\"URL\":\"URL\",\"Use as model cover\":\"Utilizați ca husă pentru model\",\"Use the complete search, filters, and sorting for every current result.\":\"Utilizați căutarea completă, filtrele și sortarea pentru fiecare rezultat curent.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Vizitarea paginii de pornire Chaturbate deschide întotdeauna pagina de pornire Camclient completă. Fiecare zonă principală rămâne în aceeași interfață personalizată.\",\"Waiting for audio\":\"În așteptarea sunetului\",\"What does the MIT license mean?\":\"Ce înseamnă licența MIT?\",\"White\":\"alb\",\"Your discovery mix\":\"Mixul tău de descoperiri\",\"Strict\":\"Strict\",\"Text\":\"Text\",\"Transparent\":\"Transparent\",\"Trans\":\"Trans\",\"Trigger\":\"Trigger\",\"Windows\":\"Windows\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Descoperă camere publice live\",\"all groups\":\"toate grupurile\",\"at least one group\":\"cel puțin un grup\",\"no measurements yet\":\"încă nu există măsurători\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] în urmă\",\"[[ROOMGRID_VALUE_0]] preview\":\"[[ROOMGRID_VALUE_0]] previzualizare\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"Profilul @[[ROOMGRID_VALUE_0]] a fost salvat\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Detalii model pentru @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] a urmat modelele importate\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"[[ROOMGRID_VALUE_0]] Elementele din Colecția mea au fost recunoscute\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"Sunt vizibile [[ROOMGRID_VALUE_0]] din [[ROOMGRID_VALUE_1]] modele încărcate.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Filtru de grup activ: [[ROOMGRID_VALUE_0]] · se potrivesc [[ROOMGRID_VALUE_1]] din [[ROOMGRID_VALUE_2]] carduri vizibile ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Coperta personalizată · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Coperta salvată local · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Exemple de imagini și coperți ([[ROOMGRID_VALUE_0]] salvate)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Stocat permanent pe acest dispozitiv · cel mai recent [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Imagine proprie · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Salvat [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Prima dată văzut: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Ultima dată văzut: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Ultima live: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Apariții: [[ROOMGRID_VALUE_0]]× · primul [[ROOMGRID_VALUE_1]] · cel mai recent [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Ora tipică: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Tehnic: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] observații locale în direct, cel mult una la fiecare 5 minute\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] din timpul de vizionare anterioară ca o estimare prudentă\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Eliminați eticheta [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Gestionați grupuri personalizate · atribuite [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Ștergeți grupul „[[ROOMGRID_VALUE_0]]”? Modelele vor rămâne în biblioteca dvs.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Grupul „[[ROOMGRID_VALUE_0]]” a fost șters\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] grupuri · posibilă selecție multiplă\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] etichete salvate\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Eliminați filtrul [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] potriviri\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtre active\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Informațiile de profil public pot fi încărcate prin „Toate detaliile”.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Organizat personal din: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Catalogat automat din: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Ultima activitate: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Apariții: [[ROOMGRID_VALUE_0]]× · ultima înregistrare [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Descoperit: [[ROOMGRID_VALUE_0]] · măsurători de calitate: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Informații de profil: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] arhivat\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] restaurat\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Pagina [[ROOMGRID_VALUE_0]] din [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modele\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] salvat\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] a urmărit\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] activ · crește automat\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] modele · [[ROOMGRID_VALUE_1]] sesiuni\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favorite\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] modele distincte\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] previzualizări\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Previzualizare/vizualizare rapidă asupra modelelor [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] zile\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"[[ROOMGRID_VALUE_0]]-zile\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] a urmărit în mod repetat\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] zile de date locale\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"medie ponderată [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] live acum\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modele recunoscute\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Nu totul a putut fi citit: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Cea mai activă zi: [[ROOMGRID_VALUE_0]] cu [[ROOMGRID_VALUE_1]]. Reținere: [[ROOMGRID_VALUE_2]] zile pentru evenimente brute; valorile mai vechi pot rămâne ca totaluri zilnice.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sesiuni\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] salvat local\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Datele locale nu au putut fi încărcate: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] este online.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] este offline.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] nu a fost observat online de ceva vreme.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favorite sunt online împreună.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Profilul public al lui [[ROOMGRID_VALUE_0]] s-a schimbat.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] modele actualizate\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Previzualizare import statistici (nu s-a schimbat încă nimic):\\n[[ROOMGRID_VALUE_0]] modele\\n[[ROOMGRID_VALUE_1]] evenimente\\n[[ROOMGRID_VALUE_2]] profiluri publice\\n\\nContinuarea înlocuiește statisticile existente.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Previzualizare live a [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] adăugat la MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"Ștergeți [[ROOMGRID_VALUE_0]]?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"Importați [[ROOMGRID_VALUE_0]] vizualizări și fluxuri [[ROOMGRID_VALUE_1]]?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] camere adăugate la spațiul de lucru\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modele salvate\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] rezultate potrivite\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] care se potrivesc cu modele live\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Partajează #[[ROOMGRID_VALUE_0]] cu un favorit\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] imagini adăugate; [[ROOMGRID_VALUE_1]] nu a putut fi procesat.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] imagini adăugate.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] camere\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] nume de utilizator valide\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Pagina [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] camere\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"Adrese URL temporare: [[ROOMGRID_VALUE_0]] adăugat, [[ROOMGRID_VALUE_1]] eșuat\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Previzualizare import statistici: [[ROOMGRID_VALUE_0]] modele și [[ROOMGRID_VALUE_1]] evenimente. Continuarea îmbină datele în siguranță și creează mai întâi o copie de rezervă.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Aspectele personalizate acceptă până la [[ROOMGRID_VALUE_0]] camere.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Scena „[[ROOMGRID_VALUE_0]]” a fost salvată\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Scena „[[ROOMGRID_VALUE_0]]” a fost încărcată\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] fluxuri active · [[ROOMGRID_VALUE_1]] instabile · [[ROOMGRID_VALUE_2]] de vizionat\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] fluxuri · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p atribuite\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Ștergeți scena „[[ROOMGRID_VALUE_0]]”?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Salvat numai pentru [[ROOMGRID_VALUE_0]]: până la [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Această cameră folosește calitate globală/adaptativă[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] a fost înlocuit cu [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Redare audio numai de la [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Plasat în slotul Focus [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Plasat în fereastra grilă [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Rezoluție [[ROOMGRID_VALUE_0]], stare [[ROOMGRID_VALUE_1]], tampon [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Sănătatea fluxului [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modele\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Eliminați filtrul: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Se potrivește cu interesele dvs.: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] preluat · [[ROOMGRID_VALUE_2]] modele live\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] modele locale\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modele live\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] catalog săli încărcate\"},\"visible\":{\"‹ Zurück\":\"‹ Înapoi\",\"● 1.2k · #tag\":\"● 1,2k · #tag\",\"1 Jahr\":\"1 an\",\"30 Tage\":\"30 de zile\",\"7 Tage\":\"7 zile\",\"90 Tage\":\"90 de zile\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Afișați modele similare\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Toate modificările sunt salvate local și se aplică și pentru următorul model.\",\"Alle eigenen Tags\":\"Toate etichetele personalizate\",\"Alle Gruppen\":\"Toate grupurile\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Solicitați date mai vechi Chaturbate\",\"Automatisch abspielen\":\"Redare automată\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Captură automată · organizată complet la editare\",\"Gruppe löschen\":\"Șterge grupul\",\"Gruppen umbenennen oder löschen\":\"Redenumiți sau ștergeți grupuri\",\"HD + Gaming\":\"Jocuri HD+\",\"IndexedDB\":\"IndexedDB\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Fiecare etichetă salvată este disponibilă automat în căutare, în completarea automată și aici ca un filtru multiplu direct.\",\"Keine Models passen zu diesen Filtern.\":\"Niciun model nu se potrivește cu aceste filtre.\",\"Keine Priorität\":\"Nu este o prioritate\",\"Konto-Daten werden eingelesen …\":\"Detaliile contului sunt citite în...\",\"Lautstärke pro Model merken\":\"Notați volumul pe model\",\"Layout & sichtbare Bereiche\":\"Aspect și zone vizibile\",\"Letzte Messung\":\"Ultima măsurătoare\",\"lokal · für Suche und Filter\":\"local · pentru căutare și filtre\",\"Maximale Stream-Lautstärke\":\"Volumul maxim al fluxului\",\"Model besitzt alle gewählten Tags\":\"Modelul deține toate etichetele selectate\",\"Model besitzt mindestens einen gewählten Tag\":\"Modelul are cel puțin o etichetă selectată\",\"Automatisch neu verbinden\":\"Reconectați automat\",\"Bei verstecktem Tab pausieren\":\"Întrerupeți în timp ce fila este ascunsă\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Aplicați detaliile contului Chaturbate existente\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Optimizează imaginea · zoom mare, poziție, filtru și rotație\",\"Bildanpassung\":\"Ajustarea imaginii\",\"Bildwerkzeuge einklappen\":\"Restrângeți instrumentele de imagine\",\"Breite des Informationsbereichs\":\"Lățimea zonei de informații\",\"Broadcaster-Verbindung verwalten\":\"Gestionați conexiunea difuzorului\",\"Chat anzeigen\":\"Afișează chatul\",\"Ctrl + Shift + X\":\"Ctrl + Shift + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Aceasta înseamnă că nu sunteți limitat la date de când scriptul a fost instalat: RoomGrid poate citi următoarea listă existentă și intrările clar recunoscute din Colecția mea. Nu sunt copiate mesaje, plăți, media sau parole.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Studioul Analytics conține, de asemenea, comparații cu perioade de timp, hărți termice, canale de descoperire, analize ale spațiului de lucru și ale calității, precum și exporturi de date.\",\"Datenschutz & Export\":\"Protecția datelor și export\",\"Deine Aktivität der letzten 14 Tage\":\"Activitatea dvs. din ultimele 14 zile\",\"Deine meistgesehenen Models\":\"Cele mai văzute modele ale tale\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"Conexiunea curentă este activă; După următoarea măsurare, aici apare cursul local.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Această vizualizare a bibliotecii este încă goală.\",\"Eigene Priorität\":\"Prioritate proprie\",\"Eigene Tags\":\"Etichete proprii\",\"Eigene Tags filtern\":\"Filtrați propriile etichete\",\"Eigenen Tag eingeben …\":\"Intră în propria ta zi...\",\"Einstellungen\":\"Setări\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter sau „Adăugați etichetă” preia schița. Se salvează permanent doar cu „Salvare profil”. Etichetele deja utilizate sunt sugerate pe măsură ce introduceți text.\",\"Ereignisse im Zeitraum\":\"Evenimentele din perioada\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Creați colecții cu nume liber, atribuiți modele mai multor grupuri și afișați unul sau mai multe grupuri împreună aici.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Mai întâi creați-vă propriul grup de mai sus.\",\"Follower\":\"Urmaritori\",\"GAMING\":\"JOCURI\",\"Gesamter Verlauf\":\"Întregul curs\",\"Grid +\":\"Grid+\",\"Größe der Bedienelemente\":\"Dimensiunea comenzilor\",\"Model ist in allen gewählten Gruppen\":\"Modelul este în toate grupurile selectate\",\"Model ist in mindestens einer gewählten Gruppe\":\"Modelul este în cel puțin un grup selectat\",\"MultiCam\":\"MultiCam\",\"MultiCam +\":\"MultiCam+\",\"My Collection öffnen\":\"Deschide Colecția mea\",\"Namen speichern\":\"Salvați numele\",\"Nicht verbunden\":\"Nu este conectat\",\"Niedrige Live-Latenz\":\"Latență live scăzută\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Încă nu există valori de utilizare locale suficiente.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Niciun grup al nostru încă. Introduceți un nume mai sus și creați-vă prima colecție.\",\"Noch keine Model-Sehzeit erfasst.\":\"Nu s-a înregistrat încă un timp de vizualizare a modelului.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Nu există încă instantanee oficiale ale contului.\",\"Noch nicht eingelesen\":\"Nu citit încă\",\"Nur auf diesem Gerät\":\"Doar pe acest dispozitiv\",\"Öffentliche/erkannte Tags\":\"Etichete publice/recunoscute\",\"Öffentliches Profil anzeigen\":\"Vezi profilul public\",\"LIVE\":\"TRĂI\",\"Offizielle Statistik verbinden\":\"Conectați statisticile oficiale\",\"Optionale offizielle Kontostatistik\":\"Statistici oficiale ale contului opțional\",\"Persönlich organisiert\":\"Organizat personal\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Player și conexiune\",\"Player neu verbinden\":\"Reconectați playerul\",\"Player-Ecken\":\"Colțurile jucătorilor\",\"Player-Hintergrund\":\"Fundalul jucătorului\",\"Player-Höhe\":\"Înălțimea jucătorului\",\"Player-Seitenverhältnis\":\"Raportul de aspect al jucătorului\",\"Position der Informationen\":\"Locația informațiilor\",\"Private Notiz\":\"Notă privată\",\"Private Notizen anzeigen\":\"Vedeți notele private\",\"Puffergröße\":\"Dimensiunea tamponului\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid nu poate ghici valorile personale ale contului Chaturbate. Aceste carduri apar numai dacă ați legat în mod conștient statisticile oficiale ale radiodifuzorului de propriul dvs. nume de utilizator și simbol.\",\"RoomGrid Suite\":\"Suită RoomGrid\",\"Seitengröße\":\"Dimensiunea paginii\",\"SingleCam\":\"SingleCam\",\"SingleCam einstellen\":\"Configurează SingleCam\",\"SingleCam Pro\":\"SingleCam Pro\",\"SingleCam-Standard wiederherstellen\":\"Restabiliți SingleCam implicit\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"De îndată ce vă salvați propria etichetă pentru un model, aceasta va apărea aici și în completarea automată.\",\"Status im Player anzeigen\":\"Afișează starea în player\",\"Status-Aktualisierung\":\"Actualizare de stare\",\"Stream immer stumm starten\":\"Porniți întotdeauna fluxul în liniște\",\"Tag hinzufügen\":\"Adăugați etichetă\",\"Tipps letzte Stunde\":\"Sfaturi ultima ora\",\"Token-Guthaben\":\"Bilanțul de simboluri\",\"Ton & Ruhe\":\"Sunet și calm\",\"Ungefähre Onlinezeiten\":\"Timp aproximativ online\",\"Verbunden\":\"Conectat\",\"Vollständige Modelstatistik\":\"Statistici complete ale modelului\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Citiți acum datele existente ale contului\",\"Weiter ›\":\"Următorul ›\",\"Zufriedenheit\":\"Satisfacția\",\"Gruppen\":\"Grupuri\",\"Schließen\":\"Închide\",\"Startseite nach eigenen Gruppen filtern\":\"Filtrează pagina principală după grupurile proprii\",\"RoomGrid Komfortzentrum\":\"Centrul de control RoomGrid\",\"Mein Nutzer\":\"Profilul meu\",\"Mein Nutzer-Dashboard\":\"Panoul profilului meu\",\"Modellbibliothek\":\"Biblioteca de modele\",\"Große Modellbibliothek\":\"Biblioteca extinsă de modele\",\"Statistikzentrum\":\"Centrul de statistici\",\"Sitzungen\":\"Sesiuni\",\"Dock + Hover\":\"Panou + trecere cu mouse-ul\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Local și privat pe acest dispozitiv\",\"Sortierung\":\"Sortare\",\"Meiste Sehzeit\":\"Cel mai lung timp de vizionare\",\"Zuletzt aktiv\":\"Activitate recentă\",\"Meiste Sitzungen\":\"Cele mai multe sesiuni\",\"Meiste Zuschauer\":\"Cei mai mulți spectatori\",\"Status\":\"Stare\",\"Alle Status\":\"Toate stările\",\"Nur live\":\"Doar live\",\"Nur Live\":\"Doar live\",\"Offline\":\"Offline\",\"Unbekannt\":\"Necunoscut\",\"Nur Bibliothek\":\"Doar biblioteca\",\"Pro Seite\":\"Pe pagină\"}}","nl":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"De taalwissel kon niet veilig worden voorbereid. Deze pagina is niet verlaten.\",\"Language\":\"Taal\",\"Default workspace\":\"Standaard werkruimte\",\"Workspace\":\"Werkruimte\",\"New workspace\":\"Nieuwe werkruimte\",\"copy\":\"kopiëren\",\"New group\":\"Nieuwe groep\",\"Saved model\":\"Opgeslagen model\",\"Search models, pages, workspaces and commands…\":\"Zoek modellen, pagina's, werkruimten en opdrachten...\",\"Type to filter · Enter to run · Esc to close\":\"Type om te filteren · Enter om uit te voeren · Esc om te sluiten\",\"No matches\":\"Geen overeenkomsten\",\"Currently active RoomGrid version\":\"Momenteel actieve RoomGrid-versie\",\"years\":\"jaren\",\"Private\":\"Privé\",\"Away\":\"Weg\",\"sessions\":\"sessies\",\"NEW\":\"NIEUW\",\"Name, subject, tag or place\":\"Naam, onderwerp, tag of plaats\",\"Website order\":\"Website-bestelling\",\"Most viewers\":\"De meeste kijkers\",\"Fewest viewers\":\"Minste kijkers\",\"Longest online\":\"Langste online\",\"Recently online\":\"Sinds kort online\",\"Age ascending\":\"Leeftijd oplopend\",\"Age descending\":\"Leeftijd aflopend\",\"New models first\":\"Eerst nieuwe modellen\",\"Saved first\":\"Eerst opgeslagen\",\"Favorites first\":\"Favorieten eerst\",\"Most watched\":\"Meest bekeken\",\"Recently watched\":\"Onlangs bekeken\",\"Random mix\":\"Willekeurige mix\",\"All loaded\":\"Allemaal geladen\",\"Previous page\":\"Vorige pagina\",\"Next page\":\"Volgende pagina\",\"Reload complete live list\":\"Herlaad de volledige livelijst\",\"All homepage options\":\"Alle startpagina-opties\",\"Reset filters\":\"Filters opnieuw instellen\",\"Quick filter\":\"Snelfilter\",\"Sort\":\"Soort\",\"Models per page\":\"Modellen per pagina\",\"Load more results\":\"Laad meer resultaten\",\"still to display\":\"nog te tonen\",\"continues automatically while scrolling\":\"gaat automatisch verder tijdens het scrollen\",\"visible\":\"zichtbaar\",\"loaded\":\"geladen\",\"page\":\"pagina\",\"active filters\":\"actieve filters\",\"loading complete live list\":\"volledige livelijst laden\",\"all matching live models found\":\"alle overeenkomende live-modellen gevonden\",\"live list incomplete, retry\":\"live lijst onvolledig, probeer het opnieuw\",\"Requests temporarily paused\":\"Verzoeken tijdelijk onderbroken\",\"Vault could not be unlocked\":\"De kluis kon niet worden ontgrendeld\",\"Preview Dock\":\"Voorbeelddock\",\"Connecting…\":\"Verbinden…\",\"Stream temporarily unavailable\":\"Stream tijdelijk niet beschikbaar\",\"Error\":\"Fout\",\"Buffering\":\"Bufferen\",\"Preview unavailable\":\"Voorbeeld niet beschikbaar\",\"Connect\":\"Verbinden\",\"Close\":\"Sluiten\",\"Opacity\":\"Dekking\",\"Age\":\"Leeftijd\",\"Gender\":\"Geslacht\",\"Location\":\"Locatie\",\"Country\":\"Land\",\"Last broadcast\":\"Laatste uitzending\",\"Fan club\":\"Fanclub\",\"Private show\":\"Privéshow\",\"Spy show\":\"Spionageshow\",\"Model comparison list\":\"Modelvergelijkingslijst\",\"Compare\":\"Vergelijken\",\"Open model details\":\"Modeldetails openen\",\"Compare now\":\"Vergelijk nu\",\"Clear\":\"Duidelijk\",\"Up to four models can be compared at once.\":\"Er kunnen maximaal vier modellen tegelijk worden vergeleken.\",\"Remove from comparison\":\"Uit de vergelijking verwijderen\",\"Add to comparison\":\"Voeg toe aan vergelijking\",\"Total watched\":\"Totaal bekeken\",\"Sessions\":\"Sessies\",\"Viewers\":\"Kijkers\",\"Rating\":\"Beoordeling\",\"Online samples\":\"Online monsters\",\"Loading quality data…\":\"Kwaliteitsgegevens laden…\",\"samples\":\"monsters\",\"stalls\":\"kraampjes\",\"No quality measurements yet\":\"Nog geen kwaliteitsmetingen\",\"Quality data unavailable\":\"Kwaliteitsgegevens niet beschikbaar\",\"Remove\":\"Verwijderen\",\"Model comparison\":\"Modelvergelijking\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Elke waarde komt uit openbare informatie, uw eigen organisatie of lokale observaties op dit apparaat. Ontbrekende waarden worden nooit geraden.\",\"Shared tags\":\"Gedeelde tags\",\"Loading public facts…\":\"Openbare feiten laden...\",\"Unknown status\":\"Onbekende status\",\"Last activity\":\"Laatste activiteit\",\"These statistics describe only your own local RoomGrid usage.\":\"Deze statistieken beschrijven alleen uw eigen lokale RoomGrid-gebruik.\",\"At a glance\":\"In één oogopslag\",\"Appearances\":\"Optredens\",\"Public facts\":\"Publieke feiten\",\"No public biography available yet.\":\"Er is nog geen openbare biografie beschikbaar.\",\"Public link\":\"Openbare link\",\"Public profile source\":\"Openbare profielbron\",\"Profile updated\":\"Profiel bijgewerkt\",\"Private note (local only)\":\"Privénotitie (alleen lokaal)\",\"Your tags\":\"Jouw tags\",\"No rating\":\"Geen beoordeling\",\"No priority\":\"Geen prioriteit\",\"Private library data\":\"Gegevens van privébibliotheek\",\"Private notes\":\"Privé notities\",\"Priority\":\"Prioriteit\",\"Favorite\":\"Favoriet\",\"Watchlist\":\"Volglijst\",\"Archived\":\"Gearchiveerd\",\"Watch again\":\"Kijk nog eens\",\"Technical note\":\"Technische noot\",\"Technical\":\"Technisch\",\"Save all changes\":\"Sla alle wijzigingen op\",\"Loading local activity…\":\"Lokale activiteit laden…\",\"Activity and playback quality\":\"Activiteit en afspeelkwaliteit\",\"Sun\":\"Zon\",\"Mon\":\"ma\",\"Tue\":\"di\",\"Wed\":\"wo\",\"Thu\":\"Do\",\"Fri\":\"Vr\",\"Sat\":\"Zat\",\"Quality samples\":\"Kwaliteitsmonsters\",\"Average startup\":\"Gemiddelde opstart\",\"Stalls\":\"Kraampjes\",\"Reconnects\":\"Maakt opnieuw verbinding\",\"Dropped frames\":\"Gevallen kaders\",\"Online observations\":\"Online observaties\",\"Frequently observed online times\":\"Vaak waargenomen online tijden\",\"Not enough online observations yet.\":\"Nog niet genoeg online observaties.\",\"Watched\":\"Bekeken\",\"Preview\":\"Voorbeeld\",\"Quick view\":\"Snel bekijken\",\"Discovered\":\"Ontdekt\",\"Quality\":\"Kwaliteit\",\"Connection\":\"Verbinding\",\"No local activity recorded yet.\":\"Nog geen lokale activiteit geregistreerd.\",\"Recent events\":\"Recente gebeurtenissen\",\"Activity data unavailable.\":\"Activiteitsgegevens niet beschikbaar.\",\"Similar models\":\"Soortgelijke modellen\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Berekend op basis van lokale/openbare tags, land en type. Bij elke suggestie wordt de reden aangegeven en er worden geen gegevens geüpload.\",\"shared tags\":\"gedeelde tags\",\"same country\":\"hetzelfde land\",\"same type\":\"hetzelfde type\",\"Not enough matching data yet.\":\"Nog niet voldoende overeenkomende gegevens.\",\"Overview\":\"Overzicht\",\"Profile\":\"Profiel\",\"Organize\":\"Organiseren\",\"Activity\":\"Activiteit\",\"Similar\":\"Vergelijkbaar\",\"Model detail sections\":\"Modeldetailsecties\",\"Previous model\":\"Vorig model\",\"Next model\":\"Volgende model\",\"Open room\":\"Open kamer\",\"Pin preview\":\"Voorbeeld vastzetten\",\"No public fields available\":\"Geen openbare velden beschikbaar\",\"Public facts currently unavailable\":\"Openbare feiten zijn momenteel niet beschikbaar\",\"Session chat search\":\"Sessiechat zoeken\",\"Search this session only…\":\"Alleen in deze sessie zoeken...\",\"New messages\":\"Nieuwe berichten\",\"Time for a short break.\":\"Tijd voor een korte pauze.\",\"Your RoomGrid time budget for today has been reached.\":\"Uw RoomGrid-tijdbudget voor vandaag is bereikt.\",\"RoomGrid action messages\":\"RoomGrid-actieberichten\",\"This change is saved automatically.\":\"Deze wijziging wordt automatisch opgeslagen.\",\"Undo\":\"Ongedaan maken\",\"Change undone\":\"Wijziging ongedaan gemaakt\",\"Undo was not applied because newer changes already exist.\":\"Ongedaan maken is niet toegepast omdat er al nieuwere wijzigingen bestaan.\",\"Chaturbate live catalog\":\"Chaturbate live catalogus\",\"RoomGrid Discover\":\"RoomGrid Ontdek\",\"Chaturbate Discover page\":\"Chaturbate Ontdekpagina\",\"Following list\":\"Volgende lijst\",\"My Collection\":\"Mijn collectie\",\"Chaturbate page\":\"Chaturbate-pagina\",\"Local record\":\"Lokaal record\",\"Personally organized\":\"Persoonlijk georganiseerd\",\"Local watch history\":\"Lokale kijkgeschiedenis\",\"Public profile\":\"Openbaar profiel\",\"Local quality measurements\":\"Lokale kwaliteitsmetingen\",\"Local model index\":\"Lokale modelindex\",\"Current live signal\":\"Huidig ​​live-signaal\",\"Within the last hour\":\"Binnen het laatste uur\",\"Within the last 24 hours\":\"Binnen de afgelopen 24 uur\",\"Within the last 30 days\":\"Binnen de afgelopen 30 dagen\",\"Historical record\":\"Historisch record\",\"Time unknown\":\"Tijd onbekend\",\"All\":\"Alle\",\"Favorites\":\"Favorieten\",\"Library\":\"Bibliotheek\",\"My Collection contained no reliably recognizable entries\":\"Mijn verzameling bevatte geen betrouwbaar herkenbare vermeldingen\",\"No group filter active · all models are shown.\":\"Geen groepsfilter actief · alle modellen worden getoond.\",\"Clear group filter\":\"Groepsfilter wissen\",\"Filter homepage by groups\":\"Filter de startpagina op groepen\",\"Close group menu\":\"Groepsmenu sluiten\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Kies een of meer van uw eigen groepen. Andere modelkaarten worden onmiddellijk verborgen op de Chaturbate-startpagina.\",\"Show all\":\"Toon alles\",\"Models in this group\":\"Modellen in deze groep\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Er is nog geen aangepaste groep. Maak er een in de modelbibliotheek en wijs daar modellen toe.\",\"In any group\":\"In welke groep dan ook\",\"In all groups\":\"In alle groepen\",\"Manage groups\":\"Beheer groepen\",\"Create group\":\"Groep aanmaken\",\"Custom groups for the homepage\":\"Aangepaste groepen voor de startpagina\",\"Main views\":\"Belangrijkste weergaven\",\"MultiCam workstation\":\"MultiCam-werkstation\",\"Camera grid, audio and layouts\":\"Cameraraster, audio en lay-outs\",\"Discover\":\"Ontdekken\",\"Full catalog, filters and sorting\":\"Volledige catalogus, filters en sortering\",\"Personal dashboard\":\"Persoonlijk dashboard\",\"Favorites, activity and shortcuts\":\"Favorieten, activiteit en snelkoppelingen\",\"Tag explorer\":\"Tagverkenner\",\"Combinations, trends and related tags\":\"Combinaties, trends en gerelateerde tags\",\"Library and statistics\":\"Bibliotheek en statistieken\",\"Statistics Center\":\"Statistiekcentrum\",\"Model comparison and local statistics\":\"Modelvergelijking en lokale statistieken\",\"Model library\":\"Modellenbibliotheek\",\"Profiles, tags, groups and notes\":\"Profielen, tags, groepen en notities\",\"My user\":\"Mijn profiel\",\"Preferences, usage and account data\":\"Voorkeuren, gebruik en accountgegevens\",\"Analytics Studio\":\"Analytics-studio\",\"Ranges, heatmaps and exports\":\"Bereiken, heatmaps en exports\",\"Settings and maintenance\":\"Instellingen en onderhoud\",\"Customize homepage\":\"Startpagina aanpassen\",\"Cards, filters and visible details\":\"Kaarten, filters en zichtbare details\",\"Appearance\":\"Verschijning\",\"Colors, density and surfaces\":\"Kleuren, dichtheid en oppervlakken\",\"Workspaces and groups\":\"Werkruimtes en groepen\",\"Layouts, templates and assignments\":\"Lay-outs, sjablonen en opdrachten\",\"Data and backup\":\"Gegevens en back-up\",\"Import, export and recovery\":\"Import, export en herstel\",\"Windows and performance\":\"Windows en prestaties\",\"Resource profiles and preview limits\":\"Resourceprofielen en previewlimieten\",\"History, private session and local data\":\"Geschiedenis, privésessie en lokale gegevens\",\"Diagnostics\":\"Diagnostiek\",\"Self-check, routes and recovery\":\"Zelfcontrole, routes en herstel\",\"RoomGrid main menu\":\"RoomGrid-hoofdmenu\",\"All pages, settings and tools in one place\":\"Alle pagina's, instellingen en tools op één plek\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Het menu blijft binnen het zichtbare venster; alle RoomGrid-gegevens blijven lokaal.\",\"RoomGrid pages\":\"RoomGrid-pagina's\",\"Statistics\":\"Statistieken\",\"Customize original homepage\":\"Pas de originele startpagina aan\",\"Homepage\":\"Startpagina\",\"Filter homepage by your own groups\":\"Filter de startpagina op uw eigen groepen\",\"Groups\":\"Groepen\",\"Activity trend\":\"Activiteitstrend\",\"Public profile text and links\":\"Openbare profieltekst en links\",\"Last known public preview\":\"Laatst bekende openbare preview\",\"Current public preview\":\"Huidige openbare preview\",\"Offline may only provide a placeholder\":\"Offline mag alleen een tijdelijke aanduiding bevatten\",\"Example images and covers\":\"Voorbeeldafbeeldingen en omslagen\",\"Reading local image archive…\":\"Lokaal beeldarchief lezen…\",\"Save new snapshot\":\"Sla nieuwe momentopname op\",\"Saving public previews…\":\"Openbare voorbeelden opslaan…\",\"No saved image yet; current public previews are still shown.\":\"Nog geen opgeslagen afbeelding; huidige openbare voorbeelden worden nog steeds weergegeven.\",\"Current public cover\":\"Huidige publieke dekking\",\"Current compact preview\":\"Huidig ​​compact voorbeeld\",\"Current live image\":\"Actueel livebeeld\",\"Room\":\"Kamer\",\"More\":\"Meer\",\"Unwatch\":\"Uitkijken\",\"Data sources and freshness\":\"Gegevensbronnen en versheid\",\"Data source\":\"Gegevensbron\",\"Full local data set\":\"Volledige lokale dataset\",\"Matching filters\":\"Bijpassende filters\",\"Shown on this page\":\"Getoond op deze pagina\",\"Latest live signals\":\"Laatste live signalen\",\"With watch history\":\"Met kijkgeschiedenis\",\"Public-profile coverage\":\"Publieke profieldekking\",\"Country coverage\":\"Landdekking\",\"Age coverage\":\"Leeftijdsdekking\",\"Tag coverage\":\"Tagdekking\",\"Which data set is being searched?\":\"Welke dataset wordt doorzocht?\",\"Last local observation\":\"Laatste lokale waarneming\",\"No observation time yet\":\"Nog geen observatietijd\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Openbaar = van huidige Chaturbate-pagina's of live catalogus. Persoonlijk = door jou opgeslagen, getagd of gegroepeerd. Lokaal = sessies, previews of kwaliteitswaarden die alleen op dit apparaat worden waargenomen. Onbekende velden blijven onbekend en worden nooit geraden.\",\"Local model statistics\":\"Lokale modelstatistieken\",\"Automatically recorded · personally organized\":\"Automatisch opgenomen · persoonlijk georganiseerd\",\"Automatically recorded in the growing model catalog\":\"Automatisch opgenomen in de groeiende modellencatalogus\",\"Total\":\"Totaal\",\"Avg. session\":\"Gem. sessie\",\"Active days\":\"Actieve dagen\",\"Share\":\"Deel\",\"Longest session\":\"Langste sessie\",\"Previews\":\"Voorvertoningen\",\"Quick views\":\"Snelle weergaven\",\"Data sources\":\"Gegevensbronnen\",\"Edit in library\":\"Bewerken in bibliotheek\",\"No experience data yet\":\"Nog geen ervaringsgegevens\",\"Online observations will be collected locally from now on.\":\"Online observaties worden voortaan lokaal verzameld.\",\"Good trend\":\"Goede tendens\",\"Moderate trend\":\"Matige tendens\",\"Initial trend\":\"Initiële trend\",\"First observation\":\"Eerste observatie\",\"Estimated from watch time\":\"Geschat op basis van kijktijd\",\"weekday open\":\"weekdag geopend\",\"time open\":\"tijd geopend\",\"Preview image\":\"Voorbeeldafbeelding\",\"Biography / room subject\":\"Biografie / kameronderwerp\",\"Catalog state\":\"Catalogus staat\",\"Live status\":\"Live-status\",\"Viewer count\":\"Aantal kijkers\",\"Country/region\":\"Land/regio\",\"Total watch time\":\"Totale kijktijd\",\"Average session\":\"Gemiddelde sessie\",\"First seen\":\"Voor het eerst gezien\",\"Last seen\":\"Laatst gezien\",\"Last live\":\"Laatste leven\",\"Approximate online times\":\"Geschatte online tijden\",\"Activity, sources and profile state\":\"Activiteit, bronnen en profielstatus\",\"Example images\":\"Voorbeeld afbeeldingen\",\"Own groups\":\"Eigen groepen\",\"Public/recognized tags\":\"Openbare/herkende tags\",\"Notes, own tags and priority\":\"Opmerkingen, eigen tags en prioriteit\",\"Local rating\":\"Lokale beoordeling\",\"Group assignment\":\"Groepsopdracht\",\"Room and preview actions\":\"Ruimte- en preview-acties\",\"Library display\":\"Bibliotheekweergave\",\"Saved in this browser\":\"Opgeslagen in deze browser\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Kies het kaartformaat, de modellen per rij, het paginaformaat en welke details elke kaart precies laat zien.\",\"Compact overview\":\"Compact overzicht\",\"Balanced\":\"Evenwichtig\",\"All details\":\"Alle details\",\"Show all details\":\"Toon alle details\",\"Core details only\":\"Alleen kerngegevens\",\"Card zoom\":\"Kaartzoom\",\"Visible information per model\":\"Zichtbare informatie per model\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Met automatische kolommen bepaalt kaartzoom hoeveel modellen naast elkaar passen. Een exacte kolomtelling overschrijft deze. Alle keuzes blijven in deze browser opgeslagen.\",\"No custom tags yet\":\"Nog geen aangepaste tags\",\"Name of the new group\":\"Naam van de nieuwe groep\",\"Models remain in the library.\":\"Modellen blijven in de bibliotheek.\",\"Custom groups\":\"Aangepaste groepen\",\"Create new group\":\"Nieuwe groep aanmaken\",\"Nothing selected — show all\":\"Niets geselecteerd - toon alles\",\"Any\":\"Elk\",\"Women\":\"Vrouwen\",\"Men\":\"Heren\",\"Couples\":\"Paren\",\"Unknown\":\"Onbekend\",\"Error/loading\":\"Fout/laden\",\"Every weekday\":\"Elke weekdag\",\"Sunday\":\"Zondag\",\"Monday\":\"Maandag\",\"Tuesday\":\"Dinsdag\",\"Wednesday\":\"Woensdag\",\"Thursday\":\"Donderdag\",\"Friday\":\"Vrijdag\",\"Saturday\":\"Zaterdag\",\"Every time\":\"Elke keer\",\"Advanced filters and sorting\":\"Geavanceerde filters en sortering\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"Combineer zoveel filters als je wilt. Meerdere geslachten, staten, landen en tags werken als meerdere selecties. Elke instelling blijft in deze browser opgeslagen.\",\"Gender/type\":\"Geslacht/type\",\"Countries/regions (multiple)\":\"Landen/regio's (meerdere)\",\"Add country…\":\"Land toevoegen…\",\"Public tags (multiple)\":\"Openbare tags (meerdere)\",\"Add tag…\":\"Label toevoegen…\",\"Public-tag matching\":\"Overeenkomsten met openbare tags\",\"At least one tag\":\"Minstens één label\",\"Every selected tag\":\"Elke geselecteerde tag\",\"Catalog source\":\"Catalogusbron\",\"All models\":\"Alle modellen\",\"Personally organized only\":\"Alleen persoonlijk georganiseerd\",\"Automatically discovered only\":\"Alleen automatisch ontdekt\",\"Profile data\":\"Profielgegevens\",\"Loaded or missing\":\"Geladen of ontbrekend\",\"Loaded only\":\"Alleen geladen\",\"Missing only\":\"Alleen ontbrekend\",\"Picture/category\":\"Afbeelding/categorie\",\"Neither HD nor gaming\":\"Noch HD, noch gaming\",\"Watch history\":\"Bekijk de geschiedenis\",\"Never watched\":\"Nooit gekeken\",\"Own tags\":\"Eigen tags\",\"With or without\":\"Met of zonder\",\"With tags only\":\"Alleen met tags\",\"Without tags only\":\"Alleen zonder tags\",\"At least one group\":\"Minstens één groep\",\"No group\":\"Geen groep\",\"With notes only\":\"Met alleen aantekeningen\",\"Without notes only\":\"Alleen zonder aantekeningen\",\"Numeric ranges and minimums\":\"Numerieke bereiken en minima\",\"Minimum age\":\"Minimumleeftijd\",\"Maximum age\":\"Maximale leeftijd\",\"Minimum viewers\":\"Minimale kijkers\",\"Maximum viewers\":\"Maximale kijkers\",\"Minimum followers\":\"Minimale volgers\",\"Maximum followers\":\"Maximale volgers\",\"Minimum total watch time\":\"Minimale totale kijktijd\",\"Currently online for at least\":\"Momenteel online voor tenminste\",\"Minimum sessions\":\"Minimale sessies\",\"Minimum rating\":\"Minimale beoordeling\",\"Minimum priority\":\"Minimale prioriteit\",\"Active within\":\"Actief van binnen\",\"days\":\"dagen\",\"Minimum appearances\":\"Minimale optredens\",\"Minimum live observations\":\"Minimale live observaties\",\"Filter by typical online time\":\"Filter op typische onlinetijd\",\"Weekday\":\"Weekdag\",\"Hour (local time)\":\"Uur (lokale tijd)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Nul of lege velden betekenen geen limiet. Onbekende leeftijden worden uitgesloten zolang er een leeftijdsgrens actief is. Tijdfilters maken gebruik van live-tijden die op dit apparaat worden waargenomen en vallen voorzichtig terug om tijden te bekijken wanneer dat nodig is.\",\"Reset all library filters\":\"Reset alle bibliotheekfilters\",\"Open live room\":\"Open woonkamer\",\"Open room page\":\"Open kamerpagina\",\"Open model room in a new tab\":\"Open modelkamer in een nieuw tabblad\",\"Open the Chaturbate single-model view in a new tab\":\"Open de Chaturbate-weergave met één model in een nieuw tabblad\",\"Save profile\":\"Profiel opslaan\",\"Restore\":\"Herstellen\",\"Archive\":\"Archief\",\"Advanced statistics filters\":\"Geavanceerde statistische filters\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Elk filter kan worden gecombineerd; landen, tags, groepen en typen ondersteunen meervoudige selectie. Keuzes worden permanent opgeslagen.\",\"Own groups (multiple)\":\"Eigen groepen (meerdere)\",\"Add group…\":\"Groep toevoegen…\",\"Tag matching\":\"Matchen van tags\",\"At least one\":\"Minstens één\",\"All selected tags\":\"Alle geselecteerde tags\",\"Group matching\":\"Groepsmatching\",\"All selected groups\":\"Alle geselecteerde groepen\",\"Cover/example image\":\"Omslag/voorbeeldafbeelding\",\"Known public image available\":\"Bekend publiek beeld beschikbaar\",\"No known image\":\"Geen bekende afbeelding\",\"Currently live\":\"Momenteel live\",\"Maximum total watch time\":\"Maximale totale kijktijd\",\"Maximum sessions\":\"Maximale sessies\",\"Minimum active days\":\"Minimale actieve dagen\",\"Minimum previews\":\"Minimale voorbeelden\",\"Minimum quick views\":\"Minimale snelle weergaven\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Nul of lege velden betekenen geen limiet. Onbekende waarden worden nooit geraden; leeftijdscategorieën sluiten onbekende leeftijden uit.\",\"Reset all statistics filters\":\"Reset alle statistiekfilters\",\"Search name, tags or notes…\":\"Zoek naam, tags of notities…\",\"Search\":\"Zoekopdracht\",\"Sorting\":\"Sorteren\",\"Most watch time\":\"De meeste kijktijd\",\"Least watch time\":\"Minste kijktijd\",\"Recently active\":\"Recent actief\",\"Longest inactive\":\"Langst inactief\",\"Most sessions\":\"De meeste sessies\",\"Fewest sessions\":\"Minste sessies\",\"Longest average session\":\"Langste gemiddelde sessie\",\"Shortest average session\":\"Kortste gemiddelde sessie\",\"Longest single session\":\"Langste enkele sessie\",\"Most active days\":\"Meest actieve dagen\",\"Highest watch share\":\"Hoogste kijkaandeel\",\"Most previews\":\"De meeste voorbeschouwingen\",\"Most quick views\":\"Meest snelle weergaven\",\"Most discoveries\":\"De meeste ontdekkingen\",\"Most followers\":\"De meeste volgers\",\"Youngest first\":\"Jongste eerst\",\"Oldest first\":\"Oudste eerst\",\"Highest rating\":\"Hoogste beoordeling\",\"Highest priority\":\"Hoogste prioriteit\",\"Most appearances\":\"De meeste optredens\",\"Most live observations\":\"De meeste live observaties\",\"First observed\":\"Eerst waargenomen\",\"Country A–Z\":\"Land A–Z\",\"Most public tags\":\"De meeste openbare tags\",\"Most groups\":\"De meeste groepen\",\"Best technical quality\":\"Beste technische kwaliteit\",\"Cover/example images first\":\"Omslag/voorbeeldafbeeldingen eerst\",\"All statuses\":\"Alle statussen\",\"Live only\":\"Leef alleen\",\"Library only\":\"Alleen bibliotheek\",\"View\":\"Weergave\",\"Active library\":\"Actieve bibliotheek\",\"Live now\":\"Leef nu\",\"With notes\":\"Met aantekeningen\",\"Newest saved/discovered\":\"Nieuwste opgeslagen/ontdekt\",\"Oldest saved/discovered\":\"Oudste bewaard/ontdekt\",\"Lowest rating\":\"Laagste beoordeling\",\"Lowest priority\":\"Laagste prioriteit\",\"Live longest now\":\"Leef nu het langst\",\"Age youngest first\":\"Leeftijd jongste eerst\",\"Age oldest first\":\"Leeftijd oudste eerst\",\"Most recently live\":\"Meest recent live\",\"First discovered\":\"Voor het eerst ontdekt\",\"Most own groups\":\"De meeste eigen groepen\",\"Card size\":\"Kaartformaat\",\"Comfortable\":\"Comfortabel\",\"Large\":\"Groot\",\"Cinema\":\"Bioscoop\",\"Extra large\":\"Extra groot\",\"Models per row\":\"Modellen per rij\",\"Automatic for window\":\"Automatisch voor raam\",\"Recorded models\":\"Opgenomen modellen\",\"Total cam time\":\"Totale cameratijd\",\"across all models\":\"over alle modellen\",\"Dock + hover\":\"Dock + aanwijzen\",\"Last 30 days\":\"Laatste 30 dagen\",\"local events\":\"lokale evenementen\",\"Top model\":\"Topmodel\",\"no watch time yet\":\"nog geen kijktijd\",\"Deeper analysis\":\"Diepere analyse\",\"Open Analytics Studio\":\"Analytics-studio openen\",\"Late night\":\"Laat in de nacht\",\"Morning\":\"Ochtend\",\"Daytime\":\"Dag\",\"Evening\":\"Avond\",\"Gaming\":\"Gamen\",\"Music & DJ\":\"Muziek en DJ\",\"Dance\":\"Dans\",\"Cosplay & anime\":\"Cosplay- en animatiefilms\",\"Fitness & sports\":\"Fitness & sport\",\"Talk & social\":\"Praten en sociaal\",\"Outdoor & travel\":\"Buiten en reizen\",\"Roleplay\":\"Rollenspel\",\"Performance\":\"Prestatie\",\"My interest statistics\":\"Mijn interessestatistieken\",\"All local data\":\"Alle lokale gegevens\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Berekend op basis van de werkelijke lokale kijktijd, sessies, previews, snelle weergaven, favorieten en momenteel bekende openbare modelinformatie.\",\"Favorite tag\":\"Favoriet label\",\"not enough data\":\"niet genoeg gegevens\",\"Favorite genre\":\"Favoriete genre\",\"from public tags\":\"van openbare tags\",\"Most-watched age\":\"Meest bekeken leeftijd\",\"no age data\":\"geen leeftijdsgegevens\",\"Most-watched country\":\"Meest bekeken land\",\"no country data\":\"geen landgegevens\",\"Favorite model type\":\"Favoriete modeltype\",\"no type data\":\"geen typegegevens\",\"Favorite viewing time\":\"Favoriete kijktijd\",\"no time data\":\"geen tijdgegevens\",\"Top-three concentration\":\"Top-drie concentratie\",\"watch time on the top three models\":\"bekijk de tijd op de drie beste modellen\",\"Favorite affinity\":\"Favoriete affiniteit\",\"watch time on current favorites\":\"bekijk de tijd van de huidige favorieten\",\"watch time on HD models\":\"kijktijd op HD-modellen\",\"Metadata coverage\":\"Dekking van metadata\",\"age\":\"leeftijd\",\"country\":\"land\",\"Tags, genres and browsing interests\":\"Tags, genres en browse-interesses\",\"Tags by watch time\":\"Tags op kijktijd\",\"Your ranking appears when watched models have public tags.\":\"Uw ranking wordt weergegeven wanneer bekeken modellen openbare tags hebben.\",\"Favorite genre worlds\":\"Favoriete genrewerelden\",\"No genre world can yet be assigned from public tags.\":\"Er kan nog geen genrewereld worden toegewezen via openbare tags.\",\"Your own tags\":\"Je eigen tags\",\"Your saved custom tags are also evaluated here by watch time.\":\"Je opgeslagen aangepaste tags worden hier ook geëvalueerd op kijktijd.\",\"Preview-interest tags\":\"Preview-interesse-tags\",\"Browsing interests appear when you use live previews or quick views.\":\"Browse-interesses verschijnen wanneer u live previews of snelle weergaven gebruikt.\",\"Most-previewed models\":\"Meest bekeken modellen\",\"No previews have been recorded in this period yet.\":\"Er zijn in deze periode nog geen previews opgenomen.\",\"Age, origin and model type\":\"Leeftijd, herkomst en modeltype\",\"Age groups by watch time\":\"Leeftijdsgroepen op kijktijd\",\"Not enough public age data is available.\":\"Er zijn onvoldoende openbare leeftijdsgegevens beschikbaar.\",\"Countries by watch time\":\"Landen op kijktijd\",\"Not enough public country data is available.\":\"Er zijn niet voldoende openbare landgegevens beschikbaar.\",\"Most-watched places\":\"Meest bekeken plaatsen\",\"Not enough public location data is available.\":\"Er zijn niet voldoende openbare locatiegegevens beschikbaar.\",\"Model types by watch time\":\"Modeltypen op kijktijd\",\"Not enough public model-type data is available.\":\"Er zijn niet voldoende openbare modelgegevens beschikbaar.\",\"Viewing rhythm and favorite signals\":\"Kijkritme en favorietsignalen\",\"Most active weekdays\":\"Meest actieve weekdagen\",\"Not enough visit events are available.\":\"Er zijn niet genoeg bezoekevenementen beschikbaar.\",\"Times of day\":\"Tijden van de dag\",\"Tags among favorites\":\"Tags onder favorieten\",\"Preference signals appear when you mark models as favorites.\":\"Voorkeurssignalen verschijnen wanneer u modellen als favoriet markeert.\",\"Countries among favorites\":\"Landen onder favorieten\",\"No known countries among favorites yet.\":\"Nog geen bekende landen onder favorieten.\",\"Age groups among favorites\":\"Leeftijdsgroepen onder favorieten\",\"No known ages among favorites yet.\":\"Nog geen leeftijden bekend onder favorieten.\",\"Model types among favorites\":\"Modeltypen onder favorieten\",\"No known model types among favorites yet.\":\"Nog geen bekende modeltypes onder favorieten.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Deze resultaten worden niet naar een server verzonden. Alleen de werkelijke kijktijd telt mee voor de kijkvoorkeuren; previews en favorieten worden afzonderlijk weergegeven. Ontbrekende leeftijds-, land-, locatie- of taggegevens worden nooit geraden.\",\"Open Following on Chaturbate\":\"Open volgen op Chaturbate\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Een volledige kijktijd- of bezoekgeschiedenis kan niet met terugwerkende kracht worden opgehaald, omdat Chaturbate geen gedocumenteerde persoonlijke geschiedenis-API biedt. U kunt een kopie van uw opgeslagen persoonlijke gegevens opvragen via de officiële gegevensexport van Chaturbate. Als die export bruikbare historievelden bevat, kan RoomGrid na validatie van het bestandsformaat een veilige import ondersteunen. De officiële Stats API levert actuele omroepwaarden; RoomGrid kan pas vanaf de eerste verbinding een eigen geschiedenis opbouwen.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Vergelijk elk lokaal waargenomen, opgeslagen en bekeken model, met kijktijd, sessies, activiteit, previews, kwaliteit en ranglijsten.\",\"Large model library\":\"Grote modelbibliotheek\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Uw modellen als beeldrijke, handige profielen met livestatus, notities, tags, beoordelingen, prioriteit, groepen en directe acties.\",\"My user dashboard\":\"Mijn gebruikersdashboard\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Uw persoonlijke lokale gebruiksgeschiedenis, gewoonten, bibliotheekgegevens en (alleen wanneer opzettelijk verbonden) accountgegevens van de officiële omroep.\",\"🔒 Local & private on this device\":\"🔒 Lokaal en privé op dit apparaat\",\"Preparing data…\":\"Gegevens voorbereiden…\",\"RoomGrid Comfort Center\":\"RoomGrid-bedieningscentrum\",\"inside the standard Chaturbate page\":\"binnen de standaard Chaturbate-pagina\",\"All RoomGrid pages and settings\":\"Alle RoomGrid-pagina's en instellingen\",\"Media paused; names and images are covered.\":\"Media gepauzeerd; namen en afbeeldingen zijn bedekt.\",\"Restore previous view\":\"Vorige weergave herstellen\",\"Enter local PIN\":\"Voer de lokale pincode in\",\"In-page notifications\":\"Meldingen op de pagina\",\"No new notifications\":\"Geen nieuwe meldingen\",\"Notifications are off\":\"Meldingen staan ​​uit\",\"RoomGrid notification\":\"RoomGrid-melding\",\"Dismiss as read\":\"Negeren als gelezen\",\"More unread notifications\":\"Meer ongelezen meldingen\",\"RoomGrid update\":\"RoomGrid-update\",\"A monitored source changed status.\":\"Een bewaakte bron veranderde van status.\",\"This is a local RoomGrid test notification.\":\"Dit is een lokale RoomGrid-testmelding.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Test succesvol: meldingen kunnen direct in de RoomGrid-balk verschijnen.\",\"Settings\":\"Instellingen\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Werkruimtes, bibliotheek, ontwerp, data, geschiedenis, meldingen, privacy en prestaties\",\"Workspaces\":\"Werkruimtes\",\"Data\":\"Gegevens\",\"History & statistics\":\"Geschiedenis & statistieken\",\"Broadcaster stats\":\"Statistieken van omroepen\",\"Notifications\":\"Meldingen\",\"Windows & performance\":\"Windows en prestaties\",\"Optional tools\":\"Optioneel gereedschap\",\"Settings pages\":\"Pagina's met instellingen\",\"RoomGrid control center\":\"RoomGrid-controlecentrum\",\"Any online model\":\"Elk onlinemodel\",\"Workspace name\":\"Naam van de werkruimte\",\"Optional description\":\"Optionele beschrijving\",\"Active\":\"Actief\",\"No description\":\"Geen beschrijving\",\"Name\":\"Naam\",\"Description\":\"Beschrijving\",\"Replace offline models\":\"Vervang offline modellen\",\"Replacement group\":\"Vervangende groep\",\"Delay seconds\":\"Vertraging seconden\",\"Save current state\":\"Bewaar huidige status\",\"New window\":\"Nieuw venster\",\"Export this workspace\":\"Exporteer deze werkruimte\",\"Duplicate\":\"Duplicaat\",\"Workspace restored\":\"Werkruimte hersteld\",\"Workspace archived\":\"Werkruimte gearchiveerd\",\"Delete permanently\":\"Permanent verwijderen\",\"Permanently delete this workspace?\":\"Deze werkruimte definitief verwijderen?\",\"Saved layout and panel sizes\":\"Opgeslagen lay-out en paneelformaten\",\"Apply\":\"Toepassen\",\"Layout template deleted\":\"Lay-outsjabloon verwijderd\",\"Parent group\":\"Oudergroep\",\"None\":\"Geen\",\"Group description\":\"Groepsbeschrijving\",\"Save current workspace\":\"Bewaar de huidige werkruimte\",\"Save layout template\":\"Lay-outsjabloon opslaan\",\"Template name\":\"Sjabloonnaam\",\"Layout templates\":\"Lay-outsjablonen\",\"Device layout\":\"Indeling van het apparaat\",\"Groups and subgroups\":\"Groepen en subgroepen\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Elke werkruimte bevat afzonderlijke desktop- en compacte lay-outs. Automatische vervanging is standaard uitgeschakeld en vult alleen offline slots nadat u deze voor die werkruimte hebt ingeschakeld.\",\"No templates yet.\":\"Nog geen sjablonen.\",\"Group name\":\"Groepsnaam\",\"New subgroup\":\"Nieuwe subgroep\",\"Enter parent group name\":\"Voer de naam van de bovenliggende groep in\",\"Subgroup name\":\"Naam van subgroep\",\"Name, note or local tag\":\"Naam, notitie of lokale tag\",\"Choose group…\":\"Groep kiezen…\",\"Add selected models to group\":\"Voeg geselecteerde modellen toe aan de groep\",\"Watch later\":\"Bekijk later\",\"Paused\":\"Gepauzeerd\",\"Collection name\":\"Naam collectie\",\"Tags, comma separated\":\"Tags, door komma's gescheiden\",\"Countries, comma separated\":\"Landen, door komma's gescheiden\",\"All types\":\"Alle soorten\",\"Rule name\":\"Regelnaam\",\"Required tags\":\"Vereiste tags\",\"Local alert\":\"Lokale waarschuwing\",\"Add to favorites\":\"Toevoegen aan favorieten\",\"Add to group\":\"Toevoegen aan groep\",\"Any tag\":\"Elk label\",\"Any country\":\"Welk land dan ook\",\"Pause\":\"Pauze\",\"Enable\":\"Inschakelen\",\"Delete\":\"Verwijderen\",\"Automation rule deleted\":\"Automatiseringsregel verwijderd\",\"Select model\":\"Selecteer model\",\"Never visited\":\"Nooit bezocht\",\"Last online\":\"Laatste online\",\"Last visited\":\"Laatst bezocht\",\"Local note\":\"Lokale opmerking\",\"Local tags, comma separated\":\"Lokale tags, door komma's gescheiden\",\"Remove later\":\"Later verwijderen\",\"Hide today\":\"Vandaag verbergen\",\"Pause 7 days\":\"Pauzeer 7 dagen\",\"Never suggest\":\"Stel nooit voor\",\"Select results\":\"Selecteer resultaten\",\"Clear selection\":\"Duidelijke selectie\",\"selected\":\"gekozen\",\"Add to watch later\":\"Toevoegen om later te bekijken\",\"Permanently delete selected models?\":\"Geselecteerde modellen definitief verwijderen?\",\"Smart collections\":\"Slimme collecties\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Collecties worden automatisch bijgewerkt naarmate de lokale metadata en statistieken groeien; modellen worden niet gedupliceerd.\",\"Tags\":\"Labels\",\"Countries\":\"Landen\",\"Favorites only\":\"Alleen favorieten\",\"Online only\":\"Alleen online\",\"Save smart collection\":\"Bespaar slimme verzameling\",\"Delete current collection\":\"Huidige collectie verwijderen\",\"Delete this smart collection? Model data stays unchanged.\":\"Deze slimme verzameling verwijderen? Modelgegevens blijven ongewijzigd.\",\"Rules & automation center\":\"Regels en automatiseringscentrum\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Wanneer lokale modelgegevens overeenkomen, kunt u één waarschuwing weergeven, een favoriet toevoegen of het model aan een gekozen groep toevoegen.\",\"Action\":\"Actie\",\"Target group\":\"Doelgroep\",\"Save rule\":\"Regel opslaan\",\"No automation rules yet.\":\"Er zijn nog geen automatiseringsregels.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Gebruikersnamen worden gededupliceerd door exacte genormaliseerde overeenkomsten. Zoeken omvat namen, notities en lokale tags. Archiveren, verwijderen uit een weergave en definitief verwijderen blijven afzonderlijke acties.\",\"No matching models.\":\"Geen bijpassende modellen.\",\"export\":\"exporteren\",\"Models and notes\":\"Modellen en opmerkingen\",\"This backup could not be read. Current data was not changed.\":\"Deze back-up kon niet worden gelezen. De huidige gegevens zijn niet gewijzigd.\",\"Import preview (nothing changed yet)\":\"Importvoorbeeld (nog niets veranderd)\",\"conflicts\":\"conflicten\",\"Includes sensitive statistics\":\"Inclusief gevoelige statistieken\",\"models\":\"modellen\",\"events\":\"evenementen\",\"quality samples\":\"kwaliteit monsters\",\"Import statistics too\":\"Importeer ook statistieken\",\"Import method\":\"Importmethode\",\"Merge (recommended)\":\"Samenvoegen (aanbevolen)\",\"Replace selected sections\":\"Vervang geselecteerde secties\",\"Apply import\":\"Importeren toepassen\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"Importeren voltooid. Van eerdere configuraties en statistieken werd afzonderlijk een back-up gemaakt.\",\"The import could not be saved safely. The backup remains available.\":\"De import kon niet veilig worden opgeslagen. De back-up blijft beschikbaar.\",\"Cancel preview\":\"Voorbeeld annuleren\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Dit bestand bevat model-, notitie- of werkruimtegegevens en moet als gevoelig worden behandeld.\",\"This import is not marked sensitive.\":\"Deze import is niet gemarkeerd als gevoelig.\",\"Data version\":\"Gegevensversie\",\"Software version\":\"Softwareversie\",\"Revision\":\"Herziening\",\"Local models\":\"Lokale modellen\",\"Selective export\":\"Selectieve export\",\"Export selected\":\"Geselecteerde exporteren\",\"Full export\":\"Volledige export\",\"Complete safety package\":\"Compleet veiligheidspakket\",\"Export statistics separately\":\"Exporteer statistieken afzonderlijk\",\"Choose file and preview\":\"Kies bestand en voorbeeld\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Het volledige veiligheidspakket bevat configuratie en gevoelige statistieken, maar nooit het officiële statistische token. Van elke import wordt een voorbeeld bekeken en er wordt afzonderlijk een back-up van de configuratie en statistieken gemaakt voordat deze wordt toegepast.\",\"Storage and backup center\":\"Opslag- en back-upcentrum\",\"Local storage\":\"Lokale opslag\",\"Site storage\":\"Locatie-opslag\",\"Available quota\":\"Beschikbaar quotum\",\"Internal statistics backups\":\"Back-ups van interne statistieken\",\"Configuration\":\"Configuratie\",\"workspaces\":\"werkruimtes\",\"LocalStorage + update vault\":\"LocalStorage + update kluis\",\"Statistics database\":\"Statistische database\",\"Secrets\":\"Geheimen\",\"The official statistics token is never included in normal or complete backups.\":\"Het officiële statistiektoken wordt nooit opgenomen in normale of volledige back-ups.\",\"Excluded\":\"Uitgesloten\",\"Create configuration restore point\":\"Configuratieherstelpunt maken\",\"Create statistics restore point\":\"Maak een herstelpunt voor statistieken\",\"Legacy cleanup\":\"Opruiming van de erfenis\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Verouderde gegevens worden nooit automatisch verwijderd. Verwijder de oude v6/v7-sleutels pas na een succesvolle beoordeling.\",\"Remove old v6/v7 copies\":\"Verwijder oude v6/v7-kopieën\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Alleen oude v6/v7-kopieën verwijderen? Huidige gegevens en back-ups blijven behouden.\",\"items removed\":\"artikelen verwijderd\",\"Blue and teal, the RoomGrid signature\":\"Blauw en groenblauw, het kenmerk van RoomGrid\",\"Ember\":\"Sintel\",\"Warm orange and rose\":\"Warm oranje en roze\",\"Frost\":\"Vorst\",\"Crisp cyan and violet\":\"Helder cyaan en violet\",\"Custom\":\"Aangepast\",\"Use the colors selected below\":\"Gebruik de hieronder geselecteerde kleuren\",\"Name the current appearance\":\"Geef het huidige uiterlijk een naam\",\"My appearance\":\"Mijn uiterlijk\",\"Delete appearance profile\":\"Uiterlijkprofiel verwijderen\",\"This appearance profile could not be imported.\":\"Dit uiterlijkprofiel kan niet worden geïmporteerd.\",\"Light\":\"Licht\",\"Dark\":\"Donker\",\"Export\":\"Exporteren\",\"Home\":\"Thuis\",\"Listings and tags\":\"Advertenties en tags\",\"Model pages\":\"Modelpagina's\",\"Use global appearance\":\"Gebruik een globale weergave\",\"This logo format is unsafe or unsupported.\":\"Dit logoformaat is onveilig of wordt niet ondersteund.\",\"The logo file could not be read.\":\"Het logobestand kon niet worden gelezen.\",\"MultiCam tools\":\"MultiCam-tools\",\"Page\":\"Pagina\",\"Panel\":\"Paneel\",\"Card surface\":\"Kaart oppervlak\",\"Input\":\"Invoer\",\"Border\":\"Grens\",\"Text\":\"Tekst\",\"Muted text\":\"Gedempte tekst\",\"Success\":\"Succes\",\"Warning\":\"Waarschuwing\",\"Danger\":\"Gevaar\",\"Info and focus\":\"Informatie en focus\",\"Data hub\":\"Datahub\",\"Text / page\":\"Tekst / pagina\",\"Text / panel\":\"Tekst / paneel\",\"Muted text / panel\":\"Gedempte tekst/paneel\",\"Accent text / accent\":\"Accenttekst/accent\",\"Desktop\":\"Bureaublad\",\"Mobile\":\"Mobiel\",\"Brand and logo studio\":\"Merk- en logostudio\",\"Tagline\":\"Slogan\",\"Brand display\":\"Merkweergave\",\"Full name\":\"Volledige naam\",\"Uploaded logo\":\"Logo geüpload\",\"Logo shape\":\"Logo-vorm\",\"Rounded\":\"Afgerond\",\"Circle\":\"Cirkel\",\"Square\":\"Vierkant\",\"Pill\":\"Pil\",\"Page title\":\"Paginatitel\",\"Website original\":\"Website origineel\",\"Brand plus original\":\"Merk plus origineel\",\"Custom title\":\"Aangepaste titel\",\"Custom page title\":\"Aangepaste paginatitel\",\"Image fit\":\"Afbeelding passend\",\"Horizontal position\":\"Horizontale positie\",\"Vertical position\":\"Verticale positie\",\"Upload light-mode logo\":\"Upload het logo in de lichtmodus\",\"Upload dark-mode logo\":\"Upload het donkere-moduslogo\",\"Remove logos\":\"Logo's verwijderen\",\"Show version\":\"Toon versie\",\"Use logo as tab icon on RoomGrid pages\":\"Gebruik het logo als tabbladpictogram op RoomGrid-pagina's\",\"Replace website logo in the original header (local only)\":\"Vervang het websitelogo in de originele header (alleen lokaal)\",\"Complete color system\":\"Compleet kleursysteem\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Bedieningspagina, paneel, kaart, invoer, rand, tekst en statuskleuren naast accenten.\",\"Derived automatically\":\"Automatisch afgeleid\",\"Fully custom\":\"Volledig op maat\",\"Editing dark palette\":\"Donker palet bewerken\",\"Editing light palette\":\"Lichtpalet bewerken\",\"Reset current palette\":\"Huidig ​​palet resetten\",\"Layout editor\":\"Lay-out-editor\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"Stel de inhoudsafstand, kaartbreedte, koptekst en zijbalk consistent in op RoomGrid-pagina's.\",\"Minimum card width\":\"Minimale kaartbreedte\",\"Section spacing\":\"Sectie-afstand\",\"Content padding\":\"Inhoud opvulling\",\"Header height\":\"Hoogte kop\",\"Maximum columns\":\"Maximale kolommen\",\"Automatic\":\"Automatisch\",\"Card aspect\":\"Kaartaspect\",\"Sidebar position\":\"Zijbalkpositie\",\"Left\":\"Links\",\"Right\":\"Rechts\",\"Keep header visible while scrolling\":\"Houd de koptekst zichtbaar tijdens het scrollen\",\"Live device preview\":\"Live apparaatvoorbeeld\",\"Contrast and accessibility\":\"Contrast en toegankelijkheid\",\"High-contrast mode\":\"Hoogcontrastmodus\",\"Underline links\":\"Onderstreep koppelingen\",\"Reduce transparency\":\"Verminder de transparantie\",\"Boost base text size\":\"Vergroot de basistekstgrootte\",\"Focus ring width\":\"Breedte focusring\",\"Minimum target size\":\"Minimale doelgrootte\",\"Respect system reduced-motion setting\":\"Respecteer de instelling voor verminderde beweging van het systeem\",\"Apply accessible defaults\":\"Toegankelijke standaardwaarden toepassen\",\"Profiles by page and screen size\":\"Profielen op pagina- en schermgrootte\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Sla één of meerdere verschijningen op en wijs deze vervolgens toe per paginatype voor desktop, tablet en mobiel. Profielen wisselen automatisch wanneer de schermgrootte verandert.\",\"Enable responsive design profiles\":\"Schakel responsieve ontwerpprofielen in\",\"Mobile maximum width\":\"Mobiele maximale breedte\",\"Tablet maximum width\":\"Maximale tabletbreedte\",\"RoomGrid design system\":\"RoomGrid-ontwerpsysteem\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Kies een merkrichting, inhoudsbreedte en kaartinformatiedichtheid. Elke pagina volgt onmiddellijk.\",\"Primary accent\":\"Primair accent\",\"Secondary accent\":\"Secundair accent\",\"Content width\":\"Breedte inhoud\",\"Centered\":\"Gecentreerd\",\"Wide\":\"Breed\",\"Full width\":\"Volledige breedte\",\"Card style\":\"Kaartstijl\",\"Visual\":\"Visueel\",\"Data-rich\":\"Datarijk\",\"Widget density\":\"Widgetdichtheid\",\"Spacious\":\"Ruim\",\"Theme and surfaces\":\"Thema en oppervlakken\",\"Surface style\":\"Oppervlaktestijl\",\"Soft\":\"Zacht\",\"Flat\":\"Vlak\",\"Glass\":\"Glas\",\"Page background\":\"Pagina-achtergrond\",\"Solid\":\"Stevig\",\"Gradient\":\"Verloop\",\"Ambient glow\":\"Omgevingsgloed\",\"Surface opacity\":\"Oppervlaktedekking\",\"Custom accent color\":\"Aangepaste accentkleur\",\"Sizing and motion\":\"Maatvoering en beweging\",\"Font style\":\"Lettertypestijl\",\"System\":\"Systeem\",\"Classic serif\":\"Klassieke schreef\",\"Monospace\":\"Monoruimte\",\"Density\":\"Dikte\",\"Font scale\":\"Lettertypeschaal\",\"Control size\":\"Controlegrootte\",\"Corner radius\":\"Hoekradius\",\"Card spacing\":\"Kaartafstand\",\"Shadow strength\":\"Schaduw kracht\",\"Sidebar width\":\"Breedte zijbalk\",\"Enable animations\":\"Animaties inschakelen\",\"Appearance profiles\":\"Uiterlijk profielen\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Sla het volledige uiterlijk op als herbruikbaar profiel. Profielen blijven beschikbaar na scriptupdates.\",\"Save current appearance\":\"Bewaar het huidige uiterlijk\",\"Import profile\":\"Profiel importeren\",\"No profiles saved yet.\":\"Nog geen profielen opgeslagen.\",\"Instant preview\":\"Direct voorbeeld\",\"Example card\":\"Voorbeeld kaart\",\"This is the combined result of the current visual settings.\":\"Dit is het gecombineerde resultaat van de huidige visuele instellingen.\",\"Example action\":\"Voorbeeld actie\",\"Per-page design profiles\":\"Ontwerpprofielen per pagina\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Kies een ander opgeslagen uiterlijk voor Home, lijsten, modelpagina's, MultiCam, Discover en de datahub.\",\"Enable per-page design\":\"Schakel ontwerp per pagina in\",\"Normal website pages\":\"Normale websitepagina's\",\"Enable RoomGrid page design\":\"Schakel RoomGrid-paginaontwerp in\",\"Tag and category pages\":\"Tag- en categoriepagina's\",\"Cards\":\"Kaarten\",\"List\":\"Lijst\",\"Page density\":\"Paginadichtheid\",\"Polish navigation\":\"Poolse navigatie\",\"Polish sidebars\":\"Poolse zijbalken\",\"Hide recognized promo and merch areas\":\"Verberg erkende promo- en merchandisegebieden\",\"Compact footer\":\"Compacte voettekst\",\"Mark saved models\":\"Markeer opgeslagen modellen\",\"Mark favorites separately\":\"Markeer favorieten afzonderlijk\",\"Mark visited models\":\"Mark bezocht modellen\",\"Show card quick actions\":\"Toon kaart snelle acties\",\"Animated live previews on home and listing pages\":\"Geanimeerde live previews op start- en vermeldingspagina's\",\"Preview refresh rate\":\"Voorbeeld van vernieuwingsfrequentie\",\"Off\":\"Uit\",\"Update mode\":\"Update-modus\",\"All visible cards simultaneously\":\"Alle zichtbare kaarten tegelijk\",\"Performance-bounded batches\":\"Prestatiegebonden batches\",\"Batch concurrency limit\":\"Limiet voor gelijktijdigheid van batches\",\"All visible\":\"Allemaal zichtbaar\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Alleen zichtbare kaarten ontvangen gedempte JPEG-frame-updates; ze pauzeren onmiddellijk buiten het scherm of op een verborgen tabblad. De gesynchroniseerde modus vernieuwt elke zichtbare kaart in één golf; hoge snelheden verhogen merkbaar de bandbreedte en het CPU-gebruik.\",\"Live video on hover\":\"Live video tijdens het zweven\",\"Play real-time video while hovering\":\"Speel realtime video af terwijl u zweeft\",\"Start delay\":\"Startvertraging\",\"Immediately\":\"Onmiddellijk\",\"Hover enlargement\":\"Zweefvergroting\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Alleen de kaart onder de wijzer speelt, altijd gedempt. Als u het tabblad verwijdert of verbergt, wordt de stream onmiddellijk gestopt.\",\"Live Preview Dock\":\"Dock voor livevoorbeelden\",\"Enable low-load live previews\":\"Schakel live previews met lage belasting in\",\"Concurrent previews\":\"Gelijktijdige voorbeelden\",\"Window size\":\"Venstergrootte\",\"Small\":\"Klein\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Voorvertoningen beginnen gedempt en pauzeren in verborgen tabbladen. Audio vereist altijd een expliciete klik.\",\"Quick info & chat focus\":\"Snelle info en chatfocus\",\"Enable model Quick View\":\"Schakel model Snelle weergave in\",\"Chat focus mode (opt-in)\":\"Chatfocusmodus (opt-in)\",\"Show local timestamps\":\"Toon lokale tijdstempels\",\"System messages\":\"Systeemberichten\",\"Dim\":\"Afm\",\"Hide\":\"Verbergen\",\"Chat filter strength\":\"Sterkte van het chatfilter\",\"Low\":\"Laag\",\"Strict\":\"Streng\",\"Highlight words (comma separated)\":\"Markeer woorden (door komma's gescheiden)\",\"Muted words (comma separated)\":\"Gedempte woorden (door komma's gescheiden)\",\"Collapse repeated messages\":\"Herhaalde berichten samenvouwen\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Chatverbeteringen veranderen alleen de huidige weergave. Ze slaan nooit automatisch chat op, verzenden geen berichten en geven nooit fooi.\",\"Shortcuts\":\"Snelkoppelingen\",\"Command menu\":\"Commandomenu\",\"Global model search\":\"Globaal zoeken naar modellen\",\"Reset design defaults\":\"Ontwerpstandaarden opnieuw instellen\",\"Optional modules\":\"Optionele modules\",\"Normal-page dock and card actions\":\"Dock- en kaartacties voor normale pagina's\",\"Real audio meters\":\"Echte audiometers\",\"Media, app and contest directories\":\"Media-, app- en wedstrijdgidsen\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Moduleschakelaars worden volledig van kracht nadat de pagina opnieuw is geladen. Kerngegevens en back-upmodules blijven ingeschakeld om gegevensverlies te voorkomen.\",\"Name the current filters and sorting\":\"Geef de huidige filters en sortering een naam\",\"My smart view\":\"Mijn slimme kijk\",\"Delete smart view\":\"Slimme weergave verwijderen\",\"Smart view deleted\":\"Slimme weergave verwijderd\",\"tags\":\"labels\",\"countries\":\"landen\",\"Saved selection\":\"Opgeslagen selectie\",\"Suggestions\":\"Suggesties\",\"Selected\":\"Gekozen\",\"Website tags\":\"Website-tags\",\"Countries (multiple selection)\":\"Landen (meerdere selectie)\",\"Select countries\":\"Selecteer landen\",\"Search countries…\":\"Landen zoeken…\",\"Recognized in current models\":\"Herkend in de huidige modellen\",\"All countries\":\"Alle landen\",\"Tags (multiple selection)\":\"Tags (meerdere selectie)\",\"Select tags\":\"Selecteer tags\",\"Search tags…\":\"Tags zoeken…\",\"Room subject\":\"Onderwerp kamer\",\"Current online duration\":\"Huidige onlineduur\",\"Local watch time and sessions\":\"Lokale kijktijd en sessies\",\"HD, new, gaming and favorite badges\":\"HD, nieuw, gaming en favoriete badges\",\"Homepage Studio\":\"Startpaginastudio\",\"Balanced preset\":\"Evenwichtige voorinstelling\",\"Compact preset\":\"Compacte voorinstelling\",\"Data-rich preset\":\"Gegevensrijke voorinstelling\",\"Cinema preset\":\"Voorinstelling voor bioscoop\",\"Layout and pagination\":\"Lay-out en paginering\",\"Show quick toolbar above models\":\"Toon snelle werkbalk boven modellen\",\"Cards per row\":\"Kaarten per rij\",\"Image ratio\":\"Beeldverhouding\",\"Information display\":\"Informatieweergave\",\"Detailed\":\"Gedetailleerd\",\"Overlay on image\":\"Overlay op afbeelding\",\"Powerful sorting\":\"Krachtig sorteren\",\"Primary sort\":\"Primaire sortering\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Sorteren verandert alleen de weergavevolgorde van geladen modelkaarten. Het verandert niets aan het aantal volgers of favorieten in uw Chaturbate-account.\",\"Restore website order\":\"Herstel de volgorde van de website\",\"Clear all filters\":\"Wis alle filters\",\"Search and range filters\":\"Zoek- en bereikfilters\",\"When several tags are selected\":\"Wanneer er meerdere tags zijn geselecteerd\",\"Model must contain every tag\":\"Het model moet elke tag bevatten\",\"At least one tag matches\":\"Er komt ten minste één tag overeen\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Landfiltering toont modellen uit elk geselecteerd land. Eigen tags en herkende websitetags blijven beschikbaar in de lijst en zijn snel te vinden met zoeken.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Na het selecteren van tags of landen doorzoekt RoomGrid de volledige actuele live catalogus. Dezelfde resultatenpagina toont in eerste instantie 120 overeenkomsten en laadt er meer in stukjes tijdens het scrollen, dus zelfs veelgebruikte tags blijven reageren. De herlaadknop in de werkbalk van de startpagina vernieuwt de livelijst.\",\"Quick conditions\":\"Snelle voorwaarden\",\"HD models only\":\"Alleen HD-modellen\",\"New models only\":\"Alleen nieuwe modellen\",\"Gaming models only\":\"Alleen gamingmodellen\",\"Only saved in RoomGrid\":\"Alleen opgeslagen in RoomGrid\",\"Only own favorites\":\"Alleen eigen favorieten\",\"Following only\":\"Alleen volgen\",\"Hide previously watched models\":\"Verberg eerder bekeken modellen\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Land, leeftijd en onlineduur komen uit de huidige openbare kaarten of openbare ruimtelijst. Als Chaturbate geen veld verstrekt, raadt RoomGrid het niet.\",\"Smart filter views\":\"Slimme filterweergaven\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Sla de huidige filters en sortering op als een dynamische weergave. Modellen die later online komen en aan de regels voldoen, verschijnen automatisch.\",\"Save current filter view\":\"Bewaar de huidige filterweergave\",\"No smart views saved yet.\":\"Er zijn nog geen slimme weergaven opgeslagen.\",\"What to show for each model\":\"Wat u voor elk model moet laten zien\",\"Import complete\":\"Importeren voltooid\",\"Invalid file\":\"Ongeldig bestand\",\"Range\":\"Bereik\",\"Today\":\"Vandaag\",\"Month\":\"Maand\",\"Year\":\"Jaar\",\"Recording enabled\":\"Opname ingeschakeld\",\"Private session (do not record)\":\"Privésessie (niet opnemen)\",\"Retention days\":\"Retentie dagen\",\"Usage time\":\"Gebruikstijd\",\"Summed camera time\":\"Opgetelde cameratijd\",\"Visits\":\"Bezoeken\",\"Known models\":\"Bekende modellen\",\"Weekly heatmap\":\"Wekelijkse hittekaart\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Geaggregeerd per weekdag en uur; donkerder betekent meer waargenomen tijd.\",\"Sunday to Saturday, 24 hours per row\":\"Zondag t/m zaterdag, 24 uur per rij\",\"Watch ranking\":\"Bekijk de ranglijst\",\"little data\":\"weinig gegevens\",\"No watch history yet.\":\"Nog geen kijkgeschiedenis.\",\"Workspace, filter and quality events\":\"Werkruimte-, filter- en kwaliteitsgebeurtenissen\",\"Recent timeline\":\"Recente tijdlijn\",\"Export sensitive statistics\":\"Exporteer gevoelige statistieken\",\"Import statistics\":\"Statistieken importeren\",\"Delete selected range\":\"Geselecteerd bereik verwijderen\",\"Delete local events in the selected range?\":\"Lokale gebeurtenissen in het geselecteerde bereik verwijderen?\",\"Clear all statistics\":\"Wis alle statistieken\",\"Permanently clear all local statistics?\":\"Alle lokale statistieken permanent wissen?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"Statistische exports bevatten modelnamen en tijdstempels en moeten als gevoelig worden behandeld. Oude gegevens worden opgeschoond volgens retentie.\",\"new\":\"nieuw\",\"Under 1 minute\":\"Minder dan 1 minuut\",\"1–5 minutes\":\"1–5 minuten\",\"5–15 minutes\":\"5–15 minuten\",\"15–30 minutes\":\"15–30 minuten\",\"30+ minutes\":\"30+ minuten\",\"Local observations and your usage\":\"Lokale observaties en uw gebruik\",\"Public profile loaded on request\":\"Openbaar profiel geladen op verzoek\",\"Local RoomGrid interaction\":\"Lokale RoomGrid-interactie\",\"Local browser player\":\"Lokale browserspeler\",\"Optional official statistics API\":\"Optionele officiële statistieken-API\",\"Local daily aggregation\":\"Lokale dagelijkse aggregatie\",\"Local data\":\"Lokale gegevens\",\"Model summaries\":\"Samenvattingen van modellen\",\"Public profiles\":\"Openbare profielen\",\"Usage events\":\"Gebruiksgebeurtenissen\",\"Official account snapshots\":\"Officiële accountmomentopnamen\",\"Daily rollups\":\"Dagelijkse roll-ups\",\"Camera time\":\"Cameratijd\",\"vs previous period\":\"versus vorige periode\",\"Daily activity\":\"Dagelijkse activiteit\",\"Playback quality\":\"Afspeelkwaliteit\",\"Privacy status\":\"Privacystatus\",\"Data provenance\":\"Gegevens herkomst\",\"Local RoomGrid usage\":\"Lokaal RoomGrid-gebruik\",\"7 days\":\"7 dagen\",\"30 days\":\"30 dagen\",\"90 days\":\"90 dagen\",\"Not enough data yet.\":\"Nog niet genoeg gegevens.\",\"Models revisited\":\"Modellen opnieuw bekeken\",\"Previews opened\":\"Previews geopend\",\"Quick Views\":\"Snelle weergaven\",\"Samples\":\"Monsters\",\"Private session\":\"Privé sessie\",\"History paused\":\"De geschiedenis is onderbroken\",\"Recording locally\":\"Lokaal opnemen\",\"Disabled\":\"Gehandicapt\",\"No cloud sync or telemetry.\":\"Geen cloudsynchronisatie of telemetrie.\",\"Raw events\":\"Ruwe gebeurtenissen\",\"Only local observations and actions from this browser.\":\"Alleen lokale observaties en acties vanuit deze browser.\",\"Time\":\"Tijd\",\"Models\":\"Modellen\",\"Online planner\":\"Onlineplanner\",\"Data & privacy\":\"Gegevens en privacy\",\"Configure statistics dashboard\":\"Statistiekdashboard configureren\",\"Preset\":\"Voorinstelling\",\"Minimal\":\"Minimaal\",\"Quality focus\":\"Kwaliteitsfocus\",\"Privacy focus\":\"Privacyfocus\",\"Reset dashboard\":\"Dashboard opnieuw instellen\",\"Size\":\"Maat\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Volgorde, zichtbaarheid en breedte worden opgeslagen in update-veilige instellingen. Elke widget toont zijn gegevensbron.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Er zijn geen dashboardwidgets zichtbaar. Open de configuratie en selecteer minimaal één widget.\",\"Median session\":\"Mediane sessie\",\"Daily usage trend\":\"Dagelijkse gebruikstrend\",\"Your trend appears here after you start using RoomGrid.\":\"Uw trend verschijnt hier nadat u RoomGrid gaat gebruiken.\",\"Session-duration distribution\":\"Verdeling van sessieduur\",\"No session-duration data yet.\":\"Nog geen gegevens over de sessieduur.\",\"Return rate\":\"Retourtarief\",\"Total sessions\":\"Totaal aantal sessies\",\"Longest observed\":\"Langst waargenomen\",\"Model ranking\":\"Modelrangschikking\",\"Select up to four models for a local comparison.\":\"Selecteer maximaal vier modellen voor een lokale vergelijking.\",\"Quick View\":\"Snelle weergave\",\"No model data yet.\":\"Nog geen modelgegevens.\",\"Models with time patterns\":\"Modellen met tijdpatronen\",\"High confidence\":\"Hoog vertrouwen\",\"Moderate confidence\":\"Matig vertrouwen\",\"Planning horizon\":\"Planningshorizon\",\"Likely next active times\":\"Waarschijnlijk volgende actieve tijden\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Berekend op basis van lokaal waargenomen online monsters; het garandeert geen uitzending. Het vertrouwen neemt toe naarmate er meer data beschikbaar zijn.\",\"High\":\"Hoog\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Nog niet genoeg online observaties. De planner verschijnt automatisch bij de bibliotheek.\",\"Discovery events\":\"Ontdekkingsevenementen\",\"Local interaction funnel\":\"Lokale interactietrechter\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Alleen uw acties op dit apparaat worden geteld; chatinhoud en betalingen worden nooit bijgehouden.\",\"Workspace events\":\"Werkruimte-evenementen\",\"Filter events\":\"Gebeurtenissen filteren\",\"Tag events\":\"Gebeurtenissen taggen\",\"Top-four workspace comparison\":\"Top-vier vergelijking van werkruimten\",\"Workspace usage\":\"Gebruik van de werkruimte\",\"No workspace data yet.\":\"Nog geen werkruimtegegevens.\",\"Average active streams\":\"Gemiddelde actieve streams\",\"Playback quality by model\":\"Afspeelkwaliteit per model\",\"No quality data yet.\":\"Nog geen kwaliteitsgegevens.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"Het statistiekbestand is ongeldig of overschrijdt de veilige limieten. De huidige gegevens zijn niet gewijzigd.\",\"Statistics import assistant · preview\":\"Assistent voor het importeren van statistieken · voorbeeld\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Er is nog niets veranderd. Selecteer de secties die u wilt importeren en bekijken.\",\"Safe merge (recommended)\":\"Veilig samenvoegen (aanbevolen)\",\"Import complete. The previous statistics were backed up automatically.\":\"Importeren voltooid. Er werd automatisch een back-up gemaakt van de eerdere statistieken.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"De import kon niet veilig worden toegepast. Huidige gegevens en back-ups blijven ongewijzigd.\",\"File schema\":\"Bestandsschema\",\"Date range\":\"Datumbereik\",\"Sensitive data\":\"Gevoelige gegevens\",\"Not marked sensitive\":\"Niet gemarkeerd als gevoelig\",\"Until manually resumed\":\"Totdat het handmatig wordt hervat\",\"Not paused\":\"Niet gepauzeerd\",\"Restore this statistics backup? The current state will be backed up first.\":\"Deze statistiekenback-up herstellen? Er wordt eerst een back-up gemaakt van de huidige status.\",\"Permanently delete this backup?\":\"Deze back-up definitief verwijderen?\",\"Events\":\"Evenementen\",\"Estimated statistics storage\":\"Geschatte opslag van statistieken\",\"Recording, pauses and private sessions\":\"Opnemen, pauzeren en privésessies\",\"Record usage statistics\":\"Registreer gebruiksstatistieken\",\"Pause 15 minutes\":\"Pauzeer 15 minuten\",\"Pause 1 hour\":\"Pauze 1 uur\",\"Pause 8 hours\":\"Pauze 8 uur\",\"Pause indefinitely\":\"Pauzeer voor onbepaalde tijd\",\"Resume history\":\"Hervat de geschiedenis\",\"History status\":\"Geschiedenisstatus\",\"Private for 30 minutes\":\"Privé gedurende 30 minuten\",\"Private for 2 hours\":\"Privé voor 2 uur\",\"Private until resumed\":\"Privé totdat het wordt hervat\",\"End private session\":\"Privésessie beëindigen\",\"Not active\":\"Niet actief\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Een privésessie stopt ook publieke modelobservaties; een normale geschiedenispauze stopt persoonlijk gebruik en kwaliteitsrecords.\",\"Automatic deletion and retention\":\"Automatische verwijdering en retentie\",\"Raw events (days)\":\"Ruwe gebeurtenissen (dagen)\",\"Quality samples (days)\":\"Kwaliteitsmonsters (dagen)\",\"Account snapshots (days)\":\"Accountmomentopnamen (dagen)\",\"Daily rollups (days)\":\"Dagelijkse rollups (dagen)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Verlopen onbewerkte gebeurtenissen worden opgenomen in de dagelijkse totalen voordat ze automatisch worden verwijderd. Kwaliteits- en accountmomentopnamen hanteren hun eigen limieten.\",\"Statistics backup center\":\"Back-upcentrum voor statistieken\",\"Create internal backup now\":\"Maak nu een interne back-up\",\"Pseudonymized export\":\"Gepseudonimiseerde export\",\"Print report\":\"Rapport afdrukken\",\"Choose import file\":\"Kies importbestand\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Er zijn nog geen back-ups van interne statistieken. Er worden er maximaal drie bewaard na import, herstel of handmatige creatie.\",\"Delete local statistics in this range? A backup will be created first.\":\"Lokale statistieken in dit bereik verwijderen? Er wordt eerst een back-up gemaakt.\",\"Clear everything\":\"Wis alles\",\"Permanently clear all local statistics? A backup will be created first.\":\"Alle lokale statistieken permanent wissen? Er wordt eerst een back-up gemaakt.\",\"Data inventory and provenance\":\"Gegevensinventaris en herkomst\",\"Stored only in this browser\":\"Alleen opgeslagen in deze browser\",\"Coverage\":\"Dekking\",\"Unknown or missing data is never inferred.\":\"Onbekende of ontbrekende gegevens worden nooit afgeleid.\",\"Metric definitions & data quality\":\"Metriekdefinities en gegevenskwaliteit\",\"Accumulated locally while a RoomGrid page is active.\":\"Lokaal verzameld terwijl een RoomGrid-pagina actief is.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Som van alle waargenomen camerasessies; gelijktijdige stromen worden toegevoegd.\",\"browser samples; no interpolation.\":\"browservoorbeelden; geen interpolatie.\",\"Privacy boundary\":\"Privacygrens\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Er worden geen chatinhoud, betalingen of mediabestanden opgeslagen. Privésessies voegen geen evenementen toe.\",\"No history yet\":\"Nog geen geschiedenis\",\"Loading\":\"Laden\",\"Connected\":\"Aangesloten\",\"Locked\":\"Vergrendeld\",\"Not connected\":\"Niet verbonden\",\"Username\":\"Gebruikersnaam\",\"24-character stats token\":\"Statistiektoken van 24 tekens\",\"At least 8 characters\":\"Minimaal 8 tekens\",\"Secure official statistics connection\":\"Beveiligde officiële statistiekenverbinding\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"Optioneel. Het token voert nooit de normale instellingen, logboeken of exports in. De sessiemodus bewaart het alleen in het geheugen; de gecodeerde modus maakt gebruik van geïsoleerde lokale opslag.\",\"Stats token\":\"Statistiekentoken\",\"Vault password\":\"Kluiswachtwoord\",\"Connect for this session\":\"Maak verbinding voor deze sessie\",\"Username or token is invalid.\":\"Gebruikersnaam of token is ongeldig.\",\"Encrypt, save & connect\":\"Versleutelen, opslaan en verbinden\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Kon niet versleuteld opslaan. Controleer de token-, wachtwoord- en userscript-manager-machtigingen.\",\"Get official token\":\"Ontvang een officieel token\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"Automatische vernieuwing wordt maximaal één keer per vijf minuten uitgevoerd. Deze module gebruikt alleen een officieel token dat u opzettelijk verstrekt.\",\"Account\":\"Rekening\",\"History snapshots\":\"Momentopnamen van de geschiedenis\",\"Last refresh\":\"Laatste vernieuwing\",\"Unlock local vault\":\"Ontgrendel de lokale kluis\",\"Password\":\"Wachtwoord\",\"Unlock\":\"Ontgrendelen\",\"Unlock failed.\":\"Ontgrendelen mislukt.\",\"Loading…\":\"Laden…\",\"Refresh now\":\"Vernieuw nu\",\"Auto-refresh every 5 minutes\":\"Automatische vernieuwing elke 5 minuten\",\"Disconnect and delete token\":\"Ontkoppel en verwijder het token\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"De lokale verbinding en het gecodeerde token verwijderen? Historische statistieken blijven bestaan.\",\"Broadcast status\":\"Uitzendstatus\",\"Token balance\":\"Token-saldo\",\"Tips last hour\":\"Tips laatste uur\",\"Followers\":\"Volgers\",\"Up votes\":\"Stemmen omhoog\",\"Down votes\":\"Neerstemmen\",\"Satisfaction\":\"Tevredenheid\",\"Registered viewers\":\"Geregistreerde kijkers\",\"Tokened viewers\":\"Getokende kijkers\",\"Time online\":\"Tijd online\",\"Follower trend\":\"Volger trend\",\"Tips trend\":\"Fooientrend\",\"Token-balance trend\":\"Token-saldo-trend\",\"Viewer trend\":\"Kijkerstrend\",\"Account Health Center\":\"Account Gezondheidscentrum\",\"Official My Collection\":\"Officieel Mijn collectie\",\"Official support\":\"Officiële ondersteuning\",\"Export local statistics\":\"Exporteer lokale statistieken\",\"Delete snapshot history\":\"Verwijder de geschiedenis van momentopnamen\",\"Delete the local official-statistics snapshots for this account?\":\"De lokale momentopnamen van officiële statistieken voor dit account verwijderen?\",\"Last error\":\"Laatste fout\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Tokenstatus, vernieuwingscadans, lokale geschiedenis en officiële links zijn hier te vinden. RoomGrid publiceert, verzendt of voert nooit automatisch transacties uit.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"De browser heeft geen toestemming voor meldingen verleend. Meldingen op de pagina blijven actief.\",\"Notification profile\":\"Meldingsprofiel\",\"Go-live only\":\"Alleen live gaan\",\"Personalized\":\"Gepersonaliseerd\",\"Always (all events)\":\"Altijd (alle evenementen)\",\"Inactive days\":\"Inactieve dagen\",\"Favorites, rating, priority and your tags decide relevance.\":\"Favorieten, beoordeling, prioriteit en uw tags bepalen de relevantie.\",\"Includes go-live, offline and public-profile changes.\":\"Inclusief go-live, offline en openbare profielwijzigingen.\",\"Master switch\":\"Hoofdschakelaar\",\"On\":\"Op\",\"Off (default)\":\"Uit (standaard)\",\"Monitored models\":\"Gecontroleerde modellen\",\"Active tab\":\"Actief tabblad\",\"This tab\":\"Dit tabblad\",\"Another tab\":\"Nog een tabblad\",\"Local notifications\":\"Lokale meldingen\",\"Notification delivery and display\":\"Kennisgeving levering en weergave\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"In-page meldingen gebruiken het vrije gebied in de RoomGrid-bovenbalk en hebben geen browsertoestemming nodig. Positie, duur en dichtheid zijn configureerbaar.\",\"Delivery\":\"Levering\",\"RoomGrid bar only\":\"Alleen RoomGrid-balk\",\"Browser notifications only\":\"Alleen browsermeldingen\",\"RoomGrid bar and browser\":\"RoomGrid-balk en browser\",\"No display, history only\":\"Geen weergave, alleen geschiedenis\",\"In-page position\":\"Positie op de pagina\",\"Free top-bar area\":\"Gratis bovenste bargedeelte\",\"Floating top center\":\"Zwevende middenboven\",\"Floating top right\":\"Zwevend rechtsboven\",\"Display style\":\"Weergavestijl\",\"Compact single line\":\"Compacte enkele lijn\",\"Display duration\":\"Weergaveduur\",\"Until dismissed\":\"Tot ontslag\",\"Visible at once\":\"In één keer zichtbaar\",\"Test notification\":\"Testmelding\",\"General rules\":\"Algemene regels\",\"Enable notifications\":\"Schakel meldingen in\",\"The browser did not grant notification permission.\":\"De browser heeft geen toestemming voor meldingen verleend.\",\"Neutral text\":\"Neutrale tekst\",\"Quiet time starts\":\"De stille tijd begint\",\"Quiet time ends\":\"De stille tijd eindigt\",\"Cooldown (minutes)\":\"Afkoelperiode (minuten)\",\"Hourly limit\":\"Uurlimiet\",\"Favorite threshold\":\"Favoriete drempel\",\"Open on click\":\"Openen met klikken\",\"Per model\":\"Voor elk model\",\"Library is empty.\":\"Bibliotheek is leeg.\",\"Per group\":\"Per groep\",\"Group\":\"Groep\",\"No groups.\":\"Geen groepen.\",\"Local notification list\":\"Lokale notificatielijst\",\"Mark all read\":\"Markeer alles als gelezen\",\"Clear list\":\"Duidelijke lijst\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Achtergrondcontroles lezen alleen de status en starten geen video. Een gebruikersscript kan alleen worden uitgevoerd als een gerelateerd sitetabblad geopend is; meerdere tabbladen kiezen automatisch één controleur.\",\"Shield status\":\"Schildstatus\",\"Ready\":\"Klaar\",\"Local PIN\":\"Lokale pincode\",\"Not set\":\"Niet ingesteld\",\"Quick shield\":\"Snel schild\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"Dekt onmiddellijk namen en afbeeldingen af ​​en pauzeert/dempt alle media. Bij het herstellen wordt de vorige mediastatus geretourneerd.\",\"Activate now\":\"Activeer nu\",\"Auto-shield after inactivity\":\"Automatische afscherming na inactiviteit\",\"Set or change local PIN\":\"Lokale pincode instellen of wijzigen\",\"Leave empty to remove\":\"Laat leeg om te verwijderen\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"De pincode is slechts een lokale toegangsbarrière, geen codering, en vervangt de accountbeveiliging van het besturingssysteem niet.\",\"History controls\":\"Geschiedeniscontroles\",\"Usage statistics\":\"Gebruiksstatistieken\",\"Pause history 1 hour\":\"Geschiedenis pauzeren 1 uur\",\"Clear statistics\":\"Duidelijke statistieken\",\"Clear all local statistics? A backup will be created first.\":\"Alle lokale statistieken wissen? Er wordt eerst een back-up gemaakt.\",\"Clear Discover settings\":\"Wis Discover-instellingen\",\"Clear local Discover filters and feeds?\":\"Lokaal wissen Filters en feeds ontdekken?\",\"Clear notification list\":\"Duidelijke notificatielijst\",\"Data boundary\":\"Gegevensgrens\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Configuratie, kijkstatistieken, notificatiegeschiedenis en Discover-voorkeuren worden lokaal in deze browser opgeslagen. RoomGrid bevat geen cloudsynchronisatie, accountupload of telemetrie.\",\"Quota\":\"Quotum\",\"Used\":\"Gebruikt\",\"Sensitivity & consent center\":\"Gevoeligheids- en toestemmingscentrum\",\"Standard configuration\":\"Standaard configuratie\",\"Theme, layout, filters\":\"Thema, lay-out, filters\",\"Local\":\"Lokaal\",\"Model names, notes, timestamps, statistics\":\"Modelnamen, opmerkingen, tijdstempels, statistieken\",\"Warn on export\":\"Waarschuw bij export\",\"Secret\":\"Geheim\",\"Optional official statistics token\":\"Optioneel officieel statistiektoken\",\"Session-only or encrypted vault\":\"Alleen sessie- of gecodeerde kluis\",\"Chat content\":\"Chatinhoud\",\"Never persisted\":\"Nooit volgehouden\",\"Ephemeral\":\"Kortstondig\",\"Stream recording/screenshots\":\"Stream opname/screenshots\",\"Not provided\":\"Niet voorzien\",\"Policy guard\":\"Beleidsbewaker\",\"Performance profile\":\"Prestatieprofiel\",\"Playing streams\":\"Streams afspelen\",\"Video elements\":\"Video-elementen\",\"Measured memory\":\"Gemeten geheugen\",\"Unavailable in browser\":\"Niet beschikbaar in browser\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"De huidige metingen liggen dichtbij de geconfigureerde limiet. Pauzeer ongebruikte camera's of kies de Eco-modus.\",\"Stream performance\":\"Stream prestaties\",\"Power\":\"Stroom\",\"Concurrent stream limit\":\"Limiet voor gelijktijdige streams\",\"Adaptive quality\":\"Adaptieve kwaliteit\",\"Gradual startup\":\"Geleidelijke opstart\",\"Automatic performance mode\":\"Automatische prestatiemodus\",\"Memory threshold\":\"Geheugendrempel\",\"Stream-count threshold\":\"Drempel voor het aantal streams\",\"Current auto state\":\"Huidige automatische status\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Ongebruikte voorbeelden worden gestopt. Nieuwe video blijft op de limiet gepauzeerd totdat er capaciteit beschikbaar is. Hoge tellingen beginnen geleidelijk; er is geen “onbeperkte camera’s” garantie.\",\"Multi-window workspaces\":\"Werkruimten met meerdere vensters\",\"Window name\":\"Vensternaam\",\"Active and recent windows\":\"Actieve en recente vensters\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"De modelbibliotheek wordt gesynchroniseerd via lokale samenvoeging; elk venster behoudt zijn eigen werkruimte en weergave-instellingen. Recente vensterassociaties kunnen na het opnieuw opstarten worden hersteld als de browser dit toestaat.\",\"Optional tools are disabled\":\"Optionele tools zijn uitgeschakeld\",\"Media, app and contest directories will not scan the current page.\":\"Media-, app- en wedstrijdmappen scannen de huidige pagina niet.\",\"Enable optional tools\":\"Schakel optionele tools in\",\"Local search\":\"Lokaal zoeken\",\"Search saved items and current page\":\"Zoek opgeslagen items en huidige pagina\",\"Rescan current page\":\"Scan de huidige pagina opnieuw\",\"Disable optional tools\":\"Schakel optionele tools uit\",\"Clear local side data\":\"Wis lokale gegevens\",\"Clear app favorites, categories and media notes?\":\"App-favorieten, categorieën en medianotities wissen?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Deze modules organiseren alleen openbare links en tekst die al toegankelijk zijn op de huidige pagina. Ze kopiëren geen media, downloaden niet automatisch en bedienen geen chat-, token- of betalingsfuncties. Inloggen, leeftijdsverificatie, beveiliging en betalingsformulieren blijven onaangetast.\",\"Photo and video areas\":\"Foto- en videoruimtes\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Op deze pagina is geen herkenbaar openbaar mediagebied aanwezig. Scan opnieuw terwijl u zich op een eigen foto- of videopagina bevindt.\",\"Apps directory\":\"Apps-map\",\"Local category\":\"Lokale categorie\",\"Found on page\":\"Gevonden op pagina\",\"No recognizable app links are present on this page.\":\"Er zijn geen herkenbare app-links aanwezig op deze pagina.\",\"Public contest results\":\"Resultaten openbare wedstrijd\",\"No readable public contest table is present on this page.\":\"Op deze pagina is geen leesbare openbare wedstrijdtabel aanwezig.\",\"Native account, help and broadcaster links\":\"Native account-, help- en omroeplinks\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Er zijn geen veilige native links gevonden op deze pagina; betalings- en inlogpaden worden opzettelijk gefilterd.\",\"Homepage filters reset\":\"Homepaginafilters worden gereset\",\"Global appearance reset\":\"Globaal uiterlijk gereset\",\"Version\":\"Versie\",\"Health score\":\"Gezondheidsscore\",\"Page type\":\"Paginatype\",\"Route generation\":\"Routegeneratie\",\"Running modules\":\"Modules uitvoeren\",\"Local errors\":\"Lokale fouten\",\"Repair center\":\"Reparatiecentrum\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Voor elke reparatie wordt een configuratieback-up gemaakt. Statistieken en modelgegevens worden door deze acties niet verwijderd.\",\"Reset home filters\":\"Thuisfilters opnieuw instellen\",\"Reset all home-page filters?\":\"Alle startpaginafilters resetten?\",\"Reset global appearance\":\"Globaal uiterlijk resetten\",\"Reset the global appearance? Saved design profiles remain.\":\"Het globale uiterlijk resetten? Opgeslagen ontwerpprofielen blijven behouden.\",\"Normalize configuration\":\"Normaliseer de configuratie\",\"Validate and rewrite the current configuration? A backup is created first.\":\"De huidige configuratie valideren en herschrijven? Er wordt eerst een back-up gemaakt.\",\"Close all previews\":\"Sluit alle voorbeelden\",\"Compatibility self-check\":\"Compatibiliteit zelfcontrole\",\"Module and page status\":\"Module- en paginastatus\",\"Page route test matrix\":\"Paginaroutetestmatrix\",\"Current route\":\"Huidige route\",\"Trigger\":\"Trekker\",\"Recent page transitions\":\"Recente paginaovergangen\",\"Capability layer\":\"Capaciteitslaag\",\"Available\":\"Beschikbaar\",\"Fallback\":\"Terugval\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Wanneer een API ontbreekt, valt alleen de betreffende module terug. Herhaalde netwerkstoringen openen tijdelijk een stroomonderbreker.\",\"Backup and recovery status\":\"Back-up- en herstelstatus\",\"Primary data\":\"Primaire gegevens\",\"Stable archive\":\"Stabiel archief\",\"Rolling backups\":\"Rollende back-ups\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"De zelfcontrole verifieert alleen de leesbaarheid en browsermogelijkheden; het overschrijft nooit automatisch de huidige gegevens. Herstel blijft gebruik maken van de bestaande bevestigde back-upstroom.\",\"Sanitized local error log\":\"Opgeschoond lokaal foutenlogboek\",\"No RoomGrid errors recorded.\":\"Er zijn geen RoomGrid-fouten geregistreerd.\",\"Run self-check again\":\"Voer de zelfcontrole opnieuw uit\",\"Export diagnostics\":\"Diagnostische gegevens exporteren\",\"Clear error log\":\"Foutenlogboek wissen\",\"Return to normal site\":\"Keer terug naar de normale locatie\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"De diagnostische export bevat geen werkruimte, modelnamen, opmerkingen of statistieken. URL-queryparameters en mogelijke geheime waarden worden uit de fouttekst verwijderd.\",\"Local history on\":\"Lokale geschiedenis aan\",\"History off\":\"Geschiedenis uitgeschakeld\",\"This settings page could not be loaded\":\"Deze instellingenpagina kan niet worden geladen\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Uw gegevens zijn niet gewijzigd. Probeer het opnieuw; Als het probleem blijft bestaan, opent u Diagnostiek.\",\"Try again\":\"Probeer het opnieuw\",\"Open Diagnostics\":\"Open Diagnostische gegevens\",\"Public results\":\"Publieke resultaten\",\"Public contest links\":\"Openbare wedstrijdlinks\",\"Page content has not appeared yet\":\"Pagina-inhoud is nog niet verschenen\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid heeft de pagina opnieuw gecontroleerd, maar kan het inhoudsgebied van Chaturbate nog steeds niet vinden. Het kan zijn dat de pagina nog steeds wordt geladen of dat de site een lege weergave retourneert.\",\"Check again\":\"Controleer opnieuw\",\"Open RoomGrid Discover\":\"Open RoomGrid Ontdek\",\"Reload page\":\"Pagina opnieuw laden\",\"Hide notice\":\"Melding verbergen\",\"Open MultiCam\":\"MultiCam openen\",\"Open workstation in a new window\":\"Open het werkstation in een nieuw venster\",\"Open Discover\":\"Open Ontdek\",\"Statistics center\":\"Statistieken centrum\",\"Backup and import\":\"Back-up en import\",\"Page and design options\":\"Pagina- en ontwerpopties\",\"Hide immediately\":\"Verberg je onmiddellijk\",\"All tools\":\"Alle gereedschappen\",\"Search & commands\":\"Zoeken en opdrachten\",\"Page design\":\"Pagina-ontwerp\",\"Official collection\":\"Officiële collectie\",\"Saved\":\"Opgeslagen\",\"Quick info\":\"Snelle informatie\",\"Loading live frame\":\"Liveframe laden\",\"Live still\":\"Leef nog steeds\",\"Not live\":\"Niet levend\",\"Pages\":\"Pagina's\",\"Refresh\":\"Vernieuwen\",\"Search…\":\"Zoekopdracht…\",\"Tag Explorer\":\"Tagverkenner\",\"Refresh public catalog\":\"Vernieuw de openbare catalogus\",\"Toggle favorite\":\"Schakel favoriet in\",\"Save\":\"Redden\",\"Remove favorite\":\"Favoriet verwijderen\",\"Removed from library\":\"Verwijderd uit bibliotheek\",\"Saved to library\":\"Opgeslagen in bibliotheek\",\"Added to favorites\":\"Toegevoegd aan favorieten\",\"Removed from favorites\":\"Verwijderd uit favorieten\",\"Watching\":\"Kijken\",\"Watch\":\"Horloge\",\"Temporary watchlist\":\"Tijdelijke volglijst\",\"Removed from watchlist\":\"Verwijderd van volglijst\",\"Added to watchlist\":\"Toegevoegd aan volglijst\",\"Choose group\":\"Kies groep\",\"Add to group…\":\"Toevoegen aan groep…\",\"Group updated\":\"Groep bijgewerkt\",\"Site room\":\"Locatie kamer\",\"Select for bulk actions\":\"Selecteer voor bulkacties\",\"viewers\":\"kijkers\",\"No public description\":\"Geen openbare beschrijving\",\"Open in Tag Explorer\":\"Openen in Tagverkenner\",\"Feed name\":\"Feednaam\",\"View name\":\"Naam bekijken\",\"Name already exists\":\"Naam bestaat al\",\"Saved-item limit reached\":\"Limiet voor opgeslagen items bereikt\",\"Feed saved\":\"Voer opgeslagen\",\"View saved\":\"Bekijk opgeslagen\",\"Applied\":\"Toegepast\",\"Backup exported\":\"Back-up geëxporteerd\",\"Backup imported\":\"Back-up geïmporteerd\",\"Invalid backup\":\"Ongeldige back-up\",\"New models\":\"Nieuwe modellen\",\"New since last visit\":\"Nieuw sinds laatste bezoek\",\"New to me\":\"Nieuw voor mij\",\"Returning\":\"Terugkerend\",\"Stable online\":\"Stabiel online\",\"Saved only\":\"Alleen opgeslagen\",\"Previously watched\":\"Eerder bekeken\",\"Local collection\":\"Lokale collectie\",\"Hide visited today\":\"Verberg vandaag bezocht\",\"No feed selected\":\"Geen feed geselecteerd\",\"No view selected\":\"Geen weergave geselecteerd\",\"Saved feeds\":\"Opgeslagen feeds\",\"Saved views\":\"Opgeslagen weergaven\",\"Save new\":\"Nieuw opslaan\",\"All public rooms\":\"Alle openbare ruimtes\",\"Known HD\":\"Bekende HD\",\"Random result\":\"Willekeurig resultaat\",\"Viewers ↓\":\"Kijkers ↓\",\"Viewers ↑\":\"Kijkers ↑\",\"Online duration ↓\":\"Onlineduur ↓\",\"Followers ↓\":\"Volgers ↓\",\"Name A–Z\":\"Naam A-Z\",\"Name Z–A\":\"Naam Z–A\",\"New first\":\"Nieuw eerst\",\"Watch time ↓\":\"Kijktijd ↓\",\"Sessions ↓\":\"Sessies ↓\",\"Last watched\":\"Laatst bekeken\",\"Name, description, tag, place\":\"Naam, beschrijving, tag, plaats\",\"Secondary sort\":\"Secundaire sortering\",\"Shuffle again\":\"Opnieuw schudden\",\"All groups\":\"Alle groepen\",\"All tags (AND)\":\"Alle tags (EN)\",\"Any tag (OR)\":\"Elke tag (OF)\",\"Excluded tags (NOT)\":\"Uitgesloten tags (NIET)\",\"Language, region, or country\":\"Taal, regio of land\",\"Minimum online minutes\":\"Minimale online minuten\",\"Maximum online minutes\":\"Maximale online minuten\",\"Minimum local watch minutes\":\"Minimale lokale kijkminuten\",\"Advanced filters and Boolean tags\":\"Geavanceerde filters en Booleaanse tags\",\"Saved views and feeds\":\"Opgeslagen weergaven en feeds\",\"Import\":\"Importeren\",\"Recent searches\":\"Recente zoekopdrachten\",\"Live previews and performance\":\"Live previews en optredens\",\"Muted live preview for every visible card\":\"Gedempte live preview voor elke zichtbare kaart\",\"Refresh rate\":\"Vernieuwingsfrequentie\",\"Real-time video while hovering\":\"Realtime video terwijl u zweeft\",\"Hover delay\":\"Zweefvertraging\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Zichtbare kaarten gebruiken JPEG-frames. Bij het zweven schakelt slechts één kaart over naar gedempte realtime video en stopt volledig wanneer de aanwijzer weggaat. Hoge framesnelheden verhogen de bandbreedte en het CPU-gebruik aanzienlijk.\",\"saved\":\"opgeslagen\",\"previews\":\"voorvertoningen\",\"Previews off\":\"Voorvertoningen uitgeschakeld\",\"No more feeds can be saved\":\"Er kunnen geen feeds meer worden opgeslagen\",\"Tag feed saved\":\"Tagfeed opgeslagen\",\"Select tag\":\"Selecteer label\",\"Tag favorite\":\"Tag favoriet\",\"Block tag\":\"Blokkeer tag\",\"Select tags first\":\"Selecteer eerst tags\",\"Tag-group name\":\"Naam van taggroep\",\"Tag group saved\":\"Taggroep opgeslagen\",\"No gaming rooms in the current catalog\":\"Geen speelkamers in de huidige catalogus\",\"All (AND)\":\"Alles (EN)\",\"Any (OR)\":\"Elke (OF)\",\"Exclude (NOT)\":\"Uitsluiten (NIET)\",\"Search tags\":\"Tags zoeken\",\"Tag search\":\"Zoeken op tags\",\"Combination\":\"Combinatie\",\"Model sorting\":\"Sorteren van modellen\",\"Show in Discover\":\"Toon in Ontdek\",\"Save tag group\":\"Taggroep opslaan\",\"Save tag feed\":\"Tagfeed opslaan\",\"No tag groups saved yet.\":\"Er zijn nog geen taggroepen opgeslagen.\",\"Unknown region\":\"Onbekende regio\",\"Unknown age\":\"Onbekende leeftijd\",\"Public “New”\":\"Openbaar “Nieuw”\",\"My new cams\":\"Mijn nieuwe camera's\",\"New-cams feed saved\":\"Feed voor nieuwe cams opgeslagen\",\"Save “My new cams” feed\":\"Sla de feed \\\"Mijn nieuwe cams\\\" op\",\"Create gaming workspace\":\"Creëer een gaming-werkruimte\",\"No matches for this combination.\":\"Geen overeenkomsten voor deze combinatie.\",\"Select tags to preview matching rooms here.\":\"Selecteer hier tags om een ​​voorbeeld van overeenkomende kamers te bekijken.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Zoek, favoriet, blokkeer en combineer openbare tags. Trends gebruiken alleen cataloguswijzigingen die op dit apparaat zijn waargenomen.\",\"Current tags\":\"Huidige tags\",\"Counts show current matches and change since the prior fetch.\":\"Tellingen tonen huidige overeenkomsten en veranderingen sinds de vorige ophaalactie.\",\"Results for selected tags\":\"Resultaten voor geselecteerde tags\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Gebruikt dezelfde sortering en SingleCam-navigatie als Discover.\",\"Categories, age, and regions\":\"Categorieën, leeftijd en regio's\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Alleen expliciete openbare velden worden getoond. Door komma's gescheiden gebieden worden gecombineerd met OR; onbekende waarden blijven zichtbaar. Er wordt geen tijdzone geraden.\",\"New and gaming\":\"Nieuw en gamen\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"Lokaal voor het eerst gezien, de publieke ‘Nieuwe’ vlag, tijdelijke volglijst en gamingthema’s blijven gescheiden.\",\"Tag groups\":\"Tag groepen\",\"Save AND, OR, or NOT combinations.\":\"Bewaar EN-, OF- of NIET-combinaties.\",\"Favorite tags\":\"Favoriete tags\",\"Your personal shortcut list.\":\"Uw persoonlijke snelkoppelingenlijst.\",\"No favorites yet\":\"Nog geen favorieten\",\"Related tags\":\"Gerelateerde tags\",\"Tags co-occurring on the currently matching models.\":\"Tags die gelijktijdig voorkomen op de momenteel overeenkomende modellen.\",\"Personal tag history\":\"Persoonlijke taggeschiedenis\",\"Recently and frequently used on this device.\":\"Recent en vaak gebruikt op dit apparaat.\",\"Bulk target group\":\"Bulkdoelgroep\",\"Selected models added to group\":\"Geselecteerde modellen toegevoegd aan groep\",\"New workspace name\":\"Nieuwe naam van de werkruimte\",\"Created from a Discover selection\":\"Gemaakt op basis van een Discover-selectie\",\"Workspace created\":\"Werkruimte gecreëerd\",\"Save all\":\"Bewaar alles\",\"Create workspace\":\"Creëer werkruimte\",\"Loading public catalog…\":\"Openbare catalogus laden…\",\"Loading the complete catalog in the background…\":\"De volledige catalogus op de achtergrond laden…\",\"Updated\":\"Bijgewerkt\",\"Public rooms\":\"Openbare kamers\",\"The catalog is loading.\":\"De catalogus wordt geladen.\",\"No matching rooms. Adjust search or filters.\":\"Geen overeenkomende kamers. Pas zoekopdracht of filters aan.\",\"Previous\":\"Vorig\",\"Next\":\"Volgende\",\"Move up\":\"Ga omhoog\",\"Move down\":\"Ga naar beneden\",\"Number shown\":\"Nummer getoond\",\"Widget width\":\"Widgetbreedte\",\"Full\":\"Vol\",\"Nothing here yet.\":\"Hier nog niets.\",\"Newly online\":\"Nieuw online\",\"Recently visited\":\"Onlangs bezocht\",\"Frequently visited\":\"Vaak bezocht\",\"Homepage appearance, pagination, and performance\":\"Uiterlijk, paginering en prestaties van de startpagina\",\"Information density\":\"Informatiedichtheid\",\"Layout\":\"Indeling\",\"Data rich\":\"Gegevens rijk\",\"Static\":\"Statisch\",\"Show statistics summary\":\"Statistiekoverzicht weergeven\",\"Show personal widgets\":\"Toon persoonlijke widgets\",\"Sections below the live catalog\":\"Secties onder de live catalogus\",\"Homepage live models\":\"Startpagina live modellen\",\"All live rooms in one place\":\"Alle liverooms op één plek\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Gebruik dezelfde volledige filters voor zoeken, sorteren, groepen, tags en lokale geschiedenis als Discover.\",\"Refresh live catalog\":\"Vernieuw de live catalogus\",\"Homepage settings\":\"Instellingen van de startpagina\",\"Matches a favorite gaming theme\":\"Past bij een favoriet gamethema\",\"Saved models\":\"Opgeslagen modellen\",\"Online favorites\":\"Online-favorieten\",\"Local watch time\":\"Lokale kijktijd\",\"Continue last MultiCam workspace\":\"Ga verder met de laatste MultiCam-werkruimte\",\"Open full Discover page\":\"Open de volledige Ontdek-pagina\",\"Online notifications\":\"Online-meldingen\",\"on\":\"op\",\"off\":\"uit\",\"Offline notifications\":\"Offlinemeldingen\",\"Feed\":\"Voer\",\"Saved filter views\":\"Opgeslagen filterweergaven\",\"Favorites currently public and online.\":\"Favorieten momenteel openbaar en online.\",\"New models or public streams that started recently.\":\"Nieuwe modellen of openbare streams die onlangs zijn gestart.\",\"Current results from the selected feed.\":\"Huidige resultaten van de geselecteerde feed.\",\"Suggestions with reasons\":\"Suggesties met redenen\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Alleen gebaseerd op gedeelde openbare tags en lokaal favoriete thema's; geen identiteitsconclusie.\",\"Sorted by local last-visit time.\":\"Gesorteerd op lokale tijd van laatste bezoek.\",\"Sorted by local watch time.\":\"Gesorteerd op lokale kijktijd.\",\"Saved models never visited\":\"Opgeslagen modellen zijn nooit bezocht\",\"Saved in the library without local watch time.\":\"Opgeslagen in de bibliotheek zonder lokale kijktijd.\",\"Draggable dashboard widgets\":\"Versleepbare dashboardwidgets\",\"Your RoomGrid homepage\":\"Uw RoomGrid-startpagina\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"Een snelle, aanpasbare livecatalogus met SingleCam, MultiCam, volledige filters, opgeslagen weergaven en uw persoonlijke widgets.\",\"Audio activity\":\"Audio-activiteit\",\"Auto quality\":\"Automatische kwaliteit\",\"Stream interrupted; status checks continue.\":\"Stream onderbroken; statuscontroles gaan door.\",\"This browser cannot play HLS.\":\"Deze browser kan HLS niet afspelen.\",\"Connecting to live stream…\":\"Verbinding maken met livestream…\",\"The room is not public right now.\":\"De kamer is momenteel niet openbaar.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Momenteel offline. Alternatieven uit hetzelfde filter worden hieronder weergegeven.\",\"Status temporarily unavailable; retrying automatically.\":\"Status tijdelijk niet beschikbaar; automatisch opnieuw proberen.\",\"Loading public profile…\":\"Openbaar profiel laden…\",\"Profile temporarily unavailable.\":\"Profiel tijdelijk niet beschikbaar.\",\"No cached profile.\":\"Geen profiel in cache.\",\"Public profile changed\":\"Openbaar profiel gewijzigd\",\"No public bio.\":\"Geen openbare biografie.\",\"Source\":\"Bron\",\"Observed\":\"Waargenomen\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Alleen links die zelf op het openbare profiel zijn gepubliceerd, worden weergegeven. Alleen een overeenkomende gebruikersnaam is geen identiteitsbewijs.\",\"Profile information\":\"Profielinformatie\",\"Preparing…\":\"Voorbereiden…\",\"Play\":\"Toneelstuk\",\"Unmute\":\"Dempen opheffen\",\"Mute\":\"Stom\",\"Contain\":\"Bevatten\",\"Cover\":\"Omslag\",\"Brightness\":\"Helderheid\",\"Saturation\":\"Verzadiging\",\"Hue\":\"Tint\",\"Grayscale\":\"Grijstinten\",\"Invert\":\"Omkeren\",\"Blur\":\"Vervagen\",\"Image zoom\":\"Beeldzoom\",\"Recording saved\":\"Opname opgeslagen\",\"Screenshot saved\":\"Schermafbeelding opgeslagen\",\"Recording\":\"Opname\",\"Mirror\":\"Spiegel\",\"Flip vertically\":\"Verticaal omdraaien\",\"Rotate\":\"Draaien\",\"Show edited\":\"Toon bewerkt\",\"Show original\":\"Origineel tonen\",\"Reset image\":\"Afbeelding opnieuw instellen\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Houd Ctrl ingedrukt en scroll om te zoomen; sleep de afbeelding wanneer ingezoomd. Dubbelklik om te resetten.\",\"Zoom out\":\"Uitzoomen\",\"Zoom in\":\"Inzoomen\",\"Reset zoom and position\":\"Zoom en positie opnieuw instellen\",\"Player window\":\"Spelervenster\",\"Fullscreen\":\"Volledig scherm\",\"Private notes, memories, or things to check next time (stored locally)\":\"Privénotities, herinneringen of dingen om de volgende keer te controleren (lokaal opgeslagen)\",\"Autosave is on\":\"Automatisch opslaan staat aan\",\"Enter your own tag…\":\"Vul je eigen tag in...\",\"Saved at\":\"Opgeslagen bij\",\"Local details saved\":\"Lokale gegevens opgeslagen\",\"Remove tag\":\"Label verwijderen\",\"No personal tags yet.\":\"Nog geen persoonlijke tags.\",\"Saving…\":\"Besparing…\",\"Create your first group under Groups.\":\"Maak uw eerste groep aan onder Groepen.\",\"Added to MultiCam\":\"Toegevoegd aan MultiCam\",\"Edit private details\":\"Privégegevens bewerken\",\"Region\":\"Regio\",\"Current online time\":\"Huidige online tijd\",\"Stream attributes\":\"Stream-attributen\",\"New\":\"Nieuw\",\"Yes\":\"Ja\",\"No / unknown\":\"Nee / onbekend\",\"Last observed\":\"Laatst waargenomen\",\"Local visits\":\"Lokale bezoeken\",\"First watched\":\"Eerst bekeken\",\"No public description.\":\"Geen openbare beschrijving.\",\"No public tags available.\":\"Geen openbare tags beschikbaar.\",\"Quick-add your own tag…\":\"Voeg snel uw eigen tag toe…\",\"Public tags\":\"Openbare tags\",\"Add\":\"Toevoegen\",\"Private details\":\"Privégegevens\",\"Everything stays in this browser and survives updates.\":\"Alles blijft in deze browser en overleeft updates.\",\"Private notebook\":\"Privé notitieboekje\",\"Keep on watchlist\":\"Blijf op de volglijst\",\"Group assignment (multiple)\":\"Groepsopdracht (meerdere)\",\"Save now\":\"Bespaar nu\",\"Chat loads only when this section is opened.\":\"Chat wordt alleen geladen als deze sectie wordt geopend.\",\"Official chat\":\"Officiële chat\",\"Official chat connected.\":\"Officiële chat verbonden.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Chat geladen; de huidige paginastructuur kon niet volledig worden geïsoleerd.\",\"Chat loaded.\":\"Chat geladen.\",\"Loading official chat…\":\"Officiële chat laden…\",\"Official chat room\":\"Officiële chatroom\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Alleen geladen wanneer dat nodig is; RoomGrid bewaart nooit chatberichten en verzendt deze ook niet automatisch.\",\"Reload chat\":\"Chat opnieuw laden\",\"Open official room in a new window\":\"Open de officiële ruimte in een nieuw venster\",\"Hide chat\":\"Chat verbergen\",\"Live observations\":\"Live observaties\",\"Trend confidence\":\"Trendvertrouwen\",\"Known time slots\":\"Bekende tijdslots\",\"Typical peak\":\"Typische piek\",\"Watched in 7 days\":\"In 7 dagen bekeken\",\"Watched in 30 days\":\"Binnen 30 dagen bekeken\",\"Current streak\":\"Huidige reeks\",\"Typical weekly online times\":\"Typische wekelijkse online tijden\",\"Online times will be learned on this device from now on.\":\"Online tijden worden vanaf nu op dit apparaat geleerd.\",\"Based on observations on this device; missing information is never guessed.\":\"Gebaseerd op observaties op dit apparaat; ontbrekende informatie wordt nooit geraden.\",\"Activity by weekday\":\"Activiteit per weekdag\",\"Most likely online times\":\"Waarschijnlijk online tijden\",\"Online-time map\":\"Online-tijdkaart\",\"Public covers and locally saved example images.\":\"Openbare covers en lokaal opgeslagen voorbeeldafbeeldingen.\",\"Import quality\":\"Kwaliteit importeren\",\"Original file\":\"Origineel bestand\",\"Very high (up to 4K)\":\"Zeer hoog (tot 4K)\",\"High (up to 1440p)\":\"Hoog (tot 1440p)\",\"Space-saving (up to 900p)\":\"Ruimtebesparend (tot 900p)\",\"Add images from computer\":\"Voeg afbeeldingen toe vanaf de computer\",\"Use website cover\":\"Gebruik websiteomslag\",\"Use first image as cover\":\"Gebruik de eerste afbeelding als omslag\",\"Covers and local gallery\":\"Covers en lokale galerij\",\"Updating images…\":\"Afbeeldingen bijwerken…\",\"Loading images…\":\"Afbeeldingen laden…\",\"Own image\":\"Eigen afbeelding\",\"Example image\":\"Voorbeeld afbeelding\",\"Current cover\":\"Huidige dekking\",\"Alternative preview\":\"Alternatief voorbeeld\",\"Current public image\":\"Huidig ​​publiek imago\",\"Saving cover…\":\"Dekking opslaan…\",\"Set as cover\":\"Instellen als dekking\",\"Delete this image from the local gallery?\":\"Deze afbeelding verwijderen uit de lokale galerij?\",\"No images yet.\":\"Nog geen afbeeldingen.\",\"saved locally\":\"lokaal opgeslagen\",\"own images\":\"eigen afbeeldingen\",\"Update public previews\":\"Openbare voorbeelden bijwerken\",\"Processing your images…\":\"Uw afbeeldingen verwerken…\",\"ALL PAGE SOUNDS OFF\":\"ALLE PAGINA KLINKT UIT\",\"PAGE SOUNDS ALLOWED\":\"PAGINAGELUIDEN TOEGESTAAN\",\"Chat/token sounds OFF\":\"Chat/token klinkt UIT\",\"Chat/token sounds ON\":\"Chat-/tokengeluiden AAN\",\"Reconnecting the player with the new settings…\":\"De speler opnieuw verbinden met de nieuwe instellingen...\",\"Turn off chat, token and tip sounds completely\":\"Schakel chat-, token- en fooigeluiden volledig uit\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Blokkeert paginageluiden van audio-elementen en webaudio. Stream audio blijft afzonderlijk regelbaar via ‘Geluid aan’.\",\"No alternatives available.\":\"Geen alternatieven beschikbaar.\",\"Similar models and offline alternatives\":\"Soortgelijke modellen en offline alternatieven\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Geprioriteerd door gedeelde tags; geen identiteit of gevolgtrekking van buitenaf.\",\"Chat\":\"Chatten\",\"Notes & organization\":\"Notities & organisatie\",\"Images\":\"Afbeeldingen\",\"SingleCam sections\":\"SingleCam-secties\",\"Back to original results\":\"Terug naar originele resultaten\",\"Private workspace\":\"Privé werkruimte\",\"Chat on/off\":\"Chatten aan/uit\",\"Hide similar models\":\"Verberg vergelijkbare modellen\",\"Show similar models\":\"Toon vergelijkbare modellen\",\"RoomGrid Home\":\"RoomGrid Thuis\",\"RoomGrid Tags\":\"RoomGrid-tags\",\"Public catalog unavailable; showing the local library.\":\"Openbare catalogus niet beschikbaar; toont de plaatselijke bibliotheek.\",\"Drag to resize sidebar\":\"Sleep om de grootte van de zijbalk te wijzigen\",\"The workspace could not be saved safely. Export a backup now.\":\"De werkruimte kon niet veilig worden gered. Exporteer nu een back-up.\",\"Open menu or restore controls\":\"Open het menu of herstel de bedieningselementen\",\"Controls restored\":\"Controles hersteld\",\"Show toolbar\":\"Werkbalk tonen\",\"Tools\":\"Hulpmiddelen\",\"Show sidebar\":\"Zijbalk tonen\",\"Restore controls\":\"Herstel controles\",\"Show toolbar, groups, and menus\":\"Werkbalk, groepen en menu's weergeven\",\"Open more tools\":\"Meer hulpmiddelen openen\",\"all watch states\":\"alle horlogestatussen\",\"Watch status\":\"Bekijk de status\",\"0 means no viewer limit.\":\"0 betekent geen kijkerslimiet.\",\"Import temporary URLs; they disappear after refresh\":\"Tijdelijke URL's importeren; ze verdwijnen na vernieuwen\",\"Filter current group\":\"Filter huidige groep\",\"All status\":\"Alle status\",\"Hide offline/private\":\"Offline/privé verbergen\",\"Collapse toolbar\":\"Werkbalk samenvouwen\",\"Windows per page\":\"Vensters per pagina\",\"Custom grid\":\"Aangepast raster\",\"Choose MultiCam audio routing\":\"Kies MultiCam-audiorouting\",\"Audio: manual\":\"Audio: handmatig\",\"Audio: solo cam\":\"Audio: solo-camera\",\"Audio: follow focus\":\"Audio: volg focus\",\"Userscript version\":\"Gebruikersscriptversie\",\"Open Discover and dashboard\":\"Open Discover en dashboard\",\"Global search and quick commands · Ctrl+K\":\"Globaal zoeken en snelle opdrachten · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Stream gezondheid, kwaliteitsbudget, audio, scènes en vervangingen\",\"Control\":\"Controle\",\"Rooms\":\"Kamers\",\"Playback\":\"Afspelen\",\"Stabilizing live preview…\":\"Live voorbeeld stabiliseren…\",\"Recovering live preview…\":\"Live voorbeeld herstellen…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"Live preview wordt kort gepauzeerd om een ​​herlaadlus te voorkomen. Probeer het binnenkort opnieuw.\",\"Collapse groups\":\"Groepen samenvouwen\",\"Expand/collapse users\":\"Gebruikers uitvouwen/samenvouwen\",\"Output volume; no live signal level is available for this stream\":\"Uitgangsvolume; er is geen live signaalniveau beschikbaar voor deze stream\",\"OFF\":\"UIT\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"Plak URL's. Kamerpagina's, m3u8, mp4/webm worden ondersteund. Normale pagina's worden zo goed mogelijk geparseerd.\",\"Actual resolution, connection and buffer\":\"Actuele resolutie, verbinding en buffer\",\"Drag to reorder / move group\":\"Sleep om de groep opnieuw te ordenen/verplaatsen\",\"All time\":\"Altijd\",\"Click a model for details.\":\"Klik op een model voor details.\",\"Most watched groups\":\"Meest bekeken groepen\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Accountvelden zoals de toetredingsdatum worden alleen weergegeven als Chaturbate ze duidelijk op dezelfde pagina weergeeft. Het script bedenkt geen waarden.\",\"Back\":\"Rug\",\"Statistics import failed: \":\"Statistieken importeren mislukt:\",\"Recent saves\":\"Recente saves\",\"Rooms per page\":\"Kamers per pagina\",\"Custom rows\":\"Aangepaste rijen\",\"Custom columns\":\"Aangepaste kolommen\",\"Main width\":\"Hoofdbreedte\",\"Main height\":\"Belangrijkste hoogte\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Modules starten alleen op overeenkomende paginatypen. Door op te slaan wordt de huidige pagina opnieuw geladen.\",\"Tools on normal pages\":\"Hulpmiddelen op normale pagina's\",\"True audio-level meters\":\"Echte audioniveaumeters\",\"MultiCam control center\":\"MultiCam-controlecentrum\",\"No active streams yet\":\"Nog geen actieve streams\",\"Stream health\":\"Gezondheid streamen\",\"Budget off\":\"Begroting uit\",\"Economy · 2160p total\":\"Economie · 2160p totaal\",\"Balanced · 4320p total\":\"Gebalanceerd · 4320p totaal\",\"High · 6480p total\":\"Hoog · 6480p totaal\",\"Ultra · 8640p total\":\"Ultra · 8640p totaal\",\"Quality budget is off; global and per-camera limits still apply.\":\"Het kwaliteitsbudget is uitgeschakeld; Er zijn nog steeds algemene limieten en limieten per camera van toepassing.\",\"Quality budget\":\"Kwaliteitsbegroting\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Verdeelt kwaliteit over actieve cams. Een expliciete kwaliteit per camera wint altijd.\",\"Give the focused cam double weight\":\"Geef de gefocuste camera dubbel gewicht\",\"Manual per camera\":\"Handleiding per camera\",\"Solo: one camera only\":\"Solo: slechts één camera\",\"Follow focused camera\":\"Volg gerichte camera\",\"Audio management\":\"Audiobeheer\",\"Routing never overwrites saved mute states or individual volume.\":\"Routing overschrijft nooit opgeslagen mute-statussen of individueel volume.\",\"Mute all\":\"Demp alles\",\"Restore saved audio\":\"Herstel opgeslagen audio\",\"Scene name\":\"Naam van de scène\",\"Grid\":\"Rooster\",\"cams\":\"camera's\",\"Load\":\"Laden\",\"No scenes saved for this workspace yet.\":\"Er zijn nog geen scènes opgeslagen voor deze werkruimte.\",\"Scenes\":\"Scènes\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Bewaar de huidige slots, lay-out en audioroutering voor herstel met één klik.\",\"Save current view as scene\":\"Sla de huidige weergave op als scène\",\"All saved online models\":\"Alle opgeslagen online modellen\",\"Replacement models\":\"Vervangende modellen\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Voorkeursmodellen worden in lijstvolgorde uitgeprobeerd; alleen dan worden online modellen uit de geselecteerde groep gebruikt. Handmatige opdrachten winnen altijd.\",\"Replace offline slots automatically\":\"Vervang offline slots automatisch\",\"Delay (seconds)\":\"Vertraging (seconden)\",\"Candidate group\":\"Kandidaat groep\",\"Preferred replacements (multiple)\":\"Voorkeursvervangers (meerdere)\",\"Replacement rule saved\":\"Vervangingsregel opgeslagen\",\"Save replacement rule\":\"Vervangingsregel opslaan\",\"Resolve duplicate shortcuts first:\":\"Los eerst dubbele snelkoppelingen op:\",\"Name, note or own tag…\":\"Naam, notitie of eigen tag…\",\"Camera menu\":\"Cameramenu\",\"Camera menu · quality and sound\":\"Cameramenu · kwaliteit en geluid\",\"Close menu\":\"Menu sluiten\",\"View and window\":\"Uitzicht en raam\",\"Quality and image\":\"Kwaliteit en imago\",\"More actions\":\"Meer acties\",\"Currently main\":\"Momenteel hoofd\",\"Set as main\":\"Instellen als hoofd\",\"Pin live preview\":\"Live voorbeeld vastzetten\",\"Volume multiplier for this cam\":\"Volumevermenigvuldiger voor deze camera\",\"Reset individual volume to 100%\":\"Individueel volume resetten naar 100%\",\"Individual volume\":\"Individueel volume\",\"Listen to this cam only\":\"Luister alleen naar deze camera\",\"Set quality for this camera only\":\"Stel de kwaliteit alleen voor deze camera in\",\"Automatic (global/adaptive)\":\"Automatisch (globaal/adaptief)\",\" · available\":\"· beschikbaar\",\"Quality for this camera\":\"Kwaliteit voor deze camera\",\"Hold for original\":\"Houd ingedrukt voor origineel\",\"Vivid\":\"Levendig\",\"Image adjustments\":\"Beeldaanpassingen\",\"More tools\":\"Meer gereedschap\",\"Show top controls\":\"Toon bovenste bedieningselementen\",\"Collapse top controls\":\"Bovenste bedieningselementen samenvouwen\",\"Show groups\":\"Groepen weergeven\",\"Windows\":\"Ramen\",\"Open workspace in new window\":\"Open de werkruimte in een nieuw venster\",\"Control center\":\"Controlecentrum\",\"Choose the data to export\":\"Kies de gegevens die u wilt exporteren\",\"Preview the file and conflicts before applying it\":\"Bekijk een voorbeeld van het bestand en de conflicten voordat u het toepast\",\"Help\":\"Hulp\",\"Changes in this version\":\"Wijzigingen in deze versie\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Werkruimten, modelbibliotheek, veilige import, globaal ontwerp, uitgebreide normale pagina's, uitgebreide Discover/dashboard en bedieningselementen voor spelerkwaliteit/afbeelding.\",\"Muted\":\"Gedempt\",\"Performance limit reached\":\"Prestatielimiet bereikt\",\"Drag to resize main width\":\"Sleep om de hoofdbreedte te wijzigen\",\"Drag to resize main height\":\"Sleep om de hoofdhoogte te wijzigen\",\"Stable\":\"Stabiel\",\"Unstable\":\"Onstabiel\",\"Refresh all cameras\":\"Vernieuw alle camera's\",\"Reconnect current models\":\"Sluit huidige modellen opnieuw aan\",\"Switch to grid view\":\"Schakel over naar rasterweergave\",\"Switch to focus view\":\"Schakel over naar focusweergave\",\"Manage workspaces\":\"Beheer werkruimtes\",\"Create, duplicate, archive and device layouts\":\"Creëer, dupliceer, archiveer en apparaatlay-outs\",\"Open model library\":\"Modelbibliotheek openen\",\"Notes, tags, ratings and bulk actions\":\"Notities, tags, beoordelingen en bulkacties\",\"Design options\":\"Ontwerpopties\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Alles-in-één multiview-werkstation voor ruimtes, opnames, waarschuwingen, screenshots, opnieuw verbinden en datatools.\",\"Username ↵\":\"Gebruikersnaam ↵\",\"Search rooms\":\"Zoek kamers\",\"Invalid username\":\"Ongeldige gebruikersnaam\",\"Hide offline\":\"Offline verbergen\",\"Hide private\":\"Privé verbergen\",\"Manual\":\"Handmatig\",\"By status\":\"Op status\",\"By name\":\"Op naam\",\"By added time\":\"Door toegevoegde tijd\",\"By watch time\":\"Op kijktijd\",\"By sessions\":\"Per sessies\",\"By last watched\":\"Door laatst bekeken\",\"By first watched\":\"Door eerst gekeken\",\"By viewer count\":\"Op basis van het aantal kijkers\",\"By last online\":\"Door laatst online\",\"Advanced filters\":\"Geavanceerde filters\",\"Watched before\":\"Eerder bekeken\",\"Currently in view\":\"Momenteel in zicht\",\"Include tag\":\"Inclusief label\",\"Exclude tag\":\"Label uitsluiten\",\"Minimum watch time (minutes)\":\"Minimale kijktijd (minuten)\",\"Watched within days\":\"Binnen enkele dagen bekeken\",\"Refresh all\":\"Vernieuw alles\",\"Online alerts\":\"Online waarschuwingen\",\"Offline alerts\":\"Offline waarschuwingen\",\"Desktop notification + card flash when a model goes online\":\"Desktopmelding + kaartflits wanneer een model online gaat\",\"Desktop notification + card flash when a model goes offline\":\"Desktopmelding + kaartflits wanneer een model offline gaat\",\"Dark mode\":\"Donkere modus\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Gebruik een donker interfacethema. Stream video en audio zijn ongewijzigd.\",\"Dark mode enabled\":\"Donkere modus ingeschakeld\",\"Light mode enabled\":\"Lichtmodus ingeschakeld\",\"Mute token/tip sounds\":\"Demp token-/tipgeluiden\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Blokkeert alleen chaturbate-tipmeldingsgeluiden. Audio streamen blijft ingeschakeld.\",\"Token/tip sounds muted\":\"Token/fooi klinkt gedempt\",\"Token/tip sounds enabled\":\"Token-/fooigeluiden ingeschakeld\",\"Clean\":\"Schoon\",\"Clean mode on\":\"Schone modus ingeschakeld\",\"Exit clean mode\":\"Verlaat de schone modus\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Verberg tijdelijk alle bedieningselementen en overlays. Sneltoets: Alt+P / Alt+C. Druk op Esc om af te sluiten.\",\"Window first\":\"Venster eerst\",\"Window-first mode on\":\"Window-first-modus ingeschakeld\",\"Exit window-first\":\"Verlaat het venster eerst\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Verberg de app-shell en houd kamervensters zichtbaar. Sneltoets: Alt+V.\",\"Show thumbnails\":\"Toon miniaturen\",\"Hide thumbnails\":\"Miniaturen verbergen\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Toon of verberg de miniatuurrail in de focusmodus. Sneltoets: Alt+T.\",\"Choose Focus rooms\":\"Kies Focuskamers\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Kies de kamers voor de Focus-modus. De eerste geselecteerde kamer is het hoofdscherm; gebruik ↑/↓ om de volgorde van de secundaire vensters in te stellen.\",\"Main screen\":\"Hoofdscherm\",\"Right window\":\"Rechter raam\",\"Bottom window\":\"Onderste raam\",\"Empty slot\":\"Lege sleuf\",\"Drag this model to a Focus slot\":\"Sleep dit model naar een Focus-slot\",\"Choose model for this slot\":\"Kies een model voor deze sleuf\",\"Select a saved model for this exact Focus slot.\":\"Selecteer een opgeslagen model voor dit exacte Focus-slot.\",\"Choose model for this window\":\"Kies een model voor dit raam\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Selecteer een opgeslagen model voor dit exacte rastervenster. Bestaande modellen blijven opgeslagen.\",\"Remove from this grid window\":\"Verwijder uit dit rastervenster\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Alleen dit slot is vrijgemaakt. Het opgeslagen model blijft in uw werkruimte staan.\",\"Show in grid window\":\"Weergeven in rastervenster\",\"Choose the exact active grid window for this model.\":\"Kies het exacte actieve rastervenster voor dit model.\",\"Current\":\"Huidig\",\"Model removed from this grid window.\":\"Model verwijderd uit dit rastervenster.\",\"No saved models in this group.\":\"Geen opgeslagen modellen in deze groep.\",\"Secondary window\":\"Secundair venster\",\"Use current page automatically\":\"Gebruik de huidige pagina automatisch\",\"Clear custom selection\":\"Aangepaste selectie wissen\",\"Apply Focus layout\":\"Focus-indeling toepassen\",\"No rooms are available in the current group and filter.\":\"Er zijn geen kamers beschikbaar in de huidige groep en filter.\",\"Fit: full image\":\"Pasvorm: volledig beeld\",\"Fit: fill window\":\"Passend: venster vullen\",\"Switch between full image and cropped fill.\":\"Schakel tussen volledige afbeelding en bijgesneden vulling.\",\"Playback settings\":\"Afspeelinstellingen\",\"Max stream quality\":\"Maximale streamkwaliteit\",\"Auto / no cap\":\"Automatisch / geen limiet\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + zoomwiel\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Zoom in op een kaart met Ctrl/Command + muiswiel, sleep terwijl u zoomt, dubbelklik om te resetten.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Verlaat de schone modus. Sneltoets: Alt+P / Alt+C of Esc.\",\"Clean · Alt+P/C / Esc\":\"Opschonen · Alt+P/C / Esc\",\"(synced from another tab)\":\"(gesynchroniseerd vanaf een ander tabblad)\",\"Type a username and press Enter. Shortcut: /\":\"Typ een gebruikersnaam en druk op Enter. Snelkoppeling: /\",\"Filter visible rooms by username\":\"Filter zichtbare kamers op gebruikersnaam\",\"Show / hide group sidebar\":\"Groepszijbalk tonen/verbergen\",\"Global volume. Per-room mute is still respected.\":\"Globaal volume. Mute per kamer wordt nog steeds gerespecteerd.\",\"Base tile size in grid view\":\"Grootte van de basistegel in rasterweergave\",\"Sort rooms in the current group\":\"Sorteer kamers in de huidige groep\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Hoogte hoofdscherm in focusmodus. U kunt het scheidingsteken ook slepen.\",\"Main-screen aspect ratio in Focus mode\":\"Beeldverhouding hoofdscherm in focusmodus\",\"Thumbnail width in Focus mode\":\"Breedte van de miniatuur in focusmodus\",\"Show every saved room. The remove button deletes globally in this view.\":\"Toon elke opgeslagen kamer. De verwijderknop verwijdert globaal in deze weergave.\",\"Create an isolated group\":\"Creëer een geïsoleerde groep\",\"More tools and maintenance\":\"Meer gereedschap en onderhoud\",\"GROUPS\":\"GROEPEN\",\"All saved\":\"Allemaal opgeslagen\",\"Default\":\"Standaard\",\"Online now\":\"Nu online\",\"New group name:\":\"Nieuwe groepsnaam:\",\"Rename\":\"Hernoemen\",\"Rename to:\":\"Hernoemen naar:\",\"Delete group\":\"Groep verwijderen\",\"Live preview\":\"Live-voorbeeld\",\"Loading preview…\":\"Voorbeeld laden…\",\"Private or away\":\"Privé of weg\",\"Preview could not be loaded\":\"Voorbeeld kan niet worden geladen\",\"Toggle mute\":\"Schakel dempen in\",\"Resume\":\"Cv\",\"Screenshot current frame\":\"Schermafbeelding huidige frame\",\"Record current card\":\"Registreer huidige kaart\",\"Stop recording\":\"Stop met opnemen\",\"Picture-in-Picture\":\"Beeld-in-beeld\",\"Remove from current group\":\"Verwijderen uit huidige groep\",\"Delete saved room\":\"Verwijder opgeslagen kamer\",\"Mirror image\":\"Spiegelbeeld\",\"Flip image\":\"Afbeelding omdraaien\",\"Rotate left\":\"Draai naar links\",\"Rotate right\":\"Draai naar rechts\",\"Reset view\":\"Weergave opnieuw instellen\",\"Copy username\":\"Kopieer gebruikersnaam\",\"Add favorite\":\"Favoriet toevoegen\",\"Move to favorites only\":\"Alleen naar favorieten gaan\",\"Copied\":\"Gekopieerd\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Schermafbeelding mislukt. Browser/CORS kan het tekenen van deze stream blokkeren.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"De opname is lokaal en slaat deze video op met de bijbehorende audio wanneer de browser deze weergeeft. Gebruik het alleen als u toestemming heeft om deze inhoud op te slaan. Doorgaan?\",\"Recording started\":\"De opname is gestart\",\"Recording is not supported for this video/browser\":\"Opnemen wordt niet ondersteund voor deze video/browser\",\"Recording segment saved\":\"Opnamesegment opgeslagen\",\"Final recording segment saved\":\"Laatste opnamesegment opgeslagen\",\"Recording paused; waiting for the stream to return\":\"Opname gepauzeerd; wachten tot de stroom terugkeert\",\"Recording resumed\":\"De opname is hervat\",\"Recording paused; click to stop\":\"Opname gepauzeerd; klik om te stoppen\",\"No recording data to save\":\"Er zijn geen opnamegegevens om op te slaan\",\"Recording settings saved\":\"Opname-instellingen opgeslagen\",\"Segment length in minutes (1-180):\":\"Segmentlengte in minuten (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Videobitsnelheid in Mbps (0,5-20):\",\"Warn before leaving while recording\":\"Waarschuw voordat u weggaat tijdens het opnemen\",\"Recording is still active. Leave anyway?\":\"De opname is nog steeds actief. Toch weggaan?\",\"Recording center\":\"Opnamecentrum\",\"No active recordings\":\"Geen actieve opnames\",\"Waiting for source\":\"Wachten op bron\",\"Saved segments\":\"Opgeslagen segmenten\",\"Duration\":\"Duur\",\"Bitrate\":\"Bitsnelheid\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Formaat: MP4 indien ondersteund door deze browser; anders de best ondersteunde terugval.\",\"Record current page\":\"Neem de huidige pagina op\",\"Record current group\":\"Neem de huidige groep op\",\"Record online rooms\":\"Neem online kamers op\",\"Stop all recordings\":\"Stop alle opnames\",\"Show recording only\":\"Alleen opname weergeven\",\"Show all rooms\":\"Toon alle kamers\",\"Open current page rooms\":\"Open de huidige paginakamers\",\"Move current page to group\":\"Verplaats huidige pagina naar groep\",\"Layout settings\":\"Lay-out instellingen\",\"Recording settings\":\"Opname-instellingen\",\"Save settings\":\"Instellingen opslaan\",\"Config backups\":\"Back-ups configureren\",\"No config backups found\":\"Geen configuratieback-ups gevonden\",\"Online/offline history\":\"Online/offline geschiedenis\",\"Group rules\":\"Groepsregels\",\"Keep favorites first\":\"Houd favorieten eerst\",\"Temporary URL manager\":\"Tijdelijke URL-beheerder\",\"Save current temporary URLs\":\"Bewaar huidige tijdelijke URL's\",\"No temporary URLs\":\"Geen tijdelijke URL's\",\"Share workspace\":\"Deel werkruimte\",\"Share current rooms and layout\":\"Deel huidige kamers en indeling\",\"Copy share link\":\"Kopieer de deellink\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Deze link herstelt kamers, groepen en indelingsinstellingen op een ander werkstationtabblad.\",\"Shared workspace imported\":\"Gedeelde werkruimte geïmporteerd\",\"Shared workspace link is invalid\":\"Link naar gedeelde werkruimte is ongeldig\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Klik op een veld en druk op de nieuwe snelkoppeling. Backspace maakt dat veld leeg.\",\"Reset shortcuts\":\"Snelkoppelingen opnieuw instellen\",\"Focus add box\":\"Focus toevoegen vak\",\"Grid view\":\"Rasterweergave\",\"Focus view\":\"Focusweergave\",\"Clean mode\":\"Schone modus\",\"Window-first mode\":\"Window-first-modus\",\"Toggle thumbnails\":\"Schakel tussen miniaturen\",\"Auto-detecting…\":\"Automatische detectie…\",\"No rooms in this group\":\"Geen kamers in deze groep\",\"Add a username at the top, switch group, or clear filters\":\"Voeg bovenaan een gebruikersnaam toe, schakel van groep of wis filters\",\"Open workstation\":\"Open werkstation\",\"Open here (replace page)\":\"Open hier (vervang pagina)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Deze pagina vervangen door het werkstation? Via de browser kunt u teruggaan.\",\"View saved list\":\"Bekijk de opgeslagen lijst\",\"Collapse dock\":\"Dok samenvouwen\",\"No room detected\":\"Geen kamer gedetecteerd\",\"Open here\":\"Open hier\",\"Add room\":\"Voeg kamer toe\",\"Remove room\":\"Kamer verwijderen\",\"Record in workstation\":\"Opnemen op werkstation\",\"Screenshot video\":\"Screenshotvideo\",\"Play / pause\":\"Afspelen/pauzeren\",\"Mute / unmute\":\"Dempen / dempen opheffen\",\"No playable video found on this page\":\"Er is geen afspeelbare video gevonden op deze pagina\",\"Recording intent queued in workstation\":\"Opnamedoel in wachtrij op werkstation\",\"Added\":\"Toegevoegd\",\"Already exists\":\"Bestaat al\",\"Failed\":\"Mislukt\",\"Add to workstation\":\"Toevoegen aan werkstation\",\"Already in workstation — click to remove\":\"Bevindt zich al op het werkstation: klik om te verwijderen\",\"Model online\":\"Modelleer online\",\"Model offline\":\"Offline modelleren\",\"Show fewer\":\"Laat minder zien\",\"No measurable audio at the moment\":\"Momenteel geen meetbaar geluid\",\"Playback paused\":\"Afspelen gepauzeerd\",\"Audio measurement unavailable or blocked\":\"Audiometing niet beschikbaar of geblokkeerd\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Toestemming voor melding geweigerd. Kaartflitser werkt nog steeds.\\n\\nU kunt dit inschakelen in de browserinstellingen.\",\"Batch add\":\"Batch toevoegen\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Plak gebruikersnamen, één per regel of gescheiden door spaties/komma's:\",\"Cancel\":\"Annuleren\",\"Change the language of RoomGrid and the website\":\"Wijzig de taal van RoomGrid en de website\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"Wijzig de RoomGrid-taal. Chaturbate blijft in de huidige websitetaal omdat er geen officiële sitetaal beschikbaar is.\",\"About\":\"Over\",\"Export config\":\"Configuratie exporteren\",\"Export usernames.txt\":\"Exporteer gebruikersnamen.txt\",\"Copy usernames\":\"Kopieer gebruikersnamen\",\"Unmute all\":\"Schakel alles uit\",\"⏸ Pause visible\":\"⏸ Pauze zichtbaar\",\"Resume visible\":\"CV zichtbaar\",\"Following\":\"Volgende\",\"Sync followed models now\":\"Sync volgde nu modellen\",\"✓ Auto-sync followed models\":\"✓ Volgde modellen automatisch synchroniseren\",\"☐ Auto-sync followed models\":\"☐ Automatische synchronisatie van gevolgde modellen\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Importeer uw momenteel gevolgde Chaturbate-modellen in de permanente volggroep. Alleen die groep wordt gesynchroniseerd.\",\"Syncing followed models…\":\"Gevolgde modellen synchroniseren…\",\"Automatic Following sync disabled\":\"Automatische volgsynchronisatie uitgeschakeld\",\"Automatic Following sync enabled\":\"Automatische volgsynchronisatie ingeschakeld\",\"Following sync needs an active Chaturbate login\":\"Voor de volgende synchronisatie is een actieve Chaturbate-login vereist\",\"Following sync failed safely; saved workspace data was not changed\":\"De volgende synchronisatie is veilig mislukt; opgeslagen werkruimtegegevens zijn niet gewijzigd\",\"Enable dark mode\":\"Schakel de donkere modus in\",\"Disable dark mode\":\"Schakel de donkere modus uit\",\"Status history\":\"Statusgeschiedenis\",\"Temporary URLs\":\"Tijdelijke URL's\",\"Shortcut panel\":\"Snelkoppelingspaneel\",\"Toggle video fit\":\"Schakel videopasvorm in\",\"Shortcuts / hints\":\"Snelkoppelingen / tips\",\"Ranking\":\"Ranglijst\",\"Model details\":\"Modeldetails\",\"My profile\":\"Mijn profiel\",\"Loading local statistics…\":\"Lokale statistieken laden…\",\"No watch data yet. Statistics start with this version.\":\"Nog geen kijkgegevens. Statistieken beginnen met deze versie.\",\"Statistics are disabled.\":\"Statistieken zijn uitgeschakeld.\",\"Enable statistics\":\"Statistieken inschakelen\",\"Disable statistics\":\"Statistieken uitschakelen\",\"Export statistics\":\"Statistieken exporteren\",\"Delete all statistics\":\"Verwijder alle statistieken\",\"Delete this model data\":\"Verwijder deze modelgegevens\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Alle lokaal verzamelde kijkstatistieken verwijderen? Uw werkruimte blijft onaangeroerd.\",\"Tracking since\":\"Volgt sinds\",\"Models watched\":\"Modellen keken\",\"Watch time\":\"Kijk naar de tijd\",\"Most watched tags\":\"Meest bekeken tags\",\"Most watched hours\":\"Meest bekeken uren\",\"Observed online\":\"Online geobserveerd\",\"Public bio\":\"Openbare biografie\",\"Public links\":\"Openbare links\",\"No public profile data cached yet.\":\"Er zijn nog geen openbare profielgegevens in de cache opgeslagen.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Alleen zichtbare, actief speelkaarten tellen. Hovervoorbeelden tellen niet mee.\",\"Statistics imported\":\"Statistieken geïmporteerd\",\"Statistics deleted\":\"Statistieken verwijderd\",\"Statistics exported\":\"Statistieken geëxporteerd\",\"Local observation only\":\"Alleen lokale observatie\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Er worden geen privéberichten, tokens, betalingen, wachtwoorden of externe tracking verzameld.\",\"Not enough observations for a time estimate\":\"Niet genoeg waarnemingen voor een tijdsschatting\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"Beweeg de muis over een knop/bediening om de hint te zien. Veelgebruikte snelkoppelingen kunnen worden bewerkt in het snelkoppelingenpaneel.\\n\\nStandaardinstellingen:\\n/ Focus gebruikersnaaminvoer\\nr Alles vernieuwen\\ng Rasterweergave\\nf Focusweergave\\nAlt+P of Alt+C Opschoningsmodus aan/uit\\nAlt+Shift+C Opnamecentrum\\nAlt+Shift+R Neem huidige pagina op\\nEsc Sluit de opschoningsmodus / volledig scherm af\\nSpatie Pauzeer/hervat het hoofdscherm in de focusweergave\\n←/→ of [/ ] Schakel naar het hoofdscherm in Focusweergave\\nDubbelklik op de kaart Volledig scherm\",\"Visible windows paused\":\"Zichtbare vensters zijn gepauzeerd\",\"Visible windows resumed\":\"Zichtbare vensters zijn hervat\",\"Import config\":\"Configuratie importeren\",\"Repair saved data\":\"Herstel opgeslagen gegevens\",\"Saved data has been normalized.\":\"Opgeslagen gegevens zijn genormaliseerd.\",\"Clear all data\":\"Wis alle gegevens\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Hierdoor worden ALLE kamers, groepen en instellingen gewist. Doorgaan?\",\"English\":\"Engels\",\"Deutsch\":\"Duits\",\"Author\":\"Auteur\",\"License\":\"Licentie\",\"If this script saves you time…\":\"Als dit script u tijd bespaart...\",\"Tip in ETH\":\"Fooi in ETH\",\"ETH address\":\"ETH-adres\",\"Copy\":\"Kopiëren\",\"Back online\":\"Terug online\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Gebaseerd op uw lokaal opgeslagen favorieten, tags en kijkactiviteit. Elke keuze laat zijn reden zien.\",\"Brave exposed no recordable video track.\":\"Brave heeft geen opneembare videotrack vrijgegeven.\",\"Black\":\"Zwart\",\"Brave or the video source blocked frame capture.\":\"Brave of de videobron blokkeerde het vastleggen van frames.\",\"Camclient\":\"Camclient\",\"Camclient as the real homepage\":\"Camclient als de echte homepage\",\"Browser exposed no audio track\":\"Browser heeft geen audiotrack zichtbaar\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient wijst deze transparante voorbeelden automatisch toe aan algemene tags. Gebruik de afbeeldingsknop op een tag om een ​​voorbeeld te kiezen, upload uw eigen PNG/WebP/SVG of verberg de afbeelding.\",\"Choose a Camclient example or upload your own transparent image.\":\"Kies een Camclient-voorbeeld of upload uw eigen transparante afbeelding.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"Kies een PNG, JPEG, WebP of veilige SVG kleiner dan 1,1 MB.\",\"Choose artwork for tag\":\"Kies een afbeelding voor de tag\",\"Click to enable meter\":\"Klik om de meter in te schakelen\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Kleuren, typografie, afstanden, hoeken, schaduwen en achtergronden zijn onmiddellijk van toepassing op Instellingen, de werkruimte, Discover en de modelbibliotheek.\",\"Close more actions\":\"Sluit meer acties\",\"Compact\":\"Compact\",\"Contrast\":\"Contrast\",\"current matches\":\"huidige wedstrijden\",\"Continuing recording\":\"Doorgaan met opnemen\",\"Accent gradient\":\"Accentverloop\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Voeg je eigen afbeeldingen toe of gebruik de screenshot-knop in de speler. Screenshots kunnen automatisch in de galerij terechtkomen en kunnen direct als modelomslag worden gebruikt.\",\"Continue loading the complete catalog\":\"Ga door met het laden van de volledige catalogus\",\"Active filters\":\"Actieve filters\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Pas de naam, het merk, de slogan en het lokale logo van Camclient aan. Logo's blijven in deze browser.\",\"Details\":\"Details\",\"All live models\":\"Allemaal levende modellen\",\"Discover something new\":\"Ontdek iets nieuws\",\"All suggestions\":\"Alle suggesties\",\"Almost silent\":\"Bijna stil\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Alle startpaginafilters, lay-out, voorbeeldweergave en pagineringsinstellingen worden opgeslagen in de stabiele instellingenkluis en overleven scriptupdates.\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Duidelijke aanbevelingsstraten helpen u iets nieuws te vinden in plaats van de catalogus op de startpagina te herhalen.\",\"Do not enlarge\":\"Niet vergroten\",\"Always active\":\"Altijd actief\",\"Eco\":\"Eco\",\"AND\":\"EN\",\"Audio\":\"Audio\",\"Aurora\":\"Aurora\",\"Fast starters\":\"Snelle starters\",\"Fill and crop\":\"Vullen en bijsnijden\",\"Automatic example\":\"Automatisch voorbeeld\",\"Auto\":\"Auto\",\"Automatically loading the complete catalog…\":\"Automatisch laden van de volledige catalogus…\",\"Automatic segments\":\"Automatische segmenten\",\"Filter\":\"Filter\",\"Fit completely\":\"Past volledig\",\"Filter suggestions\":\"Suggesties filteren\",\"Available recording data was saved.\":\"Beschikbare opnamegegevens zijn opgeslagen.\",\"Enable meter\":\"Schakel meter in\",\"Focus\":\"Focus\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Gebruik voor transparantie PNG, WebP of veilige SVG en kies Transparant als logoachtergrond. JPEG heeft geen transparantie. Stretch gebruikt onafhankelijk de gekozen breedte en hoogte.\",\"Frequent\":\"Frequent\",\"Logo height\":\"Logohoogte\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Nieuw in de catalogus, nieuw gezien door Camclient, of net begonnen met streamen.\",\"Local watch\":\"Lokaal horloge\",\"Logo width\":\"Logobreedte\",\"For you\":\"Voor jou\",\"gallery\":\"galerij\",\"Logo background\":\"Logo-achtergrond\",\"Info\":\"Info\",\"HD\":\"HD\",\"Loud\":\"Luidruchtig\",\"No recording data\":\"Geen opnamegegevens\",\"Inner spacing\":\"Binnenafstand\",\"Measured even while output is muted\":\"Gemeten zelfs terwijl de uitvoer gedempt is\",\"Medium\":\"Medium\",\"No filters active · all matching live models are shown\":\"Geen filters actief · alle overeenkomende live-modellen worden getoond\",\"No recommendations match the active filters.\":\"Er zijn geen aanbevelingen die overeenkomen met de actieve filters.\",\"No audio track\":\"Geen audiotrack\",\"Inspiration, not repetition\":\"Inspiratie, geen herhaling\",\"Interface\":\"Interface\",\"Menu\":\"Menu\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Het maakt het gebruik, wijziging en distributie van de software mogelijk. Copyright- en licentievermeldingen moeten blijven bestaan, en de software wordt geleverd zonder garantie. Camclient is geschreven door Emberhost.\",\"Micro\":\"Micro\",\"Later\":\"Later\",\"NOT\":\"NIET\",\"Offline\":\"Offline\",\"Model\":\"Model\",\"Middle-click or Ctrl+click opens a new tab\":\"Met de middelste klik of Ctrl+klik wordt een nieuw tabblad geopend\",\"Live\":\"Live\",\"Only new\":\"Alleen nieuw\",\"Models you have visited locally and that are public again now.\":\"Modellen die je lokaal hebt bezocht en die nu weer openbaar zijn.\",\"Link\":\"Link\",\"Modules\":\"Modules\",\"Live for a short time and already drawing public activity.\":\"Leef nog maar een korte tijd en trek al publieke activiteit aan.\",\"Live stream audio activity\":\"Livestream audio-activiteit\",\"Open\":\"Open\",\"Monogram\":\"Monogram\",\"Open Camclient homepage\":\"Open de Camclient-startpagina\",\"Online\":\"Online\",\"New discoveries\":\"Nieuwe ontdekkingen\",\"Mono\":\"Mono\",\"OR\":\"OF\",\"part\":\"deel\",\"Place\":\"Plaats\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Persoonlijke keuzes, nieuwe ontdekkingen, populaire live rooms en modellen die je nog nooit hebt bezocht, duidelijk gescheiden van de volledige homepagecatalogus.\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP en veilige SVG ondersteunen transparantie. Afbeeldingen worden lokaal verkleind naar 144 × 144 pixels en opgeslagen met uw instellingen.\",\"Stop and save\":\"Stop en bespaar\",\"Popular now\":\"Populair nu\",\"Screenshot\":\"Schermafbeelding\",\"Starter pick · popular now\":\"Starterskeuze · nu populair\",\"Screenshot format\":\"Screenshot-formaat\",\"Saved segment\":\"Opgeslagen segment\",\"Stretch\":\"Strek\",\"Status\":\"Status\",\"Public live models with no local watch time in Camclient.\":\"Openbare live-modellen zonder lokale kijktijd in Camclient.\",\"Privacy\":\"Privacy\",\"Screenshot failed\":\"Schermafbeelding mislukt\",\"Tag artwork and emoji\":\"Tag kunstwerk en emoji\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Screenshots worden gedownload en kunnen ook naar de lokale modellengalerij gaan.\",\"Tablet\":\"Tablet\",\"Screenshot unavailable\":\"Schermafbeelding niet beschikbaar\",\"The compressed image is still too large.\":\"De gecomprimeerde afbeelding is nog steeds te groot.\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Gerangschikt op basis van het huidige aantal publieke kijkers; catalogusgegevens worden niet permanent opgeslagen.\",\"Recent\":\"Recent\",\"Tag artwork editor\":\"Editor voor tagillustraties\",\"segment(s)\":\"segment(en)\",\"Screenshot resolution\":\"Schermresolutie\",\"Recording is still active.\":\"De opname is nog steeds actief.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"De koptekst toont alleen Home, Discover, Tag Explorer, Statistics Center, Modelbibliotheek, Mijn gebruiker en Instellingen. Elk configuratiegebied is beschikbaar als een subpagina Instellingen.\",\"Sepia\":\"Sepia\",\"The image could not be read.\":\"De afbeelding kon niet worden gelezen.\",\"The image could not be decoded.\":\"Het beeld kon niet worden gedecodeerd.\",\"Recording quality\":\"Opnamekwaliteit\",\"Recording stopped\":\"De opname is gestopt\",\"Set\":\"Set\",\"Quiet\":\"Rustig\",\"The image format is unsafe or unsupported.\":\"Het afbeeldingsformaat is onveilig of wordt niet ondersteund.\",\"Recording unavailable\":\"Opname niet beschikbaar\",\"Show no image\":\"Geen afbeelding weergeven\",\"Show\":\"Show\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Toont activiteit in de stream-audiotrack, ongeacht of de uitvoer van de speler is gedempt.\",\"The lanes respect your active search and filter settings.\":\"De rijstroken respecteren uw actieve zoek- en filterinstellingen.\",\"The video has no usable frame yet.\":\"De video heeft nog geen bruikbaar frame.\",\"Reset all\":\"Alles resetten\",\"RoomGrid My user\":\"RoomGrid Mijn gebruiker\",\"RoomGrid Model library\":\"RoomGrid-modelbibliotheek\",\"Source ended\":\"Bron beëindigd\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"De opname wordt alleen op dit apparaat opgeslagen. Ga alleen verder als u toestemming heeft om deze inhoud op te slaan.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Er zijn nog niet voldoende gegevens over lokale voorkeuren, dus dit zijn transparante starterskeuzes op basis van de huidige populariteit.\",\"Source resolution\":\"Bronresolutie\",\"Start recording\":\"Begin met opnemen\",\"RoomGrid Statistics\":\"RoomGrid-statistieken\",\"Remix\":\"Remixen\",\"Save to model gallery\":\"Opslaan in modelgalerij\",\"White\":\"Wit\",\"Your discovery mix\":\"Jouw ontdekkingsmix\",\"中文\":\"中文\",\"Trans\":\"Trans\",\"Transparent\":\"Transparant\",\"Trends\":\"Trends\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Deze instellingen bepalen de lay-out, paginering, modelinformatie, filters en prestaties van de Camclient-startpagina.\",\"Type\":\"Type\",\"Unvisited\":\"Niet bezocht\",\"Unified main navigation\":\"Uniforme hoofdnavigatie\",\"Upload own image\":\"Eigen afbeelding uploaden\",\"URL\":\"URL\",\"Use the complete search, filters, and sorting for every current result.\":\"Gebruik de volledige zoekopdracht, filters en sortering voor elk actueel resultaat.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Als u de Chaturbate-startpagina bezoekt, wordt altijd de volledige Camclient-startpagina geopend. Elk hoofdgebied blijft binnen dezelfde aangepaste interface.\",\"Use as model cover\":\"Gebruik als modelafdekking\",\"Volume\":\"Volume\",\"Week\":\"Week\",\"Waiting for audio\":\"Wachten op geluid\",\"What does the MIT license mean?\":\"Wat betekent de MIT-licentie?\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Ontdek openbare livekamers\",\"all groups\":\"alle groepen\",\"at least one group\":\"ten minste één groep\",\"no measurements yet\":\"nog geen metingen\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] geleden\",\"[[ROOMGRID_VALUE_0]] preview\":\"[[ROOMGRID_VALUE_0]] voorbeeld\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] profiel opgeslagen\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"Modeldetails voor @[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] volgde geïmporteerde modellen\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"[[ROOMGRID_VALUE_0]] Mijn collectie-items herkend\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"[[ROOMGRID_VALUE_0]] van [[ROOMGRID_VALUE_1]] geladen modellen zichtbaar.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Actief groepsfilter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] van [[ROOMGRID_VALUE_2]] zichtbare kaarten komen overeen ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Omslag op maat · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Lokaal opgeslagen dekking · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Voorbeeldafbeeldingen en omslagen ([[ROOMGRID_VALUE_0]] opgeslagen)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Permanent opgeslagen op dit apparaat · laatste [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Eigen afbeelding · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Opgeslagen [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"Voor het eerst gezien: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Laatst gezien: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Laatste live: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Uiterlijk: [[ROOMGRID_VALUE_0]]× · eerste [[ROOMGRID_VALUE_1]] · laatste [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Typische tijd: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Technisch: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] lokale live observaties, maximaal één per 5 minuten\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"[[ROOMGRID_VALUE_0]] van je vorige kijktijd als voorzichtige schatting\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"Verwijder tag [[ROOMGRID_VALUE_0]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"Aangepaste groepen beheren · toegewezen aan [[ROOMGRID_VALUE_0]]\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"Groep “[[ROOMGRID_VALUE_0]]” verwijderen? De modellen blijven in uw bibliotheek staan.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"Groep “[[ROOMGRID_VALUE_0]]” verwijderd\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] groepen · meervoudige selectie mogelijk\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] opgeslagen tags\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"Filter verwijderen [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] komt overeen\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filters actief\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Openbare profielinformatie kan worden geladen via “Alle details”.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Persoonlijk georganiseerd sinds: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Automatisch gecatalogiseerd sinds: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Laatste activiteit: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Optredens: [[ROOMGRID_VALUE_0]]× · laatst opgenomen [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Ontdekt: [[ROOMGRID_VALUE_0]] · kwaliteitsmetingen: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Profielgegevens: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] gearchiveerd\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] hersteld\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"Pagina [[ROOMGRID_VALUE_0]] van [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] modellen\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] opgeslagen\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] keek mee\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] actief · groeit automatisch\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] modellen · [[ROOMGRID_VALUE_1]] sessies\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favorieten\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] verschillende modellen\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] voorbeelden\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"Voorbeeld/snelle weergave van [[ROOMGRID_VALUE_0]] modellen\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] dagen\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"Reeks van [[ROOMGRID_VALUE_0]] dagen\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] heeft herhaaldelijk gekeken\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] dagen aan lokale gegevens\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"gewogen gemiddelde [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] nu live\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modellen herkend\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Niet alles kon gelezen worden: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"Meest actieve dag: [[ROOMGRID_VALUE_0]] met [[ROOMGRID_VALUE_1]]. Bewaartermijn: [[ROOMGRID_VALUE_2]] dagen voor onbewerkte gebeurtenissen; oudere waarden kunnen als dagelijkse totalen blijven bestaan.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] sessies\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] lokaal opgeslagen\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Lokale gegevens konden niet worden geladen: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] is nu online.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] is momenteel offline.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] is al een tijdje niet meer online te zien.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favorieten staan ​​samen online.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"Het openbare profiel van [[ROOMGRID_VALUE_0]] is gewijzigd.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] modellen bijgewerkt\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"Voorbeeld van importeren van statistieken (nog niets veranderd):\\n[[ROOMGRID_VALUE_0]] modellen\\n[[ROOMGRID_VALUE_1]] evenementen\\n[[ROOMGRID_VALUE_2]] openbare profielen\\n\\nDoorgaan vervangt de bestaande statistieken.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"Livevoorbeeld van [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] toegevoegd aan MultiCam\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"[[ROOMGRID_VALUE_0]] verwijderen?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"[[ROOMGRID_VALUE_0]] views en [[ROOMGRID_VALUE_1]] feeds importeren?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] kamers toegevoegd aan werkruimte\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modellen opgeslagen\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] overeenkomende resultaten\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] overeenkomende live-modellen\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"Deelt #[[ROOMGRID_VALUE_0]] met een favoriet\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] afbeeldingen toegevoegd; [[ROOMGRID_VALUE_1]] kon niet worden verwerkt.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] afbeeldingen toegevoegd.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] kamers\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] geldige gebruikersnamen\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Pagina [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] kamers\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"Tijdelijke URL's: [[ROOMGRID_VALUE_0]] toegevoegd, [[ROOMGRID_VALUE_1]] mislukt\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"Voorbeeld van importeren van statistieken: [[ROOMGRID_VALUE_0]] modellen en [[ROOMGRID_VALUE_1]] gebeurtenissen. Als u veilig doorgaat, worden de gegevens samengevoegd en wordt eerst een back-up gemaakt.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Aangepaste indelingen ondersteunen maximaal [[ROOMGRID_VALUE_0]] kamers.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"Scène “[[ROOMGRID_VALUE_0]]” opgeslagen\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"Scène “[[ROOMGRID_VALUE_0]]” geladen\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] actieve streams · [[ROOMGRID_VALUE_1]] onstabiel · [[ROOMGRID_VALUE_2]] om te bekijken\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p toegewezen\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"Scène “[[ROOMGRID_VALUE_0]]” verwijderen?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Alleen opgeslagen voor [[ROOMGRID_VALUE_0]]: tot [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Deze camera maakt gebruik van globale/adaptieve kwaliteit[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] werd vervangen door [[ROOMGRID_VALUE_1]]\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Alleen audio afspelen vanaf [[ROOMGRID_VALUE_0]]\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"Geplaatst in focussleuf [[ROOMGRID_VALUE_0]]\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"Geplaatst in rastervenster [[ROOMGRID_VALUE_0]]\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Resolutie [[ROOMGRID_VALUE_0]], status [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Streamgezondheid [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modellen\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Filter verwijderen: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"Sluit aan bij jouw interesses: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"Catalogus [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] opgehaald · [[ROOMGRID_VALUE_2]] live modellen\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] lokale modellen\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Catalogus [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live-modellen\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] cataloguskamers geladen\"},\"visible\":{\"IndexedDB\":\"GeïndexeerdeDB\",\"Gruppen umbenennen oder löschen\":\"Groepen hernoemen of verwijderen\",\"Gruppe löschen\":\"Groep verwijderen\",\"1 Jahr\":\"1 jaar\",\"‹ Zurück\":\"< Terug\",\"● 1.2k · #tag\":\"● 1,2k · #tag\",\"Bei verstecktem Tab pausieren\":\"Pauzeer terwijl het tabblad verborgen is\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Beeld optimaliseren · grote zoom, positie, filter en rotatie\",\"Automatisch neu verbinden\":\"Automatisch opnieuw verbinden\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Pas bestaande Chaturbate-accountgegevens toe\",\"Diese Bibliotheksansicht ist noch leer.\":\"Deze bibliotheekweergave is nog leeg.\",\"30 Tage\":\"30 Neem\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"De huidige verbinding is actief; Na de volgende meting verschijnt hier de lokale koers.\",\"Eigene Priorität\":\"Eigen prioriteit\",\"Keine Models passen zu diesen Filtern.\":\"Er zijn geen modellen die overeenkomen met deze filters.\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Elke opgeslagen tag is automatisch beschikbaar in de zoekopdracht, in de automatische aanvulling en hier als direct meervoudig filter.\",\"Keine Priorität\":\"Geen prioriteit\",\"7 Tage\":\"7 Neem\",\"HD + Gaming\":\"HD + Gamen\",\"90 Tage\":\"90 Daken\",\"AA ×\":\"AA×\",\"Bildanpassung\":\"Beeldaanpassing\",\"Broadcaster-Verbindung verwalten\":\"Beheer de omroepverbinding\",\"Breite des Informationsbereichs\":\"Breedte van het informatiegebied\",\"Eigenen Tag eingeben …\":\"Vul uw eigen dag in...\",\"Eigene Tags filtern\":\"Filter uw eigen tags\",\"AA ✓\":\"AA ✓\",\"Einstellungen\":\"Instellingen\",\"Konto-Daten werden eingelesen …\":\"Accountgegevens worden ingelezen...\",\"Layout & sichtbare Bereiche\":\"Indeling en zichtbare gebieden\",\"Lautstärke pro Model merken\":\"Let op het volume per model\",\"Eigene Tags\":\"Eigen tags\",\"Alle eigenen Tags\":\"Alle aangepaste tags\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Alle wijzigingen worden lokaal opgeslagen en gelden ook voor het volgende model.\",\"Ctrl + Shift + X\":\"Ctrl+Shift+X\",\"Bildwerkzeuge einklappen\":\"Afbeeldingshulpmiddelen samenvouwen\",\"Ctrl+K\":\"Ctrl+K\",\"Ereignisse im Zeitraum\":\"Gebeurtenissen in de periode\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Enter of “Tag toevoegen” neemt het concept over. Het wordt alleen permanent opgeslagen met “Profiel opslaan”. Terwijl u typt, worden reeds gebruikte tags voorgesteld.\",\"Chat anzeigen\":\"Toon chat\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Creëer vrij benoemde collecties, wijs modellen toe aan meerdere groepen en toon hier één of meerdere groepen samen.\",\"LIVE\":\"LIVE\",\"Alle Gruppen\":\"Alle groepen\",\"Maximale Stream-Lautstärke\":\"Maximaal streamvolume\",\"lokal · für Suche und Filter\":\"lokaal · voor zoeken en filters\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Maak hierboven eerst je eigen groep aan.\",\"Follower\":\"Volgeling\",\"Ähnliche Models anzeigen\":\"Toon vergelijkbare modellen\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"De Analytics Studio bevat ook tijdsperiodevergelijkingen, heatmaps, ontdekkingstrechters, werkruimte- en kwaliteitsanalyses en gegevensexports.\",\"GAMING\":\"SPELEN\",\"Datenschutz & Export\":\"Gegevensbescherming en export\",\"Ältere Chaturbate-Daten anfordern\":\"Vraag oudere Chaturbate-gegevens aan\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Dit betekent dat u sinds de installatie van het script niet meer beperkt bent tot gegevens: RoomGrid kan uw bestaande volglijst en de duidelijk herkenbare vermeldingen uit Mijn Collectie lezen. Er worden geen berichten, betalingen, media of wachtwoorden gekopieerd.\",\"Letzte Messung\":\"Laatste meting\",\"Gesamter Verlauf\":\"Gehele cursus\",\"Model besitzt alle gewählten Tags\":\"Model is eigenaar van alle geselecteerde tags\",\"Model besitzt mindestens einen gewählten Tag\":\"Model heeft ten minste één geselecteerde tag\",\"Automatisch abspielen\":\"Automatisch afspelen\",\"Deine Aktivität der letzten 14 Tage\":\"Uw activiteit gedurende de afgelopen 14 dagen\",\"Größe der Bedienelemente\":\"Grootte van de bedieningselementen\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Automatisch vastgelegd · volledig georganiseerd tijdens het bewerken\",\"Grid +\":\"Raster +\",\"Deine meistgesehenen Models\":\"Uw meest bekeken modellen\",\"MultiCam\":\"MultiCam\",\"Model ist in allen gewählten Gruppen\":\"Model zit in alle geselecteerde groepen\",\"MultiCam +\":\"MultiCam +\",\"Model ist in mindestens einer gewählten Gruppe\":\"Het model bevindt zich in ten minste één geselecteerde groep\",\"My Collection öffnen\":\"Open Mijn verzameling\",\"Nicht verbunden\":\"Niet verbonden\",\"Niedrige Live-Latenz\":\"Lage live latentie\",\"Verbunden\":\"Samengebonden\",\"RoomGrid Suite\":\"RoomGrid-suite\",\"Weiter ›\":\"Verder >\",\"SingleCam\":\"Enkele camera\",\"Vollständige Modelstatistik\":\"Volledige modelstatistieken\",\"Namen speichern\":\"Naam opslaan\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Lees nu bestaande accountgegevens in\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Er zijn nog steeds geen voldoende lokale gebruikswaarden.\",\"Seitengröße\":\"Paginagrootte\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Nog geen eigen groep. Voer hierboven een naam in en maak uw eerste collectie aan.\",\"Noch keine Model-Sehzeit erfasst.\":\"Er is nog geen modelkijktijd geregistreerd.\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid kan de persoonlijke Chaturbate-accountwaarden niet raden. Deze kaarten verschijnen alleen als je bewust de officiële omroepstatistieken hebt gekoppeld aan je eigen gebruikersnaam en token.\",\"SingleCam einstellen\":\"SingleCam instellen\",\"Zufriedenheit\":\"tevredenheid\",\"SingleCam Pro\":\"SingleCam Pro\",\"Noch nicht eingelesen\":\"Nog niet gelezen\",\"Nur auf diesem Gerät\":\"Alleen op dit apparaat\",\"Status im Player anzeigen\":\"Toon status in speler\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Zodra u uw eigen tag voor een model opslaat, verschijnt deze hier en in de autocomplete.\",\"SingleCam-Standard wiederherstellen\":\"Herstel de standaardwaarde van SingleCam\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Nog geen officiële momentopnamen van accounts.\",\"Status-Aktualisierung\":\"Statusupdate\",\"Öffentliche/erkannte Tags\":\"Openbare/herkende tags\",\"Öffentliches Profil anzeigen\":\"Bekijk openbaar profiel\",\"Tag hinzufügen\":\"Label toevoegen\",\"Stream immer stumm starten\":\"Begin altijd in stilte met streamen\",\"Token-Guthaben\":\"Token-saldo\",\"Tipps letzte Stunde\":\"Tips laatste uur\",\"Ton & Ruhe\":\"Geluid en kalm\",\"Ungefähre Onlinezeiten\":\"Geschatte online tijden\",\"Offizielle Statistik verbinden\":\"Verbind officiële statistieken\",\"Persönlich organisiert\":\"Persoonlijk georganiseerd\",\"PiP\":\"Pip\",\"Player & Verbindung\":\"Speler & verbinding\",\"Player neu verbinden\":\"Sluit de speler opnieuw aan\",\"Player-Ecken\":\"Speler hoeken\",\"Player-Hintergrund\":\"Speler achtergrond\",\"Player-Höhe\":\"Speler hoogte\",\"Position der Informationen\":\"Locatie van informatie\",\"Player-Seitenverhältnis\":\"Beeldverhouding van speler\",\"Private Notiz\":\"Privé notitie\",\"Private Notizen anzeigen\":\"Bekijk privénotities\",\"Puffergröße\":\"Buffergrootte\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Optionale offizielle Kontostatistik\":\"Optionele officiële accountstatistieken\",\"Gruppen\":\"Groepen\",\"Schließen\":\"Sluiten\",\"Startseite nach eigenen Gruppen filtern\":\"Startpagina filteren op eigen groepen\",\"RoomGrid Komfortzentrum\":\"RoomGrid-bedieningscentrum\",\"Mein Nutzer\":\"Mijn profiel\",\"Mein Nutzer-Dashboard\":\"Mijn profieldashboard\",\"Modellbibliothek\":\"Modellenbibliotheek\",\"Große Modellbibliothek\":\"Uitgebreide modellenbibliotheek\",\"Statistikzentrum\":\"Statistiekcentrum\",\"Sitzungen\":\"Sessies\",\"Dock + Hover\":\"Dock + aanwijzen\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Lokaal en privé op dit apparaat\",\"Sortierung\":\"Sortering\",\"Meiste Sehzeit\":\"Meeste kijktijd\",\"Zuletzt aktiv\":\"Recent actief\",\"Meiste Sitzungen\":\"Meeste sessies\",\"Meiste Zuschauer\":\"Meeste kijkers\",\"Status\":\"Status\",\"Alle Status\":\"Alle statussen\",\"Nur live\":\"Alleen live\",\"Nur Live\":\"Alleen live\",\"Offline\":\"Offline\",\"Unbekannt\":\"Onbekend\",\"Nur Bibliothek\":\"Alleen bibliotheek\",\"Pro Seite\":\"Per pagina\"}}","tr":"{\"phrases\":{\"The language switch could not be prepared safely. This page was not left.\":\"Dil anahtarı güvenli bir şekilde hazırlanamadı. Bu sayfa kalmadı.\",\"Language\":\"Dil\",\"Default workspace\":\"Varsayılan çalışma alanı\",\"Workspace\":\"Çalışma alanı\",\"New workspace\":\"Yeni çalışma alanı\",\"copy\":\"kopyala\",\"New group\":\"Yeni grup\",\"Saved model\":\"Kaydedilen model\",\"Search models, pages, workspaces and commands…\":\"Modelleri, sayfaları, çalışma alanlarını ve komutları arayın…\",\"Type to filter · Enter to run · Esc to close\":\"Filtrelemek için yazın · Çalıştırmak için Enter · Kapatmak için Esc\",\"No matches\":\"Eşleşme yok\",\"Currently active RoomGrid version\":\"Şu anda aktif RoomGrid sürümü\",\"years\":\"yıllar\",\"Live\":\"Canlı\",\"Private\":\"Özel\",\"Away\":\"uzakta\",\"Offline\":\"Çevrimdışı\",\"sessions\":\"oturumlar\",\"NEW\":\"YENİ\",\"Name, subject, tag or place\":\"Ad, konu, etiket veya yer\",\"Website order\":\"Web sitesi siparişi\",\"Most viewers\":\"Çoğu izleyici\",\"Fewest viewers\":\"En az izleyici\",\"Longest online\":\"En uzun çevrimiçi\",\"Recently online\":\"Son zamanlarda çevrimiçi\",\"Age ascending\":\"Artan yaş\",\"Age descending\":\"Yaş azalan\",\"New models first\":\"Önce yeni modeller\",\"Saved first\":\"İlk kaydedilen\",\"Favorites first\":\"Önce favoriler\",\"Most watched\":\"En çok izlenenler\",\"Recently watched\":\"Son zamanlarda izlendi\",\"Random mix\":\"Rastgele karışım\",\"All loaded\":\"Hepsi yüklendi\",\"Previous page\":\"Önceki sayfa\",\"Next page\":\"Sonraki sayfa\",\"Reload complete live list\":\"Canlı listenin tamamını yeniden yükle\",\"All homepage options\":\"Tüm ana sayfa seçenekleri\",\"Reset filters\":\"Filtreleri sıfırla\",\"Quick filter\":\"Hızlı filtre\",\"Sort\":\"Sırala\",\"Models per page\":\"Sayfa başına modeller\",\"Load more results\":\"Daha fazla sonuç yükle\",\"still to display\":\"hala görüntülenecek\",\"continues automatically while scrolling\":\"kaydırma sırasında otomatik olarak devam eder\",\"visible\":\"görünür\",\"loaded\":\"yüklü\",\"page\":\"sayfa\",\"active filters\":\"aktif filtreler\",\"loading complete live list\":\"tam canlı liste yükleniyor\",\"all matching live models found\":\"eşleşen tüm canlı modeller bulundu\",\"live list incomplete, retry\":\"canlı liste tamamlanmadı, yeniden deneyin\",\"Requests temporarily paused\":\"İstekler geçici olarak duraklatıldı\",\"Vault could not be unlocked\":\"Kasanın kilidi açılamadı\",\"Preview Dock\":\"Dock'u Önizle\",\"Connecting…\":\"Bağlanıyor…\",\"Stream temporarily unavailable\":\"Akış geçici olarak kullanılamıyor\",\"Error\":\"Hata\",\"Buffering\":\"Tamponlama\",\"Preview unavailable\":\"Önizleme kullanılamıyor\",\"Connect\":\"Bağlan\",\"Close\":\"Kapat\",\"Opacity\":\"Opaklık\",\"Info\":\"Bilgi\",\"Age\":\"Yaş\",\"Gender\":\"Cinsiyet\",\"Location\":\"Konum\",\"Country\":\"Ülke\",\"Last broadcast\":\"Son yayın\",\"Fan club\":\"Hayran kulübü\",\"Private show\":\"Özel gösteri\",\"Spy show\":\"Casus gösterisi\",\"Model comparison list\":\"Model karşılaştırma listesi\",\"Compare\":\"Karşılaştır\",\"Open model details\":\"Model ayrıntılarını aç\",\"Compare now\":\"Şimdi karşılaştır\",\"Clear\":\"Temizle\",\"Up to four models can be compared at once.\":\"Aynı anda en fazla dört model karşılaştırılabilir.\",\"Remove from comparison\":\"Karşılaştırmadan kaldır\",\"Add to comparison\":\"Karşılaştırmaya ekle\",\"Total watched\":\"Toplam izlenen\",\"Sessions\":\"Oturumlar\",\"Viewers\":\"İzleyiciler\",\"Rating\":\"Derecelendirme\",\"Online samples\":\"Çevrimiçi örnekler\",\"Loading quality data…\":\"Kalite verileri yükleniyor…\",\"samples\":\"örnekler\",\"stalls\":\"tezgahlar\",\"No quality measurements yet\":\"Henüz kalite ölçümü yok\",\"Quality data unavailable\":\"Kalite verileri mevcut değil\",\"Details\":\"Ayrıntılar\",\"Remove\":\"Kaldır\",\"Model comparison\":\"Model karşılaştırması\",\"Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.\":\"Her değer kamuya açık bilgilerden, kendi organizasyonunuzdan veya bu cihazdaki yerel gözlemlerden gelir. Eksik değerler asla tahmin edilmez.\",\"Shared tags\":\"Paylaşılan etiketler\",\"Loading public facts…\":\"Herkese açık gerçekler yükleniyor…\",\"Unknown status\":\"Bilinmeyen durum\",\"Last activity\":\"Son aktivite\",\"These statistics describe only your own local RoomGrid usage.\":\"Bu istatistikler yalnızca kendi yerel RoomGrid kullanımınızı tanımlar.\",\"At a glance\":\"Bir bakışta\",\"Type\":\"Tür\",\"Appearances\":\"Görünümler\",\"Public facts\":\"Kamuya açık gerçekler\",\"No public biography available yet.\":\"Henüz halka açık bir biyografi mevcut değil.\",\"Public link\":\"Herkese açık bağlantı\",\"Public profile source\":\"Herkese açık profil kaynağı\",\"Profile updated\":\"Profil güncellendi\",\"Private note (local only)\":\"Özel not (yalnızca yerel)\",\"Your tags\":\"Etiketleriniz\",\"No rating\":\"Derecelendirme yok\",\"No priority\":\"Öncelik yok\",\"Private library data\":\"Özel kütüphane verileri\",\"Private notes\":\"Özel notlar\",\"Priority\":\"Öncelik\",\"Favorite\":\"Favori\",\"Watchlist\":\"İzleme listesi\",\"Archived\":\"Arşivlendi\",\"Watch again\":\"Tekrar izle\",\"Technical note\":\"Teknik not\",\"Technical\":\"Teknik\",\"Save all changes\":\"Tüm değişiklikleri kaydet\",\"Loading local activity…\":\"Yerel etkinlik yükleniyor…\",\"Activity and playback quality\":\"Etkinlik ve oynatma kalitesi\",\"Sun\":\"Güneş\",\"Mon\":\"Pazartesi\",\"Tue\":\"Salı\",\"Wed\":\"Çar\",\"Thu\":\"Per\",\"Fri\":\"Cuma\",\"Sat\":\"Cumartesi\",\"Quality samples\":\"Kaliteli numuneler\",\"Average startup\":\"Ortalama başlangıç\",\"Stalls\":\"Tezgahlar\",\"Reconnects\":\"Yeniden bağlanır\",\"Dropped frames\":\"Bırakılan kareler\",\"Online observations\":\"Çevrimiçi gözlemler\",\"Frequently observed online times\":\"Sıklıkla gözlemlenen çevrimiçi zamanlar\",\"Not enough online observations yet.\":\"Henüz yeterli çevrimiçi gözlem yok.\",\"Watched\":\"izlendi\",\"Preview\":\"Önizleme\",\"Quick view\":\"Hızlı görünüm\",\"Discovered\":\"Keşfedildi\",\"Quality\":\"Kalite\",\"Connection\":\"Bağlantı\",\"No local activity recorded yet.\":\"Henüz yerel etkinlik kaydedilmedi.\",\"Recent events\":\"Son olaylar\",\"Activity data unavailable.\":\"Etkinlik verileri mevcut değil.\",\"Similar models\":\"Benzer modeller\",\"Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.\":\"Yerel/genel etiketler, ülke ve türden hesaplanır. Her önerinin nedeni gösterilir ve hiçbir veri yüklenmez.\",\"shared tags\":\"paylaşılan etiketler\",\"same country\":\"aynı ülke\",\"same type\":\"aynı tip\",\"Not enough matching data yet.\":\"Henüz yeterli eşleşen veri yok.\",\"Overview\":\"Genel Bakış\",\"Profile\":\"Profil\",\"Organize\":\"Organize et\",\"Activity\":\"Etkinlik\",\"Similar\":\"benzer\",\"Model detail sections\":\"Model detay bölümleri\",\"Previous model\":\"Önceki model\",\"Next model\":\"Sonraki model\",\"Open room\":\"Açık oda\",\"Pin preview\":\"Pin önizlemesi\",\"No public fields available\":\"Herkese açık alan yok\",\"Public facts currently unavailable\":\"Herkese açık gerçekler şu anda mevcut değil\",\"Session chat search\":\"Oturum sohbet araması\",\"Search this session only…\":\"Yalnızca bu oturumu arayın…\",\"New messages\":\"Yeni mesajlar\",\"Time for a short break.\":\"Kısa bir mola zamanı.\",\"Your RoomGrid time budget for today has been reached.\":\"Bugünkü RoomGrid zaman bütçenize ulaşıldı.\",\"RoomGrid action messages\":\"RoomGrid eylem mesajları\",\"This change is saved automatically.\":\"Bu değişiklik otomatik olarak kaydedilir.\",\"Undo\":\"Geri al\",\"Change undone\":\"Değişiklik geri alındı\",\"Undo was not applied because newer changes already exist.\":\"Daha yeni değişiklikler zaten mevcut olduğundan geri alma uygulanmadı.\",\"Chaturbate live catalog\":\"Chaturbate canlı kataloğu\",\"RoomGrid Discover\":\"RoomGrid Keşfet\",\"Chaturbate Discover page\":\"Chaturbate Keşfet sayfası\",\"Following list\":\"Takip edilen liste\",\"My Collection\":\"Koleksiyonum\",\"Chaturbate page\":\"Chaturbate sayfası\",\"Local record\":\"Yerel kayıt\",\"Personally organized\":\"Kişisel olarak organize edilmiş\",\"Local watch history\":\"Yerel izleme geçmişi\",\"Public profile\":\"Herkese açık profil\",\"Local quality measurements\":\"Yerel kalite ölçümleri\",\"Local model index\":\"Yerel model dizini\",\"Current live signal\":\"Mevcut canlı sinyal\",\"Within the last hour\":\"Son bir saat içinde\",\"Within the last 24 hours\":\"Son 24 saat içinde\",\"Within the last 30 days\":\"Son 30 gün içinde\",\"Historical record\":\"Tarihsel kayıt\",\"Time unknown\":\"Zaman bilinmiyor\",\"All\":\"Hepsi\",\"Favorites\":\"Favoriler\",\"Library\":\"Kütüphane\",\"My Collection contained no reliably recognizable entries\":\"Koleksiyonum güvenilir bir şekilde tanınabilir giriş içermiyordu\",\"No group filter active · all models are shown.\":\"Aktif grup filtresi yok · tüm modeller gösteriliyor.\",\"Clear group filter\":\"Grup filtresini temizle\",\"Filter homepage by groups\":\"Ana sayfayı gruplara göre filtreleyin\",\"Close group menu\":\"Grup menüsünü kapat\",\"Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.\":\"Kendi gruplarınızdan bir veya daha fazlasını seçin. Diğer model kartlar Chaturbate ana sayfasında hemen gizlenir.\",\"Show all\":\"Tümünü göster\",\"Models in this group\":\"Bu gruptaki modeller\",\"There is no custom group yet. Create one in the model library and assign models there.\":\"Henüz özel bir grup yok. Model kitaplığında bir tane oluşturun ve modelleri buraya atayın.\",\"In any group\":\"Herhangi bir grupta\",\"In all groups\":\"Tüm gruplarda\",\"Manage groups\":\"Grupları yönet\",\"Create group\":\"Grup oluştur\",\"Custom groups for the homepage\":\"Ana sayfa için özel gruplar\",\"Main views\":\"Ana görünümler\",\"MultiCam workstation\":\"Çoklu Kamera iş istasyonu\",\"Camera grid, audio and layouts\":\"Kamera ızgarası, ses ve düzenler\",\"Discover\":\"Keşfet\",\"Full catalog, filters and sorting\":\"Tam katalog, filtreler ve sıralama\",\"Personal dashboard\":\"Kişisel kontrol paneli\",\"Favorites, activity and shortcuts\":\"Favoriler, etkinlik ve kısayollar\",\"Tag explorer\":\"Etiket gezgini\",\"Combinations, trends and related tags\":\"Kombinasyonlar, trendler ve ilgili etiketler\",\"Library and statistics\":\"Kütüphane ve istatistikler\",\"Statistics Center\":\"İstatistik merkezi\",\"Model comparison and local statistics\":\"Model karşılaştırması ve yerel istatistikler\",\"Model library\":\"Model kütüphanesi\",\"Profiles, tags, groups and notes\":\"Profiller, etiketler, gruplar ve notlar\",\"My user\":\"Profilim\",\"Preferences, usage and account data\":\"Tercihler, kullanım ve hesap verileri\",\"Analytics Studio\":\"Analitik Stüdyosu\",\"Ranges, heatmaps and exports\":\"Aralıklar, ısı haritaları ve dışa aktarmalar\",\"Settings and maintenance\":\"Ayarlar ve bakım\",\"Customize homepage\":\"Ana sayfayı özelleştirin\",\"Cards, filters and visible details\":\"Kartlar, filtreler ve görünür ayrıntılar\",\"Appearance\":\"Görünüm\",\"Colors, density and surfaces\":\"Renkler, yoğunluk ve yüzeyler\",\"Workspaces and groups\":\"Çalışma alanları ve gruplar\",\"Layouts, templates and assignments\":\"Düzenler, şablonlar ve atamalar\",\"Data and backup\":\"Veri ve yedekleme\",\"Import, export and recovery\":\"İthalat, ihracat ve kurtarma\",\"Windows and performance\":\"Windows ve performans\",\"Resource profiles and preview limits\":\"Kaynak profilleri ve önizleme sınırları\",\"Privacy\":\"Gizlilik\",\"History, private session and local data\":\"Geçmiş, özel oturum ve yerel veriler\",\"Diagnostics\":\"Teşhis\",\"Self-check, routes and recovery\":\"Kendi kendine kontrol, rotalar ve kurtarma\",\"RoomGrid main menu\":\"RoomGrid ana menüsü\",\"All pages, settings and tools in one place\":\"Tüm sayfalar, ayarlar ve araçlar tek bir yerde\",\"The menu stays inside the visible window; all RoomGrid data remains local.\":\"Menü görünür pencerenin içinde kalır; tüm RoomGrid verileri yerel kalır.\",\"RoomGrid pages\":\"RoomGrid sayfaları\",\"Statistics\":\"İstatistikler\",\"Customize original homepage\":\"Orijinal ana sayfayı özelleştirin\",\"Homepage\":\"Ana sayfa\",\"Filter homepage by your own groups\":\"Ana sayfayı kendi gruplarınıza göre filtreleyin\",\"Groups\":\"Gruplar\",\"Activity trend\":\"Etkinlik eğilimi\",\"Link\":\"Bağlantı\",\"Public profile text and links\":\"Genel profil metni ve bağlantıları\",\"Last known public preview\":\"Bilinen son genel önizleme\",\"Current public preview\":\"Mevcut genel önizleme\",\"Offline may only provide a placeholder\":\"Çevrimdışı yalnızca yer tutucu sağlayabilir\",\"Example images and covers\":\"Örnek görseller ve kapaklar\",\"Reading local image archive…\":\"Yerel görsel arşivi okunuyor…\",\"Save new snapshot\":\"Yeni anlık görüntüyü kaydet\",\"Saving public previews…\":\"Herkese açık önizlemeler kaydediliyor…\",\"No saved image yet; current public previews are still shown.\":\"Henüz kayıtlı resim yok; mevcut genel önizlemeler hâlâ gösterilmektedir.\",\"Current public cover\":\"Mevcut kamu kapağı\",\"Current compact preview\":\"Güncel kompakt önizleme\",\"Current live image\":\"Mevcut canlı görüntü\",\"Room\":\"Oda\",\"More\":\"Daha Fazla\",\"Unwatch\":\"İzlemeyi kaldır\",\"Data sources and freshness\":\"Veri kaynakları ve güncellik\",\"Data source\":\"Veri kaynağı\",\"Full local data set\":\"Tam yerel veri seti\",\"Matching filters\":\"Eşleşen filtreler\",\"Shown on this page\":\"Bu sayfada gösterilen\",\"Latest live signals\":\"En son canlı sinyaller\",\"With watch history\":\"İzleme geçmişiyle\",\"Public-profile coverage\":\"Herkese açık profil kapsamı\",\"Country coverage\":\"Ülke kapsamı\",\"Age coverage\":\"Yaş kapsamı\",\"Tag coverage\":\"Etiket kapsamı\",\"Which data set is being searched?\":\"Hangi veri seti aranıyor?\",\"Last local observation\":\"Son yerel gözlem\",\"No observation time yet\":\"Henüz gözlem zamanı yok\",\"Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.\":\"Herkese açık = mevcut Chaturbate sayfalarından veya canlı katalogdan. Kişisel = sizin tarafınızdan kaydedilmiş, etiketlenmiş veya gruplandırılmıştır. Yerel = yalnızca bu cihazda gözlemlenen oturumlar, önizlemeler veya kalite değerleri. Bilinmeyen alanlar bilinmeyen olarak kalır ve hiçbir zaman tahmin edilmez.\",\"Local model statistics\":\"Yerel model istatistikleri\",\"Automatically recorded · personally organized\":\"Otomatik olarak kaydedildi · kişisel olarak düzenlendi\",\"Automatically recorded in the growing model catalog\":\"Büyüyen model kataloğuna otomatik olarak kaydedilir\",\"Total\":\"Toplam\",\"Avg. session\":\"Ortalama oturum\",\"Active days\":\"Aktif günler\",\"Share\":\"Paylaş\",\"Longest session\":\"En uzun oturum\",\"Previews\":\"Önizlemeler\",\"Quick views\":\"Hızlı görüntülemeler\",\"Data sources\":\"Veri kaynakları\",\"Edit in library\":\"Kitaplıkta düzenle\",\"No experience data yet\":\"Henüz deneyim verisi yok\",\"Online observations will be collected locally from now on.\":\"Artık çevrimiçi gözlemler yerel olarak toplanacak.\",\"Open\":\"Açık\",\"Good trend\":\"İyi eğilim\",\"Moderate trend\":\"Orta eğilim\",\"Initial trend\":\"Başlangıç trendi\",\"First observation\":\"İlk gözlem\",\"Estimated from watch time\":\"İzlenme süresine göre tahmin\",\"weekday open\":\"hafta içi açık\",\"time open\":\"açık zaman\",\"Preview image\":\"Resmin önizlemesi\",\"Biography / room subject\":\"Biyografi / oda konusu\",\"Catalog state\":\"Katalog durumu\",\"Live status\":\"Canlı durum\",\"Viewer count\":\"İzleyici sayısı\",\"Country/region\":\"Ülke/bölge\",\"Total watch time\":\"Toplam izlenme süresi\",\"Average session\":\"Ortalama oturum\",\"First seen\":\"İlk görülme\",\"Last seen\":\"Son görülme\",\"Last live\":\"Son canlı\",\"Approximate online times\":\"Yaklaşık çevrimiçi kalma süreleri\",\"Activity, sources and profile state\":\"Etkinlik, kaynaklar ve profil durumu\",\"Example images\":\"Örnek resimler\",\"Own groups\":\"Kendi grupları\",\"Public/recognized tags\":\"Genel/tanınan etiketler\",\"Notes, own tags and priority\":\"Notlar, kendi etiketleri ve öncelik\",\"Local rating\":\"Yerel derecelendirme\",\"Group assignment\":\"Grup ödevi\",\"Room and preview actions\":\"Oda ve önizleme işlemleri\",\"Library display\":\"Kütüphane görünümü\",\"Saved in this browser\":\"Bu tarayıcıya kaydedildi\",\"Choose card size, models per row, page size and exactly which details each card shows.\":\"Kart boyutunu, satır başına modelleri, sayfa boyutunu ve her kartın tam olarak hangi ayrıntıları göstereceğini seçin.\",\"Compact overview\":\"Kompakt genel bakış\",\"Balanced\":\"Dengeli\",\"All details\":\"Tüm ayrıntılar\",\"Show all details\":\"Tüm ayrıntıları göster\",\"Core details only\":\"Yalnızca temel ayrıntılar\",\"Card zoom\":\"Kart yakınlaştırma\",\"Visible information per model\":\"Model başına görünür bilgiler\",\"With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.\":\"Otomatik sütunlarla kart yakınlaştırma, kaç modelin yan yana sığacağını kontrol eder. Tam bir sütun sayısı bunu geçersiz kılar. Tüm seçenekler bu tarayıcıda kayıtlı kalır.\",\"No custom tags yet\":\"Henüz özel etiket yok\",\"Name of the new group\":\"Yeni grubun adı\",\"Models remain in the library.\":\"Modeller kütüphanede kalır.\",\"Custom groups\":\"Özel gruplar\",\"Create new group\":\"Yeni grup oluştur\",\"Nothing selected — show all\":\"Hiçbir şey seçilmedi — tümünü göster\",\"Any\":\"Herhangi biri\",\"Women\":\"Kadınlar\",\"Men\":\"Erkekler\",\"Couples\":\"Çiftler\",\"Unknown\":\"Bilinmiyor\",\"Error/loading\":\"Hata/yükleniyor\",\"Every weekday\":\"Hafta içi her gün\",\"Sunday\":\"Pazar\",\"Monday\":\"Pazartesi\",\"Tuesday\":\"Salı\",\"Wednesday\":\"Çarşamba\",\"Thursday\":\"perşembe\",\"Friday\":\"Cuma\",\"Saturday\":\"cumartesi\",\"Every time\":\"Her zaman\",\"Advanced filters and sorting\":\"Gelişmiş filtreler ve sıralama\",\"Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.\":\"İstediğiniz kadar filtreyi birleştirin. Çoklu cinsiyetler, eyaletler, ülkeler ve etiketler çoklu seçim olarak çalışır. Her ayar bu tarayıcıda kayıtlı kalır.\",\"Gender/type\":\"Cinsiyet/tür\",\"Countries/regions (multiple)\":\"Ülkeler/bölgeler (birden fazla)\",\"Add country…\":\"Ülke ekle…\",\"Public tags (multiple)\":\"Genel etiketler (birden fazla)\",\"Add tag…\":\"Etiket ekle…\",\"Public-tag matching\":\"Genel etiket eşleştirme\",\"At least one tag\":\"En az bir etiket\",\"Every selected tag\":\"Seçilen her etiket\",\"Catalog source\":\"Katalog kaynağı\",\"All models\":\"Tüm modeller\",\"Personally organized only\":\"Yalnızca kişisel olarak organize edilmiş\",\"Automatically discovered only\":\"Yalnızca otomatik olarak keşfedildi\",\"Profile data\":\"Profil verileri\",\"Loaded or missing\":\"Yüklendi veya eksik\",\"Loaded only\":\"Yalnızca yüklendi\",\"Missing only\":\"Yalnızca eksik\",\"Picture/category\":\"Resim/kategori\",\"Neither HD nor gaming\":\"Ne HD ne de oyun\",\"Watch history\":\"Geçmişi izle\",\"Never watched\":\"Hiç izlemedim\",\"Own tags\":\"Kendi etiketleri\",\"With or without\":\"İle veya olmadan\",\"With tags only\":\"Yalnızca etiketlerle\",\"Without tags only\":\"Yalnızca etiketler olmadan\",\"At least one group\":\"En az bir grup\",\"No group\":\"Grup yok\",\"With notes only\":\"Yalnızca notlarla\",\"Without notes only\":\"Yalnızca notlar olmadan\",\"Numeric ranges and minimums\":\"Sayısal aralıklar ve minimumlar\",\"Minimum age\":\"Minimum yaş\",\"Maximum age\":\"Maksimum yaş\",\"Minimum viewers\":\"Minimum izleyici sayısı\",\"Maximum viewers\":\"Maksimum izleyici\",\"Minimum followers\":\"Minimum takipçi\",\"Maximum followers\":\"Maksimum takipçi\",\"Minimum total watch time\":\"Minimum toplam izlenme süresi\",\"Currently online for at least\":\"Şu anda en azından çevrimiçi\",\"Minimum sessions\":\"Minimum oturumlar\",\"Minimum rating\":\"Minimum derecelendirme\",\"Minimum priority\":\"Minimum öncelik\",\"Active within\":\"İçinde aktif\",\"days\":\"günler\",\"Minimum appearances\":\"Minimum görünüş\",\"Minimum live observations\":\"Minimum canlı gözlemler\",\"Filter by typical online time\":\"Tipik çevrimiçi süreye göre filtrele\",\"Weekday\":\"Hafta içi\",\"Hour (local time)\":\"Saat (yerel saat)\",\"Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.\":\"Sıfır veya boş alanlar sınır olmadığı anlamına gelir. Yaş sınırı etkinken bilinmeyen yaşlar hariç tutulur. Zaman filtreleri, bu cihazda gözlemlenen canlı süreleri kullanır ve gerektiğinde izleme sürelerine dikkatli bir şekilde geri döner.\",\"Reset all library filters\":\"Tüm kitaplık filtrelerini sıfırla\",\"Open live room\":\"Canlı odayı aç\",\"Open room page\":\"Oda sayfasını aç\",\"Status\":\"Durum\",\"Open model room in a new tab\":\"Model odasını yeni bir sekmede aç\",\"Open the Chaturbate single-model view in a new tab\":\"Chaturbate tek model görünümünü yeni bir sekmede açın\",\"Save profile\":\"Profili kaydet\",\"Restore\":\"Geri yükle\",\"Archive\":\"Arşiv\",\"Advanced statistics filters\":\"Gelişmiş istatistik filtreleri\",\"Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.\":\"Her filtre birleştirilebilir; ülkeler, etiketler, gruplar ve türler çoklu seçimi destekler. Seçimler kalıcı olarak kaydedilir.\",\"Own groups (multiple)\":\"Kendi grupları (birden fazla)\",\"Add group…\":\"Grup ekle…\",\"Tag matching\":\"Etiket eşleştirme\",\"At least one\":\"En az bir\",\"All selected tags\":\"Seçilen tüm etiketler\",\"Group matching\":\"Grup eşleştirme\",\"All selected groups\":\"Seçilen tüm gruplar\",\"Cover/example image\":\"Kapak/örnek resim\",\"Known public image available\":\"Bilinen genel görsel mevcut\",\"No known image\":\"Bilinen resim yok\",\"Currently live\":\"Şu anda canlı\",\"Maximum total watch time\":\"Maksimum toplam izlenme süresi\",\"Maximum sessions\":\"Maksimum oturum\",\"Minimum active days\":\"Minimum aktif gün sayısı\",\"Minimum previews\":\"Minimum önizlemeler\",\"Minimum quick views\":\"Minimum hızlı görüntüleme\",\"Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.\":\"Sıfır veya boş alanlar sınır olmadığı anlamına gelir. Bilinmeyen değerler asla tahmin edilmez; yaş aralıkları bilinmeyen yaşları hariç tutar.\",\"Reset all statistics filters\":\"Tüm istatistik filtrelerini sıfırla\",\"Search name, tags or notes…\":\"Adı, etiketleri veya notları arayın…\",\"Search\":\"Ara\",\"Sorting\":\"Sıralama\",\"Most watch time\":\"En çok izlenme süresi\",\"Least watch time\":\"En az izlenme süresi\",\"Recently active\":\"Son zamanlarda aktif\",\"Longest inactive\":\"En uzun süre aktif değil\",\"Most sessions\":\"Çoğu oturum\",\"Fewest sessions\":\"En az oturum\",\"Longest average session\":\"En uzun ortalama oturum\",\"Shortest average session\":\"En kısa ortalama oturum\",\"Longest single session\":\"En uzun tek oturum\",\"Most active days\":\"En aktif günler\",\"Highest watch share\":\"En yüksek izlenme payı\",\"Most previews\":\"Çoğu önizleme\",\"Most quick views\":\"En hızlı görüntülemeler\",\"Most discoveries\":\"Çoğu keşif\",\"Most followers\":\"En çok takipçi\",\"Youngest first\":\"En genç birinci\",\"Oldest first\":\"Önce en eski\",\"Highest rating\":\"En yüksek derecelendirme\",\"Highest priority\":\"En yüksek öncelik\",\"Most appearances\":\"Çoğu görünüş\",\"Most live observations\":\"Çoğu canlı gözlem\",\"First observed\":\"İlk gözlemlenen\",\"Country A–Z\":\"A'dan Z'ye Ülke\",\"Most public tags\":\"Çoğu genel etiket\",\"Most groups\":\"Çoğu grup\",\"Best technical quality\":\"En iyi teknik kalite\",\"Cover/example images first\":\"Önce kapak/örnek görseller\",\"All statuses\":\"Tüm durumlar\",\"Live only\":\"Yalnızca canlı\",\"Library only\":\"Yalnızca kütüphane\",\"View\":\"Görünüm\",\"Active library\":\"Aktif kütüphane\",\"Live now\":\"Şimdi yaşa\",\"With notes\":\"Notlarla\",\"Newest saved/discovered\":\"En yeni kaydedilen/keşfedilen\",\"Oldest saved/discovered\":\"En eski kaydedilen/keşfedilen\",\"Lowest rating\":\"En düşük derecelendirme\",\"Lowest priority\":\"En düşük öncelik\",\"Live longest now\":\"Şimdi en uzun yaşa\",\"Age youngest first\":\"Önce en küçük yaş\",\"Age oldest first\":\"Önce en yaşlı yaş\",\"Most recently live\":\"En son canlı\",\"First discovered\":\"İlk keşfedilen\",\"Most own groups\":\"Çoğu kendi grubu\",\"Card size\":\"Kart boyutu\",\"Compact\":\"Kompakt\",\"Comfortable\":\"Rahat\",\"Large\":\"Büyük\",\"Cinema\":\"Sinema\",\"Extra large\":\"Ekstra büyük\",\"Models per row\":\"Satır başına modeller\",\"Automatic for window\":\"Pencere için otomatik\",\"Recorded models\":\"Kaydedilen modeller\",\"Total cam time\":\"Toplam kamera süresi\",\"across all models\":\"tüm modellerde\",\"Dock + hover\":\"Panel + üzerine gelme\",\"Last 30 days\":\"Son 30 gün\",\"local events\":\"yerel etkinlikler\",\"Top model\":\"Üst model\",\"no watch time yet\":\"henüz izlenme süresi yok\",\"Deeper analysis\":\"Daha derin analiz\",\"Open Analytics Studio\":\"Analytics Studio'yu açın\",\"Late night\":\"Gece geç saatlerde\",\"Morning\":\"Sabah\",\"Daytime\":\"Gündüz\",\"Evening\":\"akşam\",\"Gaming\":\"Oyun\",\"Music & DJ\":\"Müzik ve DJ\",\"Dance\":\"Dans\",\"Cosplay & anime\":\"Cosplay ve anime\",\"Fitness & sports\":\"Fitness ve spor\",\"Talk & social\":\"Konuşma ve sosyal\",\"Outdoor & travel\":\"Açık hava ve seyahat\",\"Roleplay\":\"Rol yapma\",\"Performance\":\"Performans\",\"My interest statistics\":\"İlgi alanım istatistikleri\",\"All local data\":\"Tüm yerel veriler\",\"Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.\":\"Gerçek yerel izlenme süresi, oturumlar, önizlemeler, hızlı görüntülemeler, favoriler ve şu anda bilinen herkese açık model bilgilerinden hesaplanır.\",\"Favorite tag\":\"Favori etiketi\",\"not enough data\":\"yeterli veri yok\",\"Favorite genre\":\"Favori tür\",\"from public tags\":\"genel etiketlerden\",\"Most-watched age\":\"En çok izlenen yaş\",\"no age data\":\"yaş verisi yok\",\"Most-watched country\":\"En çok izlenen ülke\",\"no country data\":\"ülke verisi yok\",\"Favorite model type\":\"Favori model türü\",\"no type data\":\"tür verisi yok\",\"Favorite viewing time\":\"Favori izleme zamanı\",\"no time data\":\"zaman verisi yok\",\"Top-three concentration\":\"İlk üç konsantrasyon\",\"watch time on the top three models\":\"İlk üç modelin izlenme süresi\",\"Favorite affinity\":\"Favori yakın ilgi alanı\",\"watch time on current favorites\":\"mevcut favorilerde izlenme süresi\",\"watch time on HD models\":\"HD modellerde izlenme süresi\",\"Metadata coverage\":\"Meta veri kapsamı\",\"age\":\"yaş\",\"country\":\"ülke\",\"Tags, genres and browsing interests\":\"Etiketler, türler ve göz atma ilgi alanları\",\"Tags by watch time\":\"İzlenme süresine göre etiketler\",\"Your ranking appears when watched models have public tags.\":\"Sıralamanız, izlenen modellerin herkese açık etiketleri olduğunda görünür.\",\"Favorite genre worlds\":\"Favori tür dünyaları\",\"No genre world can yet be assigned from public tags.\":\"Henüz genel etiketlerden hiçbir tür dünyası atanamıyor.\",\"Your own tags\":\"Kendi etiketleriniz\",\"Your saved custom tags are also evaluated here by watch time.\":\"Kaydedilen özel etiketleriniz de burada izlenme süresine göre değerlendirilir.\",\"Preview-interest tags\":\"Önizleme ilgi alanı etiketleri\",\"Browsing interests appear when you use live previews or quick views.\":\"Canlı önizlemeleri veya hızlı görünümleri kullandığınızda göz atma ilgi alanları görünür.\",\"Most-previewed models\":\"En çok önizlenen modeller\",\"No previews have been recorded in this period yet.\":\"Bu dönemde henüz hiçbir önizleme kaydedilmedi.\",\"Age, origin and model type\":\"Yaş, köken ve model türü\",\"Age groups by watch time\":\"İzlenme süresine göre yaş grupları\",\"Not enough public age data is available.\":\"Yeterli kamusal yaş verisi mevcut değil.\",\"Countries by watch time\":\"İzlenme süresine göre ülkeler\",\"Not enough public country data is available.\":\"Yeterli sayıda halka açık ülke verisi mevcut değil.\",\"Most-watched places\":\"En çok izlenen yerler\",\"Not enough public location data is available.\":\"Yeterli genel konum verisi mevcut değil.\",\"Model types by watch time\":\"İzlenme süresine göre model türleri\",\"Not enough public model-type data is available.\":\"Yeterli genel model tipi veri mevcut değil.\",\"Viewing rhythm and favorite signals\":\"İzleme ritmi ve favori sinyalleri\",\"Most active weekdays\":\"Hafta içi en aktif günler\",\"Not enough visit events are available.\":\"Yeterli ziyaret etkinliği mevcut değil.\",\"Times of day\":\"Günün saatleri\",\"Tags among favorites\":\"Favoriler arasındaki etiketler\",\"Preference signals appear when you mark models as favorites.\":\"Modelleri favori olarak işaretlediğinizde tercih sinyalleri görünür.\",\"Countries among favorites\":\"Favoriler arasında yer alan ülkeler\",\"No known countries among favorites yet.\":\"Favoriler arasında henüz bilinen bir ülke yok.\",\"Age groups among favorites\":\"Favoriler arasında yaş grupları\",\"No known ages among favorites yet.\":\"Favoriler arasında henüz bilinen bir yaş yok.\",\"Model types among favorites\":\"Favoriler arasında model çeşitleri\",\"No known model types among favorites yet.\":\"Favoriler arasında henüz bilinen bir model türü yok.\",\"These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.\":\"Bu sonuçlar bir sunucuya gönderilmez. Yalnızca gerçek izlenme süresi izleme tercihleri ​​için dikkate alınır; önizlemeler ve favoriler ayrı ayrı gösterilir. Eksik yaş, ülke, konum veya etiket verileri asla tahmin edilmez.\",\"Open Following on Chaturbate\":\"Chaturbate'te Takip Etmeyi Aç\",\"A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.\":\"Chaturbate belgelenmiş bir kişisel geçmiş API'si sağlamadığından tam izleme süresi veya ziyaret geçmişi geriye dönük olarak alınamaz. Saklanan kişisel verilerinizin bir kopyasını Chaturbate'in resmi veri aktarımı yoluyla talep edebilirsiniz. Bu dışa aktarma kullanılabilir geçmiş alanları içeriyorsa RoomGrid, dosya formatı doğrulandıktan sonra güvenli bir içe aktarma işlemini destekleyebilir. Resmi İstatistik API'si mevcut yayıncı değerlerini sağlar; RoomGrid yalnızca ilk bağlantıdan itibaren kendi geçmişini oluşturabilir.\",\"Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.\":\"Yerel olarak gözlemlenen, kaydedilen ve izlenen her modeli izlenme süresi, oturumlar, etkinlik, önizlemeler, kalite ve sıralamayla karşılaştırın.\",\"Large model library\":\"Geniş model kütüphanesi\",\"Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.\":\"Modelleriniz, canlı durum, notlar, etiketler, derecelendirmeler, öncelik, gruplar ve doğrudan eylemlerle zengin görüntülü, kullanışlı profiller olarak sunulur.\",\"My user dashboard\":\"Kullanıcı kontrol panelim\",\"Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.\":\"Özel yerel kullanım geçmişiniz, alışkanlıklarınız, kütüphane verileriniz ve yalnızca bilinçli olarak bağlantı kurulduğunda resmi yayıncı hesabı verileriniz.\",\"🔒 Local & private on this device\":\"🔒 Bu cihazda yerel ve gizli\",\"Preparing data…\":\"Veriler hazırlanıyor…\",\"RoomGrid Comfort Center\":\"RoomGrid kontrol merkezi\",\"inside the standard Chaturbate page\":\"standart Chaturbate sayfasının içinde\",\"All RoomGrid pages and settings\":\"Tüm RoomGrid sayfaları ve ayarları\",\"Media paused; names and images are covered.\":\"Medya duraklatıldı; isimler ve görseller yer alıyor.\",\"Restore previous view\":\"Önceki görünümü geri yükle\",\"Enter local PIN\":\"Yerel PIN'i girin\",\"In-page notifications\":\"Sayfa içi bildirimler\",\"No new notifications\":\"Yeni bildirim yok\",\"Notifications are off\":\"Bildirimler kapalı\",\"RoomGrid notification\":\"RoomGrid bildirimi\",\"Dismiss as read\":\"Okundu olarak reddet\",\"More unread notifications\":\"Daha fazla okunmamış bildirim\",\"RoomGrid update\":\"RoomGrid güncellemesi\",\"A monitored source changed status.\":\"İzlenen bir kaynağın durumu değişti.\",\"This is a local RoomGrid test notification.\":\"Bu yerel bir RoomGrid test bildirimidir.\",\"Test successful: notifications can appear directly in the RoomGrid bar.\":\"Başarılı test: bildirimler doğrudan RoomGrid çubuğunda görünebilir.\",\"Settings\":\"Ayarlar\",\"Workspaces, library, design, data, history, notifications, privacy and performance\":\"Çalışma alanları, kitaplık, tasarım, veriler, geçmiş, bildirimler, gizlilik ve performans\",\"Workspaces\":\"Çalışma alanları\",\"Data\":\"Veri\",\"History & statistics\":\"Tarih ve istatistik\",\"Broadcaster stats\":\"Yayıncı istatistikleri\",\"Notifications\":\"Bildirimler\",\"Windows & performance\":\"Windows ve performans\",\"Optional tools\":\"İsteğe bağlı araçlar\",\"Settings pages\":\"Ayarlar sayfaları\",\"RoomGrid control center\":\"RoomGrid kontrol merkezi\",\"Any online model\":\"Herhangi bir çevrimiçi model\",\"Workspace name\":\"Çalışma alanı adı\",\"Optional description\":\"İsteğe bağlı açıklama\",\"Active\":\"Aktif\",\"No description\":\"Açıklama yok\",\"Name\":\"İsim\",\"Description\":\"Açıklama\",\"Replace offline models\":\"Çevrimdışı modelleri değiştir\",\"Replacement group\":\"Değiştirme grubu\",\"Delay seconds\":\"Gecikme saniyesi\",\"Save current state\":\"Mevcut durumu kaydet\",\"New window\":\"Yeni pencere\",\"Export this workspace\":\"Bu çalışma alanını dışa aktar\",\"Duplicate\":\"Çoğalt\",\"Workspace restored\":\"Çalışma alanı geri yüklendi\",\"Workspace archived\":\"Çalışma alanı arşivlendi\",\"Delete permanently\":\"Kalıcı olarak sil\",\"Permanently delete this workspace?\":\"Bu çalışma alanı kalıcı olarak silinsin mi?\",\"Saved layout and panel sizes\":\"Kaydedilen düzen ve panel boyutları\",\"Apply\":\"Uygula\",\"Layout template deleted\":\"Düzen şablonu silindi\",\"Parent group\":\"Ebeveyn grubu\",\"None\":\"Yok\",\"Group description\":\"Grup açıklaması\",\"Save current workspace\":\"Mevcut çalışma alanını kaydet\",\"Save layout template\":\"Düzen şablonunu kaydet\",\"Template name\":\"Şablon adı\",\"Layout templates\":\"Düzen şablonları\",\"Device layout\":\"Cihaz düzeni\",\"Groups and subgroups\":\"Gruplar ve alt gruplar\",\"Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.\":\"Her çalışma alanı ayrı masaüstü ve kompakt düzenleri depolar. Otomatik değiştirme varsayılan olarak kapalıdır ve yalnızca söz konusu çalışma alanı için etkinleştirdikten sonra çevrimdışı yuvaları doldurur.\",\"No templates yet.\":\"Henüz şablon yok.\",\"Group name\":\"Grup adı\",\"New subgroup\":\"Yeni alt grup\",\"Enter parent group name\":\"Ebeveyn grubunun adını girin\",\"Subgroup name\":\"Alt grup adı\",\"Name, note or local tag\":\"Ad, not veya yerel etiket\",\"Choose group…\":\"Grup seç…\",\"Add selected models to group\":\"Seçilen modelleri gruba ekle\",\"Watch later\":\"Daha sonra izle\",\"Paused\":\"Duraklatıldı\",\"Collection name\":\"Koleksiyon adı\",\"Tags, comma separated\":\"Etiketler, virgülle ayrılmış\",\"Countries, comma separated\":\"Ülkeler, virgülle ayrılmış\",\"All types\":\"Tüm türler\",\"Rule name\":\"Kural adı\",\"Required tags\":\"Gerekli etiketler\",\"Local alert\":\"Yerel uyarı\",\"Add to favorites\":\"Favorilere ekle\",\"Add to group\":\"Gruba ekle\",\"Any tag\":\"Herhangi bir etiket\",\"Any country\":\"Herhangi bir ülke\",\"Pause\":\"Duraklat\",\"Enable\":\"Etkinleştir\",\"Delete\":\"Sil\",\"Automation rule deleted\":\"Otomasyon kuralı silindi\",\"Select model\":\"Modeli seçin\",\"Later\":\"Daha sonra\",\"Never visited\":\"Hiç ziyaret edilmedi\",\"Last online\":\"Son çevrimiçi\",\"Last visited\":\"Son ziyaret\",\"Local note\":\"Yerel not\",\"Local tags, comma separated\":\"Yerel etiketler, virgülle ayrılmış\",\"Remove later\":\"Daha sonra kaldır\",\"Hide today\":\"Bugünü gizle\",\"Pause 7 days\":\"7 gün duraklat\",\"Never suggest\":\"Asla önerme\",\"Select results\":\"Sonuçları seçin\",\"Clear selection\":\"Seçimi temizle\",\"selected\":\"seçilmiş\",\"Add to watch later\":\"Daha sonra izlemek için ekleyin\",\"Permanently delete selected models?\":\"Seçili modeller kalıcı olarak silinsin mi?\",\"Smart collections\":\"Akıllı koleksiyonlar\",\"Collections update automatically as local metadata and statistics grow; models are not duplicated.\":\"Koleksiyonlar, yerel meta veriler ve istatistikler büyüdükçe otomatik olarak güncellenir; modeller kopyalanmaz.\",\"Tags\":\"Etiketler\",\"Countries\":\"Ülkeler\",\"Favorites only\":\"Yalnızca favoriler\",\"Online only\":\"Yalnızca çevrimiçi\",\"Save smart collection\":\"Akıllı koleksiyonu kaydet\",\"Delete current collection\":\"Mevcut koleksiyonu sil\",\"Delete this smart collection? Model data stays unchanged.\":\"Bu akıllı koleksiyon silinsin mi? Model verileri değişmeden kalır.\",\"Rules & automation center\":\"Kurallar ve otomasyon merkezi\",\"When local model data matches, show one alert, add a favorite, or add the model to a chosen group.\":\"Yerel model verileri eşleştiğinde bir uyarı gösterin, bir favori ekleyin veya modeli seçilen bir gruba ekleyin.\",\"Action\":\"Eylem\",\"Target group\":\"Hedef grup\",\"Save rule\":\"Kuralı kaydet\",\"No automation rules yet.\":\"Henüz otomasyon kuralı yok.\",\"Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.\":\"Kullanıcı adları tam normalleştirilmiş eşleşmeyle tekilleştirilir. Arama adları, notları ve yerel etiketleri kapsar. Arşivleme, görünümden kaldırma ve kalıcı silme işlemleri ayrı işlemler olarak kalır.\",\"No matching models.\":\"Eşleşen model yok.\",\"export\":\"ihracat\",\"Models and notes\":\"Modeller ve notlar\",\"This backup could not be read. Current data was not changed.\":\"Bu yedekleme okunamadı. Mevcut veriler değişmedi.\",\"Import preview (nothing changed yet)\":\"Önizlemeyi içe aktar (henüz hiçbir şey değişmedi)\",\"conflicts\":\"çatışmalar\",\"Includes sensitive statistics\":\"Hassas istatistikler içerir\",\"models\":\"modeller\",\"events\":\"olaylar\",\"quality samples\":\"kaliteli örnekler\",\"Import statistics too\":\"İstatistikleri de içe aktar\",\"Import method\":\"İçe aktarma yöntemi\",\"Merge (recommended)\":\"Birleştir (önerilir)\",\"Replace selected sections\":\"Seçili bölümleri değiştir\",\"Apply import\":\"İçe aktarmayı uygula\",\"Import complete. Previous configuration and statistics were backed up separately.\":\"İçe aktarma tamamlandı. Önceki konfigürasyon ve istatistikler ayrı ayrı yedeklendi.\",\"The import could not be saved safely. The backup remains available.\":\"İçe aktarma güvenli bir şekilde kaydedilemedi. Yedekleme kullanılabilir durumda kalır.\",\"Cancel preview\":\"Önizlemeyi iptal et\",\"This file contains model, note or workspace data and should be treated as sensitive.\":\"Bu dosya model, not veya çalışma alanı verileri içeriyor ve hassas olarak değerlendirilmelidir.\",\"This import is not marked sensitive.\":\"Bu içe aktarma hassas olarak işaretlenmedi.\",\"Data version\":\"Veri sürümü\",\"Software version\":\"Yazılım sürümü\",\"Revision\":\"Revizyon\",\"Local models\":\"Yerel modeller\",\"Selective export\":\"Seçmeli dışa aktarma\",\"Export selected\":\"Seçileni dışa aktar\",\"Full export\":\"Tam ihracat\",\"Complete safety package\":\"Eksiksiz güvenlik paketi\",\"Export statistics separately\":\"İstatistikleri ayrı olarak dışa aktar\",\"Choose file and preview\":\"Dosyayı seçin ve önizleyin\",\"The complete safety package contains configuration and sensitive statistics, but never the official statistics token. Every import is previewed and backs up configuration and statistics separately before applying.\":\"Güvenlik paketinin tamamı, yapılandırmayı ve hassas istatistikleri içerir, ancak hiçbir zaman resmi istatistik belirtecini içermez. Her içe aktarma işlemi önizlenir ve uygulanmadan önce yapılandırma ve istatistikler ayrı ayrı yedeklenir.\",\"Storage and backup center\":\"Depolama ve yedekleme merkezi\",\"Local storage\":\"Yerel depolama\",\"Site storage\":\"Site depolama\",\"Available quota\":\"Mevcut kota\",\"Internal statistics backups\":\"Dahili istatistik yedeklemeleri\",\"Configuration\":\"Yapılandırma\",\"workspaces\":\"çalışma alanları\",\"LocalStorage + update vault\":\"LocalStorage + güncelleme kasası\",\"Statistics database\":\"İstatistik veritabanı\",\"Secrets\":\"Sırlar\",\"The official statistics token is never included in normal or complete backups.\":\"Resmi istatistik belirteci hiçbir zaman normal veya tam yedeklemelere dahil edilmez.\",\"Excluded\":\"Hariç tutuldu\",\"Create configuration restore point\":\"Yapılandırma geri yükleme noktası oluştur\",\"Create statistics restore point\":\"İstatistik geri yükleme noktası oluştur\",\"Legacy cleanup\":\"Eski temizleme\",\"Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.\":\"Eski veriler hiçbir zaman otomatik olarak silinmez. Eski v6/v7 anahtarlarını yalnızca başarılı bir incelemeden sonra kaldırın.\",\"Remove old v6/v7 copies\":\"Eski v6/v7 kopyalarını kaldırın\",\"Remove only old v6/v7 copies? Current data and backups remain.\":\"Yalnızca eski v6/v7 kopyaları kaldırılsın mı? Güncel veriler ve yedeklemeler kalır.\",\"items removed\":\"öğeler kaldırıldı\",\"Blue and teal, the RoomGrid signature\":\"Mavi ve turkuaz, RoomGrid imzası\",\"Ember\":\"kor\",\"Warm orange and rose\":\"Sıcak turuncu ve gül\",\"Frost\":\"Don\",\"Crisp cyan and violet\":\"Gevrek camgöbeği ve menekşe\",\"Custom\":\"Özel\",\"Use the colors selected below\":\"Aşağıda seçilen renkleri kullanın\",\"Name the current appearance\":\"Geçerli görünümü adlandırın\",\"My appearance\":\"Görünüşüm\",\"Delete appearance profile\":\"Görünüm profilini sil\",\"This appearance profile could not be imported.\":\"Bu görünüm profili içe aktarılamadı.\",\"Light\":\"Işık\",\"Dark\":\"Karanlık\",\"Export\":\"İhracat\",\"Home\":\"Ana sayfa\",\"Listings and tags\":\"Listeler ve etiketler\",\"Model pages\":\"Model sayfaları\",\"Use global appearance\":\"Genel görünümü kullan\",\"This logo format is unsafe or unsupported.\":\"Bu logo biçimi güvenli değil veya desteklenmiyor.\",\"The logo file could not be read.\":\"Logo dosyası okunamadı.\",\"MultiCam tools\":\"Çoklu Kamera araçları\",\"Page\":\"Sayfa\",\"Panel\":\"Paneli\",\"Card surface\":\"Kart yüzeyi\",\"Input\":\"Giriş\",\"Border\":\"Sınır\",\"Text\":\"Metin\",\"Muted text\":\"Sessiz metin\",\"Success\":\"Başarı\",\"Warning\":\"Uyarı\",\"Danger\":\"Tehlike\",\"Info and focus\":\"Bilgi ve odak\",\"Data hub\":\"Veri merkezi\",\"Text / page\":\"Metin / sayfa\",\"Text / panel\":\"Metin / panel\",\"Muted text / panel\":\"Sessiz metin / panel\",\"Accent text / accent\":\"Vurgulu metin / vurgu\",\"Desktop\":\"Masaüstü\",\"Tablet\":\"tablet\",\"Mobile\":\"Mobil\",\"Brand and logo studio\":\"Marka ve logo stüdyosu\",\"Tagline\":\"Slogan\",\"Brand display\":\"Marka ekranı\",\"Full name\":\"Tam ad\",\"Uploaded logo\":\"Yüklenen logo\",\"Logo shape\":\"Logo şekli\",\"Rounded\":\"Yuvarlak\",\"Circle\":\"daire\",\"Square\":\"Kare\",\"Pill\":\"Hap\",\"Page title\":\"Sayfa başlığı\",\"Website original\":\"Web sitesi orijinali\",\"Brand plus original\":\"Marka artı orijinal\",\"Custom title\":\"Özel başlık\",\"Custom page title\":\"Özel sayfa başlığı\",\"Image fit\":\"Görüntüye sığdırma\",\"Horizontal position\":\"Yatay konum\",\"Vertical position\":\"Dikey konum\",\"Upload light-mode logo\":\"Işık modu logosunu yükleyin\",\"Upload dark-mode logo\":\"Karanlık mod logosu yükleyin\",\"Remove logos\":\"Logoları kaldır\",\"Show version\":\"Sürümü göster\",\"Use logo as tab icon on RoomGrid pages\":\"RoomGrid sayfalarında logoyu sekme simgesi olarak kullanın\",\"Replace website logo in the original header (local only)\":\"Orijinal başlıktaki web sitesi logosunu değiştirin (yalnızca yerel)\",\"Complete color system\":\"Komple renk sistemi\",\"Control page, panel, card, input, border, text and status colors in addition to accents.\":\"Vurguların yanı sıra kontrol sayfası, panel, kart, giriş, kenarlık, metin ve durum renkleri.\",\"Derived automatically\":\"Otomatik olarak türetildi\",\"Fully custom\":\"Tamamen özel\",\"Editing dark palette\":\"Koyu paleti düzenleme\",\"Editing light palette\":\"Işık paletini düzenleme\",\"Reset current palette\":\"Geçerli paleti sıfırla\",\"Layout editor\":\"Düzen düzenleyici\",\"Set content spacing, card width, header and sidebar consistently across RoomGrid pages.\":\"RoomGrid sayfalarında içerik aralığını, kart genişliğini, başlığı ve kenar çubuğunu tutarlı bir şekilde ayarlayın.\",\"Minimum card width\":\"Minimum kart genişliği\",\"Section spacing\":\"Bölüm aralığı\",\"Content padding\":\"İçerik dolgusu\",\"Header height\":\"Başlık yüksekliği\",\"Maximum columns\":\"Maksimum sütunlar\",\"Automatic\":\"Otomatik\",\"Card aspect\":\"Kart yönü\",\"Sidebar position\":\"Kenar çubuğu konumu\",\"Left\":\"Sol\",\"Right\":\"Sağ\",\"Keep header visible while scrolling\":\"Kaydırırken başlığı görünür tutun\",\"Live device preview\":\"Canlı cihaz önizlemesi\",\"Contrast and accessibility\":\"Kontrast ve erişilebilirlik\",\"High-contrast mode\":\"Yüksek kontrast modu\",\"Underline links\":\"Bağlantıların altını çizin\",\"Reduce transparency\":\"Şeffaflığı azaltın\",\"Boost base text size\":\"Temel metin boyutunu artır\",\"Focus ring width\":\"Odak halkası genişliği\",\"Minimum target size\":\"Minimum hedef boyutu\",\"Respect system reduced-motion setting\":\"Sistemin azaltılmış hareket ayarına saygı gösterin\",\"Apply accessible defaults\":\"Erişilebilir varsayılanları uygula\",\"Profiles by page and screen size\":\"Sayfa ve ekran boyutuna göre profiller\",\"Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.\":\"Bir veya daha fazla görünümü kaydedin ve ardından bunları masaüstü, tablet ve mobil cihazlar için sayfa türüne göre atayın. Ekran boyutu değiştiğinde profiller otomatik olarak değişir.\",\"Enable responsive design profiles\":\"Duyarlı tasarım profillerini etkinleştir\",\"Mobile maximum width\":\"Mobil maksimum genişlik\",\"Tablet maximum width\":\"Tabletin maksimum genişliği\",\"RoomGrid design system\":\"RoomGrid tasarım sistemi\",\"Choose a brand direction, content width and card information density. Every page follows immediately.\":\"Bir marka yönü, içerik genişliği ve kart bilgisi yoğunluğunu seçin. Her sayfa hemen takip ediyor.\",\"Primary accent\":\"Birincil vurgu\",\"Secondary accent\":\"İkincil vurgu\",\"Content width\":\"İçerik genişliği\",\"Centered\":\"ortalanmış\",\"Wide\":\"Geniş\",\"Full width\":\"Tam genişlik\",\"Card style\":\"Kart stili\",\"Visual\":\"Görsel\",\"Data-rich\":\"Veri açısından zengin\",\"Micro\":\"Mikro\",\"Widget density\":\"Widget yoğunluğu\",\"Spacious\":\"Geniş\",\"Theme and surfaces\":\"Tema ve yüzeyler\",\"Surface style\":\"Yüzey stili\",\"Soft\":\"Yumuşak\",\"Flat\":\"Düz\",\"Glass\":\"Cam\",\"Page background\":\"Sayfa arka planı\",\"Solid\":\"Katı\",\"Gradient\":\"Gradyan\",\"Ambient glow\":\"Ortam parıltısı\",\"Surface opacity\":\"Yüzey opaklığı\",\"Custom accent color\":\"Özel vurgu rengi\",\"Sizing and motion\":\"Boyutlandırma ve hareket\",\"Font style\":\"Yazı tipi stili\",\"System\":\"Sistem\",\"Classic serif\":\"Klasik serif\",\"Monospace\":\"Tek uzay\",\"Density\":\"Yoğunluk\",\"Font scale\":\"Yazı tipi ölçeği\",\"Control size\":\"Kontrol boyutu\",\"Corner radius\":\"Köşe yarıçapı\",\"Card spacing\":\"Kart aralığı\",\"Shadow strength\":\"Gölge gücü\",\"Sidebar width\":\"Kenar çubuğu genişliği\",\"Enable animations\":\"Animasyonları etkinleştir\",\"Appearance profiles\":\"Görünüm profilleri\",\"Save the complete appearance as a reusable profile. Profiles remain available after script updates.\":\"Görünümün tamamını yeniden kullanılabilir bir profil olarak kaydedin. Komut dosyası güncellemelerinden sonra profiller kullanılabilir durumda kalır.\",\"Save current appearance\":\"Mevcut görünümü kaydet\",\"Import profile\":\"Profili içe aktar\",\"No profiles saved yet.\":\"Henüz kayıtlı profil yok.\",\"Instant preview\":\"Anında önizleme\",\"Example card\":\"Örnek kart\",\"This is the combined result of the current visual settings.\":\"Bu, mevcut görsel ayarların birleşik sonucudur.\",\"Example action\":\"Örnek eylem\",\"Per-page design profiles\":\"Sayfa başına tasarım profilleri\",\"Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.\":\"Ana Sayfa, listeler, model sayfaları, MultiCam, Keşfet ve veri merkezi için farklı bir kayıtlı görünüm seçin.\",\"Enable per-page design\":\"Sayfa başına tasarımı etkinleştir\",\"Normal website pages\":\"Normal web sitesi sayfaları\",\"Enable RoomGrid page design\":\"RoomGrid sayfa tasarımını etkinleştir\",\"Tag and category pages\":\"Etiket ve kategori sayfaları\",\"Cards\":\"Kartlar\",\"List\":\"Liste\",\"Page density\":\"Sayfa yoğunluğu\",\"Polish navigation\":\"Lehçe navigasyon\",\"Polish sidebars\":\"Lehçe kenar çubukları\",\"Hide recognized promo and merch areas\":\"Tanınan promosyon ve ürün alanlarını gizle\",\"Compact footer\":\"Kompakt altbilgi\",\"Mark saved models\":\"Kaydedilen modelleri işaretleme\",\"Mark favorites separately\":\"Favorileri ayrı ayrı işaretle\",\"Mark visited models\":\"Ziyaret edilen modelleri işaretle\",\"Show card quick actions\":\"Kart hızlı işlemlerini göster\",\"Animated live previews on home and listing pages\":\"Ana sayfa ve ilan sayfalarında animasyonlu canlı önizlemeler\",\"Preview refresh rate\":\"Yenileme hızını önizleyin\",\"Off\":\"Kapalı\",\"Update mode\":\"Güncelleme modu\",\"All visible cards simultaneously\":\"Tüm görünür kartlar aynı anda\",\"Performance-bounded batches\":\"Performansa bağlı gruplar\",\"Batch concurrency limit\":\"Toplu eşzamanlılık sınırı\",\"All visible\":\"Hepsi görünür\",\"Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.\":\"Yalnızca görünür kartlar sessiz JPEG çerçeve güncellemelerini alır; ekranın dışında veya gizli bir sekmede hemen duraklarlar. Senkronize mod, görünen her kartı tek bir dalgada yeniler; yüksek hızlar bant genişliğini ve CPU kullanımını belirgin şekilde artırır.\",\"Live video on hover\":\"Fareyle üzerine gelindiğinde canlı video\",\"Play real-time video while hovering\":\"Gezinirken gerçek zamanlı video oynatın\",\"Start delay\":\"Gecikmeyi başlat\",\"Immediately\":\"Hemen\",\"Hover enlargement\":\"Fareyle üzerine gelindiğinde büyütme\",\"Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.\":\"Yalnızca işaretçinin altındaki kart oynatılır ve her zaman sessize alınır. Sekmeyi uzaklaştırmak veya gizlemek akışı anında durdurur.\",\"Live Preview Dock\":\"Canlı Önizleme Yuvası\",\"Enable low-load live previews\":\"Düşük yüklü canlı önizlemeleri etkinleştir\",\"Concurrent previews\":\"Eşzamanlı önizlemeler\",\"Window size\":\"Pencere boyutu\",\"Small\":\"Küçük\",\"Medium\":\"Orta\",\"Previews start muted and pause in hidden tabs. Audio always requires an explicit click.\":\"Önizlemeler sessiz olarak başlar ve gizli sekmelerde duraklatılır. Ses her zaman açık bir tıklama gerektirir.\",\"Quick info & chat focus\":\"Hızlı bilgi ve sohbet odağı\",\"Enable model Quick View\":\"Modelin Hızlı Görünümünü etkinleştir\",\"Chat focus mode (opt-in)\":\"Sohbete odaklanma modu (etkinleştirme)\",\"Show local timestamps\":\"Yerel zaman damgalarını göster\",\"System messages\":\"Sistem mesajları\",\"Show\":\"Göster\",\"Dim\":\"Loş\",\"Hide\":\"Gizle\",\"Chat filter strength\":\"Sohbet filtresi gücü\",\"Low\":\"Düşük\",\"Strict\":\"Katı\",\"Highlight words (comma separated)\":\"Kelimeleri vurgulayın (virgülle ayrılmış)\",\"Muted words (comma separated)\":\"Sessiz kelimeler (virgülle ayrılmış)\",\"Collapse repeated messages\":\"Tekrarlanan mesajları daralt\",\"Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.\":\"Sohbet geliştirmeleri yalnızca mevcut ekranı değiştirir. Hiçbir zaman sohbeti saklamazlar, mesaj göndermezler veya otomatik olarak bahşiş vermezler.\",\"Shortcuts\":\"Kısayollar\",\"Command menu\":\"Komut menüsü\",\"Global model search\":\"Küresel model arama\",\"Reset design defaults\":\"Tasarım varsayılanlarını sıfırla\",\"Optional modules\":\"İsteğe bağlı modüller\",\"Normal-page dock and card actions\":\"Normal sayfa yerleştirme ve kart işlemleri\",\"Real audio meters\":\"Gerçek ses ölçerler\",\"Media, app and contest directories\":\"Medya, uygulama ve yarışma dizinleri\",\"Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.\":\"Modül anahtarları sayfa yeniden yüklendikten sonra tam etkili olur. Veri kaybını önlemek için çekirdek veriler ve yedekleme modülleri etkin kalır.\",\"Name the current filters and sorting\":\"Geçerli filtreleri adlandırma ve sıralama\",\"My smart view\":\"Akıllı görüşüm\",\"Delete smart view\":\"Akıllı görünümü sil\",\"Smart view deleted\":\"Akıllı görünüm silindi\",\"tags\":\"Etiketler\",\"countries\":\"ülkeler\",\"Saved selection\":\"Seçim kaydedildi\",\"Suggestions\":\"Öneriler\",\"Selected\":\"Seçildi\",\"Website tags\":\"Web sitesi etiketleri\",\"Countries (multiple selection)\":\"Ülkeler (çoklu seçim)\",\"Select countries\":\"Ülkeleri seçin\",\"Search countries…\":\"Ülkeleri ara…\",\"Recognized in current models\":\"Mevcut modellerde tanınır\",\"All countries\":\"Tüm ülkeler\",\"Tags (multiple selection)\":\"Etiketler (çoklu seçim)\",\"Select tags\":\"Etiketleri seçin\",\"Search tags…\":\"Etiketleri ara…\",\"Room subject\":\"Oda konusu\",\"Current online duration\":\"Mevcut çevrimiçi süre\",\"Local watch time and sessions\":\"Yerel izlenme süresi ve oturumlar\",\"HD, new, gaming and favorite badges\":\"HD, yeni, oyun ve favori rozetleri\",\"Homepage Studio\":\"Ana Sayfa Stüdyosu\",\"Balanced preset\":\"Dengeli ön ayar\",\"Compact preset\":\"Kompakt ön ayar\",\"Data-rich preset\":\"Veri açısından zengin ön ayar\",\"Cinema preset\":\"Sinema ön ayarı\",\"Layout and pagination\":\"Düzen ve sayfalandırma\",\"Show quick toolbar above models\":\"Hızlı araç çubuğunu modellerin üzerinde göster\",\"Cards per row\":\"Satır başına kartlar\",\"Image ratio\":\"Görüntü oranı\",\"Information display\":\"Bilgi ekranı\",\"Detailed\":\"Detaylı\",\"Overlay on image\":\"Resmin üzerine kaplama\",\"Powerful sorting\":\"Güçlü sıralama\",\"Primary sort\":\"Birincil sıralama\",\"Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.\":\"Sıralama yalnızca yüklü model kartlarının görüntülenme sırasını değiştirir. Chaturbate hesabınızdaki takipleri veya favorileri değiştirmez.\",\"Restore website order\":\"Web sitesi sırasını geri yükle\",\"Clear all filters\":\"Tüm filtreleri temizle\",\"Search and range filters\":\"Arama ve aralık filtreleri\",\"When several tags are selected\":\"Birkaç etiket seçildiğinde\",\"Model must contain every tag\":\"Model her etiketi içermelidir\",\"At least one tag matches\":\"En az bir etiket eşleşiyor\",\"Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.\":\"Ülke filtreleme, seçilen herhangi bir ülkedeki modelleri gösterir. Kendi etiketleriniz ve tanınan web sitesi etiketleri listede mevcut kalır ve aramayla hızlı bir şekilde bulunabilir.\",\"After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.\":\"Etiketleri veya ülkeleri seçtikten sonra RoomGrid mevcut canlı kataloğun tamamını arar. Aynı sonuç sayfası başlangıçta 120 eşleşme gösterir ve kaydırma sırasında daha fazlasını parçalar halinde yükler, böylece en yaygın etiketler bile duyarlı kalır. Ana sayfa araç çubuğundaki yeniden yükle düğmesi canlı listeyi yeniler.\",\"Quick conditions\":\"Hızlı koşullar\",\"HD models only\":\"Yalnızca HD modeller\",\"New models only\":\"Yalnızca yeni modeller\",\"Gaming models only\":\"Yalnızca oyun modelleri\",\"Only saved in RoomGrid\":\"Yalnızca RoomGrid'e kaydedildi\",\"Only own favorites\":\"Yalnızca kendi favorilerim\",\"Following only\":\"Yalnızca takip ediliyor\",\"Hide previously watched models\":\"Daha önce izlenen modelleri gizle\",\"Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.\":\"Ülke, yaş ve çevrimiçi süre, mevcut genel kartlardan veya genel oda rehberinden alınır. Chaturbate bir alan sağlamıyorsa RoomGrid bunu tahmin etmez.\",\"Smart filter views\":\"Akıllı filtre görünümleri\",\"Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.\":\"Geçerli filtreleri ve sıralamayı dinamik görünüm olarak kaydedin. Daha sonra çevrimiçi olan ve kurallara uyan modeller otomatik olarak görünür.\",\"Save current filter view\":\"Geçerli filtre görünümünü kaydet\",\"No smart views saved yet.\":\"Henüz akıllı görünüm kaydedilmedi.\",\"What to show for each model\":\"Her model için ne gösterilecek?\",\"Import complete\":\"İçe aktarma tamamlandı\",\"Invalid file\":\"Geçersiz dosya\",\"Range\":\"Aralık\",\"Today\":\"Bugün\",\"Week\":\"Hafta\",\"Month\":\"Ay\",\"Year\":\"Yıl\",\"Recording enabled\":\"Kayıt etkinleştirildi\",\"Private session (do not record)\":\"Özel oturum (kaydetmeyin)\",\"Retention days\":\"Saklama günleri\",\"Usage time\":\"Kullanım süresi\",\"Summed camera time\":\"Toplam kamera süresi\",\"Visits\":\"Ziyaretler\",\"Known models\":\"Bilinen modeller\",\"Weekly heatmap\":\"Haftalık ısı haritası\",\"Aggregated by weekday and hour; darker means more observed time.\":\"Hafta içi gün ve saate göre toplanmış; daha koyu, daha fazla gözlemlenen süre anlamına gelir.\",\"Sunday to Saturday, 24 hours per row\":\"Pazardan cumartesiye, satır başına 24 saat\",\"Watch ranking\":\"Sıralamayı izle\",\"little data\":\"az veri\",\"No watch history yet.\":\"Henüz izleme geçmişi yok.\",\"Workspace, filter and quality events\":\"Çalışma alanı, filtre ve kalite etkinlikleri\",\"Recent timeline\":\"Son zaman çizelgesi\",\"Export sensitive statistics\":\"Hassas istatistikleri dışa aktar\",\"Import statistics\":\"İstatistikleri içe aktar\",\"Delete selected range\":\"Seçilen aralığı sil\",\"Delete local events in the selected range?\":\"Seçilen aralıktaki yerel etkinlikler silinsin mi?\",\"Clear all statistics\":\"Tüm istatistikleri temizle\",\"Permanently clear all local statistics?\":\"Tüm yerel istatistikler kalıcı olarak temizlensin mi?\",\"Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.\":\"İstatistik dışa aktarmaları model adlarını ve zaman damgalarını içerir ve hassas olarak ele alınmalıdır. Eski veriler saklama durumuna göre temizlenir.\",\"new\":\"yeni\",\"Under 1 minute\":\"1 dakikadan az\",\"1–5 minutes\":\"1–5 dakika\",\"5–15 minutes\":\"5-15 dakika\",\"15–30 minutes\":\"15–30 dakika\",\"30+ minutes\":\"30+ dakika\",\"Local observations and your usage\":\"Yerel gözlemler ve kullanımınız\",\"Public profile loaded on request\":\"İstek üzerine genel profil yüklendi\",\"Local RoomGrid interaction\":\"Yerel RoomGrid etkileşimi\",\"Local browser player\":\"Yerel tarayıcı oynatıcısı\",\"Optional official statistics API\":\"İsteğe bağlı resmi istatistik API'si\",\"Local daily aggregation\":\"Yerel günlük toplama\",\"Local data\":\"Yerel veriler\",\"Model summaries\":\"Model özetleri\",\"Public profiles\":\"Herkese açık profiller\",\"Usage events\":\"Kullanım etkinlikleri\",\"Official account snapshots\":\"Resmi hesap anlık görüntüleri\",\"Daily rollups\":\"Günlük özetler\",\"Camera time\":\"Kamera zamanı\",\"vs previous period\":\"önceki döneme kıyasla\",\"Trends\":\"Trendler\",\"Daily activity\":\"Günlük aktivite\",\"Playback quality\":\"Oynatma kalitesi\",\"Privacy status\":\"Gizlilik durumu\",\"Data provenance\":\"Veri kaynağı\",\"Local RoomGrid usage\":\"Yerel RoomGrid kullanımı\",\"7 days\":\"7 gün\",\"30 days\":\"30 gün\",\"90 days\":\"90 gün\",\"Not enough data yet.\":\"Henüz yeterli veri yok.\",\"Models revisited\":\"Yeniden ziyaret edilen modeller\",\"Previews opened\":\"Önizlemeler açıldı\",\"Quick Views\":\"Hızlı Görünümler\",\"Samples\":\"Örnekler\",\"Private session\":\"Özel oturum\",\"History paused\":\"Geçmiş duraklatıldı\",\"Recording locally\":\"Yerel olarak kayıt\",\"Disabled\":\"Devre dışı\",\"No cloud sync or telemetry.\":\"Bulut senkronizasyonu veya telemetri yok.\",\"Raw events\":\"Ham olaylar\",\"Only local observations and actions from this browser.\":\"Bu tarayıcıdan yalnızca yerel gözlemler ve eylemler.\",\"Time\":\"Zaman\",\"Models\":\"Modeller\",\"Online planner\":\"Çevrimiçi planlayıcı\",\"Data & privacy\":\"Veri ve gizlilik\",\"Configure statistics dashboard\":\"İstatistik kontrol panelini yapılandırın\",\"Preset\":\"Ön ayar\",\"Minimal\":\"Asgari\",\"Quality focus\":\"Kalite odağı\",\"Privacy focus\":\"Gizlilik odağı\",\"Reset dashboard\":\"Gösterge tablosunu sıfırla\",\"Size\":\"Boyut\",\"Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.\":\"Sıra, görünürlük ve genişlik, güncelleme güvenliği olan ayarlarda saklanır. Her widget kendi veri kaynağını gösterir.\",\"No dashboard widgets are visible. Open configuration and select at least one widget.\":\"Hiçbir kontrol paneli widget'ı görünmüyor. Yapılandırmayı açın ve en az bir widget seçin.\",\"Median session\":\"Medyan oturum\",\"Daily usage trend\":\"Günlük kullanım eğilimi\",\"Your trend appears here after you start using RoomGrid.\":\"RoomGrid'i kullanmaya başladıktan sonra trendiniz burada görünür.\",\"Session-duration distribution\":\"Oturum süresi dağılımı\",\"No session-duration data yet.\":\"Henüz oturum süresi verisi yok.\",\"Return rate\":\"İade oranı\",\"Total sessions\":\"Toplam oturumlar\",\"Longest observed\":\"En uzun gözlemlenen\",\"Model ranking\":\"Model sıralaması\",\"Select up to four models for a local comparison.\":\"Yerel karşılaştırma için en fazla dört model seçin.\",\"Quick View\":\"Hızlı Görünüm\",\"No model data yet.\":\"Henüz model verisi yok.\",\"Models with time patterns\":\"Zaman desenli modeller\",\"High confidence\":\"Yüksek güven\",\"Moderate confidence\":\"Orta derecede güven\",\"Planning horizon\":\"Planlama ufku\",\"Likely next active times\":\"Muhtemelen bir sonraki aktif zamanlar\",\"Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.\":\"Yerel olarak gözlemlenen çevrimiçi örneklerden hesaplanmıştır; yayını garanti etmez. Daha fazla veriyle güven artar.\",\"High\":\"Yüksek\",\"Not enough online observations yet. The planner appears automatically with the library.\":\"Henüz yeterli çevrimiçi gözlem yok. Planlayıcı, kütüphaneyle birlikte otomatik olarak görünür.\",\"Discovery events\":\"Keşif etkinlikleri\",\"Local interaction funnel\":\"Yerel etkileşim hunisi\",\"Only your actions on this device are counted; chat content and payments are never tracked.\":\"Yalnızca bu cihazdaki işlemleriniz sayılır; sohbet içeriği ve ödemeler hiçbir zaman takip edilmez.\",\"Workspace events\":\"Çalışma alanı etkinlikleri\",\"Filter events\":\"Etkinlikleri filtrele\",\"Tag events\":\"Etkinlikleri etiketleyin\",\"Top-four workspace comparison\":\"İlk dört çalışma alanı karşılaştırması\",\"Workspace usage\":\"Çalışma alanı kullanımı\",\"No workspace data yet.\":\"Henüz çalışma alanı verisi yok.\",\"Average active streams\":\"Ortalama aktif akışlar\",\"Playback quality by model\":\"Modele göre oynatma kalitesi\",\"No quality data yet.\":\"Henüz kaliteli veri yok.\",\"The statistics file is invalid or exceeds safe limits. Current data was not changed.\":\"İstatistik dosyası geçersiz veya güvenli sınırları aşıyor. Mevcut veriler değişmedi.\",\"Statistics import assistant · preview\":\"İstatistik içe aktarma asistanı · önizleme\",\"Nothing has changed yet. Select the sections to import and review conflicts.\":\"Henüz hiçbir şey değişmedi. Çakışmaların içe aktarılacağı ve inceleneceği bölümleri seçin.\",\"Safe merge (recommended)\":\"Güvenli birleştirme (önerilir)\",\"Import complete. The previous statistics were backed up automatically.\":\"İçe aktarma tamamlandı. Önceki istatistikler otomatik olarak yedeklendi.\",\"The import could not be applied safely. Current data and backups remain unchanged.\":\"İçe aktarma güvenli bir şekilde uygulanamadı. Mevcut veriler ve yedeklemeler değişmeden kalır.\",\"File schema\":\"Dosya şeması\",\"Date range\":\"Tarih aralığı\",\"Sensitive data\":\"Hassas veriler\",\"Not marked sensitive\":\"Hassas olarak işaretlenmemiş\",\"Until manually resumed\":\"Manuel olarak devam ettirilene kadar\",\"Not paused\":\"Duraklatılmadı\",\"Restore this statistics backup? The current state will be backed up first.\":\"Bu istatistik yedeklemesi geri yüklensin mi? Önce mevcut durum yedeklenecektir.\",\"Permanently delete this backup?\":\"Bu yedekleme kalıcı olarak silinsin mi?\",\"Events\":\"Etkinlikler\",\"Estimated statistics storage\":\"Tahmini istatistik depolama alanı\",\"Recording, pauses and private sessions\":\"Kayıt, duraklatmalar ve özel oturumlar\",\"Record usage statistics\":\"Kullanım istatistiklerini kaydedin\",\"Pause 15 minutes\":\"15 dakika duraklatın\",\"Pause 1 hour\":\"1 saat duraklat\",\"Pause 8 hours\":\"8 saat duraklat\",\"Pause indefinitely\":\"Süresiz olarak duraklat\",\"Resume history\":\"Geçmişi devam ettir\",\"History status\":\"Geçmiş durumu\",\"Private for 30 minutes\":\"30 dakika boyunca özel\",\"Private for 2 hours\":\"2 saatliğine özel\",\"Private until resumed\":\"Devam ettirilene kadar gizli\",\"End private session\":\"Özel oturumu sonlandır\",\"Not active\":\"Aktif değil\",\"A private session also stops public model observations; a normal history pause stops personal usage and quality records.\":\"Özel bir oturum aynı zamanda genel model gözlemlerini de durdurur; normal bir geçmiş duraklaması kişisel kullanımı ve kaliteli kayıtları durdurur.\",\"Automatic deletion and retention\":\"Otomatik silme ve saklama\",\"Raw events (days)\":\"Ham olaylar (günler)\",\"Quality samples (days)\":\"Kaliteli numuneler (gün)\",\"Account snapshots (days)\":\"Hesap anlık görüntüleri (gün)\",\"Daily rollups (days)\":\"Günlük özetler (günler)\",\"Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.\":\"Süresi dolmuş ham olaylar, otomatik silinmeden önce günlük toplamlara dahil edilir. Kalite ve hesap anlık görüntüleri kendi sınırlarını kullanır.\",\"Statistics backup center\":\"İstatistik yedekleme merkezi\",\"Create internal backup now\":\"Şimdi dahili yedekleme oluşturun\",\"Pseudonymized export\":\"Takma adla dışa aktarma\",\"Print report\":\"Raporu yazdır\",\"Choose import file\":\"İçe aktarma dosyasını seçin\",\"No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.\":\"Henüz dahili istatistik yedeklemesi yok. İçe aktarma, geri yükleme veya manuel oluşturma sonrasında en fazla üç dosya saklanır.\",\"Delete local statistics in this range? A backup will be created first.\":\"Bu aralıktaki yerel istatistikler silinsin mi? İlk önce bir yedek oluşturulacak.\",\"Clear everything\":\"Her şeyi temizle\",\"Permanently clear all local statistics? A backup will be created first.\":\"Tüm yerel istatistikler kalıcı olarak temizlensin mi? İlk önce bir yedek oluşturulacak.\",\"Data inventory and provenance\":\"Veri envanteri ve kaynağı\",\"Stored only in this browser\":\"Yalnızca bu tarayıcıda depolanır\",\"Coverage\":\"Kapsam\",\"Unknown or missing data is never inferred.\":\"Bilinmeyen veya eksik verilerden hiçbir zaman çıkarım yapılmaz.\",\"Metric definitions & data quality\":\"Metrik tanımları ve veri kalitesi\",\"Accumulated locally while a RoomGrid page is active.\":\"RoomGrid sayfası etkinken yerel olarak toplanır.\",\"Sum of all observed camera sessions; concurrent streams are added.\":\"Gözlemlenen tüm kamera oturumlarının toplamı; eşzamanlı akışlar eklenir.\",\"browser samples; no interpolation.\":\"tarayıcı örnekleri; enterpolasyon yok.\",\"Privacy boundary\":\"Gizlilik sınırı\",\"No chat content, payments, or media files are saved. Private sessions add no events.\":\"Hiçbir sohbet içeriği, ödeme veya medya dosyası kaydedilmez. Özel oturumlar hiçbir etkinlik eklemez.\",\"No history yet\":\"Henüz geçmiş yok\",\"Loading\":\"Yükleniyor\",\"Connected\":\"Bağlı\",\"Locked\":\"Kilitli\",\"Not connected\":\"Bağlı değil\",\"Username\":\"Kullanıcı adı\",\"24-character stats token\":\"24 karakterli istatistik belirteci\",\"At least 8 characters\":\"En az 8 karakter\",\"Secure official statistics connection\":\"Güvenli resmi istatistik bağlantısı\",\"Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.\":\"İsteğe bağlı. Belirteç hiçbir zaman normal ayarlara, günlüklere veya dışa aktarımlara girmez. Oturum modu onu yalnızca bellekte tutar; şifreli mod, yalıtılmış yerel depolamayı kullanır.\",\"Stats token\":\"İstatistik jetonu\",\"Vault password\":\"Kasa şifresi\",\"Connect for this session\":\"Bu oturum için bağlanın\",\"Username or token is invalid.\":\"Kullanıcı adı veya jeton geçersiz.\",\"Encrypt, save & connect\":\"Şifreleyin, kaydedin ve bağlanın\",\"Could not save encrypted. Check the token, password and userscript-manager permissions.\":\"Şifrelenmiş olarak kaydedilemedi. Belirteci, parolayı ve kullanıcı komut dosyası yöneticisi izinlerini kontrol edin.\",\"Get official token\":\"Resmi jetonu alın\",\"Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.\":\"Otomatik yenileme en fazla beş dakikada bir çalışır. Bu modül yalnızca bilinçli olarak sağladığınız resmi bir jetonu kullanır.\",\"Account\":\"Hesap\",\"History snapshots\":\"Geçmiş anlık görüntüleri\",\"Last refresh\":\"Son yenileme\",\"Unlock local vault\":\"Yerel kasanın kilidini açın\",\"Password\":\"Şifre\",\"Unlock\":\"Kilidi aç\",\"Unlock failed.\":\"Kilit açma başarısız oldu.\",\"Loading…\":\"Yükleniyor…\",\"Refresh now\":\"Şimdi yenile\",\"Auto-refresh every 5 minutes\":\"Her 5 dakikada bir otomatik yenile\",\"Disconnect and delete token\":\"Bağlantıyı kes ve belirteci sil\",\"Delete the local connection and encrypted token? Historical statistics remain.\":\"Yerel bağlantı ve şifrelenmiş jeton silinsin mi? Tarihsel istatistikler kaldı.\",\"Broadcast status\":\"Yayın durumu\",\"Token balance\":\"Jeton bakiyesi\",\"Tips last hour\":\"Son saatteki ipuçları\",\"Followers\":\"Takipçiler\",\"Up votes\":\"Yukarı oylar\",\"Down votes\":\"Olumsuz oylar\",\"Satisfaction\":\"Memnuniyet\",\"Registered viewers\":\"Kayıtlı izleyiciler\",\"Tokened viewers\":\"Jetonlu izleyiciler\",\"Time online\":\"Çevrimiçi zaman\",\"Follower trend\":\"Takipçi trendi\",\"Tips trend\":\"İpuçları trendi\",\"Token-balance trend\":\"Jeton bakiyesi eğilimi\",\"Viewer trend\":\"İzleyici trendi\",\"Account Health Center\":\"Hesap Sağlığı Merkezi\",\"Official My Collection\":\"Resmi Koleksiyonum\",\"Official support\":\"Resmi destek\",\"Export local statistics\":\"Yerel istatistikleri dışa aktar\",\"Delete snapshot history\":\"Anlık görüntü geçmişini sil\",\"Delete the local official-statistics snapshots for this account?\":\"Bu hesaba ilişkin yerel resmi istatistik anlık görüntüleri silinsin mi?\",\"Last error\":\"Son hata\",\"Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.\":\"Token durumu, yenileme temposu, yerel tarih ve resmi bağlantılar burada yayınlanıyor. RoomGrid hiçbir zaman otomatik olarak yayınlamaz, mesaj göndermez veya işlem gerçekleştirmez.\",\"The browser did not grant notification permission. In-page notifications remain active.\":\"Tarayıcı bildirim izni vermedi. Sayfa içi bildirimler etkin kalır.\",\"Notification profile\":\"Bildirim profili\",\"Go-live only\":\"Yalnızca canlı yayına geç\",\"Personalized\":\"Kişiselleştirilmiş\",\"Always (all events)\":\"Her zaman (tüm etkinlikler)\",\"Inactive days\":\"Aktif olmayan günler\",\"Favorites, rating, priority and your tags decide relevance.\":\"Favoriler, derecelendirme, öncelik ve etiketleriniz alaka düzeyine karar verir.\",\"Includes go-live, offline and public-profile changes.\":\"Canlıya geçiş, çevrimdışı ve genel profil değişikliklerini içerir.\",\"Master switch\":\"Ana anahtar\",\"On\":\"Açık\",\"Off (default)\":\"Kapalı (varsayılan)\",\"Monitored models\":\"İzlenen modeller\",\"Active tab\":\"Etkin sekme\",\"This tab\":\"Bu sekme\",\"Another tab\":\"Başka bir sekme\",\"Local notifications\":\"Yerel bildirimler\",\"Notification delivery and display\":\"Bildirim teslimi ve gösterimi\",\"In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.\":\"Sayfa içi bildirimler RoomGrid üst çubuğundaki boş alanı kullanır ve tarayıcı iznine ihtiyaç duymaz. Konum, süre ve yoğunluk yapılandırılabilir.\",\"Delivery\":\"Teslimat\",\"RoomGrid bar only\":\"Yalnızca RoomGrid çubuğu\",\"Browser notifications only\":\"Yalnızca tarayıcı bildirimleri\",\"RoomGrid bar and browser\":\"RoomGrid çubuğu ve tarayıcı\",\"No display, history only\":\"Görüntü yok, yalnızca geçmiş\",\"In-page position\":\"Sayfa içi konum\",\"Free top-bar area\":\"Ücretsiz üst çubuk alanı\",\"Floating top center\":\"Yüzen üst merkez\",\"Floating top right\":\"Sağ üstte yüzen\",\"Display style\":\"Görüntüleme stili\",\"Compact single line\":\"Kompakt tek hat\",\"Display duration\":\"Gösterim süresi\",\"Until dismissed\":\"Görevden alınana kadar\",\"Visible at once\":\"Aynı anda görünür\",\"Test notification\":\"Test bildirimi\",\"General rules\":\"Genel kurallar\",\"Enable notifications\":\"Bildirimleri etkinleştir\",\"The browser did not grant notification permission.\":\"Tarayıcı bildirim izni vermedi.\",\"Neutral text\":\"Tarafsız metin\",\"Quiet time starts\":\"Sessizlik zamanı başlıyor\",\"Quiet time ends\":\"Sessizlik süresi sona eriyor\",\"Cooldown (minutes)\":\"Bekleme süresi (dakika)\",\"Hourly limit\":\"Saatlik limit\",\"Favorite threshold\":\"Favori eşik\",\"Open on click\":\"Tıklandığında aç\",\"Per model\":\"Model başına\",\"Model\":\"Modeli\",\"Library is empty.\":\"Kütüphane boş.\",\"Per group\":\"Grup başına\",\"Group\":\"Grup\",\"No groups.\":\"Grup yok.\",\"Local notification list\":\"Yerel bildirim listesi\",\"Mark all read\":\"Tümünü okundu olarak işaretle\",\"Clear list\":\"Listeyi temizle\",\"Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.\":\"Arka plan yalnızca okuma durumunu kontrol eder ve video başlatmaz. Bir kullanıcı betiği yalnızca ilgili site sekmesi açıkken çalıştırılabilir; birden fazla sekme otomatik olarak bir denetleyiciyi seçer.\",\"Shield status\":\"Kalkan durumu\",\"Ready\":\"Hazır\",\"Local PIN\":\"Yerel PIN\",\"Set\":\"Ayarla\",\"Not set\":\"Ayarlanmadı\",\"Quick shield\":\"Hızlı kalkan\",\"Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.\":\"İsimleri ve görselleri anında kapsar ve tüm medyayı duraklatır/sessiz hale getirir. Geri yükleme önceki medya durumunu döndürür.\",\"Activate now\":\"Şimdi etkinleştir\",\"Auto-shield after inactivity\":\"Hareketsizlikten sonra otomatik koruma\",\"Set or change local PIN\":\"Yerel PIN'i ayarlayın veya değiştirin\",\"Leave empty to remove\":\"Kaldırmak için boş bırakın\",\"The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.\":\"PIN yalnızca yerel erişim engelidir, şifreleme değildir ve işletim sistemi hesap korumasının yerini almaz.\",\"History controls\":\"Geçmiş kontrolleri\",\"Usage statistics\":\"Kullanım istatistikleri\",\"Pause history 1 hour\":\"Geçmişi 1 saat duraklat\",\"Clear statistics\":\"İstatistikleri temizle\",\"Clear all local statistics? A backup will be created first.\":\"Tüm yerel istatistikler temizlensin mi? İlk önce bir yedek oluşturulacak.\",\"Clear Discover settings\":\"Keşfet ayarlarını temizle\",\"Clear local Discover filters and feeds?\":\"Yerel Keşfet filtreleri ve yayınları temizlensin mi?\",\"Clear notification list\":\"Bildirim listesini temizle\",\"Data boundary\":\"Veri sınırı\",\"Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.\":\"Yapılandırma, izleme istatistikleri, bildirim geçmişi ve Keşfet tercihleri bu tarayıcıda yerel olarak depolanır. RoomGrid bulut senkronizasyonu, hesap yükleme veya telemetri içermez.\",\"Quota\":\"Kota\",\"Used\":\"Kullanılmış\",\"Sensitivity & consent center\":\"Hassasiyet ve rıza merkezi\",\"Standard configuration\":\"Standart konfigürasyon\",\"Theme, layout, filters\":\"Tema, düzen, filtreler\",\"Local\":\"Yerel\",\"Model names, notes, timestamps, statistics\":\"Model adları, notlar, zaman damgaları, istatistikler\",\"Warn on export\":\"İhracatta uyar\",\"Secret\":\"Gizli\",\"Optional official statistics token\":\"İsteğe bağlı resmi istatistik belirteci\",\"Session-only or encrypted vault\":\"Yalnızca oturum veya şifreli kasa\",\"Chat content\":\"Sohbet içeriği\",\"Never persisted\":\"Asla ısrar etmedim\",\"Ephemeral\":\"Geçici\",\"Stream recording/screenshots\":\"Akış kaydı/ekran görüntüleri\",\"Not provided\":\"Sağlanmadı\",\"Policy guard\":\"Politika koruyucusu\",\"Performance profile\":\"Performans profili\",\"Playing streams\":\"Akışları oynatma\",\"Video elements\":\"Video öğeleri\",\"Measured memory\":\"Ölçülen hafıza\",\"Unavailable in browser\":\"Tarayıcıda kullanılamıyor\",\"Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.\":\"Mevcut ölçümler yapılandırılan sınıra yakın. Kullanılmayan kameraları duraklatın veya Eco modunu seçin.\",\"Stream performance\":\"Akış performansı\",\"Eco\":\"Eko\",\"Power\":\"Güç\",\"Concurrent stream limit\":\"Eşzamanlı akış sınırı\",\"Adaptive quality\":\"Uyarlanabilir kalite\",\"Gradual startup\":\"Kademeli başlatma\",\"Automatic performance mode\":\"Otomatik performans modu\",\"Memory threshold\":\"Bellek eşiği\",\"Stream-count threshold\":\"Akış sayımı eşiği\",\"Current auto state\":\"Mevcut otomatik durum\",\"Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.\":\"Kullanılmayan önizlemeler durdurulur. Kapasite kullanılabilir hale gelinceye kadar yeni video sınırda duraklatılmış durumda kalır. Yüksek sayımlar yavaş yavaş başlar; “sınırsız kamera” garantisi yoktur.\",\"Multi-window workspaces\":\"Çok pencereli çalışma alanları\",\"Window name\":\"Pencere adı\",\"Active and recent windows\":\"Etkin ve son pencereler\",\"The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.\":\"Model kitaplığı yerel birleştirme yoluyla senkronize edilir; her pencere kendi çalışma alanını ve görünüm ayarlarını korur. Son pencere ilişkilendirmeleri, tarayıcının izin verdiği ölçüde yeniden başlatmanın ardından geri yüklenebilir.\",\"Optional tools are disabled\":\"İsteğe bağlı araçlar devre dışı bırakıldı\",\"Media, app and contest directories will not scan the current page.\":\"Medya, uygulama ve yarışma dizinleri mevcut sayfayı taramayacaktır.\",\"Enable optional tools\":\"İsteğe bağlı araçları etkinleştir\",\"Local search\":\"Yerel arama\",\"Search saved items and current page\":\"Kaydedilen öğeleri ve mevcut sayfayı arayın\",\"Rescan current page\":\"Geçerli sayfayı yeniden tara\",\"Disable optional tools\":\"İsteğe bağlı araçları devre dışı bırakın\",\"Clear local side data\":\"Yerel yan verileri temizle\",\"Clear app favorites, categories and media notes?\":\"Uygulama favorileri, kategorileri ve medya notları temizlensin mi?\",\"These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.\":\"Bu modüller yalnızca genel bağlantıları ve mevcut sayfada zaten erişilebilen metinleri düzenler. Medyayı kopyalamazlar, otomatik olarak indirmezler veya sohbet, jeton veya ödeme işlevlerini çalıştırmazlar. Giriş, yaş doğrulama, güvenlik ve ödeme formlarına dokunulmaz.\",\"Photo and video areas\":\"Fotoğraf ve video alanları\",\"No recognizable public media area is present on this page. Rescan while on a native photo or video page.\":\"Bu sayfada tanınabilir hiçbir kamuya açık medya alanı mevcut değil. Yerel bir fotoğraf veya video sayfasındayken yeniden tarayın.\",\"Apps directory\":\"Uygulama dizini\",\"Local category\":\"Yerel kategori\",\"Found on page\":\"Sayfada bulundu\",\"No recognizable app links are present on this page.\":\"Bu sayfada tanınabilir uygulama bağlantısı yok.\",\"Public contest results\":\"Herkese açık yarışma sonuçları\",\"No readable public contest table is present on this page.\":\"Bu sayfada okunabilir bir halka açık yarışma tablosu bulunmamaktadır.\",\"Native account, help and broadcaster links\":\"Yerel hesap, yardım ve yayıncı bağlantıları\",\"No safe native links were found on this page; payment and login paths are deliberately filtered.\":\"Bu sayfada güvenli yerel bağlantı bulunamadı; ödeme ve giriş yolları bilinçli olarak filtrelenir.\",\"Homepage filters reset\":\"Ana sayfa filtreleri sıfırlandı\",\"Global appearance reset\":\"Genel görünüm sıfırlama\",\"Version\":\"Sürüm\",\"Health score\":\"Sağlık puanı\",\"Page type\":\"Sayfa türü\",\"Route generation\":\"Rota oluşturma\",\"Running modules\":\"Çalışan modüller\",\"Local errors\":\"Yerel hatalar\",\"Repair center\":\"Onarım merkezi\",\"A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.\":\"Her onarımdan önce bir konfigürasyon yedeği oluşturulur. Bu işlemlerle istatistikler ve model verileri silinmez.\",\"Reset home filters\":\"Ana filtreleri sıfırla\",\"Reset all home-page filters?\":\"Tüm ana sayfa filtreleri sıfırlansın mı?\",\"Reset global appearance\":\"Genel görünümü sıfırla\",\"Reset the global appearance? Saved design profiles remain.\":\"Genel görünüm sıfırlansın mı? Kaydedilen tasarım profilleri kalır.\",\"Normalize configuration\":\"Yapılandırmayı normalleştir\",\"Validate and rewrite the current configuration? A backup is created first.\":\"Geçerli yapılandırma doğrulanıp yeniden yazılsın mı? Öncelikle bir yedek oluşturulur.\",\"Close all previews\":\"Tüm önizlemeleri kapat\",\"Compatibility self-check\":\"Uyumluluk kendi kendine kontrolü\",\"Module and page status\":\"Modül ve sayfa durumu\",\"Page route test matrix\":\"Sayfa rotası test matrisi\",\"Current route\":\"Mevcut rota\",\"Trigger\":\"Tetikleyici\",\"Recent page transitions\":\"Son sayfa geçişleri\",\"Capability layer\":\"Yetenek katmanı\",\"Available\":\"Mevcut\",\"Fallback\":\"Geri dönüş\",\"When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.\":\"Bir API eksik olduğunda yalnızca etkilenen modül geri çekilir. Tekrarlanan ağ arızaları devre kesiciyi geçici olarak açar.\",\"Backup and recovery status\":\"Yedekleme ve kurtarma durumu\",\"Primary data\":\"Birincil veriler\",\"Stable archive\":\"Kararlı arşiv\",\"Rolling backups\":\"Yedeklemeler devam ediyor\",\"The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.\":\"Otomatik kontrol yalnızca okunabilirliği ve tarayıcı yeteneklerini doğrular; hiçbir zaman mevcut verilerin üzerine otomatik olarak yazmaz. Kurtarma, mevcut onaylanmış yedekleme akışını kullanmaya devam eder.\",\"Sanitized local error log\":\"Temizlenmiş yerel hata günlüğü\",\"No RoomGrid errors recorded.\":\"RoomGrid hatası kaydedilmedi.\",\"Run self-check again\":\"Otomatik kontrolü tekrar çalıştırın\",\"Export diagnostics\":\"Teşhisleri dışa aktar\",\"Clear error log\":\"Hata günlüğünü temizle\",\"Return to normal site\":\"Normal siteye dön\",\"The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.\":\"Tanılama dışa aktarımı hiçbir çalışma alanı, model adı, not veya istatistik içermez. URL sorgu parametreleri ve olası gizli değerler, hata metninden kaldırıldı.\",\"Local history on\":\"Yerel tarih açık\",\"History off\":\"Geçmiş kapalı\",\"This settings page could not be loaded\":\"Bu ayarlar sayfası yüklenemedi\",\"Your data was not changed. Try again; if the problem remains, open Diagnostics.\":\"Verileriniz değiştirilmedi. Tekrar deneyin; sorun devam ederse Tanılama'yı açın.\",\"Try again\":\"Tekrar dene\",\"Open Diagnostics\":\"Teşhisi Aç\",\"Public results\":\"Herkese açık sonuçlar\",\"Public contest links\":\"Herkese açık yarışma bağlantıları\",\"Page content has not appeared yet\":\"Sayfa içeriği henüz görünmedi\",\"RoomGrid checked the page again but still cannot find Chaturbate's content area. The page may still be loading or the site returned an empty view.\":\"RoomGrid sayfayı tekrar kontrol etti ancak Chaturbate'in içerik alanını hâlâ bulamıyor. Sayfa hâlâ yükleniyor olabilir veya site boş bir görünüm döndürmüş olabilir.\",\"Check again\":\"Tekrar kontrol edin\",\"Open RoomGrid Discover\":\"RoomGrid Discover'ı açın\",\"Reload page\":\"Sayfayı yeniden yükle\",\"Hide notice\":\"Bildirimi gizle\",\"Open MultiCam\":\"MultiCam'i açın\",\"Open workstation in a new window\":\"İş istasyonunu yeni bir pencerede aç\",\"Open Discover\":\"Keşfet'i aç\",\"Statistics center\":\"İstatistik merkezi\",\"Backup and import\":\"Yedekleme ve içe aktarma\",\"Page and design options\":\"Sayfa ve tasarım seçenekleri\",\"Hide immediately\":\"Hemen gizle\",\"All tools\":\"Tüm araçlar\",\"Search & commands\":\"Arama ve komutlar\",\"Page design\":\"Sayfa tasarımı\",\"Official collection\":\"Resmi koleksiyon\",\"Saved\":\"Kaydedildi\",\"Quick info\":\"Hızlı bilgi\",\"Loading live frame\":\"Canlı çerçeve yükleniyor\",\"Live still\":\"Hareketsiz yaşa\",\"Not live\":\"Canlı değil\",\"Pages\":\"Sayfalar\",\"Refresh\":\"Yenile\",\"Search…\":\"Ara…\",\"Tag Explorer\":\"Etiket Gezgini\",\"Refresh public catalog\":\"Genel kataloğu yenile\",\"Online\":\"Çevrimiçi\",\"Toggle favorite\":\"Favoriyi değiştir\",\"Save\":\"Kaydet\",\"Remove favorite\":\"Favoriyi kaldır\",\"Removed from library\":\"Kitaplıktan kaldırıldı\",\"Saved to library\":\"Kitaplığa kaydedildi\",\"Added to favorites\":\"Favorilere eklendi\",\"Removed from favorites\":\"Favorilerden kaldırıldı\",\"Watching\":\"izliyorum\",\"Watch\":\"İzle\",\"Temporary watchlist\":\"Geçici izleme listesi\",\"Removed from watchlist\":\"İzleme listesinden kaldırıldı\",\"Added to watchlist\":\"İzleme listesine eklendi\",\"Choose group\":\"Grup seç\",\"Add to group…\":\"Gruba ekle…\",\"Group updated\":\"Grup güncellendi\",\"Site room\":\"Şantiye odası\",\"Select for bulk actions\":\"Toplu işlemler için seçin\",\"viewers\":\"izleyiciler\",\"No public description\":\"Herkese açık açıklama yok\",\"Open in Tag Explorer\":\"Etiket Gezgini'nde aç\",\"Feed name\":\"Özet akışı adı\",\"View name\":\"Adı görüntüle\",\"Name already exists\":\"Ad zaten mevcut\",\"Saved-item limit reached\":\"Kaydedilen öğe sınırına ulaşıldı\",\"Feed saved\":\"Özet akışı kaydedildi\",\"View saved\":\"Kaydedilenleri görüntüle\",\"Applied\":\"Uygulandı\",\"Backup exported\":\"Yedekleme dışa aktarıldı\",\"Backup imported\":\"Yedekleme içe aktarıldı\",\"Invalid backup\":\"Geçersiz yedekleme\",\"New models\":\"Yeni modeller\",\"New since last visit\":\"Son ziyaretten bu yana yeni\",\"New to me\":\"Benim için yeni\",\"Returning\":\"Geri dönüyor\",\"Stable online\":\"Çevrimiçi olarak istikrarlı\",\"Saved only\":\"Yalnızca kaydedildi\",\"Previously watched\":\"Daha önce izlendi\",\"Local collection\":\"Yerel koleksiyon\",\"Hide visited today\":\"Bugün ziyaret edileni gizle\",\"No feed selected\":\"Besleme seçilmedi\",\"No view selected\":\"Görünüm seçilmedi\",\"Saved feeds\":\"Kayıtlı yayınlar\",\"Saved views\":\"Kaydedilen görünümler\",\"Save new\":\"Yeniyi kaydet\",\"All public rooms\":\"Tüm umumi odalar\",\"Known HD\":\"Bilinen HD\",\"Random result\":\"Rastgele sonuç\",\"Viewers ↓\":\"İzleyiciler ↓\",\"Viewers ↑\":\"İzleyiciler ↑\",\"Online duration ↓\":\"Çevrimiçi süre ↓\",\"Followers ↓\":\"Takipçiler ↓\",\"Name A–Z\":\"A'dan Z'ye Ad\",\"Name Z–A\":\"İsim Z–A\",\"New first\":\"Yeni ilk\",\"Watch time ↓\":\"İzlenme süresi ↓\",\"Sessions ↓\":\"Oturumlar ↓\",\"Last watched\":\"Son izlenme\",\"Name, description, tag, place\":\"Ad, açıklama, etiket, yer\",\"Secondary sort\":\"İkincil sıralama\",\"Shuffle again\":\"Tekrar karıştır\",\"All groups\":\"Tüm gruplar\",\"All tags (AND)\":\"Tüm etiketler (VE)\",\"Any tag (OR)\":\"Herhangi bir etiket (VEYA)\",\"Excluded tags (NOT)\":\"Hariç tutulan etiketler (DEĞİL)\",\"Language, region, or country\":\"Dil, bölge veya ülke\",\"Minimum online minutes\":\"Minimum çevrimiçi dakika\",\"Maximum online minutes\":\"Maksimum çevrimiçi dakika\",\"Minimum local watch minutes\":\"Minimum yerel izleme dakikası\",\"Advanced filters and Boolean tags\":\"Gelişmiş filtreler ve Boole etiketleri\",\"Saved views and feeds\":\"Kaydedilen görünümler ve yayınlar\",\"Import\":\"İthalat\",\"Recent searches\":\"Son aramalar\",\"Live previews and performance\":\"Canlı önizlemeler ve performans\",\"Muted live preview for every visible card\":\"Görünür her kart için sessiz canlı önizleme\",\"Refresh rate\":\"Yenileme hızı\",\"Real-time video while hovering\":\"Gezinirken gerçek zamanlı video\",\"Hover delay\":\"Fareyle üzerine gelme gecikmesi\",\"Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.\":\"Görünür kartlar JPEG çerçeveleri kullanır. Fareyle üzerine gelindiğinde yalnızca bir kart sessiz gerçek zamanlı videoya geçer ve imleç oradan ayrıldığında tamamen durur. Yüksek kare hızları bant genişliğini ve CPU kullanımını önemli ölçüde artırır.\",\"saved\":\"kaydedildi\",\"previews\":\"önizlemeler\",\"Previews off\":\"Önizlemeler kapalı\",\"No more feeds can be saved\":\"Daha fazla yayın kaydedilemez\",\"Tag feed saved\":\"Etiket feed'i kaydedildi\",\"Select tag\":\"Etiket seçin\",\"Tag favorite\":\"Favoriyi etiketle\",\"Block tag\":\"Etiketi engelle\",\"Select tags first\":\"Önce etiketleri seçin\",\"Tag-group name\":\"Etiket grubu adı\",\"Tag group saved\":\"Etiket grubu kaydedildi\",\"No gaming rooms in the current catalog\":\"Mevcut katalogda oyun odası yok\",\"All (AND)\":\"Hepsi (VE)\",\"Any (OR)\":\"Herhangi biri (VEYA)\",\"Exclude (NOT)\":\"Hariç tut (DEĞİL)\",\"Search tags\":\"Etiketlerde ara\",\"Tag search\":\"Etiket arama\",\"Combination\":\"Kombinasyon\",\"Model sorting\":\"Model sıralama\",\"Show in Discover\":\"Keşfet'te Göster\",\"Save tag group\":\"Etiket grubunu kaydet\",\"Save tag feed\":\"Etiket feed'ini kaydet\",\"No tag groups saved yet.\":\"Henüz kayıtlı etiket grubu yok.\",\"Unknown region\":\"Bilinmeyen bölge\",\"Unknown age\":\"Bilinmeyen yaş\",\"Public “New”\":\"Kamuya Açık “Yeni”\",\"My new cams\":\"Yeni kameralarım\",\"New-cams feed saved\":\"Yeni kameraların feed'i kaydedildi\",\"Save “My new cams” feed\":\"\\\"Yeni kameralarım\\\" özet akışını kaydet\",\"Create gaming workspace\":\"Oyun çalışma alanı oluşturun\",\"No matches for this combination.\":\"Bu kombinasyon için eşleşme yok.\",\"Select tags to preview matching rooms here.\":\"Eşleşen odaların önizlemesini görmek için etiketleri burada seçin.\",\"Search, favorite, block, and combine public tags. Trends use only catalog changes observed on this device.\":\"Genel etiketleri arayın, favorilere ekleyin, engelleyin ve birleştirin. Trendler yalnızca bu cihazda gözlemlenen katalog değişikliklerini kullanır.\",\"Current tags\":\"Mevcut etiketler\",\"Counts show current matches and change since the prior fetch.\":\"Sayımlar mevcut eşleşmeleri gösterir ve önceki alımdan bu yana değişir.\",\"Results for selected tags\":\"Seçilen etiketler için sonuçlar\",\"Uses the same sorting and SingleCam navigation as Discover.\":\"Discover ile aynı sıralamayı ve SingleCam navigasyonunu kullanır.\",\"Categories, age, and regions\":\"Kategoriler, yaş ve bölgeler\",\"Only explicit public fields are shown. Comma-separated regions combine with OR; unknown values remain visible. No timezone is guessed.\":\"Yalnızca açık ortak alanlar gösterilir. Virgülle ayrılmış bölgeler OR ile birleştirilir; bilinmeyen değerler görünür kalır. Herhangi bir saat dilimi tahmin edilmiyor.\",\"New and gaming\":\"Yeni ve oyun\",\"Local first-seen, the public “New” flag, temporary watchlist, and gaming themes remain separate.\":\"İlk kez yerelde görülen, herkese açık \\\"Yeni\\\" bayrağı, geçici izleme listesi ve oyun temaları ayrı kalır.\",\"Tag groups\":\"Etiket grupları\",\"Save AND, OR, or NOT combinations.\":\"VE, VEYA veya DEĞİL kombinasyonlarını kaydedin.\",\"Favorite tags\":\"Favori etiketler\",\"Your personal shortcut list.\":\"Kişisel kısayol listeniz.\",\"No favorites yet\":\"Henüz favori yok\",\"Related tags\":\"İlgili etiketler\",\"Tags co-occurring on the currently matching models.\":\"Şu anda eşleşen modellerde birlikte bulunan etiketler.\",\"Personal tag history\":\"Kişisel etiket geçmişi\",\"Recently and frequently used on this device.\":\"Bu cihazda son zamanlarda ve sıklıkla kullanılıyor.\",\"Bulk target group\":\"Toplu hedef grup\",\"Selected models added to group\":\"Seçilen modeller gruba eklendi\",\"New workspace name\":\"Yeni çalışma alanı adı\",\"Created from a Discover selection\":\"Keşfet seçiminden oluşturuldu\",\"Workspace created\":\"Çalışma alanı oluşturuldu\",\"Save all\":\"Tümünü kaydet\",\"Create workspace\":\"Çalışma alanı oluştur\",\"Loading public catalog…\":\"Genel katalog yükleniyor…\",\"Loading the complete catalog in the background…\":\"Kataloğun tamamı arka planda yükleniyor…\",\"Updated\":\"Güncellendi\",\"Public rooms\":\"Umumi odalar\",\"The catalog is loading.\":\"Katalog yükleniyor.\",\"No matching rooms. Adjust search or filters.\":\"Eşleşen oda yok. Aramayı veya filtreleri ayarlayın.\",\"Previous\":\"Önceki\",\"Next\":\"Sonraki\",\"Move up\":\"Yukarı taşı\",\"Move down\":\"Aşağı taşı\",\"Number shown\":\"Gösterilen sayı\",\"Widget width\":\"Widget genişliği\",\"Full\":\"Tam\",\"Nothing here yet.\":\"Burada henüz bir şey yok.\",\"Newly online\":\"Yeni çevrimiçi\",\"Recently visited\":\"Son ziyaret edilenler\",\"Frequently visited\":\"Sık ziyaret edilenler\",\"Homepage appearance, pagination, and performance\":\"Ana sayfa görünümü, sayfalandırma ve performans\",\"Information density\":\"Bilgi yoğunluğu\",\"Layout\":\"Düzen\",\"Data rich\":\"Veri zengini\",\"Static\":\"Statik\",\"Show statistics summary\":\"İstatistik özetini göster\",\"Show personal widgets\":\"Kişisel widget'ları göster\",\"Sections below the live catalog\":\"Canlı kataloğun altındaki bölümler\",\"Homepage live models\":\"Ana sayfa canlı modeller\",\"All live rooms in one place\":\"Tüm canlı odalar tek bir yerde\",\"Use the same complete search, sorting, groups, tags, and local-history filters as Discover.\":\"Keşfet ile aynı eksiksiz arama, sıralama, grup, etiket ve yerel geçmiş filtrelerini kullanın.\",\"Refresh live catalog\":\"Canlı kataloğu yenile\",\"Homepage settings\":\"Ana sayfa ayarları\",\"Matches a favorite gaming theme\":\"Favori bir oyun temasıyla eşleşir\",\"Saved models\":\"Kaydedilen modeller\",\"Online favorites\":\"Çevrimiçi favoriler\",\"Local watch time\":\"Yerel izlenme süresi\",\"Continue last MultiCam workspace\":\"Son MultiCam çalışma alanına devam et\",\"Open full Discover page\":\"Keşfet sayfasının tamamını aç\",\"Online notifications\":\"Çevrimiçi bildirimler\",\"on\":\"üzerinde\",\"off\":\"kapalı\",\"Offline notifications\":\"Çevrimdışı bildirimler\",\"Feed\":\"Besleme\",\"Recent\":\"Son\",\"Frequent\":\"sık\",\"Saved filter views\":\"Kaydedilen filtre görünümleri\",\"Favorites currently public and online.\":\"Favoriler şu anda herkese açık ve çevrimiçi.\",\"New models or public streams that started recently.\":\"Yakın zamanda başlayan yeni modeller veya herkese açık yayınlar.\",\"Current results from the selected feed.\":\"Seçilen feed'in güncel sonuçları.\",\"Suggestions with reasons\":\"Nedenleriyle birlikte öneriler\",\"Based only on shared public tags and locally favored themes; no identity inference.\":\"Yalnızca paylaşılan genel etiketlere ve yerel olarak tercih edilen temalara dayanmaktadır; kimlik çıkarımı yok.\",\"Sorted by local last-visit time.\":\"Yerel son ziyaret saatine göre sıralanmıştır.\",\"Sorted by local watch time.\":\"Yerel izlenme süresine göre sıralanmıştır.\",\"Saved models never visited\":\"Kaydedilen modeller hiç ziyaret edilmedi\",\"Saved in the library without local watch time.\":\"Yerel izlenme süresi olmadan kütüphaneye kaydedildi.\",\"Draggable dashboard widgets\":\"Sürüklenebilir kontrol paneli widget'ları\",\"Your RoomGrid homepage\":\"RoomGrid ana sayfanız\",\"A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.\":\"SingleCam, MultiCam, eksiksiz filtreler, kayıtlı görünümler ve kişisel widget'larınızı içeren hızlı, özelleştirilebilir bir canlı katalog.\",\"Audio activity\":\"Ses etkinliği\",\"Auto quality\":\"Otomatik kalite\",\"Stream interrupted; status checks continue.\":\"Akış kesildi; durum kontrolleri devam ediyor.\",\"This browser cannot play HLS.\":\"Bu tarayıcı HLS'yi oynatamıyor.\",\"Connecting to live stream…\":\"Canlı yayına bağlanılıyor…\",\"The room is not public right now.\":\"Oda şu anda halka açık değil.\",\"Currently offline. Alternatives from the same filter are shown below.\":\"Şu anda çevrimdışı. Aynı filtrenin alternatifleri aşağıda gösterilmiştir.\",\"Status temporarily unavailable; retrying automatically.\":\"Durum geçici olarak kullanılamıyor; otomatik olarak yeniden deneniyor.\",\"Loading public profile…\":\"Herkese açık profil yükleniyor…\",\"Profile temporarily unavailable.\":\"Profil geçici olarak kullanılamıyor.\",\"No cached profile.\":\"Önbelleğe alınmış profil yok.\",\"Public profile changed\":\"Herkese açık profil değiştirildi\",\"No public bio.\":\"Herkese açık biyografi yok.\",\"Source\":\"Kaynak\",\"Observed\":\"Gözlemlendi\",\"Only links self-published on the public profile are shown. A matching username alone is not proof of identity.\":\"Yalnızca genel profilde kişisel olarak yayınlanan bağlantılar gösterilir. Eşleşen bir kullanıcı adı tek başına kimliğin kanıtı değildir.\",\"Profile information\":\"Profil bilgileri\",\"Preparing…\":\"Hazırlanıyor…\",\"Play\":\"Oyna\",\"Unmute\":\"Sesi aç\",\"Mute\":\"Sessiz\",\"Volume\":\"Hacim\",\"Contain\":\"içerir\",\"Cover\":\"Kapak\",\"Brightness\":\"Parlaklık\",\"Contrast\":\"Kontrast\",\"Saturation\":\"Doygunluk\",\"Hue\":\"renk tonu\",\"Sepia\":\"Sepya\",\"Grayscale\":\"Gri tonlamalı\",\"Invert\":\"Ters çevir\",\"Blur\":\"Bulanıklaştırma\",\"Image zoom\":\"Görüntü yakınlaştırma\",\"Recording saved\":\"Kayıt kaydedildi\",\"Screenshot saved\":\"Ekran görüntüsü kaydedildi\",\"Recording\":\"Kayıt\",\"Mirror\":\"Ayna\",\"Flip vertically\":\"Dikey olarak çevir\",\"Rotate\":\"Döndür\",\"Show edited\":\"Düzenlenenleri göster\",\"Show original\":\"Orijinali göster\",\"Reset image\":\"Resmi sıfırla\",\"Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.\":\"Ctrl tuşunu basılı tutun ve yakınlaştırmak için kaydırın; görüntüyü yakınlaştırdığınızda sürükleyin. Sıfırlamak için çift tıklayın.\",\"Zoom out\":\"Uzaklaştır\",\"Zoom in\":\"Yakınlaştır\",\"Reset zoom and position\":\"Yakınlaştırmayı ve konumu sıfırla\",\"Player window\":\"Oyuncu penceresi\",\"Fullscreen\":\"Tam ekran\",\"Private notes, memories, or things to check next time (stored locally)\":\"Özel notlar, anılar veya bir dahaki sefere kontrol edilecek şeyler (yerel olarak depolanır)\",\"Autosave is on\":\"Otomatik kaydetme açık\",\"Enter your own tag…\":\"Kendi etiketinizi girin…\",\"Saved at\":\"Şu tarihte kaydedildi:\",\"Local details saved\":\"Yerel ayrıntılar kaydedildi\",\"Remove tag\":\"Etiketi kaldır\",\"No personal tags yet.\":\"Henüz kişisel etiket yok.\",\"Saving…\":\"Kaydediliyor…\",\"Create your first group under Groups.\":\"Gruplar altında ilk grubunuzu oluşturun.\",\"Added to MultiCam\":\"MultiCam'e eklendi\",\"Edit private details\":\"Özel ayrıntıları düzenle\",\"Region\":\"Bölge\",\"Current online time\":\"Şu anki çevrimiçi zaman\",\"Stream attributes\":\"Akış özellikleri\",\"New\":\"Yeni\",\"Yes\":\"Evet\",\"No / unknown\":\"Hayır / bilinmiyor\",\"Last observed\":\"Son gözlemlenen\",\"Local visits\":\"Yerel ziyaretler\",\"First watched\":\"İlk izlendi\",\"No public description.\":\"Herkese açık bir açıklama yok.\",\"No public tags available.\":\"Herkese açık etiket yok.\",\"Quick-add your own tag…\":\"Kendi etiketinizi hızla ekleyin…\",\"Public tags\":\"Herkese açık etiketler\",\"Add\":\"Ekle\",\"Private details\":\"Özel ayrıntılar\",\"Everything stays in this browser and survives updates.\":\"Her şey bu tarayıcıda kalır ve güncellemelerden etkilenmez.\",\"Private notebook\":\"Özel not defteri\",\"Keep on watchlist\":\"İzleme listesine devam et\",\"Group assignment (multiple)\":\"Grup ödevi (çoklu)\",\"Save now\":\"Şimdi kaydet\",\"Chat loads only when this section is opened.\":\"Sohbet yalnızca bu bölüm açıldığında yüklenir.\",\"Official chat\":\"Resmi sohbet\",\"Official chat connected.\":\"Resmi sohbet bağlandı.\",\"Chat loaded; the current page structure could not be fully isolated.\":\"Sohbet yüklendi; mevcut sayfa yapısı tamamen izole edilemedi.\",\"Chat loaded.\":\"Sohbet yüklendi.\",\"Loading official chat…\":\"Resmi sohbet yükleniyor…\",\"Official chat room\":\"Resmi sohbet odası\",\"Loaded only when needed; RoomGrid never stores or automatically sends chat messages.\":\"Yalnızca ihtiyaç duyulduğunda yüklenir; RoomGrid hiçbir zaman sohbet mesajlarını saklamaz veya otomatik olarak göndermez.\",\"Reload chat\":\"Sohbeti yeniden yükle\",\"Open official room in a new window\":\"Resmi odayı yeni bir pencerede aç\",\"Hide chat\":\"Sohbeti gizle\",\"Live observations\":\"Canlı gözlemler\",\"Trend confidence\":\"Trend güveni\",\"Known time slots\":\"Bilinen zaman aralıkları\",\"Typical peak\":\"Tipik zirve\",\"Watched in 7 days\":\"7 gün içinde izlendi\",\"Watched in 30 days\":\"30 gün içinde izlendi\",\"Current streak\":\"Mevcut seri\",\"Typical weekly online times\":\"Tipik haftalık çevrimiçi zamanlar\",\"Online times will be learned on this device from now on.\":\"Artık çevrimiçi saatler bu cihazda öğrenilecek.\",\"Based on observations on this device; missing information is never guessed.\":\"Bu cihazda yapılan gözlemlere göre; Eksik bilgiler asla tahmin edilemez.\",\"Activity by weekday\":\"Hafta içi etkinlik\",\"Most likely online times\":\"Büyük olasılıkla çevrimiçi zamanlar\",\"Online-time map\":\"Çevrimiçi zaman haritası\",\"Public covers and locally saved example images.\":\"Herkese açık kapaklar ve yerel olarak kaydedilmiş örnek görseller.\",\"Import quality\":\"İthalat kalitesi\",\"Original file\":\"Orijinal dosya\",\"Very high (up to 4K)\":\"Çok yüksek (4K'ya kadar)\",\"High (up to 1440p)\":\"Yüksek (1440p'ye kadar)\",\"Space-saving (up to 900p)\":\"Yer tasarrufu sağlar (900p'ye kadar)\",\"Add images from computer\":\"Bilgisayardan resim ekleme\",\"Use website cover\":\"Web sitesi kapağını kullan\",\"Use first image as cover\":\"İlk resmi kapak olarak kullan\",\"Covers and local gallery\":\"Kapaklar ve yerel galeri\",\"Updating images…\":\"Resimler güncelleniyor…\",\"Loading images…\":\"Resimler yükleniyor…\",\"Own image\":\"Kendi görseli\",\"Example image\":\"Örnek resim\",\"Current cover\":\"Güncel kapak\",\"Alternative preview\":\"Alternatif önizleme\",\"Current public image\":\"Mevcut genel resim\",\"Saving cover…\":\"Kapak kaydediliyor…\",\"Set as cover\":\"Kapak olarak ayarla\",\"Delete this image from the local gallery?\":\"Bu resim yerel galeriden silinsin mi?\",\"No images yet.\":\"Henüz resim yok.\",\"saved locally\":\"yerel olarak kaydedildi\",\"own images\":\"kendi görselleri\",\"Update public previews\":\"Genel önizlemeleri güncelle\",\"Processing your images…\":\"Resimleriniz işleniyor…\",\"ALL PAGE SOUNDS OFF\":\"TÜM SAYFA SESLERİ KAPALI\",\"PAGE SOUNDS ALLOWED\":\"SAYFA SESLERİNE İZİN VERİLİR\",\"Chat/token sounds OFF\":\"Sohbet/jeton sesleri KAPALI\",\"Chat/token sounds ON\":\"Sohbet/jeton sesleri AÇIK\",\"Reconnecting the player with the new settings…\":\"Oynatıcı yeni ayarlarla yeniden bağlanıyor…\",\"Turn off chat, token and tip sounds completely\":\"Sohbet, jeton ve bahşiş seslerini tamamen kapatın\",\"Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.\":\"Ses öğelerinden ve Web Sesinden gelen sayfa seslerini engeller. Akış sesi, \\\"Ses açık\\\" aracılığıyla ayrı olarak kontrol edilebilir durumda kalır.\",\"No alternatives available.\":\"Alternatif yok.\",\"Similar models and offline alternatives\":\"Benzer modeller ve çevrimdışı alternatifler\",\"Prioritized by shared tags; no identity or external-account inference.\":\"Paylaşılan etiketlere göre önceliklendirilir; kimlik veya harici hesap çıkarımı yok.\",\"Chat\":\"Sohbet\",\"Notes & organization\":\"Notlar ve organizasyon\",\"Images\":\"Görseller\",\"SingleCam sections\":\"Tek Kamera bölümleri\",\"Back to original results\":\"Orijinal sonuçlara dön\",\"Private workspace\":\"Özel çalışma alanı\",\"Chat on/off\":\"Sohbeti aç/kapat\",\"Hide similar models\":\"Benzer modelleri gizle\",\"Show similar models\":\"Benzer modelleri göster\",\"RoomGrid Home\":\"RoomGrid Ana Sayfası\",\"RoomGrid Tags\":\"RoomGrid Etiket\",\"Public catalog unavailable; showing the local library.\":\"Genel katalog mevcut değil; yerel kütüphaneyi gösteriyor.\",\"Drag to resize sidebar\":\"Kenar çubuğunu yeniden boyutlandırmak için sürükleyin\",\"The workspace could not be saved safely. Export a backup now.\":\"Çalışma alanı güvenli bir şekilde kaydedilemedi. Şimdi bir yedeği dışa aktarın.\",\"Open menu or restore controls\":\"Menüyü aç veya kontrolleri geri yükle\",\"Controls restored\":\"Kontroller geri yüklendi\",\"Menu\":\"Menü\",\"Show toolbar\":\"Araç çubuğunu göster\",\"Tools\":\"Araçlar\",\"Show sidebar\":\"Kenar çubuğunu göster\",\"Restore controls\":\"Kontrolleri geri yükle\",\"Show toolbar, groups, and menus\":\"Araç çubuğunu, grupları ve menüleri göster\",\"Open more tools\":\"Daha fazla araç aç\",\"all watch states\":\"tüm izleme durumları\",\"Watch status\":\"İzleme durumu\",\"0 means no viewer limit.\":\"0, izleyici sınırının olmadığı anlamına gelir.\",\"Import temporary URLs; they disappear after refresh\":\"Geçici URL'leri içe aktarın; yenilemeden sonra kayboluyorlar\",\"URL\":\"URL'si\",\"Filter current group\":\"Geçerli grubu filtrele\",\"All status\":\"Tüm durum\",\"Hide offline/private\":\"Çevrimdışı/özel'i gizle\",\"Collapse toolbar\":\"Araç çubuğunu daralt\",\"Windows per page\":\"Sayfa başına Windows\",\"Custom grid\":\"Özel ızgara\",\"Choose MultiCam audio routing\":\"MultiCam ses yönlendirmesini seçin\",\"Audio: manual\":\"Ses: manuel\",\"Audio: solo cam\":\"Ses: solo kamera\",\"Audio: follow focus\":\"Ses: odağı takip et\",\"Auto\":\"Otomatik\",\"Userscript version\":\"Kullanıcı betiği sürümü\",\"Open Discover and dashboard\":\"Keşfet'i ve kontrol panelini açın\",\"Global search and quick commands · Ctrl+K\":\"Genel arama ve hızlı komutlar · Ctrl+K\",\"Stream health, quality budget, audio, scenes, and replacements\":\"Akış sağlığı, kalite bütçesi, ses, sahneler ve değiştirmeler\",\"Control\":\"Kontrol\",\"Rooms\":\"Odalar\",\"Filter\":\"Filtre\",\"Playback\":\"Oynatma\",\"Stabilizing live preview…\":\"Canlı önizleme dengeleniyor…\",\"Recovering live preview…\":\"Canlı önizleme kurtarılıyor…\",\"Live preview is briefly paused to prevent a reload loop. Try again shortly.\":\"Yeniden yükleme döngüsünü önlemek için canlı önizleme kısa süreliğine duraklatılır. Kısa süre sonra tekrar deneyin.\",\"Collapse groups\":\"Grupları daralt\",\"Expand/collapse users\":\"Kullanıcıları genişlet/daralt\",\"Output volume; no live signal level is available for this stream\":\"Çıkış hacmi; bu yayın için canlı sinyal seviyesi mevcut değil\",\"OFF\":\"KAPALI\",\"Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.\":\"URL'leri yapıştırın. Oda sayfaları, m3u8, mp4/webm desteklenmektedir. Normal sayfalar en iyi şekilde ayrıştırılacaktır.\",\"Actual resolution, connection and buffer\":\"Gerçek çözünürlük, bağlantı ve arabellek\",\"Drag to reorder / move group\":\"Grubu yeniden sıralamak / taşımak için sürükleyin\",\"All time\":\"Tüm zamanlar\",\"Click a model for details.\":\"Ayrıntılar için bir modele tıklayın.\",\"Most watched groups\":\"En çok izlenen gruplar\",\"Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.\":\"Katılma tarihi gibi hesap alanları yalnızca Chaturbate bunları aynı sayfada açıkça gösterdiğinde gösterilir. Betik değerleri icat etmez.\",\"Back\":\"Geri\",\"Statistics import failed: \":\"İstatistikleri içe aktarma başarısız oldu:\",\"Recent saves\":\"Son kaydedilenler\",\"Rooms per page\":\"Sayfa başına oda sayısı\",\"Custom rows\":\"Özel satırlar\",\"Custom columns\":\"Özel sütunlar\",\"Main width\":\"Ana genişlik\",\"Main height\":\"Ana yükseklik\",\"Modules\":\"Modüller\",\"Modules start only on matching page types. Saving reloads the current page.\":\"Modüller yalnızca eşleşen sayfa türlerinde başlar. Kaydetme geçerli sayfayı yeniden yükler.\",\"Tools on normal pages\":\"Normal sayfalardaki araçlar\",\"True audio-level meters\":\"Gerçek ses seviyesi ölçerler\",\"MultiCam control center\":\"MultiCam kontrol merkezi\",\"No active streams yet\":\"Henüz aktif akış yok\",\"Stream health\":\"Akış durumu\",\"Budget off\":\"Bütçe kapalı\",\"Economy · 2160p total\":\"Ekonomi · 2160p toplam\",\"Balanced · 4320p total\":\"Dengeli · 4320p toplam\",\"High · 6480p total\":\"Yüksek · 6480p toplam\",\"Ultra · 8640p total\":\"Ultra · 8640p toplam\",\"Quality budget is off; global and per-camera limits still apply.\":\"Kalite bütçesi kapalı; genel ve kamera başına sınırlamalar hâlâ geçerlidir.\",\"Quality budget\":\"Kalite bütçesi\",\"Distributes quality across active cams. An explicit per-camera quality always wins.\":\"Kaliteyi aktif kameralar arasında dağıtır. Kamera başına net bir kalite her zaman kazanır.\",\"Give the focused cam double weight\":\"Odaklanmış kameraya iki kat ağırlık verin\",\"Manual per camera\":\"Kamera başına manuel\",\"Solo: one camera only\":\"Solo: yalnızca bir kamera\",\"Follow focused camera\":\"Odaklanmış kamerayı takip edin\",\"Audio management\":\"Ses yönetimi\",\"Routing never overwrites saved mute states or individual volume.\":\"Yönlendirme hiçbir zaman kayıtlı sessiz durumlarının veya bireysel ses düzeyinin üzerine yazmaz.\",\"Mute all\":\"Tümünü sessize al\",\"Restore saved audio\":\"Kaydedilen sesi geri yükle\",\"Scene name\":\"Sahne adı\",\"Focus\":\"Odaklanma\",\"Grid\":\"Izgara\",\"cams\":\"kameralar\",\"Load\":\"Yük\",\"No scenes saved for this workspace yet.\":\"Bu çalışma alanı için henüz kaydedilmemiş sahneler.\",\"Scenes\":\"Sahneler\",\"Save the current slots, layout, and audio routing for one-click restoration.\":\"Tek tıklamayla geri yükleme için mevcut yuvaları, düzeni ve ses yönlendirmesini kaydedin.\",\"Save current view as scene\":\"Geçerli görünümü sahne olarak kaydet\",\"All saved online models\":\"Kaydedilen tüm çevrimiçi modeller\",\"Replacement models\":\"Yedek modeller\",\"Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.\":\"Tercih edilen modeller liste sırasına göre denenir; ancak o zaman seçilen gruptaki çevrimiçi modeller kullanılır. Manuel atamalar her zaman kazanır.\",\"Replace offline slots automatically\":\"Çevrimdışı yuvaları otomatik olarak değiştir\",\"Delay (seconds)\":\"Gecikme (saniye)\",\"Candidate group\":\"Aday grubu\",\"Preferred replacements (multiple)\":\"Tercih edilen değiştirmeler (birden fazla)\",\"Replacement rule saved\":\"Değiştirme kuralı kaydedildi\",\"Save replacement rule\":\"Değiştirme kuralını kaydet\",\"Resolve duplicate shortcuts first:\":\"Öncelikle yinelenen kısayolları çözümleyin:\",\"Name, note or own tag…\":\"Ad, not veya kendi etiketi…\",\"Camera menu\":\"Kamera menüsü\",\"Camera menu · quality and sound\":\"Kamera menüsü · kalite ve ses\",\"Close menu\":\"Menüyü kapat\",\"View and window\":\"Görünüm ve pencere\",\"Audio\":\"Ses\",\"Quality and image\":\"Kalite ve imaj\",\"More actions\":\"Daha fazla işlem\",\"Currently main\":\"Şu anda ana\",\"Set as main\":\"Ana olarak ayarla\",\"Pin live preview\":\"Canlı önizlemeyi sabitle\",\"Volume multiplier for this cam\":\"Bu kamera için ses çarpanı\",\"Reset individual volume to 100%\":\"Bireysel ses seviyesini %100'e sıfırlayın\",\"Individual volume\":\"Bireysel hacim\",\"Listen to this cam only\":\"Yalnızca bu kamerayı dinle\",\"Set quality for this camera only\":\"Kaliteyi yalnızca bu kamera için ayarlayın\",\"Automatic (global/adaptive)\":\"Otomatik (genel/uyarlanabilir)\",\" · available\":\"· mevcut\",\"Quality for this camera\":\"Bu kameranın kalitesi\",\"Hold for original\":\"Orijinal için basılı tutun\",\"Vivid\":\"Canlı\",\"Image adjustments\":\"Görüntü ayarlamaları\",\"More tools\":\"Daha fazla araç\",\"Interface\":\"Arayüz\",\"Show top controls\":\"Üst kontrolleri göster\",\"Collapse top controls\":\"Üst kontrolleri daralt\",\"Show groups\":\"Grupları göster\",\"Open workspace in new window\":\"Çalışma alanını yeni pencerede aç\",\"Control center\":\"Kontrol merkezi\",\"Choose the data to export\":\"Dışa aktarılacak verileri seçin\",\"Preview the file and conflicts before applying it\":\"Uygulamadan önce dosyayı ve çakışmaları önizleyin\",\"Help\":\"Yardım\",\"Changes in this version\":\"Bu sürümdeki değişiklikler\",\"Workspaces, model library, safe imports, global design, extended normal pages, expanded Discover/dashboard, and player quality/image controls.\":\"Çalışma alanları, model kitaplığı, güvenli içe aktarmalar, global tasarım, genişletilmiş normal sayfalar, genişletilmiş Keşfet/kontrol paneli ve oynatıcı kalitesi/görüntü kontrolleri.\",\"Muted\":\"Sessize alındı\",\"Performance limit reached\":\"Performans sınırına ulaşıldı\",\"Drag to resize main width\":\"Ana genişliği yeniden boyutlandırmak için sürükleyin\",\"Drag to resize main height\":\"Ana yüksekliği yeniden boyutlandırmak için sürükleyin\",\"Stable\":\"Kararlı\",\"Unstable\":\"Kararsız\",\"Refresh all cameras\":\"Tüm kameraları yenile\",\"Reconnect current models\":\"Mevcut modelleri yeniden bağlayın\",\"Switch to grid view\":\"Tablo görünümüne geç\",\"Switch to focus view\":\"Odak görünümüne geç\",\"Manage workspaces\":\"Çalışma alanlarını yönet\",\"Create, duplicate, archive and device layouts\":\"Cihaz düzenlerini oluşturun, çoğaltın, arşivleyin ve cihaz düzenlerini oluşturun\",\"Open model library\":\"Model kitaplığını aç\",\"Notes, tags, ratings and bulk actions\":\"Notlar, etiketler, derecelendirmeler ve toplu işlemler\",\"Design options\":\"Tasarım seçenekleri\",\"All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.\":\"Odalar, kayıt, uyarılar, ekran görüntüleri, yeniden bağlantılar ve veri araçları için hepsi bir arada çoklu görüntülü iş istasyonu.\",\"Username ↵\":\"Kullanıcı adı ↵\",\"Search rooms\":\"Oda ara\",\"Invalid username\":\"Geçersiz kullanıcı adı\",\"Hide offline\":\"Çevrimdışı gizle\",\"Hide private\":\"Özeli gizle\",\"Manual\":\"Manuel\",\"By status\":\"Duruma göre\",\"By name\":\"İsme göre\",\"By added time\":\"Eklenen süreye göre\",\"By watch time\":\"İzlenme süresine göre\",\"By sessions\":\"Oturumlara göre\",\"By last watched\":\"Son izlenme tarihine göre\",\"By first watched\":\"İlk izlendiğinde\",\"By viewer count\":\"İzleyici sayısına göre\",\"By last online\":\"Son çevrimiçine göre\",\"Advanced filters\":\"Gelişmiş filtreler\",\"Watched before\":\"Daha önce izlendi\",\"Currently in view\":\"Şu anda görünümde\",\"Include tag\":\"Etiketi dahil et\",\"Exclude tag\":\"Etiketi hariç tut\",\"Minimum watch time (minutes)\":\"Minimum izlenme süresi (dakika)\",\"Watched within days\":\"Günler içinde izlendi\",\"Refresh all\":\"Tümünü yenile\",\"Online alerts\":\"Çevrimiçi uyarılar\",\"Offline alerts\":\"Çevrimdışı uyarılar\",\"Desktop notification + card flash when a model goes online\":\"Bir model çevrimiçi olduğunda masaüstü bildirimi + kart flaşı\",\"Desktop notification + card flash when a model goes offline\":\"Bir model çevrimdışı olduğunda masaüstü bildirimi + kart flaşı\",\"Dark mode\":\"Karanlık mod\",\"Use a dark interface theme. Stream video and audio are unchanged.\":\"Karanlık bir arayüz teması kullanın. Video akışı ve ses değişmedi.\",\"Dark mode enabled\":\"Karanlık mod etkin\",\"Light mode enabled\":\"Işık modu etkin\",\"Mute token/tip sounds\":\"Jeton/bahşiş seslerini kapat\",\"Blocks Chaturbate tip notification sounds only. Stream audio stays on.\":\"Yalnızca Chaturbate ipucu bildirim seslerini engeller. Ses akışı açık kalır.\",\"Token/tip sounds muted\":\"Jeton/bahşiş sesi kapatıldı\",\"Token/tip sounds enabled\":\"Jeton/bahşiş sesleri etkin\",\"Clean\":\"Temiz\",\"Clean mode on\":\"Temizleme modu açık\",\"Exit clean mode\":\"Temiz moddan çık\",\"Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.\":\"Tüm kontrolleri ve kaplamaları geçici olarak gizleyin. Kısayol: Alt+P / Alt+C. Çıkmak için Esc tuşuna basın.\",\"Window first\":\"Önce pencere\",\"Window-first mode on\":\"Önce pencere modu açık\",\"Exit window-first\":\"Önce pencereden çık\",\"Hide the app shell and keep room windows visible. Shortcut: Alt+V.\":\"Uygulama kabuğunu gizleyin ve oda pencerelerini görünür tutun. Kısayol: Alt+V.\",\"Show thumbnails\":\"Küçük resimleri göster\",\"Hide thumbnails\":\"Küçük resimleri gizle\",\"Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.\":\"Odak modunda küçük resim rayını gösterin veya gizleyin. Kısayol: Alt+T.\",\"Choose Focus rooms\":\"Odak odalarını seçin\",\"Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.\":\"Odaklanma modu için odaları seçin. İlk seçilen oda ana ekrandır; İkincil pencerelerin sırasını ayarlamak için ↑/↓ tuşlarını kullanın.\",\"Main screen\":\"Ana ekran\",\"Right window\":\"Sağ pencere\",\"Bottom window\":\"Alt pencere\",\"Empty slot\":\"Boş yuva\",\"Drag this model to a Focus slot\":\"Bu modeli bir Odak yuvasına sürükleyin\",\"Choose model for this slot\":\"Bu yuva için model seçin\",\"Select a saved model for this exact Focus slot.\":\"Tam olarak bu Focus yuvası için kayıtlı bir model seçin.\",\"Choose model for this window\":\"Bu pencere için model seçin\",\"Select a saved model for this exact grid window. Existing models stay saved.\":\"Tam olarak bu ızgara penceresi için kaydedilmiş bir model seçin. Mevcut modeller kayıtlı kalır.\",\"Remove from this grid window\":\"Bu ızgara penceresinden kaldır\",\"Only this slot is cleared. The saved model stays in your workspace.\":\"Yalnızca bu yuva temizlendi. Kaydedilen model çalışma alanınızda kalır.\",\"Show in grid window\":\"Izgara penceresinde göster\",\"Choose the exact active grid window for this model.\":\"Bu model için tam etkin ızgara penceresini seçin.\",\"Current\":\"Mevcut\",\"Model removed from this grid window.\":\"Model bu ızgara penceresinden kaldırıldı.\",\"No saved models in this group.\":\"Bu grupta kayıtlı model yok.\",\"Secondary window\":\"İkincil pencere\",\"Use current page automatically\":\"Geçerli sayfayı otomatik olarak kullan\",\"Clear custom selection\":\"Özel seçimi temizle\",\"Apply Focus layout\":\"Odak düzenini uygula\",\"No rooms are available in the current group and filter.\":\"Geçerli grup ve filtrede müsait oda yok.\",\"Fit: full image\":\"Sığdır: tam resim\",\"Fit: fill window\":\"Sığdır: pencereyi doldur\",\"Switch between full image and cropped fill.\":\"Tam görüntü ve kırpılmış dolgu arasında geçiş yapın.\",\"Playback settings\":\"Oynatma ayarları\",\"Max stream quality\":\"Maksimum akış kalitesi\",\"Auto / no cap\":\"Otomatik / sınır yok\",\"Ctrl/Command + wheel zoom\":\"Ctrl/Command + tekerlek yakınlaştırma\",\"Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.\":\"Bir kartı Ctrl/Command + fare tekerleği ile yakınlaştırın, yakınlaştırırken sürükleyin, sıfırlamak için çift tıklayın.\",\"Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.\":\"Temiz moddan çıkın. Kısayol: Alt+P / Alt+C veya Esc.\",\"Clean · Alt+P/C / Esc\":\"Temizle · Alt+P/C / Esc\",\"(synced from another tab)\":\"(başka bir sekmeden senkronize edildi)\",\"Type a username and press Enter. Shortcut: /\":\"Bir kullanıcı adı yazın ve Enter tuşuna basın. Kısayol: /\",\"Filter visible rooms by username\":\"Görünür odaları kullanıcı adına göre filtreleyin\",\"Show / hide group sidebar\":\"Grup kenar çubuğunu göster / gizle\",\"Global volume. Per-room mute is still respected.\":\"Küresel hacim. Oda başına sessize alma işlemine hala saygı duyulmaktadır.\",\"Base tile size in grid view\":\"Izgara görünümünde temel döşeme boyutu\",\"Sort rooms in the current group\":\"Geçerli gruptaki odaları sırala\",\"Main-screen height in Focus mode. You can also drag the separator.\":\"Odak modunda ana ekran yüksekliği. Ayırıcıyı da sürükleyebilirsiniz.\",\"Main-screen aspect ratio in Focus mode\":\"Odak modunda ana ekranın en boy oranı\",\"Thumbnail width in Focus mode\":\"Odak modunda küçük resim genişliği\",\"Show every saved room. The remove button deletes globally in this view.\":\"Kaydedilen her odayı göster. Bu görünümde kaldır düğmesi genel olarak siler.\",\"Create an isolated group\":\"Yalıtılmış bir grup oluşturun\",\"More tools and maintenance\":\"Daha fazla alet ve bakım\",\"GROUPS\":\"GRUPLAR\",\"All saved\":\"Hepsi kaydedildi\",\"Default\":\"Varsayılan\",\"Online now\":\"Şu anda çevrimiçi\",\"New group name:\":\"Yeni grup adı:\",\"Rename\":\"Yeniden adlandır\",\"Rename to:\":\"Şu şekilde yeniden adlandır:\",\"Delete group\":\"Grubu sil\",\"Live preview\":\"Canlı önizleme\",\"Loading preview…\":\"Önizleme yükleniyor…\",\"Private or away\":\"Özel veya uzakta\",\"Preview could not be loaded\":\"Önizleme yüklenemedi\",\"Toggle mute\":\"Sesi kapat\",\"Resume\":\"Devam et\",\"Screenshot current frame\":\"Geçerli çerçevenin ekran görüntüsü\",\"Record current card\":\"Mevcut kartı kaydet\",\"Stop recording\":\"Kaydı durdur\",\"Picture-in-Picture\":\"Resim İçinde Resim\",\"Remove from current group\":\"Mevcut gruptan kaldır\",\"Delete saved room\":\"Kayıtlı odayı sil\",\"Mirror image\":\"Ayna görüntüsü\",\"Flip image\":\"Resmi çevir\",\"Rotate left\":\"Sola döndür\",\"Rotate right\":\"Sağa döndür\",\"Reset view\":\"Görünümü sıfırla\",\"Copy username\":\"Kullanıcı adını kopyala\",\"Add favorite\":\"Favori ekle\",\"Move to favorites only\":\"Yalnızca favorilere taşı\",\"Copied\":\"Kopyalandı\",\"Screenshot failed. Browser/CORS may block drawing this stream.\":\"Ekran görüntüsü başarısız oldu. Tarayıcı/CORS bu akışın çizilmesini engelleyebilir.\",\"Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?\":\"Kayıt yereldir ve bu videoyu tarayıcı gösterdiğinde eşleşen sesle birlikte kaydeder. Bunu yalnızca bu içeriği kaydetme izniniz olduğunda kullanın. Devam etmek?\",\"Recording started\":\"Kayıt başladı\",\"Recording is not supported for this video/browser\":\"Bu video/tarayıcı için kayıt desteklenmiyor\",\"Recording segment saved\":\"Kayıt bölümü kaydedildi\",\"Final recording segment saved\":\"Son kayıt bölümü kaydedildi\",\"Recording paused; waiting for the stream to return\":\"Kayıt duraklatıldı; akışın geri dönmesini bekliyorum\",\"Recording resumed\":\"Kayıt devam ettirildi\",\"Recording paused; click to stop\":\"Kayıt duraklatıldı; durdurmak için tıklayın\",\"No recording data to save\":\"Kaydedilecek kayıt verisi yok\",\"Recording settings saved\":\"Kayıt ayarları kaydedildi\",\"Segment length in minutes (1-180):\":\"Dakika cinsinden segment uzunluğu (1-180):\",\"Video bitrate in Mbps (0.5-20):\":\"Mbps cinsinden video bit hızı (0,5-20):\",\"Warn before leaving while recording\":\"Kayıt sırasında ayrılmadan önce uyar\",\"Recording is still active. Leave anyway?\":\"Kayıt hâlâ aktif. Yine de ayrılmak mı?\",\"Recording center\":\"Kayıt merkezi\",\"No active recordings\":\"Aktif kayıt yok\",\"Waiting for source\":\"Kaynak bekleniyor\",\"Saved segments\":\"Kaydedilen segmentler\",\"Duration\":\"Süre\",\"Bitrate\":\"Bit hızı\",\"Format: MP4 when supported by this browser; otherwise the best supported fallback.\":\"Format: Bu tarayıcı tarafından desteklendiğinde MP4; aksi takdirde en iyi desteklenen geri dönüş.\",\"Record current page\":\"Geçerli sayfayı kaydet\",\"Record current group\":\"Geçerli grubu kaydet\",\"Record online rooms\":\"Çevrimiçi odaları kaydedin\",\"Stop all recordings\":\"Tüm kayıtları durdur\",\"Show recording only\":\"Yalnızca kaydı göster\",\"Show all rooms\":\"Tüm odaları göster\",\"Open current page rooms\":\"Geçerli sayfa odalarını aç\",\"Move current page to group\":\"Geçerli sayfayı gruba taşı\",\"Layout settings\":\"Düzen ayarları\",\"Recording settings\":\"Kayıt ayarları\",\"Save settings\":\"Ayarları kaydet\",\"Config backups\":\"Yedeklemeleri yapılandırma\",\"No config backups found\":\"Yapılandırma yedeği bulunamadı\",\"Online/offline history\":\"Çevrimiçi/çevrimdışı geçmiş\",\"Group rules\":\"Grup kuralları\",\"Keep favorites first\":\"Favorileri ilk sırada tut\",\"Temporary URL manager\":\"Geçici URL yöneticisi\",\"Save current temporary URLs\":\"Mevcut geçici URL'leri kaydet\",\"No temporary URLs\":\"Geçici URL yok\",\"Share workspace\":\"Çalışma alanını paylaş\",\"Share current rooms and layout\":\"Mevcut odaları ve düzeni paylaşın\",\"Copy share link\":\"Paylaşım bağlantısını kopyala\",\"This link restores rooms, groups, and layout settings in another workstation tab.\":\"Bu bağlantı, başka bir iş istasyonu sekmesindeki odaları, grupları ve düzen ayarlarını geri yükler.\",\"Shared workspace imported\":\"Paylaşılan çalışma alanı içe aktarıldı\",\"Shared workspace link is invalid\":\"Paylaşılan çalışma alanı bağlantısı geçersiz\",\"Click a field and press the new shortcut. Backspace clears that field.\":\"Bir alanı tıklayın ve yeni kısayola basın. Geri tuşu bu alanı temizler.\",\"Reset shortcuts\":\"Kısayolları sıfırla\",\"Focus add box\":\"Ekleme kutusuna odaklan\",\"Grid view\":\"Izgara görünümü\",\"Focus view\":\"Odak görünümü\",\"Clean mode\":\"Temiz mod\",\"Window-first mode\":\"Önce pencere modu\",\"Toggle thumbnails\":\"Küçük resimleri değiştir\",\"Auto-detecting…\":\"Otomatik algılama…\",\"No rooms in this group\":\"Bu grupta oda yok\",\"Add a username at the top, switch group, or clear filters\":\"Üst tarafa bir kullanıcı adı ekleyin, grubu değiştirin veya filtreleri temizleyin\",\"Open workstation\":\"İş istasyonunu aç\",\"Open here (replace page)\":\"Burayı aç (sayfayı değiştir)\",\"Replace this page with the workstation? You can go back via the browser.\":\"Bu sayfa iş istasyonuyla değiştirilsin mi? Tarayıcı aracılığıyla geri dönebilirsiniz.\",\"View saved list\":\"Kaydedilen listeyi görüntüle\",\"Collapse dock\":\"Dock'u daralt\",\"No room detected\":\"Oda algılanmadı\",\"Open here\":\"Burada aç\",\"Add room\":\"Oda ekle\",\"Remove room\":\"Odayı kaldır\",\"Record in workstation\":\"İş istasyonuna kayıt\",\"Screenshot video\":\"Ekran görüntüsü videosu\",\"Play / pause\":\"Oynat / duraklat\",\"Mute / unmute\":\"Sesi kapat / aç\",\"No playable video found on this page\":\"Bu sayfada oynatılabilir video bulunamadı\",\"Recording intent queued in workstation\":\"Kayıt amacı iş istasyonunda kuyruğa alındı\",\"Added\":\"Eklendi\",\"Already exists\":\"Zaten var\",\"Failed\":\"Başarısız\",\"Add to workstation\":\"İş istasyonuna ekle\",\"Already in workstation — click to remove\":\"Zaten iş istasyonundasınız — kaldırmak için tıklayın\",\"Model online\":\"Çevrimiçi modeli\",\"Model offline\":\"Model çevrimdışı\",\"Show fewer\":\"Daha azını göster\",\"No measurable audio at the moment\":\"Şu anda ölçülebilir bir ses yok\",\"Playback paused\":\"Oynatma duraklatıldı\",\"Audio measurement unavailable or blocked\":\"Ses ölçümü kullanılamıyor veya engellendi\",\"Notification permission denied. Card flash still works.\\n\\nYou can enable it in browser settings.\":\"Bildirim izni reddedildi. Kart flaşı hala çalışıyor.\\n\\nTarayıcı ayarlarından etkinleştirebilirsiniz.\",\"Batch add\":\"Toplu ekleme\",\"Paste usernames, one per line or separated by spaces/commas:\":\"Kullanıcı adlarını her satıra bir tane olacak şekilde veya boşluk/virgülle ayırarak yapıştırın:\",\"Cancel\":\"İptal\",\"Change the language of RoomGrid and the website\":\"RoomGrid'in ve web sitesinin dilini değiştirme\",\"Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.\":\"RoomGrid dilini değiştirin. Chaturbate, resmi site dili mevcut olmadığından mevcut web sitesi dilinde kalır.\",\"About\":\"Hakkında\",\"Export config\":\"Yapılandırmayı dışa aktar\",\"Export usernames.txt\":\"Kullanıcı adlarını.txt dosyasını dışa aktar\",\"Copy usernames\":\"Kullanıcı adlarını kopyala\",\"Unmute all\":\"Tümünün sesini aç\",\"⏸ Pause visible\":\"⏸ Görünür duraklatma\",\"Resume visible\":\"Görünür devam ettir\",\"Following\":\"Takip ediliyor\",\"Sync followed models now\":\"Takip edilen modelleri şimdi senkronize edin\",\"✓ Auto-sync followed models\":\"✓ Takip edilen modelleri otomatik senkronize et\",\"☐ Auto-sync followed models\":\"☐ Takip edilen modelleri otomatik olarak senkronize edin\",\"Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.\":\"Şu anda takip ettiğiniz Chaturbate modellerini kalıcı Takip edilenler grubuna aktarın. Yalnızca bu grup senkronize edilir.\",\"Syncing followed models…\":\"Takip edilen modeller senkronize ediliyor…\",\"Automatic Following sync disabled\":\"Otomatik Takip senkronizasyonu devre dışı bırakıldı\",\"Automatic Following sync enabled\":\"Otomatik Takip senkronizasyonu etkin\",\"Following sync needs an active Chaturbate login\":\"Senkronizasyonun ardından etkin bir Chaturbate girişi gerekiyor\",\"Following sync failed safely; saved workspace data was not changed\":\"Aşağıdaki senkronizasyon güvenli bir şekilde başarısız oldu; kayıtlı çalışma alanı verileri değiştirilmedi\",\"Enable dark mode\":\"Karanlık modu etkinleştir\",\"Disable dark mode\":\"Karanlık modu devre dışı bırak\",\"Status history\":\"Durum geçmişi\",\"Temporary URLs\":\"Geçici URL'ler\",\"Shortcut panel\":\"Kısayol paneli\",\"Toggle video fit\":\"Videoyu sığdırmayı aç/kapat\",\"Shortcuts / hints\":\"Kısayollar / ipuçları\",\"Ranking\":\"Sıralama\",\"Model details\":\"Model ayrıntıları\",\"My profile\":\"Profilim\",\"Loading local statistics…\":\"Yerel istatistikler yükleniyor…\",\"No watch data yet. Statistics start with this version.\":\"Henüz izleme verisi yok. İstatistikler bu sürümle başlar.\",\"Statistics are disabled.\":\"İstatistikler devre dışı.\",\"Enable statistics\":\"İstatistikleri etkinleştir\",\"Disable statistics\":\"İstatistikleri devre dışı bırak\",\"Export statistics\":\"İstatistikleri dışa aktar\",\"Delete all statistics\":\"Tüm istatistikleri sil\",\"Delete this model data\":\"Bu model verilerini sil\",\"Delete all locally collected watch statistics? Your workspace stays untouched.\":\"Yerel olarak toplanan tüm izleme istatistikleri silinsin mi? Çalışma alanınız dokunulmadan kalır.\",\"Tracking since\":\"Şu tarihten bu yana takip ediliyor:\",\"Models watched\":\"İzlenen modeller\",\"Watch time\":\"İzlenme süresi\",\"Most watched tags\":\"En çok izlenen etiketler\",\"Most watched hours\":\"En çok izlenen saatler\",\"Observed online\":\"Çevrimiçi olarak gözlemlendi\",\"Public bio\":\"Herkese açık biyografi\",\"Public links\":\"Herkese açık bağlantılar\",\"No public profile data cached yet.\":\"Henüz önbelleğe alınmış genel profil verisi yok.\",\"Only visible, actively playing cards count. Hover previews do not count.\":\"Yalnızca görünen, aktif olarak oynanan kartlar sayılır. Vurgulu önizlemeler sayılmaz.\",\"Statistics imported\":\"İçe aktarılan istatistikler\",\"Statistics deleted\":\"İstatistikler silindi\",\"Statistics exported\":\"Dışa aktarılan istatistikler\",\"Local observation only\":\"Yalnızca yerel gözlem\",\"No private messages, tokens, payments, passwords or external tracking are collected.\":\"Hiçbir özel mesaj, jeton, ödeme, şifre veya harici takip toplanmaz.\",\"Not enough observations for a time estimate\":\"Zaman tahmini için yeterli gözlem yok\",\"Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\\n\\nDefaults:\\n/ Focus username input\\nr Refresh all\\ng Grid view\\nf Focus view\\nAlt+P or Alt+C Clean mode on/off\\nAlt+Shift+C Recording center\\nAlt+Shift+R Record current page\\nEsc Exit clean mode / fullscreen\\nSpace Pause/resume the main screen in Focus view\\n←/→ or [/ ] Switch the main screen in Focus view\\nDouble-click card Fullscreen\":\"İpucunu görmek için herhangi bir düğmenin/kontrolün üzerine gelin. Ortak kısayollar kısayol panelinde düzenlenebilir.\\n\\nVarsayılanlar:\\n/ Kullanıcı adı girişine odaklan\\nr Tümünü yenile\\ng Izgara görünümü\\nf Odak görünümü\\nAlt+P veya Alt+C Temizleme modu açık/kapalı\\nAlt+Shift+C Kayıt merkezi\\nAlt+Shift+R Geçerli sayfayı kaydet\\nEsc Temiz moddan / tam ekrandan çık\\nBoşluk Odak görünümünde ana ekranı duraklatın/devam ettirin\\n←/→ veya [/ ] Odak görünümünde ana ekrana geçiş yapın\\nKarta çift tıklayın Tam ekran\",\"Visible windows paused\":\"Görünür pencereler duraklatıldı\",\"Visible windows resumed\":\"Görünür pencereler devam ettirildi\",\"Import config\":\"Yapılandırmayı içe aktar\",\"Repair saved data\":\"Kaydedilen verileri onarın\",\"Saved data has been normalized.\":\"Kaydedilen veriler normalleştirildi.\",\"Clear all data\":\"Tüm verileri temizle\",\"This will erase ALL rooms, groups and settings. Continue?\":\"Bu, TÜM odaları, grupları ve ayarları silecektir. Devam etmek?\",\"English\":\"İngilizce\",\"Deutsch\":\"Almanca\",\"Author\":\"Yazar\",\"License\":\"Lisans\",\"If this script saves you time…\":\"Bu komut dosyası size zaman kazandırırsa…\",\"Tip in ETH\":\"ETH'de ipucu\",\"ETH address\":\"ETH adresi\",\"Copy\":\"Kopyala\",\"Live for a short time and already drawing public activity.\":\"Kısa bir süre için yaşıyor ve zaten kamusal aktiviteyi çekiyor.\",\"Live stream audio activity\":\"Canlı yayın ses etkinliği\",\"Local watch\":\"Yerel izleme\",\"Logo background\":\"Logo arka planı\",\"Logo height\":\"Logo yüksekliği\",\"Logo width\":\"Logo genişliği\",\"Loud\":\"Yüksek sesle\",\"Measured even while output is muted\":\"Çıkış sessize alındığında bile ölçülür\",\"Middle-click or Ctrl+click opens a new tab\":\"Orta tıklama veya Ctrl+tıklama yeni bir sekme açar\",\"Models you have visited locally and that are public again now.\":\"Yerel olarak ziyaret ettiğiniz ve artık tekrar herkese açık olan modeller.\",\"Mono\":\"Mono\",\"New discoveries\":\"Yeni keşifler\",\"New to the catalog, newly seen by Camclient, or just started streaming.\":\"Katalogda yeni olan, Camclient tarafından yeni görülen veya yayına yeni başlayan.\",\"No audio track\":\"Ses parçası yok\",\"No filters active · all matching live models are shown\":\"Aktif filtre yok · eşleşen tüm canlı modeller gösteriliyor\",\"Accent gradient\":\"Vurgu gradyanı\",\"Active filters\":\"Aktif filtreler\",\"Add your own images or use the screenshot button in the player. Screenshots can enter the gallery automatically and can be used directly as the model cover.\":\"Kendi görsellerinizi ekleyin veya oynatıcıdaki ekran görüntüsü düğmesini kullanın. Ekran görüntüleri galeriye otomatik olarak girebilir ve doğrudan model kapağı olarak kullanılabilir.\",\"All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.\":\"Tüm ana sayfa filtreleri, düzen, önizleme ve sayfalandırma ayarları kararlı ayarlar kasasında saklanır ve komut dosyası güncellemelerinden etkilenmez.\",\"All live models\":\"Tüm canlı modeller\",\"All suggestions\":\"Tüm öneriler\",\"Almost silent\":\"Neredeyse sessiz\",\"Always active\":\"Her zaman aktif\",\"AND\":\"VE\",\"Aurora\":\"Aurora\",\"Automatic example\":\"Otomatik örnek\",\"Automatic segments\":\"Otomatik segmentler\",\"Automatically loading the complete catalog…\":\"Kataloğun tamamı otomatik olarak yükleniyor…\",\"Available recording data was saved.\":\"Mevcut kayıt verileri kaydedildi.\",\"Back online\":\"Tekrar çevrimiçi\",\"Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.\":\"Yerel olarak kayıtlı favorilerinize, etiketlerinize ve görüntüleme etkinliğinize göre. Her seçim kendi nedenini gösterir.\",\"Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.\":\"Camclient adını, markasını, sloganını ve yerel logoyu özelleştirin. Logolar bu tarayıcıda kalır.\",\"Discover something new\":\"Yeni bir şey keşfedin\",\"Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.\":\"Farklı öneri şeritleri, ana sayfa kataloğunu tekrarlamak yerine yeni bir şeyler bulmanıza yardımcı olur.\",\"Do not enlarge\":\"Büyütme\",\"Enable meter\":\"Sayacı etkinleştir\",\"Fast starters\":\"Hızlı başlayanlar\",\"Fill and crop\":\"Doldur ve kırp\",\"Filter suggestions\":\"Önerileri filtrele\",\"Fit completely\":\"Tamamen sığdır\",\"For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.\":\"Şeffaflık için PNG, WebP veya güvenli SVG kullanın ve logo arka planı olarak Şeffaf'ı seçin. JPEG'in şeffaflığı yoktur. Stretch, seçilen genişliği ve yüksekliği bağımsız olarak kullanır.\",\"For you\":\"senin için\",\"gallery\":\"galeri\",\"HD\":\"HD\",\"Inner spacing\":\"İç boşluk\",\"Inspiration, not repetition\":\"İlham, tekrarlama değil\",\"It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.\":\"Yazılımın kullanılmasına, değiştirilmesine ve dağıtılmasına izin verir. Telif hakkı ve lisans bildirimleri kalmalıdır ve yazılım garantisiz olarak gelir. Camclient, Emberhost tarafından yazılmıştır.\",\"Monogram\":\"Monogram\",\"No recommendations match the active filters.\":\"Etkin filtrelerle eşleşen öneri yok.\",\"No recording data\":\"Kayıt verisi yok\",\"NOT\":\"DEĞİL\",\"Only new\":\"Sadece yeni\",\"Open Camclient homepage\":\"Camclient ana sayfasını açın\",\"OR\":\"VEYA\",\"part\":\"kısım\",\"Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.\":\"Kişisel seçimler, yeni keşifler, popüler oturma odaları ve daha önce hiç ziyaret etmediğiniz modeller; ana sayfa kataloğunun tamamından açıkça ayrılmış.\",\"Place\":\"Yer\",\"PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.\":\"PNG, WebP ve güvenli SVG şeffaflığı destekler. Resimler yerel olarak 144 × 144 piksel olarak yeniden boyutlandırılır ve ayarlarınızla kaydedilir.\",\"Popular now\":\"Şimdi popüler\",\"Public live models with no local watch time in Camclient.\":\"Camclient'te yerel izlenme süresi olmayan herkese açık canlı modeller.\",\"Quiet\":\"Sessiz\",\"Ranked by the current public viewer count; catalog data is not stored permanently.\":\"Mevcut genel izleyici sayısına göre sıralanmıştır; katalog verileri kalıcı olarak saklanmaz.\",\"Recording is still active.\":\"Kayıt hâlâ aktif.\",\"Recording quality\":\"Kayıt kalitesi\",\"Recording stopped\":\"Kayıt durduruldu\",\"Recording unavailable\":\"Kayıt kullanılamıyor\",\"Remix\":\"Remiks\",\"Reset all\":\"Tümünü sıfırla\",\"RoomGrid Model library\":\"RoomGrid Model kütüphanesi\",\"RoomGrid My user\":\"RoomGrid Kullanıcım\",\"RoomGrid Statistics\":\"RoomGrid İstatistikleri\",\"Save to model gallery\":\"Model galerisine kaydet\",\"Saved segment\":\"Kaydedilen segment\",\"Screenshot\":\"Ekran görüntüsü\",\"Screenshot failed\":\"Ekran görüntüsü başarısız oldu\",\"Screenshot format\":\"Ekran görüntüsü formatı\",\"Screenshot resolution\":\"Ekran görüntüsü çözünürlüğü\",\"Screenshot unavailable\":\"Ekran görüntüsü kullanılamıyor\",\"Screenshots are downloaded and can also enter the local model gallery.\":\"Ekran görüntüleri indirilir ve yerel model galerisine de girilebilir.\",\"segment(s)\":\"segment(ler)\",\"Black\":\"Siyah\",\"Brave exposed no recordable video track.\":\"Brave kaydedilebilir bir video parçası göstermedi.\",\"Brave or the video source blocked frame capture.\":\"Cesur veya video kaynağı kare yakalamayı engelledi.\",\"Browser exposed no audio track\":\"Tarayıcı hiçbir ses parçasını açığa çıkarmadı\",\"Camclient as the real homepage\":\"Gerçek ana sayfa olarak Camclient\",\"Camclient assigns these transparent examples to common tags automatically. Use the image button on any tag to choose an example, upload your own PNG/WebP/SVG, or hide the image.\":\"Camclient bu şeffaf örnekleri otomatik olarak ortak etiketlere atar. Bir örnek seçmek, kendi PNG/WebP/SVG dosyanızı yüklemek veya görüntüyü gizlemek için herhangi bir etiketteki görsel düğmesini kullanın.\",\"Choose a Camclient example or upload your own transparent image.\":\"Bir Camclient örneği seçin veya kendi şeffaf görüntünüzü yükleyin.\",\"Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.\":\"1,1 MB'tan küçük bir PNG, JPEG, WebP veya güvenli SVG seçin.\",\"Choose artwork for tag\":\"Etiket için resim seçin\",\"Click to enable meter\":\"Sayacı etkinleştirmek için tıklayın\",\"Close more actions\":\"Daha fazla eylemi kapat\",\"Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.\":\"Renkler, tipografi, aralıklar, köşeler, gölgeler ve arka planlar Ayarlar'a, çalışma alanına, Keşfet'e ve model kitaplığına anında uygulanır.\",\"Continue loading the complete catalog\":\"Kataloğun tamamını yüklemeye devam edin\",\"Continuing recording\":\"Kayıt devam ediyor\",\"current matches\":\"mevcut maçlar\",\"Camclient\":\"Camclient\",\"Your discovery mix\":\"Keşif karışımınız\",\"中文\":\"中文\",\"Show no image\":\"Resim gösterme\",\"Shows activity in the stream audio track independently of whether player output is muted.\":\"Oynatıcı çıkışının sessize alınıp alınmamasından bağımsız olarak akış ses parçasındaki etkinliği gösterir.\",\"Source ended\":\"Kaynak sona erdi\",\"Source resolution\":\"Kaynak çözünürlüğü\",\"Start recording\":\"Kaydı başlat\",\"Starter pick · popular now\":\"Başlangıç seçimi · şu anda popüler\",\"Stop and save\":\"Durdur ve kaydet\",\"Stretch\":\"Streç\",\"Tag artwork and emoji\":\"Resmi ve emojiyi etiketleyin\",\"Tag artwork editor\":\"Etiket resmi düzenleyicisi\",\"The compressed image is still too large.\":\"Sıkıştırılmış görüntü hâlâ çok büyük.\",\"The header only shows Home, Discover, Tag Explorer, Statistics Center, Model Library, My User, and Settings. Every configuration area is available as a Settings subpage.\":\"Başlıkta yalnızca Ana Sayfa, Keşfet, Etiket Gezgini, İstatistik Merkezi, Model Kitaplığı, Kullanıcım ve Ayarlar gösterilir. Her yapılandırma alanı Ayarlar alt sayfası olarak mevcuttur.\",\"The image could not be decoded.\":\"Görüntünün kodu çözülemedi.\",\"The image could not be read.\":\"Resim okunamadı.\",\"The image format is unsafe or unsupported.\":\"Resim biçimi güvenli değil veya desteklenmiyor.\",\"The lanes respect your active search and filter settings.\":\"Şeritler aktif arama ve filtre ayarlarınıza uygundur.\",\"The recording is saved only on this device. Continue only if you have permission to save this content.\":\"Kayıt yalnızca bu cihaza kaydedilir. Yalnızca bu içeriği kaydetme izniniz varsa devam edin.\",\"The video has no usable frame yet.\":\"Videonun henüz kullanılabilir bir çerçevesi yok.\",\"There is not enough local preference data yet, so these are transparent starter picks based on current popularity.\":\"Henüz yeterli yerel tercih verisi yok, dolayısıyla bunlar mevcut popülerliğe dayalı şeffaf başlangıç seçimleridir.\",\"These settings control the Camclient homepage layout, pagination, model information, filters, and performance.\":\"Bu ayarlar Camclient ana sayfa düzenini, sayfalandırmayı, model bilgilerini, filtreleri ve performansı kontrol eder.\",\"Transparent\":\"Şeffaf\",\"Unified main navigation\":\"Birleşik ana gezinme\",\"Unvisited\":\"Ziyaret edilmemiş\",\"Upload own image\":\"Kendi resmini yükle\",\"Use as model cover\":\"Model kapağı olarak kullanın\",\"Use the complete search, filters, and sorting for every current result.\":\"Mevcut her sonuç için tam aramayı, filtreleri ve sıralamayı kullanın.\",\"Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.\":\"Chaturbate ana sayfasını ziyaret ettiğinizde her zaman Camclient ana sayfasının tamamı açılır. Her ana alan aynı özel arayüzün içinde kalır.\",\"Waiting for audio\":\"Ses bekleniyor\",\"What does the MIT license mean?\":\"MIT lisansı ne anlama geliyor?\",\"White\":\"Beyaz\",\"Trans\":\"Trans\",\"Windows\":\"Windows\",\"SingleCam\":\"SingleCam\",\"Discover public live rooms\":\"Herkese açık canlı odaları keşfet\",\"all groups\":\"tüm gruplar\",\"at least one group\":\"en az bir grup\",\"no measurements yet\":\"henüz ölçüm yok\"},\"patterns\":{\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] ago\":\"[[ROOMGRID_VALUE_0]][[ROOMGRID_VALUE_1]] önce\",\"[[ROOMGRID_VALUE_0]] preview\":\"[[ROOMGRID_VALUE_0]] önizleme\",\"@[[ROOMGRID_VALUE_0]] profile saved\":\"@[[ROOMGRID_VALUE_0]] profili kaydedildi\",\"Model details for @[[ROOMGRID_VALUE_0]]\":\"@[[ROOMGRID_VALUE_0]] için model ayrıntıları\",\"[[ROOMGRID_VALUE_0]] followed models imported\":\"[[ROOMGRID_VALUE_0]] takip edilen modelleri içe aktardı\",\"[[ROOMGRID_VALUE_0]] My Collection items recognized\":\"[[ROOMGRID_VALUE_0]] Koleksiyonumdaki öğeler tanındı\",\"[[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] currently loaded models visible.\":\"Yüklü [[ROOMGRID_VALUE_1]] modelden [[ROOMGRID_VALUE_0]] tanesi görünür.\",\"Active group filter: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_1]] of [[ROOMGRID_VALUE_2]] visible cards match ([[ROOMGRID_VALUE_3]]).\":\"Etkin grup filtresi: [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_2]] görünür karttan [[ROOMGRID_VALUE_1]] tanesi eşleşiyor ([[ROOMGRID_VALUE_3]]).\",\"Custom cover · [[ROOMGRID_VALUE_0]]\":\"Özel kapak · [[ROOMGRID_VALUE_0]]\",\"Locally saved cover · [[ROOMGRID_VALUE_0]]\":\"Yerel olarak kaydedilen kapak · [[ROOMGRID_VALUE_0]]\",\"Example images and covers ([[ROOMGRID_VALUE_0]] saved)\":\"Örnek görseller ve kapaklar ([[ROOMGRID_VALUE_0]] kaydedildi)\",\"Permanently stored on this device · latest [[ROOMGRID_VALUE_0]]\":\"Bu cihazda kalıcı olarak saklanıyor · en son [[ROOMGRID_VALUE_0]]\",\"Own image · [[ROOMGRID_VALUE_0]]\":\"Kendi resmi · [[ROOMGRID_VALUE_0]]\",\"Saved [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\":\"Kaydedildi [[ROOMGRID_VALUE_0]] [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]]\",\"First seen: [[ROOMGRID_VALUE_0]]\":\"İlk görülme tarihi: [[ROOMGRID_VALUE_0]]\",\"Last seen: [[ROOMGRID_VALUE_0]]\":\"Son görülme tarihi: [[ROOMGRID_VALUE_0]]\",\"Last live: [[ROOMGRID_VALUE_0]]\":\"Son canlı yayın: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · first [[ROOMGRID_VALUE_1]] · latest [[ROOMGRID_VALUE_2]]\":\"Görünüşler: [[ROOMGRID_VALUE_0]]× · ilk [[ROOMGRID_VALUE_1]] · son [[ROOMGRID_VALUE_2]]\",\"Typical time: [[ROOMGRID_VALUE_0]]\":\"Tipik zaman: [[ROOMGRID_VALUE_0]]\",\"Technical: [[ROOMGRID_VALUE_0]]\":\"Teknik: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] local live observations, at most one every 5 minutes\":\"[[ROOMGRID_VALUE_0]] yerel canlı gözlemler, en fazla her 5 dakikada bir\",\"[[ROOMGRID_VALUE_0]] of your previous watch time as a cautious estimate\":\"Dikkatli bir tahmin olarak önceki izlenme sürenizin [[ROOMGRID_VALUE_0]]'si\",\"Remove tag [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] etiketini kaldır\",\"[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]] modeli[[ROOMGRID_VALUE_1]]\",\"Manage custom groups · assigned to [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]]'ye atanan özel grupları yönetin\",\"Delete group “[[ROOMGRID_VALUE_0]]”? The models will remain in your library.\":\"“[[ROOMGRID_VALUE_0]]” grubu silinsin mi? Modeller kitaplığınızda kalacaktır.\",\"Group “[[ROOMGRID_VALUE_0]]” deleted\":\"“[[ROOMGRID_VALUE_0]]” grubu silindi\",\"[[ROOMGRID_VALUE_0]] groups · multiple selection possible\":\"[[ROOMGRID_VALUE_0]] gruplar · çoklu seçim mümkün\",\"[[ROOMGRID_VALUE_0]] saved tags\":\"[[ROOMGRID_VALUE_0]] kayıtlı etiketler\",\"Remove filter [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] filtresini kaldır\",\"[[ROOMGRID_VALUE_0]] matches\":\"[[ROOMGRID_VALUE_0]] eşleşmeleri\",\"[[ROOMGRID_VALUE_0]] filters active\":\"[[ROOMGRID_VALUE_0]] filtreler etkin\",\"@[[ROOMGRID_VALUE_0]] · Public profile information can be loaded through “All details”.\":\"@[[ROOMGRID_VALUE_0]] · Genel profil bilgileri “Tüm ayrıntılar” aracılığıyla yüklenebilir.\",\"Personally organized since: [[ROOMGRID_VALUE_0]]\":\"Şu tarihten bu yana kişisel olarak organize edildi: [[ROOMGRID_VALUE_0]]\",\"Automatically cataloged since: [[ROOMGRID_VALUE_0]]\":\"Şu tarihten itibaren otomatik olarak kataloglandı: [[ROOMGRID_VALUE_0]]\",\"Last activity: [[ROOMGRID_VALUE_0]]\":\"Son etkinlik: [[ROOMGRID_VALUE_0]]\",\"Appearances: [[ROOMGRID_VALUE_0]]× · last recorded [[ROOMGRID_VALUE_1]]\":\"Görünüşler: [[ROOMGRID_VALUE_0]]× · son kaydedilen [[ROOMGRID_VALUE_1]]\",\"Discovered: [[ROOMGRID_VALUE_0]] · quality measurements: [[ROOMGRID_VALUE_1]]\":\"Keşfedilen: [[ROOMGRID_VALUE_0]] · kalite ölçümleri: [[ROOMGRID_VALUE_1]]\",\"Profile information: [[ROOMGRID_VALUE_0]]\":\"Profil bilgileri: [[ROOMGRID_VALUE_0]]\",\"@[[ROOMGRID_VALUE_0]] archived\":\"@[[ROOMGRID_VALUE_0]] arşivlendi\",\"@[[ROOMGRID_VALUE_0]] restored\":\"@[[ROOMGRID_VALUE_0]] geri yüklendi\",\"Page [[ROOMGRID_VALUE_0]] of [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] models\":\"[[ROOMGRID_VALUE_1]] sayfa [[ROOMGRID_VALUE_0]] · [[ROOMGRID_VALUE_2]] modelleri\",\"[[ROOMGRID_VALUE_0]] saved\":\"[[ROOMGRID_VALUE_0]] kaydedildi\",\"[[ROOMGRID_VALUE_0]] watched\":\"[[ROOMGRID_VALUE_0]] izledi\",\"[[ROOMGRID_VALUE_0]] active · grows automatically\":\"[[ROOMGRID_VALUE_0]] aktif · otomatik olarak büyüyor\",\"[[ROOMGRID_VALUE_0]] models · [[ROOMGRID_VALUE_1]] sessions\":\"[[ROOMGRID_VALUE_0]] modeller · [[ROOMGRID_VALUE_1]] oturumlar\",\"[[ROOMGRID_VALUE_0]] favorites\":\"[[ROOMGRID_VALUE_0]] favoriler\",\"[[ROOMGRID_VALUE_0]] distinct models\":\"[[ROOMGRID_VALUE_0]] farklı modeller\",\"[[ROOMGRID_VALUE_0]] previews\":\"[[ROOMGRID_VALUE_0]] önizlemeleri\",\"Preview/quick view across [[ROOMGRID_VALUE_0]] models\":\"[[ROOMGRID_VALUE_0]] modellerinde önizleme/hızlı görüntüleme\",\"[[ROOMGRID_VALUE_0]] days\":\"[[ROOMGRID_VALUE_0]] gün\",\"[[ROOMGRID_VALUE_0]]-day streak\":\"[[ROOMGRID_VALUE_0]] günlük seri\",\"[[ROOMGRID_VALUE_0]] watched repeatedly\":\"[[ROOMGRID_VALUE_0]] tekrar tekrar izlendi\",\"[[ROOMGRID_VALUE_0]] days of local data\":\"[[ROOMGRID_VALUE_0]] günlük yerel veri\",\"weighted average [[ROOMGRID_VALUE_0]]\":\"ağırlıklı ortalama [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] live now\":\"[[ROOMGRID_VALUE_0]] şu anda canlı\",\"[[ROOMGRID_VALUE_0]] models recognized\":\"[[ROOMGRID_VALUE_0]] modelleri tanındı\",\"Not everything could be read: [[ROOMGRID_VALUE_0]]\":\"Her şey okunamadı: [[ROOMGRID_VALUE_0]]\",\"Most active day: [[ROOMGRID_VALUE_0]] with [[ROOMGRID_VALUE_1]]. Retention: [[ROOMGRID_VALUE_2]] days for raw events; older values can remain as daily totals.\":\"En aktif gün: [[ROOMGRID_VALUE_0]] ile [[ROOMGRID_VALUE_1]]. Saklama: ham olaylar için [[ROOMGRID_VALUE_2]] gün; eski değerler günlük toplam olarak kalabilir.\",\"[[ROOMGRID_VALUE_0]] sessions\":\"[[ROOMGRID_VALUE_0]] oturum\",\"[[ROOMGRID_VALUE_0]] saved locally\":\"[[ROOMGRID_VALUE_0]] yerel olarak kaydedildi\",\"Local data could not be loaded: [[ROOMGRID_VALUE_0]]\":\"Yerel veriler yüklenemedi: [[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] is online.\":\"[[ROOMGRID_VALUE_0]] çevrimiçi.\",\"[[ROOMGRID_VALUE_0]] is offline.\":\"[[ROOMGRID_VALUE_0]] çevrimdışı.\",\"[[ROOMGRID_VALUE_0]] has not been observed online for a while.\":\"[[ROOMGRID_VALUE_0]] bir süredir çevrimiçi olarak gözlemlenmiyor.\",\"[[ROOMGRID_VALUE_0]] favorites are online together.\":\"[[ROOMGRID_VALUE_0]] favoriler birlikte çevrimiçi.\",\"[[ROOMGRID_VALUE_0]]'s public profile changed.\":\"[[ROOMGRID_VALUE_0]]'in herkese açık profili değişti.\",\"[[ROOMGRID_VALUE_0]] models updated\":\"[[ROOMGRID_VALUE_0]] modelleri güncellendi\",\"Statistics import preview (nothing changed yet):\\n[[ROOMGRID_VALUE_0]] models\\n[[ROOMGRID_VALUE_1]] events\\n[[ROOMGRID_VALUE_2]] public profiles\\n\\nContinuing replaces the existing statistics.\":\"İstatistik içe aktarma önizlemesi (henüz hiçbir şey değişmedi):\\n[[ROOMGRID_VALUE_0]] modelleri\\n[[ROOMGRID_VALUE_1]] etkinlikleri\\n[[ROOMGRID_VALUE_2]] herkese açık profiller\\n\\nDevam etmek mevcut istatistiklerin yerini alır.\",\"Live preview of [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] canlı önizlemesi\",\"[[ROOMGRID_VALUE_0]] added to MultiCam\":\"[[ROOMGRID_VALUE_0]] MultiCam'e eklendi\",\"Delete [[ROOMGRID_VALUE_0]]?\":\"[[ROOMGRID_VALUE_0]] silinsin mi?\",\"Import [[ROOMGRID_VALUE_0]] views and [[ROOMGRID_VALUE_1]] feeds?\":\"[[ROOMGRID_VALUE_0]] görünümler ve [[ROOMGRID_VALUE_1]] yayınlar içe aktarılsın mı?\",\"[[ROOMGRID_VALUE_0]] rooms added to workspace\":\"[[ROOMGRID_VALUE_0]] oda çalışma alanına eklendi\",\"[[ROOMGRID_VALUE_0]] models saved\":\"[[ROOMGRID_VALUE_0]] modelleri kaydedildi\",\"[[ROOMGRID_VALUE_0]] matching results\":\"[[ROOMGRID_VALUE_0]] eşleşen sonuçlar\",\"[[ROOMGRID_VALUE_0]] matching live models\":\"[[ROOMGRID_VALUE_0]] eşleşen canlı modeller\",\"Shares #[[ROOMGRID_VALUE_0]] with a favorite\":\"#[[ROOMGRID_VALUE_0]] bir favoriyle paylaşılıyor\",\"[[ROOMGRID_VALUE_0]] images added; [[ROOMGRID_VALUE_1]] could not be processed.\":\"[[ROOMGRID_VALUE_0]] resim eklendi; [[ROOMGRID_VALUE_1]] işlenemedi.\",\"[[ROOMGRID_VALUE_0]] images added.\":\"[[ROOMGRID_VALUE_0]] görseller eklendi.\",\"[[ROOMGRID_VALUE_0]] rooms\":\"[[ROOMGRID_VALUE_0]] odalar\",\"[[ROOMGRID_VALUE_0]] valid usernames\":\"[[ROOMGRID_VALUE_0]] geçerli kullanıcı adları\",\"Page [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] rooms\":\"Sayfa [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]], [[ROOMGRID_VALUE_2]] odalar\",\"Temporary URLs: [[ROOMGRID_VALUE_0]] added, [[ROOMGRID_VALUE_1]] failed\":\"Geçici URL'ler: [[ROOMGRID_VALUE_0]] eklendi, [[ROOMGRID_VALUE_1]] başarısız oldu\",\"Statistics import preview: [[ROOMGRID_VALUE_0]] models and [[ROOMGRID_VALUE_1]] events. Continuing safely merges the data and creates a backup first.\":\"İstatistik içe aktarma önizlemesi: [[ROOMGRID_VALUE_0]] modelleri ve [[ROOMGRID_VALUE_1]] olayları. Devam etmek, verileri güvenli bir şekilde birleştirir ve önce bir yedekleme oluşturur.\",\"Custom layouts support up to [[ROOMGRID_VALUE_0]] rooms.\":\"Özel düzenler en fazla [[ROOMGRID_VALUE_0]] odayı destekler.\",\"Scene “[[ROOMGRID_VALUE_0]]” saved\":\"“[[ROOMGRID_VALUE_0]]” sahnesi kaydedildi\",\"Scene “[[ROOMGRID_VALUE_0]]” loaded\":\"“[[ROOMGRID_VALUE_0]]” sahnesi yüklendi\",\"[[ROOMGRID_VALUE_0]] active streams · [[ROOMGRID_VALUE_1]] unstable · [[ROOMGRID_VALUE_2]] to watch\":\"[[ROOMGRID_VALUE_0]] etkin akışlar · [[ROOMGRID_VALUE_1]] kararsız · izlenecek [[ROOMGRID_VALUE_2]]\",\"[[ROOMGRID_VALUE_0]] streams · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p assigned\":\"[[ROOMGRID_VALUE_0]] akışları · [[ROOMGRID_VALUE_1]]p / [[ROOMGRID_VALUE_2]]p atandı\",\"Delete scene “[[ROOMGRID_VALUE_0]]”?\":\"“[[ROOMGRID_VALUE_0]]” sahnesi silinsin mi?\",\"Saved only for [[ROOMGRID_VALUE_0]]: up to [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]\":\"Yalnızca [[ROOMGRID_VALUE_0]] için kaydedildi: [[ROOMGRID_VALUE_1]]p[[ROOMGRID_VALUE_2]]'ye kadar\",\"This cam uses global/adaptive quality[[ROOMGRID_VALUE_0]]\":\"Bu kamera global/uyarlanabilir kaliteyi kullanıyor[[ROOMGRID_VALUE_0]]\",\"[[ROOMGRID_VALUE_0]] was replaced by [[ROOMGRID_VALUE_1]]\":\"[[ROOMGRID_VALUE_0]], [[ROOMGRID_VALUE_1]] ile değiştirildi\",\"Playing audio only from [[ROOMGRID_VALUE_0]]\":\"Yalnızca [[ROOMGRID_VALUE_0]] kaynağından ses çalınıyor\",\"Placed in Focus slot [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] Odak yuvasına yerleştirildi\",\"Placed in grid window [[ROOMGRID_VALUE_0]]\":\"[[ROOMGRID_VALUE_0]] ızgara penceresine yerleştirildi\",\"Resolution [[ROOMGRID_VALUE_0]], state [[ROOMGRID_VALUE_1]], buffer [[ROOMGRID_VALUE_2]]\":\"Çözünürlük [[ROOMGRID_VALUE_0]], durum [[ROOMGRID_VALUE_1]], arabellek [[ROOMGRID_VALUE_2]]\",\"Stream health [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\":\"Akış durumu [[ROOMGRID_VALUE_0]]/100 · [[ROOMGRID_VALUE_1]]\",\"Remove filter: [[ROOMGRID_VALUE_0]]\":\"Filtreyi kaldır: [[ROOMGRID_VALUE_0]]\",\"Matches your interests: [[ROOMGRID_VALUE_0]]\":\"İlgi alanlarınıza uygun: [[ROOMGRID_VALUE_0]]\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] fetched · [[ROOMGRID_VALUE_2]] live models\":\"[[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] kataloğu getirildi · [[ROOMGRID_VALUE_2]] canlı modeller\",\"[[ROOMGRID_VALUE_0]] local models\":\"[[ROOMGRID_VALUE_0]] yerel modeller\",\"Catalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] live models\":\"Katalog [[ROOMGRID_VALUE_0]] / [[ROOMGRID_VALUE_1]] · [[ROOMGRID_VALUE_2]] canlı modeller\",\"[[ROOMGRID_VALUE_0]] catalog rooms loaded\":\"[[ROOMGRID_VALUE_0]] katalog odaları yüklendi\"},\"visible\":{\"Automatisch neu verbinden\":\"Otomatik olarak yeniden bağlan\",\"Bei verstecktem Tab pausieren\":\"Sekme gizliyken duraklat\",\"Bestehende Chaturbate-Kontodaten übernehmen\":\"Mevcut Chaturbate hesap ayrıntılarını uygula\",\"Bild optimieren · großer Zoom, Position, Filter und Drehung\":\"Görüntüyü optimize edin · geniş yakınlaştırma, konum, filtre ve döndürme\",\"Bildanpassung\":\"Görüntü ayarı\",\"Bildwerkzeuge einklappen\":\"Resim araçlarını daralt\",\"Breite des Informationsbereichs\":\"Bilgi alanının genişliği\",\"Broadcaster-Verbindung verwalten\":\"Yayıncı bağlantısını yönet\",\"Chat anzeigen\":\"Sohbeti göster\",\"Ctrl + Shift + X\":\"Ctrl + Üst Karakter + X\",\"Ctrl+K\":\"Ctrl+K\",\"Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.\":\"Bu, komut dosyası yüklendiğinden beri verilerle sınırlı olmadığınız anlamına gelir: RoomGrid, mevcut takip listenizi ve Koleksiyonum'daki açıkça tanınabilir girişleri okuyabilir. Hiçbir mesaj, ödeme, medya veya şifre kopyalanmaz.\",\"Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.\":\"Analytics Studio'da ayrıca zaman dilimi karşılaştırmaları, ısı haritaları, keşif hunileri, çalışma alanı ve kalite analizlerinin yanı sıra veri aktarımları da bulunur.\",\"Datenschutz & Export\":\"Veri koruma ve dışa aktarma\",\"Deine Aktivität der letzten 14 Tage\":\"Son 14 gündeki etkinliğiniz\",\"Deine meistgesehenen Models\":\"En çok görüntülenen modelleriniz\",\"Model ist in allen gewählten Gruppen\":\"Model seçilen tüm gruplarda\",\"Model ist in mindestens einer gewählten Gruppe\":\"Model en az bir seçilmiş grupta yer alıyor\",\"MultiCam\":\"Çoklu Kamera\",\"MultiCam +\":\"Çoklu Kamera+\",\"My Collection öffnen\":\"Koleksiyonumu Aç\",\"Namen speichern\":\"Adı kaydet\",\"Nicht verbunden\":\"Bağlı değil\",\"Niedrige Live-Latenz\":\"Düşük canlı gecikme\",\"Noch keine ausreichenden lokalen Nutzungswerte.\":\"Hala yeterli yerel kullanım değeri yok.\",\"Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.\":\"Henüz kendimize ait bir grubumuz yok. Yukarıya bir ad girin ve ilk koleksiyonunuzu oluşturun.\",\"Noch keine Model-Sehzeit erfasst.\":\"Henüz model izleme süresi kaydedilmedi.\",\"Noch keine offiziellen Konto-Snapshots vorhanden.\":\"Henüz resmi hesap anlık görüntüsü yok.\",\"Noch nicht eingelesen\":\"Henüz okunmadı\",\"Nur auf diesem Gerät\":\"Yalnızca bu cihazda\",\"Öffentliche/erkannte Tags\":\"Genel/tanınan etiketler\",\"Öffentliches Profil anzeigen\":\"Herkese açık profili görüntüle\",\"Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.\":\"Geçerli bağlantı etkin; Bir sonraki ölçümden sonra yerel rota burada görünür.\",\"Diese Bibliotheksansicht ist noch leer.\":\"Bu kitaplık görünümü hala boş.\",\"Eigene Priorität\":\"Kendi önceliği\",\"Eigene Tags\":\"Kendi etiketleri\",\"Eigene Tags filtern\":\"Kendi etiketlerinizi filtreleyin\",\"Eigenen Tag eingeben …\":\"Kendi gününüzü girin...\",\"Einstellungen\":\"Ayarlar\",\"Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.\":\"Taslağı Enter veya “Etiket Ekle” devralır. Yalnızca “Profili kaydet” seçeneğiyle kalıcı olarak kaydedilir. Siz yazarken zaten kullanılmış etiketler önerilir.\",\"Ereignisse im Zeitraum\":\"Dönem içindeki olaylar\",\"Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.\":\"Serbestçe adlandırılmış koleksiyonlar oluşturun, modelleri birden fazla gruba atayın ve bir veya daha fazla grubu burada birlikte görüntüleyin.\",\"Erstelle oben zuerst eine eigene Gruppe.\":\"Öncelikle yukarıda kendi grubunuzu oluşturun.\",\"Follower\":\"Takipçiler\",\"GAMING\":\"OYUN\",\"Gesamter Verlauf\":\"Tüm kurs\",\"Grid +\":\"Izgara+\",\"Größe der Bedienelemente\":\"Kontrollerin boyutu\",\"Offizielle Statistik verbinden\":\"Resmi istatistikleri bağlayın\",\"Optionale offizielle Kontostatistik\":\"İsteğe bağlı resmi hesap istatistikleri\",\"Persönlich organisiert\":\"Kişisel olarak organize edilmiş\",\"PiP\":\"pip\",\"Player & Verbindung\":\"Oynatıcı ve bağlantı\",\"Player neu verbinden\":\"Oynatıcıyı yeniden bağlayın\",\"Player-Ecken\":\"Oyuncu köşeleri\",\"Player-Hintergrund\":\"Oyuncu arka planı\",\"Player-Höhe\":\"Oyuncu yüksekliği\",\"Player-Seitenverhältnis\":\"Oyuncu en boy oranı\",\"Position der Informationen\":\"Bilginin yeri\",\"Private Notiz\":\"Özel not\",\"Private Notizen anzeigen\":\"Özel notları görüntüle\",\"Puffergröße\":\"Arabellek boyutu\",\"RG\":\"RG\",\"RoomGrid\":\"RoomGrid\",\"Verbunden\":\"Bağlı\",\"Vollständige Modelstatistik\":\"Tam model istatistikleri\",\"Vorhandene Konto-Daten jetzt einlesen\":\"Mevcut hesap verilerini şimdi okuyun\",\"Weiter ›\":\"Sonraki ›\",\"Zufriedenheit\":\"Memnuniyet\",\"RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.\":\"RoomGrid kişisel Chaturbate hesap değerlerini tahmin edemez. Bu kartlar yalnızca resmi yayıncı istatistiklerini bilinçli olarak kendi kullanıcı adınıza ve jetonunuza bağladıysanız görünür.\",\"RoomGrid Suite\":\"RoomGrid Süit\",\"Seitengröße\":\"Sayfa boyutu\",\"SingleCam\":\"Tek Kamera\",\"SingleCam einstellen\":\"SingleCam’i ayarla\",\"SingleCam Pro\":\"Tek Kamera Pro\",\"SingleCam-Standard wiederherstellen\":\"SingleCam varsayılanını geri yükle\",\"Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.\":\"Bir model için kendi etiketinizi kaydettiğiniz anda, burada ve otomatik tamamlamada görünecektir.\",\"Status im Player anzeigen\":\"Durumu oynatıcıda göster\",\"Status-Aktualisierung\":\"Durum güncellemesi\",\"Stream immer stumm starten\":\"Akışı her zaman sessizce başlat\",\"Tag hinzufügen\":\"Etiket ekle\",\"Tipps letzte Stunde\":\"Son saatteki ipuçları\",\"Token-Guthaben\":\"Jeton bakiyesi\",\"Ton & Ruhe\":\"Ses ve sakin\",\"Ungefähre Onlinezeiten\":\"Yaklaşık çevrimiçi kalma süreleri\",\"Gruppe löschen\":\"Grubu sil\",\"Gruppen umbenennen oder löschen\":\"Grupları yeniden adlandırın veya silin\",\"HD + Gaming\":\"HD+ oyun\",\"IndexedDB\":\"İndekslenmişDB\",\"Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.\":\"Kaydedilen her etiket otomatik olarak aramada, otomatik tamamlamada ve burada doğrudan çoklu filtre olarak bulunur.\",\"Keine Models passen zu diesen Filtern.\":\"Bu filtrelerle eşleşen model yok.\",\"Keine Priorität\":\"Öncelik değil\",\"Konto-Daten werden eingelesen …\":\"Hesap ayrıntıları şu şekilde okunuyor:\",\"Lautstärke pro Model merken\":\"Model başına not hacmi\",\"Layout & sichtbare Bereiche\":\"Düzen ve görünür alanlar\",\"Letzte Messung\":\"Son ölçüm\",\"LIVE\":\"CANLI\",\"lokal · für Suche und Filter\":\"yerel · arama ve filtreler için\",\"Maximale Stream-Lautstärke\":\"Maksimum akış hacmi\",\"Model besitzt alle gewählten Tags\":\"Model, seçilen tüm etiketlerin sahibidir\",\"Model besitzt mindestens einen gewählten Tag\":\"Modelde seçili en az bir etiket var\",\"‹ Zurück\":\"‹ Geri\",\"● 1.2k · #tag\":\"● 1,2k · #etiket\",\"1 Jahr\":\"1 yıl\",\"30 Tage\":\"30 gün\",\"7 Tage\":\"7 gün\",\"90 Tage\":\"90 gün\",\"AA ×\":\"AA ×\",\"AA ✓\":\"AA ✓\",\"Ähnliche Models anzeigen\":\"Benzer modelleri göster\",\"Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.\":\"Tüm değişiklikler yerel olarak kaydedilir ve bir sonraki modele de uygulanır.\",\"Alle eigenen Tags\":\"Tüm özel etiketler\",\"Alle Gruppen\":\"Tüm gruplar\",\"Alt+M / Alt+A\":\"Alt+M / Alt+A\",\"Ältere Chaturbate-Daten anfordern\":\"Daha Eski Chaturbate Verilerini Talep Edin\",\"Automatisch abspielen\":\"Otomatik oynatma\",\"Automatisch erfasst · beim Bearbeiten vollständig organisiert\":\"Otomatik olarak yakalanır · düzenleme sırasında tamamen organize edilir\",\"Gruppen\":\"Gruplar\",\"Schließen\":\"Kapat\",\"Startseite nach eigenen Gruppen filtern\":\"Ana sayfayı kendi gruplarına göre filtrele\",\"RoomGrid Komfortzentrum\":\"RoomGrid kontrol merkezi\",\"Mein Nutzer\":\"Profilim\",\"Mein Nutzer-Dashboard\":\"Profil panom\",\"Modellbibliothek\":\"Model kütüphanesi\",\"Große Modellbibliothek\":\"Geniş model kütüphanesi\",\"Statistikzentrum\":\"İstatistik merkezi\",\"Sitzungen\":\"Oturumlar\",\"Dock + Hover\":\"Panel + üzerine gelme\",\"🔒 Lokal & privat auf diesem Gerät\":\"🔒 Bu cihazda yerel ve gizli\",\"Sortierung\":\"Sıralama\",\"Meiste Sehzeit\":\"En uzun izleme süresi\",\"Zuletzt aktiv\":\"Son etkinlik\",\"Meiste Sitzungen\":\"En fazla oturum\",\"Meiste Zuschauer\":\"En fazla izleyici\",\"Status\":\"Durum\",\"Alle Status\":\"Tüm durumlar\",\"Nur live\":\"Yalnızca canlı\",\"Nur Live\":\"Yalnızca canlı\",\"Offline\":\"Çevrimdışı\",\"Unbekannt\":\"Bilinmiyor\",\"Nur Bibliothek\":\"Yalnızca kütüphane\",\"Pro Seite\":\"Sayfa başına\"}}"}; const ROOMGRID_LATIN_PACK_CACHE = Object.create(null); const ROOMGRID_LATIN_TRANSLATIONS = new Proxy(Object.create(null), { get(_target, language) { if (typeof language !== 'string' || !Object.hasOwn(ROOMGRID_LATIN_PACKED, language)) return undefined; if (!ROOMGRID_LATIN_PACK_CACHE[language]) { try { ROOMGRID_LATIN_PACK_CACHE[language] = JSON.parse(ROOMGRID_LATIN_PACKED[language]); } catch (_) { ROOMGRID_LATIN_PACK_CACHE[language] = { phrases: {}, patterns: {}, visible: {} }; } } return ROOMGRID_LATIN_PACK_CACHE[language]; } }); /* ============================================================= * 0.5. 国际化 / i18n —— 默认跟随浏览器语言,可手动切换 * ============================================================= */ const I18N = { en: { // ---- workstation chrome ---- title: 'Camclient', appTagline: 'All-in-one multiview workstation for rooms, recording, alerts, screenshots, reconnects, and data tools.', addPlaceholder: 'Username ↵', searchPlaceholder: 'Search rooms', invalidUsername: 'Invalid username', hideOffline: 'Hide offline', hidePrivate: 'Hide private', onlyOnline: 'Online only', sortLabel: 'Sort', sortManual: 'Manual', sortStatus: 'By status', sortName: 'By name', sortAdded: 'By added time', sortWatchTime: 'By watch time', sortSessions: 'By sessions', sortLastWatched: 'By last watched', sortFirstWatched: 'By first watched', sortViewerCount: 'By viewer count', sortLastOnline: 'By last online', advancedFilter: 'Advanced filters', filterWatched: 'Watched before', filterNeverWatched: 'Never watched', filterFavorites: 'Favorites only', filterFollowed: 'Following only', filterInView: 'Currently in view', filterHasNotes: 'With notes', filterTagInclude: 'Include tag', filterTagExclude: 'Exclude tag', filterMinWatch: 'Minimum watch time (minutes)', filterMinSessions: 'Minimum sessions', filterLastWatchedDays: 'Watched within days', filterViewerMin: 'Minimum viewers', filterViewerMax: 'Maximum viewers', refreshAll: 'Refresh all', notifyOnline: 'Online alerts', notifyOffline: 'Offline alerts', notifyTitle: 'Desktop notification + card flash when a model goes online', notifyOfflineTitle: 'Desktop notification + card flash when a model goes offline', darkMode: 'Dark mode', darkModeHint: 'Use a dark interface theme. Stream video and audio are unchanged.', darkModeOn: 'Dark mode enabled', darkModeOff: 'Light mode enabled', tipSoundMuteLabel: 'Mute token/tip sounds', tipSoundMuteHint: 'Blocks Chaturbate tip notification sounds only. Stream audio stays on.', tipSoundMuted: 'Token/tip sounds muted', tipSoundUnmuted: 'Token/tip sounds enabled', collapseSidebar: '', viewGrid: 'Grid', viewFocus: 'Focus', viewModeLabel: 'Layout', moreOps: 'More', pureMode: 'Clean', pureModeOn: 'Clean mode on', pureModeOff: 'Exit clean mode', pureModeHint: 'Temporarily hide all controls and overlays. Shortcut: Alt+P / Alt+C. Press Esc to exit.', viewerMode: 'Window first', viewerModeOn: 'Window-first mode on', viewerModeOff: 'Exit window-first', viewerModeHint: 'Hide the app shell and keep room windows visible. Shortcut: Alt+V.', focusThumbsShow: 'Show thumbnails', focusThumbsHide: 'Hide thumbnails', focusThumbsHint: 'Show or hide the thumbnail rail in Focus mode. Shortcut: Alt+T.', focusCustomIndicator: 'Custom', focusRoomManager: 'Choose Focus rooms', focusRoomManagerHint: 'Choose the rooms for Focus mode. The first selected room is the main screen; use ↑/↓ to set the order of the secondary windows.', focusRoomSelected: (n, max) => `${n}/${max} rooms selected`, focusRoomLimit: (max) => `Select up to ${max} rooms. Increase “Rooms per page” for more slots.`, focusRoomMain: 'Main screen', focusRoomRight: 'Right window', focusRoomBottom: 'Bottom window', focusRoomEmpty: 'Empty slot', focusLivePlacement: (place) => `LIVE · ${place}`, focusDragSourceHint: 'Drag this model onto a camera window', focusDropHere: (place) => `Drop here · ${place}`, focusDropReplace: (place) => `Replace exactly: ${place}`, focusPickerTitle: 'Choose model for this slot', focusPickerHint: 'Select a saved model for this exact Focus slot.', gridPickerTitle: 'Choose model for this window', gridPickerHint: 'Select a saved model for this exact grid window. Existing models stay saved.', gridWindowLabel: (n) => `Window ${n}`, gridEmptySlot: (n) => `Empty window ${n}`, gridRemoveFromView: 'Remove from this grid window', gridRemoveHint: 'Only this slot is cleared. The saved model stays in your workspace.', sidebarAssignMenu: 'Show in grid window', sidebarAssignMenuHint: 'Choose the exact active grid window for this model.', gridAssignWindow: (n) => `Grid window ${n}`, gridAssignCurrent: 'Current', gridRemovedFromView: 'Model removed from this grid window.', gridAssigned: (n) => `Model placed in grid window ${n}.`, focusPickerCurrent: 'Current', modelPickerDisplayed: 'Already displayed', modelPickerMoveHint: 'Selecting an already displayed model moves it here, without opening a duplicate.', focusPickerEmpty: 'No saved models in this group.', focusRoomSide: 'Secondary window', focusRoomMoveUp: 'Move up', focusRoomMoveDown: 'Move down', focusRoomAuto: 'Use current page automatically', focusRoomClear: 'Clear custom selection', focusRoomApply: 'Apply Focus layout', focusRoomNoRooms: 'No rooms are available in the current group and filter.', videoFitContain: 'Fit: full image', videoFitCover: 'Fit: fill window', videoFitHint: 'Switch between full image and cropped fill.', playbackSettingsTitle: 'Playback settings', maxStreamHeight: 'Max stream quality', maxQualityAuto: 'Auto / no cap', freeZoomLabel: 'Ctrl/Command + wheel zoom', freeZoomHint: 'Zoom a card with Ctrl/Command + mouse wheel, drag while zoomed, double-click to reset.', pureExitHint: 'Exit clean mode. Shortcut: Alt+P / Alt+C or Esc.', pureExitChip: 'Clean · Alt+P/C / Esc', syncedFromOtherTab: '(synced from another tab)', hintAddInput: 'Type a username and press Enter. Shortcut: /', hintSearchInput: 'Filter visible rooms by username', hintSidebarToggle: 'Show / hide group sidebar', hintVolume: 'Global volume. Per-room mute is still respected.', hintGridSize: 'Base tile size in grid view', hintSort: 'Sort rooms in the current group', hintMainRatio: 'Main-screen height in Focus mode. You can also drag the separator.', hintMainAspect: 'Main-screen aspect ratio in Focus mode', hintThumbSize: 'Thumbnail width in Focus mode', hintGroupTab: (n) => `Switch to ${n}. Drag rooms here to add them to this group.`, hintLibraryTab: 'Show every saved room. The remove button deletes globally in this view.', hintNewGroup: 'Create an isolated group', hintMoreMenu: 'More tools and maintenance', // ---- sidebar ---- groupsHeading: 'GROUPS', groupLibrary: 'All saved', groupAll: 'Default', groupOnline: 'Online now', groupFav: 'Favorites', newGroup: 'New group', newGroupPrompt: 'New group name:', renameGroup: 'Rename', renameGroupPrompt: 'Rename to:', deleteGroup: 'Delete group', deleteGroupConfirm: (n) => `Delete group "${n}"? Rooms stay saved and are only removed from this group.`, statTotal: 'Total', statOnline: 'Online', statMuted: 'Muted', previewLive: 'Live preview', previewLoading: 'Loading preview…', previewOffline: 'Offline', previewPrivate: 'Private or away', previewUnavailable: 'Preview unavailable', previewError: 'Preview could not be loaded', // ---- card ---- opRefresh: 'Refresh', opMuteToggle: 'Toggle mute', opPause: 'Pause', opResume: 'Resume', opScreenshot: 'Screenshot current frame', opRecordStart: 'Record current card', opRecordStop: 'Stop recording', opPiP: 'Picture-in-Picture', opFullscreen: 'Fullscreen', opMoveGroup: 'Add to group', opRemove: 'Remove from current group', opDeleteRoom: 'Delete saved room', opMirror: 'Mirror image', opFlip: 'Flip image', opRotateLeft: 'Rotate left', opRotateRight: 'Rotate right', opResetView: 'Reset view', opOpenRoom: 'Open room', opCopyUsername: 'Copy username', opFavoriteAdd: 'Add favorite', opFavoriteRemove: 'Remove favorite', opMoveToFavorites: 'Move to favorites only', copied: 'Copied', screenshotSaved: 'Screenshot saved', captureFailed: 'Screenshot failed. Browser/CORS may block drawing this stream.', recordingConsent: 'Recording is local and saves this video with its matching audio when the browser exposes it. Use it only when you have permission to save this content. Continue?', recordingStarted: 'Recording started', recordingSaved: 'Recording saved', recordingUnsupported: 'Recording is not supported for this video/browser', recordingSegmentSaved: 'Recording segment saved', recordingFinalSaved: 'Final recording segment saved', recordingPausedSource: 'Recording paused; waiting for the stream to return', recordingResumed: 'Recording resumed', recordingWaiting: 'Recording paused; click to stop', recordingNoData: 'No recording data to save', recordingSettingsSaved: 'Recording settings saved', recordingSegmentPrompt: 'Segment length in minutes (1-180):', recordingBitratePrompt: 'Video bitrate in Mbps (0.5-20):', recordingExitWarnToggle: 'Warn before leaving while recording', recordingExitWarnMessage: 'Recording is still active. Leave anyway?', recordingCenter: 'Recording center', recordingCenterEmpty: 'No active recordings', recordingActive: 'Recording', recordingWaitingShort: 'Waiting for source', recordingSavedSegments: 'Saved segments', recordingDuration: 'Duration', recordingBitrate: 'Bitrate', recordingFormatHint: 'Format: MP4 when supported by this browser; otherwise the best supported fallback.', recordingRecoverPrompt: (n) => `Resume recording intent for ${n} room(s) from the previous session?`, recordCurrentPage: 'Record current page', recordCurrentGroup: 'Record current group', recordOnlineRooms: 'Record online rooms', stopAllRecordings: 'Stop all recordings', showRecordingOnly: 'Show recording only', hideRecordingOnly: 'Show all rooms', batchOpenCurrentPage: 'Open current page rooms', batchMoveCurrentPage: 'Move current page to group', layoutSettings: 'Layout settings', recordingSettingsTitle: 'Recording settings', saveSettings: 'Save settings', backupPanel: 'Config backups', noBackups: 'No config backups found', restoreBackup: 'Restore', deleteBackup: 'Delete', statusHistory: 'Online/offline history', noStatusHistory: 'No history yet', groupRules: 'Group rules', favoriteFirst: 'Keep favorites first', tempUrlManager: 'Temporary URL manager', saveTempUrls: 'Save current temporary URLs', noTempUrls: 'No temporary URLs', sharePanel: 'Share workspace', shareCurrentWorkspace: 'Share current rooms and layout', shareCopyLink: 'Copy share link', shareLinkHint: 'This link restores rooms, groups, and layout settings in another workstation tab.', shareImportPrompt: (n) => `Import shared workspace with ${n} room(s)? Your current config will be backed up first.`, shareImported: 'Shared workspace imported', shareInvalid: 'Shared workspace link is invalid', shortcutPanel: 'Shortcuts', shortcutCaptureHint: 'Click a field and press the new shortcut. Backspace clears that field.', resetShortcuts: 'Reset shortcuts', shortcutCommandPalette: 'Command menu', shortcutGlobalSearch: 'Global model search', shortcutFocusAdd: 'Focus add box', shortcutRefreshAll: 'Refresh all', shortcutGridView: 'Grid view', shortcutFocusView: 'Focus view', shortcutPureMode: 'Clean mode', shortcutViewerMode: 'Window-first mode', shortcutFocusThumbs: 'Toggle thumbnails', shortcutRecordingCenter: 'Recording center', shortcutRecordPage: 'Record current page', // ---- statuses ---- stOnline: 'Online', stOffline: 'Offline', stPrivate: 'Private', stLoading: 'Loading', stError: 'Error', stUnknown: 'Unknown', lastSeen: (t) => `Last online ${t}`, autoDetect: 'Auto-detecting…', // ---- empty state ---- emptyTitle: 'No rooms in this group', emptyHint: 'Add a username at the top, switch group, or clear filters', // ---- toasts / floating button ---- addRoom: (n) => `Add ${n}`, alreadyAdded: (n) => ` ${n} already in workstation`, openWorkstation: 'Open workstation', openWorkstationHere: 'Open here (replace page)', openWorkstationHereConfirm: 'Replace this page with the workstation? You can go back via the browser.', memoryStat: (n) => `${n} rooms saved`, memoryView: 'View saved list', collapseFAB: 'Collapse dock', dockSubtitle: 'MultiCam tools', dockCurrentRoom: (n) => `Current: ${n}`, dockNoRoom: 'No room detected', dockOpen: 'Open workstation', dockOpenHere: 'Open here', dockAdd: 'Add room', dockRemove: 'Remove room', dockRecord: 'Record in workstation', dockScreenshot: 'Screenshot video', dockPip: 'Picture-in-Picture', dockPause: 'Play / pause', dockMute: 'Mute / unmute', dockVideoMissing: 'No playable video found on this page', dockRecordQueued: 'Recording intent queued in workstation', added: 'Added', exists: 'Already exists', addFailed: 'Failed', addedNamed: (n) => `${n} added`, removedNamed: (n) => `${n} removed`, quickAddTitle: 'Add to workstation', quickRemoveTitle: 'Already in workstation — click to remove', // ---- notifications ---- notifyTitleText: 'Model online', notifyBody: (n) => `${n} just went live`, notifyOfflineTitleText: 'Model offline', notifyOfflineBody: (n) => `${n} went offline`, roomTagsLabel: 'Tags', roomTagsMore: (n) => `+${n} more tags`, roomTagsLess: 'Show fewer', audioActivity: 'Audio activity', multicamMeterVisibility: 'Audio meter visibility', multicamMeterVisibilityHint: 'Display only. Audio activity keeps being measured, including while muted.', multicamMeterVisibilityGlobal: 'Show audio meters on camera cards', multicamMeterVisibilityCamera: 'Meter on this camera', multicamMeterVisibilityGlobalOption: 'Use global setting', multicamMeterVisibilityShowOption: 'Always show', multicamMeterVisibilityHideOption: 'Always hide', audioStill: 'No measurable audio at the moment', audioMuted: 'Muted', audioPaused: 'Playback paused', audioUnavailable: 'Audio measurement unavailable or blocked', permDenied: 'Notification permission denied. Card flash still works.\n\nYou can enable it in browser settings.', // ---- batch add ---- manualImport: 'Batch add', manualImportPrompt: 'Paste usernames, one per line or separated by spaces/commas:', importReviewCancel: 'Cancel', manualImportDone: (a, e) => `Batch add complete: ${a} new, ${e} already existed`, // ---- more menu ---- moreMenu: 'More', moreMenuTitle: 'More', menuLanguage: 'Language', languageSwitcherTitle: 'Change the language of RoomGrid and the website', languageSwitcherLocalTitle: 'Change the RoomGrid language. Chaturbate remains in the current website language because no official site language is available.', menuAbout: 'About', menuExport: 'Export config', menuExportUsernames: 'Export usernames.txt', menuCopyUsernames: 'Copy usernames', menuMuteAll: 'Mute all', menuUnmuteAll: 'Unmute all', menuPauseVisible: '⏸ Pause visible', menuResumeVisible: 'Resume visible', menuStopRecordings: 'Stop all recordings', menuRecordingSettings: 'Recording settings', menuRecordingCenter: 'Recording center', menuLayoutSettings: 'Layout settings', menuFocusRoomManager: 'Choose Focus rooms', followingGroupName: 'Following', menuFollowedSync: 'Sync followed models now', menuFollowedAutoOn: '✓ Auto-sync followed models', menuFollowedAutoOff: '☐ Auto-sync followed models', followedSyncHint: 'Import your currently followed Chaturbate models into the persistent Following group. Only that group is synchronized.', followedSyncRunning: 'Syncing followed models…', followedSyncDone: (a, c, r) => `Following sync: ${a} new, ${c} attached, ${r} removed`, followedSyncDisabled: 'Automatic Following sync disabled', followedSyncEnabled: 'Automatic Following sync enabled', followedSyncLogin: 'Following sync needs an active Chaturbate login', followedSyncFailed: 'Following sync failed safely; saved workspace data was not changed', menuPlaybackSettings: 'Playback settings', menuDarkMode: 'Enable dark mode', menuLightMode: 'Disable dark mode', menuTipSound: 'Mute token/tip sounds', menuBackupPanel: 'Config backups', menuStatusHistory: 'Status history', menuStatistics: 'Usage statistics', menuGroupRules: 'Group rules', menuTempUrlManager: 'Temporary URLs', menuShareWorkspace: 'Share workspace', menuShortcutPanel: 'Shortcut panel', menuPureMode: 'Clean mode', menuViewerMode: 'Window-first mode', menuToggleThumbs: 'Toggle thumbnails', menuToggleFit: 'Toggle video fit', menuShortcutHelp: 'Shortcuts / hints', statisticsTitle: 'Usage statistics', statisticsOverview: 'Overview', statisticsRanking: 'Ranking', statisticsDetails: 'Model details', statisticsProfile: 'My profile', statisticsData: 'Data', statisticsLoading: 'Loading local statistics…', statisticsNoData: 'No watch data yet. Statistics start with this version.', statisticsDisabled: 'Statistics are disabled.', statisticsEnable: 'Enable statistics', statisticsDisable: 'Disable statistics', statisticsExport: 'Export statistics', statisticsImport: 'Import statistics', statisticsDeleteAll: 'Delete all statistics', statisticsDeleteModel: 'Delete this model data', statisticsClearConfirm: 'Delete all locally collected watch statistics? Your workspace stays untouched.', statisticsSince: 'Tracking since', statisticsModels: 'Models watched', statisticsSessions: 'Sessions', statisticsWatchTime: 'Watch time', statisticsTopTags: 'Most watched tags', statisticsTopHours: 'Most watched hours', statisticsFirstWatched: 'First watched', statisticsLastWatched: 'Last watched', statisticsObservedOnline: 'Observed online', statisticsBio: 'Public bio', statisticsLinks: 'Public links', statisticsTags: 'Public tags', statisticsNoProfile: 'No public profile data cached yet.', statisticsOpenRoom: 'Open room', statisticsEnableHint: 'Only visible, actively playing cards count. Hover previews do not count.', statisticsImported: 'Statistics imported', statisticsDeleted: 'Statistics deleted', statisticsExported: 'Statistics exported', statisticsLocalOnly: 'Local observation only', statisticsObservedOnly: 'No private messages, tokens, payments, passwords or external tracking are collected.', statisticsNoSchedule: 'Not enough observations for a time estimate', statisticsRefresh: 'Refresh', shortcutsHelp: 'Hover any button/control to see its hint. Common shortcuts can be edited in the shortcut panel.\n\nDefaults:\n/ Focus username input\nr Refresh all\ng Grid view\nf Focus view\nAlt+P or Alt+C Clean mode on/off\nAlt+Shift+C Recording center\nAlt+Shift+R Record current page\nEsc Exit clean mode / fullscreen\nSpace Pause/resume the main screen in Focus view\n←/→ or [/ ] Switch the main screen in Focus view\nDouble-click card Fullscreen', pausedVisible: 'Visible windows paused', resumedVisible: 'Visible windows resumed', menuImport: 'Import config', menuRepairData: 'Repair saved data', repairDone: 'Saved data has been normalized.', menuClearAll: 'Clear all data', clearAllConfirm: 'This will erase ALL rooms, groups and settings. Continue?', langZh: '中文', langEn: 'English', langDe: 'Deutsch', // ---- about panel ---- aboutTitle: 'About', aboutAuthor: 'Author', aboutVersion: 'Version', aboutLicense: 'License', aboutSource: 'Source', aboutDonate: 'If this script saves you time…', aboutDonateBtn: 'Tip in ETH', aboutDonateAddrLabel: 'ETH address', aboutCopyAddr: 'Copy', aboutCopied: 'Copied', aboutClose: 'Close', }, zh: { title: 'Camclient', appTagline: '多房间、多画面、录制、提醒、截图、重连和数据维护的一体化工作台。', addPlaceholder: '输入用户名 ↵', searchPlaceholder: '搜索房间', invalidUsername: '用户名格式不对', hideOffline: '隐藏离线', hidePrivate: '隐藏私密', onlyOnline: '仅看在线', sortLabel: '排序', sortManual: '手动排序', sortStatus: '按状态', sortName: '按名称', sortAdded: '按添加时间', refreshAll: '全部刷新', notifyOnline: '上线提醒', notifyOffline: '离线提醒', notifyTitle: '主播上线时桌面通知 + 卡片闪烁', notifyOfflineTitle: '主播离线时桌面通知 + 卡片闪烁', darkMode: '深色模式', darkModeHint: '使用深色界面主题,房间视频和声音不受影响。', darkModeOn: '已启用深色模式', darkModeOff: '已切换为浅色模式', tipSoundMuteLabel: '静音本地打赏提示音', tipSoundMuteHint: '仅屏蔽 Chaturbate 打赏提示音,房间视频声音不受影响。', tipSoundMuted: '已静音本地打赏提示音', tipSoundUnmuted: '已启用本地打赏提示音', collapseSidebar: '', viewGrid: '平铺', viewFocus: '主屏', viewModeLabel: '视图', moreOps: '更多', pureMode: '纯净', pureModeOn: '已进入纯净模式', pureModeOff: '退出纯净模式', pureModeHint: '暂时隐藏所有工具栏、按钮和覆盖层。快捷键:Alt+P / Alt+C;按 Esc 退出。', viewerMode: '窗口优先', viewerModeOn: '已进入窗口优先模式', viewerModeOff: '退出窗口优先', viewerModeHint: '隐藏应用外壳,尽量把空间留给房间窗口。快捷键:Alt+V。', focusThumbsShow: '显示缩略图', focusThumbsHide: '隐藏缩略图', focusThumbsHint: '显示或隐藏主屏模式的缩略图栏。快捷键:Alt+T。', focusCustomIndicator: '自定义', focusRoomManager: '选择主屏模式窗口', focusRoomManagerHint: '选择主屏模式要显示的窗口。第一个选中的是主屏;用 ↑/↓ 设置副窗口顺序。', focusRoomSelected: (n, max) => `已选择 ${n}/${max} 个窗口`, focusRoomLimit: (max) => `最多选择 ${max} 个窗口。增大“单页窗口数”即可获得更多位置。`, focusRoomMain: '主屏', focusRoomRight: '右侧窗口', focusRoomBottom: '下方窗口', focusRoomEmpty: '空位置', focusLivePlacement: (place) => `直播 · ${place}`, focusDragSourceHint: '将此模型拖到摄像头窗口', focusDropHere: (place) => `放置到 · ${place}`, focusDropReplace: (place) => `精准替换:${place}`, focusPickerTitle: '为此位置选择窗口', focusPickerHint: '为此主屏位置选择已保存窗口。', gridPickerTitle: '为此窗口选择主播', gridPickerHint: '为此网格窗口选择已保存主播。已有主播仍会保留。', gridWindowLabel: (n) => `窗口 ${n}`, gridEmptySlot: (n) => `空窗口 ${n}`, gridRemoveFromView: '从此网格窗口移除', gridRemoveHint: '仅清空此位置。已保存的主播仍保留在工作区中。', sidebarAssignMenu: '显示在网格窗口', sidebarAssignMenuHint: '为此主播选择当前活动的准确网格窗口。', gridAssignWindow: (n) => `网格窗口 ${n}`, gridAssignCurrent: '当前', gridRemovedFromView: '已从此网格窗口移除主播。', gridAssigned: (n) => `主播已放入网格窗口 ${n}。`, focusPickerCurrent: '当前', modelPickerDisplayed: '已显示', modelPickerMoveHint: '选择已显示的主播会将其移到此处,不会重复打开。', focusPickerEmpty: '此分组中没有已保存窗口。', focusRoomSide: '副窗口', focusRoomMoveUp: '上移', focusRoomMoveDown: '下移', focusRoomAuto: '自动使用当前页', focusRoomClear: '清除自定义选择', focusRoomApply: '应用主屏布局', focusRoomNoRooms: '当前分组和筛选条件下没有可用窗口。', videoFitContain: '画面:完整显示', videoFitCover: '画面:填满窗口', videoFitHint: '在完整显示和裁切填满之间切换。', playbackSettingsTitle: '播放设置', maxStreamHeight: '最高画质', maxQualityAuto: '自动 / 不限制', freeZoomLabel: 'Ctrl/Command + 滚轮缩放', freeZoomHint: '按 Ctrl/Command 加滚轮缩放窗口;放大后拖动画面;双击恢复。', pureExitHint: '退出纯净模式。快捷键:Alt+P / Alt+C 或 Esc。', pureExitChip: '纯净 · Alt+P/C / Esc', syncedFromOtherTab: '(来自其它标签页同步)', hintAddInput: '输入用户名后按 Enter 添加。快捷键:/', hintSearchInput: '按用户名过滤当前可见房间', hintSidebarToggle: '显示 / 隐藏分组侧边栏', hintVolume: '全局音量。单房间静音仍然优先。', hintGridSize: '平铺模式下的基础窗口尺寸', hintSort: '当前分组内的房间排序方式', hintMainRatio: '主屏模式下主屏高度,也可以拖动分隔条调整。', hintMainAspect: '主屏模式下主屏宽高比', hintThumbSize: '主屏模式下缩略图宽度', hintGroupTab: (n) => `切换到「${n}」。也可以把房间拖到这里加入该分组。`, hintLibraryTab: '显示所有已保存房间。在这个视图点移除会全局删除。', hintNewGroup: '创建一个互相独立的新分组', hintMoreMenu: '更多工具和维护功能', groupsHeading: '分组', groupLibrary: '全部保存', groupAll: '默认', groupOnline: '在线', groupFav: '收藏', newGroup: '新建分组', newGroupPrompt: '新分组名称:', renameGroup: '重命名', renameGroupPrompt: '重命名为:', deleteGroup: '删除分组', deleteGroupConfirm: (n) => `删除分组「${n}」?房间仍会保留,只会从该分组移除。`, statTotal: '总计', statOnline: '在线', statMuted: '静音', previewLive: '直播预览', previewLoading: '正在加载预览…', previewOffline: '离线', previewPrivate: '私密或离开', previewUnavailable: '暂无预览', previewError: '预览加载失败', opRefresh: '刷新', opMuteToggle: '静音切换', opPause: '暂停', opResume: '继续播放', opScreenshot: '截图当前画面', opRecordStart: '录制当前窗口', opRecordStop: '停止录制', opPiP: '画中画', opFullscreen: '全屏', opMoveGroup: '加入分组', opRemove: '移出当前分组', opDeleteRoom: '彻底删除房间', opMirror: '左右镜像', opFlip: '上下翻转', opRotateLeft: '向左旋转', opRotateRight: '向右旋转', opResetView: '重置画面', opOpenRoom: '打开房间', opCopyUsername: '复制用户名', opFavoriteAdd: '加入收藏', opFavoriteRemove: '取消收藏', opMoveToFavorites: '仅移到收藏', copied: '已复制', screenshotSaved: '截图已保存', captureFailed: '截图失败。浏览器跨域/CORS 可能阻止绘制该视频流。', recordingConsent: '录制只保存在本地,会在浏览器允许时保存该视频及对应音频。请只在你有权保存该内容时使用。继续?', recordingStarted: '已开始录制', recordingSaved: '录制已保存', recordingUnsupported: '当前视频或浏览器不支持录制', recordingSegmentSaved: '录制分段已保存', recordingFinalSaved: '最后录制片段已保存', recordingPausedSource: '录制已暂停,等待视频恢复', recordingResumed: '录制已恢复', recordingWaiting: '录制暂停中,点击停止', recordingNoData: '没有可保存的录制数据', recordingSettingsSaved: '录制设置已保存', recordingSegmentPrompt: '分段时长,单位分钟(1-180):', recordingBitratePrompt: '视频码率,单位 Mbps(0.5-20):', recordingExitWarnToggle: '录制中离开页面前提醒', recordingExitWarnMessage: '仍有录制正在进行,确定离开吗?', recordingCenter: '录制管理中心', recordingCenterEmpty: '暂无正在录制的窗口', recordingActive: '录制中', recordingWaitingShort: '等待视频源', recordingSavedSegments: '已保存分段', recordingDuration: '时长', recordingBitrate: '码率', recordingFormatHint: '格式:浏览器支持时优先 MP4,否则使用当前浏览器可用的最佳格式。', recordingRecoverPrompt: (n) => `检测到上次有 ${n} 个房间的录制意图,是否继续等待视频并恢复录制?`, recordCurrentPage: '录制当前页', recordCurrentGroup: '录制当前分组', recordOnlineRooms: '录制在线房间', stopAllRecordings: '停止所有录制', showRecordingOnly: '只看录制中', hideRecordingOnly: '显示全部房间', batchOpenCurrentPage: '打开当前页房间', batchMoveCurrentPage: '当前页移到分组', layoutSettings: '布局设置', recordingSettingsTitle: '录制设置', saveSettings: '保存设置', backupPanel: '配置备份', noBackups: '没有配置备份', restoreBackup: '恢复', deleteBackup: '删除', statusHistory: '上线/离线历史', noStatusHistory: '暂无历史', groupRules: '分组规则', favoriteFirst: '收藏优先置顶', tempUrlManager: '临时 URL 管理', saveTempUrls: '保存当前临时 URL', noTempUrls: '暂无临时 URL', sharePanel: '分享工作台', shareCurrentWorkspace: '分享当前房间和布局', shareCopyLink: '复制分享链接', shareLinkHint: '这个链接会在另一个工作台标签页里恢复房间、分组和布局设置。', shareImportPrompt: (n) => `导入这个分享工作台中的 ${n} 个房间?当前配置会先自动备份。`, shareImported: '已导入分享工作台', shareInvalid: '分享链接无效', shortcutPanel: '快捷键', shortcutCaptureHint: '点击输入框后按新的快捷键。Backspace 可清空当前项。', resetShortcuts: '恢复默认快捷键', shortcutCommandPalette: '命令菜单', shortcutGlobalSearch: '全局模型搜索', shortcutFocusAdd: '聚焦添加框', shortcutRefreshAll: '刷新全部', shortcutGridView: '平铺视图', shortcutFocusView: '主屏视图', shortcutPureMode: '纯净模式', shortcutViewerMode: '窗口优先模式', shortcutFocusThumbs: '显示 / 隐藏缩略图', shortcutRecordingCenter: '录制管理中心', shortcutRecordPage: '录制当前页', stOnline: '在线', stOffline: '离线', stPrivate: '私密', stLoading: '加载中', stError: '错误', stUnknown: '未知', lastSeen: (t) => `上次在线 ${t}`, autoDetect: '将自动检测上线', emptyTitle: '当前分组没有房间', emptyHint: '在顶部输入用户名添加,或切换分组 / 取消过滤', addRoom: (n) => `加入 ${n}`, alreadyAdded: (n) => ` ${n} 已在工作台`, openWorkstation: '打开工作台', openWorkstationHere: '在当前页打开(覆盖)', openWorkstationHereConfirm: '工作台将取代当前页面,需要时可用浏览器后退返回。继续?', memoryStat: (n) => `已记录 ${n} 个房间`, memoryView: '查看记忆列表', collapseFAB: '收起工具坞', dockSubtitle: 'MultiCam 工具', dockCurrentRoom: (n) => `当前:${n}`, dockNoRoom: '未识别到房间', dockOpen: '打开工作台', dockOpenHere: '当前页打开', dockAdd: '加入房间', dockRemove: '移除房间', dockRecord: '在工作台录制', dockScreenshot: '截图当前视频', dockPip: '画中画', dockPause: '播放 / 暂停', dockMute: '静音 / 取消', dockVideoMissing: '当前页没有找到可操作的视频', dockRecordQueued: '已把录制意图发送到工作台', added: '已加入', exists: '已存在', addFailed: '失败', addedNamed: (n) => `${n} 已加入`, removedNamed: (n) => `${n} 已移除`, quickAddTitle: '加入工作台', quickRemoveTitle: '已在工作台 — 点击移除', notifyTitleText: '主播上线', notifyBody: (n) => `${n} 已开播`, notifyOfflineTitleText: '主播离线', notifyOfflineBody: (n) => `${n} 已离线`, roomTagsLabel: '标签', roomTagsMore: (n) => `+${n} 个更多标签`, roomTagsLess: '收起标签', audioActivity: '声音活动', multicamMeterVisibility: '音频电平显示', multicamMeterVisibilityHint: '仅控制显示;即使静音,音频活动仍会继续测量。', multicamMeterVisibilityGlobal: '在摄像头卡片上显示音频电平', multicamMeterVisibilityCamera: '此摄像头的电平显示', multicamMeterVisibilityGlobalOption: '使用全局设置', multicamMeterVisibilityShowOption: '始终显示', multicamMeterVisibilityHideOption: '始终隐藏', audioStill: '当前没有可测量的声音', audioMuted: '已静音', audioPaused: '播放已暂停', audioUnavailable: '声音检测不可用或被阻止', permDenied: '未获得桌面通知权限,仅卡片闪烁会生效。\n\n你可以去浏览器设置里手动开启。', manualImport: '批量添加', manualImportPrompt: '粘贴用户名,支持一行一个,或用空格/逗号分隔:', importReviewCancel: '取消', manualImportDone: (a, e) => `批量添加完成:新增 ${a},已存在 ${e}`, moreMenu: '更多', moreMenuTitle: '更多', menuLanguage: '语言', menuAbout: '关于', menuExport: '导出配置', menuExportUsernames: '导出用户名 txt', menuCopyUsernames: '复制用户名列表', menuMuteAll: '全部静音', menuUnmuteAll: '取消全部静音', menuPauseVisible: '⏸ 暂停可见窗口', menuResumeVisible: '继续可见窗口', menuStopRecordings: '停止所有录制', menuRecordingSettings: '录制设置', menuRecordingCenter: '录制管理中心', menuLayoutSettings: '布局设置', menuFocusRoomManager: '选择主屏模式窗口', followingGroupName: '关注', menuFollowedSync: '立即同步关注的主播', menuFollowedAutoOn: '✓ 自动同步关注主播', menuFollowedAutoOff: '☐ 自动同步关注主播', followedSyncHint: '把当前关注的 Chaturbate 主播同步到持久化的关注分组。只会维护这个分组。', followedSyncRunning: '正在同步关注主播…', followedSyncDone: (a, c, r) => `关注同步:新增 ${a},加入 ${c},移除 ${r}`, followedSyncDisabled: '已关闭自动关注同步', followedSyncEnabled: '已开启自动关注同步', followedSyncLogin: '关注同步需要登录 Chaturbate', followedSyncFailed: '关注同步安全失败,未修改已保存的工作区数据', menuPlaybackSettings: '播放设置', menuDarkMode: '启用深色模式', menuLightMode: '关闭深色模式', menuTipSound: '静音本地打赏提示音', menuBackupPanel: '配置备份', menuStatusHistory: '状态历史', menuGroupRules: '分组规则', menuTempUrlManager: '临时 URL', menuShareWorkspace: '分享工作台', menuShortcutPanel: '快捷键面板', menuPureMode: '纯净模式', menuViewerMode: '窗口优先模式', menuToggleThumbs: '显示 / 隐藏缩略图', menuToggleFit: '切换画面适应', menuShortcutHelp: '快捷键 / 提示说明', shortcutsHelp: '鼠标停在任何按钮或控件上,会显示即时说明。常用快捷键可以在快捷键面板里修改。\n\n默认:\n/ 聚焦用户名输入框\nr 全部刷新\ng 平铺视图\nf 主屏视图\nAlt+P 或 Alt+C 开关纯净模式\nAlt+Shift+C 录制管理中心\nAlt+Shift+R 录制当前页\nEsc 退出纯净模式 / 全屏\n空格 主屏模式下暂停/继续主屏\n←/→ 或 [/ ] 主屏模式下切换主屏\n双击窗口 全屏', pausedVisible: '已暂停可见窗口', resumedVisible: '已继续可见窗口', menuImport: '导入配置', menuRepairData: '修复保存数据', repairDone: '已完成保存数据规范化。', menuClearAll: '清空所有数据', clearAllConfirm: '将清空所有房间、分组和设置,确定继续?', langZh: '中文', langEn: 'English', langDe: '德语', aboutTitle: '关于', aboutAuthor: '作者', aboutVersion: '版本', aboutLicense: '协议', aboutSource: '源码', aboutDonate: '如果这个脚本帮你节省了时间…', aboutDonateBtn: '请作者一杯咖啡 (ETH)', aboutDonateAddrLabel: 'ETH 地址', aboutCopyAddr: '复制', aboutCopied: '已复制', aboutClose: '关闭', }, }; I18N.de = { ...I18N.en, title: 'Camclient', appTagline: 'All-in-one-Arbeitsplatz für Mehrfachansicht, Aufnahmen, Benachrichtigungen, Screenshots, Reconnects und Datenverwaltung.', addPlaceholder: 'Benutzername ↵', searchPlaceholder: 'Räume suchen', invalidUsername: 'Ungültiger Benutzername', hideOffline: 'Offline ausblenden', hidePrivate: 'Private ausblenden', onlyOnline: 'Nur Online', sortLabel: 'Sortierung', sortManual: 'Manuell', sortStatus: 'Nach Status', sortName: 'Nach Name', sortAdded: 'Nach Hinzufügezeit', sortWatchTime: 'Nach Sehzeit', sortSessions: 'Nach Sitzungen', sortLastWatched: 'Nach zuletzt gesehen', sortFirstWatched: 'Nach zuerst gesehen', sortViewerCount: 'Nach Zuschauerzahl', sortLastOnline: 'Nach zuletzt online', advancedFilter: 'Erweiterte Filter', filterWatched: 'Schon gesehen', filterNeverWatched: 'Noch nie gesehen', filterFavorites: 'Nur Favoriten', filterFollowed: 'Nur Following', filterInView: 'Aktuell im Bild', filterHasNotes: 'Mit Notizen', filterTagInclude: 'Tag einschließen', filterTagExclude: 'Tag ausschließen', filterMinWatch: 'Mindestsehzeit (Minuten)', filterMinSessions: 'Mindestanzahl Sitzungen', filterLastWatchedDays: 'Zuletzt gesehen innerhalb (Tage)', filterViewerMin: 'Mindestens Zuschauer', filterViewerMax: 'Höchstens Zuschauer', refreshAll: 'Alle aktualisieren', notifyOnline: 'Online-Benachrichtigungen', notifyOffline: 'Offline-Benachrichtigungen', notifyTitle: 'Desktop-Benachrichtigung und Kartenblinken, wenn ein Model online geht', notifyOfflineTitle: 'Desktop-Benachrichtigung und Kartenblinken, wenn ein Model offline geht', darkMode: 'Dark Mode', darkModeHint: 'Dunkles Oberflächendesign verwenden. Stream-Video und -Audio bleiben unverändert.', darkModeOn: 'Dark Mode aktiviert', darkModeOff: 'Heller Modus aktiviert', tipSoundMuteLabel: 'Token-/Tip-Töne stummschalten', tipSoundMuteHint: 'Nur Chaturbate-Tip-Hinweistöne blockieren. Stream-Audio bleibt eingeschaltet.', tipSoundMuted: 'Token-/Tip-Töne stummgeschaltet', tipSoundUnmuted: 'Token-/Tip-Töne aktiviert', viewGrid: 'Raster', viewFocus: 'Fokus', viewModeLabel: 'Ansicht', moreOps: 'Mehr', pureMode: 'Clean-Modus', pureModeOn: 'Clean-Modus aktiviert', pureModeOff: 'Clean-Modus beendet', pureModeHint: 'Alle Bedienelemente und Overlays vorübergehend ausblenden. Kürzel: Alt+P / Alt+C. Mit Esc beenden.', viewerMode: 'Fensterpriorität', viewerModeOn: 'Fensterprioritätsmodus aktiviert', viewerModeOff: 'Fensterprioritätsmodus beenden', viewerModeHint: 'App-Rahmen ausblenden und den Raumfenstern mehr Platz geben. Kürzel: Alt+V.', focusThumbsShow: 'Vorschaubilder anzeigen', focusThumbsHide: 'Vorschaubilder ausblenden', focusThumbsHint: 'Vorschaubilder-Leiste im Fokusmodus ein-/ausblenden. Kürzel: Alt+T.', focusCustomIndicator: 'Individuell', focusRoomManager: 'Fokusräume auswählen', focusRoomManagerHint: 'Wähle die Räume für den Fokusmodus. Der erste ausgewählte Raum ist der Hauptbildschirm; mit ↑/↓ legst du die Reihenfolge der Nebenfenster fest.', focusRoomSelected: (n, max) => `${n}/${max} Räume ausgewählt`, focusRoomLimit: (max) => `Wähle höchstens ${max} Räume. Erhöhe „Räume pro Seite“, um mehr Plätze zu erhalten.`, focusRoomMain: 'Hauptbildschirm', focusRoomRight: 'Rechtes Fenster', focusRoomBottom: 'Unteres Fenster', focusRoomEmpty: 'Leerer Platz', focusLivePlacement: (place) => `LIVE · ${place}`, focusDragSourceHint: 'Dieses Model auf ein Kamerafenster ziehen', focusDropHere: (place) => `Hier ablegen · ${place}`, focusDropReplace: (place) => `Ersetzt genau: ${place}`, focusPickerTitle: 'Model für diesen Platz auswählen', focusPickerHint: 'Gespeichertes Model für genau diesen Fokusplatz auswählen.', gridPickerTitle: 'Model für dieses Fenster auswählen', gridPickerHint: 'Gespeichertes Model für genau dieses Rasterfenster auswählen. Vorhandene Models bleiben gespeichert.', gridWindowLabel: (n) => `Fenster ${n}`, gridEmptySlot: (n) => `Leeres Fenster ${n}`, gridRemoveFromView: 'Aus diesem Rasterfenster entfernen', gridRemoveHint: 'Nur dieser Platz wird geleert. Das gespeicherte Model bleibt im Arbeitsbereich.', sidebarAssignMenu: 'In Rasterfenster anzeigen', sidebarAssignMenuHint: 'Das genaue aktive Rasterfenster für dieses Model auswählen.', gridAssignWindow: (n) => `Rasterfenster ${n}`, gridAssignCurrent: 'Aktuell', gridRemovedFromView: 'Model aus diesem Rasterfenster entfernt.', gridAssigned: (n) => `Model in Rasterfenster ${n} platziert.`, focusPickerCurrent: 'Aktuell', modelPickerDisplayed: 'Bereits angezeigt', modelPickerMoveHint: 'Ein bereits angezeigtes Model wird hierher verschoben, nicht doppelt geöffnet.', focusPickerEmpty: 'Keine gespeicherten Models in dieser Gruppe.', focusRoomSide: 'Nebenfenster', focusRoomMoveUp: 'Nach oben', focusRoomMoveDown: 'Nach unten', focusRoomAuto: 'Aktuelle Seite automatisch verwenden', focusRoomClear: 'Individuelle Auswahl löschen', focusRoomApply: 'Fokuslayout anwenden', focusRoomNoRooms: 'Keine Räume in der aktuellen Gruppe bzw. mit diesem Filter verfügbar.', videoFitContain: 'Darstellung: vollständiges Bild', videoFitCover: 'Darstellung: Fenster ausfüllen', videoFitHint: 'Zwischen vollständiger Darstellung und zugeschnittenem Ausfüllen wechseln.', playbackSettingsTitle: 'Wiedergabeeinstellungen', maxStreamHeight: 'Maximale Streamqualität', maxQualityAuto: 'Automatisch / keine Begrenzung', freeZoomLabel: 'Strg/Command + Mausrad zum Zoomen', freeZoomHint: 'Mit Strg/Command + Mausrad zoomen; im Zoom ziehen; Doppelklick setzt zurück.', pureExitHint: 'Clean-Modus beenden. Kürzel: Alt+P / Alt+C oder Esc.', pureExitChip: 'Clean · Alt+P/C / Esc', syncedFromOtherTab: '(mit anderem Tab synchronisiert)', hintAddInput: 'Benutzernamen eingeben und Enter drücken. Kürzel: /', hintSearchInput: 'Sichtbare Räume nach Benutzername filtern', hintSidebarToggle: 'Gruppen-Seitenleiste ein-/ausblenden', hintVolume: 'Globale Lautstärke. Raumbezogene Stummschaltung hat weiterhin Vorrang.', hintGridSize: 'Grundgröße der Kacheln in der Rasteransicht', hintSort: 'Räume in der aktuellen Gruppe sortieren', hintMainRatio: 'Höhe des Hauptbildschirms im Fokusmodus. Der Trenner kann auch gezogen werden.', hintMainAspect: 'Seitenverhältnis des Hauptbildschirms im Fokusmodus', hintThumbSize: 'Breite der Vorschaubilder im Fokusmodus', hintGroupTab: (n) => `Zu ${n} wechseln. Räume hierher ziehen, um sie dieser Gruppe hinzuzufügen.`, hintLibraryTab: 'Alle gespeicherten Räume anzeigen. Entfernen löscht den Raum in dieser Ansicht global.', hintNewGroup: 'Eine unabhängige Gruppe erstellen', hintMoreMenu: 'Weitere Werkzeuge und Wartungsfunktionen', groupsHeading: 'GRUPPEN', groupLibrary: 'Alle gespeichert', groupAll: 'Standard', groupOnline: 'Jetzt online', groupFav: 'Favoriten', newGroup: 'Neue Gruppe', newGroupPrompt: 'Name der neuen Gruppe:', renameGroup: 'Umbenennen', renameGroupPrompt: 'Umbenennen in:', deleteGroup: 'Gruppe löschen', deleteGroupConfirm: (n) => `Gruppe „${n}“ löschen? Räume bleiben gespeichert und werden nur aus dieser Gruppe entfernt.`, statTotal: 'Gesamt', statOnline: 'Online', statMuted: 'Stumm', previewLive: 'Live-Vorschau', previewLoading: 'Vorschau wird geladen …', previewOffline: 'Offline', previewPrivate: 'Privat oder abwesend', previewUnavailable: 'Keine Vorschau verfügbar', previewError: 'Vorschau konnte nicht geladen werden', opRefresh: 'Aktualisieren', opMuteToggle: 'Stummschaltung umschalten', opPause: 'Pausieren', opResume: 'Fortsetzen', opScreenshot: 'Aktuelles Bild speichern', opRecordStart: 'Aktuelle Kachel aufnehmen', opRecordStop: 'Aufnahme stoppen', opPiP: 'Bild-im-Bild', opFullscreen: 'Vollbild', opMoveGroup: 'Zur Gruppe hinzufügen', opRemove: 'Aus aktueller Gruppe entfernen', opDeleteRoom: 'Gespeicherten Raum löschen', opMirror: 'Bild spiegeln', opFlip: 'Bild vertikal drehen', opRotateLeft: 'Nach links drehen', opRotateRight: 'Nach rechts drehen', opResetView: 'Ansicht zurücksetzen', opOpenRoom: 'Raum öffnen', opCopyUsername: 'Benutzernamen kopieren', opFavoriteAdd: 'Zu Favoriten hinzufügen', opFavoriteRemove: 'Aus Favoriten entfernen', opMoveToFavorites: 'Nur zu Favoriten verschieben', copied: 'Kopiert', screenshotSaved: 'Screenshot gespeichert', captureFailed: 'Screenshot fehlgeschlagen. Browser/CORS kann das Zeichnen dieses Streams blockieren.', recordingConsent: 'Die Aufnahme wird lokal gespeichert und enthält dieses Video mit dem zugehörigen Audio, sofern der Browser dies zulässt. Verwende die Funktion nur, wenn du die Erlaubnis zum Speichern hast. Fortfahren?', recordingStarted: 'Aufnahme gestartet', recordingSaved: 'Aufnahme gespeichert', recordingUnsupported: 'Aufnahme wird von diesem Video oder Browser nicht unterstützt', recordingSegmentSaved: 'Aufnahmesegment gespeichert', recordingFinalSaved: 'Letztes Aufnahmesegment gespeichert', recordingPausedSource: 'Aufnahme pausiert; warte auf die Rückkehr des Streams', recordingResumed: 'Aufnahme fortgesetzt', recordingWaiting: 'Aufnahme pausiert; zum Beenden klicken', recordingNoData: 'Keine Aufnahmedaten zum Speichern vorhanden', recordingSettingsSaved: 'Aufnahmeeinstellungen gespeichert', recordingSegmentPrompt: 'Segmentlänge in Minuten (1–180):', recordingBitratePrompt: 'Videobitrate in Mbps (0,5–20):', recordingExitWarnToggle: 'Beim Verlassen während einer Aufnahme warnen', recordingExitWarnMessage: 'Eine Aufnahme ist noch aktiv. Trotzdem verlassen?', recordingCenter: 'Aufnahmeverwaltung', recordingCenterEmpty: 'Keine aktiven Aufnahmen', recordingActive: 'Aufnahme läuft', recordingWaitingShort: 'Warte auf Quelle', recordingSavedSegments: 'Gespeicherte Segmente', recordingDuration: 'Dauer', recordingBitrate: 'Bitrate', recordingFormatHint: 'Format: MP4, wenn vom Browser unterstützt; sonst das beste verfügbare Ersatzformat.', recordingRecoverPrompt: (n) => `Aufnahme für ${n} Raum/Räume aus der vorherigen Sitzung fortsetzen?`, recordCurrentPage: 'Aktuelle Seite aufnehmen', recordCurrentGroup: 'Aktuelle Gruppe aufnehmen', recordOnlineRooms: 'Online-Räume aufnehmen', stopAllRecordings: 'Alle Aufnahmen stoppen', showRecordingOnly: 'Nur Aufnahmen anzeigen', hideRecordingOnly: 'Alle Räume anzeigen', batchOpenCurrentPage: 'Räume der aktuellen Seite öffnen', batchMoveCurrentPage: 'Räume der aktuellen Seite in Gruppe verschieben', layoutSettings: 'Layouteinstellungen', recordingSettingsTitle: 'Aufnahmeeinstellungen', saveSettings: 'Einstellungen speichern', backupPanel: 'Konfigurationssicherungen', noBackups: 'Keine Konfigurationssicherungen gefunden', restoreBackup: 'Wiederherstellen', deleteBackup: 'Löschen', statusHistory: 'Online-/Offline-Verlauf', noStatusHistory: 'Noch kein Verlauf', menuStatistics: 'Nutzungsstatistik', statisticsTitle: 'Nutzungsstatistik', statisticsOverview: 'Übersicht', statisticsRanking: 'Ranking', statisticsDetails: 'Modelldetails', statisticsProfile: 'Mein Profil', statisticsData: 'Daten', statisticsLoading: 'Lokale Statistik wird geladen…', statisticsNoData: 'Noch keine Sehzeit. Die Statistik beginnt mit dieser Version.', statisticsDisabled: 'Statistik ist deaktiviert.', statisticsEnable: 'Statistik aktivieren', statisticsDisable: 'Statistik deaktivieren', statisticsExport: 'Statistik exportieren', statisticsImport: 'Statistik importieren', statisticsDeleteAll: 'Alle Statistiken löschen', statisticsDeleteModel: 'Modelldaten löschen', statisticsClearConfirm: 'Alle lokal gesammelten Sehstatistiken löschen? Der Arbeitsbereich bleibt unverändert.', statisticsSince: 'Aufzeichnung seit', statisticsModels: 'Gesehene Models', statisticsSessions: 'Sitzungen', statisticsWatchTime: 'Sehzeit', statisticsTopTags: 'Meistgesehene Tags', statisticsTopHours: 'Meistgesehene Stunden', statisticsFirstWatched: 'Zuerst gesehen', statisticsLastWatched: 'Zuletzt gesehen', statisticsObservedOnline: 'Online beobachtet', statisticsBio: 'Öffentliche Bio', statisticsLinks: 'Öffentliche Links', statisticsTags: 'Öffentliche Tags', statisticsNoProfile: 'Noch keine öffentlichen Profildaten zwischengespeichert.', statisticsOpenRoom: 'Raum öffnen', statisticsEnableHint: 'Nur sichtbare, aktiv laufende Karten zählen. Hover-Vorschauen zählen nicht.', statisticsImported: 'Statistik importiert', statisticsDeleted: 'Statistik gelöscht', statisticsExported: 'Statistik exportiert', statisticsLocalOnly: 'Nur lokale Beobachtung', statisticsObservedOnly: 'Keine privaten Nachrichten, Tokens, Zahlungen, Passwörter oder externe Nachverfolgung werden erfasst.', statisticsNoSchedule: 'Zu wenige Beobachtungen für eine Zeitschätzung', statisticsRefresh: 'Aktualisieren', groupRules: 'Gruppenregeln', favoriteFirst: 'Favoriten oben halten', tempUrlManager: 'Verwaltung temporärer URLs', saveTempUrls: 'Aktuelle temporäre URLs speichern', noTempUrls: 'Keine temporären URLs', sharePanel: 'Arbeitsbereich teilen', shareCurrentWorkspace: 'Aktuelle Räume und Layout teilen', shareCopyLink: 'Freigabelink kopieren', shareLinkHint: 'Dieser Link stellt Räume, Gruppen und Layouteinstellungen in einem anderen Arbeitsbereich-Tab wieder her.', shareImportPrompt: (n) => `Geteilten Arbeitsbereich mit ${n} Raum/Räumen importieren? Deine aktuelle Konfiguration wird zuerst gesichert.`, shareImported: 'Arbeitsbereich importiert', shareInvalid: 'Freigabelink ist ungültig', shortcutPanel: 'Tastenkürzel', shortcutCaptureHint: 'Feld anklicken und neues Tastenkürzel drücken. Backspace leert das Feld.', resetShortcuts: 'Tastenkürzel zurücksetzen', shortcutCommandPalette: 'Befehlsmenü', shortcutGlobalSearch: 'Globale Modellsuche', shortcutFocusAdd: 'Eingabefeld fokussieren', shortcutRefreshAll: 'Alle aktualisieren', shortcutGridView: 'Rasteransicht', shortcutFocusView: 'Fokusansicht', shortcutPureMode: 'Clean-Modus', shortcutViewerMode: 'Fensterprioritätsmodus', shortcutFocusThumbs: 'Vorschaubilder umschalten', shortcutRecordingCenter: 'Aufnahmeverwaltung', shortcutRecordPage: 'Aktuelle Seite aufnehmen', stOnline: 'Online', stOffline: 'Offline', stPrivate: 'Privat', stLoading: 'Wird geladen', stError: 'Fehler', stUnknown: 'Unbekannt', lastSeen: (value) => `Zuletzt online ${value}`, autoDetect: 'Erkennung läuft automatisch…', emptyTitle: 'Keine Räume in dieser Gruppe', emptyHint: 'Oben einen Benutzernamen hinzufügen, Gruppe wechseln oder Filter löschen', addRoom: (n) => `${n} hinzufügen`, alreadyAdded: (n) => `${n} ist bereits im Arbeitsbereich`, openWorkstation: 'Arbeitsbereich öffnen', openWorkstationHere: 'Hier öffnen (Seite ersetzen)', openWorkstationHereConfirm: 'Diese Seite wird durch den Arbeitsbereich ersetzt. Mit der Browser-Zurückfunktion kommst du zurück. Fortfahren?', memoryStat: (n) => `${n} Räume gespeichert`, memoryView: 'Gespeicherte Liste anzeigen', collapseFAB: 'Dock einklappen', dockSubtitle: 'MultiCam-Werkzeuge', dockCurrentRoom: (n) => `Aktuell: ${n}`, dockNoRoom: 'Kein Raum erkannt', dockOpen: 'Arbeitsbereich öffnen', dockOpenHere: 'Hier öffnen', dockAdd: 'Raum hinzufügen', dockRemove: 'Raum entfernen', dockRecord: 'Im Arbeitsbereich aufnehmen', dockScreenshot: 'Video-Screenshot', dockPip: 'Bild-im-Bild', dockPause: 'Wiedergabe / Pause', dockMute: 'Stumm / Ton an', dockVideoMissing: 'Kein abspielbares Video auf dieser Seite gefunden', dockRecordQueued: 'Aufnahmeauftrag an den Arbeitsbereich übergeben', added: 'Hinzugefügt', exists: 'Bereits vorhanden', addFailed: 'Fehlgeschlagen', addedNamed: (n) => `${n} hinzugefügt`, removedNamed: (n) => `${n} entfernt`, quickAddTitle: 'Zum Arbeitsbereich hinzufügen', quickRemoveTitle: 'Bereits im Arbeitsbereich – zum Entfernen klicken', notifyTitleText: 'Model ist online', notifyBody: (n) => `${n} ist jetzt live`, notifyOfflineTitleText: 'Model ist offline', notifyOfflineBody: (n) => `${n} ist jetzt offline`, roomTagsLabel: 'Tags', roomTagsMore: (n) => `+${n} weitere Tags`, roomTagsLess: 'Weniger anzeigen', audioActivity: 'Audioaktivität', multicamMeterVisibility: 'Audiopegelanzeige', multicamMeterVisibilityHint: 'Nur Anzeige: Die Audioaktivität wird auch bei stummem Ton weiter gemessen.', multicamMeterVisibilityGlobal: 'Audiopegel auf den Kamerakarten anzeigen', multicamMeterVisibilityCamera: 'Pegelanzeige dieser Kamera', multicamMeterVisibilityGlobalOption: 'Globale Einstellung', multicamMeterVisibilityShowOption: 'Immer einblenden', multicamMeterVisibilityHideOption: 'Immer ausblenden', audioStill: 'Zurzeit kein messbarer Audiopegel', audioMuted: 'Stummgeschaltet', audioPaused: 'Wiedergabe pausiert', audioUnavailable: 'Audiopegelmessung nicht verfügbar oder blockiert', permDenied: 'Berechtigung für Desktop-Benachrichtigungen verweigert. Das Kartenblinken funktioniert weiterhin.\n\nDu kannst die Berechtigung in den Browsereinstellungen aktivieren.', manualImport: 'Mehrere hinzufügen', manualImportPrompt: 'Benutzernamen einfügen, jeweils eine Zeile oder durch Leerzeichen/Kommas getrennt:', importReviewCancel: 'Abbrechen', manualImportDone: (a, e) => `Mehrere hinzufügen abgeschlossen: ${a} neu, ${e} bereits vorhanden`, moreMenu: 'Mehr', moreMenuTitle: 'Mehr', menuLanguage: 'Sprache', menuAbout: 'Über', menuExport: 'Konfiguration exportieren', menuExportUsernames: 'usernames.txt exportieren', menuCopyUsernames: 'Benutzernamen kopieren', menuMuteAll: 'Alle stummschalten', menuUnmuteAll: 'Stummschaltung für alle aufheben', menuPauseVisible: '⏸ Sichtbare pausieren', menuResumeVisible: 'Sichtbare fortsetzen', menuStopRecordings: 'Alle Aufnahmen stoppen', menuRecordingSettings: 'Aufnahmeeinstellungen', menuRecordingCenter: 'Aufnahmeverwaltung', menuLayoutSettings: 'Layouteinstellungen', menuFocusRoomManager: 'Fokusräume auswählen', followingGroupName: 'Follower', menuFollowedSync: 'Follower jetzt synchronisieren', menuFollowedAutoOn: '✓ Follower automatisch synchronisieren', menuFollowedAutoOff: '☐ Follower automatisch synchronisieren', followedSyncHint: 'Aktuell gefolgte Chaturbate-Models in die dauerhafte Gruppe „Follower“ übernehmen. Nur diese Gruppe wird synchronisiert.', followedSyncRunning: 'Follower werden synchronisiert …', followedSyncDone: (a, c, r) => `Follower-Sync: ${a} neu, ${c} zugeordnet, ${r} entfernt`, followedSyncDisabled: 'Automatischer Follower-Sync deaktiviert', followedSyncEnabled: 'Automatischer Follower-Sync aktiviert', followedSyncLogin: 'Für den Follower-Sync musst du bei Chaturbate angemeldet sein', followedSyncFailed: 'Follower-Sync sicher fehlgeschlagen – gespeicherte Arbeitsbereich-Daten wurden nicht geändert', menuPlaybackSettings: 'Wiedergabeeinstellungen', menuDarkMode: 'Dark Mode aktivieren', menuLightMode: 'Dark Mode deaktivieren', menuTipSound: 'Token-/Tip-Töne stummschalten', menuBackupPanel: 'Konfigurationssicherungen', menuStatusHistory: 'Statusverlauf', menuGroupRules: 'Gruppenregeln', menuTempUrlManager: 'Temporäre URLs', menuShareWorkspace: 'Arbeitsbereich teilen', menuShortcutPanel: 'Tastenkürzel', menuPureMode: 'Clean-Modus', menuViewerMode: 'Fensterprioritätsmodus', menuToggleThumbs: 'Vorschaubilder umschalten', menuToggleFit: 'Videoanpassung umschalten', menuShortcutHelp: 'Tastenkürzel / Hinweise', shortcutsHelp: 'Mit dem Mauszeiger über eine Schaltfläche oder ein Steuerelement fahren, um den Hinweis zu sehen. Häufige Tastenkürzel können in der Tastenkürzelverwaltung geändert werden.\n\nStandard:\n/ Benutzernamen-Eingabefeld fokussieren\nr Alle aktualisieren\ng Rasteransicht\nf Fokusansicht\nAlt+P oder Alt+C Clean-Modus ein/aus\nAlt+Shift+C Aufnahmeverwaltung\nAlt+Shift+R Aktuelle Seite aufnehmen\nEsc Clean-Modus / Vollbild beenden\nLeertaste Hauptbildschirm im Fokusmodus pausieren/fortsetzen\n←/→ oder [/ ] Hauptbildschirm im Fokusmodus wechseln\nDoppelklick auf Kachel Vollbild', pausedVisible: 'Sichtbare Fenster pausiert', resumedVisible: 'Sichtbare Fenster fortgesetzt', menuImport: 'Konfiguration importieren', menuRepairData: 'Gespeicherte Daten reparieren', repairDone: 'Gespeicherte Daten wurden normalisiert.', menuClearAll: 'Alle Daten löschen', clearAllConfirm: 'Dadurch werden ALLE Räume, Gruppen und Einstellungen gelöscht. Fortfahren?', langZh: '中文', langEn: 'English', langDe: 'Deutsch', aboutTitle: 'Über', aboutAuthor: 'Autor', aboutVersion: 'Version', aboutLicense: 'Lizenz', aboutSource: 'Quelle', aboutDonate: 'Wenn dieses Script dir Zeit spart…', aboutDonateBtn: 'Mit ETH unterstützen', aboutDonateAddrLabel: 'ETH-Adresse', aboutCopyAddr: 'Kopieren', aboutCopied: 'Kopiert', aboutClose: 'Schließen', }; const FOLLOWING_UI = { en: { nav: 'Following', title: 'Models you follow', description: 'Read-only list from your official Chaturbate Following pages. Camclient does not follow or unfollow anyone here.', refresh: 'Refresh list', search: 'Search followed models', status: 'Status', all: 'All', live: 'Live', offline: 'Offline', sortName: 'Name A–Z', sortViewers: 'Most viewers', sortRecent: 'Recently online', cancel: 'Cancel loading', loading: 'Loading your official Following pages…', loadingPages: 'Pages read: {done}', results: '{count} models · {pages} pages read', partial: 'Partial list: {count} models from {pages} pages. Some pages could not be read; retry to continue.', login: 'Chaturbate sign-in is required. Open the official page, sign in, then retry.', failed: 'The Following list could not be loaded. Your existing data was not changed.', parser: 'The page format was not recognized, so this is not being treated as an empty list.', empty: 'No followed models were found on the recognized official pages.', noMatch: 'No followed models match this search and status.', open: 'Open in SingleCam', profile: 'Official profile' }, de: { nav: 'Gefolgt', title: 'Models, denen du folgst', description: 'Schreibgeschützte Liste deiner offiziellen Chaturbate-Following-Seiten. Camclient folgt hier niemandem und beendet keine Follow-Beziehung.', refresh: 'Liste aktualisieren', search: 'Gefolgte Models suchen', status: 'Status', all: 'Alle', live: 'Live', offline: 'Offline', sortName: 'Name A–Z', sortViewers: 'Meiste Zuschauer', sortRecent: 'Zuletzt online', cancel: 'Laden abbrechen', loading: 'Offizielle Following-Seiten werden geladen …', loadingPages: 'Gelesene Seiten: {done}', results: '{count} Models · {pages} Seiten gelesen', partial: 'Teilliste: {count} Models aus {pages} Seiten. Einige Seiten konnten nicht gelesen werden; versuche es erneut.', login: 'Bitte melde dich bei Chaturbate an und lade die Liste danach erneut.', failed: 'Die Following-Liste konnte nicht geladen werden. Deine gespeicherten Daten wurden nicht geändert.', parser: 'Das Seitenformat wurde nicht erkannt. Deshalb wird dies nicht als leere Liste behandelt.', empty: 'Auf den erkannten offiziellen Seiten wurden keine gefolgten Models gefunden.', noMatch: 'Keine gefolgten Models passen zu Suche und Status.', open: 'In SingleCam öffnen', profile: 'Offizielles Profil' }, zh: { nav: '已关注', title: '你关注的主播', description: '只读显示 Chaturbate 官方关注页面。Camclient 不会在此关注或取消关注任何人。', refresh: '刷新列表', search: '搜索已关注主播', status: '状态', all: '全部', live: '直播中', offline: '离线', sortName: '名称 A–Z', sortViewers: '观众最多', sortRecent: '最近在线', cancel: '取消加载', loading: '正在读取官方关注页面…', loadingPages: '已读取页面:{done}', results: '{count} 位主播 · 已读取 {pages} 页', partial: '部分列表:{pages} 页中有 {count} 位主播。部分页面无法读取;请重试。', login: '请先登录 Chaturbate,然后重试。', failed: '无法读取关注列表,现有数据未更改。', parser: '无法识别页面格式,因此不会将其误报为空列表。', empty: '在已识别的官方页面中没有找到关注的主播。', noMatch: '没有符合搜索和状态的关注主播。', open: '在 SingleCam 中打开', profile: '官方资料' }, es: { nav: 'Siguiendo', title: 'Modelos que sigues', description: 'Lista de solo lectura de tus páginas oficiales de Chaturbate. Camclient no sigue ni deja de seguir a nadie aquí.', refresh: 'Actualizar lista', search: 'Buscar modelos seguidos', status: 'Estado', all: 'Todos', live: 'En directo', offline: 'Desconectado', sortName: 'Nombre A–Z', sortViewers: 'Más espectadores', sortRecent: 'En línea recientemente', cancel: 'Cancelar carga', loading: 'Cargando tus páginas oficiales de seguidos…', loadingPages: 'Páginas leídas: {done}', results: '{count} modelos · {pages} páginas leídas', partial: 'Lista parcial: {count} modelos de {pages} páginas. Algunas páginas no se pudieron leer; vuelve a intentarlo.', login: 'Inicia sesión en Chaturbate y vuelve a intentarlo.', failed: 'No se pudo cargar la lista. No se han cambiado tus datos.', parser: 'No se reconoció el formato de la página; no se mostrará como lista vacía.', empty: 'No se encontraron modelos seguidos en las páginas oficiales reconocidas.', noMatch: 'Ningún modelo seguido coincide con la búsqueda y el estado.', open: 'Abrir en SingleCam', profile: 'Perfil oficial' }, pt: { nav: 'Seguindo', title: 'Modelos que você segue', description: 'Lista somente leitura das páginas oficiais do Chaturbate. O Camclient não segue nem deixa de seguir ninguém aqui.', refresh: 'Atualizar lista', search: 'Buscar modelos seguidos', status: 'Status', all: 'Todos', live: 'Ao vivo', offline: 'Offline', sortName: 'Nome A–Z', sortViewers: 'Mais espectadores', sortRecent: 'Online recentemente', cancel: 'Cancelar carregamento', loading: 'Carregando suas páginas oficiais de seguindo…', loadingPages: 'Páginas lidas: {done}', results: '{count} modelos · {pages} páginas lidas', partial: 'Lista parcial: {count} modelos em {pages} páginas. Algumas páginas não puderam ser lidas; tente novamente.', login: 'Entre no Chaturbate e tente novamente.', failed: 'Não foi possível carregar a lista. Seus dados não foram alterados.', parser: 'O formato da página não foi reconhecido; ela não será tratada como lista vazia.', empty: 'Nenhum modelo seguido foi encontrado nas páginas oficiais reconhecidas.', noMatch: 'Nenhum modelo seguido corresponde à busca e ao status.', open: 'Abrir no SingleCam', profile: 'Perfil oficial' }, fr: { nav: 'Abonnements', title: 'Modèles suivis', description: 'Liste en lecture seule issue de tes pages officielles Chaturbate. Camclient ne suit et ne désabonne personne ici.', refresh: 'Actualiser la liste', search: 'Rechercher les modèles suivis', status: 'Statut', all: 'Tous', live: 'En direct', offline: 'Hors ligne', sortName: 'Nom A–Z', sortViewers: 'Plus de spectateurs', sortRecent: 'Récemment en ligne', cancel: 'Annuler le chargement', loading: 'Chargement des pages officielles…', loadingPages: 'Pages lues : {done}', results: '{count} modèles · {pages} pages lues', partial: 'Liste partielle : {count} modèles sur {pages} pages. Certaines pages sont illisibles ; réessaie.', login: 'Connecte-toi à Chaturbate puis réessaie.', failed: 'Impossible de charger la liste. Tes données ne sont pas modifiées.', parser: 'Format de page non reconnu ; la liste ne sera pas présentée comme vide.', empty: 'Aucun modèle suivi sur les pages officielles reconnues.', noMatch: 'Aucun modèle suivi ne correspond à cette recherche et à ce statut.', open: 'Ouvrir dans SingleCam', profile: 'Profil officiel' }, it: { nav: 'Seguiti', title: 'Modelle che segui', description: 'Elenco di sola lettura dalle tue pagine ufficiali Chaturbate. Camclient non segue né smette di seguire nessuno.', refresh: 'Aggiorna elenco', search: 'Cerca modelli seguiti', status: 'Stato', all: 'Tutti', live: 'In diretta', offline: 'Offline', sortName: 'Nome A–Z', sortViewers: 'Più spettatori', sortRecent: 'Online di recente', cancel: 'Annulla caricamento', loading: 'Caricamento delle pagine ufficiali…', loadingPages: 'Pagine lette: {done}', results: '{count} modelli · {pages} pagine lette', partial: 'Elenco parziale: {count} modelli su {pages} pagine. Alcune pagine non sono leggibili; riprova.', login: 'Accedi a Chaturbate e riprova.', failed: 'Impossibile caricare l’elenco. I tuoi dati non sono stati modificati.', parser: 'Formato della pagina non riconosciuto; non verrà mostrato come elenco vuoto.', empty: 'Nessun modello seguito nelle pagine ufficiali riconosciute.', noMatch: 'Nessun modello seguito corrisponde alla ricerca e allo stato.', open: 'Apri in SingleCam', profile: 'Profilo ufficiale' }, ru: { nav: 'Подписки', title: 'Модели, на которых вы подписаны', description: 'Список только для чтения с официальных страниц Chaturbate. Camclient здесь не подписывает и не отменяет подписки.', refresh: 'Обновить список', search: 'Искать модели', status: 'Статус', all: 'Все', live: 'В эфире', offline: 'Не в сети', sortName: 'Имя А–Я', sortViewers: 'Больше зрителей', sortRecent: 'Недавно в сети', cancel: 'Отменить загрузку', loading: 'Загрузка официальных страниц подписок…', loadingPages: 'Прочитано страниц: {done}', results: '{count} моделей · прочитано страниц: {pages}', partial: 'Частичный список: {count} моделей на {pages} страницах. Некоторые страницы не прочитаны; попробуйте снова.', login: 'Войдите в Chaturbate и повторите попытку.', failed: 'Не удалось загрузить список. Ваши данные не изменены.', parser: 'Формат страницы не распознан; список не будет ошибочно показан пустым.', empty: 'На распознанных официальных страницах подписок модели не найдены.', noMatch: 'Нет моделей, соответствующих поиску и статусу.', open: 'Открыть в SingleCam', profile: 'Официальный профиль' }, ja: { nav: 'フォロー中', title: 'フォローしているモデル', description: 'Chaturbate公式フォローページの読み取り専用一覧です。Camclientからフォローや解除は行いません。', refresh: '一覧を更新', search: 'フォロー中のモデルを検索', status: '状態', all: 'すべて', live: '配信中', offline: 'オフライン', sortName: '名前 A–Z', sortViewers: '視聴者が多い順', sortRecent: '最近オンライン', cancel: '読み込みを中止', loading: '公式フォローページを読み込み中…', loadingPages: '読み込み済みページ: {done}', results: '{count} モデル · {pages} ページ読み込み', partial: '一部の一覧: {pages} ページから {count} モデルを取得。一部のページを読めませんでした。再試行してください。', login: 'Chaturbateにログインしてから再試行してください。', failed: '一覧を読み込めませんでした。保存データは変更されていません。', parser: 'ページ形式を認識できないため、空の一覧として扱いません。', empty: '認識できた公式ページにフォロー中のモデルはありません。', noMatch: '検索条件と状態に一致するモデルはありません。', open: 'SingleCamで開く', profile: '公式プロフィール' }, pl: { nav: 'Obserwowani', title: 'Obserwowane modele', description: 'Lista tylko do odczytu z oficjalnych stron Chaturbate. Camclient nie obserwuje ani nie przestaje obserwować modeli.', refresh: 'Odśwież listę', search: 'Szukaj obserwowanych modeli', status: 'Status', all: 'Wszystkie', live: 'Na żywo', offline: 'Offline', sortName: 'Nazwa A–Z', sortViewers: 'Najwięcej widzów', sortRecent: 'Ostatnio online', cancel: 'Anuluj ładowanie', loading: 'Wczytywanie oficjalnych stron obserwowanych…', loadingPages: 'Odczytane strony: {done}', results: '{count} modeli · odczytano {pages} stron', partial: 'Częściowa lista: {count} modeli z {pages} stron. Niektórych stron nie udało się odczytać; spróbuj ponownie.', login: 'Zaloguj się w Chaturbate i spróbuj ponownie.', failed: 'Nie udało się wczytać listy. Twoje dane nie zostały zmienione.', parser: 'Nie rozpoznano formatu strony, więc lista nie zostanie uznana za pustą.', empty: 'Na rozpoznanych oficjalnych stronach nie znaleziono obserwowanych modeli.', noMatch: 'Żaden obserwowany model nie pasuje do wyszukiwania i statusu.', open: 'Otwórz w SingleCam', profile: 'Profil oficjalny' }, ro: { nav: 'Urmărite', title: 'Modelele pe care le urmărești', description: 'Listă doar pentru citire din paginile tale oficiale Chaturbate. Camclient nu urmărește și nu elimină urmăriri aici.', refresh: 'Reîmprospătează lista', search: 'Caută modele urmărite', status: 'Stare', all: 'Toate', live: 'Live', offline: 'Offline', sortName: 'Nume A–Z', sortViewers: 'Cei mai mulți spectatori', sortRecent: 'Online recent', cancel: 'Anulează încărcarea', loading: 'Se încarcă paginile oficiale…', loadingPages: 'Pagini citite: {done}', results: '{count} modele · {pages} pagini citite', partial: 'Listă parțială: {count} modele din {pages} pagini. Unele pagini nu au putut fi citite; încearcă din nou.', login: 'Autentifică-te în Chaturbate și încearcă din nou.', failed: 'Lista nu a putut fi încărcată. Datele tale nu au fost modificate.', parser: 'Formatul paginii nu a fost recunoscut; lista nu va fi considerată goală.', empty: 'Nu s-au găsit modele urmărite în paginile oficiale recunoscute.', noMatch: 'Niciun model urmărit nu corespunde căutării și stării.', open: 'Deschide în SingleCam', profile: 'Profil oficial' }, nl: { nav: 'Gevolgd', title: 'Modellen die je volgt', description: 'Alleen-lezenlijst van je officiële Chaturbate-pagina’s. Camclient volgt of ontvolgt hier niemand.', refresh: 'Lijst vernieuwen', search: 'Gevolgde modellen zoeken', status: 'Status', all: 'Alles', live: 'Live', offline: 'Offline', sortName: 'Naam A–Z', sortViewers: 'Meeste kijkers', sortRecent: 'Recent online', cancel: 'Laden annuleren', loading: 'Officiële Following-pagina’s laden…', loadingPages: 'Pagina’s gelezen: {done}', results: '{count} modellen · {pages} pagina’s gelezen', partial: 'Gedeeltelijke lijst: {count} modellen van {pages} pagina’s. Sommige pagina’s konden niet worden gelezen; probeer opnieuw.', login: 'Log in bij Chaturbate en probeer het opnieuw.', failed: 'De lijst kon niet worden geladen. Je gegevens zijn niet gewijzigd.', parser: 'Paginaformaat niet herkend; dit wordt niet als een lege lijst getoond.', empty: 'Geen gevolgde modellen gevonden op de herkende officiële pagina’s.', noMatch: 'Geen gevolgde modellen voldoen aan zoekopdracht en status.', open: 'Openen in SingleCam', profile: 'Officieel profiel' }, tr: { nav: 'Takip Edilenler', title: 'Takip ettiğin modeller', description: 'Resmî Chaturbate takip sayfalarından salt okunur liste. Camclient burada kimseyi takip etmez veya takibi bırakmaz.', refresh: 'Listeyi yenile', search: 'Takip edilen modelleri ara', status: 'Durum', all: 'Tümü', live: 'Canlı', offline: 'Çevrimdışı', sortName: 'Ad A–Z', sortViewers: 'En çok izleyici', sortRecent: 'Yakın zamanda çevrimiçi', cancel: 'Yüklemeyi iptal et', loading: 'Resmî takip sayfaları yükleniyor…', loadingPages: 'Okunan sayfalar: {done}', results: '{count} model · {pages} sayfa okundu', partial: 'Kısmi liste: {pages} sayfadan {count} model. Bazı sayfalar okunamadı; tekrar dene.', login: 'Chaturbate hesabına giriş yapıp tekrar dene.', failed: 'Takip listesi yüklenemedi. Kayıtlı verilerin değiştirilmedi.', parser: 'Sayfa biçimi tanınmadı; liste boş olarak gösterilmeyecek.', empty: 'Tanınan resmî sayfalarda takip edilen model bulunamadı.', noMatch: 'Arama ve durumla eşleşen takip edilen model yok.', open: 'SingleCam içinde aç', profile: 'Resmî profil' }, }; I18N.en.followingUi = FOLLOWING_UI.en; I18N.zh.followingUi = FOLLOWING_UI.zh; I18N.de.followingUi = FOLLOWING_UI.de; const ROOMGRID_LANGUAGE_NAMES = { langDe: 'Deutsch', langEn: 'English', langEs: 'Español', langPt: 'Português', langFr: 'Français', langIt: 'Italiano', langRu: 'Русский', langJa: '日本語', langPl: 'Polski', langRo: 'Română', langNl: 'Nederlands', langTr: 'Türkçe', }; Object.assign(I18N.en, ROOMGRID_LANGUAGE_NAMES); Object.assign(I18N.de, ROOMGRID_LANGUAGE_NAMES); function createLatinLanguagePack(language, functions) { let pack = null; const extensions = Object.create(null); const ensure = () => { if (pack) return pack; const phrases = ROOMGRID_LATIN_TRANSLATIONS[language]?.phrases || {}; pack = { ...I18N.en }; for (const [key, value] of Object.entries(pack)) { if (typeof value === 'string') pack[key] = phrases[value] || value; } pack = { ...pack, ...functions, ...CAMF005_I18N[language], followingUi: FOLLOWING_UI[language] || FOLLOWING_UI.en, ...ROOMGRID_LANGUAGE_NAMES, ...extensions }; return pack; }; return new Proxy(Object.create(null), { get(_target, key) { return ensure()[key]; }, // Later feature dictionaries extend these lazy packs through assignment. // Keep unused languages lazy; retain additions until their first read. set(_target, key, value) { extensions[key] = value; if (pack) pack[key] = value; return true; }, has(_target, key) { return key in ensure(); }, ownKeys() { return Reflect.ownKeys(ensure()); }, getOwnPropertyDescriptor(_target, key) { const descriptor = Object.getOwnPropertyDescriptor(ensure(), key); return descriptor ? { ...descriptor, configurable: true } : undefined; }, }); } I18N.es = createLatinLanguagePack('es', { memoryView: 'Ver lista guardada', collapseFAB: 'Contraer panel', dockSubtitle: 'Herramientas MultiCam', dockNoRoom: 'Ninguna sala detectada', dockOpen: 'Abrir espacio de trabajo', dockOpenHere: 'Abrir aquí', dockAdd: 'Añadir sala', dockRemove: 'Quitar sala', dockRecord: 'Grabar en el espacio de trabajo', dockScreenshot: 'Captura del vídeo', dockPip: 'Imagen en imagen', dockPause: 'Reproducir / pausar', dockMute: 'Silenciar / activar sonido', dockVideoMissing: 'No se encontró ningún vídeo reproducible en esta página', dockRecordQueued: 'La grabación se ha preparado en el espacio de trabajo', openWorkstationHereConfirm: '¿Sustituir esta página por el espacio de trabajo? Puedes volver con el navegador.', focusRoomSelected: (n, max) => `${n}/${max} salas seleccionadas`, focusRoomLimit: max => `Selecciona hasta ${max} salas. Aumenta «Salas por página» para disponer de más espacios.`, focusLivePlacement: place => `EN DIRECTO · ${place}`, focusDropHere: place => `Soltar aquí · ${place}`, focusDropReplace: place => `Reemplazar exactamente: ${place}`, gridWindowLabel: n => `Ventana ${n}`, gridEmptySlot: n => `Ventana vacía ${n}`, gridAssignWindow: n => `Ventana de cuadrícula ${n}`, gridAssigned: n => `Modelo colocado en la ventana ${n}.`, hintGroupTab: n => `Cambiar a ${n}. Arrastra salas aquí para añadirlas a este grupo.`, deleteGroupConfirm: n => `¿Eliminar el grupo «${n}»? Las salas permanecen guardadas y solo se quitan del grupo.`, recordingRecoverPrompt: n => `¿Restaurar la intención de grabación de ${n} sala(s) de la sesión anterior?`, shareImportPrompt: n => `¿Importar el espacio compartido con ${n} sala(s)? Antes se creará una copia de seguridad.`, lastSeen: value => `Última conexión: ${value}`, addRoom: n => `Añadir ${n}`, alreadyAdded: n => ` ${n} ya está en el espacio de trabajo`, memoryStat: n => `${n} salas guardadas`, dockCurrentRoom: n => `Actual: ${n}`, addedNamed: n => `${n} añadido`, removedNamed: n => `${n} eliminado`, notifyBody: n => `${n} acaba de iniciar una transmisión`, notifyOfflineBody: n => `${n} se ha desconectado`, roomTagsMore: n => `+${n} etiquetas más`, manualImportDone: (added, existing) => `Adición por lotes completada: ${added} nuevos, ${existing} ya existían`, followedSyncDone: (added, connected, removed) => `Sincronización de seguidos: ${added} nuevos, ${connected} asignados, ${removed} eliminados`, }); I18N.pt = createLatinLanguagePack('pt', { memoryView: 'Ver lista salva', collapseFAB: 'Recolher painel', dockSubtitle: 'Ferramentas MultiCam', dockNoRoom: 'Nenhuma sala detectada', dockOpen: 'Abrir área de trabalho', dockOpenHere: 'Abrir aqui', dockAdd: 'Adicionar sala', dockRemove: 'Remover sala', dockRecord: 'Gravar na área de trabalho', dockScreenshot: 'Captura do vídeo', dockPip: 'Imagem em imagem', dockPause: 'Reproduzir / pausar', dockMute: 'Silenciar / ativar som', dockVideoMissing: 'Nenhum vídeo reproduzível foi encontrado nesta página', dockRecordQueued: 'A gravação foi preparada na área de trabalho', openWorkstationHereConfirm: 'Substituir esta página pela área de trabalho? Você pode voltar usando o navegador.', focusRoomSelected: (n, max) => `${n}/${max} salas selecionadas`, focusRoomLimit: max => `Selecione até ${max} salas. Aumente “Salas por página” para ter mais espaços.`, focusLivePlacement: place => `AO VIVO · ${place}`, focusDropHere: place => `Soltar aqui · ${place}`, focusDropReplace: place => `Substituir exatamente: ${place}`, gridWindowLabel: n => `Janela ${n}`, gridEmptySlot: n => `Janela vazia ${n}`, gridAssignWindow: n => `Janela da grade ${n}`, gridAssigned: n => `Modelo colocado na janela ${n}.`, hintGroupTab: n => `Mudar para ${n}. Arraste salas para cá para adicioná-las a este grupo.`, deleteGroupConfirm: n => `Excluir o grupo “${n}”? As salas continuam salvas e só são removidas deste grupo.`, recordingRecoverPrompt: n => `Restaurar a intenção de gravação de ${n} sala(s) da sessão anterior?`, shareImportPrompt: n => `Importar o espaço compartilhado com ${n} sala(s)? Primeiro será criado um backup.`, lastSeen: value => `Última vez online: ${value}`, addRoom: n => `Adicionar ${n}`, alreadyAdded: n => ` ${n} já está no espaço de trabalho`, memoryStat: n => `${n} salas salvas`, dockCurrentRoom: n => `Atual: ${n}`, addedNamed: n => `${n} adicionado`, removedNamed: n => `${n} removido`, notifyBody: n => `${n} acabou de entrar ao vivo`, notifyOfflineBody: n => `${n} ficou offline`, roomTagsMore: n => `+${n} tags adicionais`, manualImportDone: (added, existing) => `Adição em lote concluída: ${added} novos, ${existing} já existiam`, followedSyncDone: (added, connected, removed) => `Sincronização de seguidos: ${added} novos, ${connected} associados, ${removed} removidos`, }); I18N.fr = createLatinLanguagePack('fr', { memoryView: 'Voir la liste enregistrée', collapseFAB: 'Réduire le panneau', dockSubtitle: 'Outils MultiCam', dockNoRoom: 'Aucun salon détecté', dockOpen: 'Ouvrir l’espace de travail', dockOpenHere: 'Ouvrir ici', dockAdd: 'Ajouter le salon', dockRemove: 'Retirer le salon', dockRecord: 'Enregistrer dans l’espace de travail', dockScreenshot: 'Capture de la vidéo', dockPip: 'Image dans l’image', dockPause: 'Lecture / pause', dockMute: 'Couper / rétablir le son', dockVideoMissing: 'Aucune vidéo lisible trouvée sur cette page', dockRecordQueued: 'L’enregistrement a été préparé dans l’espace de travail', openWorkstationHereConfirm: 'Remplacer cette page par l’espace de travail ? Vous pourrez revenir avec le navigateur.', focusRoomSelected: (n, max) => `${n}/${max} salons sélectionnés`, focusRoomLimit: max => `Sélectionnez jusqu’à ${max} salons. Augmentez « Salons par page » pour obtenir plus d’emplacements.`, focusLivePlacement: place => `EN DIRECT · ${place}`, focusDropHere: place => `Déposer ici · ${place}`, focusDropReplace: place => `Remplacer exactement : ${place}`, gridWindowLabel: n => `Fenêtre ${n}`, gridEmptySlot: n => `Fenêtre vide ${n}`, gridAssignWindow: n => `Fenêtre de grille ${n}`, gridAssigned: n => `Modèle placé dans la fenêtre ${n}.`, hintGroupTab: n => `Passer à ${n}. Faites glisser des salons ici pour les ajouter à ce groupe.`, deleteGroupConfirm: n => `Supprimer le groupe « ${n} » ? Les salons restent enregistrés et sont seulement retirés du groupe.`, recordingRecoverPrompt: n => `Restaurer la demande d’enregistrement de ${n} salon(s) de la session précédente ?`, shareImportPrompt: n => `Importer l’espace partagé avec ${n} salon(s) ? Une sauvegarde sera d’abord créée.`, lastSeen: value => `Dernière connexion : ${value}`, addRoom: n => `Ajouter ${n}`, alreadyAdded: n => ` ${n} est déjà dans l’espace de travail`, memoryStat: n => `${n} salons enregistrés`, dockCurrentRoom: n => `Actuel : ${n}`, addedNamed: n => `${n} ajouté`, removedNamed: n => `${n} retiré`, notifyBody: n => `${n} vient de lancer un direct`, notifyOfflineBody: n => `${n} est hors ligne`, roomTagsMore: n => `+${n} tags supplémentaires`, manualImportDone: (added, existing) => `Ajout groupé terminé : ${added} nouveaux, ${existing} déjà présents`, followedSyncDone: (added, connected, removed) => `Synchronisation des suivis : ${added} nouveaux, ${connected} associés, ${removed} retirés`, }); I18N.it = createLatinLanguagePack('it', { memoryView: 'Vedi elenco salvato', collapseFAB: 'Riduci pannello', dockSubtitle: 'Strumenti MultiCam', dockNoRoom: 'Nessuna stanza rilevata', dockOpen: 'Apri area di lavoro', dockOpenHere: 'Apri qui', dockAdd: 'Aggiungi stanza', dockRemove: 'Rimuovi stanza', dockRecord: 'Registra nell’area di lavoro', dockScreenshot: 'Istantanea del video', dockPip: 'Picture-in-picture', dockPause: 'Riproduci / pausa', dockMute: 'Disattiva / attiva audio', dockVideoMissing: 'Nessun video riproducibile trovato in questa pagina', dockRecordQueued: 'La registrazione è stata preparata nell’area di lavoro', openWorkstationHereConfirm: 'Sostituire questa pagina con l’area di lavoro? Puoi tornare indietro con il browser.', focusRoomSelected: (n, max) => `${n}/${max} stanze selezionate`, focusRoomLimit: max => `Seleziona fino a ${max} stanze. Aumenta “Stanze per pagina” per avere più spazi.`, focusLivePlacement: place => `IN DIRETTA · ${place}`, focusDropHere: place => `Rilascia qui · ${place}`, focusDropReplace: place => `Sostituisci esattamente: ${place}`, gridWindowLabel: n => `Finestra ${n}`, gridEmptySlot: n => `Finestra vuota ${n}`, gridAssignWindow: n => `Finestra griglia ${n}`, gridAssigned: n => `Modello inserito nella finestra ${n}.`, hintGroupTab: n => `Passa a ${n}. Trascina qui le stanze per aggiungerle al gruppo.`, deleteGroupConfirm: n => `Eliminare il gruppo “${n}”? Le stanze restano salvate e vengono solo rimosse dal gruppo.`, recordingRecoverPrompt: n => `Ripristinare la richiesta di registrazione di ${n} stanza/e della sessione precedente?`, shareImportPrompt: n => `Importare l’area condivisa con ${n} stanza/e? Verrà prima creato un backup.`, lastSeen: value => `Ultima volta online: ${value}`, addRoom: n => `Aggiungi ${n}`, alreadyAdded: n => ` ${n} è già nell’area di lavoro`, memoryStat: n => `${n} stanze salvate`, dockCurrentRoom: n => `Attuale: ${n}`, addedNamed: n => `${n} aggiunto`, removedNamed: n => `${n} rimosso`, notifyBody: n => `${n} ha appena avviato una diretta`, notifyOfflineBody: n => `${n} è offline`, roomTagsMore: n => `+${n} tag aggiuntivi`, manualImportDone: (added, existing) => `Aggiunta in blocco completata: ${added} nuovi, ${existing} già presenti`, followedSyncDone: (added, connected, removed) => `Sincronizzazione seguiti: ${added} nuovi, ${connected} associati, ${removed} rimossi`, }); I18N.ru = createLatinLanguagePack('ru', { memoryView: 'Показать сохранённый список', collapseFAB: 'Свернуть панель', dockSubtitle: 'Инструменты MultiCam', dockNoRoom: 'Комната не обнаружена', dockOpen: 'Открыть рабочую область', dockOpenHere: 'Открыть здесь', dockAdd: 'Добавить комнату', dockRemove: 'Удалить комнату', dockRecord: 'Записать в рабочей области', dockScreenshot: 'Снимок видео', dockPip: 'Картинка в картинке', dockPause: 'Воспроизведение / пауза', dockMute: 'Выключить / включить звук', dockVideoMissing: 'На этой странице нет воспроизводимого видео', dockRecordQueued: 'Запись подготовлена в рабочей области', openWorkstationHereConfirm: 'Заменить эту страницу рабочей областью? Вы сможете вернуться через браузер.', focusRoomSelected: (n, max) => `Выбрано комнат: ${n}/${max}`, focusRoomLimit: max => `Выберите до ${max} комнат. Увеличьте «Комнат на странице», чтобы получить больше мест.`, focusLivePlacement: place => `В ЭФИРЕ · ${place}`, focusDropHere: place => `Перетащите сюда · ${place}`, focusDropReplace: place => `Заменить точно: ${place}`, gridWindowLabel: n => `Окно ${n}`, gridEmptySlot: n => `Пустое окно ${n}`, gridAssignWindow: n => `Окно сетки ${n}`, gridAssigned: n => `Модель размещена в окне ${n}.`, hintGroupTab: n => `Перейти к ${n}. Перетащите сюда комнаты, чтобы добавить их в группу.`, deleteGroupConfirm: n => `Удалить группу «${n}»? Комнаты останутся сохранёнными и будут удалены только из группы.`, recordingRecoverPrompt: n => `Восстановить запрос записи для ${n} комнат из предыдущего сеанса?`, shareImportPrompt: n => `Импортировать общую область с ${n} комнатами? Сначала будет создана резервная копия.`, lastSeen: value => `Последний раз в сети: ${value}`, addRoom: n => `Добавить ${n}`, alreadyAdded: n => ` ${n} уже в рабочей области`, memoryStat: n => `Сохранено комнат: ${n}`, dockCurrentRoom: n => `Текущая: ${n}`, addedNamed: n => `${n} добавлена`, removedNamed: n => `${n} удалена`, notifyBody: n => `${n} начал(а) трансляцию`, notifyOfflineBody: n => `${n} не в сети`, roomTagsMore: n => `Ещё тегов: ${n}`, manualImportDone: (added, existing) => `Пакетное добавление завершено: новых — ${added}, уже существовало — ${existing}`, followedSyncDone: (added, connected, removed) => `Синхронизация подписок: новых — ${added}, назначено — ${connected}, удалено — ${removed}`, }); I18N.ja = createLatinLanguagePack('ja', { memoryView: '保存済みリストを表示', collapseFAB: 'パネルを折りたたむ', dockSubtitle: 'MultiCamツール', dockNoRoom: 'ルームが見つかりません', dockOpen: 'ワークスペースを開く', dockOpenHere: 'ここで開く', dockAdd: 'ルームを追加', dockRemove: 'ルームを削除', dockRecord: 'ワークスペースで録画', dockScreenshot: '動画のスクリーンショット', dockPip: 'ピクチャーインピクチャー', dockPause: '再生 / 一時停止', dockMute: 'ミュート / ミュート解除', dockVideoMissing: 'このページに再生可能な動画がありません', dockRecordQueued: 'ワークスペースで録画の準備ができました', openWorkstationHereConfirm: 'このページをワークスペースに切り替えますか?ブラウザで戻ることができます。', focusRoomSelected: (n, max) => `${n}/${max}ルームを選択`, focusRoomLimit: max => `最大${max}ルームまで選択できます。「1ページあたりのルーム数」を増やすと枠を増やせます。`, focusLivePlacement: place => `ライブ · ${place}`, focusDropHere: place => `ここにドロップ · ${place}`, focusDropReplace: place => `正確に置き換え: ${place}`, gridWindowLabel: n => `ウィンドウ${n}`, gridEmptySlot: n => `空のウィンドウ${n}`, gridAssignWindow: n => `グリッドウィンドウ${n}`, gridAssigned: n => `モデルをウィンドウ${n}に配置しました。`, hintGroupTab: n => `${n}に切り替えます。ルームをここへドラッグするとグループに追加できます。`, deleteGroupConfirm: n => `グループ「${n}」を削除しますか?ルームは保存されたままで、グループからのみ削除されます。`, recordingRecoverPrompt: n => `前回のセッションの${n}ルームの録画設定を復元しますか?`, shareImportPrompt: n => `${n}ルームを含む共有ワークスペースを読み込みますか?先にバックアップを作成します。`, lastSeen: value => `最終オンライン: ${value}`, addRoom: n => `${n}を追加`, alreadyAdded: n => ` ${n}はすでにワークスペースにあります`, memoryStat: n => `${n}ルームを保存済み`, dockCurrentRoom: n => `現在: ${n}`, addedNamed: n => `${n}を追加しました`, removedNamed: n => `${n}を削除しました`, notifyBody: n => `${n}がライブ配信を開始しました`, notifyOfflineBody: n => `${n}がオフラインになりました`, roomTagsMore: n => `他${n}件のタグ`, manualImportDone: (added, existing) => `一括追加が完了しました: 新規${added}件、登録済み${existing}件`, followedSyncDone: (added, connected, removed) => `フォロー同期: 新規${added}件、関連付け${connected}件、削除${removed}件`, }); I18N.pl = createLatinLanguagePack('pl', { memoryView: 'Pokaż zapisaną listę', collapseFAB: 'Zwiń panel', dockSubtitle: 'Narzędzia MultiCam', dockNoRoom: 'Nie wykryto pokoju', dockOpen: 'Otwórz obszar roboczy', dockOpenHere: 'Otwórz tutaj', dockAdd: 'Dodaj pokój', dockRemove: 'Usuń pokój', dockRecord: 'Nagrywaj w obszarze roboczym', dockScreenshot: 'Zrzut obrazu wideo', dockPip: 'Obraz w obrazie', dockPause: 'Odtwórz / wstrzymaj', dockMute: 'Wycisz / włącz dźwięk', dockVideoMissing: 'Na tej stronie nie znaleziono odtwarzalnego wideo', dockRecordQueued: 'Nagrywanie zostało przygotowane w obszarze roboczym', openWorkstationHereConfirm: 'Zastąpić tę stronę obszarem roboczym? Możesz wrócić za pomocą przeglądarki.', focusRoomSelected: (n, max) => `Wybrano ${n}/${max} pokoi`, focusRoomLimit: max => `Wybierz maksymalnie ${max} pokoi. Zwiększ „Pokoje na stronę”, aby uzyskać więcej miejsc.`, focusLivePlacement: place => `NA ŻYWO · ${place}`, focusDropHere: place => `Upuść tutaj · ${place}`, focusDropReplace: place => `Zastąp dokładnie: ${place}`, gridWindowLabel: n => `Okno ${n}`, gridEmptySlot: n => `Puste okno ${n}`, gridAssignWindow: n => `Okno siatki ${n}`, gridAssigned: n => `Model umieszczony w oknie ${n}.`, hintGroupTab: n => `Przełącz na ${n}. Przeciągnij tutaj pokoje, aby dodać je do tej grupy.`, deleteGroupConfirm: n => `Usunąć grupę „${n}”? Pokoje pozostaną zapisane i zostaną usunięte tylko z grupy.`, recordingRecoverPrompt: n => `Przywrócić zamiar nagrywania ${n} pokoi z poprzedniej sesji?`, shareImportPrompt: n => `Zaimportować wspólny obszar roboczy z ${n} pokojami? Najpierw zostanie utworzona kopia zapasowa.`, lastSeen: value => `Ostatnio online: ${value}`, addRoom: n => `Dodaj ${n}`, alreadyAdded: n => ` ${n} jest już w obszarze roboczym`, memoryStat: n => `${n} zapisanych pokoi`, dockCurrentRoom: n => `Aktualnie: ${n}`, addedNamed: n => `Dodano ${n}`, removedNamed: n => `Usunięto ${n}`, notifyBody: n => `${n} właśnie rozpoczął transmisję`, notifyOfflineBody: n => `${n} jest offline`, roomTagsMore: n => `+${n} dodatkowych tagów`, manualImportDone: (added, existing) => `Dodawanie zbiorcze zakończone: ${added} nowych, ${existing} już istniało`, followedSyncDone: (added, connected, removed) => `Synchronizacja obserwowanych: ${added} nowych, ${connected} przypisanych, ${removed} usuniętych`, }); I18N.ro = createLatinLanguagePack('ro', { memoryView: 'Vezi lista salvată', collapseFAB: 'Restrânge panoul', dockSubtitle: 'Instrumente MultiCam', dockNoRoom: 'Nicio cameră detectată', dockOpen: 'Deschide spațiul de lucru', dockOpenHere: 'Deschide aici', dockAdd: 'Adaugă camera', dockRemove: 'Elimină camera', dockRecord: 'Înregistrează în spațiul de lucru', dockScreenshot: 'Captură video', dockPip: 'Imagine în imagine', dockPause: 'Redare / pauză', dockMute: 'Dezactivează / activează sunetul', dockVideoMissing: 'Nu s-a găsit niciun videoclip redabil pe această pagină', dockRecordQueued: 'Intenția de înregistrare a fost pregătită în spațiul de lucru', openWorkstationHereConfirm: 'Înlocuiești această pagină cu spațiul de lucru? Poți reveni folosind browserul.', focusRoomSelected: (n, max) => `${n}/${max} camere selectate`, focusRoomLimit: max => `Selectează până la ${max} camere. Mărește „Camere pe pagină” pentru mai multe poziții.`, focusLivePlacement: place => `LIVE · ${place}`, focusDropHere: place => `Plasează aici · ${place}`, focusDropReplace: place => `Înlocuiește exact: ${place}`, gridWindowLabel: n => `Fereastra ${n}`, gridEmptySlot: n => `Fereastra goală ${n}`, gridAssignWindow: n => `Fereastra grilei ${n}`, gridAssigned: n => `Modelul a fost plasat în fereastra ${n}.`, hintGroupTab: n => `Comută la ${n}. Trage camere aici pentru a le adăuga la acest grup.`, deleteGroupConfirm: n => `Ștergi grupul „${n}”? Camerele rămân salvate și sunt eliminate doar din acest grup.`, recordingRecoverPrompt: n => `Restabilești intenția de înregistrare pentru ${n} camere din sesiunea anterioară?`, shareImportPrompt: n => `Imporți spațiul de lucru partajat cu ${n} camere? Mai întâi se va crea o copie de siguranță.`, lastSeen: value => `Ultima conectare: ${value}`, addRoom: n => `Adaugă ${n}`, alreadyAdded: n => ` ${n} este deja în spațiul de lucru`, memoryStat: n => `${n} camere salvate`, dockCurrentRoom: n => `Curentă: ${n}`, addedNamed: n => `${n} adăugată`, removedNamed: n => `${n} eliminată`, notifyBody: n => `${n} tocmai a intrat live`, notifyOfflineBody: n => `${n} este offline`, roomTagsMore: n => `încă ${n} etichete`, manualImportDone: (added, existing) => `Adăugare în lot finalizată: ${added} noi, ${existing} existau deja`, followedSyncDone: (added, connected, removed) => `Sincronizarea urmăririlor: ${added} noi, ${connected} asociate, ${removed} eliminate`, }); I18N.nl = createLatinLanguagePack('nl', { memoryView: 'Opgeslagen lijst bekijken', collapseFAB: 'Paneel inklappen', dockSubtitle: 'MultiCam-hulpmiddelen', dockNoRoom: 'Geen kamer gedetecteerd', dockOpen: 'Werkruimte openen', dockOpenHere: 'Hier openen', dockAdd: 'Kamer toevoegen', dockRemove: 'Kamer verwijderen', dockRecord: 'Opnemen in werkruimte', dockScreenshot: 'Video-opname', dockPip: 'Beeld-in-beeld', dockPause: 'Afspelen / pauzeren', dockMute: 'Dempen / geluid inschakelen', dockVideoMissing: 'Geen afspeelbare video gevonden op deze pagina', dockRecordQueued: 'De opname is voorbereid in de werkruimte', openWorkstationHereConfirm: 'Deze pagina vervangen door de werkruimte? Je kunt teruggaan met de browser.', focusRoomSelected: (n, max) => `${n}/${max} kamers geselecteerd`, focusRoomLimit: max => `Selecteer maximaal ${max} kamers. Verhoog ‘Kamers per pagina’ voor meer plaatsen.`, focusLivePlacement: place => `LIVE · ${place}`, focusDropHere: place => `Hier neerzetten · ${place}`, focusDropReplace: place => `Exact vervangen: ${place}`, gridWindowLabel: n => `Venster ${n}`, gridEmptySlot: n => `Leeg venster ${n}`, gridAssignWindow: n => `Rastervenster ${n}`, gridAssigned: n => `Model in venster ${n} geplaatst.`, hintGroupTab: n => `Ga naar ${n}. Sleep kamers hierheen om ze aan deze groep toe te voegen.`, deleteGroupConfirm: n => `Groep ‘${n}’ verwijderen? Kamers blijven opgeslagen en worden alleen uit deze groep verwijderd.`, recordingRecoverPrompt: n => `De opname-instelling voor ${n} kamers uit de vorige sessie herstellen?`, shareImportPrompt: n => `Gedeelde werkruimte met ${n} kamers importeren? Eerst wordt een back-up gemaakt.`, lastSeen: value => `Laatst online: ${value}`, addRoom: n => `${n} toevoegen`, alreadyAdded: n => ` ${n} staat al in de werkruimte`, memoryStat: n => `${n} kamers opgeslagen`, dockCurrentRoom: n => `Huidig: ${n}`, addedNamed: n => `${n} toegevoegd`, removedNamed: n => `${n} verwijderd`, notifyBody: n => `${n} is zojuist live gegaan`, notifyOfflineBody: n => `${n} is offline gegaan`, roomTagsMore: n => `nog ${n} tags`, manualImportDone: (added, existing) => `Batchtoevoeging voltooid: ${added} nieuw, ${existing} bestonden al`, followedSyncDone: (added, connected, removed) => `Synchronisatie van gevolgde modellen: ${added} nieuw, ${connected} gekoppeld, ${removed} verwijderd`, }); I18N.tr = createLatinLanguagePack('tr', { memoryView: 'Kaydedilen listeyi göster', collapseFAB: 'Paneli daralt', dockSubtitle: 'MultiCam araçları', dockNoRoom: 'Oda algılanmadı', dockOpen: 'Çalışma alanını aç', dockOpenHere: 'Burada aç', dockAdd: 'Oda ekle', dockRemove: 'Odayı kaldır', dockRecord: 'Çalışma alanında kaydet', dockScreenshot: 'Video görüntüsü al', dockPip: 'Resim içinde resim', dockPause: 'Oynat / duraklat', dockMute: 'Sesi kapat / aç', dockVideoMissing: 'Bu sayfada oynatılabilir video bulunamadı', dockRecordQueued: 'Kayıt isteği çalışma alanında hazırlandı', openWorkstationHereConfirm: 'Bu sayfa çalışma alanıyla değiştirilsin mi? Tarayıcı ile geri dönebilirsin.', focusRoomSelected: (n, max) => `${n}/${max} oda seçildi`, focusRoomLimit: max => `En fazla ${max} oda seç. Daha fazla yer için “Sayfa başına oda” sayısını artır.`, focusLivePlacement: place => `CANLI · ${place}`, focusDropHere: place => `Buraya bırak · ${place}`, focusDropReplace: place => `Tam olarak değiştir: ${place}`, gridWindowLabel: n => `Pencere ${n}`, gridEmptySlot: n => `Boş pencere ${n}`, gridAssignWindow: n => `Izgara penceresi ${n}`, gridAssigned: n => `Model ${n}. pencereye yerleştirildi.`, hintGroupTab: n => `${n} grubuna geç. Odaları bu gruba eklemek için buraya sürükle.`, deleteGroupConfirm: n => `“${n}” grubu silinsin mi? Odalar kayıtlı kalır ve yalnızca bu gruptan kaldırılır.`, recordingRecoverPrompt: n => `Önceki oturumdaki ${n} oda için kayıt isteği geri yüklensin mi?`, shareImportPrompt: n => `${n} odalı paylaşılan çalışma alanı içe aktarılsın mı? Önce bir yedek oluşturulur.`, lastSeen: value => `Son çevrimiçi: ${value}`, addRoom: n => `${n} ekle`, alreadyAdded: n => ` ${n} zaten çalışma alanında`, memoryStat: n => `${n} oda kaydedildi`, dockCurrentRoom: n => `Geçerli: ${n}`, addedNamed: n => `${n} eklendi`, removedNamed: n => `${n} kaldırıldı`, notifyBody: n => `${n} şimdi canlı yayına başladı`, notifyOfflineBody: n => `${n} çevrimdışı oldu`, roomTagsMore: n => `${n} etiket daha`, manualImportDone: (added, existing) => `Toplu ekleme tamamlandı: ${added} yeni, ${existing} zaten vardı`, followedSyncDone: (added, connected, removed) => `Takip edilenler eşitlendi: ${added} yeni, ${connected} bağlandı, ${removed} kaldırıldı`, }); const CAMF005_I18N = { es: { multicamMeterVisibility: 'Visibilidad del medidor de audio', multicamMeterVisibilityHint: 'Solo cambia la visualización. La actividad de audio sigue midiéndose, incluso con el sonido silenciado.', multicamMeterVisibilityGlobal: 'Mostrar medidores en las tarjetas de cámara', multicamMeterVisibilityCamera: 'Medidor de esta cámara', multicamMeterVisibilityGlobalOption: 'Usar ajuste global', multicamMeterVisibilityShowOption: 'Mostrar siempre', multicamMeterVisibilityHideOption: 'Ocultar siempre' }, pt: { multicamMeterVisibility: 'Visibilidade do medidor de áudio', multicamMeterVisibilityHint: 'Altera apenas a exibição. A atividade de áudio continua a ser medida, mesmo sem som.', multicamMeterVisibilityGlobal: 'Mostrar medidores nos cartões das câmeras', multicamMeterVisibilityCamera: 'Medidor desta câmera', multicamMeterVisibilityGlobalOption: 'Usar configuração global', multicamMeterVisibilityShowOption: 'Mostrar sempre', multicamMeterVisibilityHideOption: 'Ocultar sempre' }, fr: { multicamMeterVisibility: 'Affichage du niveau audio', multicamMeterVisibilityHint: 'Modifie uniquement l’affichage. L’activité audio continue d’être mesurée, même lorsque le son est coupé.', multicamMeterVisibilityGlobal: 'Afficher le niveau audio sur les cartes caméra', multicamMeterVisibilityCamera: 'Niveau audio de cette caméra', multicamMeterVisibilityGlobalOption: 'Utiliser le réglage global', multicamMeterVisibilityShowOption: 'Toujours afficher', multicamMeterVisibilityHideOption: 'Toujours masquer' }, it: { multicamMeterVisibility: 'Visibilità del livello audio', multicamMeterVisibilityHint: 'Modifica solo la visualizzazione. L’attività audio continua a essere misurata anche con l’audio disattivato.', multicamMeterVisibilityGlobal: 'Mostra i livelli audio sulle schede delle videocamere', multicamMeterVisibilityCamera: 'Livello audio di questa videocamera', multicamMeterVisibilityGlobalOption: 'Usa impostazione globale', multicamMeterVisibilityShowOption: 'Mostra sempre', multicamMeterVisibilityHideOption: 'Nascondi sempre' }, ru: { multicamMeterVisibility: 'Отображение уровня звука', multicamMeterVisibilityHint: 'Меняется только отображение. Активность звука продолжает измеряться, даже если звук выключен.', multicamMeterVisibilityGlobal: 'Показывать индикаторы на карточках камер', multicamMeterVisibilityCamera: 'Индикатор этой камеры', multicamMeterVisibilityGlobalOption: 'Использовать общую настройку', multicamMeterVisibilityShowOption: 'Всегда показывать', multicamMeterVisibilityHideOption: 'Всегда скрывать' }, ja: { multicamMeterVisibility: '音声レベル表示', multicamMeterVisibilityHint: '表示のみを変更します。ミュート中も音声の活動は測定されます。', multicamMeterVisibilityGlobal: 'カメラカードに音声レベルを表示', multicamMeterVisibilityCamera: 'このカメラのレベル表示', multicamMeterVisibilityGlobalOption: '全体設定を使用', multicamMeterVisibilityShowOption: '常に表示', multicamMeterVisibilityHideOption: '常に非表示' }, pl: { multicamMeterVisibility: 'Widoczność miernika audio', multicamMeterVisibilityHint: 'Zmienia tylko wygląd. Aktywność audio jest nadal mierzona, także przy wyciszonym dźwięku.', multicamMeterVisibilityGlobal: 'Pokaż mierniki na kartach kamer', multicamMeterVisibilityCamera: 'Miernik tej kamery', multicamMeterVisibilityGlobalOption: 'Użyj ustawienia globalnego', multicamMeterVisibilityShowOption: 'Zawsze pokazuj', multicamMeterVisibilityHideOption: 'Zawsze ukrywaj' }, ro: { multicamMeterVisibility: 'Afișarea nivelului audio', multicamMeterVisibilityHint: 'Schimbă doar afișarea. Activitatea audio continuă să fie măsurată chiar și cu sunetul dezactivat.', multicamMeterVisibilityGlobal: 'Afișează indicatoarele pe cardurile camerelor', multicamMeterVisibilityCamera: 'Indicatorul acestei camere', multicamMeterVisibilityGlobalOption: 'Folosește setarea globală', multicamMeterVisibilityShowOption: 'Afișează întotdeauna', multicamMeterVisibilityHideOption: 'Ascunde întotdeauna' }, nl: { multicamMeterVisibility: 'Zichtbaarheid van de audiometer', multicamMeterVisibilityHint: 'Alleen de weergave verandert. Audioactiviteit blijft gemeten worden, ook als het geluid gedempt is.', multicamMeterVisibilityGlobal: 'Audiometers op camerakaarten tonen', multicamMeterVisibilityCamera: 'Meter van deze camera', multicamMeterVisibilityGlobalOption: 'Globale instelling gebruiken', multicamMeterVisibilityShowOption: 'Altijd tonen', multicamMeterVisibilityHideOption: 'Altijd verbergen' }, tr: { multicamMeterVisibility: 'Ses seviyesi göstergesi', multicamMeterVisibilityHint: 'Yalnızca görünümü değiştirir. Ses kapalı olsa bile ses etkinliği ölçülmeye devam eder.', multicamMeterVisibilityGlobal: 'Kamera kartlarında ses göstergelerini göster', multicamMeterVisibilityCamera: 'Bu kameranın göstergesi', multicamMeterVisibilityGlobalOption: 'Genel ayarı kullan', multicamMeterVisibilityShowOption: 'Her zaman göster', multicamMeterVisibilityHideOption: 'Her zaman gizle' }, }; const LANG_KEY = 'multicam_lang'; const ROOMGRID_LANGUAGE_COOKIE = 'roomgrid_language'; const ROOMGRID_LANGUAGE_TRANSFER_KEY = 'roomgrid_language_transfer_v1'; const ROOMGRID_LANGUAGE_DATABASES = ['ryujo_multicam_analytics_v1']; const ROOMGRID_LANGUAGES = ['de', 'en', 'es', 'pt', 'fr', 'it', 'ru', 'ja', 'pl', 'ro', 'nl', 'tr']; const ROOMGRID_LANGUAGE_HOSTS = { de: 'de.chaturbate.com', en: 'chaturbate.com', es: 'es.chaturbate.com', pt: 'pt.chaturbate.com', fr: 'fr.chaturbate.com', it: 'it.chaturbate.com', ru: 'ru.chaturbate.com', ja: 'ja.chaturbate.com', pl: 'pl.chaturbate.com', ro: '', nl: '', tr: '', }; Object.entries({ es: ['Ya mostrado', 'Seleccionar un modelo ya mostrado lo mueve aquí, sin abrir un duplicado.'], pt: ['Já exibido', 'Selecionar um modelo já exibido move-o para cá, sem abrir uma duplicata.'], fr: ['Déjà affiché', 'Sélectionner un modèle déjà affiché le déplace ici, sans ouvrir de doublon.'], it: ['Già visualizzato', 'Selezionare un modello già visualizzato lo sposta qui, senza aprire un duplicato.'], ru: ['Уже отображается', 'Выбор уже отображаемой модели перемещает её сюда без открытия дубликата.'], ja: ['表示中', '表示中のモデルを選ぶと、この枠に移動します。重複して開くことはありません。'], pl: ['Już wyświetlany', 'Wybranie już wyświetlanego modelu przenosi go tutaj, bez otwierania duplikatu.'], ro: ['Deja afișat', 'Selectarea unui model deja afișat îl mută aici, fără a deschide un duplicat.'], nl: ['Al weergegeven', 'Een al weergegeven model wordt hierheen verplaatst, zonder een duplicaat te openen.'], tr: ['Zaten gösteriliyor', 'Zaten gösterilen bir modeli seçmek, ikinci bir kopya açmadan buraya taşır.'], }).forEach(([language, [modelPickerDisplayed, modelPickerMoveHint]]) => { Object.assign(I18N[language], { modelPickerDisplayed, modelPickerMoveHint }); }); Object.entries({ en: 'Refresh profile', de: 'Profil aktualisieren', es: 'Actualizar perfil', pt: 'Atualizar perfil', fr: 'Actualiser le profil', it: 'Aggiorna profilo', ru: 'Обновить профиль', ja: 'プロフィールを更新', pl: 'Odśwież profil', ro: 'Actualizează profilul', nl: 'Profiel vernieuwen', tr: 'Profili yenile', zh: '刷新资料', }).forEach(([language, publicProfileRefresh]) => { I18N[language].publicProfileRefresh = publicProfileRefresh; }); Object.entries({ en: ['Live rooms', 'Filters & sorting', 'View', 'Overview', 'Collapse'], de: ['Live-Räume', 'Filter & Sortierung', 'Ansicht', 'Übersicht', 'Einklappen'], es: ['Salas en vivo', 'Filtros y orden', 'Vista', 'Resumen', 'Contraer'], pt: ['Salas ao vivo', 'Filtros e ordenação', 'Visualização', 'Resumo', 'Recolher'], fr: ['Salons en direct', 'Filtres et tri', 'Affichage', 'Vue d’ensemble', 'Replier'], it: ['Stanze live', 'Filtri e ordinamento', 'Visualizzazione', 'Panoramica', 'Comprimi'], ru: ['Прямые эфиры', 'Фильтры и сортировка', 'Вид', 'Обзор', 'Свернуть'], ja: ['ライブ配信', 'フィルターと並べ替え', '表示', '概要', '折りたたむ'], pl: ['Pokoje na żywo', 'Filtry i sortowanie', 'Widok', 'Przegląd', 'Zwiń'], ro: ['Camere live', 'Filtre și sortare', 'Afișare', 'Prezentare generală', 'Restrânge'], nl: ['Livekamers', 'Filters en sortering', 'Weergave', 'Overzicht', 'Inklappen'], tr: ['Canlı odalar', 'Filtreler ve sıralama', 'Görünüm', 'Genel bakış', 'Daralt'], zh: ['直播房间', '筛选与排序', '外观', '概览', '收起'], }).forEach(([language, [live, filters, display, overview, close]]) => { if (I18N[language]) I18N[language].homeToolsUi = { live, filters, display, overview, close }; }); Object.entries({ de: ['Lautstärkepegel', 'Einzelvorgaben', 'Alle an', 'Alle aus', 'Alle an/aus hat Vorrang für alle Kameras, auch bei Modelwechsel. Einzelvorgaben und Ton bleiben erhalten.', 'Standardanzeige bei Einzelvorgaben'], en: ['Audio level', 'Per-camera settings', 'All on', 'All off', 'All on/off overrides every camera, including model changes. Individual settings and audio are preserved.', 'Default display in per-camera mode'], es: ['Nivel de audio', 'Ajustes por cámara', 'Todos activados', 'Todos desactivados', 'Activar/desactivar todos tiene prioridad en todas las cámaras, incluso al cambiar de modelo. Se conservan los ajustes individuales y el sonido.', 'Visualización predeterminada por cámara'], pt: ['Nível de áudio', 'Ajustes por câmera', 'Todos ligados', 'Todos desligados', 'Ligar/desligar todos tem prioridade em todas as câmeras, inclusive ao trocar de modelo. Os ajustes individuais e o som são preservados.', 'Exibição padrão por câmera'], fr: ['Niveau audio', 'Réglages par caméra', 'Tout afficher', 'Tout masquer', 'Tout afficher/masquer est prioritaire pour chaque caméra, même après un changement de modèle. Les réglages individuels et le son sont conservés.', 'Affichage par défaut par caméra'], it: ['Livello audio', 'Impostazioni per camera', 'Tutti attivi', 'Tutti nascosti', 'Mostrare/nascondere tutti ha la precedenza su ogni camera, anche cambiando modello. Le impostazioni individuali e il suono restano invariati.', 'Visualizzazione predefinita per camera'], ru: ['Уровень звука', 'Настройки камер', 'Показать все', 'Скрыть все', 'Общий режим имеет приоритет для всех камер, даже при смене модели. Индивидуальные настройки и звук сохраняются.', 'Показ по умолчанию для камер'], ja: ['音声レベル', 'カメラごとの設定', 'すべて表示', 'すべて非表示', '一括表示・非表示はモデル切替後もすべてのカメラに優先して適用されます。個別設定と音声は変更されません。', '個別設定モードの標準表示'], pl: ['Poziom audio', 'Ustawienia kamer', 'Pokaż wszystkie', 'Ukryj wszystkie', 'Pokazanie/ukrycie wszystkich ma priorytet dla każdej kamery, także po zmianie modelu. Ustawienia indywidualne i dźwięk pozostają bez zmian.', 'Domyślne wyświetlanie dla kamer'], ro: ['Nivel audio', 'Setări individuale', 'Afișează toate', 'Ascunde toate', 'Afișarea/ascunderea tuturor are prioritate pentru fiecare cameră, inclusiv după schimbarea modelului. Setările individuale și sunetul rămân neschimbate.', 'Afișare implicită pentru camere'], nl: ['Audioniveau', 'Camera-instellingen', 'Alles aan', 'Alles uit', 'Alles aan/uit heeft voorrang voor elke camera, ook na een modelwissel. Individuele instellingen en geluid blijven behouden.', 'Standaardweergave per camera'], tr: ['Ses seviyesi', 'Kamera ayarları', 'Tümünü göster', 'Tümünü gizle', 'Tümünü göster/gizle, model değişimlerinde de her kamera için önceliklidir. Ayrı ayarlar ve ses korunur.', 'Kamera başına varsayılan gösterim'], zh: ['音频电平', '单独设置', '全部显示', '全部隐藏', '全部显示或隐藏优先应用于所有摄像头,包括切换模型后。单独设置和声音保持不变。', '单独模式默认显示'], }).forEach(([language, [multicamMeterModeLabel, multicamMeterModeIndividual, multicamMeterModeAllShow, multicamMeterModeAllHide, multicamMeterModeHint, multicamMeterDefaultLabel]]) => { if (I18N[language]) Object.assign(I18N[language], { multicamMeterModeLabel, multicamMeterModeIndividual, multicamMeterModeAllShow, multicamMeterModeAllHide, multicamMeterModeHint, multicamMeterDefaultLabel }); }); Object.entries({ de: ['Lokal gespeicherter Stand – Online-Status und Angaben können veraltet sein. Abgleich läuft bei Bedarf im Hintergrund.', 'Vollständige Liste lokal gespeichert; beim nächsten Öffnen sofort verfügbar.', 'Die Liste konnte nicht lokal gespeichert werden. Der vorherige gespeicherte Stand bleibt erhalten.'], en: ['Locally saved snapshot – online status and details may be outdated. Refresh runs in the background when needed.', 'Complete list saved locally; available immediately next time.', 'The list could not be saved locally. The previous saved snapshot is retained.'], es: ['Copia local: el estado y los datos pueden estar desactualizados. Se actualizan en segundo plano cuando hace falta.', 'Lista completa guardada localmente; disponible al volver a abrir.', 'No se pudo guardar la lista localmente. Se conserva la copia anterior.'], pt: ['Cópia local: o estado e os dados podem estar desatualizados. A atualização ocorre em segundo plano quando necessário.', 'Lista completa salva localmente; disponível ao abrir novamente.', 'Não foi possível salvar a lista localmente. A cópia anterior foi mantida.'], fr: ['Copie locale : le statut et les informations peuvent être anciens. Actualisation en arrière-plan si nécessaire.', 'Liste complète enregistrée localement ; disponible dès la prochaine ouverture.', 'Impossible de sauvegarder la liste localement. La copie précédente est conservée.'], it: ['Copia locale: stato e dati potrebbero non essere aggiornati. Aggiornamento in background quando necessario.', 'Elenco completo salvato localmente; disponibile alla prossima apertura.', 'Impossibile salvare localmente. La copia precedente viene conservata.'], ru: ['Локальная копия: статус и данные могут устареть. При необходимости обновляется в фоне.', 'Полный список сохранён локально и доступен при следующем открытии.', 'Не удалось сохранить список локально. Предыдущая копия сохранена.'], ja: ['ローカル保存済みの一覧です。オンライン状態や情報は古い場合があります。必要に応じてバックグラウンドで更新します。', '完全な一覧をローカルに保存しました。次回すぐに表示できます。', '一覧をローカルに保存できませんでした。以前の保存済み一覧は保持されます。'], pl: ['Lokalna kopia: status i dane mogą być nieaktualne. W razie potrzeby odświeżanie działa w tle.', 'Pełna lista zapisana lokalnie; dostępna przy kolejnym otwarciu.', 'Nie udało się zapisać listy lokalnie. Poprzednia kopia została zachowana.'], ro: ['Copie locală: starea și datele pot fi învechite. Actualizarea rulează în fundal când este necesar.', 'Lista completă a fost salvată local; disponibilă la următoarea deschidere.', 'Lista nu a putut fi salvată local. Copia anterioară este păstrată.'], nl: ['Lokaal opgeslagen lijst: status en gegevens kunnen verouderd zijn. Vernieuwen gebeurt zo nodig op de achtergrond.', 'Volledige lijst lokaal opgeslagen; direct beschikbaar bij de volgende opening.', 'De lijst kon niet lokaal worden opgeslagen. De vorige opgeslagen lijst blijft behouden.'], tr: ['Yerel kopya: durum ve bilgiler eski olabilir. Gerektiğinde arka planda yenilenir.', 'Tam liste yerel olarak kaydedildi; sonraki açılışta hemen kullanılabilir.', 'Liste yerel olarak kaydedilemedi. Önceki kayıt korundu.'], zh: ['本地保存的列表:在线状态和资料可能过时。需要时在后台更新。', '完整列表已保存在本地,下次打开即可显示。', '无法在本地保存列表。之前保存的列表仍保留。'], }).forEach(([language, values]) => Object.assign(I18N[language], Object.fromEntries(['followingCachePrevious', 'followingCacheSaved', 'followingCacheSaveFailed'].map((key, index) => [key, values[index]])))); function languageCookie() { try { const match = document.cookie.match(/(?:^|;\s*)roomgrid_language=([^;]+)/u); return match ? decodeURIComponent(match[1]) : ''; } catch (_) { return ''; } } function detectLang() { const stored = localStorage.getItem(LANG_KEY); if (ROOMGRID_LANGUAGES.includes(stored)) return stored; const cookieLanguage = languageCookie(); if (ROOMGRID_LANGUAGES.includes(cookieLanguage)) return cookieLanguage; const hostLanguage = String(location.hostname || '').toLocaleLowerCase().split('.')[0]; if (ROOMGRID_LANGUAGES.includes(hostLanguage)) return hostLanguage; const browserLangs = [ ...(Array.isArray(navigator.languages) ? navigator.languages : []), navigator.language, navigator.userLanguage, ].filter(Boolean).map(v => String(v).toLowerCase()); const primary = browserLangs.find(v => /^(?:de|en|es|pt|fr|it|ru|ja|pl|ro|nl|tr)/u.test(v)) || ''; const detected = ROOMGRID_LANGUAGES.find(language => primary.startsWith(language)); if (detected) return detected; return 'en'; } let LANG = detectLang(); const roomGridPatternCache = new Map(); const roomGridLocalizedValueCache = new Map(); function isAlreadyLocalizedText(text, language) { if (!roomGridLocalizedValueCache.has(language)) { const translations = ROOMGRID_LATIN_TRANSLATIONS[language] || {}; roomGridLocalizedValueCache.set(language, new Set([ ...Object.values(translations.phrases || {}), ...Object.values(translations.visible || {}), ])); } return roomGridLocalizedValueCache.get(language).has(text); } function translatedPatternText(text, language) { const patterns = ROOMGRID_LATIN_TRANSLATIONS[language]?.patterns || {}; if (!Object.keys(patterns).length) return ''; if (!roomGridPatternCache.has(language)) { const compiled = Object.entries(patterns).map(([source, target]) => { if (source === '[[ROOMGRID_VALUE_0]] model[[ROOMGRID_VALUE_1]]') { return { regex: /^(.+?) model(s?)$/u, target, captures: [0, 1] }; } const captures = []; let cursor = 0; let expression = '^'; const marker = /\[\[ROOMGRID_VALUE_(\d+)\]\]/gu; for (const match of source.matchAll(marker)) { expression += source.slice(cursor, match.index).replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); expression += '([\\s\\S]*?)'; captures.push(Number(match[1])); cursor = match.index + match[0].length; } expression += `${source.slice(cursor).replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}$`; return { regex: new RegExp(expression, 'u'), target, captures }; }); roomGridPatternCache.set(language, compiled); } for (const pattern of roomGridPatternCache.get(language)) { const match = text.match(pattern.regex); if (!match) continue; const values = {}; pattern.captures.forEach((index, captureIndex) => { values[index] = match[captureIndex + 1]; }); return pattern.target.replace(/\[\[ROOMGRID_VALUE_(\d+)\]\]/gu, (_, index) => { const value = values[Number(index)] ?? ''; return ROOMGRID_LATIN_TRANSLATIONS[language]?.phrases?.[value] || ROOMGRID_LATIN_TRANSLATIONS[language]?.visible?.[value] || translatedPatternText(value, language) || value; }); } return ''; } const latinText = (value, language = LANG) => { const text = String(value ?? ''); if (text === 'RoomGrid') return text; const translations = ROOMGRID_LATIN_TRANSLATIONS[language] || {}; if (isAlreadyLocalizedText(text, language)) return text; return translations.visible?.[text] || translations.phrases?.[text] || translatedPatternText(text, language) || text; }; const camclientUiText = value => String(value ?? '').replaceAll('RoomGrid', 'Camclient'); roomGridUiText = value => camclientUiText(latinText(value)); const t = (key, ...args) => { const v = (I18N[LANG] && I18N[LANG][key]) ?? I18N.en[key] ?? key; return camclientUiText(typeof v === 'function' ? v(...args) : v); }; const tx = (zh, en, de = en, es, pt) => { if (LANG === 'de') return camclientUiText(de); if (LANG === 'es') return camclientUiText(es ?? latinText(en, 'es')); if (LANG === 'pt') return camclientUiText(pt ?? latinText(en, 'pt')); if (['fr', 'it', 'ru', 'ja', 'pl', 'ro', 'nl', 'tr'].includes(LANG)) return camclientUiText(latinText(en, LANG)); return camclientUiText(en); }; function requestValue(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('IndexedDB request failed')); }); } async function dumpLanguageDatabase(name) { if (!window.indexedDB?.databases) return null; const databases = await indexedDB.databases(); if (!databases.some(database => database.name === name)) return null; const db = await requestValue(indexedDB.open(name)); try { const names = [...db.objectStoreNames]; if (!names.length) return null; const transaction = db.transaction(names, 'readonly'); const stores = []; for (const storeName of names) { const store = transaction.objectStore(storeName); const indexes = [...store.indexNames].map(indexName => { const index = store.index(indexName); return { name: index.name, keyPath: index.keyPath, unique: index.unique, multiEntry: index.multiEntry }; }); stores.push({ name: storeName, keyPath: store.keyPath, autoIncrement: store.autoIncrement, indexes, rows: await requestValue(store.getAll()) }); } return { name, version: db.version, stores }; } finally { db.close(); } } async function restoreLanguageDatabase(snapshot) { if (!snapshot?.name || !Array.isArray(snapshot.stores)) return; const known = window.indexedDB?.databases ? await indexedDB.databases() : []; const exists = known.some(database => database.name === snapshot.name); const request = exists ? indexedDB.open(snapshot.name) : indexedDB.open(snapshot.name, Math.max(1, Number(snapshot.version) || 1)); request.onupgradeneeded = () => { const db = request.result; snapshot.stores.forEach(schema => { const store = db.objectStoreNames.contains(schema.name) ? request.transaction.objectStore(schema.name) : db.createObjectStore(schema.name, { keyPath: schema.keyPath ?? null, autoIncrement: !!schema.autoIncrement }); (schema.indexes || []).forEach(index => { if (!store.indexNames.contains(index.name)) store.createIndex(index.name, index.keyPath, { unique: !!index.unique, multiEntry: !!index.multiEntry }); }); }); }; const db = await requestValue(request); try { const names = snapshot.stores.map(store => store.name).filter(name => db.objectStoreNames.contains(name)); if (!names.length) return; const transaction = db.transaction(names, 'readwrite'); snapshot.stores.forEach(snapshotStore => { if (!names.includes(snapshotStore.name)) return; const store = transaction.objectStore(snapshotStore.name); (snapshotStore.rows || []).forEach(row => { try { store.put(row); } catch (_) {} }); }); await new Promise((resolve, reject) => { transaction.oncomplete = resolve; transaction.onerror = () => reject(transaction.error || new Error('IndexedDB restore failed')); transaction.onabort = () => reject(transaction.error || new Error('IndexedDB restore aborted')); }); } finally { db.close(); } } async function prepareRoomGridLanguageTransfer(language) { const nonce = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; const local = {}; for (let index = 0; index < localStorage.length; index += 1) { const key = localStorage.key(index); if (key && /^(?:ryujo_|multicam_|roomgrid_)/u.test(key)) local[key] = localStorage.getItem(key); } const databases = []; for (const name of ROOMGRID_LANGUAGE_DATABASES) { try { const snapshot = await dumpLanguageDatabase(name); if (snapshot) databases.push(snapshot); } catch (_) {} } const serialized = JSON.stringify({ schema: 1, nonce, language, createdAt: Date.now(), local, databases }); if (!window.GM?.setValue) throw new Error('Shared userscript storage is unavailable'); await GM.setValue(ROOMGRID_LANGUAGE_TRANSFER_KEY, serialized); return nonce; } async function restoreRoomGridLanguageTransfer() { const url = new URL(location.href); const nonce = url.searchParams.get('roomgrid_language_transfer'); if (!nonce) return false; let serialized = ''; try { if (window.GM?.getValue) serialized = await GM.getValue(ROOMGRID_LANGUAGE_TRANSFER_KEY, ''); } catch (_) {} try { const bundle = JSON.parse(serialized || '{}'); if (bundle.nonce !== nonce || Date.now() - Number(bundle.createdAt || 0) > 10 * 60 * 1000) throw new Error('Language transfer expired'); Object.entries(bundle.local || {}).forEach(([key, value]) => { if (/^(?:ryujo_|multicam_|roomgrid_)/u.test(key) && typeof value === 'string') localStorage.setItem(key, value); }); localStorage.setItem(LANG_KEY, bundle.language); for (const database of bundle.databases || []) await restoreLanguageDatabase(database); } finally { try { if (window.GM?.deleteValue) await GM.deleteValue(ROOMGRID_LANGUAGE_TRANSFER_KEY); } catch (_) {} url.searchParams.delete('roomgrid_language_transfer'); history.replaceState(history.state, '', url.toString()); } return true; } function languageBusyOverlay(language) { const labels = { de: 'Sprache wird gewechselt … RoomGrid-Daten werden sicher übernommen.', en: 'Changing language… RoomGrid data is being transferred safely.', es: 'Cambiando el idioma… Los datos de RoomGrid se transferirán de forma segura.', pt: 'Alterando o idioma… Os dados do RoomGrid serão transferidos com segurança.', fr: 'Changement de langue… Les données RoomGrid sont transférées en toute sécurité.', it: 'Cambio della lingua… I dati di RoomGrid vengono trasferiti in modo sicuro.', ru: 'Смена языка… Данные RoomGrid безопасно переносятся.', ja: '言語を変更しています… RoomGridのデータを安全に引き継いでいます。', pl: 'Zmiana języka… Dane RoomGrid są bezpiecznie przenoszone.', ro: 'Se schimbă limba… Datele RoomGrid sunt păstrate în siguranță.', nl: 'Taal wijzigen… RoomGrid-gegevens worden veilig overgezet.', tr: 'Dil değiştiriliyor… RoomGrid verileri güvenle aktarılıyor.', }; const overlay = document.createElement('div'); overlay.dataset.roomgridLanguageBusy = '1'; Object.assign(overlay.style, { position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center', padding: '24px', color: '#fff', background: 'rgba(5,12,20,.92)', font: '800 15px/1.5 Inter,system-ui,sans-serif', textAlign: 'center' }); overlay.textContent = labels[language] || labels.en; document.documentElement.appendChild(overlay); return overlay; } async function setLang(lang) { if (!ROOMGRID_LANGUAGES.includes(lang)) return; localStorage.setItem(LANG_KEY, lang); try { document.cookie = `${ROOMGRID_LANGUAGE_COOKIE}=${encodeURIComponent(lang)}; Path=/; Domain=.chaturbate.com; Max-Age=31536000; SameSite=Lax; Secure`; } catch (_) {} const targetHost = ROOMGRID_LANGUAGE_HOSTS[lang]; if (!targetHost || location.hostname === targetHost) { location.reload(); return; } const overlay = languageBusyOverlay(lang); try { const nonce = await prepareRoomGridLanguageTransfer(lang); const target = new URL(location.href); target.hostname = targetHost; target.searchParams.set('roomgrid_language_transfer', nonce); location.assign(target.toString()); } catch (error) { overlay.remove(); alert(tx('无法安全准备语言切换。未离开当前页面。', 'The language switch could not be prepared safely. This page was not left.', 'Der Sprachwechsel konnte nicht sicher vorbereitet werden. Die aktuelle Seite wurde nicht verlassen.', 'No se pudo preparar el cambio de idioma de forma segura. No se abandonó esta página.', 'Não foi possível preparar a alteração de idioma com segurança. Esta página não foi fechada.')); } } function createLanguageSwitcher(className = '') { const languageHost = ROOMGRID_LANGUAGE_HOSTS[LANG]; const hostAligned = !languageHost || languageHost === location.hostname; const nativeOption = (value, label) => { const option = document.createElement('option'); option.value = value; option.selected = hostAligned && LANG === value; option.textContent = label; return option; }; const choices = []; if (!hostAligned) { const alignLabels = { de: '🌐 DE · Website angleichen', en: '🌐 EN · Apply to website', es: '🌐 ES · Aplicar al sitio', pt: '🌐 PT · Aplicar ao site', fr: '🌐 FR · Appliquer au site', it: '🌐 IT · Applica al sito', ru: '🌐 RU · Применить к сайту', ja: '🌐 JA · サイトにも適用', pl: '🌐 PL · Zastosuj w witrynie', ro: '🌐 RO · Aplică pe site', nl: '🌐 NL · Op website toepassen', tr: '🌐 TR · Siteye uygula', }; const align = document.createElement('option'); align.value = ''; align.selected = true; align.disabled = true; align.textContent = alignLabels[LANG] || alignLabels.en; choices.push(align); } choices.push( nativeOption('de', 'DE · Deutsch'), nativeOption('en', 'EN · English'), nativeOption('es', 'ES · Español'), nativeOption('pt', 'PT · Português'), nativeOption('fr', 'FR · Français'), nativeOption('it', 'IT · Italiano'), nativeOption('ru', 'RU · Русский'), nativeOption('ja', 'JA · 日本語'), nativeOption('pl', 'PL · Polski'), nativeOption('ro', 'RO · Română'), nativeOption('nl', 'NL · Nederlands'), nativeOption('tr', 'TR · Türkçe'), ); const select = $('select', { class: `roomgrid-language-select ${className}`.trim(), dataset: { roomgridLanguageSelect: '1' }, title: languageHost ? t('languageSwitcherTitle') : t('languageSwitcherLocalTitle'), ariaLabel: tx('语言', 'Language', 'Sprache', 'Idioma', 'Idioma'), }, choices); Object.assign(select.style, { minHeight: '34px', padding: '5px 8px', border: '1px solid currentColor', borderRadius: '6px', color: 'inherit', background: 'inherit', font: '800 11px/1 system-ui,sans-serif', cursor: 'pointer' }); select.addEventListener('change', async () => { if (!select.value) return; select.disabled = true; await setLang(select.value); select.disabled = false; }); return select; } /* ============================================================= * 0.6. 元数据 / Meta —— 关于 + 捐赠 * ============================================================= */ const META = { version: '20.116.1-custom', author: 'Emberhost', license: 'MIT', }; /* ============================================================= * 1. 持久化层 / Storage —— schema 版本号兜底 * ============================================================= */ // Stable workspace key: do not tie this to the userscript release version. // Changing it would make an update look like an empty new workstation. const STORE_KEY = 'ryujo_multicam_v8'; const SUPPORTED_LAYOUT_SIZES = [2, 4, 6, 9, 12, 15]; const MAX_LAYOUT_SLOTS = 20; // One catalog for both settings panels, persisted values and preview players. // Rates are targets; each visible card still allows only one pending frame. const PREVIEW_FPS_VALUES = Object.freeze([0, .1, .2, .25, .33, .5, .75, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 24, 25, 30, 40, 50, 60, 90, 120, 144, 165, 240]); const MAX_STORED_ROOMS = 3000; const LEGACY_STORE_KEYS = ['ryujo_multicam_v7', 'ryujo_multicam_v6']; const STABLE_ARCHIVE_KEY = 'ryujo_multicam_workspace_archive_v1'; const CONFIG_BACKUP_PREFIX = STORE_KEY + '_backup_'; const CORRUPT_CONFIG_PREFIX = STORE_KEY + '_corrupt_'; const RECORDING_INTENT_KEY = STORE_KEY + '_recording_intents_v1'; const ROOM_STATUS_HISTORY_KEY = STORE_KEY + '_status_history_v1'; const SAVED_TEMP_URLS_KEY = STORE_KEY + '_saved_temp_urls_v1'; const MAX_CONFIG_BYTES = 2 * 1024 * 1024; const MAX_CONFIG_BACKUPS = 3; const STORAGE_WRITER_ID = `writer_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`; const INJECTOR_ROUTE_POLL_VISIBLE_MS = 1000; const INJECTOR_ROUTE_POLL_HIDDEN_MS = 5000; const DEFAULT_WORKSPACE_ID = 'workspace_default'; const MAX_WORKSPACES = 60; const MAX_LAYOUT_TEMPLATES = 40; const DEFAULT_CUSTOM_PALETTE = { dark: { page: '#0b1118', panel: '#111f2c', surface: '#192a38', input: '#0f1d29', border: '#405061', text: '#f1f5f8', muted: '#aeb9c3', success: '#22c55e', warning: '#f59e0b', danger: '#ef4444', info: '#38bdf8' }, light: { page: '#f6f8fb', panel: '#ffffff', surface: '#f1f5f9', input: '#ffffff', border: '#cbd5e1', text: '#0f172a', muted: '#526174', success: '#15803d', warning: '#b45309', danger: '#b91c1c', info: '#0369a1' }, }; const ROOMGRID_BACKGROUND_PAGE_KEYS = Object.freeze(['dashboard', 'discover', 'tags', 'stats', 'library', 'user', 'settings', 'multicam', 'single', 'home', 'listing', 'room']); const defaultRoomGridBackground = (mode = 'inherit') => ({ mode, color: '#0b1118', gradientStart: '#172554', gradientEnd: '#0f766e', gradientAngle: 135, mediaOwner: '', mediaId: '', fit: 'cover', positionX: 50, positionY: 50, zoom: 100, opacity: 100, dim: 28, blur: 0, panelOpacity: 88, repeat: false, fixed: true, }); function sanitizeRoomGridBackground(input, fallbackMode = 'inherit') { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const fallback = defaultRoomGridBackground(fallbackMode); const allowedModes = fallbackMode === 'theme' ? ['theme', 'color', 'gradient', 'image'] : ['inherit', 'color', 'gradient', 'image']; const cleanReference = value => String(value || '').replace(/[\u0000-\u001f\u007f]/gu, '').trim().slice(0, 260); const owner = normalizeUsername(source.mediaOwner); return { mode: allowedModes.includes(source.mode) ? source.mode : fallback.mode, color: cleanColor(source.color, fallback.color), gradientStart: cleanColor(source.gradientStart, fallback.gradientStart), gradientEnd: cleanColor(source.gradientEnd, fallback.gradientEnd), gradientAngle: clampInt(source.gradientAngle, 0, 360, fallback.gradientAngle), mediaOwner: isLikelyUsername(owner) ? owner : '', mediaId: cleanReference(source.mediaId), fit: ['cover', 'contain', 'stretch', 'original', 'tile'].includes(source.fit) ? source.fit : fallback.fit, positionX: clampInt(source.positionX, 0, 100, fallback.positionX), positionY: clampInt(source.positionY, 0, 100, fallback.positionY), zoom: clampInt(source.zoom, 50, 240, fallback.zoom), opacity: clampInt(source.opacity, 10, 100, fallback.opacity), dim: clampInt(source.dim, 0, 90, fallback.dim), blur: clampInt(source.blur, 0, 32, fallback.blur), panelOpacity: clampInt(source.panelOpacity, 55, 100, fallback.panelOpacity), repeat: source.fit === 'tile' || !!source.repeat, fixed: source.fixed !== false, }; } const DEFAULT_ANALYTICS_WIDGETS = [ { id: 'usage', visible: true, size: 'small' }, { id: 'camera', visible: true, size: 'small' }, { id: 'activeDays', visible: true, size: 'small' }, { id: 'comparison', visible: true, size: 'small' }, { id: 'trends', visible: true, size: 'wide' }, { id: 'daily', visible: true, size: 'wide' }, { id: 'heatmap', visible: true, size: 'wide' }, { id: 'glance', visible: true, size: 'wide' }, { id: 'models', visible: false, size: 'small' }, { id: 'quality', visible: false, size: 'wide' }, { id: 'privacy', visible: true, size: 'wide' }, { id: 'provenance', visible: true, size: 'wide' }, ]; const defaultState = () => ({ v: 8, dataVersion: 3, revision: 0, updatedAt: 0, writerId: '', deletedRooms: {}, deletedGroups: {}, deletedWorkspaces: {}, rooms: [], // {id, addedAt, group, order, lastStatus, lastSeenOnline, muted, notes} groups: [ { id: LIBRARY_GROUP_ID, name: '__library__', order: 0, system: true }, { id: DEFAULT_GROUP_ID, name: '__all__', order: 1, system: true }, { id: ONLINE_GROUP_ID, name: '__online__', order: 2, system: true }, { id: FAVORITE_GROUP_ID, name: '__fav__', order: 3, system: true }, ], workspaces: [], layoutTemplates: [], settings: { volume: 0, gridSize: 400, gridCellSize: 80, layoutSize: 4, // one screen capacity: 2 | 4 | 6 | 9 | 12 | 15 useCustomLayout: false, customLayoutRows: 2, customLayoutCols: 2, pageIndex: 0, toolbarCollapsed: false, viewMode: 'grid', // 'grid' | 'focus' focusedRoomId: null, focusRoomSlots: [], // optional explicit Focus slots; index 0 is the main screen gridRoomSlots: {}, // optional explicit grid slots per view context focusMainPct: 62, // main screen width ratio in focus mode focusMainHPct: 64, // main screen height ratio in focus mode focusAspect: 'auto', focusThumbSize: 150, filter: { hideOffline: false, hidePrivate: false, onlyOnline: false, onlyWatched: false, neverWatched: false, favoritesOnly: false, followedOnly: false, inActiveView: false, hasNotes: false, tagInclude: '', tagExclude: '', minWatchMs: 0, minSessions: 0, lastWatchedDays: 0, viewerMin: 0, viewerMax: 0, }, sortBy: 'manual', notifyOnline: false, notifyOffline: false, theme: 'dark', brandPreset: 'aurora', brandName: 'Camclient', brandMonogram: 'CC', brandTagline: '', brandDisplayMode: 'monogram', brandLogoLight: '', brandLogoDark: '', brandLogoShape: 'rounded', brandLogoSize: 28, brandLogoWidth: 38, brandLogoHeight: 38, brandLogoFit: 'contain', brandLogoPositionX: 50, brandLogoPositionY: 50, brandLogoPadding: 0, brandLogoBackground: 'transparent', brandShowVersion: true, brandReplaceNativeLogo: false, brandPageTitleMode: 'native', brandCustomPageTitle: '', brandFaviconEnabled: false, accentColor: '#4f8cff', secondaryAccentColor: '#27c7b8', paletteMode: 'derived', customPalette: JSON.parse(JSON.stringify(DEFAULT_CUSTOM_PALETTE)), surfaceStyle: 'soft', density: 'comfortable', contentWidth: 'wide', cardStyle: 'balanced', inspectorEnabled: true, inspectorLastTab: 'overview', inspectorCompareIds: [], dashboardWidgetDensity: 'comfortable', fontFamily: 'system', fontScale: 1, controlScale: 1, cornerRadius: 10, cardGap: 12, shadowStrength: 55, surfaceOpacity: 100, backgroundStyle: 'solid', backgroundGlobal: defaultRoomGridBackground('theme'), backgroundPages: Object.fromEntries(ROOMGRID_BACKGROUND_PAGE_KEYS.map(key => [key, defaultRoomGridBackground('inherit')])), backgroundModels: {}, layoutPreset: 'balanced', layoutCardMinWidth: 260, layoutSectionGap: 16, layoutContentPadding: 18, layoutHeaderHeight: 56, layoutMaxColumns: 0, layoutSidebarPosition: 'left', layoutStickyHeader: true, layoutCardAspect: 'auto', highContrast: false, focusRingWidth: 3, minimumTargetSize: 36, underlineLinks: false, reduceTransparency: false, respectSystemMotion: true, largeTextBoost: false, appearanceProfiles: [], pageAppearanceProfilesEnabled: false, pageAppearanceProfiles: { home: '', listing: '', room: '', workspace: '', discover: '', hub: '' }, responsiveAppearanceProfilesEnabled: false, responsiveAppearanceProfiles: { desktop: { home: '', listing: '', room: '', workspace: '', discover: '', hub: '' }, tablet: { home: '', listing: '', room: '', workspace: '', discover: '', hub: '' }, mobile: { home: '', listing: '', room: '', workspace: '', discover: '', hub: '' }, }, appearanceBreakpoints: { mobileMax: 767, tabletMax: 1199 }, designPreviewDevice: 'desktop', designPreviewSurface: 'home', animations: true, navOrder: ['dashboard', 'discover', 'tags', 'stats', 'library', 'user', 'settings'], navHidden: [], settingsMenuFavorites: [], sidebarWidth: 220, sidebarBrowser: { query: '', tags: '', tagMode: 'all', excludeTags: '', country: '', gender: 'all', status: 'all', ageMin: 0, ageMax: 0, viewerMin: 0, viewerMax: 0, local: 'all', sort: 'manual', expanded: false }, activeWorkspaceId: DEFAULT_WORKSPACE_ID, roomGridCustomHomeEnabled: true, normalPageDesign: true, normalDesignHome: true, normalDesignListings: true, normalDesignRoom: true, normalPageView: 'grid', normalPageCardScale: 100, normalPageDensity: 'comfortable', normalHomeToolbar: true, normalHomePageSize: 24, normalHomePage: 1, normalHomeColumns: 0, normalHomeImageRatio: 'auto', normalHomeSort: 'native', normalHomeSearch: '', normalHomeStatus: 'all', normalHomeGender: 'all', normalHomeCountryFilter: '', normalHomeTagFilter: '', normalHomeTagMatchMode: 'all', normalHomeFilterPresets: [], normalHomeMinViewers: 0, normalHomeMaxViewers: 0, normalHomeMinAge: 0, normalHomeMaxAge: 0, normalHomeOnlyHd: false, normalHomeOnlyNew: false, normalHomeOnlyGaming: false, normalHomeOnlySaved: false, normalHomeOnlyFavorites: false, normalHomeOnlyFollowing: false, normalHomeHideVisited: false, normalHomeShowCountry: true, normalHomeShowLocation: true, normalHomeShowViewers: true, normalHomeShowAge: true, normalHomeShowStatus: true, normalHomeShowTags: true, normalHomeShowSubject: true, normalHomeShowOnlineTime: true, normalHomeShowLocalStats: false, normalHomeShowBadges: true, normalHomeMetaStyle: 'compact', normalHidePromos: false, normalCompactFooter: true, normalPolishNavigation: true, normalPolishSidebar: true, normalMarkSaved: true, normalMarkFavorites: true, normalMarkVisited: true, normalQuickActions: true, normalLivePreviews: true, normalLivePreviewFps: .5, normalLivePreviewMode: 'simultaneous', normalLivePreviewConcurrency: 0, normalHoverLivePreview: true, normalHoverLivePreviewDelay: 250, normalHoverLivePreviewScale: 112, previewDockEnabled: true, previewDockMax: 3, previewDockOpacity: .96, previewDockSize: 'medium', chatFocusEnabled: false, chatTimestampEnabled: true, chatSystemMode: 'dim', chatFilterLevel: 'low', chatHighlightWords: '', chatMutedWords: '', chatCollapseRepeats: true, privacyIdleMinutes: 0, activeGroup: 'all', searchQuery: '', pureMode: false, viewerMode: false, focusThumbsCollapsed: false, videoFit: 'contain', freeZoom: true, maxStreamHeight: 1080, videoTransforms: {}, showRecordingOnly: false, favoriteFirst: true, shortcuts: defaultShortcuts(), recordingSegmentMinutes: 10, recordingVideoBitrate: 6000000, recordingExitWarn: true, pollMs: { offline: 60000, private: 30000, error: 10000, online: 120000 }, sidebarCollapsed: false, sidebarHoverPreview: true, followedSyncEnabled: true, followedGroupId: null, followedSyncLastAt: 0, analyticsEnabled: true, analyticsDashboardPreset: 'balanced', analyticsDashboardWidgets: JSON.parse(JSON.stringify(DEFAULT_ANALYTICS_WIDGETS)), dailyTimeBudgetMinutes: 0, breakReminderMinutes: 0, performanceProfile: 'balanced', performanceMaxStreams: 8, adaptiveQuality: true, qualityBudgetHeight: 4320, qualityPrioritizeFocused: true, gradualStart: true, audioMeterVisible: true, multicamMeterDisplayMode: 'individual', audioRoutingMode: 'manual', audioSoloRoomId: '', multicamScenes: [], activeMulticamSceneId: '', autoPerformanceMode: false, autoPerformanceMemoryPct: 78, autoPerformanceStreamThreshold: 10, smartCollections: [], automationRules: [], windowName: '', }, }); function cleanColor(value, fallback = '#2563eb') { const text = String(value || '').trim(); return /^#[0-9a-f]{6}$/iu.test(text) ? text.toLowerCase() : fallback; } function sanitizeBrandLogoDataUrl(value) { const data = String(value || '').trim(); if (!data || data.length > 1600000) return ''; if (/^data:image\/(?:png|jpe?g|webp);base64,[a-z0-9+/=\s]+$/iu.test(data)) return data.replace(/\s+/gu, ''); const svgMatch = data.match(/^data:image\/svg\+xml(?:;charset=utf-8)?(?:;base64)?,([\s\S]+)$/iu); if (!svgMatch) return ''; let svg = ''; try { svg = /;base64,/iu.test(data) ? decodeURIComponent(escape(atob(svgMatch[1].replace(/\s+/gu, '')))) : decodeURIComponent(svgMatch[1]); } catch (_) { return ''; } if (!/^\s*\s*$/iu.test(svg)) return ''; if (/<\s*(?:script|foreignObject|iframe|object|embed|image|use|style)\b|\bon[a-z]+\s*=|(?:href|xlink:href)\s*=|url\s*\(|@import/iu.test(svg)) return ''; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; } function sanitizeCustomPalette(value) { const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; const roles = ['page', 'panel', 'surface', 'input', 'border', 'text', 'muted', 'success', 'warning', 'danger', 'info']; return Object.fromEntries(['dark', 'light'].map(mode => [mode, Object.fromEntries(roles.map(role => [role, cleanColor(source?.[mode]?.[role], DEFAULT_CUSTOM_PALETTE[mode][role])]))])); } function cleanWorkspaceLayout(input) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const output = {}; for (const key of WORKSPACE_LAYOUT_KEYS) { if (!(key in source)) continue; try { output[key] = JSON.parse(JSON.stringify(source[key])); } catch (_) { output[key] = source[key]; } } return output; } function sanitizeWorkspace(input, index = 0, fallbackLayout = {}) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; let id = String(source.id || '').trim(); if (!/^[a-z0-9_-]{1,64}$/iu.test(id)) id = index === 0 ? DEFAULT_WORKSPACE_ID : `workspace_${uuid().replace(/^g_/u, '')}`; const legacyLayout = cleanWorkspaceLayout(source.layout || fallbackLayout); const rawDevices = source.deviceLayouts && typeof source.deviceLayouts === 'object' && !Array.isArray(source.deviceLayouts) ? source.deviceLayouts : {}; const desktop = cleanWorkspaceLayout(rawDevices.desktop || legacyLayout); const compact = cleanWorkspaceLayout(rawDevices.compact || legacyLayout); return { id, name: safeGroupName(source.name, index === 0 ? tx('默认工作区', 'Default workspace', 'Standard-Arbeitsbereich') : tx('工作区', 'Workspace', 'Arbeitsbereich')), description: String(source.description || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 300), order: clampInt(source.order, 0, 10000, index), archived: !!source.archived, createdAt: Math.max(0, numeric(source.createdAt, Date.now())), modifiedAt: Math.max(0, numeric(source.modifiedAt, Date.now())), lastOpenedAt: Math.max(0, numeric(source.lastOpenedAt, 0)), replacement: { enabled: !!source.replacement?.enabled, groupId: String(source.replacement?.groupId || '').slice(0, 80), onlyOnline: source.replacement?.onlyOnline !== false, delaySeconds: clampInt(source.replacement?.delaySeconds, 0, 600, 20), fallbackRoomIds: [...new Set((Array.isArray(source.replacement?.fallbackRoomIds) ? source.replacement.fallbackRoomIds : []) .map(normalizeUsername).filter(id => usernameSyntaxOk(id)))].slice(0, 20), }, deviceLayouts: { desktop, compact }, }; } function sanitizeLayoutTemplate(input, index = 0) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const name = safeGroupName(source.name, ''); if (!name) return null; return { id: /^[a-z0-9_-]{1,64}$/iu.test(String(source.id || '')) ? String(source.id) : `layout_${uuid().replace(/^g_/u, '')}`, name, description: String(source.description || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 240), order: clampInt(source.order, 0, 10000, index), createdAt: Math.max(0, numeric(source.createdAt, Date.now())), modifiedAt: Math.max(0, numeric(source.modifiedAt, Date.now())), layout: cleanWorkspaceLayout(source.layout), }; } function ensureSystemGroups(state) { if (!state || typeof state !== 'object') return state; state.groups = Array.isArray(state.groups) ? state.groups : []; const wanted = [ { id: LIBRARY_GROUP_ID, name: '__library__', order: 0, system: true }, { id: DEFAULT_GROUP_ID, name: '__all__', order: 1, system: true }, { id: ONLINE_GROUP_ID, name: '__online__', order: 2, system: true }, { id: FAVORITE_GROUP_ID, name: '__fav__', order: 3, system: true }, ]; for (const g of wanted) { const found = state.groups.find(x => x.id === g.id); if (!found) state.groups.push({ ...g }); else { found.name = g.name; found.system = true; found.order = g.order; } } state.groups.sort((a, b) => numeric(a.order, 999) - numeric(b.order, 999)); return state; } /* ============================================================= * 1.1. 分组成员关系 / Group membership * v12 模型:所有显示分组都是独立成员关系;library 是唯一聚合管理视图。 * 在普通分组点 X 只移出当前分组;在 library 点 X 才彻底删除。 * ============================================================= */ function uniq(arr) { return [...new Set((arr || []).filter(Boolean))]; } function numeric(v, fallback = 0) { const n = Number(v); return Number.isFinite(n) ? n : fallback; } function clampInt(v, min, max, fallback = min) { const n = Math.round(Number(v)); if (!Number.isFinite(n)) return fallback; return Math.max(min, Math.min(max, n)); } function normalizeCardSize(size) { if (!size || typeof size !== 'object') return null; const cols = clampInt(size.cols ?? size.columns ?? size.w, 3, 18, 0); const rows = clampInt(size.rows ?? size.h, 3, 18, 0); if (!cols || !rows) return null; return { cols, rows }; } function normalizeCardSizeMap(map) { const out = {}; if (map && typeof map === 'object' && !Array.isArray(map)) { for (const [k, v] of Object.entries(map)) { const key = String(k || '').trim() || 'all'; const size = normalizeCardSize(v); if (size) out[key] = size; } } return out; } function setCardSizeForGroup(room, groupId, size) { if (!room) return; const key = String(groupId || 'all'); const next = normalizeCardSize(size); const map = normalizeCardSizeMap(room.cardSizeByGroup); if (next) map[key] = next; else delete map[key]; room.cardSizeByGroup = map; delete room.cardSize; if (!Object.keys(room.cardSizeByGroup).length) delete room.cardSizeByGroup; } function getRoomGroups(room) { const groups = Array.isArray(room?.groups) ? room.groups.slice() : []; if (room?.group) groups.push(room.group); // v12: all/default is a real group; only library is an aggregate view and is never stored as membership. return uniq(groups.filter(g => g && g !== LIBRARY_GROUP_ID && g !== ONLINE_GROUP_ID)); } const ROOM_SYNC_FIELDS = ['groups', 'groupOrder', 'cardSizeByGroup', 'lastStatus', 'lastSeenOnline', 'viewerCount', 'muted', 'volume', 'privateLabel', 'errorMsg', 'notes', 'localTags', 'rating', 'priority', 'watchlist', 'archived', 'suggestionHidden', 'hiddenUntil', 'pausedUntil', 'qualityHeight', 'audioMeterVisible']; function sanitizeFieldModifiedAt(value) { const out = {}; if (!value || typeof value !== 'object' || Array.isArray(value)) return out; for (const key of ROOM_SYNC_FIELDS) { const timestamp = Math.max(0, numeric(value[key], 0)); if (timestamp) out[key] = timestamp; } return out; } function normalizeRoom(room, fallbackOrder = 0) { const r = room && typeof room === 'object' ? room : { id: String(room || '') }; r.id = normalizeUsername(r.id); r.groups = getRoomGroups(r); r.group = r.groups[0] || null; r.addedAt = numeric(r.addedAt, Date.now()); r.modifiedAt = Math.max(0, numeric(r.modifiedAt, r.addedAt)); r.fieldModifiedAt = sanitizeFieldModifiedAt(r.fieldModifiedAt); r.order = numeric(r.order, fallbackOrder); if (!r.groupOrder || typeof r.groupOrder !== 'object' || Array.isArray(r.groupOrder)) r.groupOrder = {}; for (const g of r.groups) r.groupOrder[g] = numeric(r.groupOrder[g], r.order); if (!r.lastStatus) r.lastStatus = 'unknown'; r.lastSeenOnline = numeric(r.lastSeenOnline, 0); if (r.viewerCount != null && Number.isFinite(Number(r.viewerCount))) r.viewerCount = Math.max(0, Math.round(Number(r.viewerCount))); else delete r.viewerCount; r.muted = !!r.muted; if (typeof r.audioMeterVisible !== 'boolean') delete r.audioMeterVisible; if (r.volume != null && Number.isFinite(Number(r.volume))) r.volume = Math.max(0, Math.min(1, Number(r.volume))); else delete r.volume; if (r.notes) r.notes = String(r.notes).slice(0, 4000); r.localTags = [...new Set((Array.isArray(r.localTags) ? r.localTags : []) .map(value => String(value || '').trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))].slice(0, 40); if (!r.localTags.length) delete r.localTags; const sizeMap = normalizeCardSizeMap(r.cardSizeByGroup); const legacyCardSize = normalizeCardSize(r.cardSize); if (legacyCardSize && !sizeMap.all) sizeMap.all = legacyCardSize; if (Object.keys(sizeMap).length) r.cardSizeByGroup = sizeMap; else delete r.cardSizeByGroup; delete r.cardSize; return r; } function mergeRoomRecords(leftInput, rightInput) { const left = normalizeRoom({ ...(leftInput || {}) }); const right = normalizeRoom({ ...(rightInput || {}) }); const leftTime = Number(left.modifiedAt || left.addedAt || 0); const rightTime = Number(right.modifiedAt || right.addedAt || 0); const result = rightTime >= leftTime ? { ...left, ...right } : { ...right, ...left }; result.fieldModifiedAt = {}; for (const key of ROOM_SYNC_FIELDS) { const leftExplicit = Object.prototype.hasOwnProperty.call(left.fieldModifiedAt || {}, key); const rightExplicit = Object.prototype.hasOwnProperty.call(right.fieldModifiedAt || {}, key); const leftFieldTime = Number(left.fieldModifiedAt?.[key] || (leftExplicit || rightExplicit ? 0 : leftTime)); const rightFieldTime = Number(right.fieldModifiedAt?.[key] || (leftExplicit || rightExplicit ? 0 : rightTime)); const source = rightFieldTime >= leftFieldTime ? right : left; if (Object.prototype.hasOwnProperty.call(source, key)) { try { result[key] = JSON.parse(JSON.stringify(source[key])); } catch (_) { result[key] = source[key]; } } else delete result[key]; const fieldTime = Math.max(leftFieldTime, rightFieldTime); if ((leftExplicit || rightExplicit) && fieldTime) result.fieldModifiedAt[key] = fieldTime; } result.id = left.id || right.id; result.addedAt = Math.min(Number(left.addedAt) || Date.now(), Number(right.addedAt) || Date.now()); result.modifiedAt = Math.max(leftTime, rightTime); return normalizeRoom(result); } function normalizeStateMemberships(state) { if (!state || typeof state !== 'object') return state; state.rooms = Array.isArray(state.rooms) ? state.rooms : []; const merged = new Map(); state.rooms.forEach((room, idx) => { const r = normalizeRoom(room, idx); if (!r.id) return; const prev = merged.get(r.id); if (!prev) { merged.set(r.id, r); return; } const groups = uniq([...getRoomGroups(prev), ...getRoomGroups(r)]); prev.groups = groups; prev.group = groups[0] || null; prev.groupOrder = { ...(r.groupOrder || {}), ...(prev.groupOrder || {}) }; for (const g of groups) prev.groupOrder[g] = numeric(prev.groupOrder[g], prev.order); prev.order = Math.min(numeric(prev.order, idx), numeric(r.order, idx)); const mergedSizeMap = { ...(r.cardSizeByGroup || {}), ...(prev.cardSizeByGroup || {}) }; if (Object.keys(mergedSizeMap).length) prev.cardSizeByGroup = normalizeCardSizeMap(mergedSizeMap); else delete prev.cardSizeByGroup; delete prev.cardSize; prev.addedAt = Math.min(numeric(prev.addedAt, Date.now()), numeric(r.addedAt, Date.now())); prev.modifiedAt = Math.max(numeric(prev.modifiedAt, prev.addedAt), numeric(r.modifiedAt, r.addedAt)); if (r.lastStatus && r.lastStatus !== 'unknown') prev.lastStatus = r.lastStatus; if (r.lastSeenOnline) prev.lastSeenOnline = Math.max(numeric(prev.lastSeenOnline, 0), numeric(r.lastSeenOnline, 0)); if (r.viewerCount != null) prev.viewerCount = Math.max(Number(prev.viewerCount) || 0, Number(r.viewerCount) || 0); prev.muted = !!(prev.muted || r.muted); if (prev.volume == null && r.volume != null) prev.volume = r.volume; if (!prev.notes && r.notes) prev.notes = r.notes; const mergedLocalTags = uniq([...(prev.localTags || []), ...(r.localTags || [])]).slice(0, 40); if (mergedLocalTags.length) prev.localTags = mergedLocalTags; }); state.rooms = [...merged.values()]; return state; } function sanitizeState(input) { const def = defaultState(); const src = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const out = { ...def, v: numeric(src.v, def.v), dataVersion: clampInt(src.dataVersion, 1, 999, def.dataVersion), revision: Math.max(0, Math.round(numeric(src.revision, 0))), updatedAt: Math.max(0, numeric(src.updatedAt, 0)), writerId: String(src.writerId || '').slice(0, 80), deletedRooms: {}, deletedGroups: {}, deletedWorkspaces: {}, }; if (src.deletedRooms && typeof src.deletedRooms === 'object' && !Array.isArray(src.deletedRooms)) { for (const [rawId, rawAt] of Object.entries(src.deletedRooms).slice(-MAX_STORED_ROOMS)) { const id = normalizeUsername(rawId); if (isLikelyUsername(id)) out.deletedRooms[id] = Math.max(0, numeric(rawAt, 0)); } } if (src.deletedGroups && typeof src.deletedGroups === 'object' && !Array.isArray(src.deletedGroups)) { for (const [rawId, rawAt] of Object.entries(src.deletedGroups).slice(-80)) { const id = String(rawId || '').trim(); if (/^[a-z0-9_-]{1,48}$/iu.test(id) && ![LIBRARY_GROUP_ID, DEFAULT_GROUP_ID, ONLINE_GROUP_ID, FAVORITE_GROUP_ID].includes(id)) out.deletedGroups[id] = Math.max(0, numeric(rawAt, 0)); } } if (src.deletedWorkspaces && typeof src.deletedWorkspaces === 'object' && !Array.isArray(src.deletedWorkspaces)) { for (const [rawId, rawAt] of Object.entries(src.deletedWorkspaces).slice(-MAX_WORKSPACES)) { const id = String(rawId || '').trim(); if (/^[a-z0-9_-]{1,64}$/iu.test(id) && id !== DEFAULT_WORKSPACE_ID) out.deletedWorkspaces[id] = Math.max(0, numeric(rawAt, 0)); } } const rawGroups = Array.isArray(src.groups) ? src.groups.slice(0, 80) : def.groups; const seenGroups = new Set(); out.groups = []; rawGroups.forEach((g, idx) => { if (!g || typeof g !== 'object') return; let id = String(g.id || '').trim(); if (!id || !/^[a-z0-9_-]{1,48}$/i.test(id)) id = uuid(); if (seenGroups.has(id)) return; seenGroups.add(id); let name = safeGroupName(g.name, id); if (!name) name = id; const modifiedAt = Math.max(0, numeric(g.modifiedAt, 0)); const deletedAt = Number(out.deletedGroups[id] || 0); if (!g.system && deletedAt > 0 && deletedAt >= modifiedAt) return; out.groups.push({ id, name, description: String(g.description || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 240), parentId: String(g.parentId || '').trim().slice(0, 48), archived: !!g.archived, order: clampInt(g.order, 0, 10000, idx), system: !!g.system && [LIBRARY_GROUP_ID, DEFAULT_GROUP_ID, ONLINE_GROUP_ID, FAVORITE_GROUP_ID].includes(id), modifiedAt, }); }); ensureSystemGroups(out); const allGroupIds = new Set(out.groups.map(group => group.id)); out.groups.forEach(group => { if (group.system || group.parentId === group.id || !allGroupIds.has(group.parentId)) group.parentId = ''; }); const validGroupIds = new Set(out.groups.map(g => g.id).filter(id => id !== LIBRARY_GROUP_ID && id !== ONLINE_GROUP_ID)); const allowedStatus = new Set(['online', 'offline', 'private', 'loading', 'error', 'unknown']); const rawRooms = Array.isArray(src.rooms) ? src.rooms.slice(0, MAX_STORED_ROOMS) : []; out.rooms = rawRooms.map((room, idx) => { const r = normalizeRoom(room, idx); if (!isLikelyUsername(r.id)) return null; const groups = getRoomGroups(r).filter(g => validGroupIds.has(g)); const safe = { id: r.id, addedAt: Math.max(0, numeric(r.addedAt, Date.now())), modifiedAt: Math.max(0, numeric(r.modifiedAt, r.addedAt || Date.now())), fieldModifiedAt: sanitizeFieldModifiedAt(r.fieldModifiedAt), group: groups[0] || null, groups, groupOrder: {}, order: clampInt(r.order, 0, 1000000, idx), lastStatus: allowedStatus.has(r.lastStatus) ? r.lastStatus : 'unknown', lastSeenOnline: Math.max(0, numeric(r.lastSeenOnline, 0)), muted: !!r.muted, }; if (typeof r.audioMeterVisible === 'boolean') safe.audioMeterVisible = r.audioMeterVisible; if (r.volume != null) safe.volume = Math.max(0, Math.min(1, Number(r.volume))); if (r.viewerCount != null && Number.isFinite(Number(r.viewerCount))) safe.viewerCount = Math.max(0, Math.round(Number(r.viewerCount))); if (r.privateLabel) safe.privateLabel = String(r.privateLabel).slice(0, 40); if (r.errorMsg) safe.errorMsg = String(r.errorMsg).slice(0, 120); if (r.notes) safe.notes = String(r.notes).slice(0, 4000); if (r.localTags?.length) safe.localTags = r.localTags.slice(0, 40); safe.rating = clampInt(r.rating, 0, 5, 0); safe.priority = clampInt(r.priority, 0, 5, 0); safe.watchlist = !!r.watchlist; safe.archived = !!r.archived; safe.suggestionHidden = !!r.suggestionHidden; safe.hiddenUntil = Math.max(0, numeric(r.hiddenUntil, 0)); safe.pausedUntil = Math.max(0, numeric(r.pausedUntil, 0)); safe.qualityHeight = [0, 240, 360, 480, 720, 1080, 1440, 2160].includes(Number(r.qualityHeight)) ? Number(r.qualityHeight) : 0; for (const g of groups) safe.groupOrder[g] = clampInt(r.groupOrder?.[g], 0, 1000000, safe.order); const sizeMap = normalizeCardSizeMap(r.cardSizeByGroup); if (Object.keys(sizeMap).length) safe.cardSizeByGroup = sizeMap; return safe; }).filter(Boolean); normalizeStateMemberships(out); const st = src.settings && typeof src.settings === 'object' && !Array.isArray(src.settings) ? src.settings : {}; out.settings = { ...def.settings, ...st }; const allowedSettingKeys = new Set([...Object.keys(def.settings), '__focusAutoMigratedV12']); for (const key of Object.keys(out.settings)) { if (!allowedSettingKeys.has(key)) delete out.settings[key]; } out.settings.filter = { ...def.settings.filter, ...(st.filter && typeof st.filter === 'object' ? st.filter : {}) }; out.settings.filter.hideOffline = !!out.settings.filter.hideOffline; out.settings.filter.hidePrivate = !!out.settings.filter.hidePrivate; out.settings.filter.onlyOnline = !!out.settings.filter.onlyOnline; out.settings.filter.onlyWatched = !!out.settings.filter.onlyWatched; out.settings.filter.neverWatched = !!out.settings.filter.neverWatched; out.settings.filter.favoritesOnly = !!out.settings.filter.favoritesOnly; out.settings.filter.followedOnly = !!out.settings.filter.followedOnly; out.settings.filter.inActiveView = !!out.settings.filter.inActiveView; out.settings.filter.hasNotes = !!out.settings.filter.hasNotes; out.settings.filter.tagInclude = String(out.settings.filter.tagInclude || '').slice(0, 160); out.settings.filter.tagExclude = String(out.settings.filter.tagExclude || '').slice(0, 160); out.settings.filter.minWatchMs = Math.max(0, numeric(out.settings.filter.minWatchMs, 0)); out.settings.filter.minSessions = Math.max(0, Math.round(numeric(out.settings.filter.minSessions, 0))); out.settings.filter.lastWatchedDays = Math.max(0, Math.round(numeric(out.settings.filter.lastWatchedDays, 0))); out.settings.filter.viewerMin = Math.max(0, numeric(out.settings.filter.viewerMin, 0)); out.settings.filter.viewerMax = Math.max(0, numeric(out.settings.filter.viewerMax, 0)); out.settings.volume = Math.max(0, Math.min(1, Number(out.settings.volume) || 0)); out.settings.gridSize = clampInt(out.settings.gridSize, 220, 900, def.settings.gridSize); out.settings.gridCellSize = clampInt(out.settings.gridCellSize, 56, 120, def.settings.gridCellSize); out.settings.layoutSize = SUPPORTED_LAYOUT_SIZES.includes(Number(out.settings.layoutSize)) ? Number(out.settings.layoutSize) : def.settings.layoutSize; out.settings.useCustomLayout = !!out.settings.useCustomLayout; out.settings.customLayoutRows = clampInt(out.settings.customLayoutRows, 1, 5, def.settings.customLayoutRows); out.settings.customLayoutCols = clampInt(out.settings.customLayoutCols, 1, 5, def.settings.customLayoutCols); if (out.settings.customLayoutRows * out.settings.customLayoutCols > MAX_LAYOUT_SLOTS) { out.settings.customLayoutCols = Math.max(1, Math.floor(MAX_LAYOUT_SLOTS / out.settings.customLayoutRows)); } out.settings.pageIndex = clampInt(out.settings.pageIndex, 0, 100000, 0); out.settings.viewMode = out.settings.viewMode === 'focus' ? 'focus' : 'grid'; out.settings.focusedRoomId = isLikelyUsername(out.settings.focusedRoomId) ? normalizeUsername(out.settings.focusedRoomId) : null; out.settings.focusRoomSlots = Array.isArray(st.focusRoomSlots) ? st.focusRoomSlots.slice(0, MAX_LAYOUT_SLOTS).map(id => isLikelyUsername(id) ? normalizeUsername(id) : null) : []; out.settings.gridRoomSlots = sanitizeGridRoomSlots(st.gridRoomSlots); out.settings.focusMainPct = clampInt(out.settings.focusMainPct, 45, 76, def.settings.focusMainPct); out.settings.focusMainHPct = clampInt(out.settings.focusMainHPct, 44, 78, def.settings.focusMainHPct); out.settings.focusAspect = ['auto', '16:9', '4:3', '1:1', '9:16'].includes(out.settings.focusAspect) ? out.settings.focusAspect : 'auto'; out.settings.focusThumbSize = clampInt(out.settings.focusThumbSize, 96, 260, def.settings.focusThumbSize); out.settings.sortBy = ['manual', 'status', 'name', 'addedAt', 'watchTime', 'sessions', 'lastWatched', 'firstWatched', 'viewerCount', 'lastOnline'].includes(out.settings.sortBy) ? out.settings.sortBy : 'manual'; out.settings.activeGroup = out.groups.some(g => g.id === out.settings.activeGroup) ? out.settings.activeGroup : DEFAULT_GROUP_ID; out.settings.searchQuery = normalizeUsername(out.settings.searchQuery || ''); out.settings.pureMode = false; out.settings.viewerMode = !!out.settings.viewerMode; out.settings.focusThumbsCollapsed = !!out.settings.focusThumbsCollapsed; out.settings.videoFit = out.settings.videoFit === 'cover' ? 'cover' : 'contain'; out.settings.freeZoom = out.settings.freeZoom !== false; out.settings.maxStreamHeight = [0, 240, 360, 480, 720, 1080, 1440, 2160].includes(Number(out.settings.maxStreamHeight)) ? Number(out.settings.maxStreamHeight) : def.settings.maxStreamHeight; out.settings.videoTransforms = sanitizeVideoTransformMap(st.videoTransforms); out.settings.showRecordingOnly = false; out.settings.favoriteFirst = out.settings.favoriteFirst !== false; out.settings.shortcuts = sanitizeShortcuts(st.shortcuts, def.settings.shortcuts); out.settings.recordingSegmentMinutes = clampInt(out.settings.recordingSegmentMinutes, 1, 180, def.settings.recordingSegmentMinutes); out.settings.recordingVideoBitrate = clampInt(out.settings.recordingVideoBitrate, 500000, 20000000, def.settings.recordingVideoBitrate); out.settings.recordingExitWarn = out.settings.recordingExitWarn !== false; out.settings.toolbarCollapsed = !!out.settings.toolbarCollapsed; out.settings.sidebarCollapsed = !!out.settings.sidebarCollapsed; out.settings.sidebarHoverPreview = out.settings.sidebarHoverPreview !== false; out.settings.followedSyncEnabled = out.settings.followedSyncEnabled !== false; out.settings.followedGroupId = typeof out.settings.followedGroupId === 'string' && out.settings.followedGroupId.length <= 80 ? out.settings.followedGroupId : null; out.settings.followedSyncLastAt = Math.max(0, numeric(out.settings.followedSyncLastAt, 0)); out.settings.analyticsEnabled = out.settings.analyticsEnabled !== false; out.settings.analyticsDashboardPreset = ['balanced', 'minimal', 'quality', 'privacy', 'custom'].includes(out.settings.analyticsDashboardPreset) ? out.settings.analyticsDashboardPreset : def.settings.analyticsDashboardPreset; const widgetIds = new Set(DEFAULT_ANALYTICS_WIDGETS.map(item => item.id)); const rawAnalyticsWidgets = Array.isArray(out.settings.analyticsDashboardWidgets) ? out.settings.analyticsDashboardWidgets : []; const seenAnalyticsWidgets = new Set(); out.settings.analyticsDashboardWidgets = rawAnalyticsWidgets.map(item => { const id = String(item?.id || ''); if (!widgetIds.has(id) || seenAnalyticsWidgets.has(id)) return null; seenAnalyticsWidgets.add(id); return { id, visible: item?.visible !== false, size: ['small', 'wide', 'full'].includes(item?.size) ? item.size : 'wide' }; }).filter(Boolean); DEFAULT_ANALYTICS_WIDGETS.forEach(item => { if (!seenAnalyticsWidgets.has(item.id)) out.settings.analyticsDashboardWidgets.push({ ...item }); }); out.settings.dailyTimeBudgetMinutes = clampInt(out.settings.dailyTimeBudgetMinutes, 0, 1440, def.settings.dailyTimeBudgetMinutes); out.settings.breakReminderMinutes = clampInt(out.settings.breakReminderMinutes, 0, 480, def.settings.breakReminderMinutes); out.settings.notifyOnline = out.settings.notifyOnline === true; out.settings.notifyOffline = out.settings.notifyOffline === true; out.settings.performanceProfile = ['eco', 'balanced', 'power'].includes(out.settings.performanceProfile) ? out.settings.performanceProfile : 'balanced'; out.settings.performanceMaxStreams = clampInt(out.settings.performanceMaxStreams, 1, MAX_LAYOUT_SLOTS, out.settings.performanceProfile === 'eco' ? 4 : out.settings.performanceProfile === 'power' ? MAX_LAYOUT_SLOTS : 8); out.settings.adaptiveQuality = out.settings.adaptiveQuality !== false; out.settings.qualityBudgetHeight = [0, 2160, 4320, 6480, 8640, 12960, 17280].includes(Number(out.settings.qualityBudgetHeight)) ? Number(out.settings.qualityBudgetHeight) : def.settings.qualityBudgetHeight; out.settings.qualityPrioritizeFocused = out.settings.qualityPrioritizeFocused !== false; out.settings.gradualStart = out.settings.gradualStart !== false; out.settings.audioRoutingMode = ['manual', 'solo', 'focus'].includes(out.settings.audioRoutingMode) ? out.settings.audioRoutingMode : def.settings.audioRoutingMode; out.settings.audioMeterVisible = out.settings.audioMeterVisible !== false; out.settings.multicamMeterDisplayMode = ['individual', 'all-show', 'all-hide'].includes(out.settings.multicamMeterDisplayMode) ? out.settings.multicamMeterDisplayMode : 'individual'; out.settings.audioSoloRoomId = usernameSyntaxOk(normalizeUsername(out.settings.audioSoloRoomId)) ? normalizeUsername(out.settings.audioSoloRoomId) : ''; out.settings.multicamScenes = (Array.isArray(out.settings.multicamScenes) ? out.settings.multicamScenes : []).slice(-24).map((scene, index) => { const source = scene && typeof scene === 'object' && !Array.isArray(scene) ? scene : {}; const cleanIds = value => (Array.isArray(value) ? value : []).slice(0, MAX_LAYOUT_SLOTS).map(id => usernameSyntaxOk(normalizeUsername(id)) ? normalizeUsername(id) : null); return { id: String(source.id || `scene_${index}`).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || `scene_${index}`, name: String(source.name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), workspaceId: String(source.workspaceId || DEFAULT_WORKSPACE_ID).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || DEFAULT_WORKSPACE_ID, createdAt: Math.max(0, Number(source.createdAt) || Date.now()), modifiedAt: Math.max(0, Number(source.modifiedAt) || Number(source.createdAt) || Date.now()), viewMode: source.viewMode === 'focus' ? 'focus' : 'grid', layoutSize: SUPPORTED_LAYOUT_SIZES.includes(Number(source.layoutSize)) ? Number(source.layoutSize) : 4, useCustomLayout: !!source.useCustomLayout, customLayoutRows: clampInt(source.customLayoutRows, 1, 5, 2), customLayoutCols: clampInt(source.customLayoutCols, 1, 5, 2), focusedRoomId: usernameSyntaxOk(normalizeUsername(source.focusedRoomId)) ? normalizeUsername(source.focusedRoomId) : '', focusRoomSlots: cleanIds(source.focusRoomSlots), gridRoomSlots: cleanIds(source.gridRoomSlots), audioRoutingMode: ['manual', 'solo', 'focus'].includes(source.audioRoutingMode) ? source.audioRoutingMode : 'manual', audioSoloRoomId: usernameSyntaxOk(normalizeUsername(source.audioSoloRoomId)) ? normalizeUsername(source.audioSoloRoomId) : '', }; }).filter(scene => scene.name); const sceneIds = new Set(out.settings.multicamScenes.map(scene => scene.id)); out.settings.activeMulticamSceneId = sceneIds.has(String(out.settings.activeMulticamSceneId || '')) ? String(out.settings.activeMulticamSceneId) : ''; out.settings.autoPerformanceMode = !!out.settings.autoPerformanceMode; out.settings.autoPerformanceMemoryPct = clampInt(out.settings.autoPerformanceMemoryPct, 50, 95, def.settings.autoPerformanceMemoryPct); out.settings.autoPerformanceStreamThreshold = clampInt(out.settings.autoPerformanceStreamThreshold, 2, MAX_LAYOUT_SLOTS, def.settings.autoPerformanceStreamThreshold); out.settings.windowName = String(out.settings.windowName || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 60); out.settings.theme = out.settings.theme === 'light' ? 'light' : 'dark'; out.settings.brandPreset = ['aurora', 'ember', 'frost', 'custom'].includes(out.settings.brandPreset) ? out.settings.brandPreset : def.settings.brandPreset; const cleanBrandText = (value, fallback, max) => String(value || fallback).replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, max) || fallback; out.settings.brandName = cleanBrandText(out.settings.brandName, def.settings.brandName, 40); if (/^RoomGrid(?: Suite)?$/iu.test(out.settings.brandName)) out.settings.brandName = 'Camclient'; out.settings.brandMonogram = cleanBrandText(out.settings.brandMonogram, def.settings.brandMonogram, 5).toLocaleUpperCase(); if (out.settings.brandMonogram === 'RG') out.settings.brandMonogram = 'CC'; out.settings.brandTagline = cleanBrandText(out.settings.brandTagline, def.settings.brandTagline, 80); out.settings.brandDisplayMode = ['monogram', 'text', 'logo'].includes(out.settings.brandDisplayMode) ? out.settings.brandDisplayMode : def.settings.brandDisplayMode; out.settings.brandLogoLight = sanitizeBrandLogoDataUrl(out.settings.brandLogoLight); out.settings.brandLogoDark = sanitizeBrandLogoDataUrl(out.settings.brandLogoDark); out.settings.brandLogoShape = ['rounded', 'circle', 'square', 'pill'].includes(out.settings.brandLogoShape) ? out.settings.brandLogoShape : def.settings.brandLogoShape; out.settings.brandLogoSize = clampInt(out.settings.brandLogoSize, 20, 64, def.settings.brandLogoSize); out.settings.brandLogoWidth = clampInt(out.settings.brandLogoWidth, 20, 240, out.settings.brandLogoSize || def.settings.brandLogoWidth); out.settings.brandLogoHeight = clampInt(out.settings.brandLogoHeight, 20, 96, out.settings.brandLogoSize || def.settings.brandLogoHeight); out.settings.brandLogoFit = ['contain', 'cover', 'fill', 'scale-down'].includes(out.settings.brandLogoFit) ? out.settings.brandLogoFit : def.settings.brandLogoFit; out.settings.brandLogoPositionX = clampInt(out.settings.brandLogoPositionX, 0, 100, def.settings.brandLogoPositionX); out.settings.brandLogoPositionY = clampInt(out.settings.brandLogoPositionY, 0, 100, def.settings.brandLogoPositionY); out.settings.brandLogoPadding = clampInt(out.settings.brandLogoPadding, 0, 20, def.settings.brandLogoPadding); out.settings.brandLogoBackground = ['transparent', 'accent', 'panel', 'white', 'black'].includes(out.settings.brandLogoBackground) ? out.settings.brandLogoBackground : def.settings.brandLogoBackground; out.settings.brandShowVersion = out.settings.brandShowVersion !== false; out.settings.brandReplaceNativeLogo = !!out.settings.brandReplaceNativeLogo; out.settings.brandPageTitleMode = ['native', 'brand', 'custom'].includes(out.settings.brandPageTitleMode) ? out.settings.brandPageTitleMode : def.settings.brandPageTitleMode; out.settings.brandCustomPageTitle = cleanBrandText(out.settings.brandCustomPageTitle, '', 80); out.settings.brandFaviconEnabled = !!out.settings.brandFaviconEnabled; out.settings.accentColor = cleanColor(out.settings.accentColor, def.settings.accentColor); out.settings.secondaryAccentColor = cleanColor(out.settings.secondaryAccentColor, def.settings.secondaryAccentColor); out.settings.paletteMode = out.settings.paletteMode === 'custom' ? 'custom' : 'derived'; out.settings.customPalette = sanitizeCustomPalette(out.settings.customPalette); out.settings.surfaceStyle = ['soft', 'flat', 'glass'].includes(out.settings.surfaceStyle) ? out.settings.surfaceStyle : def.settings.surfaceStyle; out.settings.density = ['compact', 'comfortable', 'spacious'].includes(out.settings.density) ? out.settings.density : def.settings.density; out.settings.contentWidth = ['centered', 'wide', 'full'].includes(out.settings.contentWidth) ? out.settings.contentWidth : def.settings.contentWidth; out.settings.cardStyle = ['visual', 'balanced', 'data', 'micro'].includes(out.settings.cardStyle) ? out.settings.cardStyle : def.settings.cardStyle; out.settings.inspectorEnabled = out.settings.inspectorEnabled !== false; out.settings.inspectorLastTab = ['overview', 'profile', 'organize', 'activity', 'similar'].includes(out.settings.inspectorLastTab) ? out.settings.inspectorLastTab : def.settings.inspectorLastTab; out.settings.inspectorCompareIds = [...new Set((Array.isArray(out.settings.inspectorCompareIds) ? out.settings.inspectorCompareIds : []).map(normalizeUsername).filter(isLikelyUsername))].slice(0, 4); out.settings.dashboardWidgetDensity = ['compact', 'comfortable', 'spacious'].includes(out.settings.dashboardWidgetDensity) ? out.settings.dashboardWidgetDensity : def.settings.dashboardWidgetDensity; out.settings.fontFamily = ['system', 'rounded', 'classic', 'mono'].includes(out.settings.fontFamily) ? out.settings.fontFamily : def.settings.fontFamily; out.settings.fontScale = Math.max(.8, Math.min(1.35, numeric(out.settings.fontScale, 1))); out.settings.controlScale = Math.max(.8, Math.min(1.4, numeric(out.settings.controlScale, 1))); out.settings.cornerRadius = clampInt(out.settings.cornerRadius, 0, 24, def.settings.cornerRadius); out.settings.cardGap = clampInt(out.settings.cardGap, 4, 28, def.settings.cardGap); out.settings.shadowStrength = clampInt(out.settings.shadowStrength, 0, 100, def.settings.shadowStrength); out.settings.surfaceOpacity = clampInt(out.settings.surfaceOpacity, 65, 100, def.settings.surfaceOpacity); out.settings.backgroundStyle = ['solid', 'gradient', 'ambient'].includes(out.settings.backgroundStyle) ? out.settings.backgroundStyle : def.settings.backgroundStyle; out.settings.backgroundGlobal = sanitizeRoomGridBackground(out.settings.backgroundGlobal, 'theme'); const backgroundPagesSource = out.settings.backgroundPages && typeof out.settings.backgroundPages === 'object' && !Array.isArray(out.settings.backgroundPages) ? out.settings.backgroundPages : {}; out.settings.backgroundPages = Object.fromEntries(ROOMGRID_BACKGROUND_PAGE_KEYS.map(key => [key, sanitizeRoomGridBackground(backgroundPagesSource[key], 'inherit')])); const backgroundModelsSource = out.settings.backgroundModels && typeof out.settings.backgroundModels === 'object' && !Array.isArray(out.settings.backgroundModels) ? out.settings.backgroundModels : {}; out.settings.backgroundModels = Object.fromEntries(Object.entries(backgroundModelsSource).slice(-600).map(([rawId, config]) => { const id = normalizeUsername(rawId); return [isLikelyUsername(id) ? id : '', sanitizeRoomGridBackground(config, 'inherit')]; }).filter(([id, config]) => id && config.mode !== 'inherit')); out.settings.layoutPreset = ['balanced', 'compact', 'cinema', 'custom'].includes(out.settings.layoutPreset) ? out.settings.layoutPreset : def.settings.layoutPreset; out.settings.layoutCardMinWidth = clampInt(out.settings.layoutCardMinWidth, 160, 480, def.settings.layoutCardMinWidth); out.settings.layoutSectionGap = clampInt(out.settings.layoutSectionGap, 4, 40, def.settings.layoutSectionGap); out.settings.layoutContentPadding = clampInt(out.settings.layoutContentPadding, 0, 48, def.settings.layoutContentPadding); out.settings.layoutHeaderHeight = clampInt(out.settings.layoutHeaderHeight, 40, 96, def.settings.layoutHeaderHeight); out.settings.layoutMaxColumns = clampInt(out.settings.layoutMaxColumns, 0, 12, def.settings.layoutMaxColumns); out.settings.layoutSidebarPosition = out.settings.layoutSidebarPosition === 'right' ? 'right' : 'left'; out.settings.layoutStickyHeader = out.settings.layoutStickyHeader !== false; out.settings.layoutCardAspect = ['auto', '16/9', '4/3', '1/1', '3/4'].includes(out.settings.layoutCardAspect) ? out.settings.layoutCardAspect : def.settings.layoutCardAspect; out.settings.highContrast = !!out.settings.highContrast; out.settings.focusRingWidth = clampInt(out.settings.focusRingWidth, 1, 6, def.settings.focusRingWidth); out.settings.minimumTargetSize = clampInt(out.settings.minimumTargetSize, 28, 52, def.settings.minimumTargetSize); out.settings.underlineLinks = !!out.settings.underlineLinks; out.settings.reduceTransparency = !!out.settings.reduceTransparency; out.settings.respectSystemMotion = out.settings.respectSystemMotion !== false; out.settings.largeTextBoost = !!out.settings.largeTextBoost; out.settings.designPreviewDevice = ['desktop', 'tablet', 'mobile'].includes(out.settings.designPreviewDevice) ? out.settings.designPreviewDevice : def.settings.designPreviewDevice; out.settings.designPreviewSurface = ['home', 'workspace', 'hub'].includes(out.settings.designPreviewSurface) ? out.settings.designPreviewSurface : def.settings.designPreviewSurface; const cleanAppearanceValue = (key, value) => { if (key === 'theme') return value === 'light' ? 'light' : 'dark'; if (key === 'brandPreset') return ['aurora', 'ember', 'frost', 'custom'].includes(value) ? value : def.settings.brandPreset; if (key === 'accentColor') return cleanColor(value, def.settings.accentColor); if (key === 'secondaryAccentColor') return cleanColor(value, def.settings.secondaryAccentColor); if (key === 'surfaceStyle') return ['soft', 'flat', 'glass'].includes(value) ? value : def.settings.surfaceStyle; if (key === 'density') return ['compact', 'comfortable', 'spacious'].includes(value) ? value : def.settings.density; if (key === 'contentWidth') return ['centered', 'wide', 'full'].includes(value) ? value : def.settings.contentWidth; if (key === 'cardStyle') return ['visual', 'balanced', 'data', 'micro'].includes(value) ? value : def.settings.cardStyle; if (key === 'dashboardWidgetDensity') return ['compact', 'comfortable', 'spacious'].includes(value) ? value : def.settings.dashboardWidgetDensity; if (key === 'fontFamily') return ['system', 'rounded', 'classic', 'mono'].includes(value) ? value : def.settings.fontFamily; if (key === 'backgroundStyle') return ['solid', 'gradient', 'ambient'].includes(value) ? value : def.settings.backgroundStyle; if (key === 'paletteMode') return value === 'custom' ? 'custom' : 'derived'; if (key === 'customPalette') return sanitizeCustomPalette(value); if (key === 'layoutPreset') return ['balanced', 'compact', 'cinema', 'custom'].includes(value) ? value : def.settings.layoutPreset; if (key === 'layoutCardMinWidth') return clampInt(value, 160, 480, def.settings.layoutCardMinWidth); if (key === 'layoutSectionGap') return clampInt(value, 4, 40, def.settings.layoutSectionGap); if (key === 'layoutContentPadding') return clampInt(value, 0, 48, def.settings.layoutContentPadding); if (key === 'layoutHeaderHeight') return clampInt(value, 40, 96, def.settings.layoutHeaderHeight); if (key === 'layoutMaxColumns') return clampInt(value, 0, 12, def.settings.layoutMaxColumns); if (key === 'layoutSidebarPosition') return value === 'right' ? 'right' : 'left'; if (key === 'layoutStickyHeader') return value !== false; if (key === 'layoutCardAspect') return ['auto', '16/9', '4/3', '1/1', '3/4'].includes(value) ? value : def.settings.layoutCardAspect; if (key === 'highContrast' || key === 'underlineLinks' || key === 'reduceTransparency' || key === 'largeTextBoost') return !!value; if (key === 'respectSystemMotion') return value !== false; if (key === 'focusRingWidth') return clampInt(value, 1, 6, def.settings.focusRingWidth); if (key === 'minimumTargetSize') return clampInt(value, 28, 52, def.settings.minimumTargetSize); if (key === 'fontScale') return Math.max(.8, Math.min(1.35, numeric(value, def.settings.fontScale))); if (key === 'controlScale') return Math.max(.8, Math.min(1.4, numeric(value, def.settings.controlScale))); if (key === 'cornerRadius') return clampInt(value, 0, 24, def.settings.cornerRadius); if (key === 'cardGap') return clampInt(value, 4, 28, def.settings.cardGap); if (key === 'shadowStrength') return clampInt(value, 0, 100, def.settings.shadowStrength); if (key === 'surfaceOpacity') return clampInt(value, 65, 100, def.settings.surfaceOpacity); return undefined; }; const appearanceKeys = ['theme', 'brandPreset', 'accentColor', 'secondaryAccentColor', 'paletteMode', 'customPalette', 'surfaceStyle', 'density', 'contentWidth', 'cardStyle', 'dashboardWidgetDensity', 'fontFamily', 'fontScale', 'controlScale', 'cornerRadius', 'cardGap', 'shadowStrength', 'surfaceOpacity', 'backgroundStyle', 'layoutPreset', 'layoutCardMinWidth', 'layoutSectionGap', 'layoutContentPadding', 'layoutHeaderHeight', 'layoutMaxColumns', 'layoutSidebarPosition', 'layoutStickyHeader', 'layoutCardAspect', 'highContrast', 'focusRingWidth', 'minimumTargetSize', 'underlineLinks', 'reduceTransparency', 'respectSystemMotion', 'largeTextBoost']; out.settings.appearanceProfiles = (Array.isArray(out.settings.appearanceProfiles) ? out.settings.appearanceProfiles : []).slice(-32).map((profile, index) => { const source = profile && typeof profile === 'object' && !Array.isArray(profile) ? profile : {}; const values = source.values && typeof source.values === 'object' && !Array.isArray(source.values) ? source.values : {}; const cleanValues = {}; appearanceKeys.forEach(key => { if (key in values) cleanValues[key] = cleanAppearanceValue(key, values[key]); }); return { id: String(source.id || `appearance_${index}`).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || `appearance_${index}`, name: String(source.name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), createdAt: Math.max(0, Number(source.createdAt) || 0), values: cleanValues, }; }).filter(profile => profile.name && Object.keys(profile.values).length); out.settings.pageAppearanceProfilesEnabled = !!out.settings.pageAppearanceProfilesEnabled; const appearanceProfileIds = new Set(out.settings.appearanceProfiles.map(profile => profile.id)); const pageAppearanceSource = out.settings.pageAppearanceProfiles && typeof out.settings.pageAppearanceProfiles === 'object' && !Array.isArray(out.settings.pageAppearanceProfiles) ? out.settings.pageAppearanceProfiles : {}; out.settings.pageAppearanceProfiles = Object.fromEntries(['home', 'listing', 'room', 'workspace', 'discover', 'hub'].map(key => { const id = String(pageAppearanceSource[key] || '').replace(/[^a-z0-9_-]/giu, '').slice(0, 64); return [key, appearanceProfileIds.has(id) ? id : '']; })); out.settings.responsiveAppearanceProfilesEnabled = !!out.settings.responsiveAppearanceProfilesEnabled; const responsiveSource = out.settings.responsiveAppearanceProfiles && typeof out.settings.responsiveAppearanceProfiles === 'object' && !Array.isArray(out.settings.responsiveAppearanceProfiles) ? out.settings.responsiveAppearanceProfiles : {}; out.settings.responsiveAppearanceProfiles = Object.fromEntries(['desktop', 'tablet', 'mobile'].map(device => { const deviceSource = responsiveSource[device] && typeof responsiveSource[device] === 'object' && !Array.isArray(responsiveSource[device]) ? responsiveSource[device] : {}; return [device, Object.fromEntries(['home', 'listing', 'room', 'workspace', 'discover', 'hub'].map(key => { const id = String(deviceSource[key] || '').replace(/[^a-z0-9_-]/giu, '').slice(0, 64); return [key, appearanceProfileIds.has(id) ? id : '']; }))]; })); const breakpointSource = out.settings.appearanceBreakpoints && typeof out.settings.appearanceBreakpoints === 'object' && !Array.isArray(out.settings.appearanceBreakpoints) ? out.settings.appearanceBreakpoints : {}; const mobileMax = clampInt(breakpointSource.mobileMax, 480, 1000, def.settings.appearanceBreakpoints.mobileMax); out.settings.appearanceBreakpoints = { mobileMax, tabletMax: clampInt(breakpointSource.tabletMax, mobileMax + 100, 1800, Math.max(mobileMax + 100, def.settings.appearanceBreakpoints.tabletMax)) }; out.settings.animations = out.settings.animations !== false; const allowedNav = ['dashboard', 'discover', 'tags', 'stats', 'library', 'user', 'settings']; const savedNav = Array.isArray(out.settings.navOrder) ? out.settings.navOrder.filter(id => allowedNav.includes(id)) : allowedNav; out.settings.navOrder = [...new Set(savedNav)]; allowedNav.forEach(id => { if (!out.settings.navOrder.includes(id)) out.settings.navOrder.push(id); }); out.settings.navHidden = [...new Set((Array.isArray(out.settings.navHidden) ? out.settings.navHidden : []).filter(id => allowedNav.includes(id)))].slice(0, allowedNav.length); out.settings.settingsMenuFavorites = [...new Set((Array.isArray(out.settings.settingsMenuFavorites) ? out.settings.settingsMenuFavorites : []).filter(id => typeof id === 'string' && id.length > 0 && id.length <= 240))]; out.settings.sidebarWidth = clampInt(out.settings.sidebarWidth, 180, 460, def.settings.sidebarWidth); const sidebarBrowser = st.sidebarBrowser && typeof st.sidebarBrowser === 'object' && !Array.isArray(st.sidebarBrowser) ? st.sidebarBrowser : {}; out.settings.sidebarBrowser = { ...def.settings.sidebarBrowser }; for (const key of ['query', 'tags', 'excludeTags', 'country']) out.settings.sidebarBrowser[key] = String(sidebarBrowser[key] || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').slice(0, 240); for (const [key, choices] of Object.entries({ tagMode: ['all', 'any'], gender: ['all', 'f', 'm', 'c', 't'], status: ['all', 'online', 'offline', 'private', 'unknown'], local: ['all', 'favorites', 'watched', 'unwatched', 'notes'], sort: ['manual', 'name', 'name_desc', 'live_first', 'viewers', 'viewers_low', 'age_young', 'age_old', 'country', 'rating', 'priority', 'watch', 'sessions', 'recent', 'added', 'followers', 'duration'] })) { out.settings.sidebarBrowser[key] = choices.includes(sidebarBrowser[key]) ? sidebarBrowser[key] : def.settings.sidebarBrowser[key]; } for (const key of ['ageMin', 'ageMax', 'viewerMin', 'viewerMax']) out.settings.sidebarBrowser[key] = clampInt(sidebarBrowser[key], 0, key.startsWith('age') ? 99 : 10000000, 0); out.settings.sidebarBrowser.expanded = !!sidebarBrowser.expanded; out.settings.activeWorkspaceId = String(out.settings.activeWorkspaceId || DEFAULT_WORKSPACE_ID).slice(0, 64); out.settings.roomGridCustomHomeEnabled = true; out.settings.normalPageDesign = out.settings.normalPageDesign !== false; out.settings.normalDesignHome = out.settings.normalDesignHome !== false; out.settings.normalDesignListings = out.settings.normalDesignListings !== false; out.settings.normalDesignRoom = out.settings.normalDesignRoom !== false; out.settings.normalPageView = out.settings.normalPageView === 'list' ? 'list' : 'grid'; out.settings.normalPageCardScale = clampInt(out.settings.normalPageCardScale, 70, 150, def.settings.normalPageCardScale); out.settings.normalPageDensity = ['compact', 'comfortable', 'spacious'].includes(out.settings.normalPageDensity) ? out.settings.normalPageDensity : def.settings.normalPageDensity; out.settings.normalHomeToolbar = out.settings.normalHomeToolbar !== false; out.settings.normalHomePageSize = [0, 12, 24, 36, 48, 60, 90].includes(Number(out.settings.normalHomePageSize)) ? Number(out.settings.normalHomePageSize) : def.settings.normalHomePageSize; out.settings.normalHomePage = clampInt(out.settings.normalHomePage, 1, 100, 1); out.settings.normalHomeColumns = clampInt(out.settings.normalHomeColumns, 0, 8, 0); out.settings.normalHomeImageRatio = ['auto', '16/9', '4/3', '1/1', '3/4'].includes(out.settings.normalHomeImageRatio) ? out.settings.normalHomeImageRatio : def.settings.normalHomeImageRatio; out.settings.normalHomeSort = ['native', 'viewers_desc', 'viewers_asc', 'name_asc', 'name_desc', 'online_desc', 'online_asc', 'age_asc', 'age_desc', 'new_first', 'hd_first', 'gaming_first', 'saved_first', 'favorite_first', 'watched_desc', 'recent_desc', 'random'].includes(out.settings.normalHomeSort) ? out.settings.normalHomeSort : def.settings.normalHomeSort; out.settings.normalHomeSearch = String(out.settings.normalHomeSearch || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 120); out.settings.normalHomeStatus = ['all', 'online', 'away', 'private'].includes(out.settings.normalHomeStatus) ? out.settings.normalHomeStatus : def.settings.normalHomeStatus; out.settings.normalHomeGender = ['all', 'f', 'm', 'c', 't'].includes(out.settings.normalHomeGender) ? out.settings.normalHomeGender : def.settings.normalHomeGender; const cleanHomeSelections = (value, maxItems, maxLength) => [...new Set(String(value || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').split(/[,;|]+/u).map(item => item.trim().slice(0, 80)).filter(Boolean).slice(0, maxItems))].join(',').slice(0, maxLength); out.settings.normalHomeCountryFilter = cleanHomeSelections(out.settings.normalHomeCountryFilter, 80, 1600); out.settings.normalHomeTagFilter = cleanHomeSelections(out.settings.normalHomeTagFilter, 120, 4800); out.settings.normalHomeTagMatchMode = ['all', 'any'].includes(out.settings.normalHomeTagMatchMode) ? out.settings.normalHomeTagMatchMode : def.settings.normalHomeTagMatchMode; out.settings.normalHomeMinViewers = clampInt(out.settings.normalHomeMinViewers, 0, 10000000, 0); out.settings.normalHomeMaxViewers = clampInt(out.settings.normalHomeMaxViewers, 0, 10000000, 0); out.settings.normalHomeMinAge = clampInt(out.settings.normalHomeMinAge, 0, 99, 0); out.settings.normalHomeMaxAge = clampInt(out.settings.normalHomeMaxAge, 0, 99, 0); for (const key of ['normalHomeOnlyHd', 'normalHomeOnlyNew', 'normalHomeOnlyGaming', 'normalHomeOnlySaved', 'normalHomeOnlyFavorites', 'normalHomeOnlyFollowing', 'normalHomeHideVisited', 'normalHomeShowLocalStats']) out.settings[key] = !!out.settings[key]; for (const key of ['normalHomeShowCountry', 'normalHomeShowLocation', 'normalHomeShowViewers', 'normalHomeShowAge', 'normalHomeShowStatus', 'normalHomeShowTags', 'normalHomeShowSubject', 'normalHomeShowOnlineTime', 'normalHomeShowBadges']) out.settings[key] = out.settings[key] !== false; out.settings.normalHomeMetaStyle = ['compact', 'detailed', 'overlay'].includes(out.settings.normalHomeMetaStyle) ? out.settings.normalHomeMetaStyle : def.settings.normalHomeMetaStyle; const homePresetKeys = ['normalHomeSearch', 'normalHomeStatus', 'normalHomeGender', 'normalHomeCountryFilter', 'normalHomeTagFilter', 'normalHomeTagMatchMode', 'normalHomeMinViewers', 'normalHomeMaxViewers', 'normalHomeMinAge', 'normalHomeMaxAge', 'normalHomeOnlyHd', 'normalHomeOnlyNew', 'normalHomeOnlyGaming', 'normalHomeOnlySaved', 'normalHomeOnlyFavorites', 'normalHomeOnlyFollowing', 'normalHomeHideVisited', 'normalHomeSort']; const sanitizeHomePresetValue = (key, value) => { if (key === 'normalHomeSearch') return String(value || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 120); if (key === 'normalHomeStatus') return ['all', 'online', 'away', 'private'].includes(value) ? value : 'all'; if (key === 'normalHomeGender') return ['all', 'f', 'm', 'c', 't'].includes(value) ? value : 'all'; if (key === 'normalHomeCountryFilter') return cleanHomeSelections(value, 80, 1600); if (key === 'normalHomeTagFilter') return cleanHomeSelections(value, 120, 4800); if (key === 'normalHomeTagMatchMode') return ['all', 'any'].includes(value) ? value : 'all'; if (key === 'normalHomeMinViewers' || key === 'normalHomeMaxViewers') return clampInt(value, 0, 10000000, 0); if (key === 'normalHomeMinAge' || key === 'normalHomeMaxAge') return clampInt(value, 0, 99, 0); if (key === 'normalHomeSort') return ['native', 'viewers_desc', 'viewers_asc', 'name_asc', 'name_desc', 'online_desc', 'online_asc', 'age_asc', 'age_desc', 'new_first', 'hd_first', 'gaming_first', 'saved_first', 'favorite_first', 'watched_desc', 'recent_desc', 'random'].includes(value) ? value : 'native'; return !!value; }; out.settings.normalHomeFilterPresets = (Array.isArray(out.settings.normalHomeFilterPresets) ? out.settings.normalHomeFilterPresets : []).slice(-24).map((preset, index) => { const source = preset && typeof preset === 'object' && !Array.isArray(preset) ? preset : {}; const values = source.values && typeof source.values === 'object' && !Array.isArray(source.values) ? source.values : {}; return { id: String(source.id || `home_view_${index}`).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || `home_view_${index}`, name: String(source.name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), createdAt: Math.max(0, Number(source.createdAt) || 0), values: Object.fromEntries(homePresetKeys.filter(key => key in values).map(key => [key, sanitizeHomePresetValue(key, values[key])])), }; }).filter(preset => preset.name && Object.keys(preset.values).length); out.settings.normalHidePromos = !!out.settings.normalHidePromos; out.settings.normalCompactFooter = out.settings.normalCompactFooter !== false; out.settings.normalPolishNavigation = out.settings.normalPolishNavigation !== false; out.settings.normalPolishSidebar = out.settings.normalPolishSidebar !== false; out.settings.normalMarkSaved = out.settings.normalMarkSaved !== false; out.settings.normalMarkFavorites = out.settings.normalMarkFavorites !== false; out.settings.normalMarkVisited = out.settings.normalMarkVisited !== false; out.settings.normalQuickActions = out.settings.normalQuickActions !== false; out.settings.normalLivePreviews = out.settings.normalLivePreviews !== false; out.settings.normalLivePreviewFps = PREVIEW_FPS_VALUES.includes(Number(out.settings.normalLivePreviewFps)) ? Number(out.settings.normalLivePreviewFps) : def.settings.normalLivePreviewFps; out.settings.normalLivePreviewMode = ['simultaneous', 'bounded'].includes(out.settings.normalLivePreviewMode) ? out.settings.normalLivePreviewMode : def.settings.normalLivePreviewMode; out.settings.normalLivePreviewConcurrency = Number(out.settings.normalLivePreviewConcurrency) === 0 ? 0 : clampInt(out.settings.normalLivePreviewConcurrency, 1, 24, 4); out.settings.normalHoverLivePreview = out.settings.normalHoverLivePreview !== false; out.settings.normalHoverLivePreviewDelay = [0, 150, 250, 400, 600, 1000].includes(Number(out.settings.normalHoverLivePreviewDelay)) ? Number(out.settings.normalHoverLivePreviewDelay) : def.settings.normalHoverLivePreviewDelay; out.settings.normalHoverLivePreviewScale = clampInt(out.settings.normalHoverLivePreviewScale, 100, 600, def.settings.normalHoverLivePreviewScale); out.settings.previewDockEnabled = out.settings.previewDockEnabled !== false; out.settings.previewDockMax = clampInt(out.settings.previewDockMax, 1, 6, def.settings.previewDockMax); out.settings.previewDockOpacity = Math.max(.25, Math.min(1, numeric(out.settings.previewDockOpacity, def.settings.previewDockOpacity))); out.settings.previewDockSize = ['small', 'medium', 'large'].includes(out.settings.previewDockSize) ? out.settings.previewDockSize : def.settings.previewDockSize; out.settings.chatFocusEnabled = !!out.settings.chatFocusEnabled; out.settings.chatTimestampEnabled = out.settings.chatTimestampEnabled !== false; out.settings.chatSystemMode = ['show', 'dim', 'hide'].includes(out.settings.chatSystemMode) ? out.settings.chatSystemMode : def.settings.chatSystemMode; out.settings.chatFilterLevel = ['off', 'low', 'medium', 'strict'].includes(out.settings.chatFilterLevel) ? out.settings.chatFilterLevel : def.settings.chatFilterLevel; out.settings.chatHighlightWords = String(out.settings.chatHighlightWords || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 500); out.settings.chatMutedWords = String(out.settings.chatMutedWords || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 500); out.settings.chatCollapseRepeats = out.settings.chatCollapseRepeats !== false; out.settings.privacyIdleMinutes = [0, 1, 2, 5, 10, 15, 30, 60].includes(Number(out.settings.privacyIdleMinutes)) ? Number(out.settings.privacyIdleMinutes) : def.settings.privacyIdleMinutes; const cleanRuleTerms = value => [...new Set(String(value || '').split(/[,;|]+/u).map(item => item.trim().toLocaleLowerCase()).filter(Boolean))].slice(0, 20); out.settings.smartCollections = (Array.isArray(out.settings.smartCollections) ? out.settings.smartCollections : []).slice(-30).map((item, index) => { const source = item && typeof item === 'object' && !Array.isArray(item) ? item : {}; return { id: String(source.id || `smart_${index}`).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || `smart_${index}`, name: String(source.name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), tags: cleanRuleTerms(source.tags), countries: cleanRuleTerms(source.countries), genders: cleanRuleTerms(source.genders).filter(value => ['f','m','c','t','unknown'].includes(value)), minAge: clampInt(source.minAge, 0, 99, 0), maxAge: clampInt(source.maxAge, 0, 99, 0), minWatchMs: Math.max(0, Math.min(315360000000, Number(source.minWatchMs) || 0)), minSessions: clampInt(source.minSessions, 0, 1000000, 0), favoritesOnly: !!source.favoritesOnly, onlineOnly: !!source.onlineOnly, matchMode: source.matchMode === 'any' ? 'any' : 'all', createdAt: Math.max(0, Number(source.createdAt) || 0), }; }).filter(item => item.name); out.settings.automationRules = (Array.isArray(out.settings.automationRules) ? out.settings.automationRules : []).slice(-30).map((item, index) => { const source = item && typeof item === 'object' && !Array.isArray(item) ? item : {}; return { id: String(source.id || `rule_${index}`).replace(/[^a-z0-9_-]/giu, '').slice(0, 64) || `rule_${index}`, name: String(source.name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), enabled: source.enabled !== false, tags: cleanRuleTerms(source.tags), countries: cleanRuleTerms(source.countries), minViewers: clampInt(source.minViewers, 0, 10000000, 0), action: ['favorite', 'group', 'notify'].includes(source.action) ? source.action : 'notify', groupId: String(source.groupId || '').replace(/[^a-z0-9_-]/giu, '').slice(0, 80), }; }).filter(item => item.name && (item.tags.length || item.countries.length || item.minViewers)); out.settings.pollMs = { ...def.settings.pollMs, ...(st.pollMs && typeof st.pollMs === 'object' ? st.pollMs : {}) }; for (const [k, fallback] of Object.entries(def.settings.pollMs)) { out.settings.pollMs[k] = clampInt(out.settings.pollMs[k], 5000, 300000, fallback); } const rawWorkspaces = Array.isArray(src.workspaces) ? src.workspaces.slice(0, MAX_WORKSPACES) : []; const workspaceSeen = new Set(); out.workspaces = rawWorkspaces.map((item, index) => sanitizeWorkspace(item, index, workspaceLayoutFromSettings(out.settings))).filter(item => { if (workspaceSeen.has(item.id)) return false; workspaceSeen.add(item.id); const removedAt = Number(out.deletedWorkspaces[item.id] || 0); return item.id === DEFAULT_WORKSPACE_ID || !removedAt || removedAt < item.modifiedAt; }); if (!out.workspaces.length) out.workspaces.push(sanitizeWorkspace({ id: DEFAULT_WORKSPACE_ID, order: 0 }, 0, workspaceLayoutFromSettings(out.settings))); if (!out.workspaces.some(item => item.id === DEFAULT_WORKSPACE_ID)) out.workspaces.unshift(sanitizeWorkspace({ id: DEFAULT_WORKSPACE_ID, order: 0 }, 0, workspaceLayoutFromSettings(out.settings))); out.workspaces.sort((left, right) => left.order - right.order || left.name.localeCompare(right.name)); if (!out.workspaces.some(item => item.id === out.settings.activeWorkspaceId && !item.archived)) { out.settings.activeWorkspaceId = out.workspaces.find(item => !item.archived)?.id || DEFAULT_WORKSPACE_ID; } const templateSeen = new Set(); out.layoutTemplates = (Array.isArray(src.layoutTemplates) ? src.layoutTemplates : []).slice(0, MAX_LAYOUT_TEMPLATES).map(sanitizeLayoutTemplate).filter(item => { if (!item || templateSeen.has(item.id)) return false; templateSeen.add(item.id); return true; }); return out; } function roomInGroup(room, groupId) { if (groupId === LIBRARY_GROUP_ID) return true; if (groupId === ONLINE_GROUP_ID) return room?.lastStatus === 'online'; return getRoomGroups(room).includes(groupId || DEFAULT_GROUP_ID); } function roomOrderInGroup(room, groupId) { if (groupId === LIBRARY_GROUP_ID || groupId === DEFAULT_GROUP_ID || groupId === ONLINE_GROUP_ID) return numeric(room?.order, 0); return numeric(room?.groupOrder?.[groupId], numeric(room?.order, 0)); } function nextOrderForGroup(state, groupId) { const list = groupId === LIBRARY_GROUP_ID ? state.rooms : state.rooms.filter(r => roomInGroup(r, groupId)); return Math.max(-1, ...list.map(r => roomOrderInGroup(r, groupId))) + 1; } function ensureRoomInGroup(room, groupId, order) { normalizeRoom(room); if (!groupId || groupId === LIBRARY_GROUP_ID || groupId === ONLINE_GROUP_ID) return false; const groups = getRoomGroups(room); const existed = groups.includes(groupId); if (!existed) groups.push(groupId); room.groups = uniq(groups); room.group = room.groups[0] || null; if (!room.groupOrder || typeof room.groupOrder !== 'object' || Array.isArray(room.groupOrder)) room.groupOrder = {}; if (!existed || !Number.isFinite(Number(room.groupOrder[groupId]))) room.groupOrder[groupId] = numeric(order, room.order); return !existed; } function removeRoomFromGroup(room, groupId) { if (!room || !groupId || groupId === LIBRARY_GROUP_ID || groupId === ONLINE_GROUP_ID) return false; const before = getRoomGroups(room); const after = before.filter(g => g !== groupId); if (after.length === before.length) return false; room.groups = after; room.group = after[0] || null; if (room.groupOrder && typeof room.groupOrder === 'object') delete room.groupOrder[groupId]; return true; } function pruneConfigBackups(max = MAX_CONFIG_BACKUPS) { try { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(CONFIG_BACKUP_PREFIX)) keys.push(key); } keys.sort((a, b) => Number(b.slice(CONFIG_BACKUP_PREFIX.length)) - Number(a.slice(CONFIG_BACKUP_PREFIX.length))); keys.slice(Math.max(0, max)).forEach(key => { try { localStorage.removeItem(key); } catch (_) {} }); } catch (_) {} } function backupCurrentConfig() { try { const raw = localStorage.getItem(STORE_KEY); if (!raw) return false; pruneConfigBackups(MAX_CONFIG_BACKUPS - 1); localStorage.setItem(CONFIG_BACKUP_PREFIX + Date.now(), raw); pruneConfigBackups(MAX_CONFIG_BACKUPS); return true; } catch (_) { try { pruneConfigBackups(1); } catch (_) {} return false; } } function readJsonStorage(key, fallback) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch (_) { return fallback; } } function writeJsonStorage(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (_) { return false; } } function parseStoredState(raw) { try { if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Array.isArray(parsed.rooms)) return null; return sanitizeState(parsed); } catch (_) { return null; } } function archiveStableConfig(state) { try { const clean = sanitizeState(state); // Keep a recovery copy whenever there is real workspace content. An // explicitly empty workspace remains valid in the primary key. if (clean.rooms.length) writeJsonStorage(STABLE_ARCHIVE_KEY, clean); } catch (_) {} } function recoverStableConfig() { try { const archived = parseStoredState(localStorage.getItem(STABLE_ARCHIVE_KEY)); return archived?.rooms?.length ? archived : null; } catch (_) { return null; } } function recoverLatestConfigBackup() { try { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(CONFIG_BACKUP_PREFIX)) keys.push(key); } keys.sort((a, b) => Number(b.slice(CONFIG_BACKUP_PREFIX.length)) - Number(a.slice(CONFIG_BACKUP_PREFIX.length))); for (const key of keys) { const recovered = parseStoredState(localStorage.getItem(key)); if (recovered?.rooms?.length) return recovered; } } catch (_) {} return null; } function loadRecordingIntents() { const ids = readJsonStorage(RECORDING_INTENT_KEY, []); return Array.isArray(ids) ? [...new Set(ids.map(normalizeUsername).filter(isLikelyUsername))] : []; } function saveRecordingIntents(ids) { writeJsonStorage(RECORDING_INTENT_KEY, [...new Set((ids || []).map(normalizeUsername).filter(isLikelyUsername))]); } function setRecordingIntent(id, on) { id = normalizeUsername(id); if (!isLikelyUsername(id)) return; const ids = new Set(loadRecordingIntents()); if (on) ids.add(id); else ids.delete(id); saveRecordingIntents([...ids]); } function addRoomStatusHistory(id, status, extra = {}) { id = normalizeUsername(id); if (!isLikelyUsername(id) || !isStableRoomStatus(status)) return; const all = readJsonStorage(ROOM_STATUS_HISTORY_KEY, {}); const list = Array.isArray(all[id]) ? all[id] : []; const last = list[0]; if (last && last.status === status && Date.now() - Number(last.ts || 0) < 30000) return; list.unshift({ ts: Date.now(), status, privateLabel: extra.privateLabel ? String(extra.privateLabel).slice(0, 40) : '', }); all[id] = list.slice(0, 80); writeJsonStorage(ROOM_STATUS_HISTORY_KEY, all); } function loadRoomStatusHistory() { const all = readJsonStorage(ROOM_STATUS_HISTORY_KEY, {}); return all && typeof all === 'object' && !Array.isArray(all) ? all : {}; } function writeStoreRaw(json) { try { localStorage.setItem(STORE_KEY, json); pruneConfigBackups(MAX_CONFIG_BACKUPS); StorageHealth.clear(); return true; } catch (e) { pruneConfigBackups(0); try { localStorage.setItem(STORE_KEY, json); StorageHealth.clear(); return true; } catch (retryError) { StorageHealth.report('write', retryError); return false; } } } const StorageHealth = { error: null, listeners: new Set(), report(kind, error) { this.error = { kind, message: String(error?.message || error || kind), at: Date.now() }; try { console.warn('[RoomGrid] storage problem', this.error); } catch (_) {} try { if (typeof Diagnostics !== 'undefined') Diagnostics.report('storage', error, kind); } catch (_) {} try { window.dispatchEvent(new CustomEvent('ryujo_multicam_storage_error', { detail: this.error })); } catch (_) {} for (const listener of this.listeners) { try { listener(this.error); } catch (_) {} } }, clear() { this.error = null; }, subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }, }; function mergeConcurrentState(localState, diskState) { const local = sanitizeState(localState); const disk = sanitizeState(diskState); const tombstones = { ...(disk.deletedRooms || {}), ...(local.deletedRooms || {}) }; for (const [id, at] of Object.entries(disk.deletedRooms || {})) tombstones[id] = Math.max(Number(tombstones[id]) || 0, Number(at) || 0); const rooms = new Map(); for (const room of [...disk.rooms, ...local.rooms]) { const previous = rooms.get(room.id); rooms.set(room.id, previous ? mergeRoomRecords(previous, room) : room); } for (const [id, removedAt] of Object.entries(tombstones)) { const room = rooms.get(id); if (room && Number(removedAt) >= Number(room.modifiedAt || room.addedAt || 0)) rooms.delete(id); } const groupTombstones = { ...(disk.deletedGroups || {}), ...(local.deletedGroups || {}) }; for (const [id, at] of Object.entries(disk.deletedGroups || {})) groupTombstones[id] = Math.max(Number(groupTombstones[id]) || 0, Number(at) || 0); const groups = new Map(); for (const group of [...disk.groups, ...local.groups]) { const previous = groups.get(group.id); if (!previous || Number(group.modifiedAt || 0) >= Number(previous.modifiedAt || 0)) groups.set(group.id, group); } for (const [id, removedAt] of Object.entries(groupTombstones)) { const group = groups.get(id); if (group && !group.system && Number(removedAt) >= Number(group.modifiedAt || 0)) groups.delete(id); } const workspaceTombstones = { ...(disk.deletedWorkspaces || {}), ...(local.deletedWorkspaces || {}) }; for (const [id, at] of Object.entries(disk.deletedWorkspaces || {})) workspaceTombstones[id] = Math.max(Number(workspaceTombstones[id]) || 0, Number(at) || 0); const workspaces = new Map(); for (const workspace of [...(disk.workspaces || []), ...(local.workspaces || [])]) { const previous = workspaces.get(workspace.id); if (!previous || Number(workspace.modifiedAt || 0) >= Number(previous.modifiedAt || 0)) workspaces.set(workspace.id, workspace); } for (const [id, removedAt] of Object.entries(workspaceTombstones)) { const workspace = workspaces.get(id); if (workspace && id !== DEFAULT_WORKSPACE_ID && Number(removedAt) >= Number(workspace.modifiedAt || 0)) workspaces.delete(id); } const templates = new Map(); for (const template of [...(disk.layoutTemplates || []), ...(local.layoutTemplates || [])]) { const previous = templates.get(template.id); if (!previous || Number(template.modifiedAt || 0) >= Number(previous.modifiedAt || 0)) templates.set(template.id, template); } return sanitizeState({ ...local, revision: Math.max(local.revision, disk.revision), rooms: [...rooms.values()], groups: [...groups.values()], workspaces: [...workspaces.values()], layoutTemplates: [...templates.values()], deletedRooms: tombstones, deletedGroups: groupTombstones, deletedWorkspaces: workspaceTombstones, }); } const Storage = { backupCurrentConfig, load() { try { const keys = [STORE_KEY, ...LEGACY_STORE_KEYS]; for (const key of keys) { const raw = localStorage.getItem(key); const s = parseStoredState(raw); if (!s && raw && key === STORE_KEY) { try { localStorage.setItem(CORRUPT_CONFIG_PREFIX + Date.now(), raw); } catch (_) {} StorageHealth.report('corrupt', new Error('Stored workspace data is invalid; a recovery copy was preserved.')); } if (!s) continue; if (key !== STORE_KEY) { try { localStorage.setItem(STORE_KEY, JSON.stringify(s)); } catch (_) {} } if (!s.settings.__focusAutoMigratedV12) { s.settings.focusAspect = 'auto'; s.settings.__focusAutoMigratedV12 = true; } archiveStableConfig(s); return s; } const recovered = recoverStableConfig() || recoverLatestConfigBackup(); if (recovered) { try { localStorage.setItem(STORE_KEY, JSON.stringify(recovered)); } catch (_) {} return recovered; } return defaultState(); } catch (e) { const recovered = recoverStableConfig() || recoverLatestConfigBackup(); if (recovered) { try { localStorage.setItem(STORE_KEY, JSON.stringify(recovered)); } catch (_) {} return recovered; } console.warn('[RoomGrid] Storage load failed, fallback to default', e); return defaultState(); } }, save(state) { try { let clean = sanitizeState(state); const disk = parseStoredState(localStorage.getItem(STORE_KEY)); if (disk && Number(disk.revision || 0) > Number(clean.revision || 0) && disk.writerId !== STORAGE_WRITER_ID) clean = mergeConcurrentState(clean, disk); clean.revision = Math.max(Number(clean.revision) || 0, Number(disk?.revision) || 0) + 1; clean.updatedAt = Date.now(); clean.writerId = STORAGE_WRITER_ID; archiveStableConfig(clean); const json = JSON.stringify(clean); if (new TextEncoder().encode(json).byteLength > MAX_CONFIG_BYTES) throw new Error('Workspace exceeds the safe storage limit.'); if (!writeStoreRaw(json)) return false; // localStorage 的 storage 事件不会在当前页面触发;补一个同页事件, // 让同标签页 SPA 切换主播 / QuickAdd 状态也能立即刷新。 try { window.dispatchEvent(new CustomEvent('ryujo_multicam_storage', { detail: { state: clean } })); } catch (_) {} return true; } catch (e) { StorageHealth.report('save', e); return false; } }, clearAll() { try { localStorage.removeItem(STORE_KEY); LEGACY_STORE_KEYS.forEach(key => localStorage.removeItem(key)); localStorage.removeItem(STABLE_ARCHIVE_KEY); try { window.dispatchEvent(new CustomEvent('ryujo_multicam_storage', { detail: { state: null } })); } catch (_) {} } catch (_) {} }, has(id) { id = normalizeUsername(id); try { return !!this.load().rooms.find(r => r.id === id); } catch (_) { return false; } }, add(id) { id = normalizeUsername(id); if (!isLikelyUsername(id)) return 'failed'; const s = this.load(); if (s.rooms.find(r => r.id === id)) return 'exists'; const order = nextOrderForGroup(s, DEFAULT_GROUP_ID); s.rooms.push(normalizeRoom({ id, addedAt: Date.now(), group: DEFAULT_GROUP_ID, groups: [DEFAULT_GROUP_ID], groupOrder: {}, order, lastStatus: 'unknown', lastSeenOnline: 0, muted: false, modifiedAt: Date.now(), }, order)); if (s.deletedRooms) delete s.deletedRooms[id]; this.save(s); return 'added'; }, remove(id) { id = normalizeUsername(id); const s = this.load(); const before = s.rooms.length; s.rooms = s.rooms.filter(r => r.id !== id); if (s.rooms.length !== before) { s.deletedRooms ||= {}; s.deletedRooms[id] = Date.now(); this.save(s); return true; } return false; }, }; /* ============================================================= * 2. 状态层 / Store —— 简单响应式 + 持久化 * ============================================================= */ function createStore() { let state = Storage.load(); let revision = 0; const subs = new Set(); let persistTimer = 0; let persistenceDirty = false; const persistNow = (forceVault = false) => { clearTimeout(persistTimer); persistTimer = 0; let saved = true; if (persistenceDirty) { persistenceDirty = false; saved = Storage.save(state); if (!saved) persistenceDirty = true; } if (saved && typeof RoomGridPersistentVault !== 'undefined') { if (forceVault) void RoomGridPersistentVault.flush(true); else RoomGridPersistentVault.queue(); } return saved; }; const persistDebounced = () => { persistenceDirty = true; clearTimeout(persistTimer); persistTimer = setTimeout(() => persistNow(false), 800); }; const flushPersistence = () => persistNow(true); window.addEventListener('pagehide', flushPersistence); document.addEventListener('visibilitychange', () => { if (document.hidden) flushPersistence(); }); const notify = (path) => { for (const fn of subs) fn(state, path); }; const touchRoom = (room, fields, at = Date.now()) => { if (!room) return; room.modifiedAt = at; room.fieldModifiedAt = sanitizeFieldModifiedAt(room.fieldModifiedAt); for (const key of fields || []) room.fieldModifiedAt[key] = at; }; const syncActiveWorkspace = (source, touch = true) => { const active = source.workspaces?.find(item => item.id === source.settings.activeWorkspaceId); if (!active || active.archived) return; const device = workspaceDeviceClass(); active.deviceLayouts ||= {}; active.deviceLayouts[device] = workspaceLayoutFromSettings(source.settings); if (touch) active.modifiedAt = Date.now(); }; const update = (mutator, path = 'all') => { try { const result = mutator(state); // v15.5: mutator 返回 false 表示没有实际变化,避免同状态反复写 localStorage, // 减少多工作台之间 storage 事件 ping-pong 引发的重排/闪屏。 if (result === false) return false; } catch (err) { console.warn('[RoomGrid] store update failed', err); try { Diagnostics.report('store', err, path); } catch (_) {} return false; } revision += 1; persistDebounced(); notify(path); return true; }; return { get state() { return state; }, get revision() { return revision; }, snapshot() { return JSON.parse(JSON.stringify(state)); }, restoreSnapshot(snapshot, expectedRevision = null) { if (expectedRevision != null && revision !== Number(expectedRevision)) return false; state = sanitizeState(snapshot || defaultState()); revision += 1; persistenceDirty = true; persistNow(true); notify('all'); return true; }, subscribe(fn) { subs.add(fn); return () => subs.delete(fn); }, update, flushPersistence, replaceState(nextState, path = 'all', persist = false) { state = sanitizeState(nextState || defaultState()); revision += 1; if (persist) { persistenceDirty = true; persistNow(true); } notify(path); }, // ---- 房间 ---- addRoom(id) { id = normalizeUsername(id); if (!isLikelyUsername(id)) return false; let changed = false; update(s => { normalizeStateMemberships(s); const ag = s.settings.activeGroup || DEFAULT_GROUP_ID; const targetGroup = (ag === LIBRARY_GROUP_ID || ag === ONLINE_GROUP_ID) ? DEFAULT_GROUP_ID : ag; const existing = s.rooms.find(r => r.id === id); if (existing) { changed = ensureRoomInGroup(existing, targetGroup, nextOrderForGroup(s, targetGroup)) || changed; if (changed) touchRoom(existing, ['groups', 'groupOrder']); delete s.deletedRooms?.[id]; return; } const allOrder = nextOrderForGroup(s, DEFAULT_GROUP_ID); const groupOrder = nextOrderForGroup(s, targetGroup); s.rooms.push(normalizeRoom({ id, addedAt: Date.now(), group: targetGroup, groups: [targetGroup], groupOrder: targetGroup === DEFAULT_GROUP_ID ? {} : { [targetGroup]: groupOrder }, order: allOrder, lastStatus: 'unknown', lastSeenOnline: 0, muted: false, modifiedAt: Date.now(), }, allOrder)); if (s.deletedRooms) delete s.deletedRooms[id]; changed = true; }, 'rooms'); return changed; }, syncRoomsToGroup(ids, groupId) { const wanted = [...new Set((Array.isArray(ids) ? ids : []).map(normalizeUsername).filter(isLikelyUsername))]; const wantedSet = new Set(wanted); const result = { ok: false, added: 0, attached: 0, removed: 0, total: wanted.length }; update(s => { normalizeStateMemberships(s); const targetGroup = s.groups.find(g => g.id === groupId && !g.system); if (!targetGroup) return false; for (const id of wanted) { const existing = s.rooms.find(r => r.id === id); if (existing) { if (ensureRoomInGroup(existing, groupId, nextOrderForGroup(s, groupId))) { result.attached++; touchRoom(existing, ['groups', 'groupOrder']); } if (s.deletedRooms) delete s.deletedRooms[id]; continue; } const allOrder = nextOrderForGroup(s, DEFAULT_GROUP_ID); const groupOrder = nextOrderForGroup(s, groupId); s.rooms.push(normalizeRoom({ id, addedAt: Date.now(), group: groupId, groups: [groupId], groupOrder: { [groupId]: groupOrder }, order: allOrder, lastStatus: 'unknown', lastSeenOnline: 0, muted: false, modifiedAt: Date.now(), }, allOrder)); if (s.deletedRooms) delete s.deletedRooms[id]; result.added++; } for (const room of s.rooms) { if (!wantedSet.has(room.id) && removeRoomFromGroup(room, groupId)) { result.removed++; touchRoom(room, ['groups', 'groupOrder']); } } result.ok = true; return result.added || result.attached || result.removed ? true : false; }, 'rooms'); return result; }, removeRoom(id) { id = normalizeUsername(id); update(s => { const before = s.rooms.length; s.rooms = s.rooms.filter(r => r.id !== id); if (s.rooms.length !== before) { s.deletedRooms ||= {}; s.deletedRooms[id] = Date.now(); } }, 'rooms'); }, removeRoomFromActiveGroup(id) { id = normalizeUsername(id); let globalRemoved = false; update(s => { normalizeStateMemberships(s); const ag = s.settings.activeGroup || DEFAULT_GROUP_ID; if (ag === LIBRARY_GROUP_ID || ag === ONLINE_GROUP_ID) { const before = s.rooms.length; s.rooms = s.rooms.filter(r => r.id !== id); globalRemoved = s.rooms.length !== before; if (globalRemoved) { s.deletedRooms ||= {}; s.deletedRooms[id] = Date.now(); } return; } const r = s.rooms.find(r => r.id === id); if (r && removeRoomFromGroup(r, ag)) touchRoom(r, ['groups', 'groupOrder']); }, 'rooms'); return globalRemoved; }, patchRoom(id, patch) { id = normalizeUsername(id); const allowed = new Set(['lastStatus', 'lastSeenOnline', 'viewerCount', 'muted', 'volume', 'privateLabel', 'errorMsg', 'notes', 'localTags', 'rating', 'priority', 'watchlist', 'archived', 'suggestionHidden', 'hiddenUntil', 'pausedUntil', 'qualityHeight', 'audioMeterVisible']); const clean = {}; for (const [k, v] of Object.entries(patch || {})) if (allowed.has(k)) clean[k] = v; if ('notes' in clean) clean.notes = String(clean.notes || '').slice(0, 4000); if ('localTags' in clean) clean.localTags = [...new Set((Array.isArray(clean.localTags) ? clean.localTags : []) .map(value => String(value || '').trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))].slice(0, 40); update(s => { const r = s.rooms.find(r => r.id === id); if (!r) return false; let changed = false; const changedKeys = []; for (const [k, v] of Object.entries(clean)) { const same = Array.isArray(v) ? JSON.stringify(r[k] || []) === JSON.stringify(v) : Object.is(r[k], v); if (!same) { r[k] = v; changed = true; changedKeys.push(k); } } if (changed) { const at = Date.now(); r.modifiedAt = at; r.fieldModifiedAt = sanitizeFieldModifiedAt(r.fieldModifiedAt); changedKeys.forEach(key => { r.fieldModifiedAt[key] = at; }); } return changed; }, 'room:' + id); }, // ---- Arbeitsbereiche und Layoutvorlagen ---- saveActiveWorkspace() { update(s => { syncActiveWorkspace(s); }, 'workspaces'); }, addWorkspace(name, description = '') { const id = `workspace_${uuid().replace(/^g_/u, '')}`; update(s => { syncActiveWorkspace(s); const item = sanitizeWorkspace({ id, name: safeGroupName(name, tx('新工作区', 'New workspace', 'Neuer Arbeitsbereich')), description, order: s.workspaces.length, createdAt: Date.now(), modifiedAt: Date.now(), deviceLayouts: { desktop: workspaceLayoutFromSettings(s.settings), compact: workspaceLayoutFromSettings(s.settings), }, }, s.workspaces.length, workspaceLayoutFromSettings(s.settings)); s.workspaces.push(item); delete s.deletedWorkspaces?.[id]; }, 'workspaces'); return id; }, duplicateWorkspace(id, name = '') { const source = state.workspaces.find(item => item.id === id); if (!source) return ''; const copyId = `workspace_${uuid().replace(/^g_/u, '')}`; update(s => { const original = s.workspaces.find(item => item.id === id); if (!original) return false; const copy = sanitizeWorkspace({ ...JSON.parse(JSON.stringify(original)), id: copyId, name: safeGroupName(name, `${original.name} ${tx('副本', 'copy', 'Kopie')}`), archived: false, order: s.workspaces.length, createdAt: Date.now(), modifiedAt: Date.now(), lastOpenedAt: 0, }, s.workspaces.length, workspaceLayoutFromSettings(s.settings)); s.workspaces.push(copy); delete s.deletedWorkspaces?.[copyId]; }, 'workspaces'); return copyId; }, patchWorkspace(id, patch) { const allowed = new Set(['name', 'description', 'order', 'archived', 'replacement']); update(s => { const item = s.workspaces.find(workspace => workspace.id === id); if (!item) return false; for (const [key, value] of Object.entries(patch || {})) { if (!allowed.has(key)) continue; if (key === 'name') item.name = safeGroupName(value, item.name); else if (key === 'description') item.description = String(value || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 300); else if (key === 'order') item.order = clampInt(value, 0, 10000, item.order); else if (key === 'archived') item.archived = item.id === DEFAULT_WORKSPACE_ID ? false : !!value; else if (key === 'replacement') item.replacement = sanitizeWorkspace({ ...item, replacement: value }, 0).replacement; } item.modifiedAt = Date.now(); if (item.archived && s.settings.activeWorkspaceId === id) s.settings.activeWorkspaceId = DEFAULT_WORKSPACE_ID; }, 'workspaces'); }, reorderWorkspaces(ids) { update(s => { const order = new Map((ids || []).map((id, index) => [id, index])); const at = Date.now(); s.workspaces.forEach(item => { if (order.has(item.id)) { item.order = order.get(item.id); item.modifiedAt = at; } }); }, 'workspaces'); }, removeWorkspace(id) { if (!id || id === DEFAULT_WORKSPACE_ID) return false; let removed = false; update(s => { const before = s.workspaces.length; s.workspaces = s.workspaces.filter(item => item.id !== id); removed = s.workspaces.length !== before; if (removed) { s.deletedWorkspaces ||= {}; s.deletedWorkspaces[id] = Date.now(); } if (s.settings.activeWorkspaceId === id) s.settings.activeWorkspaceId = DEFAULT_WORKSPACE_ID; return removed; }, 'workspaces'); return removed; }, activateWorkspace(id) { let activated = false; update(s => { const target = s.workspaces.find(item => item.id === id && !item.archived); if (!target) return false; syncActiveWorkspace(s); const device = workspaceDeviceClass(); const layout = cleanWorkspaceLayout(target.deviceLayouts?.[device] || target.deviceLayouts?.desktop || {}); Object.assign(s.settings, layout, { activeWorkspaceId: target.id, pageIndex: Number(layout.pageIndex) || 0 }); target.lastOpenedAt = Date.now(); activated = true; }, 'all'); return activated; }, addLayoutTemplate(name, description = '') { const id = `layout_${uuid().replace(/^g_/u, '')}`; update(s => { s.layoutTemplates ||= []; if (s.layoutTemplates.length >= MAX_LAYOUT_TEMPLATES) s.layoutTemplates.shift(); s.layoutTemplates.push(sanitizeLayoutTemplate({ id, name, description, order: s.layoutTemplates.length, layout: workspaceLayoutFromSettings(s.settings), createdAt: Date.now(), modifiedAt: Date.now() }, s.layoutTemplates.length)); }, 'layoutTemplates'); return id; }, applyLayoutTemplate(id) { update(s => { const template = s.layoutTemplates?.find(item => item.id === id); if (!template) return false; Object.assign(s.settings, cleanWorkspaceLayout(template.layout), { pageIndex: 0 }); syncActiveWorkspace(s); }, 'all'); }, removeLayoutTemplate(id) { update(s => { const before = s.layoutTemplates?.length || 0; s.layoutTemplates = (s.layoutTemplates || []).filter(item => item.id !== id); return s.layoutTemplates.length !== before; }, 'layoutTemplates'); }, setRoomCardSize(id, groupId, size) { id = normalizeUsername(id); update(s => { normalizeStateMemberships(s); const r = s.rooms.find(r => r.id === id); if (r) { setCardSizeForGroup(r, groupId, size); touchRoom(r, ['cardSizeByGroup']); } }, 'rooms'); }, reorderRooms(orderedIds, targetGroup) { update(s => { normalizeStateMemberships(s); orderedIds.forEach((id, idx) => { const r = s.rooms.find(r => r.id === id); if (!r) return; if (!targetGroup || targetGroup === LIBRARY_GROUP_ID || targetGroup === ONLINE_GROUP_ID || targetGroup === DEFAULT_GROUP_ID) { r.order = idx; } else { ensureRoomInGroup(r, targetGroup, idx); r.groupOrder[targetGroup] = idx; } touchRoom(r, ['groupOrder']); }); }, 'rooms'); }, moveToGroup(id, groupId) { id = normalizeUsername(id); update(s => { normalizeStateMemberships(s); const r = s.rooms.find(r => r.id === id); if (!r || !groupId || groupId === LIBRARY_GROUP_ID || groupId === ONLINE_GROUP_ID) return; if (ensureRoomInGroup(r, groupId, nextOrderForGroup(s, groupId))) touchRoom(r, ['groups', 'groupOrder']); }, 'rooms'); }, toggleRoomInGroup(id, groupId) { id = normalizeUsername(id); let nowInGroup = false; update(s => { normalizeStateMemberships(s); const r = s.rooms.find(r => r.id === id); if (!r || !groupId || groupId === LIBRARY_GROUP_ID || groupId === ONLINE_GROUP_ID) return; if (roomInGroup(r, groupId)) { removeRoomFromGroup(r, groupId); nowInGroup = false; } else { ensureRoomInGroup(r, groupId, nextOrderForGroup(s, groupId)); nowInGroup = true; } touchRoom(r, ['groups', 'groupOrder']); }, 'rooms'); return nowInGroup; }, moveOnlyToGroup(id, groupId) { id = normalizeUsername(id); update(s => { normalizeStateMemberships(s); const r = s.rooms.find(r => r.id === id); if (!r || !groupId || groupId === LIBRARY_GROUP_ID || groupId === ONLINE_GROUP_ID) return; r.groups = [groupId]; r.group = groupId; r.groupOrder = r.groupOrder && typeof r.groupOrder === 'object' && !Array.isArray(r.groupOrder) ? r.groupOrder : {}; r.groupOrder[groupId] = nextOrderForGroup(s, groupId); touchRoom(r, ['groups', 'groupOrder']); }, 'rooms'); }, setAllMuted(muted) { update(s => { normalizeStateMemberships(s); s.rooms.forEach(r => { r.muted = !!muted; touchRoom(r, ['muted']); }); }, 'rooms'); }, resetTileSizes() { update(s => { normalizeStateMemberships(s); s.rooms.forEach(r => { delete r.cardSize; delete r.cardSizeByGroup; touchRoom(r, ['cardSizeByGroup']); }); }, 'rooms'); }, repairData() { update(s => { normalizeStateMemberships(s); ensureSystemGroups(s); const validGroupIds = new Set((s.groups || []).map(g => g.id).filter(id => id !== LIBRARY_GROUP_ID && id !== ONLINE_GROUP_ID)); s.rooms.forEach((r, idx) => { r.groups = getRoomGroups(r).filter(g => validGroupIds.has(g)); r.group = r.groups[0] || null; r.order = numeric(r.order, idx); if (!r.groupOrder || typeof r.groupOrder !== 'object' || Array.isArray(r.groupOrder)) r.groupOrder = {}; for (const g of Object.keys(r.groupOrder)) if (!validGroupIds.has(g)) delete r.groupOrder[g]; const sizeMap = normalizeCardSizeMap(r.cardSizeByGroup); const legacyCardSize = normalizeCardSize(r.cardSize); if (legacyCardSize && !sizeMap.all) sizeMap.all = legacyCardSize; if (Object.keys(sizeMap).length) r.cardSizeByGroup = sizeMap; else delete r.cardSizeByGroup; delete r.cardSize; }); }, 'all'); }, // ---- 分组 ---- addGroup(name, parentId = '') { const safeName = safeGroupName(name, tx('新分组', 'New group', 'Neue Gruppe')); const id = uuid(); update(s => { s.groups.push({ id, name: safeName, parentId: s.groups.some(group => group.id === parentId && !group.system) ? parentId : '', description: '', archived: false, order: s.groups.length, modifiedAt: Date.now() }); if (s.deletedGroups) delete s.deletedGroups[id]; }, 'groups'); return id; }, renameGroup(id, name) { update(s => { const g = s.groups.find(g => g.id === id); if (g && !g.system) { g.name = safeGroupName(name, g.name); g.modifiedAt = Date.now(); } }, 'groups'); }, patchGroup(id, patch) { update(s => { const g = s.groups.find(group => group.id === id); if (!g || g.system) return false; if ('name' in (patch || {})) g.name = safeGroupName(patch.name, g.name); if ('description' in (patch || {})) g.description = String(patch.description || '').slice(0, 240); if ('archived' in (patch || {})) g.archived = !!patch.archived; if ('parentId' in (patch || {})) g.parentId = patch.parentId !== id && s.groups.some(parent => parent.id === patch.parentId && !parent.system) ? patch.parentId : ''; g.modifiedAt = Date.now(); }, 'groups'); }, reorderGroups(ids) { update(s => { const order = new Map((ids || []).map((id, index) => [id, index + 10])); s.groups.forEach(group => { if (!group.system && order.has(group.id)) { group.order = order.get(group.id); group.modifiedAt = Date.now(); } }); }, 'groups'); }, removeGroup(id) { update(s => { if (s.groups.find(g => g.id === id)?.system) return; const existed = s.groups.some(g => g.id === id); s.groups = s.groups.filter(g => g.id !== id); s.rooms.forEach(r => { if (removeRoomFromGroup(r, id)) touchRoom(r, ['groups', 'groupOrder']); }); if (existed) { s.deletedGroups ||= {}; s.deletedGroups[id] = Date.now(); } if (s.settings.activeGroup === id) s.settings.activeGroup = DEFAULT_GROUP_ID; }, 'groups'); }, setActiveGroup(id) { update(s => { if (s.groups.some(g => g.id === id)) { s.settings.activeGroup = id; s.settings.pageIndex = 0; syncActiveWorkspace(s); } }, 'settings:activeGroup,pageIndex'); }, // ---- 设置 ---- patchSettings(patch) { const allowed = new Set([...Object.keys(defaultState().settings), '__focusAutoMigratedV12']); const clean = {}; for (const [k, v] of Object.entries(patch || {})) if (allowed.has(k)) clean[k] = v; const keys = Object.keys(clean); update(s => { Object.assign(s.settings, clean); if (keys.some(key => WORKSPACE_LAYOUT_KEYS.includes(key))) syncActiveWorkspace(s); }, keys.length ? 'settings:' + keys.join(',') : 'settings'); }, setGridRoomSlots(contextKey, slots, keepEmpty = false) { const key = String(contextKey || '').slice(0, 2048); if (!key) return; const clean = Array.from({ length: MAX_LAYOUT_SLOTS }, (_, idx) => { const id = normalizeUsername(Array.isArray(slots) ? slots[idx] : ''); return isLikelyUsername(id) ? id : null; }); update(s => { const next = sanitizeGridRoomSlots(s.settings.gridRoomSlots); delete next[key]; if (clean.some(Boolean) || keepEmpty) next[key] = clean; const entries = Object.entries(next).slice(-80); s.settings.gridRoomSlots = Object.fromEntries(entries); syncActiveWorkspace(s); }, 'settings:gridRoomSlots'); }, patchFilter(patch) { const allowed = new Set([ 'hideOffline', 'hidePrivate', 'onlyOnline', 'onlyWatched', 'neverWatched', 'favoritesOnly', 'followedOnly', 'inActiveView', 'hasNotes', 'tagInclude', 'tagExclude', 'minWatchMs', 'minSessions', 'lastWatchedDays', 'viewerMin', 'viewerMax', ]); const clean = {}; for (const [k, v] of Object.entries(patch || {})) if (allowed.has(k)) { clean[k] = ['tagInclude', 'tagExclude'].includes(k) ? String(v || '').slice(0, 160) : ['minWatchMs', 'minSessions', 'lastWatchedDays', 'viewerMin', 'viewerMax'].includes(k) ? Math.max(0, numeric(v, 0)) : !!v; } const keys = Object.keys(clean).map(k => 'filter.' + k); update(s => { Object.assign(s.settings.filter, clean); syncActiveWorkspace(s); }, keys.length ? 'settings:' + keys.join(',') : 'settings'); }, }; } /* ============================================================= * 2.5. Suite experience: safe data tools, appearance and commands * ============================================================= */ const SUITE_EXPORT_KIND = 'roomgrid-suite-backup'; function jsonClone(value, fallback = null) { try { return JSON.parse(JSON.stringify(value)); } catch (_) { return fallback; } } function hexRgb(hex) { const text = String(hex || '').trim(); const rgb = text.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/iu); if (rgb) return rgb.slice(1, 4).map(value => Math.max(0, Math.min(255, Number(value)))); const match = text.match(/^#([0-9a-f]{6})$/iu); if (!match) return [37, 99, 235]; return [0, 2, 4].map(index => parseInt(match[1].slice(index, index + 2), 16)); } function readableAccent(hex) { const [r, g, b] = hexRgb(hex); return (r * .299 + g * .587 + b * .114) > 165 ? '#0f172a' : '#ffffff'; } function mixRgb(base, tint, weight) { return base.map((value, index) => Math.round(value * (1 - weight) + tint[index] * weight)); } function rgbColor(values) { return `rgb(${values.join(',')})`; } function rgbaColor(values, opacity) { return `rgba(${values.join(',')},${Math.max(0, Math.min(1, Number(opacity) || 0)).toFixed(3)})`; } function relativeLuminance(color) { const values = hexRgb(color).map(value => { const channel = value / 255; return channel <= .03928 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4; }); return values[0] * .2126 + values[1] * .7152 + values[2] * .0722; } function contrastRatio(foreground, background) { const first = relativeLuminance(foreground); const second = relativeLuminance(background); return (Math.max(first, second) + .05) / (Math.min(first, second) + .05); } function roomGridBrandIdentity(settings = {}) { const dark = settings.theme !== 'light'; return { name: String(settings.brandName || 'Camclient').trim().slice(0, 40) || 'Camclient', monogram: String(settings.brandMonogram || 'CC').trim().slice(0, 5).toLocaleUpperCase() || 'CC', tagline: String(settings.brandTagline || '').trim().slice(0, 80), mode: ['monogram', 'text', 'logo'].includes(settings.brandDisplayMode) ? settings.brandDisplayMode : 'monogram', logo: String(dark ? settings.brandLogoDark || settings.brandLogoLight || '' : settings.brandLogoLight || settings.brandLogoDark || ''), shape: ['rounded', 'circle', 'square', 'pill'].includes(settings.brandLogoShape) ? settings.brandLogoShape : 'rounded', size: clampInt(settings.brandLogoSize, 20, 64, 28), width: clampInt(settings.brandLogoWidth, 20, 240, clampInt(settings.brandLogoSize, 20, 64, 38)), height: clampInt(settings.brandLogoHeight, 20, 96, clampInt(settings.brandLogoSize, 20, 64, 38)), fit: ['contain', 'cover', 'fill', 'scale-down'].includes(settings.brandLogoFit) ? settings.brandLogoFit : 'contain', positionX: clampInt(settings.brandLogoPositionX, 0, 100, 50), positionY: clampInt(settings.brandLogoPositionY, 0, 100, 50), padding: clampInt(settings.brandLogoPadding, 0, 20, 0), background: ['transparent', 'accent', 'panel', 'white', 'black'].includes(settings.brandLogoBackground) ? settings.brandLogoBackground : 'transparent', showVersion: settings.brandShowVersion !== false, }; } function roomGridBrandLogoStyle(brand, options = {}) { const forcedSize = Number(options.size) || 0; const background = brand.background === 'accent' ? 'var(--rg-brand-gradient,linear-gradient(135deg,var(--rg-accent,#4f8cff),var(--rg-secondary,#27c7b8)))' : brand.background === 'panel' ? 'var(--rg-panel,#172033)' : brand.background === 'white' ? '#ffffff' : brand.background === 'black' ? '#000000' : 'transparent'; return { width: `${forcedSize || brand.width}px`, height: `${forcedSize || brand.height}px`, maxWidth: 'none', flex: '0 0 auto', objectFit: brand.fit, objectPosition: `${brand.positionX}% ${brand.positionY}%`, padding: `${brand.padding}px`, boxSizing: 'border-box', background, borderRadius: brand.shape === 'circle' ? '50%' : brand.shape === 'square' ? '0' : brand.shape === 'pill' ? '999px' : '8px', }; } function roomGridBrandMark(settings = {}, options = {}) { const brand = roomGridBrandIdentity(settings); if (brand.mode === 'logo' && brand.logo) return $('img', { src: brand.logo, alt: brand.name, dataset: { roomgridBrandLogo: '1' }, style: roomGridBrandLogoStyle(brand, options) }); return $('span', { dataset: { roomgridBrandMark: '1' } }, brand.mode === 'text' ? brand.name : brand.monogram); } function roomGridPageTitle(settings, fallback = document.title) { const brand = roomGridBrandIdentity(settings); if (settings?.brandPageTitleMode === 'custom' && String(settings.brandCustomPageTitle || '').trim()) return String(settings.brandCustomPageTitle).trim(); if (settings?.brandPageTitleMode === 'brand') return `${brand.name} · ${fallback}`; return fallback; } function applyRoomGridBrandIdentity(settings) { const brand = roomGridBrandIdentity(settings); document.querySelectorAll('[data-roomgrid-brand-name]').forEach(node => { node.textContent = brand.name; }); document.querySelectorAll('[data-roomgrid-brand-tagline]').forEach(node => { node.textContent = brand.tagline || node.dataset.roomgridBrandTaglineFallback || ''; }); document.querySelectorAll('[data-roomgrid-brand-mark]').forEach(node => { if (brand.mode === 'logo' && brand.logo) node.replaceWith(roomGridBrandMark(settings)); else node.textContent = brand.mode === 'text' ? brand.name : brand.monogram; }); document.querySelectorAll('[data-roomgrid-brand-version]').forEach(node => { node.textContent = brand.showVersion ? `v${META.version}` : ''; node.hidden = !brand.showVersion; }); document.querySelectorAll('img[data-roomgrid-brand-logo]').forEach(node => { if (!brand.logo || brand.mode !== 'logo') { node.replaceWith(roomGridBrandMark({ ...settings, brandDisplayMode: brand.mode === 'text' ? 'text' : 'monogram' })); return; } node.src = brand.logo; node.alt = brand.name; Object.assign(node.style, roomGridBrandLogoStyle(brand)); }); const nativeLogo = document.querySelector('[data-roomgrid-native-logo-original],header a[href="/"] img,#header a[href="/"] img,[data-testid="header"] a[href="/"] img'); if (nativeLogo instanceof HTMLImageElement) { if (!nativeLogo.dataset.roomgridNativeLogoOriginal) { nativeLogo.dataset.roomgridNativeLogoOriginal = nativeLogo.src || ''; nativeLogo.dataset.roomgridNativeLogoStyle = nativeLogo.getAttribute('style') || ''; } if (settings.brandReplaceNativeLogo && brand.logo) { nativeLogo.src = brand.logo; nativeLogo.alt = brand.name; nativeLogo.title = `${brand.name} · local RoomGrid branding`; Object.assign(nativeLogo.style, roomGridBrandLogoStyle(brand)); } else if (nativeLogo.dataset.roomgridNativeLogoOriginal && nativeLogo.src !== nativeLogo.dataset.roomgridNativeLogoOriginal) { nativeLogo.src = nativeLogo.dataset.roomgridNativeLogoOriginal; nativeLogo.removeAttribute('title'); if (nativeLogo.dataset.roomgridNativeLogoStyle) nativeLogo.setAttribute('style', nativeLogo.dataset.roomgridNativeLogoStyle); else nativeLogo.removeAttribute('style'); } } let favicon = document.querySelector('link[data-roomgrid-favicon]'); if (settings.brandFaviconEnabled && brand.logo) { if (!favicon) { favicon = $('link', { rel: 'icon', dataset: { roomgridFavicon: '1' } }); document.head.appendChild(favicon); } favicon.href = brand.logo; } else favicon?.remove(); if (!document.documentElement.dataset.roomgridNativeTitle) document.documentElement.dataset.roomgridNativeTitle = document.title; document.title = roomGridPageTitle(settings, document.documentElement.dataset.roomgridNativeTitle || document.title); } function resolvedAppearancePalette(settings, dark = settings?.theme !== 'light') { const accent = cleanColor(settings?.accentColor, '#2563eb'); const secondary = cleanColor(settings?.secondaryAccentColor, '#27c7b8'); const [r, g, b] = hexRgb(accent); const [sr, sg, sb] = hexRgb(secondary); const derived = dark ? { page: rgbColor(mixRgb([7, 12, 18], [r, g, b], .07)), panel: rgbColor(mixRgb([14, 26, 38], [r, g, b], .10)), surface: rgbColor(mixRgb([23, 37, 51], [r, g, b], .11)), strong: rgbColor(mixRgb([31, 46, 61], [sr, sg, sb], .09)), input: rgbColor(mixRgb([16, 28, 40], [r, g, b], .07)), border: rgbColor(mixRgb([64, 80, 97], [r, g, b], .20)), text: '#f1f5f8', muted: '#aeb9c3', success: '#22c55e', warning: '#f59e0b', danger: '#ef4444', info: '#38bdf8', } : { page: rgbColor(mixRgb([246, 248, 251], [r, g, b], .045)), panel: rgbColor(mixRgb([255, 255, 255], [r, g, b], .025)), surface: rgbColor(mixRgb([248, 250, 252], [r, g, b], .055)), strong: rgbColor(mixRgb([238, 243, 248], [sr, sg, sb], .06)), input: '#ffffff', border: rgbColor(mixRgb([203, 213, 225], [r, g, b], .16)), text: '#0f172a', muted: '#526174', success: '#15803d', warning: '#b45309', danger: '#b91c1c', info: '#0369a1', }; if (settings?.paletteMode !== 'custom') return derived; const custom = settings.customPalette?.[dark ? 'dark' : 'light'] || {}; const color = (key, fallback) => cleanColor(custom[key], fallback); return { page: color('page', dark ? '#0b1118' : '#f6f8fb'), panel: color('panel', dark ? '#111f2c' : '#ffffff'), surface: color('surface', dark ? '#192a38' : '#f1f5f9'), strong: color('surface', dark ? '#192a38' : '#f1f5f9'), input: color('input', dark ? '#0f1d29' : '#ffffff'), border: color('border', dark ? '#405061' : '#cbd5e1'), text: color('text', dark ? '#f1f5f8' : '#0f172a'), muted: color('muted', dark ? '#aeb9c3' : '#526174'), success: color('success', dark ? '#22c55e' : '#15803d'), warning: color('warning', dark ? '#f59e0b' : '#b45309'), danger: color('danger', dark ? '#ef4444' : '#b91c1c'), info: color('info', dark ? '#38bdf8' : '#0369a1') }; } function applySuiteAppearance(store, root = document.documentElement) { const baseSettings = store?.state?.settings || defaultState().settings; const params = new URLSearchParams(location.search); const path = String(location.pathname || '/').replace(/\/+$/u, '') || '/'; const pageKey = params.has('multicam_mode') ? 'workspace' : params.has('multicam_discover') ? 'discover' : document.querySelector('[data-roomgrid-native-hub]') ? 'hub' : path === '/' ? 'home' : /^\/(discover|tags|followed|following|category|private-shows)(\/|$)/iu.test(path) ? 'listing' : 'room'; const mobileMax = clampInt(baseSettings.appearanceBreakpoints?.mobileMax, 480, 1000, 767); const tabletMax = clampInt(baseSettings.appearanceBreakpoints?.tabletMax, mobileMax + 100, 1800, 1199); const deviceKey = window.innerWidth <= mobileMax ? 'mobile' : window.innerWidth <= tabletMax ? 'tablet' : 'desktop'; const responsiveId = baseSettings.responsiveAppearanceProfilesEnabled ? String(baseSettings.responsiveAppearanceProfiles?.[deviceKey]?.[pageKey] || '') : ''; const profileId = responsiveId || (baseSettings.pageAppearanceProfilesEnabled ? String(baseSettings.pageAppearanceProfiles?.[pageKey] || '') : ''); const pageProfile = profileId ? (baseSettings.appearanceProfiles || []).find(profile => profile.id === profileId) : null; const settings = pageProfile?.values ? { ...baseSettings, ...pageProfile.values } : baseSettings; root.dataset.roomgridAppearancePage = pageKey; root.dataset.roomgridAppearanceProfile = pageProfile?.id || ''; root.dataset.roomgridAppearanceDevice = deviceKey; const accent = cleanColor(settings.accentColor, '#2563eb'); const [r, g, b] = hexRgb(accent); const secondary = cleanColor(settings.secondaryAccentColor, '#27c7b8'); const [sr, sg, sb] = hexRgb(secondary); const density = settings.density === 'compact' ? .82 : settings.density === 'spacious' ? 1.16 : 1; const surface = ['soft', 'flat', 'glass'].includes(settings.surfaceStyle) ? settings.surfaceStyle : 'soft'; const dark = settings.theme === 'dark'; const palette = resolvedAppearancePalette(settings, dark); const backgroundStyle = ['solid', 'gradient', 'ambient'].includes(settings.backgroundStyle) ? settings.backgroundStyle : 'solid'; const surfaceOpacity = clampInt(settings.surfaceOpacity, 65, 100, 100) / 100; const pageBackground = backgroundStyle === 'gradient' ? `linear-gradient(135deg,rgba(${r},${g},${b},${dark ? '.18' : '.11'}),transparent 46%),linear-gradient(315deg,rgba(${sr},${sg},${sb},${dark ? '.14' : '.09'}),transparent 52%),${palette.page}` : backgroundStyle === 'ambient' ? `radial-gradient(circle at 12% 8%,rgba(${r},${g},${b},${dark ? '.25' : '.16'}),transparent 34%),radial-gradient(circle at 88% 18%,rgba(${sr},${sg},${sb},${dark ? '.18' : '.12'}),transparent 38%),${palette.page}` : palette.page; const panelRgb = hexRgb(palette.panel); const surfaceRgb = hexRgb(palette.surface); const strongRgb = hexRgb(palette.strong); const inputRgb = hexRgb(palette.input); const fontFamilies = { system: 'Inter,system-ui,-apple-system,"Segoe UI",sans-serif', rounded: 'ui-rounded,"SF Pro Rounded","Arial Rounded MT Bold",system-ui,sans-serif', classic: 'Georgia,"Times New Roman",serif', mono: 'ui-monospace,"Cascadia Code","Segoe UI Mono",monospace', }; const cornerRadius = clampInt(settings.cornerRadius, 0, 24, 10); const shadowStrength = clampInt(settings.shadowStrength, 0, 100, 55) / 100; root.style.setProperty('--accent', accent); root.style.setProperty('--accent-hover', accent); root.style.setProperty('--accent-soft', `rgba(${r},${g},${b},.13)`); root.style.setProperty('--rg-accent', accent); root.style.setProperty('--rg-accent-rgb', `${r},${g},${b}`); root.style.setProperty('--rg-accent-text', readableAccent(accent)); root.style.setProperty('--rg-secondary', secondary); root.style.setProperty('--rg-secondary-rgb', `${sr},${sg},${sb}`); root.style.setProperty('--rg-secondary-soft', `rgba(${sr},${sg},${sb},${dark ? '.17' : '.12'})`); root.style.setProperty('--rg-page-bg', pageBackground); root.style.setProperty('--rg-panel', surfaceOpacity >= .999 ? palette.panel : rgbaColor(panelRgb, surfaceOpacity)); root.style.setProperty('--rg-surface', surfaceOpacity >= .999 ? palette.surface : rgbaColor(surfaceRgb, surfaceOpacity)); root.style.setProperty('--rg-surface-strong', surfaceOpacity >= .999 ? palette.strong : rgbaColor(strongRgb, surfaceOpacity)); root.style.setProperty('--rg-input', surfaceOpacity >= .999 ? palette.input : rgbaColor(inputRgb, Math.min(1, surfaceOpacity + .04))); root.style.setProperty('--rg-glass-panel', rgbaColor(panelRgb, Math.min(surfaceOpacity, dark ? .82 : .78))); root.style.setProperty('--rg-border', palette.border); root.style.setProperty('--rg-text', palette.text); root.style.setProperty('--rg-muted', palette.muted); root.style.setProperty('--rg-success', palette.success); root.style.setProperty('--rg-warning', palette.warning); root.style.setProperty('--rg-danger', palette.danger); root.style.setProperty('--rg-info', palette.info); root.style.setProperty('--rg-content-max', settings.contentWidth === 'full' ? 'none' : settings.contentWidth === 'centered' ? '1280px' : '1880px'); root.style.setProperty('--rg-font-family', fontFamilies[settings.fontFamily] || fontFamilies.system); root.style.setProperty('--rg-font-scale', String(Math.max(.8, Math.min(1.35, Number(settings.fontScale) || 1)))); root.style.setProperty('--rg-control-scale', String(Math.max(.8, Math.min(1.4, Number(settings.controlScale) || 1)))); root.style.setProperty('--rg-radius', `${cornerRadius}px`); root.style.setProperty('--rg-radius-small', `${Math.max(0, cornerRadius - 4)}px`); root.style.setProperty('--rg-card-gap', `${clampInt(settings.cardGap, 4, 28, 12)}px`); root.style.setProperty('--rg-shadow', shadowStrength ? `0 ${Math.round(4 + shadowStrength * 12)}px ${Math.round(12 + shadowStrength * 32)}px rgba(0,0,0,${(shadowStrength * (dark ? .42 : .22)).toFixed(3)})` : 'none'); root.style.setProperty('--rg-density', String(density)); root.style.setProperty('--rg-sidebar-width', `${clampInt(settings.sidebarWidth, 180, 460, 220)}px`); root.style.setProperty('--rg-card-min-width', `${clampInt(settings.layoutCardMinWidth, 160, 480, 260)}px`); root.style.setProperty('--rg-section-gap', `${clampInt(settings.layoutSectionGap, 4, 40, 16)}px`); root.style.setProperty('--rg-content-padding', `${clampInt(settings.layoutContentPadding, 0, 48, 18)}px`); root.style.setProperty('--rg-header-height', `${clampInt(settings.layoutHeaderHeight, 40, 96, 56)}px`); root.style.setProperty('--rg-layout-columns', String(clampInt(settings.layoutMaxColumns, 0, 12, 0))); root.style.setProperty('--rg-focus-width', `${clampInt(settings.focusRingWidth, 1, 6, 3)}px`); root.style.setProperty('--rg-target-size', `${clampInt(settings.minimumTargetSize, 28, 52, 36)}px`); root.style.colorScheme = dark ? 'dark' : 'light'; document.body?.classList.toggle('rg-dark-mode', dark); document.body?.classList.toggle('rg-no-motion', settings.animations === false); document.body?.classList.toggle('rg-force-motion', settings.respectSystemMotion === false && settings.animations !== false); document.body?.classList.toggle('rg-high-contrast', !!settings.highContrast); document.body?.classList.toggle('rg-underline-links', !!settings.underlineLinks); document.body?.classList.toggle('rg-reduce-transparency', !!settings.reduceTransparency); document.body?.classList.toggle('rg-large-text', !!settings.largeTextBoost); document.body?.classList.toggle('rg-sticky-header', settings.layoutStickyHeader !== false); document.body?.classList.toggle('rg-sidebar-right', settings.layoutSidebarPosition === 'right'); root.dataset.roomgridLayoutColumns = String(clampInt(settings.layoutMaxColumns, 0, 12, 0)); root.dataset.roomgridCardAspect = String(settings.layoutCardAspect || 'auto').replace('/', '-'); document.body?.classList.toggle('rg-surface-flat', surface === 'flat'); document.body?.classList.toggle('rg-surface-glass', surface === 'glass'); ['solid','gradient','ambient'].forEach(value => document.body?.classList.toggle(`rg-background-${value}`, backgroundStyle === value)); document.body?.classList.toggle('rg-density-compact', settings.density === 'compact'); document.body?.classList.toggle('rg-density-spacious', settings.density === 'spacious'); ['aurora','ember','frost','custom'].forEach(value => document.body?.classList.toggle(`rg-brand-${value}`, settings.brandPreset === value)); ['centered','wide','full'].forEach(value => document.body?.classList.toggle(`rg-width-${value}`, settings.contentWidth === value)); ['visual','balanced','data','micro'].forEach(value => document.body?.classList.toggle(`rg-cards-${value}`, settings.cardStyle === value)); ['compact','comfortable','spacious'].forEach(value => document.body?.classList.toggle(`rg-widget-density-${value}`, settings.dashboardWidgetDensity === value)); applyRoomGridBrandIdentity(settings); // The background image itself is resolved asynchronously from IndexedDB; // appearance controls stay synchronous and interactive while it loads. if (typeof RoomGridBackgrounds !== 'undefined') void RoomGridBackgrounds.apply(store); if (root === document.documentElement && !window.__roomGridAppearanceResizeInstalled) { window.__roomGridAppearanceResizeInstalled = true; let resizeTimer = 0; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => applySuiteAppearance(store), 140); }, { passive: true }); } return settings; } let watchOnlyMode = false; try { watchOnlyMode = sessionStorage.getItem('camclient_watch_only') === '1'; } catch (_) {} function createWatchOnlyButton(className) { const label = tx('只观看', 'Watch only', 'Nur schauen'); return $('button', { type: 'button', class: className, ariaPressed: String(watchOnlyMode), dataset: { watchOnly: '1' }, title: tx('暂停可延后任务', 'Pause deferrable work', 'Aufschiebbare Arbeit pausieren'), onclick: event => { watchOnlyMode = !watchOnlyMode; try { sessionStorage.setItem('camclient_watch_only', watchOnlyMode ? '1' : '0'); } catch (_) {} event.currentTarget.setAttribute('aria-pressed', String(watchOnlyMode)); window.dispatchEvent(new Event('camclient:watch-only')); }, }, label); } function createSuiteMore(items) { const summary = $('summary', { class: 'camclient-nav-summary' }, tx('更多', 'More', 'Mehr')); const links = $('div', { class: 'camclient-nav-links' }, items); const details = $('details', { class: 'camclient-nav-more', dataset: { camclientNavMore: '1' } }, [summary, links]); details.addEventListener('keydown', event => { if (event.key !== 'Escape' || !details.open) return; event.stopPropagation(); details.open = false; summary.focus(); }); links.addEventListener('click', event => { if (event.target.closest('button, a')) details.open = false; }); return details; } function installSuiteExperienceStyles() { if (document.getElementById('roomgrid-suite-experience-style')) return; document.head.appendChild($('style', { id: 'roomgrid-suite-experience-style', html: trustedHtml(` .rg-no-motion *, .rg-no-motion *::before, .rg-no-motion *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important} .discover-nav,.suite-global-nav{flex-wrap:wrap!important;overflow:visible!important} .camclient-nav-more{min-width:0;max-width:100%}.camclient-nav-more[open]{flex-basis:100%}.camclient-nav-summary{cursor:pointer;padding:6px 9px;font-weight:700}.camclient-nav-links,.single-secondary-actions{display:flex;flex-wrap:wrap;gap:4px}.camclient-nav-links{margin-top:5px} [data-watch-only][aria-pressed="true"]{color:var(--rg-accent-text,#fff)!important;background:var(--rg-accent,#2563eb)!important} @media(max-width:700px){.suite-global-actions,.discover-header-actions{flex-wrap:wrap!important;overflow:visible!important}.single-topbar .single-workspace-badge{display:none}.single-topbar .single-model-identity{min-width:0;order:-1;flex-basis:100%}.single-page .single-topbar{gap:4px;padding:5px 7px;margin-bottom:7px}.single-secondary-tools[open]{flex-basis:100%}} .rg-command-overlay{position:fixed;inset:0;z-index:2147483600;display:grid;place-items:start center;padding:9vh 16px;background:rgba(2,6,23,.58);backdrop-filter:blur(8px);font:calc(14px * var(--rg-font-scale,1))/1.4 Inter,system-ui,sans-serif} .rg-command{width:min(720px,100%);max-height:78vh;overflow:hidden;display:grid;grid-template-rows:auto minmax(0,1fr) auto;border:1px solid rgba(148,163,184,.35);border-radius:18px;background:#fff;color:#0f172a;box-shadow:0 30px 100px rgba(0,0,0,.38)} .rg-command-search{width:100%;min-height:58px;border:0;border-bottom:1px solid #e2e8f0;outline:0;padding:14px 18px;background:transparent;color:inherit;font:700 16px Inter,system-ui,sans-serif} .rg-command-results{overflow:auto;padding:8px;display:grid;gap:4px}.rg-command-result{width:100%;display:grid;grid-template-columns:36px minmax(0,1fr) auto;align-items:center;gap:10px;padding:10px;border:1px solid transparent;border-radius:11px;background:transparent;color:inherit;text-align:left;cursor:pointer}.rg-command-result:hover,.rg-command-result:focus,.rg-command-result.active{outline:0;border-color:rgba(var(--rg-accent-rgb,37,99,235),.28);background:rgba(var(--rg-accent-rgb,37,99,235),.09)} .rg-command-result i{width:32px;height:32px;display:grid;place-items:center;border-radius:9px;background:#f1f5f9;font-style:normal}.rg-command-result strong{display:block}.rg-command-result small{display:block;color:#64748b}.rg-command-result kbd{padding:3px 6px;border:1px solid #cbd5e1;border-radius:6px;background:#f8fafc;color:#475569;font-size:10px}.rg-command-foot{padding:8px 14px;border-top:1px solid #e2e8f0;color:#64748b;font-size:11px} .rg-dark-mode .rg-command,.discover-dark .rg-command{background:#111827;color:#f8fafc;border-color:#334155}.rg-dark-mode .rg-command-search,.discover-dark .rg-command-search,.rg-dark-mode .rg-command-foot,.discover-dark .rg-command-foot{border-color:#334155}.rg-dark-mode .rg-command-result i,.discover-dark .rg-command-result i{background:#1e293b}.rg-dark-mode .rg-command-result small,.discover-dark .rg-command-result small{color:#94a3b8}.rg-dark-mode .rg-command-result kbd,.discover-dark .rg-command-result kbd{background:#0f172a;color:#cbd5e1;border-color:#475569} body{font-family:var(--rg-font-family,Inter,system-ui,sans-serif);font-size:calc(100% * var(--rg-font-scale,1))}.rg-command-overlay,.rgcc-overlay,.discover-shell,.app-shell,.rgh-root,.roomgrid-dock{font-family:var(--rg-font-family,Inter,system-ui,sans-serif)!important}.ctrl-btn,.ctrl-input,.discover-btn,.discover-input,.rgcc-btn,.rgcc-input{min-height:calc(36px * var(--rg-control-scale,1))!important}.rg-density-compact .toolbar-group,.rg-density-compact .discover-panel,.rg-density-compact .rgcc-card{padding:calc(8px * var(--rg-density,.82))!important}.rg-density-spacious .toolbar-group,.rg-density-spacious .discover-panel,.rg-density-spacious .rgcc-card{padding:calc(14px * var(--rg-density,1.16))!important} .cam-card,.discover-card,.rgcc-card,.toolbar-group,.dashboard-section,.rgh-card,.rgh-section,.roomgrid-dock-card,[data-testid="room-card"],li.RoomCard,li.room_list_room,li.roomCard{border-radius:var(--rg-radius,10px)!important;box-shadow:var(--rg-shadow,0 10px 28px rgba(0,0,0,.22))}.ctrl-btn,.ctrl-input,.discover-btn,.discover-input,.rgcc-btn,.rgcc-input,.rgh-action,.rgh-control input,.rgh-control select{border-radius:var(--rg-radius-small,6px)!important}.discover-grid,.dashboard-widget-grid,.rgcc-grid,.rgcc-two,.rgh-grid,.rgh-library-grid{gap:var(--rg-card-gap,12px)!important} .rg-surface-flat .cam-card,.rg-surface-flat .discover-card,.rg-surface-flat .rgcc-card,.rg-surface-flat .toolbar-group,.rg-surface-flat .dashboard-section,.rg-surface-flat .rgh-card{box-shadow:none!important}.rg-surface-glass .cam-card,.rg-surface-glass .discover-card,.rg-surface-glass .rgcc-card,.rg-surface-glass .toolbar-group{backdrop-filter:blur(18px);background-color:var(--rg-glass-panel,rgba(255,255,255,.78))!important}.rg-dark-mode.rg-surface-glass .cam-card,.rg-dark-mode.rg-surface-glass .discover-card,.rg-dark-mode.rg-surface-glass .rgcc-card,.rg-dark-mode.rg-surface-glass .toolbar-group{background-color:var(--rg-glass-panel,rgba(17,24,39,.82))!important} body.rg-brand-aurora,body.rg-brand-ember,body.rg-brand-frost,body.rg-brand-custom{--rg-brand-gradient:linear-gradient(135deg,var(--rg-accent,#4f8cff),var(--rg-secondary,#27c7b8));--rg-brand-live:var(--rg-secondary,#27c7b8)}.discover-logo{background:var(--rg-brand-gradient,linear-gradient(135deg,#4f8cff,#27c7b8))!important} /* The suite's default wide layout follows its viewport, not a desktop-only cap. Keep an explicitly chosen centered reading layout unchanged. */ .rg-width-full .discover-main,.rg-width-wide .discover-main{max-width:none!important;width:100%!important}.rg-width-centered .discover-main{max-width:1280px!important}.rg-cards-visual .discover-subject,.rg-cards-visual .discover-meta{display:none}.rg-cards-data .discover-preview{aspect-ratio:16/7}.rg-cards-micro .discover-preview{aspect-ratio:16/9}.rg-cards-micro .discover-subject,.rg-cards-micro .discover-tags,.rg-cards-micro .discover-meta{display:none}.rg-cards-micro .discover-card-body{padding:7px}.rg-cards-micro .discover-card-actions{margin-top:6px} .rg-widget-density-compact .dashboard-widget-grid{gap:9px}.rg-widget-density-compact .dashboard-section{padding:8px}.rg-widget-density-spacious .dashboard-widget-grid{gap:24px}.rg-widget-density-spacious .dashboard-section{padding:18px} .rg-high-contrast{--rg-border:currentColor}.rg-high-contrast .rgcc-btn,.rg-high-contrast .discover-btn,.rg-high-contrast .ctrl-btn,.rg-high-contrast a,.rg-high-contrast input,.rg-high-contrast select{border-width:2px!important}.rg-underline-links a:not([role="button"]){text-decoration:underline!important;text-underline-offset:.16em}.rg-large-text{font-size:112%!important}.rg-reduce-transparency .rgcc-overlay,.rg-reduce-transparency .rg-command-overlay{backdrop-filter:none!important}.rg-reduce-transparency .rgcc,.rg-reduce-transparency .cam-card,.rg-reduce-transparency .discover-card,.rg-reduce-transparency .rgh-card{backdrop-filter:none!important;background-color:var(--rg-panel)!important} .rgcc-btn:focus-visible,.rgcc-input:focus-visible,.discover-btn:focus-visible,.discover-input:focus-visible,.ctrl-btn:focus-visible,.ctrl-input:focus-visible,.rgh-action:focus-visible,a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:var(--rg-focus-width,3px) solid var(--rg-info,var(--rg-accent,#38bdf8))!important;outline-offset:2px}.rgcc-btn,.discover-btn,.ctrl-btn,.rgh-action{min-height:max(var(--rg-target-size,36px),calc(36px * var(--rg-control-scale,1)))!important} .rgcc-body,.discover-main,.rgh-content{padding:var(--rg-content-padding,18px)!important}.rgcc-section,.dashboard-section,.discover-section,.rgh-section{margin-top:var(--rg-section-gap,16px)!important}.rg-sticky-header .discover-header,.rg-sticky-header .toolbar,.rg-sticky-header .rgh-header{position:sticky;top:0;z-index:30;min-height:var(--rg-header-height,56px)}.rg-sidebar-right .app-shell,.rg-sidebar-right .rgh-layout{direction:rtl}.rg-sidebar-right .app-shell>*,.rg-sidebar-right .rgh-layout>*{direction:ltr} .discover-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--rg-card-min-width,260px)),1fr))!important}html:not([data-roomgrid-layout-columns="0"]) .discover-grid{grid-template-columns:repeat(var(--rg-layout-columns),minmax(0,1fr))!important}html[data-roomgrid-card-aspect="16-9"] .discover-preview{aspect-ratio:16/9!important}html[data-roomgrid-card-aspect="4-3"] .discover-preview{aspect-ratio:4/3!important}html[data-roomgrid-card-aspect="1-1"] .discover-preview{aspect-ratio:1!important}html[data-roomgrid-card-aspect="3-4"] .discover-preview{aspect-ratio:3/4!important} @media(prefers-reduced-motion:reduce){body:not(.rg-force-motion) *,body:not(.rg-force-motion) *::before,body:not(.rg-force-motion) *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}} `) })); } function createCommandPalette(store, actionSource = () => [], options = {}) { installSuiteExperienceStyles(); let overlay = null; const getActions = () => { const actions = typeof actionSource === 'function' ? actionSource() : actionSource; const models = (store?.state?.rooms || []).map(room => ({ id: `model:${room.id}`, title: room.id, detail: [room.notes, ...(room.localTags || [])].filter(Boolean).join(' · ') || tx('保存的模型', 'Saved model', 'Gespeichertes Model'), icon: room.lastStatus === 'online' ? '●' : '○', keywords: `${room.id} ${room.notes || ''} ${(room.localTags || []).join(' ')}`, run: () => options.onModel?.(room.id), })); return [...(Array.isArray(actions) ? actions : []), ...models].filter(item => item && typeof item.run === 'function'); }; const close = () => { overlay?.remove(); overlay = null; }; const open = (initial = '') => { close(); overlay = $('div', { class: 'rg-command-overlay', dataset: { roomgridCommand: '1' } }); const input = $('input', { class: 'rg-command-search', type: 'search', value: initial, placeholder: tx('搜索模型、页面、工作区和命令…', 'Search models, pages, workspaces and commands…', 'Models, Seiten, Arbeitsbereiche und Befehle durchsuchen…'), autocomplete: 'off' }); const results = $('div', { class: 'rg-command-results' }); const panel = $('section', { class: 'rg-command', role: 'dialog', ariaModal: 'true' }, [input, results, $('div', { class: 'rg-command-foot' }, tx('输入以筛选 · Enter 执行 · Esc 关闭', 'Type to filter · Enter to run · Esc to close', 'Tippen zum Filtern · Enter ausführen · Esc schließen'))]); overlay.appendChild(panel); const render = () => { const terms = String(input.value || '').toLocaleLowerCase().split(/\s+/u).filter(Boolean); const items = getActions().filter(item => { const hay = `${item.title || ''} ${item.detail || ''} ${item.keywords || ''}`.toLocaleLowerCase(); return terms.every(term => hay.includes(term)); }).slice(0, 40); results.replaceChildren(...(items.length ? items.map((item, index) => $('button', { class: `rg-command-result${index === 0 ? ' active' : ''}`, type: 'button', onclick: () => { close(); item.run(); }, }, [$('i', {}, item.icon || '›'), $('span', {}, [$('strong', {}, item.title || ''), $('small', {}, item.detail || '')]), item.shortcut ? $('kbd', {}, item.shortcut) : $('span')])) : [$('div', { style: { padding: '24px', textAlign: 'center', color: '#64748b' } }, tx('没有匹配项', 'No matches', 'Keine Treffer'))])); }; input.addEventListener('input', render); input.addEventListener('keydown', event => { const buttons = [...results.querySelectorAll('.rg-command-result')]; let active = Math.max(0, buttons.findIndex(button => button.classList.contains('active'))); if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); buttons[active]?.classList.remove('active'); active = (active + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % Math.max(1, buttons.length); buttons[active]?.classList.add('active'); buttons[active]?.scrollIntoView({ block: 'nearest' }); } else if (event.key === 'Enter') { event.preventDefault(); buttons[active]?.click(); } }); overlay.addEventListener('click', event => { if (event.target === overlay) close(); }); render(); document.body.appendChild(overlay); setTimeout(() => input.focus(), 0); }; const onKey = event => { if (event.key === 'Escape' && overlay) { event.preventDefault(); close(); return; } const shortcuts = sanitizeShortcuts(store?.state?.settings?.shortcuts, defaultShortcuts()); const pressed = shortcutFromEvent(event); if (pressed === shortcuts.commandPalette || pressed === shortcuts.globalSearch) { event.preventDefault(); open(); } }; document.addEventListener('keydown', onKey, true); return { open, close, destroy() { document.removeEventListener('keydown', onKey, true); close(); } }; } function createSuiteDataManager(store) { const sections = ['models', 'groups', 'workspaces', 'layouts', 'settings']; const exportData = (selected = sections) => { const include = new Set(selected); const state = store.state; const payload = { kind: SUITE_EXPORT_KIND, schema: 2, softwareVersion: META.version, dataVersion: state.dataVersion || 1, exportedAt: Date.now(), sensitive: include.has('models') || include.has('groups') || include.has('workspaces'), sections: [...include] }; if (include.has('models')) payload.rooms = jsonClone(state.rooms, []); if (include.has('groups')) payload.groups = jsonClone(state.groups, []); if (include.has('workspaces')) payload.workspaces = jsonClone(state.workspaces, []); if (include.has('layouts')) payload.layoutTemplates = jsonClone(state.layoutTemplates, []); if (include.has('settings')) payload.settings = jsonClone(state.settings, {}); return payload; }; const sourceState = raw => raw?.kind === SUITE_EXPORT_KIND ? raw : raw?.state && typeof raw.state === 'object' ? raw.state : raw; const inspect = raw => { const source = sourceState(raw); if (!source || typeof source !== 'object' || Array.isArray(source)) throw new Error('invalid backup'); const current = store.state; const basis = { ...defaultState(), ...source }; if (source.groups === undefined) basis.groups = current.groups; if (source.settings === undefined) basis.settings = current.settings; if (source.workspaces === undefined) basis.workspaces = current.workspaces; if (source.layoutTemplates === undefined) basis.layoutTemplates = current.layoutTemplates; const imported = sanitizeState(basis); const changed = (incoming, existing, key = 'id') => incoming.filter(item => { const prior = existing.find(value => value[key] === item[key]); return prior && JSON.stringify(prior) !== JSON.stringify(item); }).length; return { raw: source, imported, sections: Array.isArray(raw?.sections) ? raw.sections.filter(section => sections.includes(section)) : sections.filter(section => source[section === 'models' ? 'rooms' : section === 'layouts' ? 'layoutTemplates' : section] !== undefined), counts: { models: imported.rooms.length, groups: imported.groups.filter(item => !item.system).length, workspaces: imported.workspaces.length, layouts: imported.layoutTemplates.length, settings: source.settings ? 1 : 0 }, newItems: { models: imported.rooms.filter(item => !current.rooms.some(value => value.id === item.id)).length, groups: imported.groups.filter(item => !item.system && !current.groups.some(value => value.id === item.id)).length, workspaces: imported.workspaces.filter(item => !current.workspaces.some(value => value.id === item.id)).length, layouts: imported.layoutTemplates.filter(item => !current.layoutTemplates.some(value => value.id === item.id)).length }, conflicts: { models: changed(imported.rooms, current.rooms), groups: changed(imported.groups.filter(item => !item.system), current.groups), workspaces: changed(imported.workspaces, current.workspaces), layouts: changed(imported.layoutTemplates, current.layoutTemplates) }, sensitive: raw?.sensitive !== false && (imported.rooms.length > 0 || imported.workspaces.length > 0), }; }; const mergeList = (current, incoming, mergeItem = null) => { const map = new Map((current || []).map(item => [item.id, jsonClone(item, item)])); for (const item of incoming || []) { const previous = map.get(item.id); if (!previous) map.set(item.id, item); else if (mergeItem) map.set(item.id, mergeItem(previous, item)); else if (Number(item.modifiedAt || item.addedAt || 0) >= Number(previous.modifiedAt || previous.addedAt || 0)) map.set(item.id, { ...previous, ...item }); } return [...map.values()]; }; const apply = (review, selected = review.sections, strategy = 'merge') => { if (!review?.imported) throw new Error('preview required'); const include = new Set(selected); backupCurrentConfig(); const current = sanitizeState(store.state); const incoming = review.imported; const next = jsonClone(current, current); const choose = (key, section) => { if (!include.has(section)) return; next[key] = strategy === 'replace' ? jsonClone(incoming[key], []) : mergeList(next[key], incoming[key], key === 'rooms' ? mergeRoomRecords : null); }; choose('rooms', 'models'); choose('groups', 'groups'); choose('workspaces', 'workspaces'); choose('layoutTemplates', 'layouts'); if (include.has('settings')) next.settings = strategy === 'replace' ? jsonClone(incoming.settings, {}) : { ...next.settings, ...incoming.settings }; const clean = sanitizeState(next); if (!Storage.save(clean)) throw new Error('save failed'); store.replaceState(clean, 'all'); return clean; }; const exportCsv = rooms => { const quote = value => `"${String(value ?? '').replace(/"/gu, '""')}"`; const rows = [['username','status','favorite','following','watchlist','archived','rating','priority','last_online','notes','tags','groups']]; const followed = store.state.settings.followedGroupId; for (const room of rooms || store.state.rooms) rows.push([room.id, room.lastStatus, roomInGroup(room, FAVORITE_GROUP_ID), followed ? roomInGroup(room, followed) : false, !!room.watchlist, !!room.archived, room.rating || 0, room.priority || 0, room.lastSeenOnline || 0, room.notes || '', (room.localTags || []).join('|'), getRoomGroups(room).join('|')]); return rows.map(row => row.map(quote).join(',')).join('\r\n'); }; const cleanupLegacy = () => { let count = 0; for (const key of LEGACY_STORE_KEYS) { try { if (localStorage.getItem(key) != null) { localStorage.removeItem(key); count++; } } catch (_) {} } return count; }; return { sections, exportData, inspect, apply, exportCsv, cleanupLegacy }; } function installNormalPageExperience(store, analytics, notifications, windows) { installSuiteExperienceStyles(); const style = $('style', { id: 'roomgrid-normal-page-style', html: trustedHtml(` body.rg-site-design{--rg-site-card:minmax(calc(210px * var(--rg-site-scale,1)),1fr);background:linear-gradient(180deg,rgba(var(--rg-accent-rgb),.10),transparent 360px),var(--rg-page-bg,#f6f8fb)!important;color:var(--rg-text,#0f172a)!important} body.rg-site-design.rg-dark-mode{background:linear-gradient(180deg,rgba(var(--rg-accent-rgb),.14),transparent 360px),var(--rg-page-bg,#090d14)!important;color:var(--rg-text,#e5e7eb)!important} body.rg-site-design #main>.content{flex:1 1 0%!important;min-width:0!important;width:100%!important} body.rg-site-design .discover-content,body.rg-site-design .discover-content .carousel-root,body.rg-site-design .discover-content .carousel,body.rg-site-design .discover-content .room-list-carousel,body.rg-site-design .discover-content .room-list-carousel-wrapper{width:100%!important;max-width:none!important;min-width:0!important} body.rg-site-design [data-testid="room-list"]{display:block!important;width:100%!important;min-width:0!important} body.rg-site-design [data-testid="room-list"]>.DesktopRoomlistRoot,body.rg-site-design [data-testid="room-list"] [data-testid="room-list-container"]{width:100%!important;max-width:none!important;min-width:0!important} body.rg-site-design [data-testid="room-list"] .RoomCardGrid,body.rg-site-design .room_list{display:grid!important;grid-template-columns:repeat(auto-fill,var(--rg-site-card))!important;gap:calc(14px * var(--rg-site-density,1))!important;width:100%!important;max-width:none!important;min-width:0!important} body.rg-site-design [data-testid="room-card"],body.rg-site-design li.RoomCard,body.rg-site-design li.room_list_room,body.rg-site-design li.roomCard{height:auto!important;min-height:180px!important;border-radius:14px!important;overflow:hidden!important;box-shadow:0 8px 22px rgba(15,23,42,.09)!important;border:1px solid rgba(148,163,184,.25)!important;transition:transform .15s,box-shadow .15s,border-color .15s!important} body.rg-site-design [data-testid="room-card"]:hover,body.rg-site-design li.RoomCard:hover,body.rg-site-design li.room_list_room:hover,body.rg-site-design li.roomCard:hover{transform:translateY(-2px);box-shadow:0 14px 34px rgba(15,23,42,.14)!important;border-color:rgba(var(--rg-accent-rgb),.42)!important} body.rg-site-design.rg-site-list [data-testid="room-list"] .RoomCardGrid,body.rg-site-design.rg-site-list .room_list{display:grid!important;grid-template-columns:1fr!important}body.rg-site-design.rg-site-list [data-testid="room-card"],body.rg-site-design.rg-site-list li.RoomCard,body.rg-site-design.rg-site-list li.room_list_room,body.rg-site-design.rg-site-list li.roomCard{width:100%!important;min-height:118px!important;display:grid!important;grid-template-columns:minmax(170px,280px) 1fr!important} body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) [data-testid="room-list"] .RoomCardGrid,body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) .room_list{grid-template-columns:repeat(var(--rg-home-columns),minmax(0,1fr))!important} body.rg-site-design.rg-home-custom-ratio [data-testid="room-card"] .RoomCardThumbnail,body.rg-site-design.rg-home-custom-ratio li.RoomCard .RoomCardThumbnail,body.rg-site-design.rg-home-custom-ratio [data-testid="room-card"] [data-testid="room-card-image-anchor"]{height:auto!important;aspect-ratio:var(--rg-home-image-ratio)!important;overflow:hidden!important}body.rg-site-design.rg-home-custom-ratio [data-testid="room-card"] .RoomCardThumbnail img,body.rg-site-design.rg-home-custom-ratio li.RoomCard .RoomCardThumbnail img,body.rg-site-design.rg-home-custom-ratio [data-testid="room-card"] [data-testid="room-card-image-anchor"] img{width:100%!important;height:100%!important;object-fit:cover!important} .rg-home-filter-hidden{display:none!important}.rg-home-toolbar{display:grid;grid-template-columns:minmax(190px,1.4fr) minmax(150px,.8fr) minmax(120px,.55fr) auto;gap:8px;align-items:end;width:100%;margin:9px 0 14px;padding:12px;border:1px solid #405061;border-top:4px solid #0b9ddd;border-radius:7px;background:#172533;color:#f1f5f8;font:12px/1.3 Arial,Helvetica,sans-serif;box-sizing:border-box}.rg-home-toolbar *{box-sizing:border-box}.rg-home-toolbar label{display:grid;gap:4px;color:#aeb9c3;font-size:9px;font-weight:800;text-transform:uppercase}.rg-home-toolbar input,.rg-home-toolbar select,.rg-home-toolbar button{min-height:34px;padding:6px 9px;border:1px solid #4a5b6c;border-radius:4px;background:#101c28;color:#f1f5f8;font:700 11px/1.2 Arial,Helvetica,sans-serif}.rg-home-toolbar button{cursor:pointer;background:#233443}.rg-home-toolbar button:hover{border-color:#69c9f2;background:#2b4052}.rg-home-toolbar button.primary{border-color:#f58a47;background:#f47321;color:#fff}.rg-home-toolbar-actions{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}.rg-home-toolbar-status{grid-column:1/-1;display:flex;align-items:center;gap:8px;padding-top:8px;border-top:1px solid #364756;color:#b9ddef;font-size:10px}.rg-home-toolbar-status span{flex:1}.rg-home-toolbar-status b{color:#fff}.rg-home-card-meta{display:grid;gap:6px;padding:9px 10px;border-top:1px solid #364756;background:#172533;color:#f1f5f8;font:10px/1.35 Arial,Helvetica,sans-serif}.rg-home-card-meta.detailed{padding:12px;gap:8px}.rg-home-card-meta.overlay{position:absolute;z-index:8;left:6px;right:6px;bottom:6px;border:1px solid rgba(255,255,255,.2);border-radius:5px;background:rgba(14,26,38,.88);backdrop-filter:blur(8px)}.rg-home-meta-subject{margin:0;color:#dce6ee;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rg-home-card-meta.detailed .rg-home-meta-subject{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.rg-home-meta-facts,.rg-home-meta-tags,.rg-home-meta-badges{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.rg-home-meta-fact,.rg-home-meta-tag,.rg-home-meta-badge{display:inline-flex;align-items:center;max-width:100%;min-height:20px;padding:3px 6px;border:1px solid #3d5264;border-radius:4px;background:#223747;color:#b9ddef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rg-home-meta-tag{border-color:#315d75;background:#173246;color:#9addf8}.rg-home-meta-badge{border-color:#8f5b30;background:#53371f;color:#ffd2ad;font-weight:900}.rg-home-page-note{color:#aeb9c3}.rg-home-card-meta:empty{display:none}.rg-home-catalog-more{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;margin:12px 0 22px;padding:12px;border:1px solid #405061;border-radius:7px;background:#172533;color:#b9ddef;font:700 11px/1.3 Arial,Helvetica,sans-serif;box-sizing:border-box}.rg-home-catalog-more button{min-height:36px;padding:7px 14px;border:1px solid #f58a47;border-radius:5px;background:#f47321;color:#fff;font:800 11px/1.2 Arial,Helvetica,sans-serif;cursor:pointer}.rg-home-catalog-more button:hover{filter:brightness(1.08)}body.rg-site-design .rg-home-catalog-more{border-color:var(--rg-border)!important;background:var(--rg-panel)!important;color:var(--rg-muted)!important}body.rg-site-design .rg-home-catalog-more button{border-color:var(--rg-accent)!important;background:var(--rg-accent)!important;color:var(--rg-accent-text)!important} .roomgrid-catalog-card .RoomCardThumbnail{display:block;width:100%;height:auto;aspect-ratio:4/3;overflow:hidden;background:#0b1620}.roomgrid-catalog-card .RoomCardThumbnail img{display:block;width:100%;height:100%;object-fit:cover;background:#0b1620} body.rg-site-design .rg-home-toolbar{border-color:var(--rg-border)!important;border-top-color:var(--rg-accent)!important;background:var(--rg-panel)!important;color:var(--rg-text)!important}body.rg-site-design .rg-home-toolbar label,body.rg-site-design .rg-home-page-note{color:var(--rg-muted)!important}body.rg-site-design .rg-home-toolbar input,body.rg-site-design .rg-home-toolbar select,body.rg-site-design .rg-home-toolbar button{border-color:var(--rg-border)!important;background:var(--rg-input)!important;color:var(--rg-text)!important}body.rg-site-design .rg-home-toolbar button:hover{border-color:var(--rg-secondary)!important;background:var(--rg-secondary-soft)!important}body.rg-site-design .rg-home-toolbar button.primary{border-color:var(--rg-accent)!important;background:var(--rg-accent)!important;color:var(--rg-accent-text)!important}body.rg-site-design .rg-home-toolbar-status{border-color:var(--rg-border)!important;color:var(--rg-muted)!important}body.rg-site-design .rg-home-toolbar-status b{color:var(--rg-text)!important}body.rg-site-design .rg-home-card-meta{border-color:var(--rg-border)!important;background:var(--rg-panel)!important;color:var(--rg-text)!important}body.rg-site-design .rg-home-card-meta.overlay{background:color-mix(in srgb,var(--rg-panel) 88%,transparent)!important}body.rg-site-design .rg-home-meta-subject{color:var(--rg-text)!important}body.rg-site-design .rg-home-meta-fact,body.rg-site-design .rg-home-meta-tag{border-color:var(--rg-border)!important;background:var(--rg-surface)!important;color:var(--rg-text)!important}body.rg-site-design .rg-home-meta-tag{border-color:var(--rg-secondary)!important;background:var(--rg-secondary-soft)!important}body.rg-site-design .rg-home-meta-badge{border-color:var(--rg-accent)!important;background:var(--accent-soft)!important;color:var(--rg-text)!important}body.rg-site-design [data-testid="room-card"],body.rg-site-design li.RoomCard,body.rg-site-design li.room_list_room,body.rg-site-design li.roomCard{border-color:var(--rg-border)!important;background:var(--rg-panel)!important}body.rg-site-design.rg-site-nav-polish>header,body.rg-site-design.rg-site-nav-polish [data-testid="navigation"]{border-color:var(--rg-border)!important;background-color:color-mix(in srgb,var(--rg-surface-strong) 94%,transparent)!important;color:var(--rg-text)!important}body.rg-site-design.rg-site-sidebar-polish aside:not([data-roomgrid-control-center]){border-color:var(--rg-border)!important;background:var(--rg-panel)!important;color:var(--rg-text)!important} body.rg-site-design.rg-site-promos-hidden [data-testid*="promo" i],body.rg-site-design.rg-site-promos-hidden [class*="merch" i],body.rg-site-design.rg-site-promos-hidden [class*="banner" i]:not(header *),body.rg-site-design.rg-site-promos-hidden aside [class*="ad-" i]{display:none!important} body.rg-site-design.rg-site-footer-compact footer{max-height:170px!important;overflow:auto!important;padding-block:12px!important;font-size:12px!important} body.rg-site-design.rg-site-nav-polish > header,body.rg-site-design.rg-site-nav-polish [data-testid="navigation"]{border-bottom:1px solid rgba(148,163,184,.28)!important;box-shadow:0 5px 20px rgba(15,23,42,.07)!important;backdrop-filter:blur(15px)} body.rg-site-design.rg-site-sidebar-polish aside:not([data-roomgrid-control-center]){border:1px solid rgba(148,163,184,.25)!important;border-radius:13px!important;overflow:auto!important;box-shadow:0 6px 18px rgba(15,23,42,.06)!important} .roomgrid-active-version{position:fixed;top:8px;left:168px;z-index:2147481200;display:inline-flex;align-items:center;min-height:24px;padding:4px 9px;border:1px solid rgba(125,211,252,.45);border-radius:999px;background:rgba(15,23,42,.88);color:#e0f2fe;box-shadow:0 5px 18px rgba(2,6,23,.25);backdrop-filter:blur(10px);font:800 10px/1.2 Inter,system-ui,sans-serif;letter-spacing:.02em;pointer-events:none;white-space:nowrap} .rg-dark-mode .roomgrid-active-version{background:rgba(2,6,23,.9);color:#bae6fd}@media(max-width:700px){.roomgrid-active-version{top:52px;left:8px}} .rg-site-saved{outline:2px solid rgba(var(--rg-accent-rgb),.62)!important;outline-offset:1px}.rg-site-visited::after{content:'✓';position:absolute;left:7px;top:7px;z-index:90;width:24px;height:24px;display:grid;place-items:center;border-radius:999px;background:rgba(15,23,42,.75);color:#fff;font:800 12px system-ui}.rg-site-favorite::after{content:'★';background:#f59e0b;color:#111827}@media(max-width:900px){.rg-home-toolbar{grid-template-columns:1fr 1fr}.rg-home-toolbar-actions{grid-column:1/-1;justify-content:flex-start}body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) [data-testid="room-list"] .RoomCardGrid,body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) .room_list{grid-template-columns:repeat(min(var(--rg-home-columns),3),minmax(0,1fr))!important}}@media(max-width:560px){.rg-home-toolbar{grid-template-columns:1fr}.rg-home-toolbar-actions,.rg-home-toolbar-status{grid-column:1}.rg-home-toolbar-status{align-items:flex-start;flex-direction:column}body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) [data-testid="room-list"] .RoomCardGrid,body.rg-site-design.rg-home-fixed-columns:not(.rg-site-list) .room_list{grid-template-columns:1fr!important}} `) }); document.head.appendChild(style); const versionBadge = $('div', { id: 'roomgrid-active-version', class: 'roomgrid-active-version', dataset: { roomgridActiveVersion: META.version }, title: tx('当前启用的 RoomGrid 版本', 'Currently active RoomGrid version', 'Aktuell aktive RoomGrid-Version'), ariaLabel: tx('当前启用的 RoomGrid 版本', 'Currently active RoomGrid version', 'Aktuell aktive RoomGrid-Version'), }, `RoomGrid v${META.version}`); document.body.appendChild(versionBadge); let stopped = false; let homeToolbar = null; let homeToolbarGrid = null; let homeCatalogStarted = false; let homeCatalogLoading = false; let homeCatalogComplete = false; let homeCatalogExpected = 0; let homeCatalogFailedPages = 0; let homeCatalogLoadedAt = 0; let homeCatalogAbort = null; let homeCatalogLoadTimer = 0; let homeCatalogMore = null; let homeCatalogMoreObserver = null; let homeCatalogMoreBusy = false; let homeCatalogLastRevealAt = 0; let homeCatalogMatchCount = 0; let homeCatalogRenderLimit = 120; let homeCatalogFilterSignature = ''; const homeCatalog = new Map(); const HOME_CATALOG_BATCH = 90; const HOME_CATALOG_MAX = ROOMGRID_DATA_LIMITS.publicCatalogResults; const HOME_CATALOG_CONCURRENCY = 4; const HOME_CATALOG_RENDER_INITIAL = 120; const HOME_CATALOG_RENDER_CHUNK = 120; const HOME_CARD_SELECTOR = '[data-testid="room-card"],li.RoomCard,li.room_list_room,li.roomCard,article[data-username],li[data-username]'; const isHomepage = () => (String(location.pathname || '/').replace(/\/+$/u, '') || '/') === '/'; const compact = value => { try { return new Intl.NumberFormat(undefined, { notation: Number(value) >= 10000 ? 'compact' : 'standard', maximumFractionDigits: 1 }).format(Number(value) || 0); } catch (_) { return String(Number(value) || 0); } }; const shortDuration = seconds => { const amount = Math.max(0, Number(seconds) || 0); if (!amount) return ''; if (amount >= 3600) return `${Math.floor(amount / 3600)} h ${Math.floor((amount % 3600) / 60)} min`; return `${Math.max(1, Math.floor(amount / 60))} min`; }; const textValue = value => String(value || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim(); const dataValue = (card, names) => { for (const name of names) { const value = card.getAttribute?.(`data-${name}`) ?? card.dataset?.[name.replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase())]; if (value != null && String(value).trim()) return String(value).trim(); } return ''; }; const numberValue = (card, names, selectors = '') => { const direct = dataValue(card, names); const nodeText = selectors ? card.querySelector?.(selectors)?.textContent : ''; const match = String(direct || nodeText || '').replace(/[.,](?=\d{3}\b)/gu, '').match(/\d+(?:[.,]\d+)?/u); return match ? Math.max(0, Number(match[0].replace(',', '.')) || 0) : 0; }; const cardId = card => { let id = normalizeUsername(card.getAttribute?.('data-username') || card.getAttribute?.('data-room') || card.querySelector?.('[data-username],[data-room]')?.getAttribute?.('data-username') || card.querySelector?.('[data-username],[data-room]')?.getAttribute?.('data-room') || ''); if (!id) { try { id = normalizeUsername(new URL(card.querySelector?.('a[href]')?.href || '', location.href).pathname.split('/').filter(Boolean)[0] || ''); } catch (_) {} } return isLikelyUsername(id) ? id : ''; }; const cardTags = card => [...new Set([ ...[...card.querySelectorAll?.('a[href*="/tag/"]') || []].map(link => textValue(link.textContent).replace(/^#/u, '')), ...String(dataValue(card, ['tags', 'room-tags', 'broadcast-tags']) || '').split(/[,;|]/u).map(textValue), ].filter(Boolean))].slice(0, 16); const boolValue = value => ['1', 'true', 'yes', 'on'].includes(String(value || '').toLocaleLowerCase()); const modelForCard = (card, nativeOrder = 0) => { const id = cardId(card); if (!id) return null; const room = store.state.rooms.find(item => item.id === id) || null; const stat = analytics?.getModelSync?.(id) || {}; const catalog = homeCatalog.get(id) || {}; const tags = [...new Set([...cardTags(card), ...(catalog.tags || []), ...(stat.tags || []), ...(room?.localTags || [])].map(textValue).filter(Boolean))].slice(0, 24); const rawStatus = textValue(dataValue(card, ['room-status', 'status', 'current-show']) || catalog.currentShow || stat.lastStatus || room?.lastStatus || 'online').toLocaleLowerCase(); const status = rawStatus === 'public' ? 'online' : rawStatus === 'hidden' ? 'private' : rawStatus.includes('away') ? 'away' : rawStatus.includes('private') ? 'private' : rawStatus === 'offline' ? 'offline' : 'online'; const startedRaw = numberValue(card, ['start-timestamp', 'started-at']); const startedAt = startedRaw > 100000000000 ? startedRaw / 1000 : startedRaw; const secondsOnline = Math.max(0, Number(catalog.secondsOnline || stat.secondsOnline || (startedAt > 1000000000 ? Date.now() / 1000 - startedAt : 0)) || 0); const country = textValue(dataValue(card, ['country', 'country-code']) || catalog.country || stat.country); const locationText = textValue(dataValue(card, ['location', 'room-location']) || card.querySelector?.('[data-testid*="location" i],[class*="location" i]')?.textContent || catalog.location || stat.location); const subject = textValue(dataValue(card, ['room-subject', 'subject']) || card.querySelector?.('[data-testid*="subject" i],[class*="subject" i]')?.textContent || catalog.subject || stat.subjects?.at?.(-1)); const viewers = Math.round(numberValue(card, ['viewer-count', 'num-users', 'viewers'], '[data-testid*="viewer" i],[class*="viewer" i]') || Number(catalog.viewers || stat.viewerCount || room?.viewerCount) || 0); const age = Math.round(numberValue(card, ['display-age', 'age']) || Number(catalog.age || stat.age) || 0); const gender = textValue(dataValue(card, ['gender']) || catalog.gender || stat.gender || 'unknown').toLocaleLowerCase(); const isHd = boolValue(dataValue(card, ['is-hd', 'hd'])) || !!catalog.isHd || !!stat.isHd || /\bHD\b/u.test(card.textContent || ''); const isNew = boolValue(dataValue(card, ['is-new', 'new'])) || !!catalog.isNew || !!stat.isNew || /\bNEW\b/iu.test(card.textContent || ''); const isGaming = boolValue(dataValue(card, ['is-gaming', 'gaming'])) || !!catalog.isGaming || !!stat.isGaming || tags.some(tag => /gaming|game/u.test(tag)); const favorite = !!room && roomInGroup(room, FAVORITE_GROUP_ID); const followedGroup = store.state.settings.followedGroupId; return { card, id, room, stat, nativeOrder, tags, status, country, location: locationText, subject, viewers, age, gender, secondsOnline, isHd, isNew, isGaming, saved: !!room, favorite, following: !!room && !!followedGroup && roomInGroup(room, followedGroup), visited: Number(stat.totalMs || 0) > 0, totalMs: Number(stat.totalMs || 0), lastWatchedAt: Number(stat.lastWatchedAt || 0) }; }; const visibleDetails = model => { const settings = store.state.settings; const facts = []; const badges = []; if (settings.normalHomeShowCountry && model.country) facts.push(`🌍 ${model.country}`); if (settings.normalHomeShowLocation && model.location && model.location !== model.country) facts.push(`⌖ ${model.location}`); if (settings.normalHomeShowViewers) facts.push(`👁 ${compact(model.viewers)}`); if (settings.normalHomeShowAge && model.age) facts.push(`${model.age} ${tx('岁', 'years', 'Jahre')}`); if (settings.normalHomeShowStatus) facts.push(model.status === 'online' ? tx('直播', 'Live', 'Live') : model.status === 'private' ? tx('私人', 'Private', 'Privat') : model.status === 'away' ? tx('离开', 'Away', 'Abwesend') : tx('离线', 'Offline', 'Offline')); if (settings.normalHomeShowOnlineTime && model.secondsOnline) facts.push(`◷ ${shortDuration(model.secondsOnline)}`); if (settings.normalHomeShowLocalStats && model.totalMs) facts.push(`▣ ${shortDuration(model.totalMs / 1000)} · ${compact(model.stat.sessions || 0)} ${tx('会话', 'sessions', 'Sitzungen')}`); if (settings.normalHomeShowBadges) { if (model.isHd) badges.push('HD'); if (model.isNew) badges.push(tx('新', 'NEW', 'NEU')); if (model.isGaming) badges.push('GAMING'); if (model.favorite) badges.push('★'); else if (model.saved) badges.push('GRID'); } return { facts, badges, tags: settings.normalHomeShowTags ? model.tags.slice(0, settings.normalHomeMetaStyle === 'detailed' ? 8 : 4) : [], subject: settings.normalHomeShowSubject ? model.subject : '' }; }; const syncCardDetails = model => { const details = visibleDetails(model); const signature = JSON.stringify([store.state.settings.normalHomeMetaStyle, details]); let panel = model.card.querySelector?.(':scope > [data-roomgrid-home-meta]'); if (!details.facts.length && !details.badges.length && !details.tags.length && !details.subject) { panel?.remove(); return; } if (!panel) { panel = $('div', { dataset: { roomgridHomeMeta: '1' } }); model.card.appendChild(panel); } panel.className = `rg-home-card-meta ${store.state.settings.normalHomeMetaStyle}`; if (panel.dataset.signature === signature) return; panel.dataset.signature = signature; panel.replaceChildren(...[ details.subject ? $('p', { class: 'rg-home-meta-subject', title: details.subject }, details.subject) : null, details.badges.length ? $('div', { class: 'rg-home-meta-badges' }, details.badges.map(value => $('span', { class: 'rg-home-meta-badge' }, value))) : null, details.facts.length ? $('div', { class: 'rg-home-meta-facts' }, details.facts.map(value => $('span', { class: 'rg-home-meta-fact' }, value))) : null, details.tags.length ? $('div', { class: 'rg-home-meta-tags' }, details.tags.map(value => $('span', { class: 'rg-home-meta-tag' }, `#${value}`))) : null, ].filter(Boolean)); }; const activeFilterCount = settings => [settings.normalHomeSearch, settings.normalHomeStatus !== 'all', settings.normalHomeGender !== 'all', settings.normalHomeCountryFilter, settings.normalHomeTagFilter, settings.normalHomeMinViewers, settings.normalHomeMaxViewers, settings.normalHomeMinAge, settings.normalHomeMaxAge, settings.normalHomeOnlyHd, settings.normalHomeOnlyNew, settings.normalHomeOnlyGaming, settings.normalHomeOnlySaved, settings.normalHomeOnlyFavorites, settings.normalHomeOnlyFollowing, settings.normalHomeHideVisited].filter(Boolean).length; const clearHomeFilters = () => store.patchSettings({ normalHomePage: 1, normalHomeSearch: '', normalHomeStatus: 'all', normalHomeGender: 'all', normalHomeCountryFilter: '', normalHomeTagFilter: '', normalHomeMinViewers: 0, normalHomeMaxViewers: 0, normalHomeMinAge: 0, normalHomeMaxAge: 0, normalHomeOnlyHd: false, normalHomeOnlyNew: false, normalHomeOnlyGaming: false, normalHomeOnlySaved: false, normalHomeOnlyFavorites: false, normalHomeOnlyFollowing: false, normalHomeHideVisited: false }); const ensureHomeToolbar = grid => { if (homeToolbar?.isConnected && homeToolbarGrid === grid) return homeToolbar; homeToolbar?.remove(); const search = $('input', { type: 'search', placeholder: tx('姓名、主题、标签或地点', 'Name, subject, tag or place', 'Name, Thema, Tag oder Ort') }); const sort = $('select', {}, [ ['native', tx('网站顺序', 'Website order', 'Website-Reihenfolge')], ['viewers_desc', tx('观众最多', 'Most viewers', 'Meiste Zuschauer')], ['viewers_asc', tx('观众最少', 'Fewest viewers', 'Wenigste Zuschauer')], ['name_asc', 'Name A–Z'], ['name_desc', 'Name Z–A'], ['online_desc', tx('在线时间最长', 'Longest online', 'Am längsten online')], ['online_asc', tx('刚刚上线', 'Recently online', 'Gerade erst online')], ['age_asc', tx('年龄升序', 'Age ascending', 'Alter aufsteigend')], ['age_desc', tx('年龄降序', 'Age descending', 'Alter absteigend')], ['new_first', tx('新模型优先', 'New models first', 'Neue Models zuerst')], ['hd_first', 'HD zuerst'], ['gaming_first', 'Gaming zuerst'], ['saved_first', tx('已保存优先', 'Saved first', 'Gespeicherte zuerst')], ['favorite_first', tx('收藏优先', 'Favorites first', 'Favoriten zuerst')], ['watched_desc', tx('观看最多', 'Most watched', 'Meistgesehen')], ['recent_desc', tx('最近观看', 'Recently watched', 'Zuletzt gesehen')], ['random', tx('随机混合', 'Random mix', 'Zufällige Mischung')], ].map(([value, label]) => $('option', { value }, label))); const pageSize = $('select', {}, [[12, '12'], [24, '24'], [36, '36'], [48, '48'], [60, '60'], [90, '90'], [0, tx('全部已加载', 'All loaded', 'Alle geladen')]].map(([value, label]) => $('option', { value: String(value) }, label))); const previous = $('button', { type: 'button', title: tx('上一页', 'Previous page', 'Vorherige Seite') }, '‹'); const next = $('button', { type: 'button', title: tx('下一页', 'Next page', 'Nächste Seite') }, '›'); const reloadCatalog = $('button', { type: 'button', title: tx('重新加载完整直播列表', 'Reload complete live list', 'Vollständige Live-Liste neu laden'), onclick: () => loadHomeCatalog(true) }, '↻'); const status = $('span'); const optionsButton = $('button', { class: 'primary', type: 'button', onclick: () => openControlCenter(store, analytics, notifications, windows, 'homepage') }, tx('所有首页选项', 'All homepage options', 'Alle Startseiten-Optionen')); const clear = $('button', { type: 'button', onclick: clearHomeFilters }, tx('重置筛选', 'Reset filters', 'Filter zurücksetzen')); const searchCommit = debounce(() => store.patchSettings({ normalHomeSearch: textValue(search.value).slice(0, 120), normalHomePage: 1 }), 180); search.addEventListener('input', searchCommit); sort.addEventListener('change', () => store.patchSettings({ normalHomeSort: sort.value, normalHomePage: 1 })); pageSize.addEventListener('change', () => store.patchSettings({ normalHomePageSize: Number(pageSize.value), normalHomePage: 1 })); previous.addEventListener('click', () => store.patchSettings({ normalHomePage: Math.max(1, Number(store.state.settings.normalHomePage || 1) - 1) })); next.addEventListener('click', () => store.patchSettings({ normalHomePage: Number(store.state.settings.normalHomePage || 1) + 1 })); homeToolbar = $('section', { class: 'rg-home-toolbar', dataset: { roomgridHomeToolbar: '1' } }, [ $('label', {}, [$('span', {}, tx('快速筛选', 'Quick filter', 'Schnellfilter')), search]), $('label', {}, [$('span', {}, tx('排序', 'Sort', 'Sortierung')), sort]), $('label', {}, [$('span', {}, tx('每页模型数', 'Models per page', 'Models pro Seite')), pageSize]), $('div', { class: 'rg-home-toolbar-actions' }, [previous, next, reloadCatalog, optionsButton, clear]), $('div', { class: 'rg-home-toolbar-status' }, [status]), ]); homeToolbar._roomgrid = { search, sort, pageSize, previous, next, reloadCatalog, status }; homeToolbarGrid = grid; grid.parentNode?.insertBefore(homeToolbar, grid); return homeToolbar; }; const matchesHomeFilters = (model, settings) => { const haystack = [model.id, model.subject, model.country, model.location, ...model.tags].join(' ').toLocaleLowerCase(); const query = textValue(settings.normalHomeSearch).toLocaleLowerCase(); if (query && !haystack.includes(query)) return false; if (settings.normalHomeStatus !== 'all' && model.status !== settings.normalHomeStatus) return false; if (settings.normalHomeGender !== 'all' && model.gender !== settings.normalHomeGender) return false; const places = textValue(settings.normalHomeCountryFilter).toLocaleLowerCase().split(/[,;|]+/u).map(value => value.trim()).filter(Boolean); if (places.length && !places.some(value => textValue(model.country).toLocaleLowerCase() === value || `${model.country} ${model.location}`.toLocaleLowerCase().includes(value))) return false; const wantedTags = textValue(settings.normalHomeTagFilter).toLocaleLowerCase().split(/[,;|]+/u).map(value => value.trim().replace(/^#/u, '')).filter(Boolean); const tagMatches = value => model.tags.some(tag => tag.toLocaleLowerCase() === value); if (wantedTags.length && (settings.normalHomeTagMatchMode === 'any' ? !wantedTags.some(tagMatches) : !wantedTags.every(tagMatches))) return false; if (Number(settings.normalHomeMinViewers) > 0 && model.viewers < Number(settings.normalHomeMinViewers)) return false; if (Number(settings.normalHomeMaxViewers) > 0 && model.viewers > Number(settings.normalHomeMaxViewers)) return false; if (Number(settings.normalHomeMinAge) > 0 && (!model.age || model.age < Number(settings.normalHomeMinAge))) return false; if (Number(settings.normalHomeMaxAge) > 0 && (!model.age || model.age > Number(settings.normalHomeMaxAge))) return false; if (settings.normalHomeOnlyHd && !model.isHd) return false; if (settings.normalHomeOnlyNew && !model.isNew) return false; if (settings.normalHomeOnlyGaming && !model.isGaming) return false; if (settings.normalHomeOnlySaved && !model.saved) return false; if (settings.normalHomeOnlyFavorites && !model.favorite) return false; if (settings.normalHomeOnlyFollowing && !model.following) return false; if (settings.normalHomeHideVisited && model.visited) return false; return true; }; const stableRandom = id => { let hash = 2166136261; for (const char of id) hash = Math.imul(hash ^ char.charCodeAt(0), 16777619); return hash >>> 0; }; const sortHomeModels = (models, sort) => { const compare = { native: (a, b) => a.nativeOrder - b.nativeOrder, viewers_desc: (a, b) => b.viewers - a.viewers || a.id.localeCompare(b.id), viewers_asc: (a, b) => a.viewers - b.viewers || a.id.localeCompare(b.id), name_asc: (a, b) => a.id.localeCompare(b.id), name_desc: (a, b) => b.id.localeCompare(a.id), online_desc: (a, b) => b.secondsOnline - a.secondsOnline || b.viewers - a.viewers, online_asc: (a, b) => a.secondsOnline - b.secondsOnline || b.viewers - a.viewers, age_asc: (a, b) => (a.age || 999) - (b.age || 999) || a.id.localeCompare(b.id), age_desc: (a, b) => b.age - a.age || a.id.localeCompare(b.id), new_first: (a, b) => Number(b.isNew) - Number(a.isNew) || b.viewers - a.viewers, hd_first: (a, b) => Number(b.isHd) - Number(a.isHd) || b.viewers - a.viewers, gaming_first: (a, b) => Number(b.isGaming) - Number(a.isGaming) || b.viewers - a.viewers, saved_first: (a, b) => Number(b.saved) - Number(a.saved) || b.viewers - a.viewers, favorite_first: (a, b) => Number(b.favorite) - Number(a.favorite) || b.viewers - a.viewers, watched_desc: (a, b) => b.totalMs - a.totalMs || b.lastWatchedAt - a.lastWatchedAt, recent_desc: (a, b) => b.lastWatchedAt - a.lastWatchedAt || b.totalMs - a.totalMs, random: (a, b) => stableRandom(a.id) - stableRandom(b.id), }[sort] || ((a, b) => a.nativeOrder - b.nativeOrder); return [...models].sort(compare); }; const usesCompleteLiveCatalog = settings => !!(textValue(settings.normalHomeTagFilter) || textValue(settings.normalHomeCountryFilter)); const catalogFilterModel = (catalog, nativeOrder = 0) => { const room = store.state.rooms.find(item => item.id === catalog.id) || null; const stat = analytics?.getModelSync?.(catalog.id) || {}; const tags = [...new Set([...(catalog.tags || []), ...(stat.tags || []), ...(room?.localTags || [])].map(textValue).filter(Boolean))].slice(0, 24); const rawStatus = textValue(catalog.currentShow || stat.lastStatus || room?.lastStatus || 'online').toLocaleLowerCase(); const status = rawStatus === 'public' ? 'online' : rawStatus === 'hidden' ? 'private' : rawStatus.includes('away') ? 'away' : rawStatus.includes('private') ? 'private' : rawStatus === 'offline' ? 'offline' : 'online'; const followedGroup = store.state.settings.followedGroupId; return { ...catalog, card: null, room, stat, nativeOrder, tags, status, saved: !!room, favorite: !!room && roomInGroup(room, FAVORITE_GROUP_ID), following: !!room && !!followedGroup && roomInGroup(room, followedGroup), visited: Number(stat.totalMs || 0) > 0, totalMs: Number(stat.totalMs || 0), lastWatchedAt: Number(stat.lastWatchedAt || 0) }; }; const createCatalogCard = model => { const image = $('img', { src: `https://thumb.live.mmcdn.com/riw/${encodeURIComponent(model.id)}.jpg`, alt: model.id, loading: 'lazy', decoding: 'async', referrerPolicy: 'no-referrer' }); const thumbnail = $('div', { class: 'RoomCardThumbnail' }, [image]); const details = $('div', { class: 'RoomCardDetails' }, [ $('strong', {}, rawUiText(model.id)), model.subject ? $('span', { title: model.subject }, model.subject) : null, ]); return $('li', { class: 'RoomCard RoomCard--SrchEvlv roomgrid-catalog-card', 'data-testid': 'room-card', dataset: { roomgridCatalogCard: '1', username: model.id, country: model.country || '', location: model.location || '', viewerCount: String(model.viewers || 0), displayAge: String(model.age || 0), gender: model.gender || 'unknown', currentShow: model.currentShow || 'public', roomSubject: model.subject || '', tags: (model.tags || []).join(','), isHd: model.isHd ? 'true' : '', isNew: model.isNew ? 'true' : '', isGaming: model.isGaming ? 'true' : '', }, }, [$('a', { href: `/${encodeURIComponent(model.id)}/`, title: model.subject || model.id }, [thumbnail, details])]); }; const completeFilterSignature = settings => JSON.stringify([ settings.normalHomeSearch, settings.normalHomeStatus, settings.normalHomeGender, settings.normalHomeCountryFilter, settings.normalHomeTagFilter, settings.normalHomeTagMatchMode, settings.normalHomeMinViewers, settings.normalHomeMaxViewers, settings.normalHomeMinAge, settings.normalHomeMaxAge, settings.normalHomeOnlyHd, settings.normalHomeOnlyNew, settings.normalHomeOnlyGaming, settings.normalHomeOnlySaved, settings.normalHomeOnlyFavorites, settings.normalHomeOnlyFollowing, settings.normalHomeHideVisited, settings.normalHomeSort, ]); const removeHomeCatalogMore = () => { homeCatalogMoreObserver?.disconnect?.(); homeCatalogMoreObserver = null; homeCatalogMore?.remove(); homeCatalogMore = null; homeCatalogMoreBusy = false; }; const revealMoreCatalogCards = () => { const now = performance.now(); if (homeCatalogMoreBusy || now - homeCatalogLastRevealAt < 500 || homeCatalogRenderLimit >= homeCatalogMatchCount) return; homeCatalogMoreBusy = true; homeCatalogLastRevealAt = now; homeCatalogRenderLimit = Math.min(HOME_CATALOG_MAX, homeCatalogRenderLimit + HOME_CATALOG_RENDER_CHUNK); schedule(); requestAnimationFrame(() => { homeCatalogMoreBusy = false; }); }; const updateHomeCatalogMore = (grid, shown, total, completeMode) => { const remaining = Math.max(0, total - shown); if (!completeMode || !remaining) { removeHomeCatalogMore(); return; } if (!homeCatalogMore?.isConnected) { const button = $('button', { type: 'button', onclick: revealMoreCatalogCards }); const note = $('span'); homeCatalogMore = $('div', { class: 'rg-home-catalog-more', dataset: { roomgridHomeCatalogMore: '1' } }, [button, note]); homeCatalogMore._roomgrid = { button, note }; grid.parentNode?.insertBefore(homeCatalogMore, grid.nextSibling); if ('IntersectionObserver' in window) { homeCatalogMoreObserver = new IntersectionObserver(items => { if (items.some(item => item.isIntersecting)) revealMoreCatalogCards(); }, { root: null, rootMargin: '300px 0px', threshold: .01 }); homeCatalogMoreObserver.observe(homeCatalogMore); } } const nextCount = Math.min(HOME_CATALOG_RENDER_CHUNK, remaining); const buttonText = `${tx('加载更多结果', 'Load more results', 'Weitere Treffer laden')} · ${nextCount}`; const noteText = `${remaining} ${tx('仍待显示', 'still to display', 'noch einzublenden')} · ${tx('滚动时自动继续加载', 'continues automatically while scrolling', 'beim Scrollen automatisch weiter')}`; if (homeCatalogMore._roomgrid.button.textContent !== buttonText) homeCatalogMore._roomgrid.button.textContent = buttonText; if (homeCatalogMore._roomgrid.note.textContent !== noteText) homeCatalogMore._roomgrid.note.textContent = noteText; }; const syncCatalogCards = grid => { const settings = store.state.settings; const completeMode = usesCompleteLiveCatalog(settings); const signature = completeMode ? completeFilterSignature(settings) : ''; if (signature !== homeCatalogFilterSignature) { homeCatalogFilterSignature = signature; homeCatalogRenderLimit = HOME_CATALOG_RENDER_INITIAL; } const nativeCards = [...grid.children].filter(card => card.matches?.(HOME_CARD_SELECTOR) && card.dataset.roomgridCatalogCard !== '1'); const nativeIds = new Set(nativeCards.map(cardId).filter(Boolean)); const wanted = new Map(); const matchingIds = new Set(); if (completeMode) { let index = 0; const matchingCatalog = []; for (const catalog of homeCatalog.values()) { const model = catalogFilterModel(catalog, 100000 + index++); if (model.status === 'offline' || !matchesHomeFilters(model, settings)) continue; matchingIds.add(model.id); matchingCatalog.push(model); } for (const card of nativeCards) { const model = modelForCard(card); if (model && model.status !== 'offline' && matchesHomeFilters(model, settings)) matchingIds.add(model.id); } const configuredPageSize = Math.max(0, Number(settings.normalHomePageSize) || 0); const catalogPageSize = configuredPageSize || Math.max(1, matchingIds.size); const catalogPages = Math.max(1, Math.ceil(matchingIds.size / catalogPageSize)); const catalogPage = Math.min(catalogPages, Math.max(1, Number(settings.normalHomePage) || 1)); // The complete catalog is searched, but a real page-size selection // must remain a hard visible limit. Materialize only enough records // for the requested page; the special "all loaded" setting keeps the // former bounded progressive reveal. const materializeLimit = configuredPageSize ? catalogPage * catalogPageSize : homeCatalogRenderLimit; sortHomeModels(matchingCatalog, settings.normalHomeSort).slice(0, materializeLimit).forEach(model => { if (!nativeIds.has(model.id)) wanted.set(model.id, model); }); } homeCatalogMatchCount = completeMode ? matchingIds.size : 0; grid.querySelectorAll(':scope > [data-roomgrid-catalog-card="1"]').forEach(card => { const id = cardId(card); if (!wanted.has(id)) card.remove(); else wanted.delete(id); }); const fragment = document.createDocumentFragment(); wanted.forEach(model => fragment.appendChild(createCatalogCard(model))); if (fragment.childNodes.length) { grid.appendChild(fragment); try { window.dispatchEvent(new CustomEvent('roomgrid_catalog_cards_changed')); } catch (_) {} } }; const applyHomeGrid = (grid, models) => { const settings = store.state.settings; const filtered = sortHomeModels(models.filter(model => matchesHomeFilters(model, settings)), settings.normalHomeSort); const completeMode = usesCompleteLiveCatalog(settings); const total = completeMode ? Math.max(homeCatalogMatchCount, filtered.length) : filtered.length; const configuredPageSize = Math.max(0, Number(settings.normalHomePageSize) || 0); const progressiveAllMode = completeMode && !configuredPageSize; const pageSize = progressiveAllMode ? Math.max(1, homeCatalogRenderLimit) : (configuredPageSize || Math.max(1, total)); const pages = progressiveAllMode ? 1 : Math.max(1, Math.ceil(total / pageSize)); const page = progressiveAllMode ? 1 : Math.min(pages, Math.max(1, Number(settings.normalHomePage) || 1)); const visible = new Set(filtered.slice((page - 1) * pageSize, page * pageSize).map(model => model.card)); const displayOrder = new Map(filtered.map((model, index) => [model.card, index])); models.forEach(model => { const shown = visible.has(model.card); model.card.classList.toggle('rg-home-filter-hidden', !shown); model.card.style.order = String(displayOrder.get(model.card) ?? filtered.length + model.nativeOrder); if (shown) syncCardDetails(model); }); updateHomeCatalogMore(grid, visible.size, total, progressiveAllMode); const toolbar = store.state.settings.normalHomeToolbar !== false ? ensureHomeToolbar(grid) : null; if (!toolbar) { homeToolbar?.remove(); homeToolbar = null; homeToolbarGrid = null; return; } const ui = toolbar._roomgrid; if (document.activeElement !== ui.search) ui.search.value = settings.normalHomeSearch || ''; ui.sort.value = settings.normalHomeSort; ui.pageSize.value = String(settings.normalHomePageSize); ui.previous.disabled = page <= 1; ui.next.disabled = page >= pages; ui.reloadCatalog.disabled = homeCatalogLoading; const filterCount = activeFilterCount(settings); const statusTail = [ ` ${tx('可见', 'visible', 'sichtbar')} · ${models.length} ${tx('已加载', 'loaded', 'geladen')} · ${tx('页面', 'page', 'Seite')} ${page}/${pages}`, filterCount ? ` · ${filterCount} ${tx('活动筛选', 'active filters', 'aktive Filter')}` : '', completeMode ? homeCatalogLoading ? ` · ${tx('正在加载完整直播列表', 'loading complete live list', 'vollständige Live-Liste wird geladen')} ${homeCatalog.size}${homeCatalogExpected ? `/${homeCatalogExpected}` : ''}` : homeCatalogComplete ? ` · ${tx('已找到所有匹配的直播模型', 'all matching live models found', 'alle passenden Live-Models gefunden')}` : ` · ${tx('直播列表不完整,请重试', 'live list incomplete, retry', 'Live-Liste unvollständig, erneut laden')}` : '', ].join(''); const statusSignature = `${visible.size}/${total}${statusTail}`; if (ui.status.dataset.signature !== statusSignature) { ui.status.dataset.signature = statusSignature; ui.status.replaceChildren($('span', {}, [$('b', {}, `${visible.size} / ${total}`), statusTail])); } }; const cleanupHomeCards = () => { document.querySelectorAll('.rg-home-filter-hidden').forEach(card => card.classList.remove('rg-home-filter-hidden')); document.querySelectorAll('[data-roomgrid-home-meta]').forEach(node => node.remove()); document.querySelectorAll('[data-roomgrid-catalog-card="1"]').forEach(card => card.remove()); document.querySelectorAll(HOME_CARD_SELECTOR).forEach(card => card.style.removeProperty('order')); removeHomeCatalogMore(); homeCatalogMatchCount = 0; homeCatalogFilterSignature = ''; homeCatalogRenderLimit = HOME_CATALOG_RENDER_INITIAL; homeToolbar?.remove(); homeToolbar = null; homeToolbarGrid = null; }; const loadHomeCatalog = async (force = false) => { if (homeCatalogLoading || stopped || !isHomepage()) return; const startRoute = `${location.pathname}${location.search}`; const wantFull = force || usesCompleteLiveCatalog(store.state.settings); if (!force && homeCatalogStarted && Date.now() - homeCatalogLoadedAt < 120000 && (!wantFull || homeCatalogComplete)) return; homeCatalogStarted = true; homeCatalogLoading = true; homeCatalogComplete = false; homeCatalogExpected = 0; homeCatalogFailedPages = 0; const progressTask = notifications?.beginProgress?.(tx('正在加载直播目录', 'Loading the live catalog', 'Live-Katalog wird geladen'), { delayMs: 280 }); const updateProgress = () => progressTask?.update({ label: tx('正在加载直播目录', 'Loading the live catalog', 'Live-Katalog wird geladen'), done: homeCatalog.size, total: homeCatalogExpected || null }); const controller = new AbortController(); homeCatalogAbort = controller; const timeout = setTimeout(() => controller.abort(), 90000); try { const fetchPage = async offset => { if (!isHomepage() || `${location.pathname}${location.search}` !== startRoute) throw new DOMException('Route changed', 'AbortError'); let lastError = null; const hosts = [...new Set([location.hostname, 'chaturbate.com', 'de.chaturbate.com', 'en.chaturbate.com'])]; for (const hostname of hosts) { if (controller.signal.aborted) break; try { const url = new URL('/api/ts/roomlist/room-list/', `https://${hostname}`); url.searchParams.set('offset', String(offset)); url.searchParams.set('limit', String(HOME_CATALOG_BATCH)); url.searchParams.set('_rg', `${Date.now()}_${offset}`); let payload = await roomGridRequestJson(url.toString(), { signal: controller.signal, timeoutMs: 12000 }); for (let depth = 0; depth < 3 && payload && !Array.isArray(payload.rooms) && !Array.isArray(payload.results); depth += 1) payload = payload.data || payload.payload || payload.response; const rooms = Array.isArray(payload?.rooms) ? payload.rooms : Array.isArray(payload?.results) ? payload.results : null; if (!rooms) throw new Error(`invalid catalog (${hostname})`); return { ...payload, rooms }; } catch (error) { lastError = error; if (controller.signal.aborted) break; } } throw lastError || new Error('catalog unavailable'); }; const ingest = items => { for (const item of items) { if (!isHomepage() || `${location.pathname}${location.search}` !== startRoute) { controller.abort(); break; } const id = normalizeUsername(item?.username); if (!isLikelyUsername(id)) continue; const started = Math.max(0, Number(item.start_timestamp) || 0); const model = { id, currentShow: textValue(item.current_show || 'public').toLocaleLowerCase(), subject: textValue(item.room_subject || item.subject).slice(0, 240), tags: [...new Set((Array.isArray(item.tags) ? item.tags : []).map(textValue).filter(Boolean))].slice(0, 24), gender: ['f', 'm', 'c', 't'].includes(item.gender) ? item.gender : 'unknown', age: Math.max(0, Math.min(99, Math.round(Number(item.display_age) || 0))), location: textValue(item.location).slice(0, 100), country: textValue(item.country).slice(0, 40).toUpperCase(), viewers: Math.max(0, Math.round(Number(item.num_users) || 0)), followers: Math.max(0, Math.round(Number(item.num_followers) || 0)), secondsOnline: started ? Math.max(0, Math.round(Date.now() / 1000 - started)) : 0, isHd: !!item.is_hd, isNew: !!item.is_new, isGaming: !!item.is_gaming }; homeCatalog.set(id, model); analytics?.observeModel?.({ ...model, viewerCount: model.viewers, currentShow: model.currentShow, source: 'native-home-catalog', observedAt: Date.now() }); } updateProgress(); schedule(); }; const first = await fetchPage(0); homeCatalog.clear(); ingest(first.rooms); const reportedTotal = Math.max(first.rooms.length, Math.round(Number(first.total_count) || Number(first.all_rooms_count) || first.rooms.length)); homeCatalogExpected = wantFull ? reportedTotal : first.rooms.length; updateProgress(); const step = HOME_CATALOG_BATCH; const offsets = []; for (let offset = step; offset < homeCatalogExpected; offset += step) offsets.push(offset); let cursor = 0; const worker = async () => { while (!controller.signal.aborted) { const index = cursor++; if (index >= offsets.length) return; try { ingest((await fetchPage(offsets[index])).rooms); } catch (error) { if (controller.signal.aborted) throw error; homeCatalogFailedPages += 1; } } }; await Promise.all(Array.from({ length: Math.min(HOME_CATALOG_CONCURRENCY, offsets.length) }, worker)); homeCatalogComplete = wantFull && !homeCatalogFailedPages; homeCatalogLoadedAt = Date.now(); analytics?.flush?.().catch?.(() => {}); } catch (_) { homeCatalogComplete = false; } finally { clearTimeout(timeout); homeCatalogLoading = false; if (homeCatalogAbort === controller) homeCatalogAbort = null; progressTask?.finish(); schedule(); } }; const queueHomeCatalogLoad = () => { if (homeCatalogLoadTimer || homeCatalogLoading || stopped || !isHomepage()) return; homeCatalogLoadTimer = setTimeout(() => { homeCatalogLoadTimer = 0; loadHomeCatalog(); }, 900); }; const pageAllowsDesign = settings => { const path = String(location.pathname || '/').replace(/\/+$/u, '') || '/'; if (path === '/') return settings.normalDesignHome !== false; if (/^\/[a-z0-9_-]+$/iu.test(path) && !/^\/(?:tag|tags|category|categories|apps|contest)$/iu.test(path)) return settings.normalDesignRoom !== false; return settings.normalDesignListings !== false; }; const apply = () => { const settings = applySuiteAppearance(store); const on = settings.normalPageDesign !== false && pageAllowsDesign(settings); document.body.classList.toggle('rg-site-design', on); document.body.classList.toggle('rg-site-list', on && settings.normalPageView === 'list'); document.body.classList.toggle('rg-site-promos-hidden', on && !!settings.normalHidePromos); document.body.classList.toggle('rg-site-footer-compact', on && settings.normalCompactFooter !== false); document.body.classList.toggle('rg-site-nav-polish', on && settings.normalPolishNavigation !== false); document.body.classList.toggle('rg-site-sidebar-polish', on && settings.normalPolishSidebar !== false); document.body.classList.toggle('rg-home-fixed-columns', on && isHomepage() && Number(settings.normalHomeColumns) > 0); document.body.classList.toggle('rg-home-custom-ratio', on && isHomepage() && settings.normalHomeImageRatio !== 'auto'); document.documentElement.style.setProperty('--rg-site-scale', String((Number(settings.normalPageCardScale) || 100) / 100)); document.documentElement.style.setProperty('--rg-site-density', settings.normalPageDensity === 'compact' ? '.78' : settings.normalPageDensity === 'spacious' ? '1.22' : '1'); document.documentElement.style.setProperty('--rg-home-columns', String(Math.max(1, Number(settings.normalHomeColumns) || 1))); document.documentElement.style.setProperty('--rg-home-image-ratio', settings.normalHomeImageRatio === '16/9' ? '16 / 9' : settings.normalHomeImageRatio === '4/3' ? '4 / 3' : settings.normalHomeImageRatio === '1/1' ? '1 / 1' : '3 / 4'); }; let scanFrame = 0; const scan = () => { if (stopped) return; const designEnabled = store.state.settings.normalPageDesign !== false && pageAllowsDesign(store.state.settings); const homepage = designEnabled && isHomepage(); const grids = homepage ? [...document.querySelectorAll('[data-testid="room-list"] .RoomCardGrid,.room_list')].map(grid => ({ grid, cards: [...grid.children].filter(card => card.matches?.(HOME_CARD_SELECTOR)) })).filter(item => item.cards.length) : []; const primary = grids.sort((left, right) => right.cards.length - left.cards.length)[0] || null; // Paint the controls before model decoration, sorting and metadata work. if (primary && store.state.settings.normalHomeToolbar !== false) ensureHomeToolbar(primary.grid); cancelAnimationFrame(scanFrame); scanFrame = requestAnimationFrame(() => { scanFrame = 0; if (stopped) return; const cards = document.querySelectorAll('[data-testid="room-card"],li.RoomCard,li.room_list_room,li.roomCard,article[data-username],li[data-username]'); let count = 0; for (const card of cards) { if (++count > 1200) break; if (card.offsetWidth < 90) continue; let id = normalizeUsername(card.getAttribute('data-username') || card.querySelector('[data-username]')?.getAttribute('data-username') || ''); if (!id) { const link = card.querySelector('a[href]'); try { id = normalizeUsername(new URL(link?.href || '', location.href).pathname.split('/').filter(Boolean)[0] || ''); } catch (_) {} } if (!isLikelyUsername(id)) continue; const room = store.state.rooms.find(item => item.id === id); const stat = analytics?.getModelSync?.(id); card.classList.toggle('rg-site-saved', store.state.settings.normalMarkSaved !== false && !!room); card.classList.toggle('rg-site-visited', store.state.settings.normalMarkVisited !== false && Number(stat?.totalMs || 0) > 0); card.classList.toggle('rg-site-favorite', store.state.settings.normalMarkFavorites !== false && !!room && roomInGroup(room, FAVORITE_GROUP_ID)); } if (!homepage) { cleanupHomeCards(); return; } if (!primary?.grid?.isConnected) { cleanupHomeCards(); return; } syncCatalogCards(primary.grid); const primaryCards = [...primary.grid.children].filter(card => card.matches?.(HOME_CARD_SELECTOR)); const models = primaryCards.map((card, index) => modelForCard(card, Number(card.dataset.roomgridNativeOrder || index))).filter(Boolean); primaryCards.forEach((card, index) => { if (!card.dataset.roomgridNativeOrder) card.dataset.roomgridNativeOrder = String(card.dataset.roomgridCatalogCard === '1' ? 100000 + index : index); }); applyHomeGrid(primary.grid, models); queueHomeCatalogLoad(); }); }; const schedule = debounce(scan, 250); const observer = new MutationObserver(schedule); observer.observe(document.body, { childList: true, subtree: true }); const unsubscribe = store.subscribe(() => { apply(); schedule(); }); const unsubscribeAnalytics = analytics?.subscribe?.(schedule) || (() => {}); const onRoute = () => { apply(); if (!isHomepage()) { clearTimeout(homeCatalogLoadTimer); homeCatalogLoadTimer = 0; homeCatalogAbort?.abort?.(); cleanupHomeCards(); } schedule(); }; window.addEventListener('roomgrid:navigation', onRoute); apply(); // The homepage toolbar is the main entry point for filtering and settings. // Mount it before any per-card decoration so it is usable in the first // paint even on pages containing hundreds of model cards. if (isHomepage() && store.state.settings.normalPageDesign !== false && store.state.settings.normalHomeToolbar !== false) { const earlyGrids = [...document.querySelectorAll('[data-testid="room-list"] .RoomCardGrid,.room_list')] .map(grid => ({ grid, count: [...grid.children].filter(card => card.matches?.(HOME_CARD_SELECTOR)).length })) .filter(item => item.count) .sort((left, right) => right.count - left.count); if (earlyGrids[0]) ensureHomeToolbar(earlyGrids[0].grid); } schedule(); setTimeout(schedule, 1200); return () => { stopped = true; cancelAnimationFrame(scanFrame); clearTimeout(homeCatalogLoadTimer); homeCatalogAbort?.abort?.(); observer.disconnect(); unsubscribe(); unsubscribeAnalytics(); window.removeEventListener('roomgrid:navigation', onRoute); cleanupHomeCards(); versionBadge.remove(); style.remove(); document.body.classList.remove('rg-site-design','rg-site-list','rg-site-promos-hidden','rg-site-footer-compact','rg-site-nav-polish','rg-site-sidebar-polish','rg-home-fixed-columns','rg-home-custom-ratio'); }; } const CAMCLIENT_VERSION_HISTORY = [{"version":"20.116.1-custom","date":"2026-11-05","notes":["MultiCam: Die Hover-Live-Vorschau der linken Raumliste funktioniert auch mit „Nur schauen“. Ein eigener Schalter speichert die Wahl und beendet eine laufende Vorschau beim Ausschalten."],"source":"Aktuelle Änderung"},{"version":"20.116.0-custom","date":"2026-11-05","notes":["Kompaktere Live-Navigation und MultiCam-Werkzeuge, Rückgängig für entfernte Kameras sowie verständliche Streamzustände und geordnete Ersatzmodels. Bibliothekseditoren und Designoptionen entstehen bei Bedarf. SingleCam bietet optional pausierenden Chat. Nur schauen verschiebt aufschiebbare Arbeit und lässt die Wiedergabe weiterlaufen."],"source":"Aktuelle Änderung"},{"version":"20.115.1-custom","date":"2026-11-05","notes":["MultiCam zeigt eine Ersatzverzögerung von 0 Sekunden nach dem Speichern korrekt an. Die Statusübersicht zählt Kameras und bezeichnet verbindende oder pausierte Kameras nicht mehr als aktive Streams."],"source":"Aktuelle Änderung"},{"version":"20.115.0-custom","date":"2026-11-05","notes":["MultiCam wertet Wiedergabe und Qualitätsbudget nur für eingebundene Player aus. Ausgeblendete Raumlisten werden erst beim Einblenden aktualisiert. Bildvorschauen ohne sichtbare Live-Karten oder mit deaktivierter Vorschau pausieren ihren Taktgeber."],"source":"Aktuelle Änderung"},{"version":"20.114.0-custom","date":"2026-11-04","notes":["Kompakter Bibliothekskopf: Suche und Sammlung weiter oben; sämtliche Kennzahlen und Datenquellen gemeinsam aufklappbar. Einstellungs-Direktlinks im Hauptmenü durch einen zentralen Zugang ersetzt; MultiCam-Menü ohne doppelten Einstellungen-Eintrag und mit gemeinsamem Import-/Export-Zugang."],"source":"Aktuelle Änderung"},{"version":"20.113.0-custom","date":"2026-11-04","notes":["MultiCam: wiederholte identische Fehlermetriken nicht bei jedem Fortschrittsereignis archivieren; neue Fehler und regelmäßige Messpunkte bleiben erhalten. Erfolgreich erholte Medienfehler lösen später keinen unnötigen Playerneustart mehr aus. MultiCam-Steuerung und globale Einstellungen eindeutiger benannt."],"source":"Aktuelle Änderung"},{"version":"20.112.0-custom","date":"2026-11-03","notes":["Raumabfragen bleiben bis zum vollständigen Antwortinhalt abbrechbar; ungültige Antworten werden kontrolliert behandelt. Veraltete Vorschauabrufe und Automatikaktionen stoppen, Fokus-Trennleisten geben Mauslistener frei und der automatische Sparmodus stellt persönliche Werte beim Beenden wieder her. Fehlerhafte Bilddekodierung gibt temporäre Medienadressen frei."],"source":"Aktuelle Änderung"},{"version":"20.111.0-custom","date":"2026-11-02","notes":["MultiCam: unnötige Player-Neustarts bei erneuerten Signaturen und vorübergehend unvollständigen Raumantworten vermeiden; nach endgültigen Netzwerkfehlern zeitnah mit frischer Raumabfrage wiederverbinden."],"source":"Aktuelle Änderung"},{"version":"20.110.0-custom","date":"2026-11-02","notes":["MultiCam: Sichtbare Streams beim gemeinsamen Start vor Hintergrund-Raumprüfungen laden; die übrigen Prüfungen werden nachgeholt."],"source":"Aktuelle Änderung"},{"version":"20.109.0-custom","date":"2026-11-02","notes":["MultiCam und SingleCam: Laufende Videos erhalten Vorrang bei Archivlesen und Sammlung. Große lokale Bestände werden effizienter verarbeitet; persönliche Daten und atomare Speicherung bleiben erhalten."],"source":"Aktuelle Änderung"},{"version":"20.108.0-custom","date":"2026-11-02","notes":["MultiCam: Ein kleines X entfernt einzelne Streams aus der aktuellen Raster- oder Fokusansicht; gespeicherte Modeldaten bleiben erhalten."],"source":"Aktuelle Änderung"},{"version":"20.107.0-custom","date":"2026-10-11","notes":["Gemeinsame Menüperformance: Reichhaltige lokale Models/Profiles in groben Seiten laden, feinere Bedienpausen beim Indexaufbau und unveränderte Diagnose-Sicherungsprüfung wiederverwenden. Parallele Schreibaufrufe koordinieren, atomare Speicherung und Retry erhalten; keine Historie oder Funktionen kürzen."],"source":"Aktuelle Änderung"},{"version":"20.106.0-custom","date":"2026-10-11","notes":["SingleCam nutzt die verfügbare Fensterbreite auch auf Ultrawide-Bildschirmen. Seitenleiste und untere Werkzeuge unabhängig einklappen, Zustand lokal merken und Stream maximieren ohne Neuverbinden. Alle Funktionen bleiben erreichbar."],"source":"Aktuelle Änderung"},{"version":"20.105.0-custom","date":"2026-10-11","notes":["SingleCam: Frei einstellbare responsive Streamgröße mit Breiten- und Höhenregler direkt unter dem Player. Lokal gespeichert; automatische Größe, Kino und Vollbild bleiben verfügbar."],"source":"Aktuelle Änderung"},{"version":"20.104.0-custom","date":"2026-10-11","notes":["SingleCam: Jeder Screenshot bleibt als eigenes Galeriebild erhalten, auch bei identischem Bildinhalt. Eindeutige Dateinamen verhindern Zusammenfassung bei schnellen Aufnahmen. Vorhandene Medien bleiben unverändert."],"source":"Aktuelle Änderung"},{"version":"20.103.0-custom","date":"2026-10-11","notes":["Neues Menü „Über“ neben „Hilfe“ mit Autor, aktueller Version, Lizenz und durchsuchbarer vollständiger auffindbarer Versionshistorie. Versionsdetails einzeln aufklappen; identisch in persönlicher und sauberer Veröffentlichungsdatei."],"source":"Aktuelle Änderung"},{"version":"20.102.0-custom","date":"2026-10-11","notes":["„Hilfe“ neben „Einstellungen“: kurze Erklärungen der Menüs, wichtigsten Einstellungen und lokalen Speicher-/Downloadgrenzen. Suite/SingleCam/MultiCam, Tastaturbedienung und schmale Fenster.","Getrennte saubere Veröffentlichungsdatei per build.mjs --release, mit Prüfung neutraler Standarddaten und Ausschluss bekannter Privat-/Testmarker. Keine persönlichen Browserdaten exportiert, Lizenzhinweise und Speicheridentitäten erhalten."],"source":"Änderungsprotokoll"},{"version":"20.101.0-custom","date":"2026-10-11","notes":["Gezeigte animierte Vorschauoptionen auch im Menü „Startseiten-Einstellungen“ verfügbar, mit denselben gespeicherten Werten und FPS bis 240. Direkte Optionen unter „Ansicht“ bleiben erhalten."],"source":"Änderungsprotokoll"},{"version":"20.100.0-custom","date":"2026-10-11","notes":["Alle Live-Vorschau- und Hoveroptionen direkt unter Startseite → Ansicht; FPS bis 240, Aktualisierungsart, Parallelität, Hovervideo und Startverzögerung. Gemeinsame Einstellungen bleiben erhalten.","Hover-Vergrößerung bis 600 % mit zahlreichen zusätzlichen Stufen und globalem 1%-Slider. Größe an verfügbaren Bildschirmplatz anpassen; benutzerdefinierte Zwischenwerte sichtbar erhalten.","Automatisierte Brave-Bedien-/Persistenz-/Mobil- und Decoderprüfungen; keine Änderungen der aktiven Installation oder Speicheridentitäten."],"source":"Änderungsprotokoll"},{"version":"20.99.0-custom","date":"2026-10-09","notes":["Live-Hover nach verspäteter Sichtbarkeitserkennung zuverlässig nachstarten; keine doppelten Starts durch weitere Sichtbarkeitsmeldungen. Bestehende Stummschaltung, Ein-Player-Grenze und Bereinigung erhalten.","Brave-Decoder-Regression deckt jetzt Pointerenter vor der ersten Sichtbarkeitsmeldung ab. Keine Änderung von Benutzerdaten oder Speicheridentitäten."],"source":"Änderungsprotokoll"},{"version":"20.98.0-custom","date":"2026-10-08","notes":["Geschlossene Bibliotheks-/Statistikfilter erzeugen nicht mehr vorab den vollständigen Facettenindex; Optionen werden beim tatsächlichen Öffnen mit Lade-/Fehlerstatus vorbereitet. Öffnung nach Navigation setzt keine veralteten Ergebnisse ein.","Öffentliche Tags werden flüchtig und mit genauer Inhaltsprüfung wiederverwendet. Erste große Bibliotheks-Indexsortierung erfolgt kooperativ; Sortierung, Gleichstände und vollständige Hintergrundveröffentlichung bleiben erhalten.","Interessenaggregation nur bei Änderungen ihrer tatsächlichen Eingaben neu berechnen; vorhandenen Modelindex verwenden. Einstellungssuche liest je Durchlauf einen aktuellen Meldungssnapshot statt zahlreicher vollständiger Kopien.","Fortschrittsupdates behalten unveränderte Balken- und Meldungs-DOM; echte Prozent-/Statusänderungen bleiben sichtbar. Historiennormalisierung vermeidet URL-Parser für exakt validierte kanonische Vorschaubilder und wiederholten Aufbau derselben Eventtyp-Whitelist.","Keine Datenmigration, Funktionsreduktion, Accountänderung oder Änderung der aktiven Benutzerinstallation."],"source":"Änderungsprotokoll"},{"version":"20.97.0-custom","date":"2026-10-08","notes":["Bedienung weiter entlastet, ohne Funktionen zu entfernen: Tag-Hilfe, Listenoptionen, Kombinationsregeln und Speicherwerkzeuge einzeln aufklappbar. Einfache Suche, passende Streams und aktive Regel bleiben sichtbar; Öffnungszustand im bisherigen Discover-Speicher erhalten.","Einstellungen zeigen kurze Themen mit Optionszahl. Lange Hinweise und eine verständliche Bedienhilfe sind separat aufklappbar; vorhandene Bedienelemente, zwölf Reiter und Speicherwege bleiben erhalten.","Bibliotheks-/Statistik-/Nutzerwerkzeuge erklären bereits geschlossen ihren Zweck und bieten einen Einklappknopf mit Tastaturfokus. Interessen haben drei unabhängig aufklappbare Detailbereiche; Übersicht bleibt sichtbar, Detail-DOM entsteht nur bei Bedarf.","Schnelle Tag-Änderung darf einen gerade geöffneten Bereich nicht schließen: tatsächlichen DOM-Zustand vor Neudarstellung sichern statt auf das verzögerte native Toggle-Ereignis warten. Keine Datenmigration oder Änderung der aktiven Installation."],"source":"Änderungsprotokoll"},{"version":"20.96.0-custom","date":"2026-10-08","notes":["Kompakte Bibliotheks-/Statistikansichten mit vollständigen aufklappbaren Werkzeugen, sichtbaren Filterchips und klaren Bibliotheks-Unteransichten. Lokaler7/30/90/365-Tage-Vorperiodenvergleich. Konto, lokale Nutzung und Interessen getrennt; gefolgte Liste lokal zuerst, Aktualität und fehlende Angaben erläutert.","Tag-Explorer erklärt Zweck und Kombinationen, zeigt Auswahl/Trefferzahl sichtbar und passende Streams direkt darunter. Größere Klickflächen; vollständige bestehende Gruppen-/Feed-/Bildfunktionen bleiben erreichbar.","Entdecken: „Nicht interessiert“, einen Tag/eine Woche ausblenden, weniger ähnliche Themen und Rücknahme. Gilt nur für Empfehlungen; keine Entfolgung oder Entfernung aus Katalog/Bibliothek.","Einstellungssuche über einzelne Optionen aller Reiter, dauerhaft gespeicherte Favoriten, „Nur geänderte Werte“ mit Standardvergleich. Designreset mit Vorschau, Konfigurationssicherung und geschütztem Rückgängig.","SingleCam: Galerie größer, Bildaktionen unterhalb der Medien im Menü; Auswahl, Zoom und Videoposition bleiben bei Refresh erhalten. Lokales Archiv und PC-Speicherung werden getrennt angezeigt. Tagvorschläge/Statistik/ausgeblendete Alternativen nur bei Bedarf. Keine bewegte Scrollanimation beim Einzelansichtswechsel.","Faire Benachrichtigungsprüfung über die ersten100 Regeln hinaus, deaktivierte Regeln überspringen, höchstens drei parallele Abfragen mit8s-Timeout und gemeinsamem laufenden Check. Tatsächlichen frischen Katalog wiederverwenden; fehlende Daten nicht als offline ausgeben. Alte Werkzeugpanels beim Wechsel vollständig bereinigen.","41. eigener Tag wird mit verständlichem Hinweis abgelehnt, ohne Eingabe oder bestehende Tags zu verdrängen. Bestehende Modeldaten, Medien, Notizen und Speicheridentitäten erhalten; keine Datenmigration oder neue Produktionsabhängigkeit."],"source":"Änderungsprotokoll"},{"version":"20.95.0-custom","date":"","notes":["Alle zwölf Einstellungsreiter behalten; Themen anfangs einklappen, kurze Erklärungen und einzelne/gemeinsame Faltung. Geöffnete Bereiche während Änderungen innerhalb der Instanz erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.94.0-custom","date":"","notes":["Weitere Menüoptimierungen: geschlossene Pickerlisten erst bei Öffnung aufbauen, Tag-/Sortierarbeit und wiederholte Statistikberechnung reduzieren."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.93.0-custom","date":"","notes":["Screenshots und fertige Video-Segmente sofort und verbindlich in der Model-Galerie archivieren, unabhängig vom PC-Download. Galerie nach Reload lokal zuerst, Speicherfehler ehrlich anzeigen und alte Medienquellen sicher bereinigen."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.92.0-custom","date":"","notes":["Zusätzliche Vorschau-FPS-Stufen bis 240 und tatsächliche höhere Abruf-Taktung ohne alten 20-FPS-Timerboden. Eine ausstehende Bildanfrage pro Karte und Offscreen-/Hidden-Schutz bleiben."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.91.0-custom","date":"","notes":["Vorschauwerte zwischen Menüs abgleichen, langsame Einzelbilder isolieren und gute Bilder bei Fehler behalten. Hovervideo bei Hintergrundaktualisierung nicht neu starten; sofortiger Start und Vergrößerung konsistent."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.90.0-custom","date":"","notes":["Unveränderte Einstellungsspiegel ohne wiederholten vollständigen Zeichenhash vergleichen. Bestehende Sicherungs-, Retry- und Wiederherstellungspfade bleiben erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.89.0-custom","date":"","notes":["Wiederkehrende Standardfunktionen im Tampermonkey-Scope lokal binden und zusätzliche Menü-/Kartenengpässe reduzieren."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.88.0-custom","date":"","notes":["Facetten und Karten portionsweise vorbereiten, MultiCam-Diagnose kompakt darstellen und Rechenarbeit außerhalb benötigter Bereiche reduzieren."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.87.0-custom","date":"","notes":["Gespeicherte öffentliche SingleCam-Profile und Cover zuerst verwenden, frische Profile ohne erneuten Abruf öffnen, ältere im Hintergrund aktualisieren. Privatmodus und gültige Katalogstände erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.86.0-custom","date":"","notes":["Vollständig gespeicherte offizielle Following-Liste sofort lokal anzeigen, im Hintergrund abgleichen und an das erkannte Konto binden."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.85.0-custom","date":"","notes":["MultiCam-Pegelanzeigen global alle an/aus oder nach Einzelvorgaben; gilt auch bei Modelwechsel und nach Neuladen, ohne Tonzustand zu verändern."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.84.0-custom","date":"","notes":["Startseite zeigt Streams zuerst. Filter, Ansicht und Übersicht separat aufklappen; aktive Filter sichtbar und Eingabefokus erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.83.0-custom","date":"","notes":["Schwarzen Start nach Model-Drop repariert: laufende Verbindung teilen, gültige Quelle beim Kartenaufbau zustellen, Laden bis zu echten Videodaten anzeigen und Fehler/Fortsetzen verständlich behandeln."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.82.0-custom","date":"","notes":["MultiCam-Pufferunterbrechungen und unnötige Arbeit in Seitenlisten reduziert; niedrig eingestellte Qualität berücksichtigen."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.81.0-custom","date":"","notes":["Bereits angezeigte Models in der MultiCam-Ersatzliste markieren, damit doppelte Kameras vermieden werden."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.80.0-custom","date":"","notes":["Offiziell gefolgte Models in der MultiCam-Seitenliste. Unabhängige Filter und Sortierung für Tags, Land, Alter und weitere Kriterien."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.79.0-custom","date":"","notes":["Drag-and-drop aus der linken MultiCam-Liste zuverlässiger und sichtbare Ziel-/Drop-Rückmeldung."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.78.0-custom","date":"","notes":["Notizentwürfe bei Bewertungen erhalten, Tagfilter-Fokus und gespeicherte Ansichten repariert, Tag-Hinzufügen per Button. Sicherungen umfassen getrennte Datenbereiche; Teilen und pseudonymisierte Exporte bereinigt. Taggruppen nicht still verdrängen, gespeicherte Tonvorgaben korrekt wiederherstellen, Galerie-Videos nach Reload erreichbar und Einstellungspanel nicht abschneiden."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.77.0-custom","date":"","notes":["SingleCam startet ohne Abwarten der kompletten Suite-Aktivierung. Beim Wechsel in die Einzelansicht den Katalog nicht erneut starten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.76.0-custom","date":"","notes":["Nutzerübersicht lokal zuerst und unnötige Hintergrundarbeit vermeiden. Teure Detailbereiche nur bei Bedarf laden."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.75.0-custom","date":"","notes":["Lokale Wiederverwendung von Katalog, Statistik- und Menüständen; verfügbare Daten zuerst zeigen und im Hintergrund aktualisieren."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.74.0-custom","date":"","notes":["Tag-Explorer verständlicher gegliedert: Auswahl, Kombinationsregeln, Treffer und direkte passende Streams; Gruppen und eigene Tags bleiben erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.73.1-custom","date":"","notes":["Nutzeransicht bleibt auch bei blockierter oder unvollständiger Statistik bedienbar; fehlende Daten verständlich anzeigen."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.73.0-custom","date":"","notes":["Mehr Sortier- und Filteroptionen für Gefolgt, zusätzliche erkennbare Kontoinfos und frischen Bibliotheks-Livestatus abgleichen. Gespeicherte Bilder während Aktualisierung erhalten."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.72.0-custom","date":"","notes":["Menü- und Ladeengpässe reduziert, große Statistikauswertungen zwischengespeichert, Seitenbreite responsiver. Katalog-Rückoff bei Serverlimits statt wiederholter Abrufschleifen."],"source":"Projektstatus und Prüfnachweise"},{"version":"20.71.0-custom","date":"2026-09-23","notes":["Das MultiCam-Kontrollzentrum zeigt benutzerdefinierte Gruppen wieder korrekt in der Ersatzmodel-Auswahl an. Ein außerhalb des Fensters lokaler Helfer hatte beim Öffnen einen Laufzeitfehler ausgelöst und den gesamten Inhalt verhindert.","Der Brave-Regressionstest öffnet das Kontrollzentrum mit einer eigenen Gruppe und prüft deren sichtbaren Eintrag."],"source":"Änderungsprotokoll"},{"version":"20.70.0-custom","date":"2026-09-23","notes":["Die technischen Auflösungs-/Verbindungs-/Puffer- und Gesundheits-Chips liegen nicht mehr über den MultiCam-Videokarten; die Gesundheitsübersicht bleibt im MultiCam-Kontrollzentrum.","Audiopegelanzeigen lassen sich global im MultiCam-Kontrollzentrum sowie einzeln je Kamera mit Global / Einblenden / Ausblenden steuern. Die Wahl bleibt lokal/updatefest gespeichert und ändert weder Ton noch Messung.","Brave-Regression bestätigt: Audioaktivität bleibt bei stummgeschaltetem Stream messbar, auch wenn der Meter ausgeblendet ist. Bestehende Nutzerdaten und Storage-Schemaversion bleiben erhalten."],"source":"Änderungsprotokoll"},{"version":"20.69.0-custom","date":"2026-09-23","notes":["MultiCam ist jetzt ein eigener Tab direkt neben Start in der Camclient-Hauptnavigation und damit auch aus Discover und SingleCam ohne den Aktionsbutton erreichbar.","Der Tab verwendet die bestehende MultiCam-Route und erhält vorhandene Workspace-Daten. Der bisherige rechte MultiCam-Button bleibt bestehen.","Brave-Prüfungen bestätigten den Header in allen zwölf Sprachen, die MultiCam-Route und die responsive Navigation. Es wurden keine gespeicherten Daten oder Einstellungen migriert."],"source":"Änderungsprotokoll"},{"version":"20.68.0-custom","date":"","notes":["Gemeinsamer Ladefortschritt für längere Ladephasen. Following-Erkennung robuster; Zuschaueranzeige mit lokalisiertem Fallback repariert."],"source":"Umsetzungsbericht"},{"version":"20.67.0-custom","date":"2026-09-23","notes":["„Mein Nutzer“ merkt sich abgeleitete Zusammenfassungen pro lokalem Datenstand und Zeitraum. Große Ereignisverläufe werden in abbrechbaren Abschnitten ausgewertet; die Werte für 7/30/90/365 Tage und Gesamtverlauf bleiben geprüft.","Statistikzentrum und Modellbibliothek geben große Filter- und Sortierarbeiten in UI-Zeitabschnitten frei. Ein Wechsel zu einem anderen Menü kann die laufende Arbeit abbrechen.","Neue globale Ansicht „Gefolgt“ liest die offiziellen Online- und Offline-Following-Seiten seitenweise und ausschließlich lesend. Suche, Statusfilter, Sortierung, Lade-/Teil-/Fehlerzustände und direkter Modelzugriff sind enthalten; eine Parserstörung löscht die letzte lesbare Liste nicht.","Following-Texte sind für alle aktiven Camclient-Sprachen abgedeckt. Es wurden weder Follow-Beziehungen noch lokale Nutzer-/Modeldaten verändert.","Prüfung mit 32.000 Modellen/100.000 synthetischen Ereignissen in Headless Brave: erste Statistikansicht 634 ms, warme Ansichtswechsel 96–295 ms, längste gemessene UI-Aufgabe 178 ms. Das sind reproduzierbare Fixture-Werte, keine Zusage für jedes reale Gerät oder Konto."],"source":"Änderungsprotokoll"},{"version":"20.66.0-custom","date":"2026-09-22","notes":["Statistikzentrum, Modellbibliothek und „Mein Nutzer“ zeigen beim ersten Öffnen sofort die bereits verfügbaren lokalen Daten. Die vollständige IndexedDB-Auswertung läuft danach im Hintergrund weiter und ersetzt den Snapshot automatisch.","Seitenwechsel bleiben dadurch bedienbar, auch wenn zehntausende Models oder sehr viele Ereignisse gespeichert sind. Ein langsamer oder blockierter Statistik-Import kann die Navigation nicht mehr mit „Daten werden vorbereitet …“ festhalten.","Der gemeinsame Index bleibt erhalten und wird nur einmal im Hintergrund aktualisiert; die bisherigen Einstellungen, Filter, Sortierungen und lokalen Daten bleiben unverändert.","Der vollständige öffentliche Live-Katalog wird zusätzlich updatefest aus seinem eigenen IndexedDB-Cache wiederhergestellt. Läuft der kurzfristige Katalog-Cache ab oder wird Camclient aktualisiert, fällt die Modellbibliothek nicht mehr auf den kleineren lokalen Beobachtungsbestand zurück.","Die Wiederherstellung schreibt große Kataloge gebündelt und im Hintergrund in den stabilen Statistikbestand. Dadurch bleiben auch Kataloge mit mehr als 40.000 Models erhalten, ohne die erste Bedienung zu blockieren; eine neue Live-Aktualisierung ersetzt veraltete Angaben anschließend.","Jede Modellkarte in der Bibliothek besitzt jetzt immer ein sichtbares Cover: eigenes Cover oder Archivbild, gespeicherte öffentliche Vorschau, aktuelles Thumbnail oder als letzte Stufe ein lokales Camclient-Cover mit Modelnamen. Auch offline und bei blockierten Bildservern bleibt die Karte dadurch vollständig."],"source":"Änderungsprotokoll"},{"version":"20.65.0-custom","date":"2026-09-21","notes":["Camclient erkennt Braves absichtlich deaktivierte File-System-Access-API jetzt als eigenen Browsermodus. Der falsche Hinweis, Brave zu aktualisieren oder eine nicht vorhandene Website-Berechtigung einzuschalten, wurde entfernt.","In Brave bleiben Screenshots und Aufnahmen automatisch in der lokalen Galerie und werden über Tampermonkeys Downloadschnittstelle unter Downloads/Camclient/ abgelegt; die Oberfläche erklärt diese Rückfallebene klar und zeigt keine wiederholte Fehlermeldungsflut mehr.","Der bisher wirkungslose Ordnerknopf wird in Brave zu „Bilderordner in Galerie einlesen“. Damit lässt sich ein vorhandener Ordner auswählen und alle enthaltenen Bilder dauerhaft in die Model-Galerie übernehmen.","Identische Meldungen werden zusammengefasst, statt sich mehrfach übereinander zu stapeln. Chrome und Edge behalten weiterhin die direkte, dauerhaft verknüpfte Ordnerspeicherung pro Model."],"source":"Änderungsprotokoll"},{"version":"20.64.0-custom","date":"2026-09-21","notes":["Die SingleCam-Ordnerauswahl erreicht Braves File-System-Dialog jetzt auch aus dem isolierten Tampermonkey-Kontext; nicht unterstützte oder blockierte Zugriffe werden sichtbar erklärt statt still verschluckt.","Die SingleCam-Medienansicht ist jetzt eine vollständige Galerie mit großem Viewer, Vor/Zurück-Navigation, Tastatursteuerung, Filmstreifen, Mausrad- und Schaltflächen-Zoom sowie verschiebbarem Zoomausschnitt.","Jedes Bild der Galerie – einschließlich Dateien aus dem verknüpften Model-Ordner und öffentlicher Website-Bilder – kann ausdrücklich als dauerhaftes Model-Cover gewählt werden. Die Auswahl wird lokal gespeichert und sofort an Avatar, Galerie und Bibliothek sichtbar.","Verknüpfte Model-Ordner werden nicht mehr nach 120 Mediendateien abgeschnitten; alle vorhandenen Bilder und Videos bleiben in der Galerie erreichbar."],"source":"Änderungsprotokoll"},{"version":"20.63.0-custom","date":"2026-09-21","notes":["Statistikzentrum, Modellbibliothek und „Mein Nutzer“ teilen jetzt genau eine vollständige IndexedDB-Initialisierung. Auch bei 32.000 gespeicherten Models entstehen keine parallelen Ladevorgänge oder dauerhaft zwischengespeicherten Teilbestände mehr.","Der Live-Katalog lädt auch dann vollständig bis zum letzten Datenblock, wenn die öffentliche Schnittstelle vorübergehend keine Gesamtzahl mitsendet. Abgebrochene Vorabrufe werden mit begrenztem, netzwerkschonendem Abstand automatisch fortgesetzt.","Ein erfolgreicher Katalogserver wird weiterverwendet; gedrosselte oder fehlerhafte Server erhalten eine kurze Abkühlzeit und werden ohne doppelte Warteanfrage übersprungen. Die Zielgröße eines laufenden Katalogdurchgangs bleibt stabil, auch wenn sich die öffentliche Live-Gesamtzahl währenddessen ändert.","Wechsel zum nächsten oder vorherigen Model zeichnen eine bereits geöffnete SingleCam sofort mit der neuen Kamera neu.","MultiCam-Kamera- und Werkzeugmenüs liegen zuverlässig über der einheitlichen Kopfzeile und bleiben anklickbar.","Der transparente Tag-Emoji-Sprite wurde für seine tatsächliche UI-Größe optimiert. Dadurch ist das fertige Userscript rund 1 MB kleiner, bei unverändertem Erscheinungsbild der kleinen Tag-Symbole.","Die isolierte updatefeste Sicherung blockiert den ersten sichtbaren Frame nicht mehr. Eine leere Neuinstallation startet sofort und übernimmt eine gefundene Sicherung anschließend kontrolliert.","Neue umfassende Brave-Leistungsmatrix für sechs Hauptseiten, zwölf Einstellungsseiten, sieben SingleCam-Bereiche, MultiCam und große lokale Datenbestände.","Der Sprach-Browsertest folgt jetzt ausschließlich der aktuellen eigenen Camclient-Oberfläche und prüft dort alle zwölf Sprachen sowie sämtliche Haupt- und Einstellungsseiten."],"source":"Änderungsprotokoll"},{"version":"20.62.0-custom","date":"2026-09-21","notes":["Neues updatesicheres Hintergrundstudio unter Einstellungen > Design: globale Hintergründe sowie getrennte Hintergründe für Start, Entdecken, Tag-Explorer, Statistikzentrum, Modellbibliothek, Mein Nutzer, Einstellungen, MultiCam, SingleCam und die ursprünglichen Website-Seiten.","Hintergründe unterstützen Farbe, Verlauf oder lokale PNG/JPEG/WebP-Bilder mit Füllmodus, Strecken, Originalgröße, Kachelung, Position, Zoom, Deckkraft, Abdunklung, Unschärfe, Panel-Transparenz und fixiertem Scrollverhalten.","SingleCam kann für jedes Model einen eigenen Hintergrund speichern. Eigene Galerie-Bilder lassen sich direkt als Hintergrund setzen; die Reihenfolge ist Model → Seite → global.","Hintergrundbilder und Zuordnungen werden lokal im bestehenden Medienarchiv beziehungsweise in den updatefesten Einstellungen gespeichert."],"source":"Änderungsprotokoll"},{"version":"20.61.0-custom","date":"2026-09-21","notes":["Die einheitliche Camclient-Kopfzeile mit Start, Entdecken, Tag-Explorer, Statistikzentrum, Modellbibliothek, Mein Nutzer und Einstellungen bleibt jetzt auch in SingleCam vollständig sichtbar.","MultiCam besitzt dieselbe dauerhafte Hauptnavigation einschließlich Marke, Version, Seitenmeldungen, Suche, Aktualisieren und hervorgehobenem MultiCam-Status.","SingleCam-Kopfzeile, Modelleiste und rechter Informationsbereich messen die tatsächliche Kopfhöhe, damit beim Scrollen nichts überlappt. Auch Kino-, Fokus-, Viewer- und eingeklappte Werkzeugansichten behalten die Hauptnavigation."],"source":"Änderungsprotokoll"},{"version":"20.60.0-custom","date":"2026-09-21","notes":["Die SingleCam-Aufnahme startet jetzt unmittelbar beim Klick auf „Aufnahme starten“; die wiederholte Browser-Bestätigungsabfrage wurde entfernt.","Der zweite Klick stoppt und speichert weiterhin wie bisher. Die getrennte Schutzmeldung beim Verlassen einer Seite mit laufender Aufnahme bleibt erhalten."],"source":"Änderungsprotokoll"},{"version":"20.59.0-custom","date":"2026-09-21","notes":["SingleCam kann einen lokalen Hauptordner über Braves Ordnerauswahl dauerhaft verknüpfen und legt darin automatisch pro Model einen Unterordner an.","Screenshots und segmentierte Videoaufnahmen werden bei erteilter Ordnerfreigabe ohne wiederholten Speicherdialog direkt auf dem PC gespeichert; ohne Freigabe bleibt der bisherige Browser-Download als sichere Rückfallebene erhalten.","Die rechte SingleCam-Galerie zeigt neue Screenshots und Videos sofort an, öffnet sich nach einer Aufnahme automatisch und spielt lokale Videos direkt im UI ab.","Die SingleCam-Einstellungen enthalten Ordnerstatus, Auswahl, erneute Brave-Freigabe und das gefahrlose Entfernen der Verknüpfung, ohne vorhandene Dateien zu löschen."],"source":"Änderungsprotokoll"},{"version":"20.58.0-custom","date":"2026-09-21","notes":["Jede MultiCam-Kachel besitzt jetzt eine kleine, dauerhaft sichtbare Live-Audiopegelanzeige nach derselben Messmethode wie SingleCam.","Der Pegel wird direkt am von Brave bereitgestellten Stream-Audiotrack gemessen und läuft deshalb auch weiter, wenn die jeweilige Kamera oder die Audioausgabe stummgeschaltet ist.","Audio-Tracks, die erst nach dem HLS-Start erscheinen, werden begrenzt erneut erkannt. Ohne freigegebenen Track zeigt Camclient ehrlich „nicht verfügbar“, statt die eingestellte Lautstärke als vermeintliches Tonsignal darzustellen.","Die gemeinsame Messschleife aller Kameras läuft leistungsarm mit ungefähr 12 Aktualisierungen pro Sekunde und wird bei ausgeblendeter Seite deutlich reduziert.","Pegel, Prozentwert, Stummstatus und Messquelle sind auch für Bedienhilfen ausgezeichnet; alle Analyzer, Timer und Track-Listener werden beim Entfernen einer Kamera sauber beendet."],"source":"Änderungsprotokoll"},{"version":"20.57.0-custom","date":"2026-09-21","notes":["Der vollständige Live-Katalog wird kompakt im aktuellen Brave-Tab und zusätzlich in IndexedDB zwischengespeichert. Beim Rückweg aus MultiCam oder nach einem Seitenwechsel erscheinen die bereits geladenen Live-Räume sofort, während veraltete Daten unauffällig im Hintergrund erneuert werden.","Startseite, Entdecken und Tag-Explorer behalten ihre fertige Oberfläche samt Scrollposition und Karten im Speicher. Reine Menüwechsel bauen dieselbe Seite nicht mehr erneut auf und starten die Vorschauen nur für die wieder sichtbaren Karten.","Die feste Zwei-Sekunden-Pause nach jedem 100er-Block wurde durch eine kurze, adaptive Pause ersetzt. Fehler und HTTP-Drosselung besitzen weiterhin eine eigene längere Rückfallzeit, der normale vollständige Katalog lädt dadurch aber wesentlich schneller.","Eine manuelle oder zeitgesteuerte Aktualisierung leert den vorhandenen Katalog nicht mehr. Alte Treffer bleiben bedienbar, bis der neue vollständige Stand vorliegt; erst danach werden nicht mehr aktive Räume entfernt.","Statistikzentrum, Modellbibliothek und „Mein Nutzer“ lassen die letzte fertige Ansicht während des Wechsels sichtbar, statt sie immer wieder durch „Daten werden vorbereitet …“ zu ersetzen."],"source":"Änderungsprotokoll"},{"version":"20.56.0-custom","date":"2026-09-21","notes":["„Entdecken“ ist jetzt eine eigenständige Empfehlungsseite statt einer zweiten Kopie der Startseite. Horizontale Entdeckungsbereiche zeigen „Für dich“, „Neu entdeckt“, „Gerade beliebt“, „Beliebt & frisch gestartet“, „Noch nie besucht“ und „Wieder da“.","„Für dich“ wertet ausschließlich lokal gespeicherte Favoriten, eigene und öffentliche Tags sowie lokale Sehzeit und Sitzungen aus. An jeder persönlichen Empfehlung steht sichtbar, warum sie vorgeschlagen wurde; ohne genügend Daten wird sie ehrlich als Startempfehlung anhand aktueller Beliebtheit gekennzeichnet.","Empfehlungen berücksichtigen die bereits vorhandenen Such- und Filtereinstellungen. Direkt darüber stehen Schnellaktionen für alle Vorschläge, nur neue Models, für den Nutzer neue Models und einen neu gemischten Vorschlagssatz bereit.","Der vollständige Live-Katalog mit Suche, booleschen Tags, Filtern, Sortierung, Mehrfachauswahl und Seitenwechsel bleibt darunter als eigener Bereich „Alle Live-Models“ erhalten.","Jede Empfehlungsreihe bevorzugt bisher nicht gezeigte Models, bleibt aber auch bei einem kleinen oder stark gefilterten Katalog sinnvoll gefüllt. Die Reihen sind horizontal durchblätterbar und laden Live-Vorschauen weiterhin nur für tatsächlich sichtbare Karten."],"source":"Änderungsprotokoll"},{"version":"20.55.0-custom","date":"2026-09-20","notes":["Statistikzentrum, Modellbibliothek und „Mein Nutzer“ verwenden nun gemeinsam genau einen aufgewärmten Datenindex, statt ihn bei jedem eingebetteten Seitenwechsel vollständig zu verwerfen.","Ein noch laufender Indexaufbau wird nicht mehr durch spätere Navigation überschrieben oder parallel mehrfach neu gestartet. Dadurch kann „Daten werden vorbereitet …“ nicht mehr durch schnelle oder wiederholte Menüwechsel dauerhaft hängen bleiben.","Änderungen an lokalen Daten halten den letzten nutzbaren Stand sichtbar und erneuern den Index zeitversetzt im Hintergrund. Die drei Seiten bleiben währenddessen bedienbar.","Die Cache-Promise wird nur noch von ihrem eigenen abgeschlossenen Aufbau freigegeben; ältere Aufrufe können einen neueren Ladeprozess nicht mehr versehentlich aus dem Zustand entfernen."],"source":"Änderungsprotokoll"},{"version":"20.54.0-custom","date":"2026-09-20","notes":["Startseite und Entdecken zeigen aktive Filter jetzt dauerhaft direkt unter den Hauptfiltern an, ohne dass die erweiterten Optionen geöffnet werden müssen.","Tags werden einzeln und eindeutig als UND, ODER oder NICHT angezeigt; auch Suche, Länder/Orte, Typ, Gruppe, Schnellfilter und Zahlenbereiche sind sofort sichtbar.","Jeder aktive Filter kann direkt über × entfernt oder gemeinsam über „Alle zurücksetzen“ gelöscht werden.","Die erhöhte Versionsnummer sorgt dafür, dass Tampermonkey die Datei gegenüber 20.52/20.53 als Update erkennt, sofern Name und Namespace der bestehenden Installation übereinstimmen."],"source":"Änderungsprotokoll"},{"version":"20.53.0-custom","date":"2026-09-20","notes":["Screenshots und lokale Videoaufnahmen sind in SingleCam und MultiCam aktiviert. Die Aufnahme verwendet ausschließlich Media-Tracks, die Brave ausdrücklich über captureStream bereitstellt; Browser-, CORS-, Berechtigungs- oder Schutzmechanismen werden nicht umgangen.","SingleCam besitzt eine dauerhaft sichtbare Aufnahmeleiste mit Screenshot, Start/Stop, PNG/JPEG/WebP, Originalauflösung bis 4K, Videobitraten von 2 bis 20 Mbit/s sowie automatischen Aufnahmeabschnitten von 1 bis 60 Minuten.","SingleCam-Screenshots werden als Datei heruntergeladen und können gleichzeitig in der lokalen Modellgalerie gespeichert oder direkt als eigenes Modell-Cover gesetzt werden. Format, Auflösung und Galerieoptionen bleiben nach Updates erhalten.","SingleCam-Aufnahmen enthalten die von Brave bereitgestellten Video- und Audiotracks, zeigen Laufzeit, Bitrate und Segmentnummer an und speichern jeden Abschnitt lokal als MP4, sofern unterstützt, sonst als WebM.","MultiCam zeigt wieder das vollständige Aufnahmemenü, Aufnahmeeinstellungen, Aufnahmeverwaltung und die Screenshot-/Aufnahmeaktionen jeder Kamera. MultiCam-Screenshots werden zusätzlich in der lokalen Modellgalerie archiviert.","Aktive Aufnahmen werden beim internen Seitenwechsel sauber beendet und gespeichert; vor einem echten Verlassen der Seite kann weiterhin gewarnt werden."],"source":"Änderungsprotokoll"},{"version":"20.52.0-custom","date":"2026-09-20","notes":["Statistikzentrum, Modellbibliothek und „Mein Nutzer“ sind jetzt vollständig in die neue Camclient-Oberfläche eingebettet. Sie verwenden denselben Kopfbereich und dieselbe URL-Struktur wie Start, Entdecken und Tag-Explorer; eine normale Chaturbate-Seite wird dafür nicht mehr geöffnet.","Die eingebetteten Seiten behalten sämtliche vorhandenen Statistiken, Filter, Sortierungen, Galerien, Gruppen, Notizen und Kontoauswertungen, besitzen aber keine zweite Hub-Kopfzeile, kein Schließen-Fenster und keinen Overlay-Modus mehr.","Die Camclient-Hauptnavigation besteht fest aus sieben Einträgen: Start, Entdecken, Tag-Explorer, Statistikzentrum, Modellbibliothek, Mein Nutzer und Einstellungen.","Arbeitsbereiche, Daten, Design, Benachrichtigungen, Datenschutz, Leistung, Diagnose, Hell/Dunkel, Sprache und der Link zur Originalseite wurden aus der oberen Camclient-Leiste entfernt. Alle Konfigurationen befinden sich ausschließlich als Unterseiten im zentralen Einstellungsmenü; die Sprachauswahl wurde dorthin verschoben.","Alte Statistik-/Bibliotheks-/Nutzerlinks von normalen Seiten werden auf die entsprechende neue Camclient-Seite weitergeleitet, statt den bisherigen Standardseiten-Overlay zu öffnen.","Der frühere Schalter und der einmalige Rückfall zur originalen Chaturbate-Startseite wurden entfernt. Die Camclient-Startseite ist dauerhaft aktiv; selbst alte Rückfall-URLs werden automatisch in die eigene Oberfläche überführt."],"source":"Änderungsprotokoll"},{"version":"20.51.0-custom","date":"2026-09-20","notes":["SingleCam zeigt direkt unter den Player-Steuerungen einen echten, farbcodierten Stream-Audiopegel mit Prozentwert, Pegelspitze und den Zuständen fast still, leise, mittel und laut.","Der Pegel wird aus dem Audiotrack des laufenden Videos gelesen und bleibt deshalb auch sichtbar, wenn die hörbare Player-Ausgabe stummgeschaltet ist.","Brave-Autoplay-Sperren werden verständlich behandelt: Falls der Audiokontext noch auf eine Interaktion wartet, erscheint ein eigener „Pegel aktivieren“-Knopf. Fehlende Audiotracks werden ehrlich statt als falscher Nullpegel angezeigt.","Der Pegel arbeitet gedrosselt, wird bei verborgenem Tab weiter reduziert und beim Verlassen von SingleCam einschließlich AudioContext, Timern und Listenern vollständig aufgeräumt.","Die Sicherheitsgrenze für Medien bleibt unverändert: Camclient erstellt keine Screenshots oder Aufnahmen fremder Livestreams. In „Bilder“ können eigene Bilddateien importiert und öffentliche Vorschaubilder aktualisiert werden."],"source":"Änderungsprotokoll"},{"version":"20.50.0-custom","date":"2026-09-20","notes":["Das „Mehr“-Menü bleibt jetzt auch dann zuverlässig geöffnet, wenn es aus einer durch Hover vergrößerten Modellkarte gestartet wird. Die Karte wird beim Öffnen nicht mehr schlagartig verkleinert und löst dadurch keinen unbeabsichtigten pointerleave-Schließvorgang mehr aus.","Der Wechsel vom vergrößerten Livebild in das ausgelagerte Aktionsmenü bleibt flüssig; das Menü schließt weiterhin bewusst beim Verlassen von Karte und Menü, bei Klick außerhalb, Escape, Scrollen oder Neurendern.","„SingleCam“ ist auf Startseite, Entdecken und den übrigen Camclient-Karten nun ein echter, sicherer Link. Ein normaler Linksklick öffnet SingleCam weiterhin innerhalb der aktuellen Camclient-Seite.","Mittelklick mit dem Mausrad, Strg+Klick, Cmd+Klick und Umschalt+Klick verwenden das native Browserverhalten und öffnen genau das gewählte Model in einem neuen Tab beziehungsweise Fenster.","Der Brave-Regressionstest reproduziert ausdrücklich eine vergrößerte Hover-Karte, hält das „Mehr“-Menü über die bisherige Fehlerzeit hinaus offen und prüft einen echten Mittelklick samt Modell-ID im neu geöffneten SingleCam-Tab."],"source":"Änderungsprotokoll"},{"version":"20.49.0-custom","date":"2026-09-20","notes":["Die eigenständigen Camclient-Seiten besitzen nun eine vollständige zweite Navigationszeile. Start, Entdecken, Tag-Explorer, Statistiken, Modellbibliothek, Mein Nutzer, Gruppen, Arbeitsbereiche, Daten, Benachrichtigungen, Datenschutz, Leistung, Design, Diagnose und Einstellungen sind direkt in der Kopfzeile erreichbar.","„Statistiken“, „Modellbibliothek“ und „Mein Nutzer“ öffnen die großen spezialisierten Camclient-Seiten statt des kleineren Verwaltungsbereichs. Beim Wechsel wird der einmalige Originalseiten-Rückfall korrekt gesetzt, damit die gewünschte Seite nicht von der eigenen Startseite zurückgeleitet wird.","„Einstellungen“ öffnet die vollständige Zentrale mit allen zwölf Einstellungsseiten; wichtige Bereiche wie Gruppen, Benachrichtigungen, Datenschutz, Leistung, Design und Diagnose haben zusätzlich einen direkten Einstieg.","Die neue Leiste nutzt auf großen Bildschirmen die volle Breite und wird bei geringer Fensterbreite horizontal scrollbar. Dadurch bleiben Logo, lokales Meldungsfeld und die rechten Schnellaktionen weiterhin bedienbar, ohne dass die Navigation mehrere hohe Zeilen bildet.","Bestehende Standard-Navigationen werden einmalig auf die vollständige Reihenfolge erweitert. Individuell angepasste Reihenfolgen und ausgeblendete Einträge bleiben erhalten; neue Bereiche werden lediglich ergänzt und lassen sich weiterhin im Designbereich sortieren oder ausblenden.","Der Brave-Regressionstest prüft alle 15 sichtbaren Hauptzugänge, sämtliche zwölf Einstellungsseiten und den korrekten Übergang zur großen Modellbibliothek."],"source":"Änderungsprotokoll"},{"version":"20.48.0-custom","date":"2026-09-20","notes":["Die Filter im Startseiten-Einstellungsmenü steuern nun unmittelbar auch den eigenständigen Camclient-Live-Katalog. Ausgewählte Tags, Geschlecht, Länder, Alters- und Zuschauergrenzen, Schnellfilter, Sortierung und Seitendarstellung landen nicht mehr nur im alten nativen Startseitenzustand.","Eine vorhandene Auswahl wie milfs wird beim Öffnen beziehungsweise Aktualisieren der eigenen Startseite übernommen, dauerhaft gespeichert und setzt die sichtbare Ergebnisliste auf Seite 1 zurück.","Das „Mehr“-Menü einer Modellkarte wird beim Öffnen auf die oberste Seitenebene gesetzt und bleibt dadurch auch bei leistungsoptimierten Karten vollständig innerhalb des Brave-Fensters.","Das Menü schließt sich nach dem Verlassen von Karte und Menü automatisch. Es schließt außerdem bei Klick außerhalb, Escape, Scrollen, Größenänderung, Auswahl einer Aktion oder einem Neurendern der Modellliste; zurückbleibende Ereignisbeobachter werden dabei entfernt.","Ein eigener Brave-Regressionstest prüft den vollständigen Weg von der Tag-Auswahl bis zur gefilterten Modellliste und das Öffnen, Positionieren sowie automatische Schließen des „Mehr“-Menüs."],"source":"Änderungsprotokoll"},{"version":"20.47.0-custom","date":"2026-09-20","notes":["Das Logo-Studio besitzt getrennte Regler für Breite und Höhe sowie die Modi „vollständig einpassen“, „ausfüllen und beschneiden“, „strecken“ und „nicht vergrößern“. Dadurch können breite, hohe und bewusst verzerrte Logos passend in der Camclient-Kopfzeile dargestellt werden.","Horizontale und vertikale Bildposition, Innenabstand, Form und Hintergrund sind einstellbar. Transparente PNG-, WebP- und sichere SVG-Dateien bleiben bei der Einstellung „Transparent“ wirklich ohne farbigen Kasten; JPEG wird ausdrücklich als nicht transparent erklärt.","Logo-Uploads dürfen nun bis zu 1,1 MB groß sein. Alle neuen Logo-Werte werden wie die übrigen Einstellungen dauerhaft und updatefest gespeichert, in der Live-Vorschau gezeigt und auch beim optionalen Ersetzen des Original-Headers angewendet.","Der Tag-Explorer zeigt ein neues, transparentes Camclient-Beispielset für Musik, Gaming, Neu, Welt, Tanz und Favoriten. Häufige passende Tags erhalten automatisch ein Bild.","Jeder einzelne Tag hat einen Bild-Knopf. Darüber lassen sich ein Beispiel auswählen, ein eigenes PNG/WebP/JPEG/SVG lokal hochladen, die automatische Zuordnung wiederherstellen oder das Bild vollständig ausblenden.","Eigene Tag-Bilder werden lokal auf 144 × 144 Pixel verkleinert, sicher geprüft und zusammen mit den übrigen Tag-Einstellungen gespeichert. Der vollständige Tag-Explorer erklärt jetzt direkt, wie Auswahl, Kombinationen und Tag-Bilder verwendet werden."],"source":"Änderungsprotokoll"},{"version":"20.46.0-custom","date":"2026-09-20","notes":["Der in echtem Brave sichtbare Fehler „Ungültige Katalogantwort“ ist behoben: Camclient wertet Tampermonkeys Netzwerkantwort jetzt unabhängig davon aus, ob sie als Text, bereits geparstes Objekt oder verschachtelte Nutzlast geliefert wird.","Neben der modernen Tampermonkey-Schnittstelle wird die klassische GM_xmlhttpRequest-Schnittstelle unterstützt. Der JSON-Text wird bewusst selbst geparst, damit Brave-/Tampermonkey-Versionen keine Blob- oder Wrapper-Antwort mehr fälschlich als ungültigen Katalog behandeln.","Wenn ein Host durch Brave Shields oder einen Inhaltsfilter gestört wird, probiert Camclient nacheinander die offiziellen Haupt-, deutschen und englischen Chaturbate-Hosts. Erst wenn alle Varianten scheitern, erscheint die lokale Bibliothek als klar gekennzeichneter Rückfall.","Die künstliche Grenze von 12.000 Katalog- und Bibliothekseinträgen wurde entfernt. Der vollständige öffentliche Live-Katalog wird bis zur von Chaturbate gemeldeten Gesamtzahl beziehungsweise bis zur ersten leeren oder verkürzten Seite geladen.","Die Seitengröße steuert weiterhin nur, wie viele Karten gleichzeitig gerendert werden. Während der automatische Abruf läuft, wachsen Gesamtzahl und Seitenzahl fortlaufend bis zum vollständigen aktuell gelieferten Live-Bestand.","Die erste Seite und ihre aktuellen Vorschaubilder erscheinen sofort. Weitere 100er-Seiten werden bewusst serverfreundlich im Hintergrund ergänzt; die offiziellen Sprachhosts werden wechselnd verwendet und HTTP-429-Drosselungen mit Abstand wiederholt, statt den Katalog unvollständig abzubrechen.","Bildquellen der alternativen offiziellen Antwortform werden ebenfalls übernommen. Platzhalter aus dem lokalen Rückfall verschwinden damit, sobald der öffentliche Katalog erfolgreich geladen wurde.","Der Fortschritt unterscheidet jetzt ausdrücklich zwischen „Katalog … / …“ und „… lokale Modelle“, damit ein Rückfall nicht mehr wie ein erfolgreich geladener Gesamtkatalog aussieht."],"source":"Änderungsprotokoll"},{"version":"20.45.0-custom","date":"2026-09-20","notes":["Der eigenständige Camclient-Live-Katalog verwendet in Tampermonkey jetzt einen internen, Brave-resistenten Netzwerkabruf. Dadurch führt ein von Brave beziehungsweise einem Inhaltsblocker gesperrter Seitenabruf nicht mehr zum irreführenden Rückfall auf die kleine lokale Bibliothek.","Startseite und Entdecken laden den vollständigen aktuell von Chaturbate gemeldeten öffentlichen Live-Bestand automatisch in 100er-Blöcken bis zum gemeldeten Ende beziehungsweise bis zur nachweislich letzten Teilseite. Die Seitengröße begrenzt nur noch die gleichzeitig aufgebauten Karten, nicht mehr den Gesamtkatalog.","Auch Katalogantworten ohne Gesamtzahl werden vollständig weiter paginiert. Leere und verkürzte letzte Blöcke beenden den Abruf sauber; maximal 12.000 Ergebnisse bleiben als Sicherheits- und Leistungsgrenze bestehen.","Ein Wechsel zwischen 12 und 180 Models pro Seite setzt nur die sichtbare Seite zurück und lässt den Hintergrundabruf weiterlaufen. Der sichtbare Katalogfortschritt zeigt geladene und insgesamt gemeldete Räume an.","Netzwerkblöcke erhalten drei begrenzte Versuche. Die bisherige lokale Bibliothek bleibt ausschließlich als verständlicher Rückfall erhalten, wenn der öffentliche Katalog wirklich nicht erreichbar ist.","Brave-Regressionsprüfungen decken automatisches Mehrfach-Paging, Antworten ohne Gesamtzahl, Wechsel der Seitengröße und die Trennung zwischen Seitengröße und Gesamtkatalog ab."],"source":"Änderungsprotokoll"},{"version":"20.44.0-custom","date":"","notes":["Produkt- und Scriptname auf Camclient umgestellt.","Autor in Metadaten und Über-Seite auf Emberhost korrigiert.","Alte fremde Spendenadresse vollständig aus Metadaten und Oberfläche entfernt.","Standardmarke auf Camclient/CC migriert, ohne eigene Logos oder individuelle Markennamen zu überschreiben.","Lizenzbedeutung direkt in der Über-Seite erklärt.","Alte technische Speicherbezeichner bleiben ausschließlich als updatefeste Kompatibilitätsschicht erhalten.","Erforderliche MIT-Hinweise für tatsächlich übernommene Legacy-Anteile sind getrennt von der Camclient-Produkturheberschaft dokumentiert."],"source":"Änderungsprotokoll"},{"version":"20.43.0-custom","date":"2026-09-20","notes":["RoomGrid besitzt jetzt ein eigenes Benachrichtigungsfeld im freien mittleren Bereich der oberen Leiste. Statusänderungen können vollständig innerhalb der Seite erscheinen, ohne eine Desktop-/Browser-Benachrichtigungsberechtigung anzufordern.","Die Ausgabe ist im Benachrichtigungszentrum frei wählbar: nur RoomGrid-Leiste, nur Browser, Leiste und Browser gemeinsam oder keine Einblendung bei weiterhin lokalem Verlauf. Der Hauptschalter deaktiviert die Überwachung vollständig.","Meldungen in der Seite können im freien Kopfzeilenfeld, oben mittig schwebend oder oben rechts schwebend angezeigt werden. Zusätzlich sind kompakte, komfortable und ausführliche Darstellung, 3–30 Sekunden Einblenddauer beziehungsweise manuelles Schließen und ein bis vier gleichzeitig sichtbare Meldungen konfigurierbar.","In-Page-Meldungen zeigen ungelesene Ereignisse, Modelbezug, Uhrzeit und Statussymbol. Ein Klick öffnet das konfigurierte Ziel; das × markiert die Meldung als gelesen, ohne den lokalen Verlauf zu löschen.","Eine Testmeldung im Einstellungsbereich erlaubt die sofortige Kontrolle von Position und Darstellung. Alle neuen Werte liegen im bestehenden versionsunabhängigen Einstellungstresor.","Bestehende Nutzer mit bereits aktivierten Desktop-Benachrichtigungen behalten bei der Migration ihren Browser-Ausgabeweg; neue Konfigurationen verwenden standardmäßig die lokale RoomGrid-Leiste.","Brave-Regressionsprüfungen decken die drei Positionen, die berechtigungsfreie In-Page-Aktivierung, Testmeldung, Lesestatus, Kopfzeilenmontage, Update-Persistenz und alle zwölf Sprachen ab."],"source":"Änderungsprotokoll"},{"version":"20.42.0-custom","date":"2026-09-20","notes":["Ein ausgewähltes eigenes Logo aktiviert jetzt automatisch den Logo-Anzeigemodus. Das Bild erscheint unmittelbar in der Designvorschau und in den RoomGrid-Kopfbereichen; ein vorher ausgewähltes Logo bleibt weiterhin updatefest im versionsunabhängigen Einstellungstresor.","Die drei booleschen Tagfelder der eigenständigen Start-/Entdecken-Seite besitzen nun eine echte Autovervollständigung aus allen bekannten Website-, beobachteten und eigenen Tags. Treffer werden bereits ab dem ersten Zeichen angezeigt und lassen sich per Maus oder Tastatur übernehmen.","Beim Tippen bleibt der Bereich „Erweiterte Filter und boolesche Tags“ geöffnet. Wert, Cursorposition und Tastaturfokus überleben die verzögerte Ergebnisaktualisierung, sodass auch längere Begriffe ohne erneutes Öffnen geschrieben werden können.","Der aufgeklappte Zustand der erweiterten Filter wird dauerhaft gespeichert.","Die eigenständigen RoomGrid-Karten beachten jetzt sämtliche Schalter aus „Was pro Model angezeigt wird“: Land/Region, Ort, Zuschauerzahl, Alter, Live-Status, Tags, Raumthema, Online-Dauer, lokale Sehzeit/Sitzungen sowie HD-/Neu-/Gaming-/Favoriten-Abzeichen lassen sich wirklich einzeln ein- und ausblenden.","Der vollständige Brave-Test prüft Logo-Upload und automatischen Moduswechsel, mehrbuchstabige Tag-Eingabe mit Fokus-Erhalt, Vorschlagsauswahl mit echtem Filterergebnis sowie die sichtbare Wirkung der Karten-Detailoptionen."],"source":"Änderungsprotokoll"},{"version":"20.41.0-custom","date":"2026-09-20","notes":["Die eigenständige RoomGrid-Startseite nutzt auf breiten und ultrabreiten Monitoren nun die vollständige verfügbare Fensterbreite statt der bisherigen festen Inhaltsgrenze von ungefähr 1.880 Pixeln.","Layout- und Leistungsprofile zeigen jetzt immer den tatsächlich aktiven Zustand als vollständig gefüllte Akzent-Schaltfläche mit Häkchen. Der zuvor fest markierte Standardzustand bleibt nach einem Wechsel nicht mehr fälschlich hervorgehoben.","„Neu online“, „Zuletzt besucht“, „Häufig besucht“ und „Noch nie besucht“ besitzen direkt über dem Live-Katalog eigene dauerhafte Sichtbarkeitsschalter. Alle vier Bereiche werden bei der einmaligen Aktualisierung auf diese Version standardmäßig ausgeblendet und lassen sich unabhängig wieder einschalten.","Der öffentliche Live-Katalog wird mit der maximal erlaubten API-Blockgröße von 100 Räumen geladen und anschließend automatisch vollständig im Hintergrund ergänzt. Die Oberfläche aktualisiert sich nur nach mehreren Blöcken, damit tausende Ergebnisse nicht zu einer Neurender-Schleife führen.","Fehlgeschlagene Katalogabfragen erhalten einen begrenzten zweiten Versuch und ein längeres Zeitfenster. Die lokale Bibliothek bleibt nur der Rückfall, wenn beide öffentlichen Abrufe scheitern.","Die Startseite unterstützt nun zusätzlich 120 oder 180 Models pro sichtbarer Seite; die eigentliche Katalogmenge und die Zahl der Ergebnisseiten wachsen unabhängig davon bis zum vollständigen gemeldeten Live-Bestand.","Der Brave-Gesamttest prüft die vier standardmäßig verborgenen Bereiche, die echten aktiven Auswahlzustände, die volle 2.560-Pixel-Breite und den weiterhin flüssigen vollständigen 602-Model-Testkatalog."],"source":"Änderungsprotokoll"},{"version":"20.40.0-custom","date":"2026-09-20","notes":["Die normale Root-Adresse / öffnet nun standardmäßig eine vollständig eigenständige RoomGrid-Startseite. Die originale Chaturbate-Startseite bleibt über „Originalseite“ beziehungsweise ?roomgrid_native_home=1 einmalig erreichbar; die Umschaltung kann im Startseiten-Studio dauerhaft deaktiviert werden.","Die neue Startseite verwendet dieselbe schnelle Vollseiten-Infrastruktur wie Entdecken und SingleCam statt die native Kartenoberfläche nachträglich umzubauen. Dadurch stehen Navigation, Filter und erste Katalogkarten unabhängig von der großen lokalen Statistik-Historie früh zur Verfügung.","Der vollständige Live-Bereich übernimmt Suche, Schnellfilter, Gruppen, lokale Auswahl, zwei Sortierstufen, boolesche Tagfilter, Land/Region, Alter, Zuschauer, Onlinedauer, Sehzeit, Sitzungen, gespeicherte Ansichten, Feeds und Mehrfachaktionen.","Eigene Startseitenoptionen steuern 12–90 Models pro Seite, Auto oder 2–8 Spalten, 70–150 % Kartengröße, vier Bildformate, drei Informationsdichten sowie Statistik- und Widget-Sichtbarkeit.","Vier Layout-Presets und vier Leistungsprofile schalten mit einem Klick zwischen kompakter, ausgewogener, datenreicher oder Kino-Darstellung sowie statischen, sparsamen, ausgewogenen oder flüssigen Live-Vorschauen um.","Nur die aktuelle Ergebnisseite wird als großes Kartenraster aufgebaut. Livebilder bleiben sichtbarkeitsgesteuert, Hover-Video bleibt auf genau eine stumme Karte begrenzt und ausgeblendete persönliche Widgets werden nicht im Hintergrund erzeugt.","Alle neuen Startseiten-, Filter-, Seiten-, Layout- und Leistungswerte werden sanitisiert und im versionsunabhängigen Tampermonkey-Tresor gespiegelt.","Brave-Tests prüfen die Root-Weiterleitung, den ausdrücklichen Originalseiten-Rückfall, die neue Vollseite, Kartenraster, Seitengröße, Spaltenzahl, Persistenz und bestehende native Funktionen."],"source":"Änderungsprotokoll"},{"version":"20.39.0-custom","date":"2026-09-20","notes":["SingleCam besitzt jetzt pro Model eine dauerhafte lokale Bildergalerie. Eigene JPEG-, PNG- und WebP-Dateien können direkt vom Computer importiert und als Originaldatei, sehr hohe 4K-Qualität, hohe 1440p-Qualität oder platzsparende 900p-Version gespeichert werden.","Bis zu 24 selbst importierte Bilder pro Model bleiben getrennt vom begrenzten automatischen Vorschauarchiv erhalten. Aktualisierungen und die automatische Archivbereinigung löschen weder eigene Bilder noch das gewählte Cover.","Jedes Galeriebild kann als eigenes lokales Model-Cover festgelegt werden. Das Cover erscheint in SingleCam, Modellbibliothek und Statistikzentrum; auf Wunsch kann jederzeit wieder das Website-Cover verwendet werden.","Eigene Bilder lassen sich aus der Galerie entfernen. Importqualität, Galeriedaten und Cover-Zuordnung bleiben nach Script-Updates gespeichert.","Der öffentliche Profilimport übernimmt nun neben der Biografie weitere benannte Infobereiche sowie selbst veröffentlichte externe Links. X/Twitter, Instagram, TikTok, YouTube, OnlyFans, Snapchat und weitere Links werden in SingleCam und in den aufklappbaren Profilinformationen von Bibliothek und Statistik angezeigt.","Profil-Links werden weiterhin ausschließlich innerhalb des öffentlichen Profilbereichs gelesen; allgemeine Footer- und Seitennavigation werden nicht als Model-Link übernommen.","Die bewusste Sicherheitsgrenze bleibt bestehen: RoomGrid zeichnet keine fremden Livestreams und keine Videos auf. Offizielle zulässige Aufnahmen bleiben in Chaturbates „My Collection“.","Der Brave-Gesamttest prüft Bildimport, Qualitäts-Persistenz, lokale Cover-Auswahl, IndexedDB-Dauerhaftigkeit, Coverdarstellung in Bibliothek und Statistik sowie zusätzliche Profiltexte und Social-Links."],"source":"Änderungsprotokoll"},{"version":"20.38.0-custom","date":"2026-09-20","notes":["SingleCam besitzt jetzt einen dauerhaft pro Model gespeicherten 100–1.200-%-Bildzoom mit zehn Presets, Plus/Minus, Strg+Mausrad, Ziehen im vergrößerten Bild, horizontaler und vertikaler Position sowie Doppelklick-Rücksetzung. Die bestehende Transformationsspeicherung bleibt updatefest.","Ein neuer, vollständig optionaler Chat-Bereich lädt die offizielle Raumseite erst beim Öffnen. Er kann über Cockpit, Kopfzeile und Einstellungen ein- oder ausgeblendet beziehungsweise neu geladen werden; beim Schließen wird der eingebettete Raum freigegeben.","Öffentliche Tags und eigene Tags werden in der Übersicht getrennt gezeigt. Eigene Tags lassen sich dort direkt mit der vorhandenen Autovervollständigung ergänzen und bleiben mit Bibliothek, Suche und Filtern synchron.","Standard-Raumdaten wie Land, Ort, Alter, Typ, Follower, Following, Onlinezeit, Thema, Zuschauer, HD-, Gaming- und Neu-Status werden nach dem Raumabruf in die Übersicht übernommen und zugleich in den lokalen Statistikbestand eingespeist.","Die Modelstatistik enthält zusätzliche 7-/30-Tage-Werte, aktuelle Serie, Trend-Verlässlichkeit, bekannte Zeitfenster, Wochentagsbalken und die wahrscheinlichsten einzelnen Onlinezeiten. Die Wochenkarte verwendet nun korrekt Sonntag bis Samstag entsprechend der gespeicherten Messindizes.","Ähnliche Models lassen sich direkt in der Kopfzeile aus- und wieder einblenden. Zusätzlich sind Player-Höhe, Cockpitbreite, Bedienelementgröße, Player-Ecken und das Status-Overlay frei wählbar.","Der Brave-Gesamttest prüft jetzt alle sieben SingleCam-Bereiche, den verzögert geladenen Chat, Standard-Raumdaten, Tag-Schnelleingabe, ähnliche Models, 1.200-%-Zoom samt Position und die erweiterte Onlinezeit-Visualisierung."],"source":"Änderungsprotokoll"},{"version":"20.37.0-custom","date":"2026-09-20","notes":["Die bisherige Einzelansicht wurde zu einer eigenständigen „SingleCam Pro“-Seite im Stil des MultiCam-Arbeitsbereichs umgebaut. Die normale Chaturbate-Raumseite ist nicht mehr der primäre Zielpunkt.","Normale Model-Links, Modellbibliothek, Statistikzentrum, Inspector, Planer, Benachrichtigungen und Befehlsuche öffnen jetzt konsistent die RoomGrid-SingleCam. Direkte Aufrufe einer Model-URL werden ebenfalls dorthin weitergeleitet.","SingleCam besitzt ein festes Cockpit mit sechs Bereichen: Übersicht, Notizen & Ordnung, Statistik, Bilder, öffentliches Profil und Einstellungen. Der zuletzt verwendete Bereich bleibt gespeichert.","Private Notizen speichern automatisch und dürfen jetzt bis zu 4.000 Zeichen enthalten. Eigene Tags haben Autovervollständigung, Chips und direktes Entfernen; Bewertung, Priorität, Merkliste und mehrere Gruppen lassen sich auf derselben Seite pflegen.","Die Statistikseite pro Model zeigt Sehzeit, Sitzungen, Durchschnitt, längste Sitzung, aktive Tage, Auftritte, Online-Samples, Follower, letzte Aktivität, typischen Spitzenzeitpunkt und eine lokale Wochen-Zeitkarte.","Eine eigene Bildseite zeigt aktuelle öffentliche Cover, alternative Vorschauen und das updatefeste lokale Archiv öffentlicher Beispielbilder. Eine manuelle Aktualisierung ist direkt möglich; Videos werden nicht aufgezeichnet.","Player, Bildfilter, Qualitätswahl, Ton, PiP, Kino, Vollbild, Verbindungsoptionen und Layout bleiben vollständig erhalten und sind responsiv mit dem neuen Cockpit verbunden.","Beim direkten Start in SingleCam werden weder der vollständige Discover-Katalog noch die große Statistikoberfläche blockierend aufgebaut. Nachgeladene Statistik aktualisiert nur ihren Bereich und startet den Player nicht mehr neu.","Der Brave-Gesamttest prüft die neue Vollseite, alle sechs Bereiche, Notiz-/Tag-Persistenz, Layouts und mobile bis breite Viewports."],"source":"Änderungsprotokoll"},{"version":"20.36.2-custom","date":"2026-09-20","notes":["Statistikzentrum, Modellbibliothek und Nutzerseite verwenden jetzt einen vorgewärmten gemeinsamen Datenindex. Ein Wechsel zwischen diesen Menüs liest den großen lokalen Bestand nicht erneut vollständig ein.","Die Indexerstellung arbeitet nach einem Zeitbudget statt nach starren Mini-Paketen. Dadurch bleiben Eingaben möglich, ohne bei über 30.000 Models hunderte künstliche Wartezyklen zu erzeugen.","Suchtexte, Sitzungs-/Vorschau-Summen, aktuelle Ereigniszahlen und Top-Model werden einmal beim Indexaufbau berechnet und in allen Menüs wiederverwendet.","Modellereignisse, Qualitätswerte und typische Onlinezeiten werden ohne unnötige Zwischenarrays in einem Durchgang ausgewertet. Das senkt Speicherbedarf und Rechenzeit großer Bibliotheken.","Cover-Archivprüfungen, öffentliche Momentaufnahmen und Bildkompression starten erst nach der interaktiven Kartendarstellung, in kleinen Leerlauf-Paketen. Raum-, Detail-, Filter- und Menüschaltflächen sind dadurch sofort verfügbar.","Gleichzeitige automatische und manuelle Bildpaket-Anforderungen werden zusammengeführt. Die Beispielgalerie wartet jetzt zuverlässig auf ein bereits laufendes Cover-/Vorschaupaket, statt einen unvollständigen Zwischenstand anzuzeigen.","Statistikänderungen verwerfen einen bereits nutzbaren Index nicht mehr schlagartig. Die sichtbare Ansicht bleibt bedienbar und erhält im Hintergrund eine frische Momentaufnahme.","Der Brave-Stresstest wartet nun nicht nur auf den Dialog, sondern auf vollständig benutzbare Statistik- und Bibliotheksinhalte mit 32.000 Models und misst zusätzlich den direkten Menüwechsel."],"source":"Änderungsprotokoll"},{"version":"20.36.1-custom","date":"2026-09-20","notes":["„Models pro Seite“ ist jetzt auch bei Tag-, Länder- und kombinierten Startseitenfiltern eine feste Obergrenze. Bei 48 erscheinen niemals automatisch 120, 240 oder noch mehr Karten.","Der vollständige Live-Katalog wird weiterhin vollständig im Hintergrund durchsucht; die Treffer werden anschließend lokal in echte Seiten mit Zurück-/Weiter-Navigation aufgeteilt.","Nur die ausdrückliche Auswahl „Alle geladen“ verwendet weiterhin das begrenzte progressive Nachladen.","Der Brave-Gesamttest prüft nun einen vollständigen Katalog mit 602 Treffern bei Seitengröße 12, den Wechsel auf Seite 2 sowie die strikte Begrenzung der sichtbaren Karten und des DOM-Bestands."],"source":"Änderungsprotokoll"},{"version":"20.36.0-custom","date":"2026-09-20","notes":["Die eigentliche Ursache der langen Unbedienbarkeit auf Startseite und Discover wurde behoben: große lokale Statistikbestände werden nicht mehr mit einem einzigen IndexedDB.getAll() geladen, kopiert und normalisiert.","Modelle, Profile, Ereignisse, Qualitätswerte, Konto-Snapshots und Tageswerte werden jetzt über Cursor progressiv eingelesen. Browser-Eingaben, Navigation und Darstellung erhalten zwischen Datensätzen wieder zuverlässig Rechenzeit.","Discover wartet nicht mehr auf die vollständige lokale Historie. Oberfläche und öffentlicher Live-Katalog starten sofort; lokale Statistikwerte ergänzen die bereits bedienbare Ansicht nachträglich.","Statistikzentrum, Modellbibliothek und Mein Nutzer verwenden für ihre Darstellung referenzbasierte Momentaufnahmen statt vollständiger JSON-Tiefenkopien großer Datenmengen.","Der gemeinsame Index für Statistikzentrum und Modellbibliothek wird in kleinen Arbeitspaketen erzeugt. Kopfzeile, Tabs und Schließen bleiben auch bei zehntausenden Models und sehr vielen Ereignissen bedienbar.","Ein neuer Brave-Stresstest verwendet 32.000 gespeicherte Models. Dabei öffnete das Startseitenmenü nach rund 99 ms, Discover war nach rund 364 ms bereit und der erweiterte Filter reagierte nach rund 37 ms; die größte gemessene Hauptthread-Aufgabe lag bei 326 ms.","Bestehende Start-, Persistenz- und Sprachtests sowie der vollständige statische Funktionscheck wurden erneut erfolgreich ausgeführt."],"source":"Änderungsprotokoll"},{"version":"20.35.0-custom","date":"2026-09-20","notes":["Der Start auf der originalen Homepage blockiert nicht mehr bis zu 1,8 Sekunden auf den isolierten Tampermonkey-Einstellungstresor. Gültige lokale Einstellungen starten sofort; eine eventuell nötige Tresorergänzung läuft begrenzt im Hintergrund. Nur eine tatsächlich leere Installation wartet höchstens 240 ms auf die Wiederherstellung.","Kopfzeile, aktive Versionsanzeige und Startseiten-Schnellleiste werden zuerst aufgebaut. Kartenaktionen und Live-Vorschauen registrieren zunächst die ersten 48 relevanten Karten; der restliche Katalog folgt in freien Browser-Zeitfenstern.","Statistikstart, vollständige Medienerfassung, Metadatenabgleich, Sortierung und Katalogsynchronisierung wurden aus dem kritischen ersten Seitenaufbau entfernt und laufen progressiv nach.","Die großen Übersetzungspakete werden nur noch für die gerade benötigte Sprache erzeugt. Deutsche oder englische Seiten müssen die übrigen Sprachobjekte nicht mehr beim Start vollständig anlegen.","Ein eigener Brave-Starttest simuliert 240 Startseitenkarten und 1,2 Sekunden langsamen Tampermonkey-Speicher. Die kritische Oberfläche erschien dabei statt nach 2,6 bis 3,0 Sekunden bereits nach ungefähr 0,45 Sekunden; Schnellaktionen und Vorschau-Anmeldung waren nach rund 0,46 Sekunden verfügbar.","Der vollständige Brave-Gesamttest, alle zwölf Sprachen, Persistenz, MultiCam, Discover, Bibliothek, Statistikzentrum sowie die großen Startseiten- und Katalogfälle wurden erneut erfolgreich geprüft."],"source":"Änderungsprotokoll"},{"version":"20.34.0-custom","date":"2026-09-20","notes":["Die Modellbibliothek besitzt jetzt ein echtes, dauerhaftes Archiv öffentlicher Cover- und Vorschaubilder statt nur flüchtiger Bildadressen.","Sichtbare Modellkarten sichern automatisch ein komprimiertes Cover, eine kompakte Vorschau und bei Live-Modellen eine öffentliche JPEG-Momentaufnahme.","Pro Modell bleiben bis zu vier zeitlich unterschiedliche Bilder erhalten; ein globales Limit, zwei parallele Abrufe und WebP-Verkleinerung schützen Speicher und Leistung.","Jede Galerie zeigt gespeicherte Bilder mit Datum, das aktuelle öffentliche Livebild und eine Schaltfläche für eine neue Momentaufnahme.","Das Archiv liegt updatefest in einer eigenen IndexedDB. HLS-/Videostreams werden weiterhin weder aufgezeichnet noch als Screenshot ausgelesen.","Der vollständige Brave-Test prüft nun Blob-Speicherung, Cover-, Vorschau- und Momentaufnahme-Datensätze sowie deren sichtbare Galerie."],"source":"Änderungsprotokoll"},{"version":"20.33.1-custom","date":"2026-09-20","notes":["Jede Karte der Modellbibliothek besitzt direkt unter dem Namen einen gut sichtbaren Button „▶ Live-Raum öffnen“; bei Offline-Models heißt er „Raumseite öffnen“.","Der Modelname selbst ist ebenfalls mit der offiziellen Chaturbate-Einzelansicht verlinkt.","Raumlinks öffnen in einem neuen Tab, damit aktive Bibliotheksfilter, Sortierung und Scrollposition erhalten bleiben.","Der Brave-Test klickt den neuen Link tatsächlich an und bestätigt, dass die korrekte Einzelraum-URL geöffnet wird."],"source":"Änderungsprotokoll"},{"version":"20.33.0-custom","date":"2026-09-20","notes":["Das Statistikzentrum bevorzugt auf jeder Modelkarte die zuletzt bekannte öffentliche Vorschauquelle und fällt erst danach auf Chaturbates aktuelle große und kleine Vorschaubilder zurück.","Jede Statistik-Karte besitzt nun eine aufklappbare Galerie mit bis zu drei öffentlichen Beispielquellen einschließlich des aktuellen Livebilds. Offline-Platzhalter werden ehrlich gekennzeichnet; RoomGrid fertigt keine heimlichen Stream-Screenshots an.","Ein neuer direkt sichtbarer Bereich „Erweiterte Statistikfilter“ kombiniert Geschlecht/Typ, mehrere Länder, mehrere öffentliche Tags, mehrere eigene Gruppen, Katalogquelle, Profildaten, Cover-Verfügbarkeit, HD/Gaming, Sehverlauf, eigene Tags, Gruppenzuordnung und private Notizen.","Hinzu kommen Alters-, Zuschauer-, Follower-, Sehzeit-, Sitzungs-, Aktivitäts-, Vorschau-, Schnellinfo-, Bewertungs-, Prioritäts-, Erscheinungs- und Live-Beobachtungsgrenzen.","Die Statistiksortierung wurde von fünf auf 33 Verfahren erweitert, unter anderem auf auf-/absteigende Sehzeit, Aktivität, Sitzungen, Durchschnitts- und längste Sitzung, Vorschauen, Schnellinfos, Entdeckungen, Alter, Follower, Bewertung, Qualität, Cover, Länder, Tags und Gruppen.","Sortierung, Status, „Nur Bibliothek“ und alle neuen Statistikfilter werden updatefest gespeichert.","Der vollständige Brave-Test prüft Coverquellen, Beispielgalerie, sämtliche Sortierwerte, kombinierte Mehrfachfilter und deren dauerhafte Speicherung."],"source":"Änderungsprotokoll"},{"version":"20.32.3-custom","date":"2026-09-20","notes":["Noch nicht geschriebene Einstellungsänderungen werden beim Ausblenden, Tabwechsel oder Schließen sofort in den stabilen Hauptspeicher geschrieben; das bisherige kurze Speicher-Zeitfenster entfällt damit.","Direkt danach wird der versionsunabhängige Tampermonkey-Einstellungstresor aktualisiert. Design, Seitenansicht, Filter, Tags, Gruppen, Bibliothek, Vorschau, MultiCam, Statistik-Dashboard, Sprache und Module bleiben dadurch auch nach Script-Updates erhalten.","Neue zukünftige Einstellungs- und Präferenzmodule werden anhand ihrer stabilen RoomGrid-Schlüssel automatisch in die Update-Sicherung aufgenommen. Temporäre Laufzeitdaten, Diagnosen und Zugangsdaten bleiben ausdrücklich ausgeschlossen.","Übernommene geteilte Arbeitsbereiche werden nun ebenfalls sofort dauerhaft gespeichert.","Ein Brave-Persistenztest ändert unmittelbar vor pagehide eine Einstellung, leert anschließend den gesamten Seitenspeicher und prüft die vollständige Wiederherstellung einschließlich eines bislang unbekannten zukünftigen Präferenzmoduls."],"source":"Änderungsprotokoll"},{"version":"20.32.2-custom","date":"2026-09-20","notes":["Die Tag-Mehrfachauswahl der Startseitenfilter zeigt beim Tippen eine eigene Vorschlagsleiste mit sofortigen Treffern aus allen lokal bekannten Tags.","Zusätzlich wird Chaturbates native Tag-Autovervollständigung angebunden. Dadurch werden auch Website-Tags vorgeschlagen, die noch bei keinem lokal erfassten Model vorkamen; alt liefert beispielsweise alternative.","Vorschläge werden mit Anfangstreffern zuerst sortiert, lassen sich per Mausklick, Pfeiltaste oder Enter übernehmen und werden unmittelbar als updatesicher gespeicherte Filterchips behandelt.","Die native Website-Suche wird nach jeder Abfrage auf ihren vorherigen Wert zurückgesetzt. Bei nicht verfügbarer Website-Komponente bleibt die lokale Vervollständigung vollständig nutzbar.","Der Brave-Oberflächentest prüft die Website-Vervollständigung, Auswahl, Speicherung und Entfernung eines vorgeschlagenen Tags."],"source":"Änderungsprotokoll"},{"version":"20.32.1-custom","date":"2026-09-20","notes":["Die animierten JPEG-Vorschauen auf normalen Seiten und in RoomGrid Entdecken verwenden nun einen FPS-abhängigen Zeitplan statt eines festen 50-ms-Dauertakts. Bei niedrigen Bildraten oder abgeschalteter Vorschau sinkt die Leerlaufarbeit deutlich, während 20 FPS weiterhin mit 50 ms bedient werden.","Große RoomGrid-Kartenlisten überspringen außerhalb des sichtbaren Bereichs Layout und Darstellung, behalten aber eine stabile Platzhaltergröße; dadurch bleiben Modellbibliothek und Entdecken bei langen Seiten flüssiger.","Die kurze Suche nach Chaturbates Tip-Ton-Funktion beendet sich jetzt sofort nach erfolgreicher Einbindung und läuft nicht unnötig bis zum Ende des 30-Sekunden-Fensters weiter.","Das MultiCam-Gesundheitszentrum führt gestartete Kameras auch während eines Qualitätswechsels oder Verbindungsaufbaus als „connecting“ auf, statt die Zeile in diesem kurzen Übergang verschwinden zu lassen.","Der ausschließlich mit Brave laufende Oberflächentest misst jetzt alle drei Leistungsprofile, die Renderzeit aller zwölf Zentralseiten, den vollständigen 602-Model-Katalog, die begrenzte Erweiterung auf 240 Karten, DOM-Wachstum, Speichernutzung, Layoutsprünge und lange Hauptthread-Aufgaben.","Harte Stabilitätsgrenzen lassen den Test bei schweren Blockaden, langsamen Menüwechseln, ungebremstem DOM-Wachstum oder zu langsamen großen Ergebnislisten fehlschlagen."],"source":"Änderungsprotokoll"},{"version":"20.32.0-custom","date":"2026-09-20","notes":["Paket 6 der großen Ausbau-Roadmap umgesetzt: Statistik und Datenschutz wurden zu einem konfigurierbaren, nachvollziehbaren und sicher rücksetzbaren lokalen Datenzentrum ausgebaut.","Das Statistik-Dashboard besitzt vier Presets sowie frei sortierbare Widgets; Sichtbarkeit und Breite jedes Bausteins bleiben updatesicher gespeichert.","Ein Import-Assistent prüft Statistikdateien vor jeder Änderung, zeigt Bereiche, Umfang, Zeitraum und Konflikte und erlaubt eine sichere Zusammenführung oder ein gezieltes Ersetzen ausgewählter Bereiche.","Jede Statistikansicht nennt ihre Datenherkunft. Ein Datenbestand zeigt Anzahl, Zeitraum, ungefähren Speicherbedarf und Sensibilität von Model-, Profil-, Ereignis-, Qualitäts-, Konto- und Tagesdaten.","Verlaufspausen sind für 15 Minuten, eine Stunde, acht Stunden oder unbegrenzt möglich. Private Sitzungen können zeitlich begrenzt werden und unterbinden zusätzlich öffentliche Model-Beobachtungen.","Rohereignisse, Qualitätsproben, Konto-Snapshots und Tageswerte besitzen getrennte Aufbewahrungsfristen. Alte Rohereignisse werden vor dem Löschen zu Tageswerten verdichtet.","Die Statistik-Sicherungszentrale hält bis zu drei interne Wiederherstellungspunkte. Vor Import, Wiederherstellung oder Löschung wird automatisch eine Sicherung angelegt.","Die allgemeine Datenzentrale erstellt vollständige Sicherungen aus Konfiguration und Statistik, ohne geheime Broadcaster-Token in Exporte aufzunehmen.","Laufzeit-, Persistenz-, Sprach- und Oberflächentests wurden um die neuen Datenschutz- und Statistikabläufe erweitert und ausschließlich mit Brave ausgeführt."],"source":"Änderungsprotokoll"},{"version":"20.31.0-custom","date":"2026-09-20","notes":["Paket 5 der großen Ausbau-Roadmap umgesetzt: Das Designzentrum vereint Brand-/Logo-Studio, vollständige Farbvariablen, Layout-Editor, Live-Gerätevorschau, Barrierefreiheitsprüfung und responsive Seitenprofile.","Name, Kürzel, Unterzeile, Version, Logogröße und -form sowie getrennte Logos für helles und dunkles Design sind lokal anpassbar; optional lassen sich damit Seitentitel, Tab-Symbol und das Logo in der Originalkopfzeile gestalten.","Logoimporte sind auf kleine PNG-, JPEG-, WebP- und streng geprüfte SVG-Dateien begrenzt. Aktive Inhalte, externe Referenzen und Ereignisattribute werden nicht übernommen.","Neben Primär- und Sekundärakzent stehen nun elf semantische Farben je Hell-/Dunkelmodus für Seite, Panel, Karten, Eingaben, Rahmen, Text, Hinweise und Zustände bereit.","Der Layout-Editor steuert gemeinsame Kartenbreite, Abstände, Inhaltsrand, Kopfzeilenhöhe, Spaltenzahl, Kartenformat, Seitenleistenposition und fixierte Kopfzeilen; drei Presets dienen als Ausgangspunkt.","Eine eigene Miniaturansicht simuliert Startseite, MultiCam und Datenzentrum in Desktop-, Tablet- und Mobilbreite und folgt allen Änderungen sofort.","Vier live berechnete WCAG-Kontrastprüfungen, sichtbare Fokussteuerung, Mindest-Bedienflächen, hohe Kontraste, Link-Unterstreichung, reduzierte Transparenz, größere Schrift und System-Bewegungseinstellungen wurden ergänzt.","Bis zu 32 Designprofile lassen sich global, je Seitentyp sowie getrennt für Desktop, Tablet und Mobil zuweisen; frei einstellbare Breakpoints wechseln die Profile ohne Neuladen.","Sämtliche neuen Designwerte werden unter dem unveränderten Hauptspeicher und im versionsunabhängigen Tampermonkey-Tresor bewahrt und in allen zwölf Sprachen angeboten."],"source":"Änderungsprotokoll"},{"version":"20.30.0-custom","date":"2026-09-20","notes":["Paket 4 der großen Ausbau-Roadmap umgesetzt: Ein eigenes MultiCam-Kontrollzentrum bündelt Stream-Gesundheit, Qualitätsbudget, Audioverwaltung, Szenen und automatische Ersatzmodels.","Jeder aktive Stream erhält einen nachvollziehbaren Gesundheitswert aus Verbindung, Startzeit, Puffer, Abbrüchen, Neuverbindungen und verworfenen Frames; kritische Zustände erscheinen zusätzlich direkt an der Kamerakarte.","Ein Gesamt-Qualitätsbudget verteilt verfügbare Auflösung auf aktive Kameras, kann das Hauptbild stärker gewichten und respektiert weiterhin jede ausdrücklich gewählte Einzelkamera-Qualität.","Die neue Audioverwaltung bietet manuelle Steuerung, Solo und „Hauptbild folgen“, ohne gespeicherte Stummschaltungen oder Einzellautstärken zu überschreiben.","MultiCam-Szenen speichern Fensterbelegung, Raster/Fokus-Layout und Audio-Routing je Arbeitsbereich und stellen diese Kombination mit einem Klick wieder her.","Ersatzregeln unterstützen nun eine geordnete Liste bevorzugter Models, bevor auf die allgemeine Kandidatengruppe zurückgegriffen wird.","Fehlgeschlagene Hover-Livevorschauen erhalten eine begrenzte Wartezeit mit Rückfallbild statt fortlaufender Nachladeversuche.","Das lange Kameramenü ist in sechs kompakte, aufklappbare Bereiche gegliedert; Einzelqualität, Audio-Solo und Bildanpassungen bleiben direkt pro Kamera verfügbar.","Sämtliche neuen Einstellungen werden unter dem unveränderten Hauptspeicher und im versionsunabhängigen Tampermonkey-Tresor gesichert; alle neuen Oberflächen sind in zwölf Sprachen enthalten."],"source":"Änderungsprotokoll"},{"version":"20.29.0-custom","date":"2026-09-20","notes":["Paket 3 der großen Ausbau-Roadmap umgesetzt: Die bisherige Schnellinfo wurde zu einem großen Modell-Inspector mit den fünf Bereichen Übersicht, Profil, Organisieren, Aktivität und Ähnlich ausgebaut.","Vorheriges und nächstes Model lassen sich im aktuellen Kartenkontext per Schaltfläche oder Pfeiltaste öffnen; Fokusführung, Escape-Schließen und mobile Vollbreite wurden ergänzt.","Notizen, eigene Tags mit Autovervollständigung, Bewertung, Priorität, Favorit, Merkliste, Archiv und sämtliche eigenen Gruppen können gemeinsam bearbeitet und revisionssicher rückgängig gemacht werden.","Öffentliche Profilangaben, lokale Sehaktivität, Wiedergabequalität und beobachtete Onlinezeiten erhalten verständliche Herkunfts- und Frischeangaben; fehlende Werte werden nicht geschätzt.","Ähnliche Models werden lokal aus Typ, Land und gemeinsamen Tags bestimmt und zeigen den jeweiligen Grund für den Vorschlag.","Eine updatefest gespeicherte Vergleichsleiste vergleicht bis zu vier Models seitenübergreifend einschließlich Sehzeit, Sitzungen, Zuschauer, Profilwerten, Tags und Qualitätsdaten.","Kartenaktionen in Statistikzentrum und Modellbibliothek wurden auf „Raum“, „Details“ und ein aufklappbares „Mehr“-Menü verdichtet; Vorschau, Vergleich, Favorit, MultiCam und Merkliste bleiben direkt erreichbar.","Der Modell-Inspector und seine Vergleichsansicht wurden in alle zwölf RoomGrid-Sprachen aufgenommen; der Übersetzungsaufbau berücksichtigt nun ebenfalls sämtliche Texte des Erlebnisdienst-Moduls."],"source":"Änderungsprotokoll"},{"version":"20.28.0-custom","date":"2026-09-20","notes":["Paket 2 der großen Ausbau-Roadmap umgesetzt: ein gemeinsames RoomGrid-Hauptmenü verbindet die wichtigsten Ansichten und Einstellungen in logisch benannten Bereichen.","Das Menü ist über die Versionsanzeige der Originalkopfzeile, die eingebetteten RoomGrid-Seiten und „Alle Werkzeuge“ im Dock erreichbar; es bleibt auch bei schmalen Fenstern vollständig innerhalb des sichtbaren Browserbereichs.","Tastaturbedienung, Fokusführung, Escape-Schließen, Klick außerhalb und verständliche Zustandsmeldungen für das neue Menü ergänzt.","Statistikzentrum und Modellbibliothek zeigen nun den Umfang der vollständigen lokalen Datenbasis, aktuelle Filtertreffer, sichtbaren Seitenbereich sowie Abdeckung von Profilen, Ländern, Alter, Tags und eigener Beobachtung.","Technische Quellcodes durch verständliche Herkunftsangaben ersetzt und Karten um Kennzeichen für persönliche Organisation, öffentliche Daten, lokale Beobachtung und Datenfrische ergänzt; unbekannte Angaben werden ausdrücklich nicht geschätzt.","Ein gemeinsames Aktionszentrum mit revisionssicherem „Rückgängig“ für Profil-, Gruppen-, Workspace-, Layout-, Smart-View-, Automations-, Massen- und Reparaturaktionen ergänzt. Neuere Änderungen werden dabei niemals versehentlich überschrieben.","Endgültige Löschaktionen behalten ihre ausdrückliche Sicherheitsabfrage."],"source":"Änderungsprotokoll"},{"version":"20.27.0-custom","date":"2026-09-20","notes":["Paket 1 der großen Ausbau-Roadmap umgesetzt: zentrale, generationsbasierte Seitenerkennung für klassische Navigation und Chaturbates dynamische Seitenwechsel.","Startseite, native Discover-/Tags-/Following-/Private-Show-Seiten, Sammlung, Nachrichten und Modelseiten werden nun als eigene Seitentypen erkannt; geschützte Systempfade gelten nicht mehr fälschlich als Modelname.","Suite-Module wechseln bei URL-Änderungen kontrolliert, alte Module werden beendet und fehlgeschlagene Starts höchstens zweimal mit wachsendem Abstand wiederholt.","Die Originalkopfzeile wird über zusätzliche aktuelle Header-Anker sowie ausdrückliche Navigations- und Gesundheitsereignisse zuverlässig nachgebunden.","Laufende vollständige Startseiten-Katalogabfragen werden beim Verlassen der Startseite sofort abgebrochen, damit keine alten Ladevorgänge oder Ergebnisse in eine neue Seite hineinlaufen.","Leere native Inhaltsseiten erhalten nach zwei Prüfungen eine sichtbare, nicht blockierende Wiederherstellungshilfe mit erneuter Prüfung, RoomGrid-Discover und manuellem Neuladen.","Diagnose um eine elfteilige Routen-Testmatrix, aktives Modul, Kopfzeilenstatus, Inhaltsstatus, Routen-Generation und die letzten Seitenwechsel erweitert."],"source":"Änderungsprotokoll"},{"version":"20.26.0-custom","date":"2026-09-20","notes":["Vier Benachrichtigungsprofile pro Model und Gruppe ergänzt: Aus, nur Live-Start, personalisiert und alle Ereignisse.","Dynamische intelligente Sammlungen und ein lokales Regel-/Automationszentrum für Tags, Länder, Zuschauer, Favoriten und Gruppen eingebaut.","Statistikzentrum um direkten 7-/30-/90-Tage-Vergleich sowie einen 48-Stunden-Online-Planer aus lokal beobachteten Zeitmustern erweitert.","Designprofile lassen sich nun einzeln importieren/exportieren und getrennt für Startseite, Listen, Modelseiten, MultiCam, Entdecken und Datenzentrum zuweisen.","Chat-Fokus um Filterstärken, Hervorhebungswörter, ausgeblendete Wörter und Wiederholungs-Unterdrückung erweitert, ohne Chatinhalt zu speichern.","Automatischen Leistungsmodus mit Speicher- und Streamschwellen sowie automatischen Datenschutzschirm nach Inaktivität ergänzt.","Quick View zeigt lokal berechnete ähnliche Models; Tastenkürzel erkennen Konflikte vor dem Speichern.","Diagnose um Gesundheitswert und ein sicherndes Reparaturzentrum für Filter, Design, Konfiguration und Vorschauen erweitert."],"source":"Änderungsprotokoll"},{"version":"20.25.0-custom","date":"2026-09-20","notes":["Tiefere Gestaltung für Schriftstil, Hintergrund, Flächendeckkraft, Rundungen, Kartenabstand und Schattenstärke ergänzt.","Bis zu 16 wiederverwendbare, updatefeste Designprofile mit Sofortanwendung und Überschreiben über denselben Profilnamen hinzugefügt.","Dynamische intelligente Startseiten-Ansichten speichern Filter und Sortierung; später passende Live-Models werden automatisch einbezogen.","Die visuellen Variablen einheitlich auf MultiCam, Entdecken, Kontrollzentrum, Modellbibliothek und das aktivierte Design der Originalseiten angewendet.","Brave- und Speichertests für die neue Gestaltungsebene erweitert."],"source":"Änderungsprotokoll"},{"version":"20.24.0-custom","date":"2026-09-20","notes":["Alle zwölf Seiten des Einstellungszentrums werden nun vollständig in Brave durchlaufen und auf Ladeabschluss, sichtbare Dialoggrenzen, benannte Bedienelemente, eindeutige IDs und saubere Tab-Zustände geprüft.","Asynchrone Einstellungsseiten können bei einem lokalen Statistik- oder Speicherfehler nicht mehr dauerhaft auf „Wird geladen …“ hängen bleiben; stattdessen erscheint ein sicherer Fehlerzustand mit Wiederholen- und Diagnose-Aktion.","Sichtbare null-Zeilen im Daten-, Leistungs- und Statistik-Menü entfernt, wenn optionale Hinweise oder Importvorschauen nicht vorhanden sind.","Einstellungszentrum und native Statistik-/Bibliotheks-/Nutzeransichten um eindeutige Tab-/Panel-Beziehungen, aria-busy-Status, automatisch benannte Platzhalterfelder und zuverlässigere Fokusnavigation ergänzt.","Der Sprachtest öffnet jetzt jede Einstellungsseite in allen zwölf Sprachen und erkennt leere Reiter, Platzhalterausgaben sowie übrig gebliebene deutsche Kerntexte.","Den vollständigen Laufzeit-, Speicher-, Übersetzungs-, Startseiten-, Discover-, MultiCam-, Bibliotheks-, Statistik- und Responsive-Test erneut ausschließlich mit Brave ausgeführt."],"source":"Änderungsprotokoll"},{"version":"20.23.0-custom","date":"2026-09-15","notes":["Kritischen Parserfehler im Sprachgenerator behoben, der vollständige Übersetzungen zu Zwei-Buchstaben-Fragmenten kürzen konnte; alle zwölf Sprachpakete anschließend neu validiert.","Dynamische Übersetzungssätze bewahren ihre Werte-Platzhalter; technische Einstellungsnamen und Beispielkennungen werden nicht länger als sichtbare Texte in Sprachpakete aufgenommen.","Modelle, eigene Tags, Gruppen-, Feed- und Profilnamen werden ausdrücklich nicht übersetzt und bleiben bei Sprachwechseln unverfälscht.","Native Statistik-, Bibliotheks- und Nutzeransichten sowie das Einstellungszentrum um korrekte Tab-Zustände, Fokusführung, Escape- und Pfeiltastensteuerung ergänzt.","Versteckte 450-Raum-Grenze in Discover entfernt und auf die gemeinsame sichere Kataloggrenze von 12.000 angehoben; Importgrenzen aller Statistikoberflächen vereinheitlicht.","Gruppen-Popover und Werkzeugmenü bleiben im Tastaturfokus, schließen kontrolliert und geben den Fokus an den auslösenden Knopf zurück.","Neuer automatischer Sprach-Audit erkennt abgeschnittene Werte, fehlende Platzhalter, leere Übersetzungen und lange unübersetzte UI-Texte vor dem Build."],"source":"Änderungsprotokoll"},{"version":"20.22.0-custom","date":"2026-09-15","notes":["Modellbibliothek um einen eigenen umfangreichen Filterbereich mit Mehrfachauswahl für Geschlecht/Typ, Live-Status, Länder/Regionen und öffentliche Tags erweitert.","Kombinierbare Filter für Alter, Zuschauer, Follower, Gesamtsehzeit, aktuelle Onlinedauer, Sitzungen, Bewertung, Priorität, Aktivität, Auftritte und lokale Live-Beobachtungen ergänzt.","Zusätzliche Zustandsfilter für Katalogquelle, Profildaten, HD/Gaming, Sehverlauf, eigene Tags, Gruppen und private Notizen eingebaut.","Typische Onlinezeiten lassen sich nach Wochentag und lokaler Stunde filtern; beobachtete Livezeiten werden bevorzugt und Sehzeiten dienen als vorsichtiger Ersatz.","Sortierung von 6 auf 30 Möglichkeiten erweitert, darunter Alter in beide Richtungen, Zuschauer, Follower, Sitzungen, Aktivität, Länder, Geschlecht, Tags und Gruppen.","Alle neuen Filter, Mehrfachauswahlen, Zahlenwerte, Bibliotheksansicht und Sortierung werden updatefest gespeichert; ein zentraler Rücksetzknopf entfernt alle Bibliotheksfilter."],"source":"Änderungsprotokoll"},{"version":"20.21.0-custom","date":"2026-09-15","notes":["Das MultiCam-Werkzeugmenü richtet seine linke Kante jetzt korrekt am Menüknopf aus und kann nicht mehr links aus dem Browserfenster herausragen.","Breite, Höhe und Position werden gegen den tatsächlich sichtbaren Browserbereich begrenzt – einschließlich schmaler Fenster, Browserzoom und Größenänderungen bei bereits geöffnetem Menü.","Der Menüknopf meldet seinen geöffneten Zustand für Hilfstechnologien; sämtliche neuen Größen- und Ansichts-Listener werden beim Schließen wieder entfernt."],"source":"Änderungsprotokoll"},{"version":"20.20.0-custom","date":"2026-09-14","notes":["Wiederholtes Nachladen der MultiCam-Seitenleisten-Vorschau beseitigt: Statusänderungen anderer Kameras dürfen die aktive Hover-Vorschau nicht mehr zerstören.","HLS-Player pro Hover dedupliziert, 160-ms-Verlassens-Toleranz und einen begrenzten 60-Sekunden-Streamquellen-Cache ergänzt.","Offizielles Vorschaubild bleibt während des Starts sichtbar; die stumme Hover-Vorschau nutzt einen kleinen Puffer und eine ressourcenschonende Qualitätsgrenze von etwa 480p.","Fehlerbehandlung auf genau einen kontrollierten HLS-Wiederherstellungsversuch begrenzt, damit keine Endlosschleife entsteht."],"source":"Änderungsprotokoll"},{"version":"20.19.0-custom","date":"2026-09-14","notes":["Kameramenü auf die sichtbare Fensterfläche begrenzt und eine dauerhaft sichtbare Kopfzeile mit Modellname und Schließen-Knopf ergänzt; beim Öffnen startet es immer ganz oben.","Instabile Audio-Wellen durch eine horizontale Hybridanzeige ersetzt: echter Pegel über captureStream, ansonsten ehrliche Anzeige der tatsächlichen Ausgabelautstärke.","Eigene Qualitätsauswahl je Kamera im Menü klar abgetrennt, sofort nur auf den betroffenen HLS-Player angewendet und vor der globalen adaptiven Grenze priorisiert."],"source":"Änderungsprotokoll"},{"version":"20.18.0-custom","date":"2026-09-14","notes":["Leere Bildbereiche bei über Tag-/Länderfilter ergänzten Startseitenkarten beseitigt.","Offizielles Vorschaubild und festes Seitenverhältnis stehen bereits vor dem ersten Liveframe bereit.","Neu eingefügte Filterkarten werden sofort beim sichtbarkeitsgesteuerten Vorschauplaner registriert und sichtbare Karten beginnen unmittelbar mit animierten JPEG-Livebildern.","Brave-Stresstest prüft für 602 Treffer nun ausdrücklich Bildquellen, Registrierung und animierte Frames."],"source":"Änderungsprotokoll"},{"version":"20.17.0-custom","date":"2026-09-14","notes":["Dauerhafte RoomGrid-Einstellungen werden zusätzlich in einem versionsunabhängigen, isolierten Tampermonkey-Tresor gespiegelt.","Design, Seitenoptionen, Tag-/Länderfilter, eigene Tags/Gruppen, Bibliotheksansicht, Sprache, Vorschau- und Moduloptionen werden vor dem UI-Start wiederhergestellt, falls der lokale Bereich nach einem Update fehlt oder beschädigt ist.","Gültige aktuelle Werte bleiben maßgeblich; die Sicherung überschreibt keine neueren vorhandenen Einstellungen."],"source":"Änderungsprotokoll"},{"version":"20.16.0-custom","date":"2026-09-14","notes":["Startseiten-Tag- und Länderfilter für sehr große Trefferlisten optimiert: lineare Sortierung statt quadratischer Kartensuche.","Vollständiger Live-Katalog auf bis zu 12.000 Models erweitert, 90-Sekunden-Ladefenster und bis zu drei Versuche pro Katalogseite ergänzt.","Eine zusammenhängende Ergebnisseite rendert zunächst 120 Treffer und lädt beim Scrollen oder per Knopf weitere 120 nach; Gesamtzahl und Restmenge bleiben sichtbar.","Endlosschleifenartige Status-DOM-Aktualisierungen und angesammelte Katalog-Ladetimer beseitigt.","Exakter Tag-Abgleich verhindert falsche Teiltreffer.","Brave-Stresstest mit 602 passenden Models ergänzt."],"source":"Änderungsprotokoll"},{"version":"20.15.0-custom","date":"2026-09-14","notes":["Rumänisch, Niederländisch und Türkisch als vollständige RoomGrid-Sprachen in Kopfzeile, Dock, Startseiten-Studio, Discover, MultiCam, Statistikzentrum, Model-Bibliothek, Nutzerbereich, Einstellungen und dynamischen Hinweisen ergänzt.","Der Sprachschalter bietet jetzt zwölf Sprachen und erkennt ro, nl und tr automatisch aus der Browser-Sprache.","Die drei neuen Sprachen bleiben bewusst auf der aktuell geöffneten Chaturbate-Domain: Die rumänische und niederländische Subdomain leiten auf die englische Hauptseite um, eine türkische Subdomain ist nicht verfügbar. So gibt es keine falsche Weiterleitung und lokale Daten bleiben unverändert.","Neue Tampermonkey-Metadaten-Sprachen sowie hochwertige Übersetzungen für dynamische Kernmeldungen ergänzt.","Der Übersetzungsgenerator schützt Domainnamen vor versehentlicher Übersetzung.","Brave-, Laufzeit- und statische Sprachtests auf zwölf Sprachen erweitert."],"source":"Änderungsprotokoll"},{"version":"20.14.0-custom","date":"2026-09-14","notes":["Startseiten-Tag- und Länderfilter laden jetzt alle Seiten des aktuellen Live-Katalogs nach, statt ausschließlich die schon sichtbare native Ergebnisseite zu filtern.","Passende Live-Models werden dedupliziert, auf der originalen Startseite ergänzt und unabhängig von der eingestellten Seitengröße gemeinsam auf Seite 1 angezeigt.","Fortschritts-/Vollständigkeitsanzeige und ein Knopf zum manuellen Neuladen der vollständigen Live-Liste ergänzt; parallele Abfragen, Zeitlimit und Obergrenze verhindern Endlosschleifen."],"source":"Änderungsprotokoll"},{"version":"20.13.0-custom","date":"2026-09-14","notes":["Fehler behoben, durch den Design-Presets und eigene Akzentfarben zwar gespeichert wurden, große Teile der sichtbaren Oberfläche aber bei fest eingebauten Farben blieben.","Gemeinsame, aus Primärfarbe, Sekundärfarbe und Hell/Dunkel abgeleitete Flächenpalette für normale Seiten, Startseitenwerkzeuge, Dock, Zentrale, Modellbibliothek, Statistikzentrum, Discover und MultiCam ergänzt.","Eigene Farben als dauerhaften Designmodus ergänzt und Farbwähler auf sofortige Live-Aktualisierung umgestellt."],"source":"Änderungsprotokoll"},{"version":"20.12.0-custom","date":"2026-09-14","notes":["Freie Texteingaben für Tags und Länder im Startseiten-Studio durch durchsuchbare Mehrfachauswahl-Menüs ersetzt.","Eigene Tags und alle lokal erkannten öffentlichen Website-Tags werden getrennt gruppiert, bleiben dauerhaft auswählbar und erscheinen als einzeln entfernbare Chips.","Vollständige ISO-Länderliste mit lokalisierten Ländernamen ergänzt; aktuell bei Models erkannte Länder stehen für schnellen Zugriff oben.","Mehrere Länder werden als ODER-Filter behandelt. Für mehrere Tags kann zwischen „alle Tags“ und „mindestens ein Tag“ gewählt werden.","Auswahlgrenzen und Bereinigung erweitert, ohne bestehende gespeicherte Textfilter zu verlieren; alle Änderungen wirken sofort und bleiben lokal gespeichert.","Neue Mehrfachauswahl in alle neun Sprachen übernommen und in Brave auf Suche, Auswahl, Chips, Speicherung und tatsächliche Filterwirkung geprüft."],"source":"Änderungsprotokoll"},{"version":"20.11.0-custom","date":"2026-09-14","notes":["Persönliches Interessenprofil im Nutzer-Dashboard ergänzt: Lieblings-Tags, Themenwelten, eigene Tags, Altersgruppen, exaktes Lieblingsalter, Länder, Orte und Modeltypen nach tatsächlicher lokaler Sehzeit.","Wochentags- und Tageszeitstatistik sowie Top-3-Konzentration, Favoriten-Anteil, HD-Anteil, gewichtetes Durchschnittsalter und sichtbare Metadaten-Abdeckung ergänzt.","Vorschauen und Schnellinfos als eigene Stöberinteressen ausgewertet; aktuelle Favoriten getrennt nach Tags, Ländern, Altersgruppen und Modeltypen dargestellt.","Künftige Besuchsereignisse um begrenzte öffentliche Metadaten-Schnappschüsse erweitert, damit historische Auswertungen bei späteren Profiländerungen stabil bleiben.","Sämtliche Interessenwerte bleiben lokal, unterscheiden klar zwischen Sehzeit, Vorschau und Favoriten und raten keine fehlenden Modelangaben."],"source":"Änderungsprotokoll"},{"version":"20.10.0-custom","date":"2026-09-14","notes":["Große Modellbibliothek um einen eigenen Darstellungsbereich mit drei sofort nutzbaren Presets erweitert.","Modelzahl pro Bibliotheksseite unabhängig einstellbar (12–240), automatische oder feste 1–6 Spalten sowie stufenlose Kartenvergrößerung von 70–160 % ergänzt.","28 Informationsbereiche pro Model einzeln ein-/ausblendbar gemacht: Bild, Profil, öffentliche Angaben, lokale Statistik, Onlinezeit-Schätzung, Bildergalerie, Gruppen, Tags, Bewertung, Editor und Aktionen.","Alle Einstellungen werden direkt übernommen und lokal gespeichert; ausgeblendete Bilder hinterlassen keinen leeren Kartenkopf.","Neue Bibliotheksbedienung in alle neun Sprachen übernommen und Brave-Tests für Raster, Regler, Seitenumfang, Detailumschalter und Speicherung ergänzt."],"source":"Änderungsprotokoll"},{"version":"20.9.0-custom","date":"2026-09-14","notes":["Vollständiges Startseiten-Studio für die originale Chaturbate-Startseite ergänzt: Schnellleiste, 17 Sortierungen, kombinierbare Filter und lokale Seitennavigation.","Einstellbare Modelzahl pro Seite (12–90 oder alle), zwei bis acht Karten pro Reihe, Kartengröße, Raster/Liste und fünf Bildformate ergänzt.","Öffentliche Modelinformationen pro Karte frei konfigurierbar gemacht: Herkunft, Ort, Zuschauer, Alter, Status, Tags, Raumthema, Online-Dauer, lokale Sehstatistik und Abzeichen.","Vier Layoutprofile (Ausgewogen, Kompakt, Datenreich, Kino) und drei Informationsdarstellungen (kompakt, ausführlich, Bild-Overlay) ergänzt.","Einen direkten „Startseite“-Eintrag in der originalen Kopfzeile und einen Schnellzugriff im RoomGrid-Dock ergänzt.","Öffentliche Chaturbate-Katalogdaten werden zur Metadaten-Ergänzung lokal und begrenzt verarbeitet; fehlende Angaben werden nicht geschätzt."],"source":"Änderungsprotokoll"},{"version":"20.8.1-custom","date":"2026-09-14","notes":["Zurückspringenden Regler für die Hover-Vergrößerung behoben: Änderungen erfolgen jetzt live, ohne das Einstellungsfenster beim Ziehen neu aufzubauen.","Sichtbare Prozentanzeige an allen Größen- und Deckkraftreglern im Designbereich ergänzt.","Hover-Vergrößerung von 100–135 % auf 100–180 % erweitert und numerisch gespeichert.","Brave-Oberflächentest ergänzt: Regler auf 150 % ziehen, Wert speichern und die tatsächliche 1,5-fache Modelkarten-Vergrößerung prüfen."],"source":"Änderungsprotokoll"},{"version":"20.8.0-custom","date":"2026-09-14","notes":["Französisch, Italienisch, Russisch, Japanisch und Polnisch als vollständige weitere Sprachen ergänzt; RoomGrid unterstützt damit neun direkt auswählbare Sprachen.","Sprachumschalter in Originalkopfzeile, Discover, SingleCam und MultiCam um die fünf Sprachen erweitert und mit den offiziellen Chaturbate-Domains fr, it, ru, ja und pl verbunden.","Sämtliche neuen Sprachpakete lokal in das Userscript eingebaut: über 1.300 reguläre Sätze, rund 1.400 direkte UI-Beschriftungen und zusätzlich dynamische Muster für Zahlen, Namen, Seitenangaben und Statistikmeldungen.","Dynamische Bedienmeldungen, Gruppenfilterbanner, Dock, Modelbibliothek, Statistikzentrum und Nutzerbereich sprachübergreifend vereinheitlicht.","Sichere LocalStorage-/IndexedDB-Übergabe und den ausschließlichen Brave-Oberflächentest auf alle neun Domains erweitert."],"source":"Änderungsprotokoll"},{"version":"20.7.1-custom","date":"2026-09-14","notes":["Deutsche Resttexte im Startseiten-Gruppenfilter für Spanisch und Portugiesisch beseitigt.","Den sichtbaren null-Platzhalter aus dem Gruppenmenü entfernt.","Weitere dynamische Texte in SingleCam, Modelbibliothek, Onlinezeit-Schätzung und Fehlermeldungen vollständig übersetzt.","Den Brave-Sprachtest um Gruppenmenü, aktiven Filterbanner und die Suche nach deutschen Resttexten erweitert."],"source":"Änderungsprotokoll"},{"version":"20.7.0-custom","date":"2026-09-14","notes":["Durchgängiges Vier-Sprachen-System für Deutsch, Englisch, Spanisch und Portugiesisch ergänzt. Der sichtbare Sprachwähler sitzt in der originalen Chaturbate-Kopfzeile, in Entdecken/SingleCam und in der MultiCam-Werkzeugleiste sowie weiterhin im Mehr-Menü.","Spanische und portugiesische Sprachpakete für über 1.200 internationalisierte Sätze ergänzt. Zusätzlich werden über 1.300 noch nicht modernisierte direkte UI-Beschriftungen sprachübergreifend vereinheitlicht, darunter Statistikzentrum, Bibliothek, Nutzerseite, Gruppen, Zentrale, Discover und SingleCam.","Der Wechsel verwendet die offiziellen Sprachdomains von Chaturbate und behält Pfad, Ansicht und Suchparameter bei. Dadurch wird neben RoomGrid auch die eigentliche Website offiziell übersetzt.","Vor einem Domainwechsel werden RoomGrid-Arbeitsbereiche, lokale Einstellungen und die IndexedDB mit Model-/Statistikdaten einmalig in den isolierten Userscript-Speicher gelegt, auf der Zieldomain wiederhergestellt und anschließend aus dem Übergabespeicher gelöscht.","Sprachbezeichnungen bleiben immer in Eigenschreibweise sichtbar. Fachbegriffe wie SingleCam, Model, Stream, Token und Tip wurden mit festen Übersetzungen gegen missverständliche allgemeine Übersetzungen abgesichert.","Einen eigenen Brave-Test ergänzt, der alle vier Domains nacheinander durchläuft, originale Website-Navigation, RoomGrid-Kopfzeile, Statistikzentrum, Discover und MultiCam prüft und die Datenübernahme über jeden Sprachwechsel verifiziert."],"source":"Änderungsprotokoll"},{"version":"20.6.0-custom","date":"2026-09-14","notes":["SingleCam als deutlich größere, ruhigere Einzelansicht im gemeinsamen Chaturbate-Dark-Look neu gegliedert: große Playerfläche, feste Navigationsleiste, kompakte Hauptbedienung, aufklappbare Bildoptimierung und klar getrennte Informationskarten.","Direkt auf der Einzelansicht ein dauerhaft gespeichertes Einstellungszentrum ergänzt. Es steuert Seitenlayout, Position der Informationen, Seitenverhältnis, Bildanpassung, Playerhintergrund, sichtbare Detailbereiche und kompakte Bildwerkzeuge.","Wiedergabe- und Verbindungsoptionen ergänzt: Autoplay, stummer Streamstart, Lautstärke je Model, Maximallautstärke, Pause im Hintergrund, Niedriglatenzmodus, HLS-Puffer, automatische Wiederverbindung, Statusintervall und manuelles Neuverbinden.","Einen harten Schalter „Chat-, Token- und Tip-Töne vollständig aus“ ergänzt. Er blockiert klassische Audioelemente und Web-Audio-Samples, erfasst auch später eingefügte Töne und gilt gespeichert ebenfalls auf normalen Chaturbate-Seiten; der eigentliche SingleCam-Videoton bleibt separat bedienbar.","Wechsel zum nächsten oder vorherigen Model übernimmt die SingleCam-Einstellungen ohne Playerreste. Fokus-, Links-/Rechts- und Unterhalb-Layouts sowie die neue Einzelansicht wurden in Brave automatisiert geprüft."],"source":"Änderungsprotokoll"},{"version":"20.5.0-custom","date":"2026-09-14","notes":["Die große Modellbibliothek von der bisherigen Liste manuell organisierter Räume auf den vollständigen, dauerhaft in IndexedDB gespeicherten Modelkatalog umgestellt. Jedes auf normalen Listen-, Such-, Tag- und RoomGrid-Entdecken-Seiten auftauchende Model erweitert die Bibliothek automatisch.","Auch der bewusst leichte native /discover/-Modus besitzt nun einen gedrosselten, rein lesenden Katalogscanner. Er erfasst höchstens neue Modelkarten und verzichtet weiterhin auf Vorschau-, Dock- und sonstige schwere Seiteneingriffe.","Pro Model werden zusätzlich erste/letzte Beobachtung, gedrosselte Anzahl der Erscheinungen, Fundquellen, letzte öffentliche Beschreibungen, Tags und sichere Vorschaubild-Verweise dauerhaft und begrenzt gespeichert.","Die Bibliothek zeigt nun automatisch katalogisierte und persönlich organisierte Models gemeinsam, bietet bis zu 240 Modelle pro Seite und weist beide Mengen getrennt aus. Erst beim Bearbeiten von Notiz, Tags, Bewertung, Archiv oder Gruppe wird ein zusätzlicher Organisationseintrag angelegt.","Jede Bibliothekskarte besitzt eine bei Bedarf ladende Galerie mit bis zu drei Vorschaubildquellen einschließlich eines aktuellen Livebildes, ohne hunderte Bilder ungefragt gleichzeitig abzurufen.","Das Statistikzentrum erhielt auf jeder Modelkarte die vollständige Mehrfach-Gruppenzuordnung sowie einen direkten Sprung zum gefilterten Bibliotheksprofil."],"source":"Änderungsprotokoll"},{"version":"20.4.0-custom","date":"2026-09-14","notes":["In der originalen Chaturbate-Kopfzeile den neuen direkten Menüpunkt „Gruppen“ ergänzt. Er zeigt alle selbst angelegten Gruppen samt Anzahl zugeordneter Models, ohne die normale Seite verlassen zu müssen.","Eine oder mehrere Gruppen können unmittelbar aktiviert werden. Auf der Startseite bleiben dann ausschließlich Modelkarten sichtbar, die zur Auswahl gehören; bei Mehrfachauswahl steht eine ODER- oder UND-Verknüpfung bereit.","Aktive Auswahl wird als orange markierter Zähler in der Kopfzeile und zusätzlich als zurücksetzbare Filterleiste oberhalb der Startseitenkarten angezeigt. Der Zustand bleibt lokal gespeichert.","Dynamisch nachgeladene Startseitenkarten werden automatisch mitgefiltert. Gelöschte Gruppen werden sicher aus dem Filter entfernt, und „Alle anzeigen“ stellt die vollständige Startseite sofort wieder her."],"source":"Änderungsprotokoll"},{"version":"20.3.0-custom","date":"2026-09-14","notes":["Große Modellbibliothek pro Model um zusätzliche Fakten erweitert: durchschnittliche und längste Sitzung, aktive Tage, erste/letzte Sichtung, genaue letzte Live-Beobachtung, Vorschau-/Schnellinfozähler, Speicherzeitpunkt, letzte Aktivität, Entdeckungen, Qualitätsmessungen und Profilstand.","Lokale Erfahrungswerte für ungefähre Onlinezeiten ergänzt. Live-Beobachtungen werden datensparsam höchstens einmal je fünf Minuten und Model in einem lokalen Wochen-/Stundenraster gesammelt; bis genügend Beobachtungen vorliegen, kann die eigene Sehzeit klar gekennzeichnet als vorsichtige Näherung dienen.","Eigene Tags als komfortablen Editor neu aufgebaut: sichtbare Tag-Chips, Entfernen per Klick, Hinzufügen per Enter oder Schaltfläche, ausdrücklicher Speicherschritt und Autovervollständigung aus bereits bei anderen Models verwendeten eigenen Tags.","Gespeicherte eigene Tags wirken unmittelbar auf die globale Bibliothekssuche und erscheinen zusätzlich in einem neuen direkten Mehrfachfilter, wahlweise mit ODER- oder UND-Verknüpfung.","Neue große Gruppenverwaltung direkt oberhalb der Modellkarten ergänzt: frei benannte Gruppen erstellen, umbenennen und löschen, ohne Models aus der Bibliothek zu entfernen.","Jedes Model besitzt nun einen aufklappbaren Mehrfach-Gruppenwähler. Ein Model kann gleichzeitig mehreren eigenen Gruppen angehören.","Gruppenmenü um Mehrfachfilter erweitert: mehrere Gruppen gemeinsam anzeigen, wahlweise Models aus mindestens einer oder aus allen ausgewählten Gruppen; Auswahl und Filtermodus bleiben lokal gespeichert."],"source":"Änderungsprotokoll"},{"version":"20.2.0-custom","date":"2026-09-14","notes":["Sämtliche direkt in die normale Chaturbate-Seite eingebauten RoomGrid-Bereiche auf einen gemeinsamen Chaturbate-Dark-Look umgestellt: flache dunkelblaue Flächen, originale Navigationsanmutung, cyanblaue aktive Markierung und orange Hauptaktionen.","Die drei RoomGrid-Zugänge samt Versionsnummer erscheinen jetzt wie weitere Punkte der originalen Kopfzeile statt als türkisfarbene Sonderleiste; Symbol-Dopplungen und Glaseffekte wurden dort entfernt.","Große Statistik-, Bibliotheks- und Nutzerseiten, Modelkarten, Filter, Diagramme und das komplette Einstellungszentrum verwenden nun dasselbe einheitliche Farbsystem, dieselben kompakten Radien und dieselbe flache Komponentensprache.","Den Werkzeugdock auf normalen Seiten ebenfalls vereinheitlicht: dunkle Originalflächen, blaue Kopfmarkierung, orange Hauptaktion und deutlich ruhigere Rahmen statt blau-türkisem Verlauf.","Im Nutzer-Dashboard wird nun ausdrücklich angezeigt, seit wann RoomGrid auf diesem Gerät lokale Messwerte besitzt.","Bei den bestehenden Kontodaten einen direkten Zugang zu Chaturbates offiziellem persönlichen Datenexport ergänzt. Damit lassen sich ältere bei Chaturbate gespeicherte personenbezogene Daten anfordern; ein künftiger Dateiimport bleibt bewusst vom tatsächlich gelieferten Exportformat abhängig."],"source":"Änderungsprotokoll"},{"version":"20.1.1-custom","date":"2026-09-12","notes":["Kopfzeilenverankerung an die echte aktuelle Chaturbate-Struktur angepasst: RoomGrid hängt nun gezielt in der sichtbaren zweiten Navigationszeile mit data-testid=\"header-nav-bar\" direkt hinter HOME, DISCOVER, TAGS, PRIVATE SHOWS, FOLLOWING und MERCH.","Den zuvor nicht abgedeckten SPA-Header ohne semantisches
berücksichtigt; dadurch verschwinden die drei RoomGrid-Menüs nicht mehr außerhalb eines großen oder abgeschnittenen Kopfcontainers.","„Mein Nutzer“ um eine freiwillige, rein lesende Übernahme bereits vorhandener Chaturbate-Kontodaten erweitert: gesamte Following-Liste inklusive Offline-Seiten, aktuelle Following-Gesamt-/Livezahlen und sicher erkennbare My-Collection-Einträge.","Bereits vor der Script-Installation gefolgte Models werden in eine eigene Following-Gruppe und in die Modelstatistik übernommen; erkannte My-Collection-Models ergänzen die Statistik, ohne Medien zu kopieren.","Bibliotheksgrenze von 1.200 auf 3.000 Models erhöht, damit auch große bestehende Following-Listen wie die im echten Konto sichtbaren 1.293 Einträge vollständig Platz finden.","Im Nutzer-Dashboard klar ausgewiesen, welche Daten rückwirkend übernommen werden können und welche nicht: Eine frühere persönliche Seh-/Besuchshistorie ist ohne von Chaturbate bereitgestellte Verlaufsdaten technisch nicht rekonstruierbar.","Reale öffentliche Chaturbate-Kopfzeile zusätzlich mit einem sauberen Brave-Live-DOM-Test geprüft; der normale Funktions- und Darstellungstest bleibt ebenfalls ausschließlich auf Brave festgelegt."],"source":"Änderungsprotokoll"},{"version":"20.1.0-custom","date":"2026-09-12","notes":["Drei direkte Zugänge in der originalen Chaturbate-Kopfzeile ergänzt: „Statistiken“, „Model-Bibliothek“ und „Mein Nutzer“. Auf dem bewusst leicht gehaltenen nativen /discover/ führen dieselben Links sicher zur normalen Seite, ohne dort die schweren Beobachter zu starten.","Neues großes Statistikzentrum direkt innerhalb der normalen Website ergänzt. Es wertet alle lokal beobachteten, gespeicherten und angesehenen Models aus und bietet Suche, Status-/Bibliotheksfilter, fünf Sortierungen, 24 bis 120 Einträge pro Seite und vollständige Ranglisten statt einer kleinen Top-Liste.","Modelstatistiken stark erweitert: Gesamt-/Durchschnitts-/längste Sehzeit, Sitzungen, aktive Tage, Anteil, erste/letzte Sichtung, zuletzt live, typische Wochenzeit, Vorschau-/Schnellinfo-/Discover-Zähler, Technikwerte, Tags, 14-Tage-Trend und direkter Raum-/Vorschau-/Detailzugriff.","Datengrundlage erweitert: Auf normalen Start-/Listenseiten sowie in RoomGrid Entdecken werden lokal gesehene Modelnamen und verfügbare Status-, Zuschauer- und Tagangaben nun auch ohne gestartete Wiedergabe in den Modelbestand übernommen.","Modellbibliothek als eigene große bildreiche Seite neu aufgebaut: große öffentliche Livebilder, optionale echte Hover-Livevorschau, Profilname/-text, Status, Zuschauer, Gruppen, Sehwerte, Notizen, eigene Tags, Bewertung, Priorität, Favorit, Merkliste, Archiv und drei wählbare Kartengrößen.","Persönliches Nutzer-Dashboard ergänzt: heutige/gewählte Nutzungszeit, aktive Tage und Serie, Modelkontakte, Wiederkehrquote, Bibliotheks-/Favoritenwerte, Vorschauen, Sehzeit, 14-Tage-Diagramm, Ereignisübersicht und meistgesehene Models.","Optionale offizielle Broadcaster-Kontowerte samt lokalen Snapshots klar getrennt in das Nutzer-Dashboard eingebunden; ohne bewusste Token-Verbindung werden keine persönlichen Chaturbate-Werte behauptet oder geraten.","Kopfzeilen- und Großseiten-Design an die blau/türkise RoomGrid-Marke angepasst, responsiv für schmale und breite Fenster umgesetzt und weiterhin mit der aktiven Versionsnummer versehen."],"source":"Änderungsprotokoll"},{"version":"20.0.4-custom","date":"2026-09-12","notes":["Vorschau-Auswahl auf insgesamt 19 Stufen erweitert: „Aus“ sowie 0,1, 0,2, 0,25, 0,33, 0,5, 0,75, 1, 1,5, 2, 3, 4, 5, 6, 8, 10, 12, 15 und maximal 20 FPS.","Flackernde blau/grüne Vorschau- und FPS-Plaketten von den Karten entfernt; die Bildvorschau aktualisiert sich nun ohne wechselnde Statusüberlagerung.","Echtes, latenzarmes HLS-Livevideo beim Darüberfahren für normale Start-/Listenkarten und RoomGrid Entdecken ergänzt. Es läuft immer stumm, nur für eine Karte gleichzeitig und wird beim Verlassen sofort vollständig beendet.","Hover-Vergrößerung von 100 bis 135 Prozent sowie Startverzögerungen von sofort bis eine Sekunde einstellbar gemacht.","Hohe Bildraten technisch bis 20 FPS beschleunigt; sichtbarkeits-, Scroll- und Tab-Pausen bleiben aktiv. Ab 10 FPS weist die Oberfläche ausdrücklich auf die starke Bandbreiten- und CPU-Last hin.","Den vollständigen Browsertest fest auf Brave umgestellt; ein Chrome-Fallback ist nicht mehr vorhanden."],"source":"Änderungsprotokoll"},{"version":"20.0.3-custom","date":"2026-09-09","notes":["Live-Vorschauen auf Start-/Listenseiten und in RoomGrid Entdecken auf gemeinsame Aktualisierungswellen umgestellt: Alle gerade sichtbaren Karten starten nun gleichzeitig statt nacheinander.","12 Bildraten von 0,1 FPS bis 5 FPS plus „Aus“ ergänzt; langsame Intervalle zeigen zusätzlich die Sekunden pro Bild an.","Als Standard ist „Alle sichtbaren gleichzeitig“ aktiv. Für schwächere Geräte bleibt „Leistungsgestaffelt“ mit frei wählbaren Grenzen von 1 bis 24 parallelen Abrufen verfügbar.","Einstellungen, Speicherbereinigung und Browser-Regressionsprüfungen für beide Vorschau-Systeme erweitert; der Browsertest weist jetzt eine gemeinsame Drei-Karten-Welle ausdrücklich nach."],"source":"Änderungsprotokoll"},{"version":"20.0.2-custom","date":"2026-09-09","notes":["Animierte, stumme JPEG-Live-Vorschauen für alle gerade sichtbaren Karten auf der normalen Start- und den unterstützten Listenseiten ergänzt.","Eigenes Einstellungsfeld unter „Seitendesign“ mit Ein/Aus, 0,25/0,5/1 FPS und maximal ein bis drei gleichzeitigen Bildabrufen ergänzt; Standard ist ressourcenschonend 0,5 FPS bei zwei Abrufen.","Vorschauen pausieren beim Scrollen, außerhalb des Sichtbereichs und in ausgeblendeten Tabs und stellen anschließend das normale Vorschaubild wieder her.","Originales Chaturbate-/discover/ in einen leichten Kompatibilitätsmodus versetzt, damit die dynamische Seite nicht mehr von mehreren RoomGrid-Beobachtern und Vollseitenscans belastet wird.","Inkonsistenten Schnellaktions-Schalter korrigiert, sodass die Einstellung nun tatsächlich greift."],"source":"Änderungsprotokoll"},{"version":"20.0.1-custom","date":"2026-09-09","notes":["Reale Chaturbate-Startseitenstruktur korrigiert: Der äußere room-list-Container bleibt vollbreit, während nur das tatsächliche RoomCardGrid als Raster formatiert wird.","Karten-Erkennung auf echte Karten begrenzt, damit verschachtelte RoomCard-Elemente nicht mehrfach gestaltet oder durchsucht werden.","QuickAdd- und Tag-Scans auf präzise Kartenanker reduziert; unveränderte Tag-Blöcke werden nicht erneut aufgebaut.","Discover-Katalogabruf nach zehn Sekunden sicher abgebrochen und auf die lokale Bibliothek zurückgeführt, statt unbegrenzt im Ladezustand zu bleiben.","Browser-Regressionstest an die im echten Brave beobachtete Homepage-DOM-Struktur angepasst.","Aktive RoomGrid-Version auf normalen Chaturbate-Seiten dauerhaft oben links sichtbar gemacht."],"source":"Änderungsprotokoll"},{"version":"20.0.0-custom","date":"2026-09-09","notes":["„Ultimate“-Ausbau mit neuem Analytics Studio, optionalem Broadcaster-Dashboard, Live Preview Dock, Model Quick View, Chat Focus und Wohlfühlfunktionen abgeschlossen.","Start-/Entdecken-Seiten um sparsame sichtbarkeitsgesteuerte Live-Vorschauen, weitere Smart-Ansichten, Karten-Schnellaktionen und ein frei anordenbares 12-Spalten-Dashboard erweitert.","Live Preview Dock für bis zu sechs stumme HLS-Vorschauen mit Audio-Solo, PiP, Größenwahl, Transparenz, Verschieben, Größenänderung, Edge-Snap und positionsbezogener Workspace-Speicherung ergänzt.","Model Quick View mit lokalen Beobachtungswerten, Notizen, Tags, Textvorlagen, Schnellnavigation und bewusst abrufbaren öffentlichen Profilinformationen ergänzt.","Analytics-Speicher auf 100.000 Rohereignisse erweitert; ältere Daten werden in Tages-Rollups verdichtet. Qualitätsproben und optionale offizielle Konto-Snapshots besitzen getrennte, begrenzte Speicherbereiche.","Analytics Studio um Zeiträume, Vorperiodenvergleich, Tagestrend, Wochen-Heatmap, Sitzungsdauerverteilung, Model-Rangliste/-Vergleich, Entdecken-Funnel, Workspace-Vergleich, QoE-Auswertung, CSV/JSON/Pseudonym-Export, Druckbericht und Bereichslöschung erweitert.","Optionales Broadcaster-Dashboard auf Basis der offiziellen Stats-API ergänzt: Token-Guthaben, Trinkgeld, Follower, Votes, Zufriedenheit, Zuschauer-Mix, Broadcast-Status, Onlinezeit und lokale Trend-Snapshots.","Stats-Token standardmäßig nur im Arbeitsspeicher; optionaler lokaler Tresor nutzt PBKDF2-SHA-256 mit 210.000 Iterationen und AES-GCM-256. Token erscheinen weder in normalen Einstellungen noch Protokollen oder Exporten.","Chat Focus als rein lokale, abschaltbare Lesekomfort-Schicht ergänzt: Zeitstempel, Systemnachrichten dimmen/ausblenden, flüchtige Suche, Mention-Markierung und Ungelesen-Trenner; keine Chat-Automation und keine persistierten Chatinhalte.","Player Lab in SingleCam und MultiCam um Farbton, Sepia, Graustufen, Invertierung, Unschärfe, Presets, Zurücksetzen und „Original halten“ erweitert.","Designsystem um Aurora/Ember/Frost, zwei Akzentfarben, Inhaltsbreite, vier Kartenstile, Widget-Dichten sowie korrigierte Glasflächen im Dunkelmodus erweitert.","Capability Layer, Fehler-Circuit-Breaker, sichere Abbruchpfade, Tab-Sichtbarkeitsstopps, harte Stream-Bereinigung und Ressourcengrenzen ergänzt.","Aufnahme- und Screenshot-Funktionen vollständig aus der Oberfläche entfernt und durch eine feste Laufzeitsperre deaktiviert; sensible Medien werden von RoomGrid nicht gespeichert.","Daten-/Privatsphärebereich um Messwertdefinitionen, Datenqualitätsangaben, private Sitzungen, Aufbewahrung, Zeitbudget und Pausenerinnerungen erweitert.","Aktuelle native Discover-Carousels anhand der echten Chaturbate-DOM-Struktur kompatibel gemacht: volle Seitenbreite, keine erzwungene Grid-Stapelung und Gestaltung der neuen roomCard-Klasse.","Build auf 17 Module und Schema 3 angehoben; statische Prüfungen, VM-Laufzeittest und echter Chromium-Oberflächentest um die neuen Funktionen und Sicherheitsgrenzen erweitert."],"source":"Änderungsprotokoll"},{"version":"17.0.0-custom","date":"2026-09-08","notes":["Gesamte 19-Punkte-Roadmap in einer erweiterten Suite zusammengeführt und Abschlussstatus dokumentiert.","Benannte Workspaces mit Beschreibung, Reihenfolge, Duplizieren, Archiv/Wiederherstellung, Einzelexport, Desktop-/Kompaktlayout und gespeichertem letzten Zustand ergänzt.","Layoutvorlagen, Gruppenbeschreibungen, Untergruppen, Gruppenreihenfolge und Gruppenarchiv ergänzt.","Optionaler Offline-Ersatz pro Workspace mit Quellgruppe und Verzögerung; standardmäßig aus und auf die aktuell sichtbare feste Belegung begrenzt.","Zentrale Modellbibliothek mit getrennten Bereichen für Favoriten, Following, Merkliste, Pausen und Archiv sowie Notizen, eigene Tags, Bewertung, Priorität und Massenaktionen ergänzt.","Sicheres Datenzentrum mit selektivem Export, Importvorschau, Mengen-/Konfliktanzeige, Zusammenführen als Standard, bewusster Bereichsersetzung und manueller v6/v7-Bereinigung ergänzt.","Mehr-Tab-Zusammenführung um feldweise Änderungszeiten erweitert, damit parallele Änderungen nicht mehr unnötig einen vollständigen Modeldatensatz ersetzen.","Globale Designzentrale mit freier Akzentfarbe, Oberflächenstilen, Dichte, Schrift-/Bediengrößen, abschaltbarer Bewegung, sortierbarer Navigation und Modulschaltern ergänzt.","Optionales Design normaler Start-, Tag-/Kategorie- und Modelseiten mit Karten-/Liste, Skalierung, Dichte, Navigation, Seitenleisten, Werbe-/Footersteuerung, Markierungen und Karten-Schnellaktionen ergänzt.","Befehlsmenü und globale Modelsuche samt konfigurierbaren Ctrl+K-/Ctrl+Shift+K-Kürzeln in normale Seiten, Entdecken und MultiCam integriert.","MultiCam-Modelpicker um Suche in Name, Notiz und eigenen Tags erweitert; stumme Hover-Vorschau, Gruppenansicht und exakte Slotwahl bleiben integriert.","MultiCam-Player um individuelle Qualität, Auflösungs-/Verbindungs-/Puffermesswerte und gespeicherte Helligkeit/Kontrast/Sättigung erweitert.","SingleCam um gespeicherte Bildkorrekturen, PiP, separates Playerfenster und individuelle Qualitätsvorgabe erweitert.","Entdecken um Suchverlauf, Zufallssortierung/Neu-Mischen, Auswahlleiste, Massenübernahme und Workspace-Erstellung ergänzt.","Persönliches Dashboard um verschiebbare/ausblendbare Bereiche, einstellbare Bereichsgröße, gespeicherte Filterstarts und begründete Vorschläge erweitert.","Direkte alte Konfigurationsimporte entfernt; alle normalen Einstiege führen in den geprüften Vorschau-Workflow. Statistikimporte zeigen vor dem Ersetzen eine Inhaltsvorschau.","Installierbare Datei wird aus 16 Modulen erzeugt; Version in Metadaten, Oberfläche und Prüfdefinitionen auf 17.0.0-custom angehoben.","Abschließender echter Browserdurchlauf ergänzt: Start, Entdecken, Tag-Explorer, SingleCam, MultiCam, Zentrale, Designs und Leistungsoptionen wurden interaktiv geprüft.","Normale Chaturbate-Inhaltsseiten wachsen in flexiblen Seitenlayouts wieder auf die verfügbare Breite; die Suite-Kopfzeile bricht auf Tablet und Mobil ohne horizontales Seitenüberlaufen um.","Bedingt vorhandene Bereiche erzeugen keine sichtbaren null-Platzhalter mehr.","Öffentliche Profilangaben werden nur aus den eigentlichen Biofeldern gelesen; externe Links werden ausschließlich aus dem „Über mich“-Bereich übernommen und generische Seiten-/Footerlinks verworfen.","Browser-Regressionsprüfungen um Profilfallen sowie Darstellungsbreiten von 390, 900 und 2560 Pixeln erweitert."],"source":"Änderungsprotokoll"},{"version":"16.4.0-custom","date":"2026-09-08","notes":["Roadmap-Punkt 18 als drei getrennte, abschaltbare Nebenmodule umgesetzt: Foto-/Videobereiche, Apps-Verzeichnis sowie öffentliche Contest-/native Linkübersicht.","Öffentlich zugängliche Foto-/Videobereiche der aktuellen Seite werden nur als Links aufgelistet und können lokal durchsucht und mit Notizen versehen werden; Medien werden weder kopiert noch automatisch heruntergeladen.","Apps-Verzeichnis mit lokalen Favoriten, Kategorien und Notizen ergänzt. Es werden nur sichere Same-Origin-Links verwaltet; keine App-Interaktion, kein Chat und keine Token-/Zahlungsaktion wird ausgeführt.","Öffentliche Contest-Tabellen werden lesend erkannt und mit klar getrennten Tabellenzellen dargestellt; bei fehlender Tabelle werden nur bereits vorhandene öffentliche Contest-Links angeboten.","Bereits auf der Seite vorhandene native Konto-, Hilfe-/Support- und Broadcaster-Links werden gesammelt. Login-, Signup-, Altersprüfungs-, Billing-, Checkout-, Token- und Zahlungswege sind ausdrücklich ausgefiltert.","Roadmap-Punkt 19 mit Diagnosezentrum abgeschlossen: Modul-/Seitentypstatus, Browserfähigkeiten, Speicher-/Backupprüfung, Mehrfensterzustand, Laufzeit-/Speicherwert und verständliches lokales Fehlerprotokoll.","Fehlertexte werden begrenzt, URLs verlieren Query/Fragment, mögliche Token-/Passwort-/Session-/Schlüsselwerte werden redigiert; Diagnoseexporte enthalten keine Workspaces, Modelnamen, Notizen oder Statistikdaten.","Rückkehr zur normalen Website, manuell wiederholbarer Selbsttest, Löschen des Fehlerprotokolls und Diagnoseexport ergänzt.","Storage-, Modulstart/-stopp-, Katalog-, Following-, Screenshot- und Menüfehler werden in das bereinigte Diagnoseprotokoll übernommen, ohne Seiteninhalte mitzuschreiben.","Laufzeit-, statische und echte Browsertests um Pfadfilter, Datenlimits, Medien-/App-/Contest-Erkennung, lokale App-Metadaten, Diagnoseansicht und wiederholtes Öffnen/Schließen erweitert."],"source":"Änderungsprotokoll"},{"version":"16.3.0-custom","date":"2026-09-08","notes":["Gemeinsame „Zentrale“ in MultiCam und Entdecken ergänzt: Verlauf/Statistik, Benachrichtigungen, Datenschutz sowie Fenster/Leistung sind in vier klar getrennten Seiten erreichbar.","Chronologische lokale Ereignisse für Besuche, Nutzungszeit, Arbeitsbereiche, Filter, Tags, entdeckte Models und Qualitäts-/Verbindungsangaben ergänzt; Gesamtnutzungszeit und summierte Cam-Zeit werden getrennt ausgewiesen.","Tages-/Wochen-/Monats-/Jahresauswahl, Wochen-Heatmap, Rangliste, typische Zeitfenster mit Hinweis bei geringer Datenbasis, Aufbewahrungsfrist, private Sitzung sowie Löschen einzelner Einträge oder ganzer Zeiträume ergänzt.","Statistikexporte als sensibel gekennzeichnet und Schema 2 um Ereignisse und Aufbewahrungsmetadaten erweitert; alte aggregierte Werte bleiben beim Datenbank-Upgrade erhalten.","Neues Benachrichtigungszentrum standardmäßig ausgeschaltet: Regeln je Model/Gruppe, Online/Offline, öffentliche Profiländerung, längere Inaktivität, Favoriten-Schwelle, Ruhezeiten, Cooldown, Stundenlimit, neutrale Texte und lokale Meldungsliste.","Statusprüfungen für Benachrichtigungen starten kein Video; mehrere passende Tabs bestimmen über eine lokale Lease genau einen Prüfer. Userscript-bedingte Hintergrundgrenzen werden in der Oberfläche erklärt.","Schnell-Ausblendung über Ctrl+Shift+X: Namen/Bilder werden neutral verdeckt, alle Medien pausiert und stummgeschaltet und ihr vorheriger Zustand beim Wiederherstellen zurückgesetzt.","Optionale lokale PIN, Speicherübersicht, private Sitzung und selektive Löschaktionen ergänzt; die Oberfläche weist ausdrücklich darauf hin, dass die PIN keine Verschlüsselung ist und keine Cloud-/Telemetrieübertragung stattfindet.","Benannte Mehrfenster-Arbeitsbereiche, Wiederherstellung zuletzt verwendeter Fensterzuordnungen, aktive Fensteranzeige sowie lokale Revisions-, Änderungszeit- und Löschmarken für konfliktärmere Bibliotheks-/Gruppensynchronisierung ergänzt.","Leistungsprofile Sparsam/Ausgewogen/Leistung, gleichzeitiges Streamlimit, gestaffelter Start, adaptive HLS-Qualität, gemessene Ressourcenwarnungen und ein ausdrücklicher Hinweis gegen „unbegrenzt viele Cams“ ergänzt.","Laufzeit-, statische und echte Browser-Oberflächentests auf Version 16.3 und die neue Zentrale erweitert."],"source":"Änderungsprotokoll"},{"version":"16.2.0-custom","date":"2026-09-08","notes":["Tag-Explorer als dritte Suite-Hauptansicht ergänzt: Suche, Auswahl, Favoriten, Blockliste, UND-/ODER-/NICHT-Kombinationen und gespeicherte Tag-Gruppen.","Tag-Feeds, persönliche Tag-Historie, häufig verwendete und gemeinsam auftretende Tags sowie lokale Katalogtrends ergänzt.","Trends ausdrücklich auf die begrenzten, lokal beobachteten Katalogabrufe beschränkt; gespeichert werden höchstens 400 Tags, 1.200 Models und 120 Verlaufseinträge.","Kategorie-, Alters- und Regionszugänge ergänzt; mehrere Regionen lassen sich per ODER kombinieren, unbekannte Werte bleiben sichtbar und Zeitzonen werden nicht geraten.","„Seit letztem Besuch neu“, öffentliche Neu-Markierung, temporäre Beobachtungsliste, persönlicher Neue-Cams-Feed, Gaming-Themenfavoriten und Gaming-Arbeitsbereich ergänzt.","SingleCam mit großem HLS-Player, Kino-/Vollbildmodus, Ton, Lautstärke, Qualitätswahl, Einpassen/Ausfüllen, Spiegeln und Drehen ergänzt.","Vorheriges/nächstes Model verwendet die ursprüngliche Ergebnisliste; die Rückkehr stellt die Ausgangsansicht samt Filtern wieder her.","Offline-/Privatwechsel werden weiter geprüft und zeigen Alternativen aus demselben Ergebnis oder Models mit gemeinsamen Tags.","Vollständige Modeldetails mit öffentlichen Angaben, gruppierten selbst veröffentlichten Links, Quellenzeitpunkt, lokalen Notizen/eigenen Tags, Favoriten/Following, Beobachtungs- und Sehstatistik ergänzt.","Änderungen eines bereits gespeicherten öffentlichen Profils werden sichtbar markiert; gleiche Benutzernamen werden ausdrücklich nicht als Identitätsnachweis behandelt.","Lokale Tags werden update-kompatibel im stabilen ryujo_multicam_v8-Modell bereinigt und gespeichert.","Laufzeit-, statische und echte Browser-Oberflächentests um Tag-Explorer, SingleCam-Navigation, Profilansicht sowie Notiz-/Tag-Speicherung erweitert."],"source":"Änderungsprotokoll"},{"version":"16.1.0-custom","date":"2026-09-08","notes":["Eigene Suite-Seite „Entdecken“ mit öffentlichem Same-Origin-Katalog und kontrolliertem Nachladen ergänzt.","Persönliche Startseite mit Online-Favoriten, neu gestarteten Streams, zuletzt und häufig besuchten Models, noch nie besuchten gespeicherten Models sowie Feed-Ergebnissen ergänzt.","Suche über Name, Beschreibung, Tags, Ort und lokale Notizen ergänzt.","UND-, ODER- und NICHT-Tagregeln sowie Filter nach Kategorie, Land/Region/Sprache, Zuschauerzahl, Online-Dauer, lokaler Sehzeit, Sitzungen, Favoriten, Following, Gruppe und Besuchsstatus ergänzt.","Zwei priorisierte Sortierungen und paginiertes Kartenraster ergänzt.","Bis zu zehn lokale Ansichten und 50 lokale Feeds mit geprüfter JSON-Sicherung ergänzt.","Direkte Kartenaktionen für Raum, Bibliothek, Favoriten, Gruppen und MultiCam ergänzt.","Stumme Live-Vorschau aller sichtbaren Karten über einzelne JPEG-Livebilder ergänzt; Aus/0,5/1/2 FPS und ein Limit von 1–4 gleichzeitigen Bildabrufen sind einstellbar.","Außerhalb des sichtbaren Bereichs, während schnellem Scrollen und in ausgeblendeten Tabs werden keine neuen Livebilder geladen.","Entdecken und Dashboard zählen nicht als Sehzeit und starten weder Video- noch Audiodecoder.","Der normale RoomGrid-Werkzeugdock und die MultiCam-Leiste verlinken direkt auf Entdecken.","Modulstart ans Ende des Ein-Datei-Builds verschoben, damit alle Modulkonstanten vor dem Start initialisiert sind.","Laufzeit- und Browserprüfungen für Route, Datengrenzen, Medien-URLs, Katalog, Suche, MultiCam-Übernahme, Dashboard und Livebildplaner ergänzt."],"source":"Änderungsprotokoll"},{"version":"16.0.0-custom","date":"2026-09-08","notes":["Quellcode in 11 geordnete Buildmodule aufgeteilt; Ausgabe bleibt eine einzelne .user.js.","Seitentyp-Erkennung und isolierter Modulstarter ergänzt.","Seitentools und Audiopegel können separat ein- oder ausgeschaltet werden.","Freie Raster mit 1–5 Zeilen und Spalten bis maximal 20 Cams ergänzt.","Gespeicherte individuelle Lautstärke je Cam ergänzt; sie wirkt als Faktor der globalen Lautstärke.","Audiopegel zeigen den echten RMS-Messwert und unterscheiden aktiv, still, stumm, pausiert und nicht verfügbar.","Die aktuell lauteste messbare Cam wird dezent hervorgehoben.","Audio-Wiederanlauf repariert: pro Videoelement wird nur eine MediaElementAudioSourceNode erzeugt und sicher wiederverbunden.","Gespeicherte Rasterplätze repariert: vollständige Kontextschlüssel statt fehlerhafter 220-Zeichen-Kürzung; Altwerte bleiben bestmöglich lesbar.","Beschädigte Konfiguration wird vor einer Wiederherstellung separat erhalten.","Speicher-/Quotenfehler werden in der Oberfläche sichtbar gemeldet.","Reproduzierbarer Build, Syntaxprüfung, statische Invarianten und ein isolierter Laufzeittest ergänzt."],"source":"Änderungsprotokoll"},{"version":"15.9.20-custom","date":"","notes":["Vollständiger gesicherter Ausgangsstand vor dem modularen Ausbau."],"source":"Änderungsprotokoll"}]; function restoreCamclientDialogFocus(dialog, trigger) { dialog.remove(); requestAnimationFrame(() => { if (trigger?.isConnected && !document.querySelector('[data-camclient-help][open], [data-camclient-about][open]')) trigger.focus(); }); } function openCamclientAbout(trigger) { const existing = document.querySelector('[data-camclient-about]'); if (existing) { existing.focus(); return; } const de = LANG === 'de'; const title = de ? 'Über Camclient' : 'About Camclient'; const close = $('button', { type: 'button', class: 'discover-btn', autofocus: true }, de ? 'Schließen' : 'Close'); const search = $('input', { type: 'search', placeholder: de ? 'Version oder Funktion suchen …' : 'Search version or feature…', ariaLabel: de ? 'Versionshistorie durchsuchen' : 'Search version history', style: { width: '100%', boxSizing: 'border-box', padding: '10px' } }); const count = $('p', { role: 'status', 'aria-live': 'polite', style: { fontSize: '12px' } }); const results = $('div'); let showAll = false; const allVersions = $('button', { type: 'button', class: 'discover-btn', dataset: { camclientAllVersions: '1' }, onclick: () => { showAll = !showAll; update(); } }); const rows = CAMCLIENT_VERSION_HISTORY.map(entry => ({ entry, text: `${entry.version} ${entry.notes.join(' ')}`.toLocaleLowerCase() })); const buildRow = entry => { const details = $('details', { dataset: { camclientVersion: entry.version }, style: { padding: '10px 0', borderBottom: '1px solid #405061' } }, [ $('summary', { style: { cursor: 'pointer', fontWeight: '700' } }, `${entry.version}${entry.date ? ` · ${entry.date}` : ''}`), ]); let built = false; details.addEventListener('toggle', () => { if (!details.open || built) return; built = true; details.appendChild($('ul', { style: { lineHeight: '1.5', fontSize: '13px', paddingLeft: '20px' } }, entry.notes.map(note => $('li', {}, note)))); }); return details; }; const update = () => { const query = search.value.trim().toLocaleLowerCase(); const matching = query ? rows.filter(row => row.text.includes(query)) : rows; const shown = query || showAll ? matching : matching.slice(0, 12); results.replaceChildren(...shown.map(row => row.details ??= buildRow(row.entry))); count.textContent = de ? `${shown.length} von ${rows.length} dokumentierten Versionen` : `${shown.length} of ${rows.length} documented versions`; allVersions.hidden = !!query || rows.length <= 12; allVersions.textContent = showAll ? (de ? 'Nur jüngste Versionen' : 'Recent versions only') : (de ? 'Alle Versionen' : 'All versions'); }; search.addEventListener('input', update); update(); const dialog = $('dialog', { dataset: { camclientAbout: '1' }, 'aria-labelledby': 'camclient-about-title', style: { width: 'min(800px, calc(100vw - 32px))', maxHeight: 'calc(100vh - 48px)', boxSizing: 'border-box', overflow: 'auto', padding: '20px', border: '1px solid #405061', borderRadius: '12px', background: 'var(--rg-panel,#111f2c)', color: 'var(--rg-text,#f1f5f8)', } }, [ $('div', { style: { position: 'sticky', top: '-20px', background: 'var(--rg-panel,#111f2c)', padding: '8px 0', zIndex: '1' } }, [ $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px' } }, [$('h2', { id: 'camclient-about-title' }, title), close]), $('p', {}, `${de ? 'Aktuelle Version' : 'Current version'}: ${META.version} · ${de ? 'Autor' : 'Author'}: ${META.author} · ${de ? 'Lizenz' : 'License'}: ${META.license}`), search, count, allVersions, ]), $('p', { style: { fontSize: '12px', lineHeight: '1.5' } }, de ? 'Alle im Projekt auffindbaren Versionsstände einschließlich älterer Vorläufer. Änderungsnotizen aus Protokoll und Projektberichten; fehlende Angaben werden nicht ergänzt. Die historischen Texte sind auf Deutsch. Die Liste sagt nicht aus, welche Versionen du installiert hast.' : 'All versions found in the project, including earlier predecessors. Notes come from the changelog and project reports; missing information is not invented. Historical notes are in German. This list does not indicate your installed update history.'), results, ]); close.addEventListener('click', () => dialog.close()); dialog.addEventListener('close', () => restoreCamclientDialogFocus(dialog, trigger), { once: true }); document.body.appendChild(dialog); dialog.showModal(); } function openCamclientHelp(trigger, navigate = null) { const existing = document.querySelector('[data-camclient-help]'); if (existing) { existing.focus(); return; } const de = LANG === 'de'; const topics = [ ['Startseite', 'Home', 'Live-Räume sofort sehen. Aktive Filter bleiben sichtbar; „Ansicht“ steuert Karten, FPS und Hover-Vorschau.', 'Browse live rooms. Active filters remain visible; Display controls cards, FPS and hover previews.'], ['Entdecken', 'Discover', 'Neue und zu deinen lokalen Interessen passende Models finden. Uninteressante Vorschläge ausblenden oder wieder zulassen.', 'Find new models and local-interest recommendations. Hide unwanted suggestions or allow them again.'], ['Tag-Explorer', 'Tag Explorer', 'Tags suchen und kombinieren: UND verlangt alle, ODER mindestens einen, NICHT schließt Tags aus. Passende Streams erscheinen darunter.', 'Combine tags: AND requires all, OR any, NOT excludes. Matching streams appear below.'], ['Statistikzentrum', 'Statistics', 'Lokal beobachtete Sehzeit, Sitzungen und Aktivität nach Zeitraum vergleichen. Keine vollständigen offiziellen Account-Statistiken.', 'Compare locally observed watch time, sessions and activity by period. Not complete official account statistics.'], ['Modellbibliothek', 'Model library', 'Models mit Favoriten, Gruppen, eigenen Tags, Notizen und Covern organisieren. Sammlung durchsuchen, filtern und sortieren.', 'Organize models with favorites, groups, tags, notes and covers. Search, filter and sort your collection.'], ['Gefolgt', 'Following', 'Offizielle Following-Liste des angemeldeten Chaturbate-Kontos anzeigen und filtern. Gespeicherte Stände können veraltet sein; bei Bedarf aktualisieren.', 'View and filter the signed-in account’s official follows. Cached data may be stale; refresh when needed.'], ['Konto & Nutzung', 'Account & usage', 'Erkannte Kontoinfos, lokale Nutzung und Interessen getrennt ansehen. Kontoinfos hängen von Anmeldung und Website-Verfügbarkeit ab.', 'View detected account information, local usage and interests separately. Account data depends on login and availability.'], ['SingleCam', 'SingleCam', 'Ein Model groß ansehen: Zoom, Ton, Chat, Profil, Tags und Notizen. Screenshots und fertige Aufnahmen in seiner Galerie ansehen und ein Cover auswählen.', 'Watch one model with zoom, audio, chat, profile, tags and notes. View screenshots and finished recordings in its gallery and choose a cover.'], ['MultiCam', 'MultiCam', 'Models aus der linken Liste ins Raster ziehen. Kameras ersetzen, Qualität, Audio und Pegelanzeigen steuern. Niedrigere Qualität reduziert die Last.', 'Drag models from the sidebar into a grid. Replace cameras and adjust quality, audio and meters. Lower quality reduces load.'], ['Einstellungen', 'Settings', 'Optionen suchen, favorisieren und nur geänderte Werte anzeigen. Themen einzeln aufklappen: Startseite/Filter, Design/Logo/Hintergründe, Vorschau/FPS/Hover, Player/Audio, Benachrichtigungen, Datenschutz, Leistung und Diagnose.', 'Search options, favorite them or show changed values. Expand topics for home/filters, design/logos/backgrounds, previews/FPS/hover, player/audio, notifications, privacy, performance and diagnostics.'], ['Daten & Arbeitsbereiche', 'Data & workspaces', 'Layouts und Sammlungen verwalten, Sicherungen exportieren und importieren. Persönliche Sicherungen vor dem Weitergeben prüfen.', 'Manage layouts and collections, export/import backups. Review personal backups before sharing them.'], ['Speicherung & Downloads', 'Storage & downloads', 'Persönliche Daten liegen im eigenen Browser, nicht im Script. Updates behalten sie bei stabiler Script-Identität; Browserdatenlöschung kann sie entfernen. Regelmäßig sichern. Downloads ohne Abfrage in den Browser-Einstellungen aktivieren; Ordnerzugriff hängt vom Browser ab. Hohe Vorschau-FPS erhöhen die Last, nicht automatisch die Serverbildrate.', 'Personal data stays in your browser, not in the script. Stable-identity updates preserve it; clearing browser data can remove it. Back up regularly. Configure download prompts in browser settings; directory access depends on the browser. High preview FPS increases load, not necessarily server frame rate.'], ]; const targets = ['dashboard', 'discover', 'tags', 'stats', 'library', 'following', 'user', '', 'multicam']; const close = $('button', { type: 'button', class: 'discover-btn', autofocus: true }, de ? 'Schließen' : 'Close'); const versions = $('button', { type: 'button', class: 'discover-btn', dataset: { camclientAboutButton: '1' }, onclick: () => { dialog.close(); openCamclientAbout(trigger); } }, de ? 'Versionshistorie' : 'Version history'); const dialog = $('dialog', { dataset: { camclientHelp: '1' }, 'aria-labelledby': 'camclient-help-title', style: { width: 'min(760px, calc(100vw - 32px))', maxHeight: 'calc(100vh - 48px)', boxSizing: 'border-box', overflow: 'auto', padding: '20px', border: '1px solid #405061', borderRadius: '12px', background: 'var(--rg-panel,#111f2c)', color: 'var(--rg-text,#f1f5f8)', } }, [ $('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: '12px' } }, [$('h2', { id: 'camclient-help-title' }, de ? 'Camclient – Hilfe & Version' : 'Camclient – Help & version'), versions, close]), ...topics.map(([titleDe, titleEn, textDe, textEn], index) => $('section', {}, [ $('h3', { style: { marginBottom: '4px', fontSize: '14px' } }, de ? titleDe : titleEn), $('p', { style: { marginTop: '0', lineHeight: '1.5', fontSize: '13px' } }, de ? textDe : textEn), navigate && targets[index] ? $('button', { type: 'button', class: 'discover-btn', onclick: () => { dialog.close(); navigate(targets[index]); } }, de ? `${titleDe} öffnen` : `Open ${titleEn}`) : null, ])), ]); close.addEventListener('click', () => dialog.close()); dialog.addEventListener('close', () => restoreCamclientDialogFocus(dialog, trigger), { once: true }); document.body.appendChild(dialog); dialog.showModal(); } /* ============================================================= * 3. 通知 / Notify * ============================================================= */ const Notify = { granted: false, init() { if ('Notification' in window && Notification.permission === 'granted') this.granted = true; }, async request() { if (!('Notification' in window)) return false; if (Notification.permission === 'granted') { this.granted = true; return true; } if (Notification.permission === 'denied') return false; const r = await Notification.requestPermission(); this.granted = r === 'granted'; return this.granted; }, fire(title, body, tag = '', onClick = null) { if (!this.granted) return; try { const n = new Notification(title, { body, silent: false, tag: tag || ('multicam-' + title) }); n.onclick = () => { window.focus(); try { onClick?.(); } catch (_) {} n.close(); }; setTimeout(() => n.close(), 8000); } catch (_) {} }, }; /* ============================================================= * 3.05. Diagnose / Compatibility * Begrenztes, bereinigtes lokales Fehlerprotokoll ohne Seiteninhalte. * ============================================================= */ const DIAGNOSTICS_STORE_KEY = 'ryujo_multicam_diagnostics_v1'; const DIAGNOSTICS_MAX_ERRORS = 100; const Diagnostics = (() => { const startedAt = Date.now(); const moduleStates = new Map(); let installed = false; function cleanText(value, max = 300) { return String(value ?? '') .replace(/https?:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?/giu, match => { try { const url = new URL(match); const file = url.pathname.split('/').pop() || ''; return /\.(?:js|mjs|css)$/iu.test(file) ? `${url.origin}/[file]/${file}` : `${url.origin}/[path]`; } catch (_) { return '[URL]'; } }) .replace(/(token|password|passwd|secret|session|auth|key)\s*[:=]\s*[^\s,;]+/giu, '$1=[redacted]') .replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, max); } function sanitizeEntry(input) { const source = input && typeof input === 'object' ? input : {}; return { id: String(source.id || `d_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`).slice(0, 50), at: Math.max(0, Number(source.at) || Date.now()), module: cleanText(source.module || 'unknown', 50), severity: ['info', 'warning', 'error'].includes(source.severity) ? source.severity : 'error', message: cleanText(source.message, 300), detail: cleanText(source.detail, 500), }; } function load() { try { const source = JSON.parse(localStorage.getItem(DIAGNOSTICS_STORE_KEY) || '{}'); return { errors: (Array.isArray(source?.errors) ? source.errors : []).slice(-DIAGNOSTICS_MAX_ERRORS).map(sanitizeEntry) }; } catch (_) { return { errors: [] }; } } function save(state) { try { localStorage.setItem(DIAGNOSTICS_STORE_KEY, JSON.stringify({ schema: 1, errors: state.errors.slice(-DIAGNOSTICS_MAX_ERRORS) })); return true; } catch (_) { return false; } } function report(module, error, detail = '', severity = 'error') { const state = load(); const message = cleanText(error?.message || error || 'unknown error'); const stack = cleanText(error?.stack || '', 500); const entry = sanitizeEntry({ module, severity, message, detail: detail || stack }); const duplicate = state.errors[state.errors.length - 1]; if (!duplicate || duplicate.module !== entry.module || duplicate.message !== entry.message || entry.at - duplicate.at > 30000) state.errors.push(entry); save(state); moduleStates.set(entry.module, { status: severity === 'error' ? 'error' : 'degraded', detail: entry.message, at: entry.at }); return entry; } function mark(module, status = 'ok', detail = '') { module = cleanText(module, 50); if (!module) return; moduleStates.set(module, { status: ['ok', 'degraded', 'disabled', 'error'].includes(status) ? status : 'degraded', detail: cleanText(detail, 180), at: Date.now() }); } function probeDom(module, selectors, root = document) { const list = (Array.isArray(selectors) ? selectors : [selectors]).map(String).filter(Boolean); try { const selector = list.find(value => root?.querySelector?.(value)); if (selector) { mark(module, 'ok', selector); return { ok: true, selector, root }; } mark(module, 'degraded', `DOM anchor unavailable: ${list.join(' | ')}`); return { ok: false, selector: '', root: null }; } catch (error) { report(module, error, 'DOM compatibility check failed'); return { ok: false, selector: '', root: null }; } } function getErrors() { return load().errors.slice().reverse(); } function clear() { save({ errors: [] }); } function backupStatus() { const validation = backupStatus.validation ||= new Map(); const readValidated = key => { const value = localStorage.getItem(key); const previous = validation.get(key); if (previous && previous.value === value) return previous.valid; const valid = !!parseStoredState(value); validation.set(key, { value, valid }); return valid; }; let primary = false; let archive = false; let backups = 0; try { primary = readValidated(STORE_KEY); } catch (_) {} try { archive = readValidated(STABLE_ARCHIVE_KEY); } catch (_) {} try { for (let i = 0; i < localStorage.length; i++) if (String(localStorage.key(i) || '').startsWith(CONFIG_BACKUP_PREFIX)) backups += 1; } catch (_) {} return { primary, archive, backups }; } async function runSelfCheck() { const checks = []; try { const key = `${DIAGNOSTICS_STORE_KEY}_probe`; localStorage.setItem(key, '1'); const ok = localStorage.getItem(key) === '1'; localStorage.removeItem(key); checks.push({ id: 'storage', status: ok ? 'ok' : 'error', detail: ok ? 'read/write' : 'roundtrip failed' }); } catch (error) { checks.push({ id: 'storage', status: 'error', detail: cleanText(error?.message) }); } const backup = backupStatus(); checks.push({ id: 'workspace', status: backup.primary ? 'ok' : 'degraded', detail: `primary=${backup.primary}; archive=${backup.archive}; backups=${backup.backups}` }); checks.push({ id: 'indexeddb', status: 'indexedDB' in window ? 'ok' : 'degraded', detail: 'local statistics database' }); checks.push({ id: 'notifications', status: 'Notification' in window ? 'ok' : 'degraded', detail: 'browser notification API' }); checks.push({ id: 'crypto', status: typeof crypto !== 'undefined' && !!crypto.subtle ? 'ok' : 'degraded', detail: 'local PIN hash API' }); try { const windowState = JSON.parse(localStorage.getItem(WINDOW_COORDINATION_KEY) || '{}'); const peers = Object.values(windowState?.peers || {}); const stale = peers.filter(peer => Date.now() - Number(peer?.lastSeenAt || 0) > 20000).length; checks.push({ id: 'multi-window', status: stale ? 'degraded' : 'ok', detail: `${peers.length} active; ${stale} stale` }); } catch (error) { checks.push({ id: 'multi-window', status: 'degraded', detail: cleanText(error?.message) }); } const memory = typeof performance !== 'undefined' ? performance.memory : null; const heapRatio = memory?.jsHeapSizeLimit ? Number(memory.usedJSHeapSize || 0) / Number(memory.jsHeapSizeLimit) : 0; checks.push({ id: 'long-run', status: heapRatio >= 0.85 ? 'degraded' : 'ok', detail: `uptime=${Math.round((Date.now() - startedAt) / 1000)}s${heapRatio ? `; heap=${Math.round(heapRatio * 100)}%` : ''}` }); const runtime = typeof SuiteRuntime !== 'undefined' ? SuiteRuntime.status() : { pageType: detectPageType(), running: [] }; const routeMatrix = typeof SuiteRuntime !== 'undefined' ? SuiteRuntime.routeMatrix() : []; routeMatrix.forEach(route => checks.push({ id: `route:${route.id}`, status: route.ok ? 'ok' : 'error', detail: `${route.sample} -> ${route.actual}; expected=${route.expected}; module=${route.module}`, })); const expectedModule = runtime.pageType === 'workstation' ? 'workstation' : runtime.pageType === 'discover' ? 'discover' : 'pageTools'; checks.push({ id: 'route:active-module', status: runtime.running?.includes(expectedModule) ? 'ok' : 'error', detail: `page=${runtime.pageType}; expected=${expectedModule}; running=${(runtime.running || []).join(',') || 'none'}` }); const headerRequired = ROOMGRID_NORMAL_PAGE_TYPES?.includes?.(runtime.pageType); if (headerRequired) checks.push({ id: 'route:header', status: document.querySelector('[data-roomgrid-header-menu]') ? 'ok' : 'degraded', detail: document.querySelector('[data-roomgrid-header-menu]') ? 'RoomGrid header mounted' : 'RoomGrid header anchor unavailable' }); const routeHealth = typeof RoomGridRouteHealth !== 'undefined' ? RoomGridRouteHealth.status() : null; if (routeHealth) checks.push({ id: 'route:content', status: routeHealth.status === 'degraded' ? 'degraded' : 'ok', detail: `${routeHealth.pageType}: ${routeHealth.status}; ${routeHealth.detail || '—'}` }); const pageType = detectPageType(); const pageAnchor = pageType === 'workstation' ? '.app-shell' : pageType === 'discover' ? '.discover-shell' : 'body'; checks.push({ id: `dom:${pageType}`, status: document.querySelector(pageAnchor) ? 'ok' : 'error', detail: pageAnchor }); checks.forEach(check => mark(check.id, check.status, check.detail)); return checks; } function snapshot() { const runtime = typeof SuiteRuntime !== 'undefined' ? SuiteRuntime.status() : { pageType: detectPageType(), running: [], errors: [] }; const backup = backupStatus(); return { schema: 1, sensitive: false, generatedAt: Date.now(), version: META.version, pageType: runtime.pageType, uptimeMs: Date.now() - startedAt, visibility: document.visibilityState || 'unknown', online: navigator.onLine !== false, language: String(navigator.language || ''), features: { indexedDB: 'indexedDB' in window, notifications: 'Notification' in window, crypto: typeof crypto !== 'undefined' && !!crypto.subtle, hlsJs: !!window.Hls, nativeHls: !!document.createElement?.('video')?.canPlayType?.('application/vnd.apple.mpegurl'), }, workspace: backup, runtime: { running: runtime.running || [], errors: (runtime.errors || []).map(sanitizeEntry) }, navigation: { generation: Number(runtime.generation) || 0, routeKey: cleanText(runtime.routeKey || '', 240), lastReason: cleanText(runtime.lastReason || '', 80), lastNavigationAt: Number(runtime.lastNavigationAt) || 0, routeHistory: (runtime.routeHistory || []).map(item => ({ at: Number(item.at) || 0, from: cleanText(item.from || '', 40), to: cleanText(item.to || '', 40), route: cleanText(item.route || '', 240), reason: cleanText(item.reason || '', 80), })), content: typeof RoomGridRouteHealth !== 'undefined' ? RoomGridRouteHealth.status() : null, }, modules: [...moduleStates.entries()].map(([id, value]) => ({ id, ...value })), experience: typeof RoomGridUx !== 'undefined' ? RoomGridUx.status() : null, errors: load().errors, }; } function exportReport() { downloadBlob(new Blob([JSON.stringify(snapshot(), null, 2)], { type: 'application/json' }), `roomgrid-diagnostics-${stampForFile()}.json`); } function normalSiteUrl() { const url = new URL(location.href); url.searchParams.delete('multicam_mode'); url.searchParams.delete('multicam_discover'); url.searchParams.delete('suite_view'); url.searchParams.delete('model'); url.searchParams.delete('workspace'); url.searchParams.delete('window_name'); url.searchParams.delete('window_id'); url.hash = ''; return url.toString(); } function init() { if (installed) return; installed = true; window.addEventListener('error', event => { const text = `${event?.message || ''} ${event?.filename || ''}`; if (/roomgrid|multicam|userscript/iu.test(text)) report('runtime', event.error || event.message, 'window error'); }); window.addEventListener('unhandledrejection', event => { const text = String(event?.reason?.stack || event?.reason || ''); if (/roomgrid|multicam|userscript/iu.test(text)) report('runtime', event.reason, 'unhandled rejection'); }); mark('diagnostics', 'ok', 'local bounded log'); } return { init, report, mark, probeDom, getErrors, clear, runSelfCheck, snapshot, exportReport, backupStatus, normalSiteUrl, cleanText }; })(); /* ============================================================= * 3.1. Lokale Nutzungsstatistik / Analytics * F-009: bewusst getrennt von ryujo_multicam_v8, damit Workspace-Daten * weder durch Wachstum noch durch Import/Löschen der Statistik berührt werden. * ============================================================= */ function createAnalytics() { // Tampermonkey resolves free built-ins through its Window proxy on every // lookup. Keep stable intrinsics local to these high-volume data loops. const { Math, Number, String, Array, Object, Date, Set, Map, Promise, JSON, URL } = globalThis; const DB_NAME = 'ryujo_multicam_analytics_v1'; const DB_VERSION = 4; const MIN_SESSION_MS = 5000; const MAX_EVENTS = 100000; const MAX_QUALITY_SAMPLES = 20000; const MAX_ACCOUNT_SNAPSHOTS = 20000; const MAX_ANALYTICS_BACKUPS = 3; let db = null; let enabled = true; let ready = false; let initPromise = null; let resolveDatabaseReady; const databaseReady = new Promise(resolve => { resolveDatabaseReady = resolve; }); const profileReads = new Map(); let timer = 0; let timedStateTimer = 0; let visibilityHandler = null; let provider = null; let meta = { startedAt: 0, totalUsageMs: 0, usageDays: {}, retentionDays: 365, retentionPolicy: { eventsDays: 365, qualityDays: 180, accountDays: 365, rollupDays: 3650 }, privateSession: false, privateSessionUntil: 0, historyPausedUntil: 0 }; const models = new Map(); const profiles = new Map(); const sessions = new Map(); const events = []; const qualitySamples = []; const accountSnapshots = []; const dailyRollups = new Map(); let usageSession = null; const dirtyModels = new Set(); const dirtyProfiles = new Set(); const listeners = new Set(); let dataRevision = 0; let inventoryEstimate = null; let inventoryEstimatePromise = null; const clone = (value) => { try { return JSON.parse(JSON.stringify(value)); } catch (_) { return value; } }; const safeId = (id) => normalizeUsername(id); const validId = (id) => isLikelyUsername(id); const requestResult = (request) => new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('IndexedDB request failed')); }); // A playing video needs regular main-thread opportunities for presentation // and controls. This check only changes background work granularity; it // never pauses a stream or skips a record. const playbackIsActive = () => { try { return [...document.querySelectorAll('video')].some(video => !video.paused && !video.ended && video.readyState >= 2); } catch (_) { return false; } }; // Large accounts can contain tens of thousands of models and up to one // hundred thousand events. Rich primary-key records use coarse pages; // indexed event stores keep one efficient request. Normalize in short UI // slices without reintroducing the old tiny-cursor paging overhead. const readStoreProgressively = async (storeName, visit, options = {}) => { if (!db?.objectStoreNames?.contains?.(storeName)) return 0; const limit = Math.max(0, Number(options.limit) || Number.MAX_SAFE_INTEGER); // Model/profile records contain rich arrays. Returning all 60k rows in // one onsuccess forces a large native structured-clone pause before the // JS slice budget can help. Use coarse keyset pages, not tiny cursors; // preserve every raw primary key and never cap the stored collection. if ((storeName === 'models' || storeName === 'profiles') && !options.index) { let seen = 0, lastKey; while (seen < limit) { const source = db.transaction(storeName, 'readonly').objectStore(storeName); const range = lastKey === undefined ? null : IDBKeyRange.lowerBound(lastKey, true); let playbackActive = playbackIsActive(); const pageSize = playbackActive ? 256 : 4096; const requested = Math.min(pageSize, limit - seen); const values = await requestResult(source.getAll(range, requested)); if (!values.length) break; let sliceStartedAt = performance.now(); for (let index = 0; index < values.length; index++) { visit(values[index], seen + index); if (index && index % 16 === 0 && performance.now() - sliceStartedAt >= (playbackActive ? 3 : 8)) { if (globalThis.scheduler?.yield) await globalThis.scheduler.yield(); else await new Promise(next => setTimeout(next, 0)); sliceStartedAt = performance.now(); playbackActive = playbackIsActive(); } } seen += values.length; lastKey = values[values.length - 1].id; if (values.length < requested) break; } return seen; } const tx = db.transaction(storeName, 'readonly'); let source = tx.objectStore(storeName); if (options.index && source.indexNames.contains(options.index)) source = source.index(options.index); let values = await requestResult(source.getAll()); if (options.direction === 'prev') values.reverse(); if (values.length > limit) values = values.slice(0, limit); let sliceStartedAt = performance.now(); for (let index = 0; index < values.length; index += 1) { visit(values[index], index); if (index && index % 256 === 0 && performance.now() - sliceStartedAt >= 8) { await new Promise(next => setTimeout(next, 0)); sliceStartedAt = performance.now(); } } return values.length; }; const transactionDone = (tx) => new Promise((resolve, reject) => { tx.oncomplete = () => resolve(true); tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction failed')); tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted')); }); const emit = () => { dataRevision += 1; for (const fn of listeners) { try { fn(); } catch (_) {} } }; function cleanDailyRollup(input) { if (!input || typeof input !== 'object' || !/^\d{4}-\d{2}-\d{2}$/u.test(String(input.day || ''))) return null; const cleanMap = value => Object.fromEntries(Object.entries(value && typeof value === 'object' && !Array.isArray(value) ? value : {}).slice(0, 40).map(([key, amount]) => [String(key).slice(0, 40), Math.max(0, Number(amount) || 0)])); return { day: String(input.day), counts: cleanMap(input.counts), durations: cleanMap(input.durations), updatedAt: Math.max(0, Number(input.updatedAt) || Date.now()) }; } function mergeIntoRollups(items) { for (const event of items) { const day = new Date(event.at).toISOString().slice(0, 10); const rollup = dailyRollups.get(day) || { day, counts: {}, durations: {}, updatedAt: Date.now() }; rollup.counts[event.type] = (Number(rollup.counts[event.type]) || 0) + 1; rollup.durations[event.type] = (Number(rollup.durations[event.type]) || 0) + Math.max(0, Number(event.durationMs) || 0); rollup.updatedAt = Date.now(); dailyRollups.set(day, rollup); } } function emptyModel(id) { return { id, totalMs: 0, sessions: 0, firstWatchedAt: 0, lastWatchedAt: 0, longestSessionMs: 0, lastStatus: 'unknown', lastOnlineAt: 0, viewerCount: 0, // Keep the 168 weekly slots sparse. Consumers already treat missing // entries as zero; eagerly allocating 336 numbers for every observed // model multiplied startup time and memory for very large libraries. hours: [], onlineHours: [], onlineSamples: 0, lastOnlineSampleAt: 0, days: {}, tags: [], firstObservedAt: 0, lastObservedAt: 0, appearances: 0, sources: [], subjects: [], previewUrls: [], country: '', location: '', gender: 'unknown', age: 0, followers: 0, secondsOnline: 0, isHd: false, isNew: false, isGaming: false, }; } function cleanPreviewUrl(value, id) { // Already canonical local thumbnails are common during large-history // hydration. Validate the whole string before bypassing URL parsing. if (typeof value === 'string' && /^https:\/\/thumb\.live\.mmcdn\.com\/riw?\/[a-zA-Z0-9_-]+\.jpg$/u.test(value)) return value; try { const url = new URL(String(value || ''), location.origin); if (url.origin === 'https://thumb.live.mmcdn.com' && /^\/riw?\/[a-zA-Z0-9_-]+\.jpg$/u.test(url.pathname)) return `${url.origin}${url.pathname}`; if (url.origin === 'https://jpeg.live.mmcdn.com' && url.pathname === '/stream') return `https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(id)}`; } catch (_) {} return ''; } function cleanModel(input) { if (!input || typeof input !== 'object' || !validId(input.id)) return null; const id = safeId(input.id); const out = { ...emptyModel(id), ...input, id }; out.totalMs = Math.max(0, Number(out.totalMs) || 0); out.sessions = Math.max(0, Math.round(Number(out.sessions) || 0)); out.firstWatchedAt = Math.max(0, Number(out.firstWatchedAt) || 0); out.lastWatchedAt = Math.max(0, Number(out.lastWatchedAt) || 0); out.longestSessionMs = Math.max(0, Number(out.longestSessionMs) || 0); out.lastOnlineAt = Math.max(0, Number(out.lastOnlineAt) || 0); out.viewerCount = Math.max(0, Math.round(Number(out.viewerCount) || 0)); out.onlineSamples = Math.max(0, Math.round(Number(out.onlineSamples) || 0)); out.lastOnlineSampleAt = Math.max(0, Number(out.lastOnlineSampleAt) || 0); out.firstObservedAt = Math.max(0, Number(out.firstObservedAt) || 0); out.lastObservedAt = Math.max(out.firstObservedAt, Number(out.lastObservedAt) || 0); out.appearances = Math.max(0, Math.min(1000000000, Math.round(Number(out.appearances) || 0))); out.lastStatus = ['online', 'offline', 'private', 'loading', 'error', 'unknown'].includes(out.lastStatus) ? out.lastStatus : 'unknown'; // Already numeric buckets are the common case in stored histories. // Preserve the original normalization for legacy/non-numeric values. out.hours = Array.isArray(out.hours) ? out.hours.slice(0, 168).map(value => typeof value === 'number' && value >= 0 ? (value || 0) : Math.max(0, Number(value) || 0)) : []; out.onlineHours = Array.isArray(out.onlineHours) ? out.onlineHours.slice(0, 168).map(value => typeof value === 'number' && value >= 0 && (value === Infinity || Number.isInteger(value)) ? (value || 0) : Math.max(0, Math.round(Number(value) || 0))) : []; out.days = out.days && typeof out.days === 'object' && !Array.isArray(out.days) ? Object.fromEntries( Object.entries(out.days).slice(-450).map(([key, value]) => [String(key).slice(0, 10), Math.max(0, Number(value) || 0)]), ) : {}; out.tags = [...new Set((Array.isArray(out.tags) ? out.tags : []).map(v => String(v || '').trim().slice(0, 80)).filter(Boolean))].slice(0, 80); out.sources = [...new Set((Array.isArray(out.sources) ? out.sources : []).map(value => String(value || '').replace(/[^a-zA-Z0-9_\/-]/gu, '').slice(0, 60)).filter(Boolean))].slice(-16); out.subjects = [...new Set((Array.isArray(out.subjects) ? out.subjects : []).map(value => String(value || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 240)).filter(Boolean))].slice(-8); out.previewUrls = [...new Set((Array.isArray(out.previewUrls) ? out.previewUrls : []).map(value => cleanPreviewUrl(value, id)).filter(Boolean))].slice(-4); out.country = String(out.country || '').replace(/[^\p{L}\p{N} .,'’()\/-]/gu, '').trim().slice(0, 40); out.location = String(out.location || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 100); out.gender = ['f', 'm', 'c', 't'].includes(out.gender) ? out.gender : 'unknown'; out.age = clampInt(out.age, 0, 99, 0); out.followers = Math.max(0, Math.round(Number(out.followers) || 0)); out.secondsOnline = Math.max(0, Math.min(604800, Math.round(Number(out.secondsOnline) || 0))); out.isHd = !!out.isHd; out.isNew = !!out.isNew; out.isGaming = !!out.isGaming; return out; } function cleanProfile(input) { if (!input || typeof input !== 'object' || !validId(input.id)) return null; const id = safeId(input.id); return { id, parserVersion: clampInt(input.parserVersion, 0, 20, 0), displayName: String(input.displayName || id).slice(0, 140), bio: String(input.bio || '').slice(0, 4000), sections: (Array.isArray(input.sections) ? input.sections : []).slice(0, 16).map(section => ({ label: String(section?.label || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 100), text: String(section?.text || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 3000), })).filter(section => section.label && section.text), links: [...new Set((Array.isArray(input.links) ? input.links : []).map(v => String(v || '').trim()).filter(v => /^https?:\/\//i.test(v)))].slice(0, 40), tags: [...new Set((Array.isArray(input.tags) ? input.tags : []).map(v => String(v || '').trim().slice(0, 80)).filter(Boolean))].slice(0, 80), sourceUrl: String(input.sourceUrl || '').slice(0, 500), observedAt: Math.max(0, Number(input.observedAt) || Date.now()), viewerCount: Math.max(0, Math.round(Number(input.viewerCount) || 0)), lastStatus: String(input.lastStatus || 'unknown').slice(0, 20), }; } const allowedEventTypes = new Set(['visit', 'usage', 'workspace', 'filter', 'tag', 'discovered', 'online', 'connection', 'quality', 'preview', 'quickview', 'chat_focus', 'account', 'player']); function cleanEvent(input) { if (!input || typeof input !== 'object') return null; const type = allowedEventTypes.has(input.type) ? input.type : ''; const at = Math.max(0, Number(input.at) || 0); if (!type || !at) return null; const id = validId(input.id) ? safeId(input.id) : ''; return { eventId: String(input.eventId || `${at}_${Math.random().toString(36).slice(2, 10)}`).slice(0, 80), type, id, at, endAt: Math.max(at, Number(input.endAt) || at), durationMs: Math.max(0, Number(input.durationMs) || 0), workspaceId: String(input.workspaceId || '').slice(0, 80), filter: String(input.filter || '').slice(0, 240), tags: [...new Set((Array.isArray(input.tags) ? input.tags : []).map(value => String(value || '').trim().slice(0, 80)).filter(Boolean))].slice(0, 40), status: String(input.status || '').slice(0, 30), quality: Math.max(0, Math.round(Number(input.quality) || 0)), detail: String(input.detail || '').slice(0, 300), country: String(input.country || '').replace(/[^\p{L}\p{N} .,'’()\/-]/gu, '').trim().slice(0, 40), location: String(input.location || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 100), gender: ['f', 'm', 'c', 't'].includes(input.gender) ? input.gender : 'unknown', age: clampInt(input.age, 0, 99, 0), subject: String(input.subject || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 240), isGaming: !!input.isGaming, isHd: !!input.isHd, }; } function cleanQualitySample(input) { if (!input || typeof input !== 'object') return null; const at = Math.max(0, Number(input.at) || Date.now()); const id = validId(input.id) ? safeId(input.id) : ''; return { sampleId: String(input.sampleId || `${at}_${Math.random().toString(36).slice(2, 9)}`).slice(0, 80), at, id, startupMs: Math.max(0, Number(input.startupMs) || 0), stallMs: Math.max(0, Number(input.stallMs) || 0), stalls: Math.max(0, Math.round(Number(input.stalls) || 0)), reconnects: Math.max(0, Math.round(Number(input.reconnects) || 0)), totalFrames: Math.max(0, Math.round(Number(input.totalFrames) || 0)), droppedFrames: Math.max(0, Math.round(Number(input.droppedFrames) || 0)), width: Math.max(0, Math.round(Number(input.width) || 0)), height: Math.max(0, Math.round(Number(input.height) || 0)), bufferSeconds: Math.max(0, Math.min(600, Number(input.bufferSeconds) || 0)), activeStreams: Math.max(0, Math.min(MAX_LAYOUT_SLOTS, Math.round(Number(input.activeStreams) || 0))), profile: ['eco', 'balanced', 'power'].includes(input.profile) ? input.profile : '', state: String(input.state || '').slice(0, 40), }; } function cleanAccountSnapshot(input) { if (!input || typeof input !== 'object' || !validId(input.username)) return null; const at = Math.max(0, Number(input.at) || Date.now()); const n = (value, min = 0) => Number.isFinite(Number(value)) ? Math.max(min, Math.round(Number(value))) : min; return { snapshotId: String(input.snapshotId || `${at}_${Math.random().toString(36).slice(2, 9)}`).slice(0, 80), at, username: safeId(input.username), tokenBalance: n(input.tokenBalance), tipsLastHour: n(input.tipsLastHour), votesUp: n(input.votesUp), votesDown: n(input.votesDown), satisfactionScore: Math.max(-1, Math.min(100, Number(input.satisfactionScore) || 0)), lastBroadcast: String(input.lastBroadcast || '').slice(0, 80), timeOnline: n(input.timeOnline, -1), followers: n(input.followers), viewers: n(input.viewers), registeredViewers: n(input.registeredViewers), tokenedViewers: n(input.tokenedViewers), }; } function cleanMeta(input) { const source = input && typeof input === 'object' ? input : {}; const usageDays = source.usageDays && typeof source.usageDays === 'object' && !Array.isArray(source.usageDays) ? Object.fromEntries(Object.entries(source.usageDays).slice(-450).map(([key, value]) => [String(key).slice(0, 10), Math.max(0, Number(value) || 0)])) : {}; const legacyRetention = clampInt(source.retentionDays, 7, 3650, 365); const retention = source.retentionPolicy && typeof source.retentionPolicy === 'object' ? source.retentionPolicy : {}; const cleanUntil = value => Number(value) === -1 ? -1 : Math.max(0, Number(value) || 0); return { startedAt: Math.max(0, Number(source.startedAt) || 0), totalUsageMs: Math.max(0, Number(source.totalUsageMs) || 0), usageDays, retentionDays: legacyRetention, retentionPolicy: { eventsDays: clampInt(retention.eventsDays, 7, 3650, legacyRetention), qualityDays: clampInt(retention.qualityDays, 7, 3650, Math.min(legacyRetention, 180)), accountDays: clampInt(retention.accountDays, 7, 3650, legacyRetention), rollupDays: clampInt(retention.rollupDays, 30, 3650, Math.max(legacyRetention, 3650)), }, privateSession: !!source.privateSession, privateSessionUntil: cleanUntil(source.privateSessionUntil), historyPausedUntil: cleanUntil(source.historyPausedUntil), }; } function timedStateActive(until) { return Number(until) === -1 || Number(until) > Date.now(); } function refreshTimedStates() { let changed = false; if (meta.privateSession && meta.privateSessionUntil > 0 && meta.privateSessionUntil <= Date.now()) { meta.privateSession = false; meta.privateSessionUntil = 0; changed = true; } if (meta.historyPausedUntil > 0 && meta.historyPausedUntil <= Date.now()) { meta.historyPausedUntil = 0; changed = true; } if (changed) { scheduleTimedStateRefresh(); flushMeta(); emit(); if (enabled && provider && !meta.privateSession && !timedStateActive(meta.historyPausedUntil)) start(provider); } return changed; } function scheduleTimedStateRefresh() { clearTimeout(timedStateTimer); timedStateTimer = 0; const upcoming = [meta.privateSession ? meta.privateSessionUntil : 0, meta.historyPausedUntil].filter(value => Number(value) > Date.now()).sort((a, b) => a - b)[0]; if (upcoming) timedStateTimer = setTimeout(() => { timedStateTimer = 0; refreshTimedStates(); }, Math.max(50, upcoming - Date.now() + 20)); } function privateSessionActive() { refreshTimedStates(); return !!meta.privateSession && (meta.privateSessionUntil === 0 || timedStateActive(meta.privateSessionUntil)); } function historyPaused() { refreshTimedStates(); return timedStateActive(meta.historyPausedUntil); } function getOrCreate(id) { id = safeId(id); if (!validId(id)) return null; let model = models.get(id); if (!model) { model = emptyModel(id); models.set(id, model); dirtyModels.add(id); } return model; } function addBucketTime(model, ms, now) { if (!model || ms <= 0) return; const date = new Date(now); const hourIndex = date.getDay() * 24 + date.getHours(); model.hours[hourIndex] = (Number(model.hours[hourIndex]) || 0) + ms; const key = date.toISOString().slice(0, 10); model.days[key] = (Number(model.days[key]) || 0) + ms; const keys = Object.keys(model.days).sort(); keys.slice(0, Math.max(0, keys.length - 450)).forEach(k => delete model.days[k]); } function updateRoomMetadata(item) { const model = getOrCreate(item?.id); if (!model) return; const room = item.room || {}; if (room.lastStatus) model.lastStatus = room.lastStatus; if (Number.isFinite(Number(room.viewerCount))) model.viewerCount = Math.max(0, Math.round(Number(room.viewerCount))); if (room.lastStatus === 'online') model.lastOnlineAt = Math.max(model.lastOnlineAt || 0, Number(room.lastSeenOnline) || Date.now()); if (Array.isArray(room.tags)) model.tags = [...new Set([...model.tags, ...room.tags.map(v => String(v).trim()).filter(Boolean)])].slice(0, 80); dirtyModels.add(model.id); } function observeModel(input, options = {}) { const id = safeId(input?.id); if (!validId(id) || !enabled || privateSessionActive()) return false; const existed = models.has(id); const model = getOrCreate(id); if (!model) return false; const source = input?.room && typeof input.room === 'object' ? input.room : input || {}; const observedAt = Math.max(0, Number(input?.observedAt) || Date.now()); const rawStatus = String(source.lastStatus || source.currentShow || source.status || '').toLocaleLowerCase(); const status = rawStatus === 'public' ? 'online' : rawStatus === 'hidden' ? 'private' : rawStatus; let changed = !existed; if (['online', 'offline', 'private'].includes(status) && model.lastStatus !== status) { model.lastStatus = status; changed = true; } const viewers = Number(source.viewerCount ?? source.numUsers ?? source.num_users); if (Number.isFinite(viewers) && viewers >= 0 && model.viewerCount !== Math.round(viewers)) { model.viewerCount = Math.round(viewers); changed = true; } if (status === 'online') { const liveAt = Math.max(0, Number(source.lastSeenOnline || observedAt) || Date.now()); if (liveAt > model.lastOnlineAt) { model.lastOnlineAt = liveAt; changed = true; } const sampleAt = observedAt; if (!model.lastOnlineSampleAt || sampleAt - model.lastOnlineSampleAt >= 300000) { const sampleDate = new Date(sampleAt); const sampleIndex = sampleDate.getDay() * 24 + sampleDate.getHours(); model.onlineHours[sampleIndex] = (Number(model.onlineHours[sampleIndex]) || 0) + 1; model.onlineSamples = (Number(model.onlineSamples) || 0) + 1; model.lastOnlineSampleAt = sampleAt; changed = true; } } const tags = [...new Set((Array.isArray(source.tags) ? source.tags : []).map(value => String(value || '').trim().toLocaleLowerCase().slice(0, 80)).filter(Boolean))].slice(0, 80); const mergedTags = [...new Set([...(model.tags || []), ...tags])].slice(0, 80); if (JSON.stringify(mergedTags) !== JSON.stringify(model.tags || [])) { model.tags = mergedTags; changed = true; } if (!model.firstObservedAt) { model.firstObservedAt = observedAt; changed = true; } if (!model.lastObservedAt || observedAt - model.lastObservedAt >= 60000) { model.appearances = Math.min(1000000000, (Number(model.appearances) || 0) + 1); changed = true; } if (observedAt > (Number(model.lastObservedAt) || 0)) { model.lastObservedAt = observedAt; changed = true; } const sourceName = String(input?.source || '').replace(/[^a-zA-Z0-9_\/-]/gu, '').slice(0, 60); if (sourceName && !model.sources.includes(sourceName)) { model.sources = [...model.sources, sourceName].slice(-16); changed = true; } const subject = String(source.subject || source.roomSubject || source.room_subject || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 240); if (subject && !model.subjects.includes(subject)) { model.subjects = [...model.subjects, subject].slice(-8); changed = true; } const previewUrl = cleanPreviewUrl(source.imageUrl || source.img || input?.imageUrl, id); if (previewUrl && !model.previewUrls.includes(previewUrl)) { model.previewUrls = [...model.previewUrls, previewUrl].slice(-4); changed = true; } const publicFields = { country: String(source.country || '').replace(/[^\p{L}\p{N} .,'’()\/-]/gu, '').trim().slice(0, 40), location: String(source.location || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 100), gender: ['f', 'm', 'c', 't'].includes(source.gender) ? source.gender : '', age: clampInt(source.age ?? source.displayAge ?? source.display_age, 0, 99, 0), followers: Math.max(0, Math.round(Number(source.followers ?? source.numFollowers ?? source.num_followers) || 0)), secondsOnline: Math.max(0, Math.min(604800, Math.round(Number(source.secondsOnline ?? source.seconds_online) || 0))), }; for (const [key, value] of Object.entries(publicFields)) if (value && model[key] !== value) { model[key] = value; changed = true; } for (const [key, raw] of [['isHd', source.isHd ?? source.is_hd], ['isNew', source.isNew ?? source.is_new], ['isGaming', source.isGaming ?? source.is_gaming]]) if (raw != null && model[key] !== !!raw) { model[key] = !!raw; changed = true; } if (changed) { dirtyModels.add(id); if (!options.silent) emit(); } return changed; } // Bulk catalog recovery used after an update or a temporary catalog // request failure. Updating tens of thousands of entries must emit and // flush once, not once per model, otherwise the hub becomes unresponsive. async function observeModelsBatch(items = []) { if (!Array.isArray(items) || !items.length || !enabled || privateSessionActive()) return 0; let changed = 0; let sliceStartedAt = performance.now(); let playbackActive = playbackIsActive(); for (let index = 0; index < items.length; index += 1) { if (observeModel(items[index], { silent: true })) changed += 1; // Collection gives playback frequent opportunities while still // completing every observation and emitting one logical update. if (index && (playbackActive ? index % 16 === 0 && performance.now() - sliceStartedAt >= 3 : index % 256 === 0)) { await new Promise(resolve => setTimeout(resolve, 0)); sliceStartedAt = performance.now(); playbackActive = playbackIsActive(); } } if (changed) emit(); return changed; } async function persistEvent(event) { if (!db || !event) return; try { const tx = db.transaction('events', 'readwrite'); tx.objectStore('events').put(event); await transactionDone(tx); } catch (_) {} } function recordEvent(input, options = {}) { if (!enabled || !ready || privateSessionActive() || historyPaused()) return null; const id = validId(input?.id) ? safeId(input.id) : ''; const model = id ? models.get(id) : null; const event = cleanEvent({ ...(model ? { tags: model.tags || [], country: model.country || '', location: model.location || '', gender: model.gender || 'unknown', age: model.age || 0, subject: model.subjects?.at?.(-1) || '', isGaming: !!model.isGaming, isHd: !!model.isHd, } : {}), ...input, eventId: input?.eventId || `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`, }); if (!event) return null; if (events.length < MAX_EVENTS || event.at >= Number(events[0]?.at || 0)) { if (!events.length || event.at >= Number(events[events.length - 1]?.at || 0)) events.push(event); else { // Visits can close with their session's start time, so append is not // always chronological. Insert only that out-of-order event instead // of sorting the full 100k-event history after every interaction. let low = 0; let high = events.length; while (low < high) { const middle = (low + high) >>> 1; if (Number(events[middle]?.at || 0) <= event.at) low = middle + 1; else high = middle; } events.splice(low, 0, event); } if (events.length > MAX_EVENTS) events.shift(); } if (options.persist !== false) persistEvent(event); if (!options.silent) emit(); return clone(event); } async function recordEventsBatch(inputs = []) { if (!Array.isArray(inputs) || !inputs.length) return 0; let recorded = 0; for (let offset = 0; offset < inputs.length; offset += 256) { const chunk = []; for (const input of inputs.slice(offset, offset + 256)) { const event = recordEvent(input, { silent: true, persist: false }); if (event) { chunk.push(event); recorded += 1; } } if (db && chunk.length) { try { const tx = db.transaction('events', 'readwrite'); const target = tx.objectStore('events'); chunk.forEach(event => target.put(event)); await transactionDone(tx); } catch (_) {} } if (offset + 256 < inputs.length) await new Promise(resolve => setTimeout(resolve, 0)); } if (recorded) emit(); return recorded; } function closeModelSession(id, session, now = Date.now()) { if (!session?.counted || privateSessionActive() || historyPaused()) return; const model = models.get(id); recordEvent({ type: 'visit', id, at: session.startedAt, endAt: now, durationMs: session.elapsedMs, tags: model?.tags || [], status: model?.lastStatus || '' }); } function closeUsageSession(now = Date.now()) { if (!usageSession || privateSessionActive() || historyPaused()) { usageSession = null; return; } if (usageSession.elapsedMs >= MIN_SESSION_MS) recordEvent({ type: 'usage', at: usageSession.startedAt, endAt: now, durationMs: usageSession.elapsedMs }); usageSession = null; } function addUsageTime(ms, now) { if (ms <= 0 || privateSessionActive() || historyPaused()) return; meta.totalUsageMs += ms; const key = new Date(now).toISOString().slice(0, 10); meta.usageDays[key] = (Number(meta.usageDays[key]) || 0) + ms; const keys = Object.keys(meta.usageDays).sort(); keys.slice(0, Math.max(0, keys.length - 450)).forEach(day => delete meta.usageDays[day]); } async function pruneRetention() { const policy = meta.retentionPolicy || { eventsDays: meta.retentionDays, qualityDays: meta.retentionDays, accountDays: meta.retentionDays, rollupDays: 3650 }; const eventCutoff = Date.now() - policy.eventsDays * 86400000; const qualityCutoff = Date.now() - policy.qualityDays * 86400000; const accountCutoff = Date.now() - policy.accountDays * 86400000; const rollupCutoff = Date.now() - policy.rollupDays * 86400000; const staleEvents = events.filter(event => event.at < eventCutoff); const staleIds = staleEvents.map(event => event.eventId); if (staleEvents.length) mergeIntoRollups(staleEvents); if (staleIds.length) events.splice(0, events.length, ...events.filter(event => event.at >= eventCutoff)); if (db && staleIds.length) { try { const tx = db.transaction(['events', 'dailyRollups'], 'readwrite'); const store = tx.objectStore('events'); staleIds.forEach(id => store.delete(id)); staleEvents.forEach(event => tx.objectStore('dailyRollups').put(dailyRollups.get(new Date(event.at).toISOString().slice(0, 10)))); await transactionDone(tx); } catch (_) {} } const staleQuality = qualitySamples.filter(item => item.at < qualityCutoff).map(item => item.sampleId); const staleAccounts = accountSnapshots.filter(item => item.at < accountCutoff).map(item => item.snapshotId); const staleRollups = [...dailyRollups.values()].filter(item => new Date(`${item.day}T23:59:59`).getTime() < rollupCutoff).map(item => item.day); if (staleQuality.length) qualitySamples.splice(0, qualitySamples.length, ...qualitySamples.filter(item => item.at >= qualityCutoff)); if (staleAccounts.length) accountSnapshots.splice(0, accountSnapshots.length, ...accountSnapshots.filter(item => item.at >= accountCutoff)); staleRollups.forEach(day => dailyRollups.delete(day)); if (db && (staleQuality.length || staleAccounts.length || staleRollups.length)) { try { const names = []; if (db.objectStoreNames.contains('qualitySamples')) names.push('qualitySamples'); if (db.objectStoreNames.contains('accountSnapshots')) names.push('accountSnapshots'); if (db.objectStoreNames.contains('dailyRollups')) names.push('dailyRollups'); if (names.length) { const tx = db.transaction(names, 'readwrite'); staleQuality.forEach(id => tx.objectStore('qualitySamples')?.delete(id)); staleAccounts.forEach(id => tx.objectStore('accountSnapshots')?.delete(id)); staleRollups.forEach(day => tx.objectStore('dailyRollups')?.delete(day)); await transactionDone(tx); } } catch (_) {} } return staleIds.length + staleQuality.length + staleAccounts.length + staleRollups.length; } function sample() { if (!enabled || !ready || privateSessionActive() || historyPaused() || document.hidden || typeof provider !== 'function') return; let items = []; try { items = provider() || []; } catch (_) { items = []; } const now = Date.now(); const active = new Set(); (Array.isArray(items) ? items : []).forEach(item => { const id = safeId(item?.id); const video = item?.video; if (!validId(id) || !video || video.paused || video.ended || Number(video.readyState) < 2) return; let visible = false; try { const rect = video.getBoundingClientRect(); visible = rect.width > 2 && rect.height > 2 && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth; } catch (_) {} if (!visible) return; active.add(id); updateRoomMetadata(item); let session = sessions.get(id); if (!session) { session = { startedAt: now, lastSampleAt: now, elapsedMs: 0, committedMs: 0, counted: false }; sessions.set(id, session); } const gap = now - session.lastSampleAt; const delta = gap > 20000 ? 0 : Math.max(0, Math.min(15000, gap)); session.lastSampleAt = now; session.elapsedMs += delta; const model = getOrCreate(id); if (!model) return; if (!session.counted && session.elapsedMs >= MIN_SESSION_MS) { session.counted = true; model.sessions += 1; model.totalMs += session.elapsedMs; model.longestSessionMs = Math.max(model.longestSessionMs, session.elapsedMs); model.firstWatchedAt = model.firstWatchedAt || session.startedAt; model.lastWatchedAt = now; addBucketTime(model, session.elapsedMs, now); session.committedMs = session.elapsedMs; } else if (session.counted) { const add = Math.max(0, session.elapsedMs - session.committedMs); if (add) { model.totalMs += add; model.lastWatchedAt = now; model.longestSessionMs = Math.max(model.longestSessionMs, session.elapsedMs); addBucketTime(model, add, now); session.committedMs = session.elapsedMs; } } dirtyModels.add(id); }); for (const [id, session] of sessions) if (!active.has(id)) { closeModelSession(id, session, now); sessions.delete(id); } if (active.size) { if (!usageSession) usageSession = { startedAt: now, lastSampleAt: now, elapsedMs: 0 }; const gap = now - usageSession.lastSampleAt; const delta = gap > 20000 ? 0 : Math.max(0, Math.min(15000, gap)); usageSession.lastSampleAt = now; usageSession.elapsedMs += delta; addUsageTime(delta, now); } else closeUsageSession(now); if (dirtyModels.size) { flush(); emit(); } if (active.size) flushMeta(); } function mergeHydratedModel(stored) { const current = models.get(stored.id); if (!current) { models.set(stored.id, stored); return; } const currentIsNewer = Number(current.lastObservedAt || 0) >= Number(stored.lastObservedAt || 0); const merged = cleanModel({ ...stored, ...(currentIsNewer ? { lastStatus: current.lastStatus, lastOnlineAt: Math.max(Number(stored.lastOnlineAt) || 0, Number(current.lastOnlineAt) || 0), viewerCount: current.viewerCount, country: current.country || stored.country, location: current.location || stored.location, gender: current.gender !== 'unknown' ? current.gender : stored.gender, age: current.age || stored.age, followers: current.followers || stored.followers, secondsOnline: current.secondsOnline || stored.secondsOnline, isHd: current.isHd, isNew: current.isNew, isGaming: current.isGaming, } : {}), firstObservedAt: stored.firstObservedAt && current.firstObservedAt ? Math.min(stored.firstObservedAt, current.firstObservedAt) : (stored.firstObservedAt || current.firstObservedAt || 0), lastObservedAt: Math.max(Number(stored.lastObservedAt) || 0, Number(current.lastObservedAt) || 0), appearances: Math.max(Number(stored.appearances) || 0, Number(current.appearances) || 0), onlineSamples: Math.max(Number(stored.onlineSamples) || 0, Number(current.onlineSamples) || 0), lastOnlineSampleAt: Math.max(Number(stored.lastOnlineSampleAt) || 0, Number(current.lastOnlineSampleAt) || 0), tags: [...new Set([...(stored.tags || []), ...(current.tags || [])])], sources: [...new Set([...(stored.sources || []), ...(current.sources || [])])], subjects: [...new Set([...(stored.subjects || []), ...(current.subjects || [])])], previewUrls: [...new Set([...(stored.previewUrls || []), ...(current.previewUrls || [])])], }); if (merged) { models.set(stored.id, merged); dirtyModels.add(stored.id); } } async function hydrate() { if (!('indexedDB' in window)) { resolveDatabaseReady(false); ready = true; meta.startedAt ||= Date.now(); return false; } try { db = await new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const database = req.result; if (!database.objectStoreNames.contains('models')) database.createObjectStore('models', { keyPath: 'id' }); if (!database.objectStoreNames.contains('profiles')) database.createObjectStore('profiles', { keyPath: 'id' }); if (!database.objectStoreNames.contains('meta')) database.createObjectStore('meta', { keyPath: 'key' }); if (!database.objectStoreNames.contains('events')) { const eventStore = database.createObjectStore('events', { keyPath: 'eventId' }); eventStore.createIndex('at', 'at', { unique: false }); eventStore.createIndex('type', 'type', { unique: false }); eventStore.createIndex('model', 'id', { unique: false }); } if (!database.objectStoreNames.contains('qualitySamples')) { const qualityStore = database.createObjectStore('qualitySamples', { keyPath: 'sampleId' }); qualityStore.createIndex('at', 'at', { unique: false }); qualityStore.createIndex('model', 'id', { unique: false }); } if (!database.objectStoreNames.contains('accountSnapshots')) { const accountStore = database.createObjectStore('accountSnapshots', { keyPath: 'snapshotId' }); accountStore.createIndex('at', 'at', { unique: false }); accountStore.createIndex('username', 'username', { unique: false }); } if (!database.objectStoreNames.contains('dailyRollups')) database.createObjectStore('dailyRollups', { keyPath: 'day' }); if (!database.objectStoreNames.contains('backups')) { const backupStore = database.createObjectStore('backups', { keyPath: 'backupId' }); backupStore.createIndex('at', 'at', { unique: false }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error || new Error('IndexedDB unavailable')); }); const metaTx = db.transaction('meta', 'readonly'); const storedMeta = await requestResult(metaTx.objectStore('meta').get('main')); meta = cleanMeta(storedMeta); scheduleTimedStateRefresh(); // One requested profile can be read before the complete model/event // archive hydrates. Privacy metadata is known before this barrier opens. resolveDatabaseReady(true); await readStoreProgressively('models', value => { const model = cleanModel(value); if (model) mergeHydratedModel(model); }); await readStoreProgressively('profiles', value => { const profile = cleanProfile(value); if (profile && !dirtyProfiles.has(profile.id) && Number(profiles.get(profile.id)?.observedAt || 0) <= profile.observedAt) profiles.set(profile.id, profile); }); const newestEvents = []; await readStoreProgressively('events', value => { const event = cleanEvent(value); if (event) newestEvents.push(event); }, { index: 'at', direction: 'prev', limit: MAX_EVENTS }); newestEvents.reverse().forEach(event => events.push(event)); const newestQuality = []; await readStoreProgressively('qualitySamples', value => { const item = cleanQualitySample(value); if (item) newestQuality.push(item); }, { index: 'at', direction: 'prev', limit: MAX_QUALITY_SAMPLES }); newestQuality.reverse().forEach(item => qualitySamples.push(item)); const newestAccounts = []; await readStoreProgressively('accountSnapshots', value => { const item = cleanAccountSnapshot(value); if (item) newestAccounts.push(item); }, { index: 'at', direction: 'prev', limit: MAX_ACCOUNT_SNAPSHOTS }); newestAccounts.reverse().forEach(item => accountSnapshots.push(item)); await readStoreProgressively('dailyRollups', value => { const item = cleanDailyRollup(value); if (item) dailyRollups.set(item.day, item); }); if (!meta.startedAt) { meta.startedAt = Date.now(); await flushMeta(); } ready = true; await flush(); emit(); // Retention maintenance is not part of interactive startup. Run it in // an idle window after the complete state is available. const maintain = () => pruneRetention().catch(() => {}); try { if ('requestIdleCallback' in window) requestIdleCallback(maintain, { timeout: 120000 }); else setTimeout(maintain, 30000); } catch (_) { setTimeout(maintain, 30000); } return true; } catch (err) { resolveDatabaseReady(false); console.warn('[RoomGrid] analytics unavailable', err); ready = true; meta.startedAt ||= Date.now(); return false; } } function init(initialEnabled = true) { enabled = initialEnabled !== false; if (ready) return Promise.resolve(true); if (initPromise) return initPromise; const pending = hydrate(); initPromise = pending.finally(() => { if (initPromise === wrapped) initPromise = null; }); const wrapped = initPromise; return wrapped; } function whenReady() { return ready ? Promise.resolve(true) : (initPromise || init(enabled)); } async function flushMeta() { if (!db) return; try { const tx = db.transaction('meta', 'readwrite'); tx.objectStore('meta').put({ key: 'main', ...meta }); await transactionDone(tx); } catch (_) {} } async function flush() { if (flush.pending) { await flush.pending; if (dirtyModels.size || dirtyProfiles.size) return flush(); return; } if (!db || !ready || (!dirtyModels.size && !dirtyProfiles.size)) return; flush.pending = (async () => { const modelIds = [...dirtyModels]; const profileIds = [...dirtyProfiles]; dirtyModels.clear(); dirtyProfiles.clear(); try { const tx = db.transaction(['models', 'profiles'], 'readwrite'); const done = transactionDone(tx); const ms = tx.objectStore('models'); const ps = tx.objectStore('profiles'); try { modelIds.forEach(id => { const model = models.get(id); if (model) ms.put(cleanModel(model)); }); profileIds.forEach(id => { const profile = profiles.get(id); if (profile) ps.put(cleanProfile(profile)); }); } catch (error) { tx.abort(); await done.catch(() => {}); throw error; } await done; } catch (_) { modelIds.forEach(id => dirtyModels.add(id)); profileIds.forEach(id => dirtyProfiles.add(id)); } })(); try { await flush.pending; } finally { flush.pending = null; } } function start(nextProvider) { provider = nextProvider; if (timer || !enabled) return; timer = setInterval(sample, 5000); visibilityHandler = () => { if (!document.hidden) return; const now = Date.now(); for (const [id, session] of sessions) closeModelSession(id, session, now); sessions.clear(); closeUsageSession(now); }; document.addEventListener('visibilitychange', visibilityHandler); sample(); } function stop() { if (timer) { clearInterval(timer); timer = 0; } if (visibilityHandler) { document.removeEventListener('visibilitychange', visibilityHandler); visibilityHandler = null; } const now = Date.now(); for (const [id, session] of sessions) closeModelSession(id, session, now); sessions.clear(); closeUsageSession(now); } async function setEnabled(value) { enabled = value !== false; if (!enabled) stop(); else if (provider) start(provider); await flushMeta(); } async function setPrivateSession(value, minutes = 0) { const next = !!value; if (next && !privateSessionActive()) stop(); meta.privateSession = next; meta.privateSessionUntil = next ? (Number(minutes) > 0 ? Date.now() + clampInt(minutes, 1, 10080, 60) * 60000 : -1) : 0; scheduleTimedStateRefresh(); if (!next && enabled && provider) start(provider); await flushMeta(); emit(); } async function setHistoryPause(minutes = 0) { const value = Number(minutes); meta.historyPausedUntil = value < 0 ? -1 : value > 0 ? Date.now() + clampInt(value, 1, 10080, 60) * 60000 : 0; scheduleTimedStateRefresh(); if (historyPaused()) stop(); else if (enabled && provider && !privateSessionActive()) start(provider); await flushMeta(); emit(); } async function setRetentionDays(value) { meta.retentionDays = clampInt(value, 7, 3650, 365); meta.retentionPolicy = { eventsDays: meta.retentionDays, qualityDays: meta.retentionDays, accountDays: meta.retentionDays, rollupDays: Math.max(meta.retentionDays, Number(meta.retentionPolicy?.rollupDays) || 3650) }; await pruneRetention(); await flushMeta(); emit(); } async function setRetentionPolicy(input = {}) { const source = input && typeof input === 'object' ? input : {}; meta.retentionPolicy = { eventsDays: clampInt(source.eventsDays, 7, 3650, meta.retentionPolicy?.eventsDays || 365), qualityDays: clampInt(source.qualityDays, 7, 3650, meta.retentionPolicy?.qualityDays || 180), accountDays: clampInt(source.accountDays, 7, 3650, meta.retentionPolicy?.accountDays || 365), rollupDays: clampInt(source.rollupDays, 30, 3650, meta.retentionPolicy?.rollupDays || 3650), }; meta.retentionDays = meta.retentionPolicy.eventsDays; await pruneRetention(); await flushMeta(); emit(); } async function getModels() { await flush(); return [...models.values()].map(clone); } function getModelsSync() { return [...models.values()]; } function getModelSync(id) { return models.get(safeId(id)) || null; } async function getModel(id) { await flush(); return clone(models.get(safeId(id)) || null); } function getProfileSync(id) { return profiles.get(safeId(id)) || null; } async function getProfile(id) { id = safeId(id); if (!validId(id) || privateSessionActive()) return null; if (profiles.has(id)) return profiles.get(id); if (!initPromise && !ready) return null; if (profileReads.has(id)) return profileReads.get(id); const pending = (async () => { if (!await databaseReady || !db || privateSessionActive()) return null; try { const row = await requestResult(db.transaction('profiles', 'readonly').objectStore('profiles').get(id)); return privateSessionActive() ? null : profiles.get(id) || cleanProfile(row); } catch (_) { return null; } })(); profileReads.set(id, pending); try { return await pending; } finally { if (profileReads.get(id) === pending) profileReads.delete(id); } } function getEventsSync(options = {}) { const type = String(options.type || ''); const id = options.id ? safeId(options.id) : ''; const from = Math.max(0, Number(options.from) || 0); const to = Math.max(0, Number(options.to) || Number.MAX_SAFE_INTEGER); const limit = clampInt(options.limit, 1, MAX_EVENTS, 500); return events.filter(event => (!type || event.type === type) && (!id || event.id === id) && event.at >= from && event.at <= to).slice(-limit).reverse(); } async function getEvents(options = {}) { return getEventsSync(options).map(clone); } async function recordQualitySample(input) { if (!enabled || !ready || privateSessionActive() || historyPaused()) return null; const item = cleanQualitySample(input); if (!item) return null; qualitySamples.push(item); while (qualitySamples.length > MAX_QUALITY_SAMPLES) qualitySamples.shift(); if (db) { try { const tx = db.transaction('qualitySamples', 'readwrite'); tx.objectStore('qualitySamples').put(item); await transactionDone(tx); } catch (_) {} } emit(); return clone(item); } function getQualitySamplesSync(options = {}) { const from = Math.max(0, Number(options.from) || 0); const to = Math.max(from, Number(options.to) || Number.MAX_SAFE_INTEGER); const id = options.id ? safeId(options.id) : ''; const limit = clampInt(options.limit, 1, MAX_QUALITY_SAMPLES, 5000); return qualitySamples.filter(item => item.at >= from && item.at <= to && (!id || item.id === id)).slice(-limit); } async function getQualitySamples(options = {}) { return getQualitySamplesSync(options).map(clone); } function getDailyRollups() { return [...dailyRollups.values()].sort((a, b) => a.day.localeCompare(b.day)).map(clone); } async function recordAccountSnapshot(input) { if (!enabled || !ready || privateSessionActive() || historyPaused()) return null; const item = cleanAccountSnapshot(input); if (!item) return null; accountSnapshots.push(item); while (accountSnapshots.length > MAX_ACCOUNT_SNAPSHOTS) accountSnapshots.shift(); if (db) { try { const tx = db.transaction('accountSnapshots', 'readwrite'); tx.objectStore('accountSnapshots').put(item); await transactionDone(tx); } catch (_) {} } emit(); return clone(item); } function getAccountSnapshotsSync(options = {}) { const from = Math.max(0, Number(options.from) || 0); const to = Math.max(from, Number(options.to) || Number.MAX_SAFE_INTEGER); const username = options.username ? safeId(options.username) : ''; const limit = clampInt(options.limit, 1, MAX_ACCOUNT_SNAPSHOTS, 5000); return accountSnapshots.filter(item => item.at >= from && item.at <= to && (!username || item.username === username)).slice(-limit); } async function getAccountSnapshots(options = {}) { return getAccountSnapshotsSync(options).map(clone); } async function clearAccountSnapshots(username = '') { const target = username ? safeId(username) : ''; const removed = accountSnapshots.filter(item => !target || item.username === target); const keep = accountSnapshots.filter(item => target && item.username !== target); accountSnapshots.splice(0, accountSnapshots.length, ...keep); if (db && removed.length) { try { const tx = db.transaction('accountSnapshots', 'readwrite'); const store = tx.objectStore('accountSnapshots'); removed.forEach(item => store.delete(item.snapshotId)); await transactionDone(tx); } catch (_) {} } emit(); return removed.length; } function getUsageSummary() { refreshTimedStates(); return clone({ startedAt: Number(meta.startedAt) || 0, totalUsageMs: meta.totalUsageMs || 0, usageDays: meta.usageDays || {}, retentionDays: meta.retentionDays, retentionPolicy: meta.retentionPolicy, privateSession: privateSessionActive(), privateSessionUntil: meta.privateSessionUntil, historyPaused: historyPaused(), historyPausedUntil: meta.historyPausedUntil, }); } async function upsertProfile(input) { const profile = cleanProfile(input); if (!profile) return false; profiles.set(profile.id, profile); dirtyProfiles.add(profile.id); const model = getOrCreate(profile.id); if (model) { model.tags = [...new Set([...model.tags, ...profile.tags])].slice(0, 80); dirtyModels.add(profile.id); } await flush(); emit(); return true; } async function exportData() { await flush(); return { schema: 4, kind: 'roomgrid-statistics', sensitive: true, exportedAt: Date.now(), meta: clone(meta), startedAt: meta.startedAt || 0, provenance: { models: 'local-observation-and-user-activity', profiles: 'user-requested-public-profile', events: 'local-roomgrid-interaction', qualitySamples: 'local-browser-player', accountSnapshots: 'optional-official-stats-api', dailyRollups: 'local-aggregation' }, models: [...models.values()].map(clone), profiles: [...profiles.values()].map(clone), events: events.map(clone), qualitySamples: qualitySamples.map(clone), accountSnapshots: accountSnapshots.map(clone), dailyRollups: getDailyRollups(), }; } function dataInventory() { let earliestAt = 0, latestAt = 0; for (const list of [events, qualitySamples, accountSnapshots]) for (const item of list) { const at = Number(item.at) || 0; if (at) { earliestAt = earliestAt ? Math.min(earliestAt, at) : at; latestAt = Math.max(latestAt, at); } } return { models: models.size, profiles: profiles.size, events: events.length, qualitySamples: qualitySamples.length, accountSnapshots: accountSnapshots.length, dailyRollups: dailyRollups.size, earliestAt, latestAt, estimatedBytes: inventoryEstimate?.estimatedBytes ?? null, estimatePending: inventoryEstimate?.revision !== dataRevision, sources: [ { id: 'models', count: models.size, source: 'local-observation-and-user-activity', sensitive: true }, { id: 'profiles', count: profiles.size, source: 'user-requested-public-profile', sensitive: true }, { id: 'events', count: events.length, source: 'local-roomgrid-interaction', sensitive: true }, { id: 'qualitySamples', count: qualitySamples.length, source: 'local-browser-player', sensitive: false }, { id: 'accountSnapshots', count: accountSnapshots.length, source: 'optional-official-stats-api', sensitive: true }, { id: 'dailyRollups', count: dailyRollups.size, source: 'local-aggregation', sensitive: true }, ], }; } // Exact UTF-16 JSON estimate, but never stringify the entire history on // the input thread. Counts remain immediately available; only storage // views request this sliced estimate. No persisted data is modified. async function measureDataInventory() { if (inventoryEstimate?.revision === dataRevision) return dataInventory(); if (inventoryEstimatePromise) return inventoryEstimatePromise; inventoryEstimatePromise = (async () => { const revision = dataRevision; const sections = { models: models.values(), profiles: profiles.values(), events, qualitySamples, accountSnapshots, dailyRollups: dailyRollups.values() }; let length = JSON.stringify({ meta }).length; let sliceAt = performance.now(); for (const [key, values] of Object.entries(sections)) { length += JSON.stringify(key).length + 4; // comma, colon and [] let first = true; for (const value of values) { length += (first ? 0 : 1) + JSON.stringify(value).length; first = false; if (performance.now() - sliceAt >= 8) { await new Promise(resolve => setTimeout(resolve, 0)); sliceAt = performance.now(); } } } inventoryEstimate = { revision, estimatedBytes: length * 2 }; return dataInventory(); })(); try { return await inventoryEstimatePromise; } finally { inventoryEstimatePromise = null; } } function inspectImport(input) { const source = input?.kind === 'roomgrid-complete-backup' ? input.statistics : input; if (!source || typeof source !== 'object') throw new Error('invalid statistics file'); const arrays = ['models', 'profiles', 'events', 'qualitySamples', 'accountSnapshots', 'dailyRollups']; if (!arrays.some(key => Array.isArray(source[key]))) throw new Error('invalid statistics file'); if ((source.models?.length || 0) > ROOMGRID_DATA_LIMITS.modelRecords || (source.events?.length || 0) > MAX_EVENTS || (source.qualitySamples?.length || 0) > MAX_QUALITY_SAMPLES || (source.accountSnapshots?.length || 0) > MAX_ACCOUNT_SNAPSHOTS) throw new Error('statistics file exceeds limits'); const clean = { models: (source.models || []).map(cleanModel).filter(Boolean), profiles: (source.profiles || []).map(cleanProfile).filter(Boolean), events: (source.events || []).slice(-MAX_EVENTS).map(cleanEvent).filter(Boolean), qualitySamples: (source.qualitySamples || []).slice(-MAX_QUALITY_SAMPLES).map(cleanQualitySample).filter(Boolean), accountSnapshots: (source.accountSnapshots || []).slice(-MAX_ACCOUNT_SNAPSHOTS).map(cleanAccountSnapshot).filter(Boolean), dailyRollups: (source.dailyRollups || []).slice(-4000).map(cleanDailyRollup).filter(Boolean), }; const currentKeys = { models: new Set(models.keys()), profiles: new Set(profiles.keys()), events: new Set(events.map(item => item.eventId)), qualitySamples: new Set(qualitySamples.map(item => item.sampleId)), accountSnapshots: new Set(accountSnapshots.map(item => item.snapshotId)), dailyRollups: new Set(dailyRollups.keys()) }; const keyOf = { models: item => item.id, profiles: item => item.id, events: item => item.eventId, qualitySamples: item => item.sampleId, accountSnapshots: item => item.snapshotId, dailyRollups: item => item.day }; const counts = Object.fromEntries(arrays.map(key => [key, clean[key].length])); const conflicts = Object.fromEntries(arrays.map(key => [key, clean[key].filter(item => currentKeys[key].has(keyOf[key](item))).length])); const times = [...clean.events.map(item => item.at), ...clean.qualitySamples.map(item => item.at), ...clean.accountSnapshots.map(item => item.at)].filter(Boolean).sort((a, b) => a - b); return { source, clean, sections: arrays.filter(key => clean[key].length), counts, conflicts, sensitive: source.sensitive !== false, schema: Number(source.schema) || 0, earliestAt: times[0] || 0, latestAt: times.at(-1) || 0, provenance: source.provenance && typeof source.provenance === 'object' ? clone(source.provenance) : {} }; } const mergeNumericMax = (a, b) => Math.max(Number(a) || 0, Number(b) || 0); function mergeModel(current, incoming) { if (!current) return incoming; const recent = Number(incoming.lastObservedAt || incoming.lastWatchedAt || 0) >= Number(current.lastObservedAt || current.lastWatchedAt || 0) ? incoming : current; const out = { ...current, ...recent }; ['totalMs','sessions','lastWatchedAt','longestSessionMs','lastOnlineAt','viewerCount','onlineSamples','lastOnlineSampleAt','lastObservedAt','appearances','followers','secondsOnline'].forEach(key => { out[key] = mergeNumericMax(current[key], incoming[key]); }); const firstWatched = [current.firstWatchedAt, incoming.firstWatchedAt].filter(Boolean); const firstObserved = [current.firstObservedAt, incoming.firstObservedAt].filter(Boolean); out.firstWatchedAt = firstWatched.length ? Math.min(...firstWatched) : 0; out.firstObservedAt = firstObserved.length ? Math.min(...firstObserved) : 0; out.tags = [...new Set([...(current.tags || []), ...(incoming.tags || [])])].slice(0, 80); out.sources = [...new Set([...(current.sources || []), ...(incoming.sources || [])])].slice(-16); out.subjects = [...new Set([...(current.subjects || []), ...(incoming.subjects || [])])].slice(-8); out.previewUrls = [...new Set([...(current.previewUrls || []), ...(incoming.previewUrls || [])])].slice(-4); out.hours = Array.from({ length: 168 }, (_, index) => mergeNumericMax(current.hours?.[index], incoming.hours?.[index])); out.onlineHours = Array.from({ length: 168 }, (_, index) => mergeNumericMax(current.onlineHours?.[index], incoming.onlineHours?.[index])); out.days = Object.fromEntries([...new Set([...Object.keys(current.days || {}), ...Object.keys(incoming.days || {})])].map(day => [day, mergeNumericMax(current.days?.[day], incoming.days?.[day])])); return cleanModel(out); } async function persistAll() { if (db) { const tx = db.transaction(['models', 'profiles', 'meta', 'events', 'qualitySamples', 'accountSnapshots', 'dailyRollups'], 'readwrite'); tx.objectStore('models').clear(); tx.objectStore('profiles').clear(); tx.objectStore('events').clear(); tx.objectStore('qualitySamples').clear(); tx.objectStore('accountSnapshots').clear(); tx.objectStore('dailyRollups').clear(); models.forEach(item => tx.objectStore('models').put(item)); profiles.forEach(item => tx.objectStore('profiles').put(item)); events.forEach(item => tx.objectStore('events').put(item)); qualitySamples.forEach(item => tx.objectStore('qualitySamples').put(item)); accountSnapshots.forEach(item => tx.objectStore('accountSnapshots').put(item)); dailyRollups.forEach(item => tx.objectStore('dailyRollups').put(item)); tx.objectStore('meta').put({ key: 'main', ...meta }); await transactionDone(tx); } } async function createBackup(reason = 'manual') { if (!db?.objectStoreNames?.contains('backups')) return null; const data = await exportData(); const item = { backupId: `analytics_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, at: Date.now(), reason: String(reason || 'manual').slice(0, 40), data }; const tx = db.transaction('backups', 'readwrite'); tx.objectStore('backups').put(item); await transactionDone(tx); const all = await requestResult(db.transaction('backups', 'readonly').objectStore('backups').getAll()); const stale = (all || []).sort((a, b) => Number(b.at) - Number(a.at)).slice(MAX_ANALYTICS_BACKUPS); if (stale.length) { const cleanup = db.transaction('backups', 'readwrite'); stale.forEach(entry => cleanup.objectStore('backups').delete(entry.backupId)); await transactionDone(cleanup); } return { backupId: item.backupId, at: item.at, reason: item.reason }; } async function getBackups() { if (!db?.objectStoreNames?.contains('backups')) return []; const all = await requestResult(db.transaction('backups', 'readonly').objectStore('backups').getAll()); return (all || []).sort((a, b) => Number(b.at) - Number(a.at)).slice(0, MAX_ANALYTICS_BACKUPS).map(item => ({ backupId: item.backupId, at: Number(item.at) || 0, reason: String(item.reason || ''), counts: { models: item.data?.models?.length || 0, events: item.data?.events?.length || 0, qualitySamples: item.data?.qualitySamples?.length || 0 } })); } async function deleteBackup(backupId) { if (!db?.objectStoreNames?.contains('backups')) return false; const tx = db.transaction('backups', 'readwrite'); tx.objectStore('backups').delete(String(backupId || '')); await transactionDone(tx); emit(); return true; } async function restoreBackup(backupId) { if (!db?.objectStoreNames?.contains('backups')) throw new Error('backup unavailable'); const item = await requestResult(db.transaction('backups', 'readonly').objectStore('backups').get(String(backupId || ''))); if (!item?.data) throw new Error('backup unavailable'); await createBackup('before-restore'); return importData(item.data, { strategy: 'replace', backup: false }); } async function importData(input, options = {}) { const review = input?.clean && input?.sections ? input : inspectImport(input); const strategy = options.strategy === 'replace' ? 'replace' : 'merge'; const selected = new Set((Array.isArray(options.sections) ? options.sections : review.sections).filter(key => review.sections.includes(key))); if (!selected.size) throw new Error('no statistics sections selected'); if (options.backup !== false) await createBackup('before-import'); const next = review.clean; if (selected.has('models')) { if (strategy === 'replace') models.clear(); next.models.forEach(item => models.set(item.id, strategy === 'merge' ? mergeModel(models.get(item.id), item) : item)); } if (selected.has('profiles')) { if (strategy === 'replace') profiles.clear(); next.profiles.forEach(item => { const current = profiles.get(item.id); if (strategy === 'replace' || !current || item.observedAt >= current.observedAt) profiles.set(item.id, item); }); } const mergeArray = (target, incoming, key, limit) => { if (strategy === 'replace') target.length = 0; const map = new Map(target.map(item => [item[key], item])); incoming.forEach(item => map.set(item[key], item)); target.splice(0, target.length, ...[...map.values()].sort((a, b) => Number(a.at || 0) - Number(b.at || 0)).slice(-limit)); }; if (selected.has('events')) mergeArray(events, next.events, 'eventId', MAX_EVENTS); if (selected.has('qualitySamples')) mergeArray(qualitySamples, next.qualitySamples, 'sampleId', MAX_QUALITY_SAMPLES); if (selected.has('accountSnapshots')) mergeArray(accountSnapshots, next.accountSnapshots, 'snapshotId', MAX_ACCOUNT_SNAPSHOTS); if (selected.has('dailyRollups')) { if (strategy === 'replace') dailyRollups.clear(); next.dailyRollups.forEach(item => { const current = dailyRollups.get(item.day); if (!current || strategy === 'replace') dailyRollups.set(item.day, item); else dailyRollups.set(item.day, cleanDailyRollup({ day: item.day, updatedAt: Math.max(current.updatedAt, item.updatedAt), counts: Object.fromEntries([...new Set([...Object.keys(current.counts || {}), ...Object.keys(item.counts || {})])].map(key => [key, mergeNumericMax(current.counts?.[key], item.counts?.[key])])), durations: Object.fromEntries([...new Set([...Object.keys(current.durations || {}), ...Object.keys(item.durations || {})])].map(key => [key, mergeNumericMax(current.durations?.[key], item.durations?.[key])])) })); }); } const importedMeta = cleanMeta({ ...meta, ...(review.source.meta || {}), startedAt: Number(review.source.meta?.startedAt || review.source.startedAt) || meta.startedAt || Date.now() }); meta = cleanMeta({ ...importedMeta, privateSession: meta.privateSession, privateSessionUntil: meta.privateSessionUntil, historyPausedUntil: meta.historyPausedUntil, retentionPolicy: meta.retentionPolicy }); dirtyModels.clear(); dirtyProfiles.clear(); await pruneRetention(); await persistAll(); emit(); return { strategy, sections: [...selected], counts: review.counts, conflicts: review.conflicts }; } async function deleteModel(id) { id = safeId(id); models.delete(id); profiles.delete(id); sessions.delete(id); const removeEvents = events.filter(event => event.id === id).map(event => event.eventId); events.splice(0, events.length, ...events.filter(event => event.id !== id)); dirtyModels.delete(id); dirtyProfiles.delete(id); if (db) { try { const tx = db.transaction(['models', 'profiles', 'events'], 'readwrite'); tx.objectStore('models').delete(id); tx.objectStore('profiles').delete(id); removeEvents.forEach(eventId => tx.objectStore('events').delete(eventId)); await transactionDone(tx); } catch (_) {} } emit(); } async function clearAll() { const controls = { retentionDays: meta.retentionDays, retentionPolicy: meta.retentionPolicy, privateSession: meta.privateSession, privateSessionUntil: meta.privateSessionUntil, historyPausedUntil: meta.historyPausedUntil }; models.clear(); profiles.clear(); sessions.clear(); events.length = 0; qualitySamples.length = 0; accountSnapshots.length = 0; dailyRollups.clear(); dirtyModels.clear(); dirtyProfiles.clear(); meta = cleanMeta({ startedAt: Date.now(), ...controls }); if (db) { try { const tx = db.transaction(['models', 'profiles', 'meta', 'events', 'qualitySamples', 'accountSnapshots', 'dailyRollups'], 'readwrite'); tx.objectStore('models').clear(); tx.objectStore('profiles').clear(); tx.objectStore('events').clear(); tx.objectStore('qualitySamples').clear(); tx.objectStore('accountSnapshots').clear(); tx.objectStore('dailyRollups').clear(); tx.objectStore('meta').put({ key: 'main', ...meta }); await transactionDone(tx); } catch (_) {} } emit(); } function subtractEventAggregates(removed) { for (const event of removed.filter(item => item.type === 'visit' && item.id)) { const model = models.get(event.id); if (!model) continue; model.totalMs = Math.max(0, model.totalMs - event.durationMs); model.sessions = Math.max(0, model.sessions - 1); const remaining = events.filter(item => item.type === 'visit' && item.id === event.id); model.firstWatchedAt = remaining.length ? Math.min(...remaining.map(item => item.at)) : 0; model.lastWatchedAt = remaining.length ? Math.max(...remaining.map(item => item.endAt || item.at)) : 0; const date = new Date(event.at); const day = date.toISOString().slice(0, 10); if (model.days?.[day] != null) { model.days[day] = Math.max(0, Number(model.days[day]) - event.durationMs); if (!model.days[day]) delete model.days[day]; } const hour = date.getDay() * 24 + date.getHours(); if (model.hours?.[hour] != null) model.hours[hour] = Math.max(0, Number(model.hours[hour]) - event.durationMs); dirtyModels.add(model.id); } for (const event of removed.filter(item => item.type === 'usage')) { meta.totalUsageMs = Math.max(0, meta.totalUsageMs - event.durationMs); const day = new Date(event.at).toISOString().slice(0, 10); if (meta.usageDays?.[day] != null) { meta.usageDays[day] = Math.max(0, Number(meta.usageDays[day]) - event.durationMs); if (!meta.usageDays[day]) delete meta.usageDays[day]; } } } async function deleteEvent(eventId) { eventId = String(eventId || '').slice(0, 80); const removed = events.filter(event => event.eventId === eventId); if (!removed.length) return false; events.splice(0, events.length, ...events.filter(event => event.eventId !== eventId)); subtractEventAggregates(removed); if (db) { try { const tx = db.transaction('events', 'readwrite'); tx.objectStore('events').delete(eventId); await transactionDone(tx); } catch (_) {} } await flush(); await flushMeta(); emit(); return true; } async function deleteRange(from, to) { from = Math.max(0, Number(from) || 0); to = Math.max(from, Number(to) || Date.now()); const removed = events.filter(event => event.at >= from && event.at <= to); const removedRollups = [...dailyRollups.values()].filter(item => { const at = new Date(`${item.day}T00:00:00`).getTime(); return at >= from && at <= to; }); events.splice(0, events.length, ...events.filter(event => event.at < from || event.at > to)); subtractEventAggregates(removed); removedRollups.forEach(item => { dailyRollups.delete(item.day); meta.totalUsageMs = Math.max(0, meta.totalUsageMs - Number(item.durations?.usage || 0)); delete meta.usageDays[item.day]; }); if (db) { try { const tx = db.transaction(['events', 'dailyRollups'], 'readwrite'); removed.forEach(event => tx.objectStore('events').delete(event.eventId)); removedRollups.forEach(item => tx.objectStore('dailyRollups').delete(item.day)); await transactionDone(tx); } catch (_) {} } await flush(); await flushMeta(); emit(); return removed.length + removedRollups.length; } return { init, whenReady, start, stop, sample, flush, setEnabled, setPrivateSession, setHistoryPause, setRetentionDays, setRetentionPolicy, observeModel, observeModelsBatch, recordEventsBatch, getModels, getModelsSync, getModel, getModelSync, getProfile, getProfileSync, getEvents, getEventsSync, getUsageSummary, getDailyRollups, recordEvent, recordQualitySample, getQualitySamples, getQualitySamplesSync, recordAccountSnapshot, getAccountSnapshots, getAccountSnapshotsSync, clearAccountSnapshots, upsertProfile, exportData, inspectImport, importData, dataInventory, measureDataInventory, getDataRevision: () => dataRevision, createBackup, getBackups, restoreBackup, deleteBackup, deleteModel, deleteEvent, deleteRange, clearAll, isEnabled: () => enabled, isReady: () => ready, isPrivateSession: () => privateSessionActive(), isHistoryPaused: () => historyPaused(), startedAt: () => meta.startedAt || 0, subscribe(fn) { if (typeof fn === 'function') listeners.add(fn); return () => listeners.delete(fn); }, }; } /* ============================================================= * 3.2. RoomGrid Ultimate services * Preview Dock, Model Inspector, ephemeral Chat Focus and the * opt-in official broadcaster statistics client. * ============================================================= */ const RoomGridUltimate = (() => { const ACCOUNT_CONFIG_KEY = 'roomgrid_account_stats_config_v1'; const ACCOUNT_VAULT_KEY = 'roomgrid_account_stats_vault_v1'; const PREVIEW_LAYOUT_KEY = 'roomgrid_preview_layout_v1'; const CONTEXT_TTL_MS = 5 * 60000; const CONTEXT_FAILURE_COOLDOWN_MS = 10 * 60000; const contexts = new Map(); const contextFailures = new Map(); const circuitState = new Map(); let activeStore = null; let activeAnalytics = null; let accountStats = null; let inspector = null; let compareTray = null; let inspectorContextIds = []; let inspectorSeed = {}; let previewRoot = null; let previewCounter = 0; let inlineHover = null; const previews = new Map(); let chatCleanup = null; let wellbeingCleanup = null; let automationCleanup = null; let performanceCleanup = null; let ultimateStoreUnsubscribe = null; function serviceNotice(message, duration = 5000) { const node = $('div', { role: 'status', style: { position: 'fixed', right: '14px', bottom: '14px', zIndex: '2147482600', maxWidth: 'min(420px,calc(100vw - 28px))', padding: '10px 13px', border: '1px solid rgba(148,163,184,.42)', borderRadius: '10px', background: 'rgba(15,23,42,.96)', color: '#f8fafc', boxShadow: '0 14px 38px rgba(0,0,0,.34)', font: '700 12px/1.4 system-ui,sans-serif' } }, rawUiText(message)); document.body?.appendChild(node); setTimeout(() => node.remove(), Math.max(1200, Number(duration) || 5000)); } const gmGet = async (key, fallback = null) => { try { if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') return await GM.getValue(key, fallback); } catch (_) {} return fallback; }; const gmSet = async (key, value) => { try { if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') { await GM.setValue(key, value); return true; } } catch (_) {} return false; }; const gmDelete = async key => { try { if (typeof GM !== 'undefined' && typeof GM.deleteValue === 'function') { await GM.deleteValue(key); return true; } } catch (_) {} return false; }; const bytesToBase64 = bytes => { let binary = ''; for (const value of bytes) binary += String.fromCharCode(value); return btoa(binary); }; const base64ToBytes = value => Uint8Array.from(atob(String(value || '')), char => char.charCodeAt(0)); const validAccountToken = value => /^[a-z0-9]{24}$/iu.test(String(value || '').trim()); async function guardedRequest(key, operation) { const current = circuitState.get(key) || { failures: 0, openUntil: 0 }; if (current.openUntil > Date.now()) throw new Error(tx('暂时暂停请求', 'Requests temporarily paused', 'Anfragen vorübergehend pausiert')); try { const result = await operation(); circuitState.delete(key); return result; } catch (error) { if (error?.name === 'AbortError') throw error; const failures = current.failures + 1; circuitState.set(key, { failures, openUntil: failures >= 3 ? Date.now() + 5 * 60000 : 0 }); throw error; } } function capabilities() { return { isolatedStorage: typeof GM !== 'undefined' && typeof GM.getValue === 'function' && typeof GM.setValue === 'function', webCrypto: typeof crypto !== 'undefined' && !!crypto.subtle, hls: !!(window.Hls?.isSupported?.() || document.createElement('video').canPlayType('application/vnd.apple.mpegurl')), pictureInPicture: !!document.pictureInPictureEnabled, indexedDb: typeof indexedDB !== 'undefined', intersectionObserver: typeof IntersectionObserver !== 'undefined', }; } function previewLayoutId(id) { return `${String(activeStore?.state?.settings?.activeWorkspaceId || 'default').slice(0, 64)}:${id}`; } function loadPreviewLayouts() { try { const value = JSON.parse(localStorage.getItem(PREVIEW_LAYOUT_KEY) || '{}'); return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } catch (_) { return {}; } } function savePreviewLayout(id, node) { try { const rect = node.getBoundingClientRect(); const layouts = loadPreviewLayouts(); layouts[previewLayoutId(id)] = { left: Math.round(rect.left), top: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height), at: Date.now() }; const trimmed = Object.fromEntries(Object.entries(layouts).sort((a, b) => Number(b[1]?.at || 0) - Number(a[1]?.at || 0)).slice(0, 120)); localStorage.setItem(PREVIEW_LAYOUT_KEY, JSON.stringify(trimmed)); } catch (_) {} } async function deriveVaultKey(passphrase, salt, usage) { const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, ['deriveKey']); return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' }, material, { name: 'AES-GCM', length: 256 }, false, usage); } function createAccountStats(analytics) { let config = { username: '', enabled: false, autoRefresh: false, persistence: 'session', lastSuccessAt: 0, lastError: '' }; let token = ''; let locked = false; let initialized = false; let loading = false; let pollTimer = 0; let latest = null; const listeners = new Set(); const emit = () => listeners.forEach(fn => { try { fn(status()); } catch (_) {} }); const schedule = () => { clearTimeout(pollTimer); if (!config.autoRefresh || !config.enabled || !token || document.hidden) return; const wait = Math.max(300000, config.lastSuccessAt ? 300000 - (Date.now() - config.lastSuccessAt) : 0); pollTimer = setTimeout(() => fetchNow().catch(() => {}), Math.max(1000, wait)); }; const saveConfig = async () => gmSet(ACCOUNT_CONFIG_KEY, { ...config, lastError: String(config.lastError || '').slice(0, 160) }); async function init() { if (initialized) return status(); initialized = true; const stored = await gmGet(ACCOUNT_CONFIG_KEY, null); if (stored && typeof stored === 'object') { const username = normalizeUsername(stored.username); config = { username: isLikelyUsername(username) ? username : '', enabled: !!stored.enabled, autoRefresh: !!stored.autoRefresh, persistence: stored.persistence === 'encrypted' ? 'encrypted' : 'session', lastSuccessAt: Math.max(0, Number(stored.lastSuccessAt) || 0), lastError: String(stored.lastError || '').slice(0, 160), }; } locked = config.persistence === 'encrypted' && !!await gmGet(ACCOUNT_VAULT_KEY, null); emit(); schedule(); return status(); } function sanitizeSnapshot(input) { const source = input && typeof input === 'object' ? input : {}; const username = normalizeUsername(source.username || config.username); if (!isLikelyUsername(username)) throw new Error('invalid username'); const number = (key, min = -1) => { const value = Number(source[key]); return Number.isFinite(value) ? Math.max(min, Math.round(value)) : min; }; return { snapshotId: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, at: Date.now(), username, tokenBalance: number('token_balance', 0), tipsLastHour: number('tips_in_last_hour', 0), votesUp: number('votes_up', 0), votesDown: number('votes_down', 0), satisfactionScore: Math.max(-1, Math.min(100, Number(source.satisfaction_score) || 0)), lastBroadcast: source.last_broadcast === -1 ? '' : String(source.last_broadcast || '').slice(0, 80), timeOnline: number('time_online', -1), followers: number('num_followers', 0), viewers: number('num_viewers', 0), registeredViewers: number('num_registered_viewers', 0), tokenedViewers: number('num_tokened_viewers', 0), }; } async function setSession(username, nextToken) { username = normalizeUsername(username); nextToken = String(nextToken || '').trim(); if (!isLikelyUsername(username) || !validAccountToken(nextToken)) throw new Error('invalid credentials'); config = { ...config, username, enabled: true, persistence: 'session', lastError: '' }; token = nextToken; locked = false; await gmDelete(ACCOUNT_VAULT_KEY); await saveConfig(); emit(); schedule(); return true; } async function saveEncrypted(username, nextToken, passphrase) { username = normalizeUsername(username); nextToken = String(nextToken || '').trim(); passphrase = String(passphrase || ''); if (!isLikelyUsername(username) || !validAccountToken(nextToken) || passphrase.length < 8) throw new Error('invalid vault data'); if (typeof crypto === 'undefined' || !crypto.subtle) throw new Error('Web Crypto unavailable'); const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); const key = await deriveVaultKey(passphrase, salt, ['encrypt']); const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(JSON.stringify({ username, token: nextToken }))); const saved = await gmSet(ACCOUNT_VAULT_KEY, { v: 1, salt: bytesToBase64(salt), iv: bytesToBase64(iv), cipher: bytesToBase64(new Uint8Array(cipher)) }); if (!saved) throw new Error('isolated storage unavailable'); config = { ...config, username, enabled: true, persistence: 'encrypted', lastError: '' }; token = nextToken; locked = false; await saveConfig(); emit(); schedule(); return true; } async function unlock(passphrase) { const vault = await gmGet(ACCOUNT_VAULT_KEY, null); if (!vault || typeof crypto === 'undefined' || !crypto.subtle) throw new Error('vault unavailable'); try { const salt = base64ToBytes(vault.salt); const iv = base64ToBytes(vault.iv); const key = await deriveVaultKey(String(passphrase || ''), salt, ['decrypt']); const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, base64ToBytes(vault.cipher)); const parsed = JSON.parse(new TextDecoder().decode(plain)); if (!validAccountToken(parsed.token) || normalizeUsername(parsed.username) !== config.username) throw new Error('invalid vault'); token = parsed.token; locked = false; config.lastError = ''; emit(); schedule(); return true; } catch (_) { locked = true; config.lastError = tx('保险库无法解锁', 'Vault could not be unlocked', 'Tresor konnte nicht entsperrt werden'); emit(); throw new Error('unlock failed'); } } async function fetchNow() { await init(); if (loading) return latest; if (!config.enabled || !token || locked) throw new Error('account stats locked'); if (config.lastSuccessAt && Date.now() - config.lastSuccessAt < 285000 && latest) return latest; loading = true; config.lastError = ''; emit(); try { const url = new URL('/statsapi/', location.origin); url.searchParams.set('username', config.username); url.searchParams.set('token', token); const raw = await guardedRequest('official-account-stats', async () => { const response = await fetch(url.toString(), { credentials: 'omit', cache: 'no-store', referrerPolicy: 'no-referrer' }); if (!response.ok) { if ([401, 403, 429].includes(response.status)) config.autoRefresh = false; throw new Error(`HTTP ${response.status}`); } return response.json(); }); latest = sanitizeSnapshot(raw); config.lastSuccessAt = latest.at; await analytics?.recordAccountSnapshot?.(latest); await saveConfig(); analytics?.recordEvent?.({ type: 'account', at: Date.now(), detail: 'official-stats-refresh' }); return { ...latest }; } catch (error) { config.lastError = String(error?.message || error || 'request failed').slice(0, 160); await saveConfig(); throw error; } finally { loading = false; emit(); schedule(); } } async function patch(next = {}) { if ('enabled' in next) config.enabled = !!next.enabled; if ('autoRefresh' in next) config.autoRefresh = !!next.autoRefresh; await saveConfig(); emit(); schedule(); } async function clear() { clearTimeout(pollTimer); pollTimer = 0; token = ''; latest = null; locked = false; config = { username: '', enabled: false, autoRefresh: false, persistence: 'session', lastSuccessAt: 0, lastError: '' }; await gmDelete(ACCOUNT_CONFIG_KEY); await gmDelete(ACCOUNT_VAULT_KEY); emit(); } function status() { return { ...config, locked, unlocked: !!token && !locked, loading, latest: latest ? { ...latest } : null, isolatedStorage: typeof GM !== 'undefined' && typeof GM.getValue === 'function' }; } document.addEventListener?.('visibilitychange', () => { if (!document.hidden) schedule(); }); return { init, status, setSession, saveEncrypted, unlock, fetchNow, patch, clear, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } }; } function installStyles() { if (document.getElementById('roomgrid-ultimate-style')) return; document.head.appendChild($('style', { id: 'roomgrid-ultimate-style', html: trustedHtml(` .rg-preview-dock{position:fixed;inset:0;z-index:2147482100;pointer-events:none;font:12px/1.35 Inter,system-ui,sans-serif;color:#f8fafc}.rg-preview-window{position:fixed;pointer-events:auto;min-width:240px;min-height:175px;width:360px;height:260px;resize:both;overflow:hidden;border:1px solid rgba(148,163,184,.34);border-radius:16px;background:#080d16;box-shadow:0 22px 70px rgba(0,0,0,.46);display:grid;grid-template-rows:auto minmax(0,1fr) auto}.rg-preview-window.size-small{width:280px;height:210px}.rg-preview-window.size-large{width:480px;height:350px}.rg-preview-head{display:flex;align-items:center;gap:7px;min-height:34px;padding:6px 8px;cursor:move;background:linear-gradient(135deg,rgba(var(--rg-accent-rgb,79,140,255),.94),rgba(var(--rg-secondary-rgb,39,199,184),.88));font-weight:850}.rg-preview-head span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rg-preview-head button{margin-left:auto}.rg-preview-quality{padding:2px 6px;border-radius:999px;background:rgba(2,6,23,.35);font-size:9px;font-weight:750}.rg-preview-stage{position:relative;min-height:0;background:#030712}.rg-preview-stage video{display:block;width:100%;height:100%;object-fit:cover;background:#030712}.rg-preview-state{position:absolute;inset:0;display:grid;place-items:center;padding:18px;text-align:center;color:#cbd5e1;background:radial-gradient(circle,rgba(30,41,59,.65),rgba(3,7,18,.88))}.rg-preview-state:empty{display:none}.rg-preview-actions{display:flex;align-items:center;gap:5px;overflow:auto;padding:6px;background:#0f172a;border-top:1px solid #263449}.rg-preview-actions button{min-height:29px;padding:4px 8px;border:1px solid #334155;border-radius:8px;background:#172033;color:#f8fafc;cursor:pointer;white-space:nowrap}.rg-preview-actions input{width:70px}.rg-preview-close{width:25px;height:25px;border:0;border-radius:7px;background:rgba(15,23,42,.34);color:#fff;cursor:pointer} .rg-inline-hover-host{isolation:isolate}.rg-inline-hover-video{position:absolute!important;inset:0!important;z-index:95!important;display:block!important;width:100%!important;height:100%!important;margin:0!important;border:0!important;border-radius:inherit!important;background:#030712!important;object-fit:cover!important;opacity:0;pointer-events:none!important;transition:opacity .14s ease}.rg-inline-hover-video.is-playing{opacity:1} .rg-inspector-backdrop{position:fixed;inset:0;z-index:2147483200;background:rgba(2,6,23,.36);backdrop-filter:blur(3px)}.rg-inspector{position:absolute;top:0;right:0;width:min(480px,100%);height:100%;overflow:auto;padding:18px;border-left:1px solid rgba(148,163,184,.3);background:linear-gradient(180deg,#111a29,#09101b);color:#f8fafc;box-shadow:-24px 0 70px rgba(0,0,0,.38);font:13px/1.5 Inter,system-ui,sans-serif}.rg-inspector-head{display:flex;align-items:center;gap:10px;position:sticky;top:-18px;z-index:2;margin:-18px -18px 14px;padding:15px 18px;background:rgba(9,16,27,.94);backdrop-filter:blur(14px);border-bottom:1px solid #263449}.rg-inspector-head strong{font-size:19px}.rg-inspector-head button{margin-left:auto}.rg-inspector-card{margin:10px 0;padding:13px;border:1px solid #263449;border-radius:14px;background:#121d2d}.rg-inspector-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.rg-inspector-stat{padding:9px;border-radius:10px;background:#0b1320}.rg-inspector-stat small{display:block;color:#94a3b8}.rg-inspector-stat strong{display:block;margin-top:3px;font-size:15px}.rg-inspector-actions{display:flex;flex-wrap:wrap;gap:7px}.rg-inspector button{min-height:34px;padding:6px 10px;border:1px solid #3a4b64;border-radius:9px;background:#19263a;color:#f8fafc;cursor:pointer}.rg-inspector button.primary{border-color:var(--rg-accent,#4f8cff);background:var(--rg-accent,#4f8cff);color:var(--rg-accent-text,#fff)}.rg-inspector textarea,.rg-inspector input{width:100%;margin-top:6px;padding:8px;border:1px solid #3a4b64;border-radius:9px;background:#08111e;color:#f8fafc}.rg-inspector-facts{display:flex;flex-wrap:wrap;gap:6px}.rg-inspector-fact{padding:5px 8px;border-radius:999px;background:rgba(var(--rg-accent-rgb,79,140,255),.14);color:#bfdbfe} .rg-inspector-v2{width:min(720px,100%);padding-bottom:92px}.rg-inspector-v2 .rg-inspector-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px}.rg-inspector-title{min-width:0}.rg-inspector-title strong,.rg-inspector-title small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rg-inspector-title small{color:#94a3b8;font-size:10px}.rg-inspector-head-nav{display:flex;gap:5px}.rg-inspector-head-nav button{margin:0;min-width:38px;padding:4px}.rg-inspector-hero{display:grid;grid-template-columns:190px minmax(0,1fr);gap:14px;align-items:stretch}.rg-inspector-hero-media{position:relative;min-height:125px;overflow:hidden;border-radius:12px;background:#030712}.rg-inspector-hero-media img{display:block;width:100%;height:100%;object-fit:cover}.rg-inspector-hero-media span{position:absolute;left:8px;bottom:8px;padding:4px 7px;border-radius:999px;background:rgba(2,6,23,.8);font-size:10px}.rg-inspector-tabs{position:sticky;top:50px;z-index:2;display:flex;gap:5px;overflow-x:auto;margin:12px -2px;padding:4px 2px;background:rgba(9,16,27,.94)}.rg-inspector-tabs button{flex:1 0 auto}.rg-inspector-tabs button[aria-selected="true"]{border-color:var(--rg-accent,#4f8cff);background:rgba(var(--rg-accent-rgb,79,140,255),.2);color:#fff}.rg-inspector-panel[hidden]{display:none}.rg-inspector-fields{display:grid;grid-template-columns:1fr 1fr;gap:9px}.rg-inspector-fields label{display:grid;gap:4px;color:#cbd5e1;font-size:11px}.rg-inspector-fields textarea{grid-column:1/-1;min-height:92px;resize:vertical}.rg-inspector select{width:100%;min-height:36px;padding:7px;border:1px solid #3a4b64;border-radius:8px;background:#08111e;color:#f8fafc}.rg-inspector-checks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.rg-inspector-checks label{display:flex;align-items:center;gap:8px;min-height:36px;padding:7px 9px;border:1px solid #2d3d53;border-radius:9px;background:#0b1320}.rg-inspector-checks input{width:auto;margin:0}.rg-inspector-timeline{display:grid;gap:7px}.rg-inspector-event{display:grid;grid-template-columns:105px minmax(0,1fr) auto;gap:8px;padding:8px;border-bottom:1px solid #263449}.rg-inspector-event small{color:#94a3b8}.rg-inspector-source{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.rg-inspector-source span{padding:4px 7px;border:1px solid #334155;border-radius:999px;color:#cbd5e1;font-size:10px}.rg-inspector-footer{position:sticky;bottom:-18px;z-index:3;display:flex;flex-wrap:wrap;gap:7px;margin:14px -18px -18px;padding:12px 18px;background:rgba(9,16,27,.97);border-top:1px solid #263449}.rg-compare-tray{position:fixed;left:50%;bottom:14px;z-index:2147483100;display:flex;align-items:center;gap:6px;max-width:calc(100vw - 24px);overflow:auto;padding:8px;border:1px solid #3a4b64;border-radius:12px;background:rgba(9,16,27,.97);color:#fff;box-shadow:0 16px 44px rgba(0,0,0,.45);transform:translateX(-50%);font:12px/1.3 Inter,system-ui,sans-serif}.rg-compare-tray button{min-height:32px;padding:5px 9px;border:1px solid #3a4b64;border-radius:8px;background:#19263a;color:#fff;white-space:nowrap;cursor:pointer}.rg-compare-tray .primary{border-color:var(--rg-accent,#4f8cff);background:var(--rg-accent,#4f8cff)}.rg-inspector-compare{width:min(1120px,100%)}.rg-compare-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.rg-compare-card{min-width:0;padding:11px;border:1px solid #2d3d53;border-radius:12px;background:#101a29}.rg-compare-card img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:9px;background:#030712}.rg-compare-card h3{margin:9px 0}.rg-compare-table{display:grid;grid-template-columns:1fr auto;gap:5px;margin:8px 0}.rg-compare-table span:nth-child(odd){color:#94a3b8}.rg-compare-table span:nth-child(even){font-weight:800;text-align:right}.rg-inspector-empty{padding:14px;border:1px dashed #3a4b64;border-radius:10px;color:#94a3b8;text-align:center} body.rg-chat-focus [data-roomgrid-system-message="1"]{transition:opacity .15s}body.rg-chat-focus.rg-chat-system-dim [data-roomgrid-system-message="1"]{opacity:.32;filter:grayscale(.7)}body.rg-chat-focus.rg-chat-system-hide [data-roomgrid-system-message="1"]{display:none!important}.rg-chat-time{display:inline-block;margin-right:5px;color:#94a3b8;font-size:10px;font-variant-numeric:tabular-nums}.rg-chat-mentioned,.rg-chat-highlighted{border-left:3px solid var(--rg-accent,#4f8cff)!important;background:rgba(var(--rg-accent-rgb,79,140,255),.1)!important}.rg-chat-collapsed,.rg-chat-muted{display:none!important}.rg-chat-unread{margin:6px 0;padding:3px 8px;border-radius:999px;background:var(--rg-accent,#4f8cff);color:#fff;font:700 10px/1.4 system-ui;text-align:center}.rg-chat-toolbar{position:fixed;left:12px;bottom:12px;z-index:2147482000;display:flex;gap:5px;padding:6px;border:1px solid rgba(148,163,184,.34);border-radius:11px;background:rgba(15,23,42,.92);box-shadow:0 12px 34px rgba(0,0,0,.3)}.rg-chat-toolbar input{width:190px;min-height:31px;padding:5px 8px;border:1px solid #475569;border-radius:8px;background:#0b1220;color:#fff}.rg-chat-toolbar button{min-height:31px;border:1px solid #475569;border-radius:8px;background:#1e293b;color:#fff;cursor:pointer} @media(max-width:720px){.rg-preview-window{width:min(92vw,360px);height:250px}.rg-inspector{width:100%}.rg-inspector-hero{grid-template-columns:1fr}.rg-inspector-hero-media{max-height:210px}.rg-inspector-fields,.rg-inspector-checks{grid-template-columns:1fr}.rg-inspector-fields textarea{grid-column:auto}.rg-inspector-event{grid-template-columns:1fr}.rg-compare-tray{left:8px;right:8px;transform:none}.rg-inspector-head-nav button{min-width:34px}} `) })); } function hardStopPreview(entry) { try { entry.abort?.abort(); } catch (_) {} try { entry.resizeObserver?.disconnect?.(); } catch (_) {} try { entry.hls?.stopLoad?.(); entry.hls?.detachMedia?.(); entry.hls?.destroy?.(); } catch (_) {} try { stopMediaElement(entry.video, true); } catch (_) {} entry.node?.remove(); previews.delete(entry.id); } function stopInlineHoverPreview(host = null) { const entry = inlineHover; if (!entry || (host && entry.host !== host)) return false; inlineHover = null; clearTimeout(entry.timeout); try { entry.abort?.abort(); } catch (_) {} try { entry.hls?.stopLoad?.(); entry.hls?.detachMedia?.(); entry.hls?.destroy?.(); } catch (_) {} try { stopMediaElement(entry.video, true); } catch (_) {} entry.video?.remove(); entry.host?.classList?.remove('rg-inline-hover-host'); return true; } async function startInlineHoverPreview(id, host) { id = normalizeUsername(id); if (!isLikelyUsername(id) || !host?.isConnected || document.hidden) return false; if (inlineHover?.id === id && inlineHover.host === host) return true; stopInlineHoverPreview(); installStyles(); const video = $('video', { class: 'rg-inline-hover-video', muted: true, autoplay: true, playsInline: true, preload: 'auto', disablePictureInPicture: true, dataset: { state: 'connecting', roomgridHoverLive: id } }); video.muted = true; video.defaultMuted = true; video.volume = 0; host.classList.add('rg-inline-hover-host'); host.appendChild(video); const entry = { id, host, video, hls: null, abort: new AbortController(), timeout: 0 }; inlineHover = entry; video.addEventListener('playing', () => { if (inlineHover === entry) { video.dataset.state = 'playing'; video.classList.add('is-playing'); } }); try { entry.timeout = setTimeout(() => entry.abort.abort(), 10000); const data = await guardedRequest(`hover:${id}`, async () => { const response = await fetch(`/api/chatvideocontext/${encodeURIComponent(id)}/`, { credentials: 'include', cache: 'no-store', signal: entry.abort.signal, referrerPolicy: 'strict-origin-when-cross-origin' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); clearTimeout(entry.timeout); entry.timeout = 0; if (inlineHover !== entry || !host.isConnected || data.room_status !== 'public' || !isSafeStreamUrl(data.hls_source)) throw new Error(data.room_status || 'stream unavailable'); video.dataset.state = 'attached'; if (window.Hls && Hls.isSupported()) { // A small muted hover player should begin with the lightest rung, // not spend its initial bandwidth estimating a high-resolution one. const hls = new Hls({ startLevel: 0, lowLatencyMode: true, liveDurationInfinity: true, liveSyncDurationCount: 1, liveMaxLatencyDurationCount: 3, maxBufferLength: 4, backBufferLength: 0, enableWorker: true }); entry.hls = hls; hls.on(Hls.Events.MANIFEST_PARSED, () => { if (inlineHover === entry && !document.hidden) video.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_event, detail) => { if (detail?.fatal && inlineHover === entry) stopInlineHoverPreview(host); }); hls.loadSource(data.hls_source); hls.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = data.hls_source; video.play().catch(() => {}); } else throw new Error('HLS unsupported'); activeAnalytics?.recordEvent?.({ type: 'preview', id, at: Date.now(), detail: 'card-hover-live' }); return true; } catch (_) { if (inlineHover === entry) stopInlineHoverPreview(host); return false; } } function ensurePreviewRoot() { installStyles(); if (!previewRoot?.isConnected) { previewRoot = $('div', { class: 'rg-preview-dock', ariaLabel: tx('预览停靠栏', 'Preview Dock', 'Preview Dock') }); document.body.appendChild(previewRoot); } return previewRoot; } function dragWindow(node, handle, id) { handle.addEventListener('pointerdown', event => { if (event.button !== 0 || event.target.closest('button,input')) return; event.preventDefault(); handle.setPointerCapture?.(event.pointerId); const rect = node.getBoundingClientRect(); const startX = event.clientX; const startY = event.clientY; const move = next => { const left = Math.max(0, Math.min(innerWidth - 120, rect.left + next.clientX - startX)); const top = Math.max(0, Math.min(innerHeight - 60, rect.top + next.clientY - startY)); node.style.left = `${left}px`; node.style.top = `${top}px`; node.style.right = 'auto'; node.style.bottom = 'auto'; }; const end = () => { document.removeEventListener('pointermove', move); document.removeEventListener('pointerup', end); const latest = node.getBoundingClientRect(); if (latest.left < 16) node.style.left = '8px'; if (latest.top < 16) node.style.top = '8px'; if (innerWidth - latest.right < 16) node.style.left = `${Math.max(0, innerWidth - latest.width - 8)}px`; if (innerHeight - latest.bottom < 16) node.style.top = `${Math.max(0, innerHeight - latest.height - 8)}px`; savePreviewLayout(id, node); }; document.addEventListener('pointermove', move); document.addEventListener('pointerup', end, { once: true }); }); } async function attachPreviewStream(entry) { entry.state.textContent = tx('正在连接…', 'Connecting…', 'Verbindung wird hergestellt …'); entry.abort = new AbortController(); try { const data = await guardedRequest(`preview:${entry.id}`, async () => { const response = await fetch(`/api/chatvideocontext/${encodeURIComponent(entry.id)}/`, { credentials: 'include', cache: 'no-store', signal: entry.abort.signal, referrerPolicy: 'strict-origin-when-cross-origin' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); if (data.room_status !== 'public' || !isSafeStreamUrl(data.hls_source)) throw new Error(data.room_status || 'stream unavailable'); const video = entry.video; if (window.Hls && Hls.isSupported()) { const hls = new Hls({ liveDurationInfinity: true, lowLatencyMode: true, maxBufferLength: 8 }); entry.hls = hls; hls.loadSource(data.hls_source); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (!entry.userPaused && !document.hidden) video.play().catch(() => {}); }); if (Hls.Events.LEVEL_SWITCHED) hls.on(Hls.Events.LEVEL_SWITCHED, (_, detail) => { const level = hls.levels?.[Number(detail?.level)]; entry.quality.textContent = level?.height ? `${level.height}p` : 'HLS'; }); hls.on(Hls.Events.ERROR, (_, detail) => { if (detail?.fatal) { entry.state.textContent = tx('流暂时不可用', 'Stream temporarily unavailable', 'Stream vorübergehend nicht verfügbar'); entry.quality.textContent = tx('错误', 'Error', 'Fehler'); } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = data.hls_source; video.play().catch(() => {}); } else throw new Error('HLS unsupported'); video.addEventListener('waiting', () => { entry.quality.textContent = tx('缓冲', 'Buffering', 'Puffert'); }); video.addEventListener('playing', () => { entry.state.textContent = ''; entry.quality.textContent = video.videoHeight ? `${video.videoHeight}p` : tx('直播', 'Live', 'Live'); }); activeAnalytics?.recordEvent?.({ type: 'preview', id: entry.id, at: Date.now(), detail: 'dock-open' }); } catch (error) { if (!entry.abort?.signal?.aborted) entry.state.textContent = String(error?.message || tx('预览不可用', 'Preview unavailable', 'Vorschau nicht verfügbar')); } } function openPreview(id) { id = normalizeUsername(id); if (!isLikelyUsername(id) || activeStore?.state?.settings?.previewDockEnabled === false) return false; const existing = previews.get(id); if (existing) { existing.node.style.zIndex = String(2147482200 + ++previewCounter); return true; } const limit = clampInt(activeStore?.state?.settings?.previewDockMax, 1, 6, 3); while (previews.size >= limit) hardStopPreview([...previews.values()].sort((a, b) => a.openedAt - b.openedAt)[0]); const video = $('video', { muted: true, autoplay: true, playsInline: true, disablePictureInPicture: false }); video.muted = true; video.volume = .45; const state = $('div', { class: 'rg-preview-state' }); const qualityBadge = $('span', { class: 'rg-preview-quality' }, tx('连接', 'Connect', 'Verbindet')); const closeButton = $('button', { class: 'rg-preview-close', type: 'button', title: tx('关闭', 'Close', 'Schließen') }, '×'); const head = $('div', { class: 'rg-preview-head' }, [$('span', {}, `● ${id}`), qualityBadge, closeButton]); const opacity = Math.max(.25, Math.min(1, Number(activeStore?.state?.settings?.previewDockOpacity) || .96)); const savedLayout = loadPreviewLayouts()[previewLayoutId(id)]; const position = savedLayout ? { left: `${Math.max(0, Math.min(innerWidth - 120, Number(savedLayout.left) || 0))}px`, top: `${Math.max(0, Math.min(innerHeight - 60, Number(savedLayout.top) || 0))}px`, width: `${clampInt(savedLayout.width, 240, 900, 360)}px`, height: `${clampInt(savedLayout.height, 175, 700, 260)}px` } : { right: `${18 + (previews.size % 3) * 28}px`, bottom: `${18 + (previews.size % 3) * 28}px` }; const node = $('section', { class: `rg-preview-window size-${activeStore?.state?.settings?.previewDockSize || 'medium'}`, style: { ...position, opacity: String(opacity), zIndex: String(2147482200 + ++previewCounter) } }); const entry = { id, node, video, state, quality: qualityBadge, hls: null, abort: null, userPaused: false, openedAt: Date.now() }; const playButton = $('button', { type: 'button', onclick: () => { entry.userPaused = !video.paused; if (video.paused) { entry.userPaused = false; entry.hls?.startLoad?.(); video.play().catch(() => {}); } else { entry.userPaused = true; video.pause(); entry.hls?.stopLoad?.(); } } }, '⏯'); const muteButton = $('button', { type: 'button', onclick: () => { const enableAudio = video.muted; if (enableAudio) previews.forEach(other => { other.video.muted = true; if (other.muteButton) other.muteButton.textContent = '🔇'; }); video.muted = !enableAudio; muteButton.textContent = video.muted ? '🔇' : '🔊'; } }, '🔇'); entry.muteButton = muteButton; const opacityInput = $('input', { type: 'range', min: '.25', max: '1', step: '.05', value: String(opacity), title: tx('透明度', 'Opacity', 'Deckkraft') }); opacityInput.addEventListener('input', () => { node.style.opacity = opacityInput.value; }); opacityInput.addEventListener('change', () => activeStore?.patchSettings?.({ previewDockOpacity: Number(opacityInput.value) })); const actions = $('div', { class: 'rg-preview-actions' }, [ playButton, muteButton, opacityInput, $('button', { type: 'button', onclick: async () => { try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await video.requestPictureInPicture?.(); } catch (_) {} } }, 'PiP'), $('button', { type: 'button', onclick: () => openInspector(id) }, tx('信息', 'Info', 'Info')), $('button', { type: 'button', onclick: () => { if (!activeStore.state.rooms.some(room => room.id === id)) activeStore.addRoom(id); } }, 'Grid +'), ]); node.append(head, $('div', { class: 'rg-preview-stage' }, [video, state]), actions); closeButton.onclick = () => hardStopPreview(entry); dragWindow(node, head, id); const resizeObserver = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(debounce(() => savePreviewLayout(id, node), 250)) : null; resizeObserver?.observe(node); entry.resizeObserver = resizeObserver; previews.set(id, entry); ensurePreviewRoot().appendChild(node); attachPreviewStream(entry); return true; } function contextFacts(data) { if (!data || typeof data !== 'object') return []; const labels = { display_age: tx('年龄', 'Age', 'Alter'), gender: tx('性别', 'Gender', 'Geschlecht'), location: tx('地点', 'Location', 'Ort'), country: tx('国家', 'Country', 'Land'), last_broadcast: tx('上次直播', 'Last broadcast', 'Letzter Broadcast'), fan_club_cost: tx('粉丝俱乐部', 'Fan club', 'Fanclub'), private_show_tokens_per_minute: tx('私人秀', 'Private show', 'Private Show'), spy_private_show_tokens_per_minute: tx('观看私人秀', 'Spy show', 'Spy Show'), }; return Object.entries(labels).map(([key, label]) => [label, data[key]]).filter(([, value]) => value !== undefined && value !== null && String(value).trim()).slice(0, 12); } async function fetchContextInfo(id) { const cached = contexts.get(id); if (cached && Date.now() - cached.at < CONTEXT_TTL_MS) return cached.data; if ((contextFailures.get(id) || 0) > Date.now()) throw new Error('cooldown'); try { const data = await guardedRequest(`context:${id}`, async () => { const response = await fetch(`/api/biocontext/${encodeURIComponent(id)}/`, { credentials: 'include', cache: 'no-store', referrerPolicy: 'strict-origin-when-cross-origin' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }); contexts.set(id, { at: Date.now(), data }); return data; } catch (error) { contextFailures.set(id, Date.now() + CONTEXT_FAILURE_COOLDOWN_MS); throw error; } } const inspectorDate = value => Number(value) > 0 ? new Date(Number(value)).toLocaleString() : '—'; const inspectorImage = (id, model = {}) => (model.previewUrls || []).filter(Boolean).at?.(-1) || `https://thumb.live.mmcdn.com/riw/${encodeURIComponent(id)}.jpg`; const inspectorRoom = id => activeStore?.state?.rooms?.find(item => item.id === id) || null; const inspectorTerms = value => [...new Set(String(value || '').split(/[,;|\n]+/u).map(item => item.trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))].slice(0, 40); const compareIds = () => [...new Set((activeStore?.state?.settings?.inspectorCompareIds || []).map(normalizeUsername).filter(isLikelyUsername))].slice(0, 4); function collectInspectorContext(id, seed = {}) { const supplied = Array.isArray(seed.contextIds) ? seed.contextIds : []; const visible = [...document.querySelectorAll('[data-roomgrid-model-stat],[data-roomgrid-library-model],[data-discover-username],[data-multicam-username]')] .map(node => node.dataset.roomgridModelStat || node.dataset.roomgridLibraryModel || node.dataset.discoverUsername || node.dataset.multicamUsername); const stored = activeStore?.state?.rooms?.map(item => item.id) || []; const ids = [...new Set([...supplied, ...visible, ...stored].map(normalizeUsername).filter(isLikelyUsername))].slice(0, 1200); if (!ids.includes(id)) ids.unshift(id); return ids; } function renderCompareTray() { const ids = compareIds(); compareTray?.remove(); compareTray = null; if (!ids.length || !document.body) return; compareTray = $('div', { class: 'rg-compare-tray', dataset: { roomgridCompareTray: '1' }, role: 'region', ariaLabel: tx('模型比较列表', 'Model comparison list', 'Model-Vergleichsliste') }, [ $('strong', {}, `${tx('比较', 'Compare', 'Vergleich')} ${ids.length}/4`), ...ids.map(id => $('button', { type: 'button', title: tx('打开模型信息', 'Open model details', 'Modeldetails öffnen'), onclick: () => openInspector(id, { contextIds: ids }) }, rawUiText(`@${id}`))), $('button', { class: 'primary', type: 'button', onclick: openComparison }, tx('现在比较', 'Compare now', 'Jetzt vergleichen')), $('button', { type: 'button', onclick: () => { activeStore?.patchSettings?.({ inspectorCompareIds: [] }); renderCompareTray(); } }, tx('清空', 'Clear', 'Leeren')), ]); document.body.appendChild(compareTray); } function toggleCompare(id) { id = normalizeUsername(id); if (!isLikelyUsername(id) || !activeStore) return false; const ids = compareIds(); const active = ids.includes(id); if (!active && ids.length >= 4) { RoomGridUx?.notify?.(tx('最多可同时比较四个模型。', 'Up to four models can be compared at once.', 'Es können höchstens vier Models gleichzeitig verglichen werden.'), { tone: 'warning' }); return false; } const next = active ? ids.filter(value => value !== id) : [...ids, id]; activeStore.patchSettings({ inspectorCompareIds: next }); renderCompareTray(); document.querySelectorAll('[data-roomgrid-compare-model]').forEach(button => { if (button.dataset.roomgridCompareModel !== id) return; button.classList.toggle('active', !active); button.setAttribute('aria-pressed', String(!active)); button.textContent = !active ? tx('从比较中移除', 'Remove from comparison', 'Aus Vergleich entfernen') : tx('加入比较', 'Add to comparison', 'Zum Vergleich'); }); return true; } function compareMetric(label, value) { return [$('span', {}, label), $('span', {}, rawUiText(String(value ?? '—')))]; } function trapInspectorFocus(container, event) { if (event.key !== 'Tab') return; const nodes = [...container.querySelectorAll('button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),a[href],[tabindex]:not([tabindex="-1"])')].filter(node => !node.hidden && node.getClientRects().length); if (!nodes.length) return; const first = nodes[0]; const last = nodes.at(-1); if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } function openComparison() { const ids = compareIds(); if (!ids.length) return false; const opener = document.activeElement; inspector?.remove(); const backdrop = $('div', { class: 'rg-inspector-backdrop', dataset: { roomgridComparison: '1' } }); inspector = backdrop; const close = () => { backdrop.remove(); if (inspector === backdrop) inspector = null; if (opener?.isConnected) opener.focus(); }; const cards = ids.map(id => { const room = inspectorRoom(id); const model = activeAnalytics?.getModelSync?.(id) || {}; const tags = [...new Set([...(room?.localTags || []), ...(model.tags || [])])].slice(0, 8); const metrics = $('div', { class: 'rg-compare-table' }, [ ...compareMetric(tx('总观看', 'Total watched', 'Gesamt gesehen'), formatCompactDuration(model.totalMs || 0)), ...compareMetric(tx('会话', 'Sessions', 'Sitzungen'), model.sessions || 0), ...compareMetric(tx('观众', 'Viewers', 'Zuschauer'), model.viewerCount || '—'), ...compareMetric(tx('国家', 'Country', 'Land'), model.country || '—'), ...compareMetric(tx('年龄', 'Age', 'Alter'), model.age || '—'), ...compareMetric(tx('评分', 'Rating', 'Bewertung'), room?.rating ? `${room.rating}/5` : '—'), ...compareMetric(tx('在线样本', 'Online samples', 'Online-Samples'), model.onlineSamples || 0), ]); const quality = $('span', { class: 'rg-inspector-fact' }, tx('质量数据正在加载…', 'Loading quality data…', 'Qualitätsdaten werden geladen …')); activeAnalytics?.getQualitySamples?.({ id, limit: 500 }).then(samples => { if (!quality.isConnected) return; const total = samples.reduce((sum, item) => sum + Number(item.totalFrames || 0), 0); const dropped = samples.reduce((sum, item) => sum + Number(item.droppedFrames || 0), 0); const stalls = samples.reduce((sum, item) => sum + Number(item.stalls || 0), 0); quality.textContent = samples.length ? `${samples.length} ${tx('样本', 'samples', 'Samples')} · ${stalls} ${tx('卡顿', 'stalls', 'Stopps')} · ${total ? (dropped / total * 100).toFixed(2) : '0.00'} %` : tx('尚无质量测量', 'No quality measurements yet', 'Noch keine Qualitätsmessungen'); }).catch(() => { if (quality.isConnected) quality.textContent = tx('质量数据不可用', 'Quality data unavailable', 'Qualitätsdaten nicht verfügbar'); }); return $('article', { class: 'rg-compare-card', dataset: { roomgridCompareCard: id } }, [ $('img', { src: inspectorImage(id, model), alt: tx(`${id} 预览`, `${id} preview`, `${id} Vorschau`), loading: 'lazy', referrerPolicy: 'no-referrer' }), $('h3', {}, rawUiText(`@${id}`)), metrics, $('div', { class: 'rg-inspector-facts' }, tags.map(tag => $('span', { class: 'rg-inspector-fact' }, rawUiText(`#${tag}`)))), quality, $('div', { class: 'rg-inspector-actions', style: { marginTop: '9px' } }, [ $('button', { class: 'primary', type: 'button', onclick: () => openInspector(id, { contextIds: ids }) }, tx('详情', 'Details', 'Details')), $('button', { type: 'button', onclick: () => toggleCompare(id) }, tx('移除', 'Remove', 'Entfernen')), ]), ]); }); const common = ids.map(id => new Set([...(inspectorRoom(id)?.localTags || []), ...(activeAnalytics?.getModelSync?.(id)?.tags || [])].map(value => String(value).toLocaleLowerCase()))); const shared = common.length ? [...common[0]].filter(tag => common.every(set => set.has(tag))).slice(0, 16) : []; const drawer = $('aside', { class: 'rg-inspector rg-inspector-compare', role: 'dialog', ariaModal: 'true', ariaLabel: tx('模型比较', 'Model comparison', 'Modelvergleich') }, [ $('div', { class: 'rg-inspector-head' }, [$('strong', {}, `${tx('模型比较', 'Model comparison', 'Modelvergleich')} · ${ids.length}/4`), $('button', { type: 'button', onclick: close }, tx('关闭', 'Close', 'Schließen'))]), $('section', { class: 'rg-inspector-card' }, [ $('p', {}, tx('所有数值均来自公开资料、你自己的整理或此设备上的本地观察。缺失值不会被猜测。', 'Every value comes from public information, your own organization, or local observations on this device. Missing values are never guessed.', 'Alle Werte stammen aus öffentlichen Angaben, deiner eigenen Organisation oder lokalen Beobachtungen auf diesem Gerät. Fehlende Werte werden nicht geraten.')), shared.length ? $('div', { class: 'rg-inspector-facts' }, [$('strong', {}, tx('共同标签', 'Shared tags', 'Gemeinsame Tags')), ...shared.map(tag => $('span', { class: 'rg-inspector-fact' }, rawUiText(`#${tag}`)))]) : null, ]), $('div', { class: 'rg-compare-grid' }, cards), ]); backdrop.appendChild(drawer); backdrop.addEventListener('click', event => { if (event.target === backdrop) close(); }); backdrop.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); close(); } else trapInspectorFocus(drawer, event); }); document.body.appendChild(backdrop); drawer.querySelector('button')?.focus(); return true; } function openInspector(id, seed = {}) { id = normalizeUsername(id); if (!isLikelyUsername(id) || activeStore?.state?.settings?.inspectorEnabled === false) return false; const opener = document.activeElement; inspector?.remove(); inspectorSeed = seed && typeof seed === 'object' ? seed : {}; inspectorContextIds = collectInspectorContext(id, inspectorSeed); const position = Math.max(0, inspectorContextIds.indexOf(id)); const room = inspectorRoom(id); const storedModel = activeAnalytics?.getModelSync?.(id) || {}; const model = { ...inspectorSeed, ...storedModel, id }; const profile = activeAnalytics?.getProfileSync?.(id) || {}; const backdrop = $('div', { class: 'rg-inspector-backdrop', dataset: { roomgridInspector: id } }); inspector = backdrop; const close = () => { backdrop.remove(); if (inspector === backdrop) inspector = null; if (opener?.isConnected) opener.focus(); }; const navigate = delta => { if (inspectorContextIds.length < 2) return; const next = inspectorContextIds[(position + delta + inspectorContextIds.length) % inspectorContextIds.length]; openInspector(next, { contextIds: inspectorContextIds }); }; const facts = $('div', { class: 'rg-inspector-facts' }, [$('span', { class: 'rg-inspector-fact' }, tx('正在加载公开信息…', 'Loading public facts…', 'Öffentliche Angaben werden geladen …'))]); const image = $('img', { src: inspectorImage(id, model), alt: tx(`${id} 预览`, `${id} preview`, `${id} Vorschau`), referrerPolicy: 'no-referrer' }); image.addEventListener('error', () => { if (!image.dataset.fallback) { image.dataset.fallback = '1'; image.src = `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(id)}.jpg`; } }); const stat = (label, value) => $('div', { class: 'rg-inspector-stat' }, [$('small', {}, label), $('strong', {}, rawUiText(String(value ?? '—')))]); const overview = $('section', { id: `rg-inspector-${id}-overview`, class: 'rg-inspector-panel', dataset: { inspectorPanel: 'overview' }, role: 'tabpanel' }, [ $('section', { class: 'rg-inspector-card rg-inspector-hero' }, [ $('div', { class: 'rg-inspector-hero-media' }, [image, $('span', {}, rawUiText(room?.lastStatus || model.lastStatus || model.currentShow || tx('未知状态', 'Unknown status', 'Status unbekannt')))]), $('div', {}, [ $('div', { class: 'rg-inspector-grid' }, [ stat(tx('总观看', 'Total watched', 'Gesamt gesehen'), formatCompactDuration(model.totalMs || 0)), stat(tx('会话', 'Sessions', 'Sitzungen'), model.sessions || 0), stat(tx('观众', 'Viewers', 'Zuschauer'), model.viewerCount || room?.viewerCount || '—'), stat(tx('最后活动', 'Last activity', 'Letzte Aktivität'), inspectorDate(model.lastWatchedAt || model.lastObservedAt)), ]), $('p', { style: { color: '#94a3b8' } }, tx('这些统计只描述您自己的本地 RoomGrid 使用情况。', 'These statistics describe only your own local RoomGrid usage.', 'Diese Statistik beschreibt ausschließlich deine lokale RoomGrid-Nutzung.')), ]), ]), $('section', { class: 'rg-inspector-card' }, [ $('strong', {}, tx('一览', 'At a glance', 'Auf einen Blick')), $('div', { class: 'rg-inspector-grid', style: { marginTop: '9px' } }, [ stat(tx('国家', 'Country', 'Land'), model.country || '—'), stat(tx('地点', 'Location', 'Ort'), model.location || '—'), stat(tx('年龄', 'Age', 'Alter'), model.age || '—'), stat(tx('类型', 'Type', 'Typ'), model.gender || '—'), stat(tx('出现次数', 'Appearances', 'Auftritte'), model.appearances || 0), stat(tx('在线样本', 'Online samples', 'Online-Samples'), model.onlineSamples || 0), ]), ]), ]); const publicTags = [...new Set([...(model.tags || []), ...(profile.tags || [])])].slice(0, 40); const sourceInfo = typeof RoomGridDataProvenance !== 'undefined' ? RoomGridDataProvenance.itemSources({ room, model, profile: Object.keys(profile).length ? profile : null, sessions: model.sessions || 0 }) : []; const profileLinks = [...new Set([...(profile.links || []), profile.sourceUrl].filter(value => /^https?:\/\//iu.test(String(value || ''))))].slice(0, 20); const profilePanel = $('section', { id: `rg-inspector-${id}-profile`, class: 'rg-inspector-panel', dataset: { inspectorPanel: 'profile' }, role: 'tabpanel', hidden: true }, [ $('section', { class: 'rg-inspector-card' }, [ $('strong', {}, tx('公开信息', 'Public facts', 'Öffentliche Angaben')), facts, profile.bio || model.subjects?.length ? $('p', {}, rawUiText(profile.bio || model.subjects.at(-1))) : $('p', { class: 'rg-inspector-empty' }, tx('尚无公开简介。', 'No public biography available yet.', 'Noch keine öffentliche Beschreibung vorhanden.')), publicTags.length ? $('div', { class: 'rg-inspector-facts' }, publicTags.map(tag => $('span', { class: 'rg-inspector-fact' }, rawUiText(`#${tag}`)))) : null, profileLinks.length ? $('div', { class: 'rg-inspector-actions' }, profileLinks.map((url, index) => $('button', { type: 'button', onclick: () => openNoopener(url) }, index ? `${tx('公开链接', 'Public link', 'Öffentlicher Link')} ${index + 1}` : tx('公开个人资料来源', 'Public profile source', 'Öffentliche Profilquelle')))) : null, $('div', { class: 'rg-inspector-source' }, sourceInfo.map(source => $('span', {}, rawUiText(source.label)))), $('p', { style: { color: '#94a3b8' } }, `${tx('资料更新时间', 'Profile updated', 'Profil aktualisiert')}: ${inspectorDate(profile.observedAt || model.lastObservedAt)}`), ]), ]); const notes = $('textarea', { rows: 4, placeholder: tx('私人备注(仅限本地)', 'Private note (local only)', 'Private Notiz (nur lokal)'), value: room?.notes || '' }); const tags = $('input', { type: 'text', placeholder: tx('自己的标签', 'Your tags', 'Eigene Tags'), value: (room?.localTags || []).join(', ') }); tags.setAttribute('list', `rg-inspector-tags-${id}`); const tagSuggestions = [...new Set((activeStore.state.rooms || []).flatMap(item => item.localTags || []))].sort().slice(0, 500); const rating = $('select', {}, [0, 1, 2, 3, 4, 5].map(value => $('option', { value: String(value), selected: Number(room?.rating || 0) === value }, value ? `${value}/5` : tx('无评分', 'No rating', 'Keine Bewertung')))); const priority = $('select', {}, [0, 1, 2, 3, 4, 5].map(value => $('option', { value: String(value), selected: Number(room?.priority || 0) === value }, value ? `${value}/5` : tx('无优先级', 'No priority', 'Keine Priorität')))); const favorite = $('input', { type: 'checkbox', checked: !!room && roomInGroup(room, FAVORITE_GROUP_ID) }); const watchlist = $('input', { type: 'checkbox', checked: !!room?.watchlist }); const archived = $('input', { type: 'checkbox', checked: !!room?.archived }); const customGroups = (activeStore.state.groups || []).filter(group => !group.system && !group.archived); const groupChecks = new Map(customGroups.map(group => [group.id, $('input', { type: 'checkbox', checked: !!room && roomInGroup(room, group.id) })])); const saveOrganization = () => { const nextTags = inspectorTerms(tags.value); RoomGridUx.undoable(activeStore, tx(`@${id} 资料已保存`, `@${id} profile saved`, `Profil von @${id} gespeichert`), () => { if (!inspectorRoom(id)) activeStore.addRoom(id); activeStore.patchRoom(id, { notes: notes.value, localTags: nextTags, rating: Number(rating.value) || 0, priority: Number(priority.value) || 0, watchlist: watchlist.checked, archived: archived.checked }); const current = inspectorRoom(id); if (current && roomInGroup(current, FAVORITE_GROUP_ID) !== favorite.checked) activeStore.toggleRoomInGroup(id, FAVORITE_GROUP_ID); groupChecks.forEach((input, groupId) => { const latest = inspectorRoom(id); if (latest && roomInGroup(latest, groupId) !== input.checked) activeStore.toggleRoomInGroup(id, groupId); }); }); }; const organizePanel = $('section', { id: `rg-inspector-${id}-organize`, class: 'rg-inspector-panel', dataset: { inspectorPanel: 'organize' }, role: 'tabpanel', hidden: true }, [ $('section', { class: 'rg-inspector-card' }, [ $('strong', {}, tx('私人资料库', 'Private library data', 'Private Bibliotheksdaten')), $('div', { class: 'rg-inspector-fields', style: { marginTop: '9px' } }, [ $('label', {}, [tx('私人备注', 'Private notes', 'Private Notizen'), notes]), $('label', {}, [tx('自己的标签', 'Your tags', 'Eigene Tags'), tags, $('datalist', { id: `rg-inspector-tags-${id}` }, tagSuggestions.map(value => $('option', { value })))]), $('label', {}, [tx('评分', 'Rating', 'Bewertung'), rating]), $('label', {}, [tx('优先级', 'Priority', 'Priorität'), priority]), ]), $('div', { class: 'rg-inspector-checks' }, [ $('label', {}, [favorite, tx('收藏', 'Favorite', 'Favorit')]), $('label', {}, [watchlist, tx('观察列表', 'Watchlist', 'Merkliste')]), $('label', {}, [archived, tx('已归档', 'Archived', 'Archiviert')]), ...customGroups.map(group => $('label', {}, [groupChecks.get(group.id), rawUiText(group.name)])), ]), $('div', { class: 'rg-inspector-actions', style: { marginTop: '10px' } }, [ $('button', { type: 'button', onclick: () => { notes.value = `${notes.value}${notes.value ? '\n' : ''}${tx('再次查看', 'Watch again', 'Erneut ansehen')}: `; notes.focus(); } }, tx('再次查看', 'Watch again', 'Erneut ansehen')), $('button', { type: 'button', onclick: () => { notes.value = `${notes.value}${notes.value ? '\n' : ''}${tx('技术备注', 'Technical note', 'Techniknotiz')}: `; notes.focus(); } }, tx('技术', 'Technical', 'Technik')), $('button', { class: 'primary', type: 'button', onclick: saveOrganization }, tx('保存所有更改', 'Save all changes', 'Alle Änderungen speichern')), ]), ]), ]); const activityBody = $('div', { class: 'rg-inspector-timeline' }, [$('div', { class: 'rg-inspector-empty' }, tx('本地活动正在加载…', 'Loading local activity…', 'Lokale Aktivität wird geladen …'))]); const activityPanel = $('section', { id: `rg-inspector-${id}-activity`, class: 'rg-inspector-panel', dataset: { inspectorPanel: 'activity' }, role: 'tabpanel', hidden: true }, [ $('section', { class: 'rg-inspector-card' }, [$('strong', {}, tx('活动和播放质量', 'Activity and playback quality', 'Aktivität und Wiedergabequalität')), activityBody]), ]); Promise.all([ activeAnalytics?.getEvents?.({ id, limit: 30 }) || Promise.resolve([]), activeAnalytics?.getQualitySamples?.({ id, limit: 500 }) || Promise.resolve([]), ]).then(([events, samples]) => { if (!activityBody.isConnected) return; const frames = samples.reduce((sum, item) => sum + Number(item.totalFrames || 0), 0); const dropped = samples.reduce((sum, item) => sum + Number(item.droppedFrames || 0), 0); const stalls = samples.reduce((sum, item) => sum + Number(item.stalls || 0), 0); const reconnects = samples.reduce((sum, item) => sum + Number(item.reconnects || 0), 0); const startup = samples.length ? samples.reduce((sum, item) => sum + Number(item.startupMs || 0), 0) / samples.length : 0; const onlineHours = (model.onlineHours || []).map((value, index) => ({ value: Number(value) || 0, index })).sort((a, b) => b.value - a.value).slice(0, 3).filter(item => item.value > 0); const dayNames = [tx('周日', 'Sun', 'So'), tx('周一', 'Mon', 'Mo'), tx('周二', 'Tue', 'Di'), tx('周三', 'Wed', 'Mi'), tx('周四', 'Thu', 'Do'), tx('周五', 'Fri', 'Fr'), tx('周六', 'Sat', 'Sa')]; const summary = $('div', { class: 'rg-inspector-grid' }, [ stat(tx('质量样本', 'Quality samples', 'Qualitätsproben'), samples.length), stat(tx('平均启动时间', 'Average startup', 'Ø Startzeit'), samples.length ? `${Math.round(startup)} ms` : '—'), stat(tx('卡顿', 'Stalls', 'Pufferstopps'), stalls), stat(tx('重新连接', 'Reconnects', 'Neu verbunden'), reconnects), stat(tx('丢帧', 'Dropped frames', 'Bildverlust'), frames ? `${(dropped / frames * 100).toFixed(2)} %` : '—'), stat(tx('在线观察', 'Online observations', 'Online-Beobachtungen'), model.onlineSamples || 0), ]); const likely = onlineHours.length ? $('div', { class: 'rg-inspector-facts' }, [ $('strong', {}, tx('经常观察到的在线时间', 'Frequently observed online times', 'Häufig beobachtete Onlinezeiten')), ...onlineHours.map(item => $('span', { class: 'rg-inspector-fact' }, `${dayNames[Math.floor(item.index / 24)]} ${String(item.index % 24).padStart(2, '0')}:00 · ${item.value}×`)), ]) : $('div', { class: 'rg-inspector-empty' }, tx('在线观察数据还不够。', 'Not enough online observations yet.', 'Noch nicht genug Online-Beobachtungen.')); const eventLabels = { visit: tx('观看', 'Watched', 'Angesehen'), preview: tx('预览', 'Preview', 'Vorschau'), quickview: tx('快速信息', 'Quick view', 'Schnellinfo'), discovered: tx('发现', 'Discovered', 'Entdeckt'), online: 'Live', player: 'Player', quality: tx('质量', 'Quality', 'Qualität'), connection: tx('连接', 'Connection', 'Verbindung') }; const timeline = events.length ? events.map(event => $('div', { class: 'rg-inspector-event' }, [ $('small', {}, inspectorDate(event.at)), $('span', {}, eventLabels[event.type] || event.type), $('span', {}, event.durationMs ? formatCompactDuration(event.durationMs) : rawUiText(String(event.detail || event.status || ''))), ])) : [$('div', { class: 'rg-inspector-empty' }, tx('尚无本地活动记录。', 'No local activity recorded yet.', 'Noch keine lokale Aktivität aufgezeichnet.'))]; activityBody.replaceChildren(summary, likely, $('strong', {}, tx('最近事件', 'Recent events', 'Letzte Ereignisse')), ...timeline); }).catch(() => { if (activityBody.isConnected) activityBody.replaceChildren($('div', { class: 'rg-inspector-empty' }, tx('活动数据不可用。', 'Activity data unavailable.', 'Aktivitätsdaten nicht verfügbar.'))); }); const referenceTags = new Set([...(room?.localTags || []), ...(model.tags || [])].map(value => String(value).toLocaleLowerCase())); const similar = (activeStore?.state?.rooms || []).filter(item => item.id !== id).map(item => { const itemModel = activeAnalytics?.getModelSync?.(item.id) || {}; const itemTags = [...new Set([...(item.localTags || []), ...(itemModel.tags || [])].map(value => String(value).toLocaleLowerCase()))]; const overlapTags = itemTags.filter(value => referenceTags.has(value)); const metadata = (model.country && itemModel.country === model.country ? 2 : 0) + (model.gender && itemModel.gender === model.gender ? 1 : 0); return { item, score: overlapTags.length * 3 + metadata, overlapTags, sameCountry: !!model.country && itemModel.country === model.country, sameType: !!model.gender && itemModel.gender === model.gender }; }).filter(entry => entry.score > 0).sort((left, right) => right.score - left.score || Number(right.item.rating || 0) - Number(left.item.rating || 0)).slice(0, 12); const similarPanel = $('section', { id: `rg-inspector-${id}-similar`, class: 'rg-inspector-panel', dataset: { inspectorPanel: 'similar' }, role: 'tabpanel', hidden: true }, [ $('section', { class: 'rg-inspector-card' }, [ $('strong', {}, tx('相似模型', 'Similar models', 'Ähnliche Models')), $('p', { style: { color: '#94a3b8' } }, tx('根据本地标签、公开标签、国家和类型计算;每个建议都会显示原因,且不会上传任何数据。', 'Calculated from local/public tags, country and type. Every suggestion shows its reason, and no data is uploaded.', 'Berechnet aus lokalen/öffentlichen Tags, Land und Typ. Jeder Vorschlag zeigt seine Begründung; es werden keine Daten hochgeladen.')), $('div', { class: 'rg-inspector-timeline' }, similar.length ? similar.map(entry => $('div', { class: 'rg-inspector-event' }, [ $('button', { type: 'button', onclick: () => openInspector(entry.item.id, { contextIds: [id, ...similar.map(item => item.item.id)] }) }, rawUiText(`@${entry.item.id}`)), $('span', {}, rawUiText([entry.overlapTags.length ? `${entry.overlapTags.length} ${tx('共同标签', 'shared tags', 'gemeinsame Tags')}` : '', entry.sameCountry ? tx('同一国家', 'same country', 'gleiches Land') : '', entry.sameType ? tx('同一类型', 'same type', 'gleicher Typ') : ''].filter(Boolean).join(' · '))), $('button', { type: 'button', onclick: () => toggleCompare(entry.item.id) }, tx('比较', 'Compare', 'Vergleichen')), ])) : [$('div', { class: 'rg-inspector-empty' }, tx('还没有足够的匹配数据。', 'Not enough matching data yet.', 'Noch nicht genug passende Daten.'))]), ]), ]); const panels = [overview, profilePanel, organizePanel, activityPanel, similarPanel]; const tabs = [ ['overview', tx('概览', 'Overview', 'Übersicht')], ['profile', tx('资料', 'Profile', 'Profil')], ['organize', tx('整理', 'Organize', 'Organisieren')], ['activity', tx('活动', 'Activity', 'Aktivität')], ['similar', tx('相似', 'Similar', 'Ähnlich')], ]; const tabButtons = new Map(); const activateTab = name => { if (!tabs.some(([value]) => value === name)) name = 'overview'; panels.forEach(panel => { const selected = panel.dataset.inspectorPanel === name; panel.hidden = !selected; }); tabButtons.forEach((button, value) => { const selected = value === name; button.setAttribute('aria-selected', String(selected)); button.tabIndex = selected ? 0 : -1; }); if (activeStore.state.settings.inspectorLastTab !== name) activeStore.patchSettings({ inspectorLastTab: name }); }; const tablist = $('div', { class: 'rg-inspector-tabs', role: 'tablist', ariaLabel: tx('模型详情区域', 'Model detail sections', 'Bereiche der Modeldetails') }, tabs.map(([name, label]) => { const button = $('button', { id: `rg-inspector-${id}-tab-${name}`, type: 'button', role: 'tab', 'aria-controls': `rg-inspector-${id}-${name}`, dataset: { inspectorTab: name }, onclick: () => activateTab(name) }, label); tabButtons.set(name, button); return button; })); const activeCompare = compareIds().includes(id); const compareButton = $('button', { type: 'button', dataset: { roomgridCompareModel: id }, ariaPressed: String(activeCompare), onclick: () => toggleCompare(id) }, activeCompare ? tx('从比较中移除', 'Remove from comparison', 'Aus Vergleich entfernen') : tx('加入比较', 'Add to comparison', 'Zum Vergleich')); const drawer = $('aside', { class: 'rg-inspector rg-inspector-v2', role: 'dialog', ariaModal: 'true', ariaLabel: tx(`@${id} 模型详情`, `Model details for @${id}`, `Modeldetails für @${id}`) }, [ $('div', { class: 'rg-inspector-head' }, [ $('div', { class: 'rg-inspector-head-nav' }, [ $('button', { type: 'button', disabled: inspectorContextIds.length < 2, title: tx('上一个模型', 'Previous model', 'Vorheriges Model'), onclick: () => navigate(-1) }, '‹'), $('button', { type: 'button', disabled: inspectorContextIds.length < 2, title: tx('下一个模型', 'Next model', 'Nächstes Model'), onclick: () => navigate(1) }, '›'), ]), $('div', { class: 'rg-inspector-title' }, [$('strong', {}, rawUiText(profile.displayName || `@${id}`)), $('small', {}, `${position + 1}/${inspectorContextIds.length} · @${id}`)]), $('button', { type: 'button', onclick: close }, tx('关闭', 'Close', 'Schließen')), ]), tablist, ...panels, $('div', { class: 'rg-inspector-footer' }, [ $('button', { class: 'primary', type: 'button', onclick: () => openNoopener(roomGridSingleCamUrl(id, { source: 'inspector' })) }, tx('打开房间', 'Open room', 'Raum öffnen')), $('button', { type: 'button', onclick: () => openPreview(id) }, tx('固定预览', 'Pin preview', 'Vorschau anheften')), $('button', { type: 'button', onclick: () => { if (!inspectorRoom(id)) activeStore.addRoom(id); } }, 'MultiCam +'), compareButton, ]), ]); backdrop.appendChild(drawer); backdrop.addEventListener('click', event => { if (event.target === backdrop) close(); }); backdrop.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); close(); return; } if (!event.target.closest('input,textarea,select') && event.key === 'ArrowLeft') { event.preventDefault(); navigate(-1); } if (!event.target.closest('input,textarea,select') && event.key === 'ArrowRight') { event.preventDefault(); navigate(1); } trapInspectorFocus(drawer, event); }); document.body.appendChild(backdrop); activateTab(activeStore.state.settings.inspectorLastTab || 'overview'); drawer.querySelector('.rg-inspector-head button')?.focus(); activeAnalytics?.recordEvent?.({ type: 'quickview', id, at: Date.now(), detail: 'model-inspector-v2' }); fetchContextInfo(id).then(data => { if (!facts.isConnected) return; const items = contextFacts(data); facts.replaceChildren(...(items.length ? items.map(([label, value]) => $('span', { class: 'rg-inspector-fact' }, `${label}: ${String(value).slice(0, 100)}`)) : [$('span', { class: 'rg-inspector-fact' }, tx('没有可用的公开字段', 'No public fields available', 'Keine öffentlichen Felder verfügbar'))])); }).catch(() => { if (facts.isConnected) facts.replaceChildren($('span', { class: 'rg-inspector-fact' }, tx('目前没有公开信息', 'Public facts currently unavailable', 'Öffentliche Angaben derzeit nicht verfügbar'))); }); return true; } function installChatFocus(store) { chatCleanup?.(); chatCleanup = null; if (!store?.state?.settings?.chatFocusEnabled) { document.body?.classList.remove('rg-chat-focus','rg-chat-system-dim','rg-chat-system-hide'); return () => {}; } const seen = new WeakSet(); const allNodes = new Set(); const repeated = new Map(); let query = ''; let needsDivider = false; const toolbar = $('div', { class: 'rg-chat-toolbar', ariaLabel: tx('会话聊天搜索', 'Session chat search', 'Chat-Sitzungssuche') }); const search = $('input', { type: 'search', placeholder: tx('仅搜索当前会话…', 'Search this session only…', 'Nur diese Sitzung durchsuchen…'), autocomplete: 'off' }); const refreshNodeVisibility = node => { node.style.display = !query || String(node.textContent || '').toLocaleLowerCase().includes(query) ? '' : 'none'; }; const clearSearch = $('button', { type: 'button', onclick: () => { search.value = ''; query = ''; allNodes.forEach(refreshNodeVisibility); } }, '×'); toolbar.append(search, clearSearch); document.body.appendChild(toolbar); search.addEventListener('input', debounce(() => { query = search.value.trim().toLocaleLowerCase(); allNodes.forEach(refreshNodeVisibility); }, 120)); const applyMode = () => { document.body.classList.add('rg-chat-focus'); document.body.classList.toggle('rg-chat-system-dim', store.state.settings.chatSystemMode === 'dim'); document.body.classList.toggle('rg-chat-system-hide', store.state.settings.chatSystemMode === 'hide'); }; const scan = root => { const candidates = root?.querySelectorAll?.('[data-testid*="chat-message" i], [class*="ChatMessage"], [class*="chatMessage"], li[class*="message" i]') || []; let count = 0; for (const node of candidates) { if (++count > 600 || seen.has(node) || node.closest('[data-roomgrid-control-center]')) continue; seen.add(node); allNodes.add(node); const descriptor = `${node.className || ''} ${node.getAttribute?.('data-testid') || ''} ${node.textContent || ''}`.slice(0, 500); const text = String(node.textContent || '').replace(/\s+/gu, ' ').trim().toLocaleLowerCase(); const settings = store.state.settings; const highlights = parseTerms(settings.chatHighlightWords || '').map(value => value.toLocaleLowerCase()); const muted = parseTerms(settings.chatMutedWords || '').map(value => value.toLocaleLowerCase()); if (/notice|system|tip menu|entered|left|joined|app|bot|followed/iu.test(descriptor)) node.dataset.roomgridSystemMessage = '1'; if (/mention|highlight|private[-_ ]?message|\bpm\b/iu.test(`${node.className || ''} ${node.getAttribute?.('data-testid') || ''}`)) node.classList.add('rg-chat-mentioned'); if (highlights.some(term => text.includes(term))) node.classList.add('rg-chat-highlighted'); if (muted.some(term => text.includes(term))) node.classList.add('rg-chat-muted'); const level = settings.chatFilterLevel || 'low'; if ((level === 'medium' || level === 'strict') && /(^|\s)(app|bot|notice)(:|\s)|joined the room|left the room/iu.test(text)) node.classList.add('rg-chat-muted'); if (level === 'strict' && /tip menu|followed|has tipped|tokens? remaining|goal:/iu.test(text)) node.classList.add('rg-chat-muted'); if (settings.chatCollapseRepeats !== false && text.length >= 4) { const count = (repeated.get(text) || 0) + 1; repeated.set(text, count); if (count > 1) node.classList.add('rg-chat-collapsed'); } if (needsDivider && !document.hidden) { node.before($('div', { class: 'rg-chat-unread' }, tx('新消息', 'New messages', 'Neue Nachrichten'))); needsDivider = false; } if (store.state.settings.chatTimestampEnabled !== false && !node.querySelector('.rg-chat-time')) { node.prepend($('time', { class: 'rg-chat-time', dateTime: new Date().toISOString() }, new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }))); } } }; applyMode(); scan(document); const observer = new MutationObserver(mutations => mutations.forEach(mutation => mutation.addedNodes.forEach(node => { if (node.nodeType === 1) scan(node.parentElement || node); }))); observer.observe(document.body, { childList: true, subtree: true }); const unsub = store.subscribe(() => { applyMode(); scan(document); }); const visibility = () => { if (document.hidden) needsDivider = true; }; document.addEventListener('visibilitychange', visibility); chatCleanup = () => { observer.disconnect(); unsub(); document.removeEventListener('visibilitychange', visibility); toolbar.remove(); document.body.classList.remove('rg-chat-focus','rg-chat-system-dim','rg-chat-system-hide'); document.querySelectorAll('.rg-chat-time,.rg-chat-unread').forEach(node => node.remove()); document.querySelectorAll('.rg-chat-mentioned,.rg-chat-highlighted,.rg-chat-muted,.rg-chat-collapsed').forEach(node => node.classList.remove('rg-chat-mentioned','rg-chat-highlighted','rg-chat-muted','rg-chat-collapsed')); document.querySelectorAll('[data-roomgrid-system-message]').forEach(node => delete node.dataset.roomgridSystemMessage); allNodes.forEach(node => node.style.removeProperty('display')); }; activeAnalytics?.recordEvent?.({ type: 'chat_focus', at: Date.now(), detail: 'enabled' }); return chatCleanup; } function formatCompactDuration(ms) { const minutes = Math.round(Math.max(0, Number(ms) || 0) / 60000); return minutes >= 60 ? `${Math.floor(minutes / 60)} h ${minutes % 60} min` : `${minutes} min`; } function installWellbeing(store, analytics) { wellbeingCleanup?.(); let activeSince = Date.now(); let checking = false; const check = async () => { if (document.hidden || checking) { activeSince = Date.now(); return; } checking = true; try { const settings = store.state.settings; const breakMinutes = clampInt(settings.breakReminderMinutes, 0, 480, 0); if (breakMinutes && Date.now() - activeSince >= breakMinutes * 60000) { serviceNotice(tx('休息一下:活动提醒时间到了。', 'Time for a short break.', 'Zeit für eine kurze Pause.'), 6500); activeSince = Date.now(); } const budget = clampInt(settings.dailyTimeBudgetMinutes, 0, 1440, 0); if (budget) { const day = new Date(); const dayKey = `${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()}`; const noticeKey = `roomgrid_budget_notice_${dayKey}`; if (sessionStorage.getItem(noticeKey) !== '1') { const from = new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime(); const events = await analytics.getEvents({ type: 'usage', from, limit: 5000 }); const used = events.reduce((sum, event) => sum + Math.max(0, Number(event.durationMs) || 0), 0); if (used >= budget * 60000) { sessionStorage.setItem(noticeKey, '1'); serviceNotice(tx('你今天设定的 RoomGrid 时间预算已达到。', 'Your RoomGrid time budget for today has been reached.', 'Dein RoomGrid-Zeitbudget für heute ist erreicht.'), 8000); } } } } catch (_) {} finally { checking = false; } }; const timer = setInterval(check, 60000); const visibility = () => { if (!document.hidden) activeSince = Date.now(); }; document.addEventListener('visibilitychange', visibility); wellbeingCleanup = () => { clearInterval(timer); document.removeEventListener('visibilitychange', visibility); }; } function installAutomationRules(store, analytics) { automationCleanup?.(); const notified = new Set(); let busy = false; let disposed = false; const evaluate = debounce(() => { if (disposed || busy) return; busy = true; try { const rules = (store.state.settings.automationRules || []).filter(rule => rule.enabled !== false); if (!rules.length) return; for (const room of store.state.rooms) { const model = analytics?.getModelSync?.(room.id) || {}; const tags = new Set([...(room.localTags || []), ...(model.tags || [])].map(value => String(value).toLocaleLowerCase())); const country = String(model.country || '').toLocaleLowerCase(); for (const rule of rules) { if (rule.tags?.length && !rule.tags.every(tag => tags.has(String(tag).toLocaleLowerCase()))) continue; if (rule.countries?.length && !rule.countries.some(value => country.includes(String(value).toLocaleLowerCase()))) continue; if (Number(rule.minViewers || 0) > Number(model.viewerCount || room.viewerCount || 0)) continue; if (rule.action === 'favorite' && !roomInGroup(room, FAVORITE_GROUP_ID)) store.toggleRoomInGroup(room.id, FAVORITE_GROUP_ID); else if (rule.action === 'group' && rule.groupId && !roomInGroup(room, rule.groupId) && store.state.groups.some(group => group.id === rule.groupId)) store.toggleRoomInGroup(room.id, rule.groupId); else if (rule.action === 'notify') { const key = `${rule.id}:${room.id}`; if (!notified.has(key)) { notified.add(key); serviceNotice(`${rawUiText(rule.name)} · ${room.id}`, 5500); } } } } } finally { busy = false; } }, 350); const unsubscribe = store.subscribe(evaluate); const timer = setInterval(evaluate, 30000); evaluate(); automationCleanup = () => { disposed = true; unsubscribe(); clearInterval(timer); }; } function installAdaptivePerformance(store) { performanceCleanup?.(); let baseline = null; let autoApplied = false; let applied = null; const restoreBaseline = () => { const restore = {}; if (baseline && applied) for (const key of Object.keys(baseline)) { if (store.state.settings[key] === applied[key]) restore[key] = baseline[key]; } baseline = null; applied = null; autoApplied = false; if (Object.keys(restore).length) store.patchSettings(restore); return Object.keys(restore).length > 0; }; const apply = () => { const settings = store.state.settings; if (!settings.autoPerformanceMode) { restoreBaseline(); document.documentElement.dataset.roomgridAutoPerformance = 'off'; return; } const memory = performance?.memory; const heapRatio = memory?.jsHeapSizeLimit ? Number(memory.usedJSHeapSize || 0) / Number(memory.jsHeapSizeLimit) : 0; const playing = [...document.querySelectorAll('video')].filter(video => !video.paused && !video.ended).length; const constrained = document.hidden || heapRatio * 100 >= Number(settings.autoPerformanceMemoryPct || 78) || playing >= Number(settings.autoPerformanceStreamThreshold || 10); document.documentElement.dataset.roomgridAutoPerformance = constrained ? 'eco' : 'normal'; if (constrained && !autoApplied) { baseline = { performanceProfile: settings.performanceProfile, performanceMaxStreams: settings.performanceMaxStreams, maxStreamHeight: settings.maxStreamHeight }; autoApplied = true; applied = { performanceProfile: 'eco', performanceMaxStreams: Math.min(4, Number(settings.performanceMaxStreams) || 4), maxStreamHeight: Math.min(480, Number(settings.maxStreamHeight) || 480) }; store.patchSettings(applied); } else if (!constrained && autoApplied) { restoreBaseline(); } }; const timer = setInterval(apply, 10000); const visibility = () => apply(); document.addEventListener('visibilitychange', visibility); apply(); performanceCleanup = () => { clearInterval(timer); document.removeEventListener('visibilitychange', visibility); if (restoreBaseline()) store.flushPersistence?.(); document.documentElement.dataset.roomgridAutoPerformance = 'off'; }; } function init(store, analytics) { activeStore = store; activeAnalytics = analytics; installStyles(); ultimateStoreUnsubscribe?.(); let compareSignature = JSON.stringify(compareIds()); ultimateStoreUnsubscribe = store.subscribe((_state, path) => { if (path !== 'all' && !String(path || '').startsWith('settings')) return; const nextSignature = JSON.stringify(compareIds()); if (nextSignature !== compareSignature) { compareSignature = nextSignature; renderCompareTray(); } }); renderCompareTray(); if (!accountStats) { accountStats = createAccountStats(analytics); accountStats.init().catch(() => {}); } installChatFocus(store); installWellbeing(store, analytics); installAutomationRules(store, analytics); installAdaptivePerformance(store); return () => { wellbeingCleanup?.(); wellbeingCleanup = null; automationCleanup?.(); automationCleanup = null; performanceCleanup?.(); performanceCleanup = null; chatCleanup?.(); chatCleanup = null; ultimateStoreUnsubscribe?.(); ultimateStoreUnsubscribe = null; inspector?.remove(); inspector = null; compareTray?.remove(); compareTray = null; stopInlineHoverPreview(); [...previews.values()].forEach(hardStopPreview); previewRoot?.remove(); previewRoot = null; }; } document.addEventListener?.('visibilitychange', () => { if (document.hidden) stopInlineHoverPreview(); for (const entry of previews.values()) { if (document.hidden) { try { entry.video.pause(); entry.hls?.stopLoad?.(); } catch (_) {} } else if (!entry.userPaused) { try { entry.hls?.startLoad?.(); entry.video.play().catch(() => {}); } catch (_) {} } } }); return { init, openPreview, openInspector, openComparison, toggleCompare, startInlineHoverPreview, stopInlineHoverPreview, capabilities, comparisonStatus() { return { ids: compareIds(), count: compareIds().length, maximum: 4 }; }, getAccountStats(analytics = activeAnalytics) { if (!accountStats) { accountStats = createAccountStats(analytics); accountStats.init().catch(() => {}); } return accountStats; }, previewStatus() { return { active: previews.size, max: clampInt(activeStore?.state?.settings?.previewDockMax, 1, 6, 3) }; }, closeAllPreviews() { [...previews.values()].forEach(hardStopPreview); }, refreshChatFocus() { if (activeStore) installChatFocus(activeStore); }, }; })(); /* ============================================================= * Shared experience services: safe actions and data provenance * ============================================================= */ const RoomGridUx = (() => { let host = null; let style = null; const actions = []; function ensure() { if (!style?.isConnected) { style = $('style', { id: 'roomgrid-ux-style', html: trustedHtml(` .rgux-toast-host{position:fixed;right:18px;bottom:18px;z-index:2147483600;display:grid;gap:9px;width:min(420px,calc(100vw - 24px));pointer-events:none;font:13px/1.4 Arial,Helvetica,sans-serif;color-scheme:dark} body:has(.roomgrid-dock:not(.is-collapsed)) .rgux-toast-host{bottom:164px} .rgux-toast{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:12px 13px;border:1px solid var(--rg-border,#536273);border-left:4px solid var(--rg-secondary,#69c9f2);border-radius:7px;background:var(--rg-panel,#172533);color:var(--rg-text,#f1f5f8);box-shadow:0 14px 34px rgba(0,0,0,.42);pointer-events:auto;animation:rgux-in .16s ease-out} .rgux-toast[data-tone="success"]{border-left-color:#42b883}.rgux-toast[data-tone="warning"]{border-left-color:#f59e0b}.rgux-toast[data-tone="danger"]{border-left-color:#fb7185} .rgux-toast-message{min-width:0;overflow-wrap:anywhere}.rgux-toast-message strong{display:block;font-size:13px}.rgux-toast-message small{display:block;margin-top:2px;color:var(--rg-muted,#aeb9c3);font-size:11px} .rgux-toast-action,.rgux-toast-close{min-height:32px;padding:6px 9px;border:1px solid var(--rg-border,#536273);border-radius:5px;background:var(--rg-surface,#233443);color:var(--rg-text,#fff);font:800 11px/1 Arial,Helvetica,sans-serif;cursor:pointer}.rgux-toast-action:hover,.rgux-toast-close:hover{border-color:var(--rg-secondary,#69c9f2)}.rgux-toast-close{width:32px;padding:0;font-size:17px} @keyframes rgux-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}} @media(max-width:660px){.rgux-toast-host{right:8px;bottom:8px;width:calc(100vw - 16px)}body:has(.roomgrid-dock:not(.is-collapsed)) .rgux-toast-host{bottom:86px}.rgux-toast{grid-template-columns:minmax(0,1fr) auto}.rgux-toast-close{display:none}} `) }); document.head?.appendChild(style); } if (!host?.isConnected) { host = $('div', { class: 'rgux-toast-host', dataset: { roomgridToastHost: '1' }, role: 'region', ariaLabel: tx('RoomGrid 操作消息', 'RoomGrid action messages', 'RoomGrid-Aktionsmeldungen'), ariaLive: 'polite' }); document.body?.appendChild(host); } return host; } function notify(message, options = {}) { const target = ensure(); if (!target) return () => {}; const tone = ['success', 'warning', 'danger'].includes(options.tone) ? options.tone : 'info'; const entry = { id: uuid(), at: Date.now(), message: String(message || ''), tone, action: String(options.actionLabel || '') }; actions.push(entry); if (actions.length > 40) actions.shift(); const toast = $('section', { class: 'rgux-toast', dataset: { tone, roomgridToast: entry.id }, role: tone === 'danger' ? 'alert' : 'status' }); let timer = 0; const close = () => { clearTimeout(timer); toast.remove(); }; const messageNode = $('div', { class: 'rgux-toast-message' }, [ $('strong', {}, rawUiText(message)), options.detail ? $('small', {}, rawUiText(options.detail)) : null, ]); const actionButton = options.actionLabel && typeof options.onAction === 'function' ? $('button', { class: 'rgux-toast-action', type: 'button', onclick: () => { close(); options.onAction(); } }, rawUiText(options.actionLabel)) : null; toast.append(messageNode, ...(actionButton ? [actionButton] : []), $('button', { class: 'rgux-toast-close', type: 'button', ariaLabel: tx('关闭', 'Close', 'Schließen'), onclick: close }, '×')); target.appendChild(toast); while (target.children.length > 4) target.firstElementChild?.remove(); timer = setTimeout(close, clampInt(options.duration, 1800, 30000, actionButton ? 10000 : 4200)); return close; } function undoable(store, message, mutate, options = {}) { if (!store || typeof mutate !== 'function') return undefined; const snapshot = store.snapshot?.(); const beforeRevision = Number(store.revision) || 0; const result = mutate(); const afterRevision = Number(store.revision) || 0; if (!snapshot || afterRevision === beforeRevision) return result; notify(message, { tone: options.tone || 'success', detail: options.detail || tx('该更改会自动保存。', 'This change is saved automatically.', 'Diese Änderung wird automatisch gespeichert.'), actionLabel: tx('撤销', 'Undo', 'Rückgängig'), duration: options.duration || 12000, onAction: () => { if (store.restoreSnapshot?.(snapshot, afterRevision)) { notify(tx('更改已撤销', 'Change undone', 'Änderung rückgängig gemacht'), { tone: 'success' }); options.onUndo?.(); } else { notify(tx('由于之后已有其他更改,未自动撤销。', 'Undo was not applied because newer changes already exist.', 'Nicht automatisch rückgängig gemacht, weil inzwischen weitere Änderungen vorliegen.'), { tone: 'warning', duration: 7000 }); } }, }); return result; } function status() { return { recentActions: actions.slice(-20), visible: host?.children?.length || 0 }; } function clear() { host?.remove(); host = null; style?.remove(); style = null; } return { notify, undoable, status, clear }; })(); const RoomGridDataProvenance = (() => { const known = new Map([ ['native-home-catalog', [tx('Chaturbate 实时目录', 'Chaturbate live catalog', 'Chaturbate-Livekatalog'), 'public']], ['roomgrid-discover', [tx('RoomGrid 发现', 'RoomGrid Discover', 'RoomGrid Entdecken'), 'public']], ['native-discover', [tx('Chaturbate 发现页', 'Chaturbate Discover page', 'Chaturbate-Entdecken-Seite'), 'public']], ['account-following', [tx('已关注列表', 'Following list', 'Following-Liste'), 'account']], ['my-collection', [tx('我的收藏', 'My Collection', 'My Collection'), 'account']], ]); function sourceInfo(value) { const id = String(value || '').trim(); if (known.has(id)) { const [label, tone] = known.get(id); return { id, label, tone }; } if (/^native\/?/u.test(id)) { const path = id.replace(/^native\/?/u, '').replaceAll('-', ' ').replaceAll('_', ' ').trim(); return { id, label: path ? `${tx('Chaturbate 页面', 'Chaturbate page', 'Chaturbate-Seite')}: ${path}` : tx('Chaturbate 页面', 'Chaturbate page', 'Chaturbate-Seite'), tone: 'public' }; } return { id: id || 'local', label: id ? id.replaceAll('-', ' ').replaceAll('_', ' ') : tx('本地资料', 'Local record', 'Lokaler Datensatz'), tone: 'local' }; } function itemSources(item) { const sources = []; const push = source => { if (source?.id && !sources.some(entry => entry.id === source.id)) sources.push(source); }; if (item?.room) push({ id: 'organized', label: tx('个人整理', 'Personally organized', 'Persönlich organisiert'), tone: 'personal' }); (item?.model?.sources || []).forEach(value => push(sourceInfo(value))); if ((Number(item?.sessions) || 0) > 0) push({ id: 'watch-history', label: tx('本地观看记录', 'Local watch history', 'Lokaler Sehverlauf'), tone: 'local' }); if (item?.profile) push({ id: 'public-profile', label: tx('公开个人资料', 'Public profile', 'Öffentliches Profil'), tone: 'public' }); if ((item?.samples?.length || 0) > 0) push({ id: 'quality-samples', label: tx('本地质量测量', 'Local quality measurements', 'Lokale Qualitätsmessungen'), tone: 'local' }); if ((item?.model?.tags || []).includes('my-collection')) push(sourceInfo('my-collection')); return sources.length ? sources : [{ id: 'local', label: tx('本地模型索引', 'Local model index', 'Lokaler Modellindex'), tone: 'local' }]; } function freshness(item) { const at = Math.max(0, Number(item?.model?.lastObservedAt) || 0, Number(item?.lastActivity) || 0, Number(item?.room?.lastSeenOnline) || 0); if (['online', 'public'].includes(String(item?.status || ''))) return { at: at || Date.now(), tone: 'live', label: tx('当前直播信号', 'Current live signal', 'Aktuelles Live-Signal') }; const age = at ? Date.now() - at : Infinity; if (age <= 3600000) return { at, tone: 'fresh', label: tx('最近一小时', 'Within the last hour', 'Innerhalb der letzten Stunde') }; if (age <= 86400000) return { at, tone: 'recent', label: tx('最近 24 小时', 'Within the last 24 hours', 'Innerhalb der letzten 24 Stunden') }; if (age <= 30 * 86400000) return { at, tone: 'historic', label: tx('最近 30 天', 'Within the last 30 days', 'Innerhalb der letzten 30 Tage') }; return { at, tone: 'stale', label: at ? tx('历史记录', 'Historical record', 'Historischer Datensatz') : tx('时间未知', 'Time unknown', 'Zeitpunkt unbekannt') }; } function coverage(items) { const list = Array.isArray(items) ? items : []; const count = predicate => list.reduce((sum, item) => sum + (predicate(item) ? 1 : 0), 0); return { total: list.length, saved: count(item => !!item.room), live: count(item => ['online', 'public'].includes(String(item.status || ''))), watched: count(item => Number(item.sessions) > 0), profile: count(item => !!item.profile), age: count(item => Number(item.model?.age) >= 18), country: count(item => !!String(item.model?.country || '').trim()), tags: count(item => (item.tags || []).length > 0), sourced: count(item => (item.model?.sources || []).length > 0), }; } return { sourceInfo, itemSources, freshness, coverage }; })(); /* ============================================================= * 3.3.7. Dauerhaftes Archiv oeffentlicher Modell-Vorschaubilder * * Speichert oeffentliche JPEG-/Thumbnail-Endpunkte sowie Bilder, die der * Nutzer selbst importiert, sowie ausdrücklich erstellte Videoaufnahmen. * Automatische Vorschauen bleiben streng * begrenzt; eigene Bilder und die lokale Cover-Auswahl werden getrennt davon * und updatefest in IndexedDB abgelegt. * ============================================================= */ const RoomGridModelMedia = (() => { const DB_NAME = 'roomgrid_model_media_v1'; const DB_VERSION = 1; const MAX_PUBLIC_PER_MODEL = 4; // User-created/imported media is part of the personal gallery and must not // silently disappear. Public preview snapshots stay bounded separately. const MAX_USER_PER_MODEL = Number.MAX_SAFE_INTEGER; const MAX_RECORDS = 24000; const MAX_QUEUE = 480; const MAX_CONCURRENT = 2; const REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; const MAX_SOURCE_BYTES = 3 * 1024 * 1024; const MAX_USER_SOURCE_BYTES = 20 * 1024 * 1024; const MAX_WIDTH = 480; const MAX_HEIGHT = 270; let databasePromise = null; let active = 0; let queue = []; const pending = new Map(); const packPending = new Map(); const recentlyScheduled = new Map(); const requestResult = request => new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('IndexedDB request failed')); }); const transactionDone = transaction => new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(true); transaction.onerror = () => reject(transaction.error || new Error('IndexedDB transaction failed')); transaction.onabort = () => reject(transaction.error || new Error('IndexedDB transaction aborted')); }); function safeModelId(value) { const id = normalizeUsername(value); return isLikelyUsername(id) ? id : ''; } function safeSource(value, id) { try { const url = new URL(String(value || '')); if (url.origin === 'https://thumb.live.mmcdn.com' && /^\/riw?\/[a-zA-Z0-9_-]+\.jpg$/u.test(url.pathname)) return url.toString(); if (url.origin === 'https://jpeg.live.mmcdn.com' && url.pathname === '/stream' && normalizeUsername(url.searchParams.get('room')) === id) return url.toString(); } catch (_) {} return ''; } function openDatabase() { if (databasePromise) return databasePromise; if (!('indexedDB' in window)) return Promise.resolve(null); databasePromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const database = request.result; if (!database.objectStoreNames.contains('images')) { const store = database.createObjectStore('images', { keyPath: 'mediaId' }); store.createIndex('model', 'id', { unique: false }); store.createIndex('capturedAt', 'capturedAt', { unique: false }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('Model media database unavailable')); }).catch(error => { try { Diagnostics.report('model-media', error, 'local public-preview archive unavailable', 'warning'); } catch (_) {} return null; }); return databasePromise; } async function list(id) { id = safeModelId(id); const database = id ? await openDatabase() : null; if (!database) return []; try { const transaction = database.transaction('images', 'readonly'); const records = await requestResult(transaction.objectStore('images').index('model').getAll(IDBKeyRange.only(id))); return (records || []).filter(record => record?.blob instanceof Blob).sort((a, b) => { if (!!a.isCustomCover !== !!b.isCustomCover) return a.isCustomCover ? -1 : 1; if (!!a.userOwned !== !!b.userOwned) return a.userOwned ? -1 : 1; return Number(b.capturedAt) - Number(a.capturedAt); }); } catch (_) { return []; } } async function fetchBlob(url) { if (globalThis.GM?.xmlHttpRequest) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url, responseType: 'blob', timeout: 15000, anonymous: true, onload: response => response.status >= 200 && response.status < 300 && response.response instanceof Blob ? resolve(response.response) : reject(new Error(`Preview request failed (${response.status || 0})`)), onerror: () => reject(new Error('Preview request failed')), ontimeout: () => reject(new Error('Preview request timed out')), }); }); } const response = await fetch(url, { credentials: 'omit', cache: 'no-store', referrerPolicy: 'no-referrer' }); if (!response.ok) throw new Error(`Preview request failed (${response.status})`); return response.blob(); } async function imageSource(blob) { if ('createImageBitmap' in window) { const bitmap = await createImageBitmap(blob); return { source: bitmap, width: bitmap.width, height: bitmap.height, close: () => bitmap.close?.() }; } const url = URL.createObjectURL(blob); const image = new Image(); image.decoding = 'async'; image.src = url; try { await image.decode(); } catch (error) { URL.revokeObjectURL(url); throw error; } return { source: image, width: image.naturalWidth, height: image.naturalHeight, close: () => URL.revokeObjectURL(url) }; } async function compactImage(blob) { if (!(blob instanceof Blob) || !/^image\/(?:jpeg|png|webp)$/iu.test(blob.type) || !blob.size || blob.size > MAX_SOURCE_BYTES) throw new Error('Unsupported public preview image'); const decoded = await imageSource(blob); try { const scale = Math.min(1, MAX_WIDTH / Math.max(1, decoded.width), MAX_HEIGHT / Math.max(1, decoded.height)); const width = Math.max(1, Math.round(decoded.width * scale)); const height = Math.max(1, Math.round(decoded.height * scale)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d', { alpha: false }); if (!context) throw new Error('Canvas unavailable'); context.drawImage(decoded.source, 0, 0, width, height); const output = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', .66)); if (!(output instanceof Blob) || !output.size) throw new Error('Preview compression failed'); return { blob: output, width, height }; } finally { decoded.close(); } } async function compactUserImage(blob, preset = 'high') { if (!(blob instanceof Blob) || !/^image\/(?:jpeg|png|webp)$/iu.test(blob.type) || !blob.size || blob.size > MAX_USER_SOURCE_BYTES) throw new Error('Unsupported local image'); preset = ['original', 'high', 'balanced', 'compact'].includes(preset) ? preset : 'high'; const decoded = await imageSource(blob); try { if (decoded.width < 1 || decoded.height < 1 || decoded.width * decoded.height > 50000000) throw new Error('Local image dimensions are unsupported'); if (preset === 'original') return { blob, width: decoded.width, height: decoded.height, preset }; const settings = { high: { width: 3840, height: 2160, quality: .94 }, balanced: { width: 2560, height: 1440, quality: .86 }, compact: { width: 1600, height: 900, quality: .74 }, }[preset]; const scale = Math.min(1, settings.width / decoded.width, settings.height / decoded.height); const width = Math.max(1, Math.round(decoded.width * scale)); const height = Math.max(1, Math.round(decoded.height * scale)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d', { alpha: false }); if (!context) throw new Error('Canvas unavailable'); context.drawImage(decoded.source, 0, 0, width, height); const output = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', settings.quality)); if (!(output instanceof Blob) || !output.size) throw new Error('Local image processing failed'); return { blob: output, width, height, preset }; } finally { decoded.close(); } } async function contentHash(blob) { const bytes = await blob.arrayBuffer(); const digest = await crypto.subtle.digest('SHA-256', bytes); return [...new Uint8Array(digest).slice(0, 10)].map(value => value.toString(16).padStart(2, '0')).join(''); } async function pruneModel(database, id) { const transaction = database.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const records = await requestResult(store.index('model').getAll(IDBKeyRange.only(id))); const newestFirst = records.sort((a, b) => Number(b.capturedAt) - Number(a.capturedAt)); const publicRecords = newestFirst.filter(record => !record.userOwned && !record.isCustomCover); const userRecords = newestFirst.filter(record => record.userOwned && !record.isCustomCover); publicRecords.slice(MAX_PUBLIC_PER_MODEL).forEach(record => store.delete(record.mediaId)); userRecords.slice(MAX_USER_PER_MODEL).forEach(record => store.delete(record.mediaId)); await transactionDone(transaction); } async function pruneGlobal(database) { const countTransaction = database.transaction('images', 'readonly'); const count = await requestResult(countTransaction.objectStore('images').count()); if (count <= MAX_RECORDS) return; const deleteCount = Math.min(500, count - MAX_RECORDS); const transaction = database.transaction('images', 'readwrite'); const index = transaction.objectStore('images').index('capturedAt'); let removed = 0; await new Promise((resolve, reject) => { const request = index.openCursor(); request.onsuccess = event => { const cursor = event.target.result; if (!cursor || removed >= deleteCount) { resolve(); return; } if (!cursor.value?.userOwned && !cursor.value?.isCustomCover) { cursor.delete(); removed += 1; } cursor.continue(); }; request.onerror = () => reject(request.error || new Error('Media pruning failed')); }); await transactionDone(transaction); } async function saveOne(id, sourceUrl, kind) { id = safeModelId(id); sourceUrl = safeSource(sourceUrl, id); kind = ['cover', 'preview', 'snapshot'].includes(kind) ? kind : 'snapshot'; const database = id && sourceUrl ? await openDatabase() : null; if (!database) return null; const sourceBlob = await fetchBlob(sourceUrl); const compact = await compactImage(sourceBlob); const hash = await contentHash(compact.blob); const mediaId = `${id}:${kind}:${hash}`; const existingTransaction = database.transaction('images', 'readonly'); const existing = await requestResult(existingTransaction.objectStore('images').get(mediaId)); if (existing) return existing; const record = { mediaId, id, kind, hash, sourceUrl, capturedAt: Date.now(), width: compact.width, height: compact.height, bytes: compact.blob.size, blob: compact.blob }; const transaction = database.transaction('images', 'readwrite'); transaction.objectStore('images').put(record); await transactionDone(transaction); await pruneModel(database, id); if (Math.random() < .04) await pruneGlobal(database); window.dispatchEvent(new CustomEvent('roomgrid:model-media-saved', { detail: { id, kind, capturedAt: record.capturedAt } })); return record; } async function importFile(id, file, options = {}) { id = safeModelId(id); const database = id ? await openDatabase() : null; if (!database || !(file instanceof Blob)) throw new Error('Local image unavailable'); const compact = await compactUserImage(file, options.quality); const hash = await contentHash(compact.blob); // Explicit screenshots are separate archive items, even when the stream // has not changed. Ordinary image imports retain content deduplication. const mediaId = `${id}:upload:${hash}${options.keepDuplicate ? `:${crypto.randomUUID()}` : ''}`; const existingTransaction = database.transaction('images', 'readonly'); const existing = await requestResult(existingTransaction.objectStore('images').get(mediaId)); if (existing) { if (options.makeCover && !existing.isCustomCover) return setCustomCover(id, mediaId); return existing; } const fileName = String(options.fileName || file.name || 'local-image').replace(/[\u0000-\u001f\u007f]/gu, ' ').trim().slice(0, 180); const record = { mediaId, id, kind: 'upload', hash, sourceUrl: '', capturedAt: Date.now(), width: compact.width, height: compact.height, bytes: compact.blob.size, blob: compact.blob, userOwned: true, fileName, qualityPreset: compact.preset, isCustomCover: false, }; const transaction = database.transaction('images', 'readwrite'); transaction.objectStore('images').put(record); await transactionDone(transaction); await pruneModel(database, id); const saved = options.makeCover ? await setCustomCover(id, mediaId) : record; window.dispatchEvent(new CustomEvent('roomgrid:model-media-saved', { detail: { id, kind: 'upload', capturedAt: record.capturedAt } })); return saved; } async function importUrl(id, sourceUrl, options = {}) { id = safeModelId(id); sourceUrl = safeSource(sourceUrl, id); if (!id || !sourceUrl) throw new Error('Public image unavailable'); const blob = await fetchBlob(sourceUrl); return importFile(id, blob, { ...options, fileName: options.fileName || `camclient-${id}-${Date.now()}.webp`, }); } async function setCustomCover(id, mediaId) { id = safeModelId(id); mediaId = String(mediaId || ''); const database = id && mediaId ? await openDatabase() : null; if (!database) return null; const transaction = database.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const records = await requestResult(store.index('model').getAll(IDBKeyRange.only(id))); const selected = records.find(record => record.mediaId === mediaId && record.blob instanceof Blob && record.blob.type.startsWith('image/')); if (!selected) { transaction.abort(); return null; } records.forEach(record => { const next = record.mediaId === mediaId; if (!!record.isCustomCover !== next) store.put({ ...record, isCustomCover: next, coverUpdatedAt: next ? Date.now() : 0 }); }); await transactionDone(transaction); window.dispatchEvent(new CustomEvent('roomgrid:model-cover-changed', { detail: { id, mediaId } })); return { ...selected, isCustomCover: true, coverUpdatedAt: Date.now() }; } async function clearCustomCover(id) { id = safeModelId(id); const database = id ? await openDatabase() : null; if (!database) return false; const transaction = database.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const records = await requestResult(store.index('model').getAll(IDBKeyRange.only(id))); records.filter(record => record.isCustomCover).forEach(record => store.put({ ...record, isCustomCover: false, coverUpdatedAt: 0 })); await transactionDone(transaction); window.dispatchEvent(new CustomEvent('roomgrid:model-cover-changed', { detail: { id, mediaId: '' } })); return true; } async function getCustomCover(id) { return (await list(id)).find(record => record.isCustomCover && record.blob.type.startsWith('image/')) || null; } // Recordings are user-owned: the automatic preview pruning never removes // them. The existing Blob store requires no database/key migration. async function importVideo(id, blob, options = {}) { id = safeModelId(id); if (!id || !(blob instanceof Blob) || !blob.size || !/^video\/(?:mp4|webm)(?:;.*)?$/iu.test(blob.type)) throw new Error('Unsupported local recording'); const database = await openDatabase(); if (!database) throw new Error('Local gallery unavailable'); const record = { mediaId: `${id}:recording:${crypto.randomUUID()}`, id, kind: 'recording', sourceUrl: '', capturedAt: Date.now(), width: 0, height: 0, bytes: blob.size, blob, userOwned: true, fileName: String(options.fileName || '').replace(/[\r\n]/gu, '').slice(0, 180), qualityPreset: 'original', isCustomCover: false, }; const transaction = database.transaction('images', 'readwrite'); const done = transactionDone(transaction); try { transaction.objectStore('images').put(record); } catch (error) { transaction.abort(); await done.catch(() => {}); throw error; } await done; window.dispatchEvent(new CustomEvent('roomgrid:model-media-saved', { detail: { id, record } })); return record; } const portableFields = ['mediaId', 'id', 'kind', 'hash', 'sourceUrl', 'capturedAt', 'width', 'height', 'bytes', 'userOwned', 'fileName', 'qualityPreset', 'isCustomCover', 'coverUpdatedAt']; function validateImport(data) { if (!data || data.schema !== 1 || !Array.isArray(data.records)) throw new Error('Invalid media backup'); const ids = new Set(); const covers = new Set(); for (const record of data.records) { if (!record || safeModelId(record.id) !== record.id || !record.id || typeof record.mediaId !== 'string' || !record.mediaId.startsWith(`${record.id}:`) || record.mediaId.length > 250 || ids.has(record.mediaId)) throw new Error('Invalid media identity'); ids.add(record.mediaId); if (typeof record.blobType !== 'string' || !/^(?:image\/(?:jpeg|png|webp)|video\/(?:mp4|webm)(?:;.*)?)$/iu.test(record.blobType)) throw new Error('Unsupported backup media type'); if (typeof record.blobBase64 !== 'string' || !record.blobBase64.length || record.blobBase64.length % 4 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(record.blobBase64)) throw new Error('Invalid media bytes'); const length = record.blobBase64.length / 4 * 3 - (record.blobBase64.endsWith('==') ? 2 : record.blobBase64.endsWith('=') ? 1 : 0); if (!Number.isSafeInteger(record.bytes) || record.bytes !== length || length < 1) throw new Error('Media byte count mismatch'); if (!Number.isFinite(record.capturedAt) || record.capturedAt < 0 || (record.sourceUrl && !safeSource(record.sourceUrl, record.id))) throw new Error('Invalid media metadata'); if (!['cover', 'preview', 'snapshot', 'upload', 'recording'].includes(record.kind) || typeof record.userOwned !== 'boolean' || typeof record.isCustomCover !== 'boolean' || !Number.isFinite(record.width) || record.width < 0 || !Number.isFinite(record.height) || record.height < 0) throw new Error('Invalid media attributes'); if (record.blobType.startsWith('video/') && (!record.userOwned || record.kind !== 'recording')) throw new Error('Invalid recording ownership'); if (record.fileName !== undefined && (typeof record.fileName !== 'string' || record.fileName.length > 180)) throw new Error('Invalid media filename'); if (record.isCustomCover) { if (!record.blobType.startsWith('image/') || covers.has(record.id)) throw new Error('Invalid custom cover'); covers.add(record.id); } } return data; } async function exportData() { const database = await openDatabase(); if (!database) throw new Error('Cannot export local gallery'); const records = await requestResult(database.transaction('images', 'readonly').objectStore('images').getAll()); // Reject overlarge JSON media before allocating base64 strings. This is // an explicit backup-format limit, never a limit on the user's gallery. const encodedBytes = records.reduce((sum, record) => sum + 4 * Math.ceil((record.blob?.size || 0) / 3), 0); if (encodedBytes > 512 * 1024 * 1024) throw new Error('Gallery exceeds the 512 MiB JSON backup limit; back up large media files separately'); const result = { schema: 1, records: [] }; for (const record of records) { if (!(record.blob instanceof Blob) || !record.blob.size) throw new Error('Unreadable gallery record'); const blobBase64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result).split(',')[1]); reader.onerror = () => reject(reader.error || new Error('Media export failed')); reader.readAsDataURL(record.blob); }); const portable = Object.fromEntries(portableFields.filter(key => record[key] !== undefined).map(key => [key, record[key]])); result.records.push({ ...portable, userOwned: !!record.userOwned, isCustomCover: !!record.isCustomCover, bytes: record.blob.size, blobType: record.blob.type, blobBase64 }); } validateImport(result); return result; } async function importData(data) { validateImport(data); const database = await openDatabase(); if (!database) throw new Error('Cannot restore local gallery'); // Decode before opening the transaction, so validation/allocation failures // cannot leave a half-imported gallery. All writes commit atomically. const records = data.records.map(portable => { const chunks = []; for (let offset = 0; offset < portable.blobBase64.length; offset += 65536) { const decoded = atob(portable.blobBase64.slice(offset, offset + 65536)); chunks.push(Uint8Array.from(decoded, character => character.charCodeAt(0))); } const blob = new Blob(chunks, { type: portable.blobType }); return { ...Object.fromEntries(portableFields.filter(key => portable[key] !== undefined).map(key => [key, portable[key]])), blob }; }); const transaction = database.transaction('images', 'readwrite'); const done = transactionDone(transaction); const images = transaction.objectStore('images'); const importedIds = new Set(records.map(record => record.mediaId)); const selectedCovers = new Map(records.filter(record => record.isCustomCover).map(record => [record.id, record.mediaId])); try { for (const [id, mediaId] of selectedCovers) { const request = images.index('model').getAll(IDBKeyRange.only(id)); request.onsuccess = () => { try { request.result.filter(record => record.isCustomCover && record.mediaId !== mediaId && !importedIds.has(record.mediaId)).forEach(record => images.put({ ...record, isCustomCover: false })); } catch (_) { transaction.abort(); } }; } records.forEach(record => images.put(record)); } catch (error) { transaction.abort(); await done.catch(() => {}); throw error; } await done; for (const id of new Set(records.map(record => record.id))) window.dispatchEvent(new CustomEvent('roomgrid:model-media-saved', { detail: { id } })); return { imported: records.length }; } async function removeMedia(id, mediaId) { id = safeModelId(id); mediaId = String(mediaId || ''); const database = id && mediaId ? await openDatabase() : null; if (!database) return false; const transaction = database.transaction('images', 'readwrite'); const store = transaction.objectStore('images'); const record = await requestResult(store.get(mediaId)); if (!record || record.id !== id) { transaction.abort(); return false; } store.delete(mediaId); await transactionDone(transaction); window.dispatchEvent(new CustomEvent('roomgrid:model-media-removed', { detail: { id, mediaId, wasCover: !!record.isCustomCover } })); if (record.isCustomCover) window.dispatchEvent(new CustomEvent('roomgrid:model-cover-changed', { detail: { id, mediaId: '' } })); return true; } function drain() { while (active < MAX_CONCURRENT && queue.length) { const task = queue.shift(); active += 1; saveOne(task.id, task.url, task.kind).then(task.resolve, task.reject).finally(() => { pending.delete(task.key); active -= 1; drain(); }); } } function enqueue(id, url, kind) { id = safeModelId(id); url = safeSource(url, id); if (!id || !url || queue.length >= MAX_QUEUE) return Promise.resolve(null); const normalizedSource = url.replace(/([?&])f=[^&]+/u, '$1f=live'); const key = `${id}:${kind}:${normalizedSource}`; if (pending.has(key)) return pending.get(key); const promise = new Promise((resolve, reject) => { queue.push({ id, url, kind, key, resolve, reject }); drain(); }).catch(() => null); pending.set(key, promise); return promise; } async function capturePack(id, options = {}) { id = safeModelId(id); if (!id) return []; const activePack = packPending.get(id); if (activePack) { const completed = await activePack; if (!options.force) return completed; } const work = (async () => { const now = Date.now(); const current = await list(id); const publicRecords = current.filter(record => !record.userOwned); const last = Math.max(0, ...publicRecords.map(record => Number(record.capturedAt) || 0)); const previousSchedule = recentlyScheduled.get(id) || 0; // A fresh local preview is already useful. Requiring three distinct // hashes repeatedly downloaded unchanged/offline thumbnails on reload. if (!options.force && publicRecords.length > 0 && now - last < REFRESH_INTERVAL_MS) return current; if (!options.force && now - previousSchedule < 60000) return current; recentlyScheduled.set(id, now); const supplied = (Array.isArray(options.urls) ? options.urls : []).map(url => safeSource(url, id)).filter(Boolean); const cover = supplied[0] || `https://thumb.live.mmcdn.com/riw/${encodeURIComponent(id)}.jpg`; const candidates = [ [cover, 'cover'], [`https://thumb.live.mmcdn.com/ri/${encodeURIComponent(id)}.jpg`, 'preview'], ]; if (options.live) candidates.push([`https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(id)}&f=${now}_${Math.random().toString(36).slice(2, 8)}`, 'snapshot']); await Promise.all(candidates.map(([url, kind]) => enqueue(id, url, kind))); return list(id); })(); packPending.set(id, work); try { return await work; } finally { if (packPending.get(id) === work) packPending.delete(id); } } async function removeModel(id) { id = safeModelId(id); const database = id ? await openDatabase() : null; if (!database) return false; const transaction = database.transaction('images', 'readwrite'); const index = transaction.objectStore('images').index('model'); await new Promise((resolve, reject) => { const request = index.openCursor(IDBKeyRange.only(id)); request.onsuccess = event => { const cursor = event.target.result; if (!cursor) { resolve(); return; } cursor.delete(); cursor.continue(); }; request.onerror = () => reject(request.error || new Error('Media removal failed')); }); await transactionDone(transaction); return true; } function objectUrl(record) { return record?.blob instanceof Blob ? URL.createObjectURL(record.blob) : ''; } return { list, capturePack, importFile, importVideo, importUrl, setCustomCover, clearCustomCover, getCustomCover, removeMedia, removeModel, objectUrl, exportData, validateImport, importData, limits: Object.freeze({ publicPerModel: MAX_PUBLIC_PER_MODEL, userPerModel: MAX_USER_PER_MODEL, records: MAX_RECORDS, width: MAX_WIDTH, height: MAX_HEIGHT, userBytes: MAX_USER_SOURCE_BYTES }), }; })(); /* ============================================================= * 3.3.7b. Updatefeste Seiten- und Model-Hintergründe * * Bilder liegen als komprimierte Blobs im bestehenden Medienarchiv. In den * Einstellungen werden ausschließlich kleine Referenzen und Darstellungswerte * gespeichert. Dadurch bleiben Konfigurationen schnell und Script-Updates * ersetzen weder eigene Hintergründe noch deren Zuordnungen. * ============================================================= */ const RoomGridBackgrounds = (() => { const STYLE_ID = 'roomgrid-page-background-style'; const LAYER_ID = 'roomgrid-page-background'; let activeObjectUrl = ''; let applyRevision = 0; function installStyles() { if (document.getElementById(STYLE_ID)) return; document.head.appendChild($('style', { id: STYLE_ID, html: trustedHtml(` body.rg-page-background-active{isolation:isolate;background:transparent!important} #${LAYER_ID}{position:fixed;inset:0;z-index:-1;overflow:hidden;pointer-events:none;background:var(--rg-page-bg,#0b1118)} #${LAYER_ID} .rg-page-background-visual,#${LAYER_ID} .rg-page-background-shade{position:absolute;inset:0;width:100%;height:100%;pointer-events:none} #${LAYER_ID} .rg-page-background-visual{transform-origin:center;will-change:transform,filter;transition:opacity .18s ease,filter .18s ease,transform .18s ease} #${LAYER_ID} .rg-page-background-visual img{display:block;width:100%;height:100%;object-position:var(--rg-bg-x,50%) var(--rg-bg-y,50%)} #${LAYER_ID} .rg-page-background-shade{background:rgba(0,0,0,var(--rg-bg-dim,.28))} body.rg-page-background-active .discover-shell, body.rg-page-background-active .discover-main, body.rg-page-background-active .app-shell, body.rg-page-background-active .rgh-root, body.rg-page-background-active .rgh-shell{background:transparent!important} body.rg-page-background-active:not(.rg-viewer-mode):not(.rg-focus-mode) .app-shell>main, body.rg-page-background-active:not(.rg-viewer-mode):not(.rg-focus-mode) .app-shell>.sidebar{background:color-mix(in srgb,var(--rg-panel,#111f2c) var(--rg-bg-panel-opacity,88%),transparent)!important;backdrop-filter:blur(10px)} body.rg-page-background-active .dashboard-section, body.rg-page-background-active .single-panel, body.rg-page-background-active .single-topbar, body.rg-page-background-active .discover-panel, body.rg-page-background-active .rgh-card, body.rg-page-background-active .rgh-section{background-color:color-mix(in srgb,var(--rg-panel,var(--dc-panel,#111f2c)) var(--rg-bg-panel-opacity,88%),transparent)!important;backdrop-filter:blur(10px)} body.rg-page-background-active .rgcc-overlay{background:rgba(5,12,18,.56)} @media(prefers-reduced-motion:reduce){#${LAYER_ID} .rg-page-background-visual{transition:none}} `) })); } function context(explicitScope = '') { const params = new URLSearchParams(location.search); const modelId = normalizeUsername(params.get('model') || document.querySelector('.single-page[data-single-id]')?.dataset.singleId || ''); if (explicitScope && ROOMGRID_BACKGROUND_PAGE_KEYS.includes(explicitScope)) return { scope: explicitScope, modelId: explicitScope === 'single' ? modelId : '' }; if (document.querySelector('[data-roomgrid-control-center]')) return { scope: 'settings', modelId: '' }; if (params.has('multicam_mode')) return { scope: 'multicam', modelId: '' }; if (params.has('multicam_discover')) { const requested = String(params.get('suite_view') || 'dashboard'); const scope = ROOMGRID_BACKGROUND_PAGE_KEYS.includes(requested) ? requested : 'dashboard'; return { scope, modelId: scope === 'single' && isLikelyUsername(modelId) ? modelId : '' }; } const path = String(location.pathname || '/').replace(/\/+$/u, '') || '/'; if (path === '/') return { scope: 'home', modelId: '' }; if (/^\/(?:discover|tags|followed|following|category|categories|apps|contest|private-shows)(?:\/|$)/iu.test(path)) return { scope: 'listing', modelId: '' }; const roomId = normalizeUsername(path.split('/').filter(Boolean)[0] || ''); return { scope: 'room', modelId: isLikelyUsername(roomId) ? roomId : '' }; } function configuration(settings, explicitScope = '', explicitModelId = '') { const current = context(explicitScope); const scope = current.scope; const modelId = normalizeUsername(explicitModelId || current.modelId); const globalConfig = sanitizeRoomGridBackground(settings?.backgroundGlobal, 'theme'); const pageConfig = sanitizeRoomGridBackground(settings?.backgroundPages?.[scope], 'inherit'); const modelConfig = scope === 'single' && isLikelyUsername(modelId) ? sanitizeRoomGridBackground(settings?.backgroundModels?.[modelId], 'inherit') : defaultRoomGridBackground('inherit'); const selected = modelConfig.mode !== 'inherit' ? modelConfig : pageConfig.mode !== 'inherit' ? pageConfig : globalConfig; const source = modelConfig.mode !== 'inherit' ? 'model' : pageConfig.mode !== 'inherit' ? 'page' : 'global'; return { scope, modelId, source, config: selected }; } function ensureLayer() { installStyles(); let layer = document.getElementById(LAYER_ID); if (layer) return layer; layer = $('div', { id: LAYER_ID, ariaHidden: 'true' }, [ $('div', { class: 'rg-page-background-visual' }), $('div', { class: 'rg-page-background-shade' }), ]); document.body.prepend(layer); return layer; } function clearObjectUrl() { if (!activeObjectUrl) return; try { URL.revokeObjectURL(activeObjectUrl); } catch (_) {} activeObjectUrl = ''; } async function mediaUrl(config) { if (config.mode !== 'image' || !config.mediaOwner || !config.mediaId) return ''; const records = await RoomGridModelMedia.list(config.mediaOwner); const record = records.find(item => item.mediaId === config.mediaId && item.blob instanceof Blob && item.blob.type.startsWith('image/')); return record ? RoomGridModelMedia.objectUrl(record) : ''; } async function apply(store, options = {}) { const revision = ++applyRevision; const current = configuration(store?.state?.settings || {}, options.scope || '', options.modelId || ''); const config = current.config; const root = document.documentElement; root.dataset.roomgridBackgroundScope = current.scope; root.dataset.roomgridBackgroundSource = current.source; if (config.mode === 'theme') { clearObjectUrl(); document.body?.classList.remove('rg-page-background-active'); document.getElementById(LAYER_ID)?.remove(); delete root.dataset.roomgridBackgroundMode; return current; } const layer = ensureLayer(); const visual = layer.querySelector('.rg-page-background-visual'); const shade = layer.querySelector('.rg-page-background-shade'); let url = ''; if (config.mode === 'image') url = await mediaUrl(config); if (revision !== applyRevision || !layer.isConnected) { if (url) URL.revokeObjectURL(url); return current; } clearObjectUrl(); activeObjectUrl = url; visual.replaceChildren(); visual.style.background = ''; visual.style.backgroundImage = ''; visual.style.backgroundRepeat = ''; visual.style.backgroundSize = ''; visual.style.backgroundPosition = ''; const zoom = Math.max(.5, Math.min(2.4, Number(config.zoom) / 100 || 1)); visual.style.opacity = String(Math.max(.1, Math.min(1, Number(config.opacity) / 100 || 1))); visual.style.filter = config.blur ? `blur(${config.blur}px)` : ''; visual.style.transform = `scale(${zoom})`; layer.style.position = config.fixed === false ? 'absolute' : 'fixed'; layer.style.minHeight = config.fixed === false ? '100vh' : ''; root.style.setProperty('--rg-bg-x', `${config.positionX}%`); root.style.setProperty('--rg-bg-y', `${config.positionY}%`); root.style.setProperty('--rg-bg-dim', String(Math.max(0, Math.min(.9, Number(config.dim) / 100 || 0)))); root.style.setProperty('--rg-bg-panel-opacity', `${config.panelOpacity}%`); shade.style.display = config.dim > 0 ? '' : 'none'; if (config.mode === 'color') visual.style.background = config.color; else if (config.mode === 'gradient') visual.style.background = `linear-gradient(${config.gradientAngle}deg,${config.gradientStart},${config.gradientEnd})`; else if (config.mode === 'image' && url) { if (config.fit === 'tile') { visual.style.backgroundImage = `url("${url}")`; visual.style.backgroundRepeat = 'repeat'; visual.style.backgroundSize = 'auto'; visual.style.backgroundPosition = `${config.positionX}% ${config.positionY}%`; } else { const img = $('img', { src: url, alt: '' }); img.style.objectFit = config.fit === 'stretch' ? 'fill' : config.fit === 'original' ? 'none' : config.fit; visual.appendChild(img); } } else visual.style.background = config.color; document.body.classList.add('rg-page-background-active'); root.dataset.roomgridBackgroundMode = config.mode; window.dispatchEvent(new CustomEvent('roomgrid:background-applied', { detail: { scope: current.scope, modelId: current.modelId, source: current.source, mode: config.mode, found: config.mode !== 'image' || !!url } })); return { ...current, found: config.mode !== 'image' || !!url }; } function patch(store, scope, next, modelId = '') { scope = String(scope || 'global'); if (scope === 'global') { store.patchSettings({ backgroundGlobal: sanitizeRoomGridBackground({ ...store.state.settings.backgroundGlobal, ...(next || {}) }, 'theme') }); } else if (scope === 'model') { const id = normalizeUsername(modelId); if (!isLikelyUsername(id)) return false; const config = sanitizeRoomGridBackground({ ...(store.state.settings.backgroundModels?.[id] || defaultRoomGridBackground('inherit')), ...(next || {}) }, 'inherit'); const models = { ...(store.state.settings.backgroundModels || {}) }; if (config.mode === 'inherit') delete models[id]; else models[id] = config; store.patchSettings({ backgroundModels: models }); } else { if (!ROOMGRID_BACKGROUND_PAGE_KEYS.includes(scope)) return false; const pages = { ...(store.state.settings.backgroundPages || {}) }; pages[scope] = sanitizeRoomGridBackground({ ...(pages[scope] || defaultRoomGridBackground('inherit')), ...(next || {}) }, 'inherit'); store.patchSettings({ backgroundPages: pages }); } void apply(store); return true; } async function importImage(store, scope, file, modelId = '') { if (!(file instanceof Blob) || !/^image\/(?:jpeg|png|webp)$/iu.test(file.type) || !file.size) throw new Error('Unsupported background image'); const id = normalizeUsername(modelId); const owner = scope === 'model' && isLikelyUsername(id) ? id : scope === 'global' ? 'camclient_bg_global' : `camclient_bg_${String(scope).replace(/[^a-z0-9_]/giu, '_').slice(0, 32)}`; const record = await RoomGridModelMedia.importFile(owner, file, { quality: 'high', fileName: file.name || `background-${scope}.webp` }); patch(store, scope, { mode: 'image', mediaOwner: owner, mediaId: record.mediaId }, id); await apply(store); return record; } async function useModelMedia(store, modelId, mediaId) { const id = normalizeUsername(modelId); const records = isLikelyUsername(id) ? await RoomGridModelMedia.list(id) : []; const record = records.find(item => item.mediaId === mediaId && item.blob instanceof Blob && item.blob.type.startsWith('image/')); if (!record) return false; patch(store, 'model', { mode: 'image', mediaOwner: id, mediaId: record.mediaId }, id); await apply(store); return true; } async function previewUrl(config) { return mediaUrl(sanitizeRoomGridBackground(config, config?.mode === 'theme' ? 'theme' : 'inherit')); } return { apply, patch, importImage, useModelMedia, previewUrl, context, configuration, pageKeys: ROOMGRID_BACKGROUND_PAGE_KEYS }; })(); /* ============================================================= * 3.3.8. Vom Nutzer gewaehltes Camclient-Medienarchiv * * Der Browser gibt einer Website niemals stillschweigend einen beliebigen * PC-Ordner frei. Deshalb waehlt der Nutzer den Stammordner genau einmal * ueber die File System Access API. Der Handle wird in IndexedDB bewahrt; * danach schreibt Camclient pro Model in einen eigenen Unterordner, ohne * fuer jede Aufnahme erneut den Download-Dialog zu oeffnen. * ============================================================= */ const RoomGridCaptureDirectory = (() => { const DB_NAME = 'camclient_capture_directory_v1'; const DB_VERSION = 1; const STORE_NAME = 'handles'; const ROOT_KEY = 'capture-root'; const MAX_GALLERY_FILES = Number.MAX_SAFE_INTEGER; const MEDIA_EXTENSION = /\.(?:png|jpe?g|webp|gif|avif|mp4|webm|mov|m4v)$/iu; let databasePromise = null; let rootHandle; const requestResult = request => new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('IndexedDB request failed')); }); const transactionDone = transaction => new Promise((resolve, reject) => { transaction.oncomplete = () => resolve(true); transaction.onerror = () => reject(transaction.error || new Error('IndexedDB transaction failed')); transaction.onabort = () => reject(transaction.error || new Error('IndexedDB transaction aborted')); }); function pickerTarget() { const candidates = [ window, typeof unsafeWindow !== 'undefined' ? unsafeWindow : null, window.wrappedJSObject || null, ]; return candidates.find(candidate => typeof candidate?.showDirectoryPicker === 'function') || null; } function supported() { return !!pickerTarget() && 'indexedDB' in window; } function unsupportedReason() { if (navigator.brave && typeof navigator.brave.isBrave === 'function') return 'brave-disabled'; if (!window.isSecureContext) return 'insecure-context'; return 'api-unavailable'; } function safeModelId(value) { const id = normalizeUsername(value); return isLikelyUsername(id) ? id : ''; } function safeFileName(value) { const cleaned = String(value || 'capture') .replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/gu, '-') .replace(/[. ]+$/u, '') .trim() .slice(0, 180); return cleaned || `capture-${Date.now()}`; } function openDatabase() { if (databasePromise) return databasePromise; if (!('indexedDB' in window)) return Promise.resolve(null); databasePromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const database = request.result; if (!database.objectStoreNames.contains(STORE_NAME)) database.createObjectStore(STORE_NAME); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error('Capture directory database unavailable')); }).catch(error => { try { Diagnostics.report('capture-directory', error, 'directory handle database unavailable', 'warning'); } catch (_) {} return null; }); return databasePromise; } async function loadRoot() { if (rootHandle !== undefined) return rootHandle; const database = await openDatabase(); if (!database) { rootHandle = null; return null; } try { const transaction = database.transaction(STORE_NAME, 'readonly'); rootHandle = await requestResult(transaction.objectStore(STORE_NAME).get(ROOT_KEY)) || null; } catch (_) { rootHandle = null; } return rootHandle; } async function persistRoot(handle) { const database = await openDatabase(); if (!database) return false; try { const transaction = database.transaction(STORE_NAME, 'readwrite'); transaction.objectStore(STORE_NAME).put(handle, ROOT_KEY); await transactionDone(transaction); return true; } catch (error) { // Test doubles and a few hardened browser profiles cannot clone a // handle. The selected folder still remains usable for this tab. try { Diagnostics.report('capture-directory', error, 'directory handle could not be persisted', 'warning'); } catch (_) {} return false; } } async function permissionFor(handle, mode = 'readwrite') { if (!handle) return 'not-configured'; if (typeof handle.queryPermission !== 'function') return 'granted'; try { return await handle.queryPermission({ mode }); } catch (_) { return 'prompt'; } } async function status() { if (!supported()) return { supported: false, configured: false, permission: 'unsupported', name: '', reason: unsupportedReason() }; const handle = await loadRoot(); return { supported: true, configured: !!handle, permission: handle ? await permissionFor(handle, 'readwrite') : 'not-configured', name: String(handle?.name || ''), }; } async function choose() { if (!supported()) return { supported: false, configured: false, permission: 'unsupported', name: '', reason: unsupportedReason() }; const target = pickerTarget(); const openPicker = options => Reflect.apply(target.showDirectoryPicker, target, [options]); let handle; try { handle = await openPicker({ id: 'camclient-media-library', mode: 'readwrite', startIn: 'videos' }); } catch (error) { if (error?.name !== 'TypeError') throw error; handle = await openPicker({ mode: 'readwrite' }); } if (!handle || handle.kind !== 'directory') throw new Error('No directory selected'); rootHandle = handle; await persistRoot(handle); const result = await status(); window.dispatchEvent(new CustomEvent('roomgrid:capture-directory-changed', { detail: result })); return result; } async function grant() { const handle = await loadRoot(); if (!handle) return status(); if (typeof handle.requestPermission === 'function') { try { await handle.requestPermission({ mode: 'readwrite' }); } catch (_) {} } const result = await status(); window.dispatchEvent(new CustomEvent('roomgrid:capture-directory-changed', { detail: result })); return result; } async function clear() { rootHandle = null; const database = await openDatabase(); if (database) { try { const transaction = database.transaction(STORE_NAME, 'readwrite'); transaction.objectStore(STORE_NAME).delete(ROOT_KEY); await transactionDone(transaction); } catch (_) {} } const result = await status(); window.dispatchEvent(new CustomEvent('roomgrid:capture-directory-changed', { detail: result })); return result; } async function modelDirectory(id, create = false) { id = safeModelId(id); const root = id ? await loadRoot() : null; if (!root) return null; const permission = await permissionFor(root, create ? 'readwrite' : 'read'); if (permission !== 'granted') return null; try { return await root.getDirectoryHandle(id, { create }); } catch (_) { return null; } } async function save(id, fileName, blob) { id = safeModelId(id); if (!id || !(blob instanceof Blob) || !blob.size) return { saved: false, reason: 'invalid' }; if (!supported()) return { saved: false, reason: 'unsupported' }; const root = await loadRoot(); if (!root) return { saved: false, reason: 'not-configured' }; const permission = await permissionFor(root, 'readwrite'); if (permission !== 'granted') return { saved: false, reason: 'permission', permission, name: String(root.name || '') }; const name = safeFileName(fileName); try { const directory = await root.getDirectoryHandle(id, { create: true }); const fileHandle = await directory.getFileHandle(name, { create: true }); const writable = await fileHandle.createWritable(); await writable.write(blob); await writable.close(); const result = { saved: true, reason: '', name, folder: String(root.name || ''), modelFolder: id, type: blob.type, size: blob.size, lastModified: Date.now() }; window.dispatchEvent(new CustomEvent('roomgrid:capture-file-saved', { detail: { id, ...result } })); return result; } catch (error) { try { Diagnostics.report('capture-directory-write', error, `${id}/${name}`, 'warning'); } catch (_) {} return { saved: false, reason: 'write-failed', name, folder: String(root.name || '') }; } } async function list(id) { id = safeModelId(id); const currentStatus = await status(); if (!id || !currentStatus.configured || currentStatus.permission !== 'granted') return { files: [], status: currentStatus }; const directory = await modelDirectory(id, false); if (!directory) return { files: [], status: currentStatus }; const files = []; try { for await (const entry of directory.values()) { if (entry?.kind !== 'file' || !MEDIA_EXTENSION.test(String(entry.name || ''))) continue; const file = await entry.getFile(); const type = String(file.type || '').toLocaleLowerCase(); const kind = type.startsWith('video/') || /\.(?:mp4|webm|mov|m4v)$/iu.test(entry.name) ? 'video' : 'image'; files.push({ name: String(entry.name), file, kind, type: file.type, size: file.size, lastModified: Number(file.lastModified) || 0, source: 'directory' }); } } catch (error) { try { Diagnostics.report('capture-directory-list', error, id, 'warning'); } catch (_) {} } files.sort((left, right) => right.lastModified - left.lastModified || right.name.localeCompare(left.name)); return { files, status: currentStatus }; } return { supported, status, choose, grant, clear, save, list, limits: Object.freeze({ galleryFiles: MAX_GALLERY_FILES }) }; })(); /* ============================================================= * 3.4. Native Kopfzeile + große Statistik-/Bibliotheksseiten * ============================================================= */ const RoomGridNativeHub = (() => { // Native page fixtures do not pay Tampermonkey's per-access Window proxy // cost. Resolve stable built-ins once, not millions of times per index. const { Math, Number, String, Array, Object, Date, Set, Map, Promise, JSON, URL } = globalThis; const PREFS_KEY = 'roomgrid_native_hub_prefs_v1'; const ACCOUNT_IMPORT_KEY = 'roomgrid_native_account_import_v1'; const FOLLOWING_CACHE_KEY = 'roomgrid_following_snapshot_v1'; const FOLLOWING_FRESH_MS = 10 * 60 * 1000; const ACCOUNT_PAGE_LIMIT = 80; const PAGE_SIZES = [24, 48, 72, 120, 240]; const LIBRARY_PAGE_SIZES = [12, 18, 24, 36, 48, 72, 120, 240]; const LIBRARY_DETAIL_DEFAULTS = Object.freeze({ libraryShowImage: true, libraryShowBio: true, libraryShowCatalogBadge: true, libraryShowStatus: true, libraryShowViewers: true, libraryShowCountry: true, libraryShowLocation: true, libraryShowAge: true, libraryShowType: true, libraryShowWatchTime: true, libraryShowSessions: true, libraryShowAverageSession: true, libraryShowLongestSession: true, libraryShowActiveDays: true, libraryShowFirstSeen: true, libraryShowLastSeen: true, libraryShowLastLive: true, libraryShowPreviews: true, libraryShowQuickViews: true, libraryShowOnlineEstimate: true, libraryShowActivityDetails: true, libraryShowExampleGallery: true, libraryShowGroups: true, libraryShowPublicTags: true, libraryShowEditor: true, libraryShowRating: true, libraryShowGroupAssignment: true, libraryShowActions: true, }); const LIBRARY_FILTER_DEFAULTS = Object.freeze({ libraryGenders: [], libraryStatuses: [], libraryCountries: [], libraryPublicTags: [], libraryPublicTagMode: 'any', libraryOrganization: 'all', libraryProfileState: 'all', libraryFeature: 'all', libraryWatchState: 'all', libraryOwnTagsState: 'all', libraryGroupsState: 'all', libraryNotesState: 'all', libraryAgeMin: 0, libraryAgeMax: 0, libraryViewerMin: 0, libraryViewerMax: 0, libraryWatchMinMinutes: 0, libraryFollowersMin: 0, libraryFollowersMax: 0, libraryCurrentOnlineMinMinutes: 0, librarySessionsMin: 0, libraryRatingMin: 0, libraryPriorityMin: 0, libraryActivityDays: 0, libraryAppearancesMin: 0, libraryOnlineSamplesMin: 0, libraryOnlineHour: -1, libraryOnlineWeekday: -1, }); const LIBRARY_SORTS = new Set([ 'recent', 'oldest', 'added', 'added_oldest', 'watch', 'watch_low', 'rating', 'rating_low', 'priority', 'priority_low', 'viewers', 'viewers_low', 'age_young', 'age_old', 'sessions', 'avg_session', 'longest_session', 'active_days', 'appearances', 'online_samples', 'followers', 'current_duration', 'last_online', 'first_observed', 'name', 'name_desc', 'country', 'gender', 'tag_count', 'group_count', ]); const STATS_FILTER_DEFAULTS = Object.freeze({ statsGenders: [], statsCountries: [], statsPublicTags: [], statsPublicTagMode: 'any', statsGroups: [], statsGroupMode: 'any', statsOrganization: 'all', statsProfileState: 'all', statsFeature: 'all', statsWatchState: 'all', statsMediaState: 'all', statsOwnTagsState: 'all', statsGroupsState: 'all', statsNotesState: 'all', statsAgeMin: 0, statsAgeMax: 0, statsViewerMin: 0, statsViewerMax: 0, statsFollowersMin: 0, statsFollowersMax: 0, statsWatchMinMinutes: 0, statsWatchMaxMinutes: 0, statsSessionsMin: 0, statsSessionsMax: 0, statsActiveDaysMin: 0, statsPreviewsMin: 0, statsQuickViewsMin: 0, statsRatingMin: 0, statsPriorityMin: 0, statsAppearancesMin: 0, statsOnlineSamplesMin: 0, statsActivityDays: 0, }); const STATS_SORTS = new Set([ 'watch', 'watch_low', 'recent', 'oldest', 'sessions', 'sessions_low', 'avg_session', 'avg_session_low', 'longest_session', 'active_days', 'share', 'previews', 'quickviews', 'discovered', 'viewers', 'viewers_low', 'followers', 'age_young', 'age_old', 'rating', 'priority', 'appearances', 'online_samples', 'last_online', 'first_observed', 'country', 'gender', 'tag_count', 'group_count', 'quality', 'media', 'name', 'name_desc', ]); let activeStore = null; let activeAnalytics = null; let activeNotifications = null; let activeWindows = null; let root = null; let content = null; let embedded = false; let embeddedTarget = null; let embeddedViewHandler = null; let headerMenu = null; let headerHost = null; let style = null; let observer = null; let unsubscribeStore = null; let unsubscribeAnalytics = null; let previousOverflow = ''; let cache = null; let cachePromise = null; let cacheDirty = false; let cacheGeneration = 0; let cacheRefreshPromise = null; let cacheWarmupHandle = 0; let cacheRefreshHandle = 0; let cacheRefreshTimer = 0; let catalogRestorePromise = null; let catalogRestoreStatus = 'idle'; let mediaHydrationHandle = 0; let mediaHydrationTimer = 0; let mediaHydrationQueue = []; const modelCoverCache = new Map(); // Only the last completed page per local view is retained. This is a // bounded, transient presentation cache, never another copy of the archive. const hubPageCache = new Map(); // Unsaved editor fields belong to the user, not to a rendered card. Keep // only changed fields across refreshes and menu navigation; committing a // profile clears its draft without changing any persistent storage format. const libraryDrafts = new Map(); let libraryEditorId = ''; const openLibraryGroupPickers = new Set(); const openHubTools = new Set(); const statsPeriodMemo = new WeakMap(); let mediaCacheGeneration = 0; let catalogStatusGeneration = 0; let cachedPrivateSession = false; let renderRevision = 0; let hoverTimer = 0; let lightweight = false; let accountImportBusy = false; let groupMenuPanel = null; let groupMenuButton = null; let groupMenuOutsideHandler = null; let globalMenuPanel = null; let globalMenuButton = null; let globalMenuOutsideHandler = null; let globalMenuReturnFocus = null; let previousHubFocus = null; let groupMenuReturnFocus = null; let homeFilterBanner = null; let homeFilterStats = { visible: 0, total: 0 }; let userSummaryMemo = new WeakMap(); let followingRequest = null; let followingLoadPromise = null; let followingAccountGeneration = 0; let followingCacheChecked = ''; let followingState = { status: 'idle', rows: [], pages: 0, partial: false, error: '', recognized: false }; let followingQuery = ''; let followingStatusFilter = 'all'; let followingSort = 'name'; const FOLLOWING_FILTER_DEFAULTS = Object.freeze({ gender: 'all', country: '', tags: '', tagMode: 'all', viewerMin: 0, viewerMax: 0, ageMin: 0, ageMax: 0, local: 'all', group: '' }); const FOLLOWING_SORTS = new Set(['name', 'name_desc', 'live_first', 'viewers', 'viewers_low', 'recent', 'watch', 'watch_low', 'sessions', 'rating', 'followers', 'current_duration']); let followingFilters = { ...FOLLOWING_FILTER_DEFAULTS }; let followingPage = 1; let followingPageSize = 48; let sessionAccount = { username: '', checkedAt: 0, status: 'unknown', followedOnline: null, followedTotal: null }; let embeddedSubscriptions = []; let embeddedCatalogHandler = null; let embeddedCoverHandler = null; const viewState = { view: 'stats', query: '', status: 'all', savedOnly: false, sort: 'watch', page: 1, libraryMode: 'active', librarySort: 'recent', libraryPage: 1, pageSize: 48, libraryPageSize: 48, statsRange: 30, libraryGroups: [], libraryGroupMode: 'any', libraryTags: [], libraryTagMode: 'any', homeGroups: [], homeGroupMode: 'any', density: 'comfortable', libraryColumns: 0, libraryCardScale: 100, userRange: 30, ...LIBRARY_DETAIL_DEFAULTS, ...LIBRARY_FILTER_DEFAULTS, ...STATS_FILTER_DEFAULTS, }; function readPrefs() { try { const value = JSON.parse(localStorage.getItem(PREFS_KEY) || '{}'); if (['all', 'live', 'offline'].includes(value.followingStatus)) followingStatusFilter = value.followingStatus; if (FOLLOWING_SORTS.has(value.followingSort)) followingSort = value.followingSort; followingQuery = String(value.followingQuery || '').slice(0, 200); if (PAGE_SIZES.includes(value.followingPageSize)) followingPageSize = value.followingPageSize; const filters = value.followingFilters || {}; if (['all', 'f', 'm', 'c', 't', 'unknown'].includes(filters.gender)) followingFilters.gender = filters.gender; for (const key of ['country', 'tags', 'group']) followingFilters[key] = String(filters[key] || '').slice(0, key === 'tags' ? 500 : 80); if (['all', 'any'].includes(filters.tagMode)) followingFilters.tagMode = filters.tagMode; if (['all', 'favorites', 'watched', 'unwatched', 'organized', 'unorganized', 'notes'].includes(filters.local)) followingFilters.local = filters.local; for (const key of ['viewerMin', 'viewerMax', 'ageMin', 'ageMax']) followingFilters[key] = clampInt(filters[key], 0, key.startsWith('age') ? 99 : 1000000000, 0); if (PAGE_SIZES.includes(Number(value.pageSize))) viewState.pageSize = Number(value.pageSize); if (STATS_SORTS.has(value.sort)) viewState.sort = value.sort; if (['all', 'live', 'offline', 'private', 'unknown', 'error'].includes(value.status)) viewState.status = value.status; if (typeof value.savedOnly === 'boolean') viewState.savedOnly = value.savedOnly; const legacyLibraryPageSize = Number(value.libraryPageSize ?? value.pageSize); if (LIBRARY_PAGE_SIZES.includes(legacyLibraryPageSize)) viewState.libraryPageSize = legacyLibraryPageSize; if (['compact', 'comfortable', 'large', 'cinema', 'focus'].includes(value.density)) viewState.density = value.density; if (Number.isInteger(Number(value.libraryColumns)) && Number(value.libraryColumns) >= 0 && Number(value.libraryColumns) <= 6) viewState.libraryColumns = Number(value.libraryColumns); if (Number.isFinite(Number(value.libraryCardScale))) viewState.libraryCardScale = Math.max(70, Math.min(160, Math.round(Number(value.libraryCardScale) / 5) * 5)); Object.keys(LIBRARY_DETAIL_DEFAULTS).forEach(key => { if (typeof value[key] === 'boolean') viewState[key] = value[key]; }); if (Array.isArray(value.libraryGroups)) viewState.libraryGroups = [...new Set(value.libraryGroups.map(item => String(item || '').slice(0, 80)).filter(Boolean))].slice(0, 40); if (['any', 'all'].includes(value.libraryGroupMode)) viewState.libraryGroupMode = value.libraryGroupMode; if (Array.isArray(value.libraryTags)) viewState.libraryTags = [...new Set(value.libraryTags.map(item => String(item || '').trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))].slice(0, 40); if (['any', 'all'].includes(value.libraryTagMode)) viewState.libraryTagMode = value.libraryTagMode; if (['active', 'all', 'collection', 'groups', 'favorites', 'watchlist', 'online', 'notes', 'archived'].includes(value.libraryMode)) viewState.libraryMode = value.libraryMode; if ([7, 30, 90, 365].includes(Number(value.statsRange))) viewState.statsRange = Number(value.statsRange); if (LIBRARY_SORTS.has(value.librarySort)) viewState.librarySort = value.librarySort; if (Array.isArray(value.libraryGenders)) viewState.libraryGenders = [...new Set(value.libraryGenders.map(item => String(item || '')).filter(item => ['f', 'm', 'c', 't', 'unknown'].includes(item)))].slice(0, 5); if (Array.isArray(value.libraryStatuses)) viewState.libraryStatuses = [...new Set(value.libraryStatuses.map(item => String(item || '')).filter(item => ['live', 'offline', 'private', 'unknown', 'error'].includes(item)))].slice(0, 5); if (Array.isArray(value.libraryCountries)) viewState.libraryCountries = [...new Set(value.libraryCountries.map(item => String(item || '').trim().slice(0, 40)).filter(Boolean))].slice(0, 80); if (Array.isArray(value.libraryPublicTags)) viewState.libraryPublicTags = [...new Set(value.libraryPublicTags.map(item => String(item || '').trim().toLocaleLowerCase().slice(0, 80)).filter(Boolean))].slice(0, 80); if (['any', 'all'].includes(value.libraryPublicTagMode)) viewState.libraryPublicTagMode = value.libraryPublicTagMode; if (['all', 'organized', 'catalog'].includes(value.libraryOrganization)) viewState.libraryOrganization = value.libraryOrganization; if (['all', 'loaded', 'missing'].includes(value.libraryProfileState)) viewState.libraryProfileState = value.libraryProfileState; if (['all', 'hd', 'gaming', 'both', 'neither'].includes(value.libraryFeature)) viewState.libraryFeature = value.libraryFeature; if (['all', 'watched', 'unwatched'].includes(value.libraryWatchState)) viewState.libraryWatchState = value.libraryWatchState; for (const key of ['libraryOwnTagsState', 'libraryGroupsState', 'libraryNotesState']) if (['all', 'with', 'without'].includes(value[key])) viewState[key] = value[key]; for (const key of ['libraryAgeMin', 'libraryAgeMax']) viewState[key] = clampInt(value[key], 0, 99, 0); for (const key of ['libraryViewerMin', 'libraryViewerMax', 'libraryFollowersMin', 'libraryFollowersMax']) viewState[key] = clampInt(value[key], 0, 1000000000, 0); viewState.libraryWatchMinMinutes = clampInt(value.libraryWatchMinMinutes, 0, 1000000000, 0); viewState.libraryCurrentOnlineMinMinutes = clampInt(value.libraryCurrentOnlineMinMinutes, 0, 1000000000, 0); viewState.librarySessionsMin = clampInt(value.librarySessionsMin, 0, 1000000000, 0); viewState.libraryRatingMin = clampInt(value.libraryRatingMin, 0, 5, 0); viewState.libraryPriorityMin = clampInt(value.libraryPriorityMin, 0, 5, 0); viewState.libraryActivityDays = clampInt(value.libraryActivityDays, 0, 36500, 0); viewState.libraryAppearancesMin = clampInt(value.libraryAppearancesMin, 0, 1000000000, 0); viewState.libraryOnlineSamplesMin = clampInt(value.libraryOnlineSamplesMin, 0, 1000000000, 0); viewState.libraryOnlineHour = clampInt(value.libraryOnlineHour, -1, 23, -1); viewState.libraryOnlineWeekday = clampInt(value.libraryOnlineWeekday, -1, 6, -1); if (Array.isArray(value.statsGenders)) viewState.statsGenders = [...new Set(value.statsGenders.map(item => String(item || '')).filter(item => ['f', 'm', 'c', 't', 'unknown'].includes(item)))].slice(0, 5); if (Array.isArray(value.statsCountries)) viewState.statsCountries = [...new Set(value.statsCountries.map(item => String(item || '').trim().slice(0, 40)).filter(Boolean))].slice(0, 80); if (Array.isArray(value.statsPublicTags)) viewState.statsPublicTags = [...new Set(value.statsPublicTags.map(item => String(item || '').trim().toLocaleLowerCase().slice(0, 80)).filter(Boolean))].slice(0, 80); if (Array.isArray(value.statsGroups)) viewState.statsGroups = [...new Set(value.statsGroups.map(item => String(item || '').trim().slice(0, 80)).filter(Boolean))].slice(0, 60); if (['any', 'all'].includes(value.statsPublicTagMode)) viewState.statsPublicTagMode = value.statsPublicTagMode; if (['any', 'all'].includes(value.statsGroupMode)) viewState.statsGroupMode = value.statsGroupMode; if (['all', 'organized', 'catalog'].includes(value.statsOrganization)) viewState.statsOrganization = value.statsOrganization; if (['all', 'loaded', 'missing'].includes(value.statsProfileState)) viewState.statsProfileState = value.statsProfileState; if (['all', 'hd', 'gaming', 'both', 'neither'].includes(value.statsFeature)) viewState.statsFeature = value.statsFeature; if (['all', 'watched', 'unwatched'].includes(value.statsWatchState)) viewState.statsWatchState = value.statsWatchState; if (['all', 'saved', 'missing', 'live'].includes(value.statsMediaState)) viewState.statsMediaState = value.statsMediaState; for (const key of ['statsOwnTagsState', 'statsGroupsState', 'statsNotesState']) if (['all', 'with', 'without'].includes(value[key])) viewState[key] = value[key]; for (const key of ['statsAgeMin', 'statsAgeMax']) viewState[key] = clampInt(value[key], 0, 99, 0); for (const key of ['statsViewerMin', 'statsViewerMax', 'statsFollowersMin', 'statsFollowersMax', 'statsWatchMinMinutes', 'statsWatchMaxMinutes', 'statsSessionsMin', 'statsSessionsMax', 'statsActiveDaysMin', 'statsPreviewsMin', 'statsQuickViewsMin', 'statsAppearancesMin', 'statsOnlineSamplesMin']) viewState[key] = clampInt(value[key], 0, 1000000000, 0); viewState.statsRatingMin = clampInt(value.statsRatingMin, 0, 5, 0); viewState.statsPriorityMin = clampInt(value.statsPriorityMin, 0, 5, 0); viewState.statsActivityDays = clampInt(value.statsActivityDays, 0, 36500, 0); if (Array.isArray(value.homeGroups)) viewState.homeGroups = [...new Set(value.homeGroups.map(item => String(item || '').slice(0, 80)).filter(Boolean))].slice(0, 40); if (['any', 'all'].includes(value.homeGroupMode)) viewState.homeGroupMode = value.homeGroupMode; } catch (_) {} } function savePrefs() { try { localStorage.setItem(PREFS_KEY, JSON.stringify({ followingStatus: followingStatusFilter, followingSort, followingQuery, followingFilters: { ...followingFilters }, followingPageSize, pageSize: viewState.pageSize, sort: viewState.sort, status: viewState.status, savedOnly: viewState.savedOnly, statsRange: viewState.statsRange, libraryPageSize: viewState.libraryPageSize, density: viewState.density, libraryColumns: viewState.libraryColumns, libraryCardScale: viewState.libraryCardScale, libraryGroups: viewState.libraryGroups, libraryGroupMode: viewState.libraryGroupMode, libraryTags: viewState.libraryTags, libraryTagMode: viewState.libraryTagMode, libraryMode: viewState.libraryMode, librarySort: viewState.librarySort, ...Object.fromEntries(Object.keys(LIBRARY_FILTER_DEFAULTS).map(key => [key, Array.isArray(viewState[key]) ? [...viewState[key]] : viewState[key]])), homeGroups: viewState.homeGroups, homeGroupMode: viewState.homeGroupMode, ...Object.fromEntries(Object.keys(STATS_FILTER_DEFAULTS).map(key => [key, Array.isArray(viewState[key]) ? [...viewState[key]] : viewState[key]])), ...Object.fromEntries(Object.keys(LIBRARY_DETAIL_DEFAULTS).map(key => [key, !!viewState[key]])), })); } catch (_) {} } const compactNumber = value => new Intl.NumberFormat(undefined, { notation: Math.abs(Number(value) || 0) >= 10000 ? 'compact' : 'standard', maximumFractionDigits: 1 }).format(Math.max(0, Number(value) || 0)); function duration(ms) { const minutes = Math.round(Math.max(0, Number(ms) || 0) / 60000); if (minutes < 60) return `${minutes} min`; const hours = Math.floor(minutes / 60); return hours < 48 ? `${hours} h ${minutes % 60} min` : `${Math.floor(hours / 24)} d ${hours % 24} h`; } const dateTime = value => value ? new Date(value).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }) : '—'; const dateOnly = value => value ? new Date(value).toLocaleDateString([], { dateStyle: 'medium' }) : '—'; const thumbUrl = id => `https://thumb.live.mmcdn.com/riw/${encodeURIComponent(id)}.jpg`; const smallThumbUrl = id => `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(id)}.jpg`; // Every library card must retain a visual cover, even when an offline // room has no public thumbnail and the browser blocks both image hosts. // The generated SVG is local, deterministic and contains no user HTML. const generatedCoverUrl = id => { const label = String(id || '?').replace(/[^a-zA-Z0-9 _.-]/gu, '').slice(0, 28) || '?'; const initial = label.slice(0, 1).toLocaleUpperCase(); return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`${initial}${label}`)}`; }; const liveFrameUrl = id => `https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(id)}&f=${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; const statusLabel = value => ({ online: 'Live', public: 'Live', private: 'Privat', offline: 'Offline', loading: 'Lädt', error: 'Fehler', unknown: 'Unbekannt' }[String(value || '')] || 'Unbekannt'); const groupLabel = group => group?.id === DEFAULT_GROUP_ID ? tx('全部', 'All', 'Alle') : group?.id === FAVORITE_GROUP_ID ? tx('收藏', 'Favorites', 'Favoriten') : group?.id === ONLINE_GROUP_ID ? 'Live' : group?.id === LIBRARY_GROUP_ID ? tx('模型库', 'Library', 'Bibliothek') : String(group?.name || group?.id || ''); const parseTags = value => [...new Set(String(value || '').split(/[,;\n]+/u).map(item => item.trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))].slice(0, 40); const isFavorite = room => !!room && roomInGroup(room, FAVORITE_GROUP_ID); const asDay = date => date.toISOString().slice(0, 10); function readAccountImport() { try { const source = JSON.parse(localStorage.getItem(ACCOUNT_IMPORT_KEY) || '{}'); return { schema: 1, username: isLikelyUsername(normalizeUsername(source.username)) ? normalizeUsername(source.username) : '', capturedAt: Math.max(0, Number(source.capturedAt) || 0), importedAt: Math.max(0, Number(source.importedAt) || 0), followedOnline: Math.max(0, Math.round(Number(source.followedOnline) || 0)), followedTotal: Math.max(0, Math.round(Number(source.followedTotal) || 0)), followedImported: Math.max(0, Math.round(Number(source.followedImported) || 0)), collectionItems: Math.max(0, Math.round(Number(source.collectionItems) || 0)), collectionModels: [...new Set((Array.isArray(source.collectionModels) ? source.collectionModels : []).map(normalizeUsername).filter(isLikelyUsername))].slice(0, 500), collectionRecognized: !!source.collectionRecognized, lastMessage: String(source.lastMessage || '').slice(0, 240), lastError: String(source.lastError || '').slice(0, 240), }; } catch (_) { return { schema: 1, username: '', capturedAt: 0, importedAt: 0, followedOnline: 0, followedTotal: 0, followedImported: 0, collectionItems: 0, collectionModels: [], collectionRecognized: false, lastMessage: '', lastError: '' }; } } function saveAccountImport(next) { const clean = { ...readAccountImport(), ...(next || {}), schema: 1 }; try { localStorage.setItem(ACCOUNT_IMPORT_KEY, JSON.stringify(clean)); } catch (_) {} return clean; } function numberFromText(value) { const digits = String(value || '').replace(/[^0-9]/gu, ''); return digits ? Math.max(0, Number(digits) || 0) : 0; } function captureSessionAccount(doc = document) { const header = doc.querySelector('#desktop-spa-header,[data-testid="header-top-row"],header,[role="banner"]'); const node = header?.querySelector('[data-username][data-testid*="user" i],[data-testid*="user-menu" i] [data-username],[class*="UserMenu"] [data-username],#user_username'); const username = normalizeUsername(node?.getAttribute('data-username') || node?.textContent || ''); const followed = header?.querySelector('a[href*="/followed-cams"]'); const match = String(followed?.textContent || '').replace(/\s+/gu, '').match(/\(([\d.,]+)\/([\d.,]+)\)/u); const nextUsername = isLikelyUsername(username) ? username : ''; if (sessionAccount.username !== nextUsername) { followingAccountGeneration += 1; followingRequest?.abort(); followingCacheChecked = ''; followingState = { status: 'idle', rows: [], pages: 0, partial: false, error: '', recognized: false }; } sessionAccount = { username: nextUsername, checkedAt: Date.now(), status: nextUsername ? 'recognized' : 'unknown', followedOnline: match ? numberFromText(match[1]) : null, followedTotal: match ? numberFromText(match[2]) : null }; return sessionAccount; } function sessionAccountPanel() { const values = $('div', { class: 'rgh-metrics', dataset: { sessionAccount: '1' } }); const message = $('p', { class: 'rgh-bio', role: 'status' }); const draw = () => { const recognized = sessionAccount.status === 'recognized'; values.replaceChildren( metric(tx('当前网站账户', 'Current website account', 'Aktuelles Website-Konto'), recognized ? `@${sessionAccount.username}` : tx('无法确定', 'Not identifiable', 'Nicht sicher erkennbar')), metric(tx('已关注', 'Official Following', 'Offiziell gefolgt'), sessionAccount.followedTotal == null ? '—' : compactNumber(sessionAccount.followedTotal)), metric(tx('在线关注', 'Followed online', 'Gefolgt online'), sessionAccount.followedOnline == null ? '—' : compactNumber(sessionAccount.followedOnline)), ); message.textContent = tx('只读取当前会话的官方页眉。未显示的账户字段不会猜测;下方的 API 连接不一定属于当前登录用户。', 'Only the official header of the current session is read. Missing account fields are not guessed; the API connection below need not belong to the signed-in user.', 'Gelesen wird nur die offizielle Kopfzeile der aktuellen Sitzung. Nicht sichtbare Kontofelder werden nicht geraten. Die API-Verbindung unten muss nicht zum angemeldeten Website-Konto gehören.'); }; const refresh = action(tx('重新读取账户信息', 'Read account info again', 'Kontoinfos neu lesen'), async () => { refresh.disabled = true; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { const response = await fetch(new URL('/followed-cams/', location.origin), { credentials: 'include', cache: 'no-store', signal: controller.signal }); const finalUrl = new URL(response.url || location.origin); if (!response.ok || finalUrl.origin !== location.origin || /\/login\/?$/iu.test(finalUrl.pathname)) throw new Error('Account header unavailable'); captureSessionAccount(new DOMParser().parseFromString(await response.text(), 'text/html')); if (values.isConnected) draw(); } catch (_) { sessionAccount = { ...sessionAccount, username: '', status: 'unknown', followedOnline: null, followedTotal: null }; if (values.isConnected) { draw(); message.textContent = tx('无法读取账户页眉;请检查 Chaturbate 登录状态。', 'The account header could not be read; check your Chaturbate sign-in.', 'Die Konto-Kopfzeile konnte nicht gelesen werden. Bitte Chaturbate-Anmeldung prüfen.'); } } finally { clearTimeout(timeout); refresh.disabled = false; } }); draw(); return $('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, tx('你的 Chaturbate 账户', 'Your Chaturbate account', 'Dein Chaturbate-Konto')), refresh]), values, message, action(tx('打开官方关注列表', 'Open official Following', 'Offizielle Following-Liste'), () => setView('following')), ]); } function captureHeaderAccountSummary() { const previous = readAccountImport(); const header = document.querySelector('#desktop-spa-header,[data-testid="header-top-row"],header,[role="banner"]'); const followedLinks = [...document.querySelectorAll('a[href*="/followed-cams"]')].filter(link => !link.closest('[data-roomgrid-native-hub]')); const followed = followedLinks.find(link => { const rect = link.getBoundingClientRect?.(); return rect && rect.width > 0 && rect.top >= 0 && rect.top < 180; }) || followedLinks[0]; const match = String(followed?.textContent || '').replace(/\s+/gu, '').match(/\(([\d.,]+)\/([\d.,]+)\)/u); let username = previous.username; if (header) { const accountNode = header.querySelector('[data-username][data-testid*="user" i],[data-testid*="user-menu" i] [data-username],[class*="UserMenu"] [data-username]'); const candidate = normalizeUsername(accountNode?.getAttribute?.('data-username') || accountNode?.textContent || ''); if (isLikelyUsername(candidate)) username = candidate; } const next = { username, capturedAt: Date.now(), followedOnline: match ? numberFromText(match[1]) : previous.followedOnline, followedTotal: match ? numberFromText(match[2]) : previous.followedTotal, }; if (next.username !== previous.username || next.followedOnline !== previous.followedOnline || next.followedTotal !== previous.followedTotal || !previous.capturedAt) return saveAccountImport(next); return previous; } function safeAccountPageUrl(href, type) { try { const url = new URL(String(href || ''), location.origin); if (url.origin !== location.origin) return null; const path = url.pathname.replace(/\/+$/u, '') || '/'; if (type === 'following' && !['/followed-cams', '/followed-cams/offline'].includes(path)) return null; if (type === 'collection' && path !== '/my_collection') return null; url.hash = ''; return url; } catch (_) { return null; } } function roomIdFromHref(href) { try { const url = new URL(String(href || ''), location.origin); if (url.origin !== location.origin) return ''; const match = url.pathname.match(/^\/([^/]+)\/?$/u); const id = match ? normalizeUsername(decodeURIComponent(match[1])) : ''; return isLikelyUsername(id) && !['accounts', 'apps', 'billing', 'discover', 'tags', 'support', 'login', 'signup'].includes(id) ? id : ''; } catch (_) { return ''; } } async function fetchAccountDocument(url, type) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { const response = await fetch(url.href, { credentials: 'include', cache: 'no-store', headers: { Accept: 'text/html' }, signal: controller.signal }); const finalUrl = new URL(response.url || url.href, location.origin); if (finalUrl.origin !== location.origin || /\/(?:auth\/login|accounts\/login)(?:\/|$)/iu.test(finalUrl.pathname)) throw Object.assign(new Error('Bitte zuerst bei Chaturbate anmelden.'), { code: 'login' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const doc = new DOMParser().parseFromString(await response.text(), 'text/html'); if (/\blog\s*in\b|\bsign\s*in\b|anmelden/iu.test(doc.title || '') && !doc.querySelector('li.room_list_room,[data-testid="room-card"],[class*="RoomCard"],[class*="collection" i]')) throw Object.assign(new Error('Bitte zuerst bei Chaturbate anmelden.'), { code: 'login' }); return doc; } finally { clearTimeout(timeout); } } function extractFollowingDocument(doc) { const cardSelector = 'li.room_list_room,li.RoomCard,li.roomCard,[data-testid="room-card"],[class*="RoomCard"],[class*="room-card"]'; const cards = [...new Set([...doc.querySelectorAll(`${cardSelector},[data-testid="room-card-username"]`)].map(node => node.matches('li.room_list_room,li.RoomCard,li.roomCard,[data-testid="room-card"],[class*="RoomCard"],[class*="room-card"]') ? node : node.closest(cardSelector) || node, ))]; const ids = new Set(); const add = value => { const id = normalizeUsername(String(value || '').trim()); if (isLikelyUsername(id)) ids.add(id); }; cards.forEach(card => { add(card.getAttribute('data-room')); add(card.getAttribute('data-username')); card.querySelectorAll('[data-room],[data-username]').forEach(node => add(node.getAttribute('data-room') || node.getAttribute('data-username'))); if (card.matches('[data-testid="room-card-username"]')) add(card.textContent); add(card.querySelector('[data-testid="room-card-username"],[class*="username" i],[class*="broadcaster" i]')?.textContent); card.querySelectorAll('a[href]').forEach(link => add(roomIdFromHref(link.getAttribute('href')))); }); const emptyMarkers = [...doc.querySelectorAll('[data-testid*="empty" i],[class*="empty-state" i],[class*="no-follow" i]')]; const emptyText = [ ...emptyMarkers, doc.querySelector('main'), doc.body ] .filter(Boolean).map(node => String(node.textContent || '').replace(/\s+/gu, ' ').trim()).join(' '); const explicitEmpty = (!ids.size && emptyMarkers.length > 0) || /(?:you\s+(?:are\s+)?not\s+following\s+(?:any(?:one|body|\s+models?)?|anyone\s+yet)|no\s+followed\s+models?|you\s+have\s+no\s+followed\s+models?|du\s+folgst\s+keinen?\s+models?|keine\s+gefolgten\s+models?|no\s+sigues\s+a\s+ning[uú]n\s+modelo|n[aã]o\s+segue\s+nenhum\s+modelo)/iu.test(emptyText); // Current official pages ship placeholders; roomlist-prefetch populates them // from the same-origin read-only room-list endpoint. Unknown HTML stays an error. const spaPlaceholder = !ids.size && !!doc.querySelector('#roomlist_root[data-testid="room-list"] li.roomCard.placeholder') && !!doc.querySelector('script[src*="/roomlist-prefetch."]'); return { ids: [...ids], recognized: ids.size > 0 || explicitEmpty, explicitEmpty, cardCount: cards.length, spaPlaceholder }; } async function readExistingFollowing() { const queue = ['/followed-cams/', '/followed-cams/offline/'].map(path => new URL(path, location.origin)); const seen = new Set(); const ids = new Set(); let recognized = false; while (queue.length && seen.size < ACCOUNT_PAGE_LIMIT && ids.size < MAX_STORED_ROOMS) { const url = queue.shift(); if (!url || seen.has(url.href)) continue; seen.add(url.href); const { doc, parsed, nextPages } = await fetchFollowingPage(url); if (!parsed.recognized) throw new Error('Die Following-Seite konnte nicht sicher erkannt werden.'); recognized = true; parsed.ids.forEach(id => { if (ids.size < MAX_STORED_ROOMS) ids.add(id); }); (nextPages || followingPageLinks(doc)).forEach(urlValue => { if (!seen.has(urlValue.href)) queue.push(urlValue); }); } if (!recognized) throw new Error('Keine lesbare Following-Seite gefunden.'); return [...ids]; } async function readExistingCollection() { const queue = [new URL('/my_collection/', location.origin)]; const seen = new Set(); const models = new Set(); let items = 0; let recognized = false; while (queue.length && seen.size < 40) { const url = queue.shift(); if (!url || seen.has(url.href)) continue; seen.add(url.href); const doc = await fetchAccountDocument(url, 'collection'); const scopes = [...doc.querySelectorAll('[data-testid*="collection-item" i],[data-testid*="recording" i],[class*="collectionItem" i],[class*="recordingCard" i],[data-room][class*="video" i]')]; const cards = [...new Set(scopes.map(node => node.closest('article,li,[data-testid*="card" i]') || node))]; const empty = /collection is empty|no recordings|keine aufnahmen|sammlung ist leer/iu.test(String(doc.body?.textContent || '')); if (cards.length || empty || doc.querySelector('[data-testid*="collection" i],#my_collection,[class*="my_collection" i]')) recognized = true; items += cards.length; cards.forEach(card => { const direct = normalizeUsername(card.getAttribute?.('data-room') || card.getAttribute?.('data-username') || ''); if (isLikelyUsername(direct)) models.add(direct); card.querySelectorAll?.('a[href]')?.forEach(link => { const id = roomIdFromHref(link.getAttribute('href')); if (id) models.add(id); }); }); [...doc.querySelectorAll('a[href]')].map(link => safeAccountPageUrl(link.getAttribute('href'), 'collection')).filter(urlValue => urlValue?.searchParams.has('page')).forEach(urlValue => { if (!seen.has(urlValue.href)) queue.push(urlValue); }); } return { recognized, items, models: [...models].slice(0, 500) }; } function ensureFollowingGroup() { const knownId = activeStore.state.settings.followedGroupId; let group = activeStore.state.groups.find(item => item.id === knownId && !item.system); if (!group) group = activeStore.state.groups.find(item => !item.system && /^(following|gefolgt|follower)$/iu.test(String(item.name || '').trim())); if (!group) { const id = activeStore.addGroup('Following'); group = activeStore.state.groups.find(item => item.id === id); } if (group && activeStore.state.settings.followedGroupId !== group.id) activeStore.patchSettings({ followedGroupId: group.id }); return group; } async function importExistingAccountData() { if (accountImportBusy) return; accountImportBusy = true; saveAccountImport({ lastError: '', lastMessage: 'Bestehende Chaturbate-Daten werden gelesen …' }); scheduleRender(); const results = await Promise.allSettled([readExistingFollowing(), readExistingCollection()]); const next = captureHeaderAccountSummary(); const messages = []; const errors = []; if (results[0].status === 'fulfilled') { const ids = results[0].value; const group = ensureFollowingGroup(); if (group) { activeStore.syncRoomsToGroup(ids, group.id); activeStore.patchSettings({ followedGroupId: group.id, followedSyncLastAt: Date.now() }); ids.forEach(id => activeAnalytics.observeModel?.({ id, observedAt: Date.now() })); activeAnalytics.flush?.().catch?.(() => {}); next.followedImported = ids.length; next.followedTotal = Math.max(next.followedTotal, ids.length); messages.push(tx(`${ids.length} 个已关注模型已导入`, `${ids.length} followed models imported`, `${ids.length} bereits gefolgte Models übernommen`, `${ids.length} modelos seguidos importados`, `${ids.length} modelos seguidos importados`)); } } else errors.push(String(results[0].reason?.message || results[0].reason)); if (results[1].status === 'fulfilled') { next.collectionItems = results[1].value.items; next.collectionModels = results[1].value.models; next.collectionRecognized = results[1].value.recognized; results[1].value.models.forEach(id => activeAnalytics.observeModel?.({ id, observedAt: Date.now(), tags: ['my-collection'] })); activeAnalytics.flush?.().catch?.(() => {}); messages.push(results[1].value.recognized ? tx(`${results[1].value.items} 个“我的收藏”条目已识别`, `${results[1].value.items} My Collection items recognized`, `${results[1].value.items} My-Collection-Einträge erkannt`, `${results[1].value.items} elementos de Mi colección reconocidos`, `${results[1].value.items} itens de Minha coleção reconhecidos`) : tx('“我的收藏”中没有可可靠识别的条目', 'My Collection contained no reliably recognizable entries', 'My Collection enthielt keine sicher erkennbaren Einträge', 'Mi colección no contenía elementos reconocibles de forma fiable', 'Minha coleção não continha itens que pudessem ser reconhecidos com segurança')); } else errors.push(String(results[1].reason?.message || results[1].reason)); saveAccountImport({ ...next, importedAt: Date.now(), lastMessage: messages.join(' · '), lastError: errors.join(' · ') }); accountImportBusy = false; invalidate(); } function metric(label, value, note = '') { return $('article', { class: 'rgh-metric' }, [ $('small', {}, label), $('strong', {}, String(value)), note ? $('span', {}, note) : null, ]); } // Fold the tools, never the result cards. The per-instance state survives // data refreshes, while heavy picker DOM is built only when requested. function hubTools(key, title, factory, note = '') { const explanations = { 'library-filters': tx('按标签、国家或其他条件缩小列表', 'Narrow the list by tags, country or other criteria', 'Nur bei Bedarf: nach Tags, Land und weiteren Kriterien eingrenzen'), 'library-display': tx('调整卡片大小和可见信息', 'Choose card size and visible information', 'Kartengröße und sichtbare Informationen anpassen'), 'library-groups': tx('建立收藏并管理自己的标签', 'Create collections and manage your own tags', 'Sammlungen anlegen und eigene Tags verwalten'), 'following-metrics': tx('上次更新、收藏及本地使用', 'Last update, favorites and local usage', 'Letzte Aktualisierung, Favoriten und lokale Nutzung ansehen'), }; note ||= explanations[key] || ''; const body = $('div', { class: 'rgh-tools-body' }); const details = $('details', { class: 'rgh-tools', dataset: { hubTools: key }, open: openHubTools.has(key) }, [ $('summary', {}, [$('strong', {}, title), note ? $('small', {}, note) : null]), body, ]); let built = false; const populate = () => { if (!built && details.open) { body.replaceChildren(); built = true; const appendResult = result => body.append(...(Array.isArray(result) ? result : [result]).filter(Boolean)); const result = factory(); if (result?.then) { const pending = $('p', { class: 'rgh-bio', role: 'status' }, tx('正在准备选项…', 'Preparing options…', 'Optionen werden vorbereitet …')); body.appendChild(pending); result.then(value => { if (!details.isConnected) return; pending.remove(); if (value) appendResult(value); else { built = false; } }).catch(() => { if (!details.isConnected) return; built = false; pending.textContent = tx('请重新打开此区域重试。', 'Reopen this section to retry.', 'Zum Wiederholen diesen Bereich erneut öffnen.'); }); } else appendResult(result); body.appendChild($('button', { type: 'button', class: 'rgh-action rgh-tools-close', onclick: () => { details.open = false; openHubTools.delete(key); details.querySelector(':scope > summary')?.focus({ preventScroll: true }); } }, tx('收起此区域', 'Collapse this section', 'Diesen Bereich einklappen'))); } }; details.addEventListener('toggle', () => { if (details.open) openHubTools.add(key); else openHubTools.delete(key); populate(); }); populate(); return details; } function foldInterestTopics(section) { if (!section) return; [...section.querySelectorAll(':scope > .rgh-interest-section-title')].forEach((heading, index) => { const grid = heading.nextElementSibling; if (!grid?.classList.contains('rgh-interest-grid')) return; const title = heading.textContent; const count = grid.children.length; grid.remove(); const fold = hubTools(`user-interest-topic-${index}`, title, () => grid, tx(`${count} 项详细分析 · 按需打开`, `${count} detailed breakdowns · open when needed`, `${count} Auswertungen · bei Bedarf öffnen`)); heading.replaceWith(fold); }); } function compactMetrics(target, key, keep = 3) { const row = target.querySelector(':scope > .rgh-metrics'); if (!row || row.children.length <= keep) return; const extras = [...row.children].slice(keep); const rest = $('div', { class: 'rgh-metrics' }, extras); row.after(hubTools(key, tx('更多指标与说明', 'More metrics and explanations', 'Weitere Kennzahlen und Erklärungen'), () => rest)); } function activeHubFilters(kind) { const defaults = kind === 'library' ? LIBRARY_FILTER_DEFAULTS : STATS_FILTER_DEFAULTS; const chips = []; const commit = () => { viewState.page = 1; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; const chip = (label, remove) => $('button', { type: 'button', class: 'rgh-filter-chip', ariaLabel: `${label} · ${tx('删除筛选', 'Remove filter', 'Filter entfernen')}`, onclick: () => { remove(); commit(); } }, `${label} ×`); if (viewState.query) chips.push(chip(`${tx('搜索', 'Search', 'Suche')}: ${viewState.query}`, () => { viewState.query = ''; })); const labelFor = key => ({ Countries: tx('国家', 'Country', 'Land'), PublicTags: tx('标签', 'Tag', 'Tag'), Genders: tx('类型', 'Type', 'Typ'), Statuses: 'Status', Groups: tx('组', 'Group', 'Gruppe'), Tags: tx('自己的标签', 'Own tag', 'Eigener Tag') }[key.replace(/^(library|stats)/u, '')] || key.replace(/^(library|stats)/u, '').replace(/([a-z])([A-Z])/gu, '$1 $2')); const extraLabels = { PublicTagMode: ['Tag matching', 'Tag-Verknüpfung'], GroupMode: ['Group matching', 'Gruppen-Verknüpfung'], Organization: ['Source', 'Quelle'], ProfileState: ['Profile data', 'Profildaten'], Feature: ['Picture/category', 'Bild/Kategorie'], WatchState: ['Watch history', 'Sehverlauf'], MediaState: ['Media', 'Medien'], OwnTagsState: ['Own tags', 'Eigene Tags'], GroupsState: ['Group assignment', 'Gruppenzuordnung'], NotesState: ['Notes', 'Notizen'], AgeMin: ['Minimum age', 'Mindestalter'], AgeMax: ['Maximum age', 'Höchstalter'], ViewerMin: ['Minimum viewers', 'Mindestens Zuschauer'], ViewerMax: ['Maximum viewers', 'Höchstens Zuschauer'], FollowersMin: ['Minimum followers', 'Mindestens Follower'], FollowersMax: ['Maximum followers', 'Höchstens Follower'], WatchMinMinutes: ['Minimum watch minutes', 'Mindestens Sehminuten'], WatchMaxMinutes: ['Maximum watch minutes', 'Höchstens Sehminuten'], CurrentOnlineMinMinutes: ['Minimum live minutes', 'Mindestens Online-Minuten'], SessionsMin: ['Minimum sessions', 'Mindestens Sitzungen'], SessionsMax: ['Maximum sessions', 'Höchstens Sitzungen'], RatingMin: ['Minimum rating', 'Mindestbewertung'], PriorityMin: ['Minimum priority', 'Mindestpriorität'], ActivityDays: ['Active within days', 'Aktiv in letzten Tagen'], AppearancesMin: ['Minimum appearances', 'Mindestens Sichtungen'], OnlineSamplesMin: ['Minimum live observations', 'Mindestens Live-Beobachtungen'], OnlineHour: ['Observed hour', 'Beobachtete Stunde'], OnlineWeekday: ['Observed weekday', 'Beobachteter Wochentag'], ActiveDaysMin: ['Minimum active days', 'Mindestens aktive Tage'], PreviewsMin: ['Minimum previews', 'Mindestens Vorschauen'], QuickViewsMin: ['Minimum quick views', 'Mindestens Schnellinfos'] }; const filterLabel = key => { const label = extraLabels[key.replace(/^(library|stats)/u, '')]; return label ? tx(label[0], label[0], label[1]) : labelFor(key); }; const filterValue = value => ({ f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), all: tx('全部', 'All', 'Alle'), any: tx('至少一个', 'At least one', 'Mindestens eines'), live: 'Live', offline: 'Offline', unknown: tx('未知', 'Unknown', 'Unbekannt'), with: tx('带有', 'With', 'Mit'), without: tx('不带', 'Without', 'Ohne'), watched: tx('已观看', 'Watched', 'Angesehen'), unwatched: tx('未观看', 'Unwatched', 'Nicht angesehen'), organized: tx('已整理', 'Organized', 'Organisiert'), catalog: tx('自动记录', 'Automatically recorded', 'Automatisch erfasst'), loaded: tx('已加载', 'Loaded', 'Geladen'), missing: tx('缺少', 'Missing', 'Fehlt') }[value] || value); const keys = { ...defaults, ...(kind === 'library' ? { libraryGroups: [], libraryTags: [] } : {}) }; Object.entries(keys).forEach(([key, fallback]) => { const value = viewState[key]; if (Array.isArray(fallback)) (value || []).forEach(entry => { const text = /Groups$/u.test(key) ? customLibraryGroups().find(group => group.id === entry)?.name || entry : entry; chips.push(chip(`${filterLabel(key)}: ${/Genders$|Statuses$/u.test(key) ? filterValue(text) : text}`, () => { viewState[key] = viewState[key].filter(item => item !== entry); })); }); else if (value !== fallback) chips.push(chip(`${filterLabel(key)}: ${filterValue(value)}`, () => { viewState[key] = fallback; })); }); if (kind === 'stats' && viewState.status !== 'all') chips.push(chip(`Status: ${viewState.status}`, () => { viewState.status = 'all'; })); if (kind === 'stats' && viewState.savedOnly) chips.push(chip(tx('仅资料库', 'Library only', 'Nur Bibliothek'), () => { viewState.savedOnly = false; })); return $('div', { class: 'rgh-active-filters', dataset: { hubActiveFilters: kind } }, [ $('strong', {}, tx('当前筛选', 'Active filters', 'Aktive Filter')), ...chips, !chips.length ? $('span', {}, tx('没有附加筛选', 'No additional filters', 'Keine zusätzlichen Filter')) : action(tx('全部重置', 'Reset all', 'Alle zurücksetzen'), () => { Object.entries(keys).forEach(([key, value]) => { viewState[key] = Array.isArray(value) ? [] : value; }); viewState.query = ''; if (kind === 'stats') { viewState.status = 'all'; viewState.savedOnly = false; } commit(); }), ]); } function action(text, run, kind = '') { return $('button', { class: `rgh-action ${kind}`.trim(), type: 'button', onclick: run }, text); } function selectControl(label, value, entries, onChange) { const select = $('select', { value: String(value), onchange: event => onChange(event.target.value) }, entries.map(([id, text]) => $('option', { value: String(id), selected: String(id) === String(value) }, text))); return $('label', { class: 'rgh-control' }, [$('span', {}, label), select]); } function rangeControl(label, value, onInput, min = 70, max = 160, step = 5) { const output = $('output', {}, `${value} %`); const input = $('input', { type: 'range', min, max, step, value, oninput: event => { const next = Number(event.target.value); output.textContent = `${next} %`; onInput(next); } }); return $('label', { class: 'rgh-range-control' }, [$('span', {}, [label, output]), input]); } function applyLibraryLayout() { if (!root) return; const bases = { compact: 275, comfortable: 340, large: 420, cinema: 520, focus: 680 }; const base = bases[viewState.density] || bases.comfortable; root.dataset.density = viewState.density; root.dataset.libraryColumns = String(viewState.libraryColumns); root.style.setProperty('--rgh-library-columns', String(Math.max(1, viewState.libraryColumns || 1))); root.style.setProperty('--rgh-library-card-min', `${Math.round(base * viewState.libraryCardScale / 100)}px`); } function installStyles() { if (style?.isConnected) return; style = $('style', { id: 'roomgrid-native-hub-style', html: trustedHtml(` :root{--rgh-brand:var(--rg-accent,#0b9ddd);--rgh-brand2:var(--rg-secondary,#69c9f2);--rgh-accent:var(--rg-accent,#f47321);--rgh-bg:var(--rg-page-bg,#0e1a26);--rgh-panel:var(--rg-panel,#172533);--rgh-panel2:var(--rg-surface-strong,#1f2e3d);--rgh-text:var(--rg-text,#f1f5f8);--rgh-muted:var(--rg-muted,#aeb9c3);--rgh-line:var(--rg-border,#405061)} [data-roomgrid-header-host]{position:relative!important;z-index:2147481650!important} .rgh-header-menu{display:inline-flex!important;align-items:stretch;align-self:stretch;gap:0;margin:0 0 0 10px;padding:0;border:0;border-left:1px solid #405061;border-radius:0;background:transparent;box-shadow:none;font:700 12px/1 Arial,Helvetica,sans-serif;vertical-align:middle} .rgh-header-menu button{position:relative;display:inline-flex;align-items:center;gap:0;min-height:40px;padding:0 11px;border:0;border-bottom:3px solid transparent;border-radius:0;background:transparent;color:#d8e0e7;font:inherit;text-transform:uppercase;cursor:pointer;white-space:nowrap} .rgh-header-menu button:hover{background:#263746;color:#78d4fb}.rgh-header-menu button.is-active{border-bottom-color:#0b9ddd;background:#223444;color:#78d4fb}.rgh-header-menu button.has-filter{border-bottom-color:#f47321;color:#fff}.rgh-header-group-count{display:inline-grid!important;min-width:18px;height:18px;margin-left:6px;padding:0 5px;place-items:center;border-radius:9px;background:#f47321;color:#fff!important;font-size:9px;line-height:18px}.rgh-header-group-count[hidden]{display:none!important} .rgh-header-menu .rgh-language-select{align-self:center;min-height:30px!important;margin:0 8px;padding:4px 7px!important;border-color:#405061!important;background:#101c28!important;color:#d8e0e7!important;text-transform:none} .rgh-header-menu [data-roomgrid-header-view] b{display:none}.rgh-header-menu .rgh-header-brand{margin-right:3px;border-bottom-color:#f47321;color:#fff;font-weight:900;letter-spacing:.02em}.rgh-header-menu .rgh-header-brand b{margin-left:3px;color:#f7a16c} body:has([data-roomgrid-header-menu]) #roomgrid-active-version{display:none!important} .rgh-global-menu{position:fixed;z-index:2147481760;width:min(520px,calc(100vw - 16px));max-height:min(720px,calc(100vh - 18px));overflow:auto;border:1px solid #536273;border-top:4px solid #f47321;border-radius:0 0 7px 7px;background:#172533;color:#f1f5f8;box-shadow:0 16px 42px rgba(0,0,0,.48);font:13px/1.4 Arial,Helvetica,sans-serif;color-scheme:dark}.rgh-global-menu *{box-sizing:border-box}.rgh-global-head{position:sticky;top:0;z-index:3;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:13px 14px;border-bottom:1px solid #405061;background:#1d2b39}.rgh-global-head strong{display:block;font-size:15px}.rgh-global-head small{display:block;margin-top:2px;color:#aeb9c3;font-size:10px}.rgh-global-head button{width:32px;height:32px;border:1px solid #536273;border-radius:4px;background:#263746;color:#fff;font-size:18px;cursor:pointer}.rgh-global-section{padding:12px 14px;border-bottom:1px solid #334454}.rgh-global-section h3{margin:0 0 8px;color:#78d4fb;font-size:10px;text-transform:uppercase;letter-spacing:.09em}.rgh-global-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.rgh-global-item{display:grid;grid-template-columns:32px minmax(0,1fr);gap:9px;align-items:center;min-height:54px;padding:8px 9px;border:1px solid #405061;border-radius:5px;background:#1c2c3a;color:#f1f5f8;text-align:left;cursor:pointer}.rgh-global-item:hover,.rgh-global-item:focus-visible{border-color:#69c9f2;background:#263746;outline:none}.rgh-global-item.is-active{border-color:#f47321;background:#2f3032}.rgh-global-item b{display:grid;width:30px;height:30px;place-items:center;border-radius:4px;background:#101c28;color:#78d4fb;font-size:15px}.rgh-global-item span{display:block;overflow:hidden;text-overflow:ellipsis;font-weight:800;white-space:nowrap}.rgh-global-item small{display:block;margin-top:2px;overflow:hidden;color:#aeb9c3;font-size:9px;text-overflow:ellipsis;white-space:nowrap}.rgh-global-foot{padding:10px 14px;color:#aeb9c3;font-size:10px} .rgh-group-popover{position:fixed;z-index:2147481750;width:min(390px,calc(100vw - 16px));max-height:min(620px,calc(100vh - 18px));overflow:auto;padding:0;border:1px solid #536273;border-top:4px solid #f47321;border-radius:0 0 7px 7px;background:#172533;color:#f1f5f8;box-shadow:0 14px 38px rgba(0,0,0,.45);font:13px/1.4 Arial,Helvetica,sans-serif;color-scheme:dark}.rgh-group-popover *{box-sizing:border-box}.rgh-group-popover-head{position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:10px;padding:13px 14px;border-bottom:1px solid #405061;background:#1d2b39}.rgh-group-popover-head strong{flex:1;font-size:14px}.rgh-group-popover-head button{width:30px;height:30px;border:1px solid #536273;border-radius:4px;background:#263746;color:#fff;font-size:18px;cursor:pointer}.rgh-group-popover-copy{margin:0;padding:12px 14px 8px;color:#aeb9c3;font-size:11px}.rgh-group-popover-actions{display:flex;gap:7px;padding:0 14px 10px}.rgh-group-popover-actions button,.rgh-group-mode button,.rgh-group-popover-footer button{min-height:34px;padding:7px 10px;border:1px solid #4a5b6c;border-radius:4px;background:#233443;color:#f1f5f8;font:700 11px/1.2 inherit;cursor:pointer}.rgh-group-popover-actions button:hover,.rgh-group-mode button:hover,.rgh-group-popover-footer button:hover{border-color:#69c9f2}.rgh-group-popover-actions button.is-active,.rgh-group-mode button.is-active{border-color:#f47321;background:#5a351d;color:#fff}.rgh-group-list{display:grid;gap:6px;padding:0 14px 12px}.rgh-group-list label{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:9px;min-height:42px;padding:8px 10px;border:1px solid #405061;border-radius:5px;background:#1c2c3a;cursor:pointer}.rgh-group-list label:hover{border-color:#69c9f2}.rgh-group-list label.is-active{border-color:#f47321;background:#2a3035}.rgh-group-list input{accent-color:#f47321}.rgh-group-list span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:750}.rgh-group-list b{min-width:28px;padding:3px 6px;border-radius:4px;background:#101c28;color:#aeb9c3;font-size:9px;text-align:center}.rgh-group-mode{display:grid;grid-template-columns:1fr 1fr;gap:7px;padding:0 14px 12px}.rgh-group-popover-status{padding:10px 14px;border-top:1px solid #405061;background:#101c28;color:#b9ddef;font-size:11px;font-weight:750}.rgh-group-popover-empty{margin:0 14px 12px;padding:18px;border:1px dashed #536273;border-radius:5px;color:#aeb9c3;text-align:center}.rgh-group-popover-footer{display:flex;justify-content:flex-end;padding:10px 14px;border-top:1px solid #405061}.rgh-home-group-hidden{display:none!important}.rgh-home-filter-banner{display:flex;align-items:center;gap:12px;width:100%;margin:10px 0 14px;padding:11px 13px;border:1px solid #405061;border-left:4px solid #f47321;border-radius:5px;background:#172533;color:#f1f5f8;font:700 12px/1.35 Arial,Helvetica,sans-serif}.rgh-home-filter-banner span{flex:1}.rgh-home-filter-banner button{min-height:32px;padding:6px 10px;border:1px solid #536273;border-radius:4px;background:#263746;color:#fff;font:700 11px/1 inherit;cursor:pointer}.rgh-home-filter-banner button:hover{border-color:#69c9f2} .rgh-root{position:fixed;inset:var(--rgh-top,0px) 0 0;z-index:2147481500;overflow:auto;overscroll-behavior:contain;background:var(--rgh-bg);color:var(--rgh-text);font:14px/1.45 Arial,Helvetica,sans-serif;color-scheme:dark} .rgh-root.rgh-root-embedded{position:relative;inset:auto;z-index:auto;min-height:0;overflow:visible;--rgh-bg:var(--dc-bg,#0e1a26);--rgh-panel:var(--dc-panel,#172533);--rgh-panel2:var(--dc-panel-soft,#1f2e3d);--rgh-text:var(--dc-text,#f1f5f8);--rgh-muted:var(--dc-muted,#aeb9c3);--rgh-line:var(--dc-line,#405061);background:transparent;color:var(--rgh-text);font-family:inherit;color-scheme:inherit}.rgh-root-embedded .rgh-shell{width:100%;min-height:0;padding:0 0 48px}.rgh-root-embedded .rgh-content{width:100%} .rgh-root *{box-sizing:border-box}.rgh-shell{width:min(1760px,100%);min-height:100%;margin:auto;padding:24px clamp(14px,2.5vw,42px) 70px} .rgh-topbar{position:sticky;top:0;z-index:40;display:flex;align-items:center;gap:16px;margin:-24px calc(clamp(14px,2.5vw,42px)*-1) 22px;padding:12px clamp(14px,2.5vw,42px);border-bottom:1px solid var(--rgh-line);background:#1d2b39} .rgh-logo{display:grid;place-items:center;flex:0 0 42px;height:42px;border-radius:5px;background:#f47321;color:#fff;font-weight:1000;font-size:19px;box-shadow:none} .rgh-title{min-width:180px}.rgh-title strong{display:block;font-size:15px}.rgh-title small{display:block;color:var(--rgh-muted);font-size:11px} .rgh-tabs{display:flex;align-items:center;gap:0;flex:1}.rgh-tabs button,.rgh-close,.rgh-menu-button{min-height:38px;padding:8px 13px;border:1px solid transparent;border-bottom:3px solid transparent;border-radius:0;background:transparent;color:var(--rgh-text);font-weight:780;cursor:pointer}.rgh-tabs button:hover,.rgh-tabs button.is-active{border-bottom-color:var(--rgh-brand);background:#263746;color:#78d4fb} .rgh-close,.rgh-menu-button{font-size:20px;line-height:1;padding-inline:13px;border:1px solid #4a5b6c;border-radius:5px;background:#263746}.rgh-menu-button{margin-left:auto}.rgh-close{margin-left:0}.rgh-hero{display:flex;justify-content:space-between;align-items:flex-end;gap:22px;margin:6px 0 20px;padding:22px 24px;border:1px solid var(--rgh-line);border-top:4px solid var(--rgh-brand);border-radius:7px;background:var(--rgh-panel);box-shadow:none} .rgh-kicker{margin:0 0 6px;color:#78d4fb;text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:900}.rgh-hero h1{margin:0;font-size:clamp(26px,3vw,42px);line-height:1.08;letter-spacing:-.035em}.rgh-hero p{max-width:780px;margin:10px 0 0;color:var(--rgh-muted);font-size:14px}.rgh-private{flex:0 0 auto;padding:7px 10px;border:1px solid #486071;border-radius:4px;color:#c7d2db;background:#223341;font-size:11px;font-weight:800} .rgh-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:11px;margin-bottom:18px}.rgh-metric{min-height:104px;padding:16px;border:1px solid var(--rgh-line);border-radius:6px;background:var(--rgh-panel);box-shadow:none}.rgh-metric small{display:block;color:var(--rgh-muted);font-size:11px;font-weight:760;text-transform:uppercase;letter-spacing:.06em}.rgh-metric strong{display:block;margin-top:7px;font-size:clamp(21px,2vw,31px);line-height:1.1}.rgh-metric span{display:block;margin-top:7px;color:#78d4fb;font-size:11px} .rgh-toolbar{display:flex;align-items:end;gap:10px;flex-wrap:wrap;margin:0 0 17px;padding:13px;border:1px solid var(--rgh-line);border-radius:6px;background:var(--rgh-panel)}.rgh-search{flex:1 1 260px}.rgh-control{display:grid;gap:5px;min-width:135px;color:var(--rgh-muted);font-size:11px;font-weight:700}.rgh-control input,.rgh-control select,.rgh-search input,.rgh-card textarea,.rgh-card input,.rgh-card select{width:100%;min-height:38px;padding:8px 10px;border:1px solid var(--rgh-line);border-radius:5px;background:#101c28;color:var(--rgh-text);font:inherit;outline:none}.rgh-control input:focus,.rgh-control select:focus,.rgh-search input:focus,.rgh-card textarea:focus,.rgh-card input:focus{border-color:var(--rgh-brand)} .rgh-check{display:flex;align-items:center;gap:7px;min-height:38px;color:var(--rgh-muted);font-size:12px;font-weight:700}.rgh-check input{accent-color:var(--rgh-accent)} .rgh-library-filter-center{border-top:4px solid var(--rgh-accent)}.rgh-library-filter-center .rgh-section-head{align-items:flex-start;flex-wrap:wrap}.rgh-filter-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(175px,1fr));gap:10px;margin:13px 0}.rgh-filter-grid .rgh-control{min-width:0}.rgh-filter-toggle-block{display:grid;gap:7px;margin:13px 0}.rgh-filter-toggle-block>strong,.rgh-filter-picker>strong{color:var(--rgh-muted);font-size:11px}.rgh-filter-toggle-list{display:flex;flex-wrap:wrap;gap:7px}.rgh-filter-toggle{display:inline-flex;align-items:center;gap:7px;min-height:35px;padding:7px 9px;border:1px solid var(--rgh-line);border-radius:5px;background:var(--rg-surface,#233443);color:var(--rgh-text);font:750 11px/1.2 inherit;cursor:pointer}.rgh-filter-toggle:hover{border-color:var(--rgh-brand2)}.rgh-filter-toggle.is-active{border-color:var(--rgh-accent);background:color-mix(in srgb,var(--rgh-accent) 24%,var(--rgh-panel));color:#fff}.rgh-filter-toggle b{min-width:22px;padding:2px 5px;border-radius:4px;background:rgba(0,0,0,.22);font-size:9px;text-align:center}.rgh-filter-pickers{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));gap:11px;margin:13px 0}.rgh-filter-picker{display:grid;align-content:start;gap:7px;min-width:0;padding:11px;border:1px solid var(--rgh-line);border-radius:6px;background:var(--rgh-panel2)}.rgh-filter-picker-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}.rgh-filter-picker select{min-width:0;min-height:38px;padding:8px 10px;border:1px solid var(--rgh-line);border-radius:5px;background:var(--rg-input,#101c28);color:var(--rgh-text)}.rgh-filter-chips{display:flex;flex-wrap:wrap;gap:6px;min-height:27px}.rgh-filter-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:5px 7px;border:1px solid var(--rgh-brand2);border-radius:5px;background:var(--rg-secondary-soft,#172d3b);color:var(--rgh-text);font:750 10px/1.2 inherit;cursor:pointer;overflow:hidden;text-overflow:ellipsis}.rgh-filter-empty{align-self:center;color:var(--rgh-muted);font-size:10px}.rgh-filter-result{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.rgh-filter-result b{padding:5px 8px;border-radius:5px;background:var(--rg-secondary-soft,#172d3b);color:var(--rgh-brand2);font-size:11px}.rgh-filter-note{flex:1 1 320px;margin:10px 0 0;color:var(--rgh-muted);font-size:10px}.rgh-filter-actions{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-top:13px;padding-top:12px;border-top:1px solid var(--rgh-line)} .rgh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px}.rgh-root[data-density="compact"] .rgh-grid{grid-template-columns:repeat(auto-fill,minmax(275px,1fr))}.rgh-root[data-density="large"] .rgh-grid{grid-template-columns:repeat(auto-fill,minmax(420px,1fr))}.rgh-root[data-density="cinema"] .rgh-grid{grid-template-columns:repeat(auto-fill,minmax(520px,1fr))}.rgh-root[data-density="focus"] .rgh-grid{grid-template-columns:repeat(auto-fill,minmax(680px,1fr))} .rgh-dataset-scope{display:grid;gap:10px;margin:0 0 17px;padding:13px 14px;border:1px solid var(--rgh-line);border-left:4px solid var(--rgh-brand2);border-radius:6px;background:var(--rgh-panel)}.rgh-dataset-scope-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.rgh-dataset-scope-head strong{font-size:13px}.rgh-dataset-scope-head span{color:var(--rgh-muted);font-size:10px}.rgh-dataset-chips,.rgh-lineage{display:flex;flex-wrap:wrap;gap:6px}.rgh-dataset-chip,.rgh-lineage span{padding:5px 7px;border:1px solid var(--rgh-line);border-radius:5px;background:#101c28;color:var(--rgh-muted);font-size:9px;font-weight:800}.rgh-dataset-chip b{margin-right:4px;color:#fff}.rgh-lineage{margin:10px 0}.rgh-lineage span[data-tone="personal"]{border-color:#f47321;color:#ffbd91}.rgh-lineage span[data-tone="public"]{border-color:#2683ad;color:#8edcff}.rgh-lineage span[data-tone="account"]{border-color:#8b5cf6;color:#c4b5fd}.rgh-lineage span[data-tone="local"]{border-color:#3c8d68;color:#86efac}.rgh-lineage span[data-tone="live"]{border-color:#22c55e;background:#153524;color:#86efac}.rgh-lineage span[data-tone="stale"]{border-color:#715f45;color:#d6b98d}.rgh-source-legend{margin-top:9px;color:var(--rgh-muted);font-size:10px} .rgh-library-grid{grid-template-columns:repeat(auto-fill,minmax(var(--rgh-library-card-min,340px),1fr))}.rgh-library-grid>.rgh-card{content-visibility:auto;contain-intrinsic-size:auto 620px}.rgh-root:not([data-library-columns="0"]) .rgh-library-grid{grid-template-columns:repeat(var(--rgh-library-columns),minmax(0,1fr))}.rgh-root[data-density="compact"] .rgh-library-grid .rgh-card-body{padding:27px 11px 11px}.rgh-root[data-density="compact"] .rgh-library-grid .rgh-statgrid{grid-template-columns:repeat(2,1fr);gap:5px}.rgh-root[data-density="focus"] .rgh-library-grid .rgh-card-body{padding-inline:22px} .rgh-card{position:relative;overflow:hidden;border:1px solid var(--rgh-line);border-radius:7px;background:var(--rgh-panel);box-shadow:none;transition:border-color .16s,transform .16s}.rgh-card:hover{border-color:#69c9f2;transform:translateY(-2px)} .rgh-media{position:relative;isolation:isolate;overflow:hidden;aspect-ratio:16/9;background:#1d2e3d}.rgh-media img{width:100%;height:100%;display:block;object-fit:cover}.rgh-media img.is-error{opacity:0}.rgh-media::after{content:attr(data-initial);position:absolute;inset:0;z-index:-1;display:grid;place-items:center;color:rgba(255,255,255,.36);font-size:58px;font-weight:1000}.rgh-avatar{position:absolute;left:16px;bottom:-24px;z-index:6;width:54px;height:54px;border:3px solid var(--rgh-panel);border-radius:50%;object-fit:cover;background:#24384a}.rgh-status{position:absolute;top:12px;left:12px;z-index:7;padding:5px 8px;border:1px solid #536273;border-radius:4px;background:rgba(14,26,38,.86);color:#e7edf7;font-size:10px;font-weight:900;text-transform:uppercase}.rgh-status.online{background:#287b55;border-color:#40936b;color:#effff7}.rgh-rank{position:absolute;top:12px;right:12px;z-index:7;padding:5px 8px;border-radius:4px;background:rgba(14,26,38,.86);font-size:10px;font-weight:900}.rgh-media-source{position:absolute;right:9px;bottom:8px;z-index:7;max-width:calc(100% - 92px);padding:4px 7px;border:1px solid rgba(255,255,255,.22);border-radius:4px;background:rgba(8,17,27,.78);color:#dbeafe;font-size:8px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rgh-example-gallery{margin:10px 0;border:1px solid #364756;border-radius:5px;background:#121f2b}.rgh-example-gallery>summary{padding:9px 11px;color:#b9ddef;font-size:11px;font-weight:800;cursor:pointer}.rgh-example-gallery-controls{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 9px 9px}.rgh-example-gallery-status{min-width:0;color:var(--rgh-muted);font-size:9px;line-height:1.35}.rgh-example-gallery-save{flex:0 0 auto;padding:6px 8px;border:1px solid var(--rgh-brand2);border-radius:4px;background:#17364a;color:#dff6ff;font-size:9px;font-weight:900;cursor:pointer}.rgh-example-gallery-save:disabled{opacity:.55;cursor:wait}.rgh-example-gallery-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:0 9px 9px}.rgh-example-shot{position:relative;overflow:hidden;aspect-ratio:16/10;border:1px solid #405061;border-radius:4px;background:#0b1520}.rgh-example-shot img{display:block;width:100%;height:100%;object-fit:cover}.rgh-example-shot span{position:absolute;inset:auto 0 0;padding:4px 5px;background:rgba(5,12,20,.78);color:#fff;font-size:8px;font-weight:800;text-align:center}.rgh-example-shot.is-archived{border-color:var(--rgh-brand2)}.rgh-media-source.is-archived{border-color:#16a77c;background:rgba(7,66,53,.88);color:#d8fff2}.rgh-catalog-badge{display:inline-flex;margin:0 0 8px;padding:4px 7px;border:1px solid #315d75;border-radius:4px;background:#172d3b;color:#9addf8;font-size:9px;font-weight:800} .rgh-card-body{padding:31px 16px 16px}.rgh-card.no-media .rgh-card-body{padding-top:16px}.rgh-card-title{display:flex;align-items:center;gap:8px;margin-bottom:6px}.rgh-card-title h2{min-width:0;margin:0;font-size:18px;overflow:hidden;text-overflow:ellipsis}.rgh-card-title .rgh-fav{color:#ffc75a}.rgh-model-room-title{color:inherit;text-decoration:none}.rgh-model-room-title:hover{text-decoration:underline;text-decoration-color:var(--rgh-brand2)}.rgh-library-quick-actions{display:flex;gap:7px;margin:8px 0 10px}.rgh-library-quick-actions .rgh-room-direct{display:inline-flex;align-items:center;justify-content:center;min-width:min(210px,100%);text-decoration:none}.rgh-library-quick-actions .rgh-room-direct.is-live{border-color:#37a96f;background:#25764f}.rgh-bio{display:-webkit-box;min-height:40px;margin:7px 0 12px;overflow:hidden;color:var(--rgh-muted);font-size:12px;-webkit-line-clamp:2;-webkit-box-orient:vertical}.rgh-public-profile{margin:7px 0 10px;border:1px solid var(--rgh-line);border-radius:5px;background:#162636}.rgh-public-profile>summary{padding:7px 9px;color:#9addf8;font-size:10px;font-weight:850;cursor:pointer}.rgh-public-profile-body{display:grid;gap:8px;padding:0 9px 9px;color:var(--rgh-muted);font-size:10px;line-height:1.45}.rgh-public-profile-body strong{display:block;color:var(--rgh-text);font-size:9px;text-transform:uppercase}.rgh-public-profile-body p{margin:2px 0 0;white-space:pre-wrap}.rgh-profile-links{display:flex;flex-wrap:wrap;gap:5px}.rgh-profile-links a{max-width:180px;padding:4px 6px;border:1px solid #3b6379;border-radius:4px;color:#9addf8;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rgh-statgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin:10px 0}.rgh-stat{padding:8px;border:1px solid #364756;border-radius:5px;background:#1c2c3a}.rgh-stat small{display:block;color:var(--rgh-muted);font-size:9px;text-transform:uppercase}.rgh-stat strong{display:block;margin-top:3px;font-size:13px}.rgh-tags,.rgh-groups{display:flex;flex-wrap:wrap;gap:5px;margin:8px 0}.rgh-tag{max-width:160px;padding:4px 7px;border-radius:4px;background:#243d4e;color:#9addf8;font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rgh-group{padding:4px 7px;border:1px solid #486071;border-radius:4px;color:#b8c8d4;font-size:10px}.rgh-spark{display:flex;align-items:end;gap:3px;height:38px;margin:10px 0 5px}.rgh-spark i{flex:1;min-width:3px;border-radius:2px 2px 0 0;background:var(--rgh-brand);opacity:.86}.rgh-meta{display:grid;gap:3px;margin:9px 0;color:var(--rgh-muted);font-size:11px}.rgh-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}.rgh-action{min-height:34px;padding:7px 10px;border:1px solid var(--rgh-line);border-radius:5px;background:#233443;color:var(--rgh-text);font:700 11px/1.2 inherit;cursor:pointer}.rgh-action:hover{border-color:#69c9f2;background:#2b4052}.rgh-action.primary{border-color:#f58a47;background:#f47321;color:#fff}.rgh-action.active{border-color:#d99c3f;background:#7e531e;color:#ffe1a4}.rgh-action.danger{border-color:#77434a;color:#ffb0b7}.rgh-action.disabled{opacity:.45;pointer-events:none}.rgh-action-more{position:relative}.rgh-action-more>summary{list-style:none}.rgh-action-more>summary::-webkit-details-marker{display:none}.rgh-action-menu{position:absolute;right:0;bottom:calc(100% + 6px);z-index:40;display:grid;gap:6px;width:min(230px,80vw);padding:8px;border:1px solid var(--rgh-line);border-radius:7px;background:var(--rgh-panel2);box-shadow:0 18px 45px rgba(0,0,0,.4)}.rgh-action-menu .rgh-action{width:100%;text-align:left}.rgh-card:has(.rgh-action-more[open]){overflow:visible;z-index:42}.rgh-edit{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-top:11px}.rgh-edit textarea{min-height:70px;resize:vertical;grid-row:span 2}.rgh-stars{display:flex;align-items:center;gap:2px}.rgh-stars button{border:0;background:transparent;color:#62718a;font-size:18px;cursor:pointer;padding:1px}.rgh-stars button.on{color:#ffc24d} .rgh-online-estimate{display:grid;gap:5px;margin:11px 0;padding:10px 11px;border:1px solid #315d75;border-left:4px solid var(--rgh-brand);border-radius:5px;background:#172d3b}.rgh-online-estimate small{display:block;color:#9addf8;font-size:9px;font-weight:850;text-transform:uppercase;letter-spacing:.06em}.rgh-online-estimate strong{display:block;margin-top:3px;font-size:13px}.rgh-online-estimate span{color:var(--rgh-muted);font-size:10px}.rgh-public-tags>small{color:var(--rgh-muted);font-size:9px;font-weight:800;text-transform:uppercase}.rgh-profile-editor{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(130px,.6fr);gap:9px;margin-top:12px;padding:11px;border:1px solid #364756;border-radius:6px;background:#121f2b}.rgh-note-editor,.rgh-priority-editor{display:grid;align-content:start;gap:5px;color:var(--rgh-muted);font-size:10px;font-weight:800}.rgh-note-editor textarea{min-height:78px;resize:vertical}.rgh-tag-editor{position:relative;grid-column:1/-1;display:grid;gap:7px}.rgh-editor-label{display:flex;align-items:center;justify-content:space-between;gap:8px}.rgh-editor-label strong{font-size:11px}.rgh-editor-label span,.rgh-tag-editor>small{color:var(--rgh-muted);font-size:9px}.rgh-tag-editor-chips{display:flex;flex-wrap:wrap;gap:5px;min-height:26px;align-items:center}.rgh-tag-editor-chips>span{color:var(--rgh-muted);font-size:10px}.rgh-tag-editor-chips button{padding:4px 7px;border:1px solid #315d75;border-radius:4px;background:#243d4e;color:#9addf8;font-size:10px;cursor:pointer}.rgh-tag-entry{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}.rgh-tag-entry .rgh-action{margin:0}.rgh-tag-suggestions{display:none;grid-template-columns:repeat(auto-fit,minmax(125px,1fr));gap:5px;padding:7px;border:1px solid #3f5364;border-radius:5px;background:#1c2c3a}.rgh-tag-suggestions.is-open{display:grid}.rgh-tag-suggestions button{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px;border:1px solid transparent;border-radius:4px;background:#233443;color:#9addf8;cursor:pointer;text-align:left}.rgh-tag-suggestions button:hover{border-color:#69c9f2}.rgh-tag-suggestions button span{color:var(--rgh-muted);font-size:9px} .rgh-library-display{border-top:4px solid var(--rgh-brand)}.rgh-library-layout-row{display:flex;align-items:end;gap:10px;flex-wrap:wrap;margin:12px 0}.rgh-range-control{display:grid;gap:5px;min-width:220px;flex:1;color:var(--rgh-muted);font-size:11px;font-weight:700}.rgh-range-control>span{display:flex;justify-content:space-between;gap:10px}.rgh-range-control output{color:#78d4fb;font-weight:900}.rgh-range-control input{width:100%;accent-color:var(--rgh-accent)}.rgh-library-presets{display:flex;flex-wrap:wrap;gap:7px}.rgh-detail-options{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:7px;margin-top:13px}.rgh-detail-check{display:flex;align-items:center;gap:8px;min-height:38px;padding:8px 10px;border:1px solid #364756;border-radius:5px;background:#1c2c3a;color:#dce6ee;font-size:11px;font-weight:720;cursor:pointer}.rgh-detail-check:hover{border-color:#69c9f2}.rgh-detail-check input{accent-color:#f47321}.rgh-library-display-note{margin:10px 0 0;color:var(--rgh-muted);font-size:10px} .rgh-group-center{border-top:4px solid #f47321}.rgh-tag-filter-center{border-top:4px solid #0b9ddd}.rgh-group-create{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:8px;max-width:760px}.rgh-group-create input,.rgh-group-admin-row input{min-height:36px;padding:7px 9px;border:1px solid var(--rgh-line);border-radius:5px;background:#101c28;color:var(--rgh-text)}.rgh-group-filterbar{display:flex;flex-wrap:wrap;gap:7px;margin:13px 0}.rgh-group-filter{display:inline-flex;align-items:center;gap:8px;min-height:34px;padding:6px 9px;border:1px solid #445465;border-radius:5px;background:#233443;color:#f1f5f8;font-weight:750;cursor:pointer}.rgh-group-filter:hover{border-color:#69c9f2}.rgh-group-filter.is-active{border-color:#0b9ddd;background:#24485c;color:#bcecff}.rgh-group-filter b{min-width:22px;padding:2px 5px;border-radius:4px;background:rgba(0,0,0,.2);font-size:9px;text-align:center}.rgh-group-admin,.rgh-group-picker{margin-top:10px;border:1px solid #364756;border-radius:5px;background:#121f2b}.rgh-group-admin>summary,.rgh-group-picker>summary{padding:9px 11px;color:#b9ddef;font-weight:800;font-size:11px;cursor:pointer}.rgh-group-admin-list,.rgh-group-picker-list{display:grid;gap:7px;padding:0 10px 10px}.rgh-group-admin-row{display:grid;grid-template-columns:minmax(150px,1fr) auto auto;gap:7px}.rgh-group-picker-list{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}.rgh-group-picker-list label{display:flex;align-items:center;gap:7px;padding:7px;border:1px solid #364756;border-radius:4px;background:#1c2c3a;font-size:11px;cursor:pointer}.rgh-group-picker-list input{accent-color:#f47321}.rgh-muted-copy{color:var(--rgh-muted);font-size:11px}.rgh-empty.rgh-empty-small{padding:18px} .rgh-section{margin:18px 0;padding:18px;border:1px solid var(--rgh-line);border-radius:7px;background:var(--rgh-panel)}.rgh-section-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:13px}.rgh-section h2,.rgh-section h3{margin:0}.rgh-chart{display:flex;align-items:end;gap:6px;height:160px;padding-top:20px}.rgh-bar{display:flex;flex:1;min-width:12px;height:100%;align-items:center;justify-content:end;flex-direction:column;gap:5px}.rgh-bar i{width:min(28px,80%);min-height:2px;border-radius:3px 3px 0 0;background:var(--rgh-brand)}.rgh-bar span{color:var(--rgh-muted);font-size:9px;white-space:nowrap}.rgh-list{display:grid;gap:7px}.rgh-list-row{display:grid;grid-template-columns:minmax(120px,1.3fr) repeat(4,minmax(80px,.7fr));gap:9px;align-items:center;padding:9px 11px;border:1px solid #364756;border-radius:5px;background:#1c2c3a;font-size:12px}.rgh-list-row strong{overflow:hidden;text-overflow:ellipsis}.rgh-list-row span{color:var(--rgh-muted)} .rgh-empty{grid-column:1/-1;padding:55px 25px;border:1px dashed #516171;border-radius:7px;text-align:center;color:var(--rgh-muted)}.rgh-pagination{display:flex;align-items:center;justify-content:center;gap:10px;margin:22px 0}.rgh-pagination span{color:var(--rgh-muted);font-size:12px}.rgh-loading{display:grid;min-height:45vh;place-items:center;color:var(--rgh-muted);font-weight:800} .rgh-account-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}.rgh-account-card{padding:16px;border:1px solid var(--rgh-line);border-radius:6px;background:#1c2c3a}.rgh-account-card h3{margin:0 0 10px}.rgh-account-card dl{display:grid;grid-template-columns:1fr auto;gap:7px;margin:0}.rgh-account-card dt{color:var(--rgh-muted)}.rgh-account-card dd{margin:0;font-weight:800} .rgh-interest-center{border-top:4px solid var(--rgh-accent)}.rgh-interest-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(155px,1fr));gap:10px;margin:0 0 16px}.rgh-interest-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(285px,1fr));gap:12px}.rgh-interest-card{min-width:0;padding:15px;border:1px solid #405061;border-radius:6px;background:#121f2b}.rgh-interest-card h3{margin:0 0 13px;color:#eaf4fa;font-size:14px}.rgh-interest-list{display:grid;gap:10px}.rgh-interest-row{display:grid;gap:4px}.rgh-interest-line{display:flex;align-items:center;justify-content:space-between;gap:9px}.rgh-interest-line strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.rgh-interest-line span{flex:0 0 auto;color:#8ddcff;font-size:10px;font-weight:800}.rgh-interest-row small{color:var(--rgh-muted);font-size:9px}.rgh-interest-track{height:6px;overflow:hidden;border-radius:5px;background:#263746}.rgh-interest-track i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--rgh-brand),var(--rgh-accent))}.rgh-interest-note{margin:14px 0 0;padding:10px 12px;border-left:4px solid #536273;background:#101c28;color:var(--rgh-muted);font-size:10px}.rgh-interest-section-title{margin:20px 0 10px!important;font-size:16px} .rgh-topbar{background:var(--rgh-panel2)}.rgh-logo,.rgh-action.primary{border-color:var(--rgh-accent);background:var(--rgh-accent);color:var(--rg-accent-text,#fff)}.rgh-tabs button:hover,.rgh-tabs button.is-active{background:var(--rg-secondary-soft,#263746);color:var(--rgh-brand2)}.rgh-card:hover,.rgh-action:hover,.rgh-detail-check:hover,.rgh-group-filter:hover{border-color:var(--rgh-brand2)}.rgh-control input,.rgh-control select,.rgh-search input,.rgh-card textarea,.rgh-card input,.rgh-card select,.rgh-group-create input,.rgh-group-admin-row input{background:var(--rg-input,#101c28)}.rgh-stat,.rgh-account-card,.rgh-detail-check,.rgh-list-row,.rgh-group-picker-list label{border-color:var(--rgh-line);background:var(--rg-surface,#1c2c3a);color:var(--rgh-text)}.rgh-action,.rgh-group-filter{border-color:var(--rgh-line);background:var(--rg-surface,#233443);color:var(--rgh-text)}.rgh-tag,.rgh-online-estimate,.rgh-catalog-badge{border-color:var(--rgh-brand2);background:var(--rg-secondary-soft,#172d3b);color:var(--rgh-text)}.rgh-group-center,.rgh-interest-center{border-top-color:var(--rgh-accent)}.rgh-tag-filter-center,.rgh-library-display{border-top-color:var(--rgh-brand)}.rgh-interest-card,.rgh-profile-editor,.rgh-example-gallery,.rgh-group-admin,.rgh-group-picker{border-color:var(--rgh-line);background:var(--rgh-panel2)}.rgh-interest-card h3{color:var(--rgh-text)}.rgh-range-control output,.rgh-kicker,.rgh-metric span{color:var(--rgh-brand2)} .rg-inline-hover-video{position:absolute!important;inset:0!important;z-index:5!important;width:100%!important;height:100%!important;object-fit:cover!important;background:#050a12!important}.rg-inline-hover-video:not(.is-playing){opacity:0} .rgh-content>.rgh-hero{padding:12px 16px;margin:4px 0 12px;align-items:center}.rgh-content>.rgh-hero h1{font-size:clamp(23px,2vw,30px)}.rgh-content>.rgh-hero p{margin-top:5px;font-size:12px}.rgh-content .rgh-metric{min-height:74px;padding:10px 12px}.rgh-content .rgh-metric strong{font-size:23px}.rgh-tools{margin:10px 0;border:1px solid var(--rgh-line);border-radius:7px;background:var(--rgh-panel)}.rgh-tools>summary{padding:11px 14px;cursor:pointer;min-height:44px}.rgh-tools>summary small{margin-left:12px;color:var(--rgh-muted);font-size:11px}.rgh-tools>summary:focus-visible{outline:2px solid var(--rgh-brand2);outline-offset:2px}.rgh-tools-body{padding:0 12px 12px;max-height:min(65vh,680px);overflow:auto}.rgh-tools-body>.rgh-section{margin:8px 0}.rgh-active-filters,.rgh-library-subviews{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin:9px 0}.rgh-active-filters{padding:8px 10px;border:1px solid var(--rgh-line);border-radius:6px;font-size:11px}.rgh-library-subviews .is-active{background:var(--rgh-accent);color:#fff}.rgh-period-comparison{padding:8px 0}.rgh-period-comparison p{font-size:12px;color:var(--rgh-muted)}.rgh-following-toolbar{margin-bottom:10px} .rgh-tools-rack{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}.rgh-tools-rack>.rgh-tools{margin:0;min-width:0;flex:1 1 220px}.rgh-tools-rack>.rgh-tools[open]{flex-basis:100%;order:1}.rgh-tools-rack summary small{display:block;margin:5px 0 0 16px;line-height:1.4}.rgh-tools-close{display:block;margin:12px 0 0 auto}.rgh-tools .rgh-tools .rgh-tools-body{max-height:none;overflow:visible} .rgh-hero.rgh-library-heading{padding:12px 16px;gap:12px}.rgh-library-heading h1{font-size:22px;margin:0}.rgh-library-heading .rgh-library-summary{margin:6px 0 0;line-height:1.4;font-size:12px;color:var(--rgh-muted)} @media(max-width:920px){.rgh-topbar{flex-wrap:wrap}.rgh-title{flex:1}.rgh-tabs{order:3;flex-basis:100%;overflow:auto}.rgh-hero{align-items:flex-start;flex-direction:column}.rgh-list-row{grid-template-columns:1fr 1fr}.rgh-list-row>*:nth-child(n+4){display:none}.rgh-tools>summary small{display:block;margin:4px 0 0 16px}.rgh-tools-rack>.rgh-tools{flex-basis:155px}} @media(max-width:660px){.rgh-header-menu{gap:2px;margin-inline:3px}.rgh-header-menu button{padding:6px}.rgh-header-menu button span{display:none}.rgh-header-menu .rgh-header-brand b{display:none}.rgh-header-group-count{margin-left:0}.rgh-group-popover,.rgh-global-menu{max-height:calc(100vh - 12px)}.rgh-global-grid{grid-template-columns:1fr}.rgh-home-filter-banner{align-items:flex-start;flex-direction:column}.rgh-shell{padding-inline:10px}.rgh-topbar{margin-inline:-10px;padding-inline:10px}.rgh-grid,.rgh-root[data-density="compact"] .rgh-grid,.rgh-root[data-density="large"] .rgh-grid,.rgh-root[data-density="cinema"] .rgh-grid,.rgh-root[data-density="focus"] .rgh-grid,.rgh-root:not([data-library-columns="0"]) .rgh-library-grid{grid-template-columns:1fr!important}.rgh-card:hover{transform:none}.rgh-statgrid{grid-template-columns:repeat(2,1fr)}.rgh-edit,.rgh-profile-editor,.rgh-group-create,.rgh-group-admin-row,.rgh-interest-grid{grid-template-columns:1fr}.rgh-edit textarea{grid-row:auto}.rgh-tag-entry{grid-template-columns:1fr}.rgh-private{white-space:normal}.rgh-chart{gap:2px}.rgh-bar span{font-size:7px}} `) }); document.head.appendChild(style); } function findHeaderHost() { const candidates = [ document.querySelector('[data-testid="header-nav-bar"]:not(.HeaderNavBar--hidden)'), document.querySelector('#desktop-spa-header nav:not(.HeaderNavBar--hidden)'), document.querySelector('#desktop-spa-header [data-testid="header-top-row"]'), document.querySelector('#desktop-spa-header [data-testid*="nav" i]'), document.querySelector('header [data-testid*="nav" i]'), document.querySelector('[role="banner"] nav'), document.querySelector('header nav'), document.querySelector('#desktop-spa-header'), document.querySelector('[data-testid="header"]'), document.querySelector('header'), document.querySelector('[role="banner"]'), document.querySelector('#header'), document.querySelector('body > nav'), ]; return candidates.find(node => node?.isConnected && !node.closest('[data-roomgrid-native-hub]')) || null; } const NATIVE_CARD_SELECTOR = '[data-testid="room-card"],li.RoomCard,li.room_list_room,li.roomCard,article[data-username],li[data-username]'; function customHomeGroups() { return (activeStore?.state?.groups || []).filter(group => group && !group.system && !group.archived); } function isHomepage() { return (location.pathname.replace(/\/+$/u, '') || '/') === '/'; } function nativeCardId(card) { const direct = normalizeUsername(card?.getAttribute?.('data-username') || card?.getAttribute?.('data-room') || ''); if (isLikelyUsername(direct)) return direct; const nested = card?.querySelector?.('[data-username],[data-room]'); const nestedId = normalizeUsername(nested?.getAttribute?.('data-username') || nested?.getAttribute?.('data-room') || ''); if (isLikelyUsername(nestedId)) return nestedId; for (const link of card?.querySelectorAll?.('a[href]') || []) { const id = roomIdFromHref(link.getAttribute('href')); if (id) return id; } return ''; } function nativeRoomCards() { return [...document.querySelectorAll(NATIVE_CARD_SELECTOR)].filter(card => !card.parentElement?.closest?.(NATIVE_CARD_SELECTOR)); } function selectedHomeGroupNames() { const selected = new Set(viewState.homeGroups); return customHomeGroups().filter(group => selected.has(group.id)).map(groupLabel); } function updateHomeFilterStatus() { const status = groupMenuPanel?.querySelector?.('.rgh-group-popover-status'); if (!status) return; if (!viewState.homeGroups.length) status.textContent = tx('未启用分组筛选 · 显示所有模型。', 'No group filter active · all models are shown.', 'Kein Gruppenfilter aktiv · alle Models werden angezeigt.', 'No hay ningún filtro de grupo activo · se muestran todos los modelos.', 'Nenhum filtro de grupo ativo · todos os modelos são exibidos.'); else status.textContent = tx(`${homeFilterStats.total} 个当前加载的模型中有 ${homeFilterStats.visible} 个可见。`, `${homeFilterStats.visible} of ${homeFilterStats.total} currently loaded models visible.`, `${homeFilterStats.visible} von ${homeFilterStats.total} aktuell geladenen Models sichtbar.`, `${homeFilterStats.visible} de ${homeFilterStats.total} modelos cargados visibles.`, `${homeFilterStats.visible} de ${homeFilterStats.total} modelos carregados visíveis.`); } function removeHomeFilterBanner() { homeFilterBanner?.remove(); homeFilterBanner = null; } function updateHomeFilterBanner() { if (!isHomepage() || !viewState.homeGroups.length) { removeHomeFilterBanner(); return; } if (!homeFilterBanner?.isConnected) { homeFilterBanner = $('div', { class: 'rgh-home-filter-banner', dataset: { roomgridHomeGroupBanner: '1' } }, [ $('span'), $('button', { type: 'button', onclick: () => setHomeGroupFilter([]) }, tx('清除分组筛选', 'Clear group filter', 'Gruppenfilter löschen', 'Borrar filtro de grupo', 'Limpar filtro de grupo')), ]); } const names = selectedHomeGroupNames(); const allMode = viewState.homeGroupMode === 'all' && names.length > 1; homeFilterBanner.querySelector('span').textContent = tx( `启用的分组筛选:${names.join(', ') || '—'} · ${homeFilterStats.total} 张可见卡片中有 ${homeFilterStats.visible} 张匹配(${allMode ? '所有组' : '至少一个组'})。`, `Active group filter: ${names.join(', ') || '—'} · ${homeFilterStats.visible} of ${homeFilterStats.total} visible cards match (${allMode ? 'all groups' : 'at least one group'}).`, `Aktiver Gruppenfilter: ${names.join(', ') || '—'} · ${homeFilterStats.visible} von ${homeFilterStats.total} sichtbaren Karten passen (${allMode ? 'alle Gruppen' : 'mindestens eine Gruppe'}).`, `Filtro de grupo activo: ${names.join(', ') || '—'} · coinciden ${homeFilterStats.visible} de ${homeFilterStats.total} tarjetas visibles (${allMode ? 'todos los grupos' : 'al menos un grupo'}).`, `Filtro de grupo ativo: ${names.join(', ') || '—'} · ${homeFilterStats.visible} de ${homeFilterStats.total} cartões visíveis correspondem (${allMode ? 'todos os grupos' : 'pelo menos um grupo'}).`, ); const anchor = document.querySelector('#roomlist_root,[data-testid="room-list"],.RoomCardGrid') || nativeRoomCards()[0]; if (anchor?.parentElement && homeFilterBanner.parentElement !== anchor.parentElement) anchor.parentElement.insertBefore(homeFilterBanner, anchor); else if (anchor?.parentElement && homeFilterBanner.nextElementSibling !== anchor) anchor.parentElement.insertBefore(homeFilterBanner, anchor); } function applyHomeGroupFilter() { if (!activeStore) return; const valid = new Set(customHomeGroups().map(group => group.id)); const clean = viewState.homeGroups.filter(id => valid.has(id)); if (clean.length !== viewState.homeGroups.length) { viewState.homeGroups = clean; savePrefs(); } const cards = nativeRoomCards(); if (!isHomepage() || !clean.length) { document.querySelectorAll('.rgh-home-group-hidden').forEach(card => card.classList.remove('rgh-home-group-hidden')); document.body?.classList?.remove('rgh-home-group-filter-active'); homeFilterStats = { visible: cards.filter(card => !!nativeCardId(card)).length, total: cards.filter(card => !!nativeCardId(card)).length }; removeHomeFilterBanner(); syncHeaderButtons(); updateHomeFilterStatus(); return; } const rooms = new Map((activeStore.state.rooms || []).map(room => [normalizeUsername(room.id), room])); let visible = 0; let total = 0; cards.forEach(card => { const id = nativeCardId(card); if (!id) { card.classList.remove('rgh-home-group-hidden'); return; } total += 1; const room = rooms.get(id); const matches = !!room && (viewState.homeGroupMode === 'all' ? clean.every(groupId => roomInGroup(room, groupId)) : clean.some(groupId => roomInGroup(room, groupId))); card.classList.toggle('rgh-home-group-hidden', !matches); if (matches) visible += 1; }); homeFilterStats = { visible, total }; document.body?.classList?.add('rgh-home-group-filter-active'); updateHomeFilterBanner(); syncHeaderButtons(); updateHomeFilterStatus(); } const scheduleHomeGroupFilter = debounce(applyHomeGroupFilter, 90); function setHomeGroupFilter(groupIds, mode = viewState.homeGroupMode) { const valid = new Set(customHomeGroups().map(group => group.id)); viewState.homeGroups = [...new Set((groupIds || []).map(String).filter(id => valid.has(id)))].slice(0, 40); viewState.homeGroupMode = mode === 'all' ? 'all' : 'any'; savePrefs(); applyHomeGroupFilter(); renderHeaderGroupMenu(); } function positionHeaderGroupMenu() { if (!groupMenuPanel?.isConnected || !groupMenuButton?.isConnected) return; const rect = groupMenuButton.getBoundingClientRect(); const width = Math.min(390, Math.max(260, innerWidth - 16)); const left = Math.max(8, Math.min(rect.left, innerWidth - width - 8)); const below = rect.bottom + 1; const availableBelow = innerHeight - below - 8; groupMenuPanel.style.width = `${width}px`; groupMenuPanel.style.left = `${Math.round(left)}px`; groupMenuPanel.style.top = `${Math.round(availableBelow >= 240 ? below : Math.max(8, rect.top - Math.min(620, groupMenuPanel.scrollHeight) - 1))}px`; groupMenuPanel.style.maxHeight = `${Math.max(180, Math.floor(availableBelow >= 240 ? availableBelow : rect.top - 9))}px`; } function closeHeaderGroupMenu() { const restoreFocus = !!groupMenuPanel?.contains(document.activeElement); const returnTarget = groupMenuReturnFocus?.isConnected ? groupMenuReturnFocus : groupMenuButton; groupMenuPanel?.remove(); groupMenuPanel = null; if (groupMenuOutsideHandler) document.removeEventListener('pointerdown', groupMenuOutsideHandler, true); groupMenuOutsideHandler = null; groupMenuReturnFocus = null; syncHeaderButtons(); if (restoreFocus) requestAnimationFrame(() => returnTarget?.focus?.()); } function renderHeaderGroupMenu() { if (!groupMenuPanel?.isConnected) return; const groups = customHomeGroups(); const selected = new Set(viewState.homeGroups); const roomCounts = new Map(groups.map(group => [group.id, (activeStore?.state?.rooms || []).filter(room => roomInGroup(room, group.id)).length])); groupMenuPanel.replaceChildren(...[ $('div', { class: 'rgh-group-popover-head' }, [ $('strong', {}, tx('按群组筛选主页', 'Filter homepage by groups', 'Startseite nach Gruppen filtern', 'Filtrar la página de inicio por grupos', 'Filtrar a página inicial por grupos')), $('button', { type: 'button', onclick: closeHeaderGroupMenu, title: tx('关闭', 'Close', 'Schließen'), ariaLabel: tx('关闭群组菜单', 'Close group menu', 'Gruppenmenü schließen', 'Cerrar menú de grupos', 'Fechar menu de grupos') }, '×'), ]), $('p', { class: 'rgh-group-popover-copy' }, tx('选择一个或多个自己的群组。其他模特卡片会立即从 Chaturbate 主页隐藏。', 'Choose one or more of your own groups. Other model cards are immediately hidden on the Chaturbate homepage.', 'Wähle eine oder mehrere eigene Gruppen. Andere Modelkarten werden auf der Chaturbate-Startseite sofort ausgeblendet.', 'Elige uno o más grupos propios. Las demás tarjetas de modelos se ocultarán inmediatamente en la página de inicio de Chaturbate.', 'Escolha um ou mais grupos próprios. Os outros cartões de modelos serão ocultados imediatamente na página inicial do Chaturbate.')), $('div', { class: 'rgh-group-popover-actions' }, [ $('button', { class: selected.size ? '' : 'is-active', type: 'button', onclick: () => setHomeGroupFilter([]) }, tx('显示全部', 'Show all', 'Alle anzeigen', 'Mostrar todo', 'Mostrar tudo')), ]), groups.length ? $('div', { class: 'rgh-group-list' }, groups.map(group => $('label', { class: selected.has(group.id) ? 'is-active' : '' }, [ $('input', { type: 'checkbox', checked: selected.has(group.id), onchange: event => { const next = new Set(viewState.homeGroups); if (event.target.checked) next.add(group.id); else next.delete(group.id); setHomeGroupFilter([...next]); } }), $('span', { title: rawUiText(groupLabel(group)) }, rawUiText(groupLabel(group))), $('b', { title: tx('此群组中的模特', 'Models in this group', 'Models in dieser Gruppe', 'Modelos de este grupo', 'Modelos neste grupo') }, String(roomCounts.get(group.id) || 0)), ]))) : $('div', { class: 'rgh-group-popover-empty' }, tx('还没有自己的群组。请在模特库中创建群组并分配模特。', 'There is no custom group yet. Create one in the model library and assign models there.', 'Noch keine eigene Gruppe vorhanden. Erstelle sie in der Modellbibliothek und ordne dort Models zu.', 'Todavía no hay ningún grupo propio. Créalo en la biblioteca de modelos y asigna modelos allí.', 'Ainda não há nenhum grupo próprio. Crie um na biblioteca de modelos e atribua modelos a ele.')), selected.size > 1 ? $('div', { class: 'rgh-group-mode' }, [ $('button', { class: viewState.homeGroupMode === 'any' ? 'is-active' : '', type: 'button', onclick: () => setHomeGroupFilter(viewState.homeGroups, 'any') }, tx('任意组', 'In any group', 'In einer Gruppe', 'En cualquier grupo', 'Em qualquer grupo')), $('button', { class: viewState.homeGroupMode === 'all' ? 'is-active' : '', type: 'button', onclick: () => setHomeGroupFilter(viewState.homeGroups, 'all') }, tx('所有组', 'In all groups', 'In allen Gruppen', 'En todos los grupos', 'Em todos os grupos')), ]) : null, $('div', { class: 'rgh-group-popover-status' }), $('div', { class: 'rgh-group-popover-footer' }, [ $('button', { type: 'button', onclick: () => { closeHeaderGroupMenu(); open('library'); } }, groups.length ? tx('管理群组', 'Manage groups', 'Gruppen verwalten', 'Administrar grupos', 'Gerenciar grupos') : tx('创建群组', 'Create group', 'Gruppe erstellen', 'Crear grupo', 'Criar grupo')), ]), ].filter(Boolean)); updateHomeFilterStatus(); positionHeaderGroupMenu(); } function openHeaderGroupMenu(button = groupMenuButton) { closeGlobalMenu(false); if (lightweight || !activeStore) { location.href = new URL('/?roomgrid_groups=1', location.origin).toString(); return; } if (groupMenuPanel?.isConnected && groupMenuButton === button) { closeHeaderGroupMenu(); return; } closeHeaderGroupMenu(); groupMenuButton = button; groupMenuReturnFocus = document.activeElement; groupMenuPanel = $('section', { class: 'rgh-group-popover', dataset: { roomgridHeaderGroupMenu: '1' }, role: 'dialog', ariaLabel: tx('主页自定义群组', 'Custom groups for the homepage', 'Eigene Gruppen für die Startseite', 'Grupos propios para la página de inicio', 'Grupos próprios para a página inicial'), lang: LANG, tabIndex: '-1' }); document.body.appendChild(groupMenuPanel); renderHeaderGroupMenu(); groupMenuOutsideHandler = event => { if (!groupMenuPanel?.contains(event.target) && !groupMenuButton?.contains(event.target)) closeHeaderGroupMenu(); }; setTimeout(() => groupMenuOutsideHandler && document.addEventListener('pointerdown', groupMenuOutsideHandler, true), 0); syncHeaderButtons(); requestAnimationFrame(() => groupMenuPanel?.querySelector('button,input')?.focus()); } function closeGlobalMenu(restoreFocus = true) { if (globalMenuOutsideHandler) document.removeEventListener('pointerdown', globalMenuOutsideHandler, true); globalMenuOutsideHandler = null; globalMenuPanel?.remove(); globalMenuPanel = null; const returnTarget = globalMenuReturnFocus; globalMenuReturnFocus = null; syncHeaderButtons(); if (restoreFocus && returnTarget?.isConnected) requestAnimationFrame(() => returnTarget.focus?.()); } function positionGlobalMenu() { if (!globalMenuPanel?.isConnected) return; const rect = globalMenuButton?.getBoundingClientRect?.(); const width = Math.min(520, Math.max(300, window.innerWidth - 16)); let left = rect ? rect.left : window.innerWidth - width - 18; let top = rect ? rect.bottom : 64; left = Math.max(8, Math.min(window.innerWidth - width - 8, left)); top = Math.max(8, Math.min(window.innerHeight - 120, top)); Object.assign(globalMenuPanel.style, { left: `${Math.round(left)}px`, top: `${Math.round(top)}px`, maxHeight: `${Math.max(110, Math.round(window.innerHeight - top - 8))}px` }); } function suiteUrl(view) { const url = new URL('/', location.origin); if (view === 'workstation') url.searchParams.set('multicam_mode', '1'); else { url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', view); } return url.toString(); } function menuItem(spec) { return $('button', { class: `rgh-global-item${spec.active ? ' is-active' : ''}`, type: 'button', dataset: { roomgridGlobalTarget: spec.id }, onclick: () => { closeGlobalMenu(false); spec.run(); }, }, [$('b', {}, spec.icon), $('span', {}, [$('span', {}, spec.label), $('small', {}, spec.detail || '')])]); } function openGlobalMenu(button = null) { if (globalMenuPanel?.isConnected && (!button || globalMenuButton === button)) { closeGlobalMenu(); return; } closeGlobalMenu(false); closeHeaderGroupMenu(); globalMenuButton = button || document.querySelector('.rgh-header-brand'); globalMenuReturnFocus = document.activeElement; const goHub = view => { location.href = suiteUrl(view); }; const goControl = tab => { if (!lightweight && activeStore && activeAnalytics) { close(); openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, tab); } else location.href = new URL(`/?roomgrid_control=${encodeURIComponent(tab)}`, location.origin).toString(); }; const groups = [ [tx('主要视图', 'Main views', 'Hauptansichten'), [ { id: 'workstation', icon: '▦', label: tx('多画面工作站', 'MultiCam workstation', 'MultiCam-Arbeitsbereich'), detail: tx('摄像头网格、音频和布局', 'Camera grid, audio and layouts', 'Kameraraster, Audio und Layouts'), run: () => openNoopener(suiteUrl('workstation')) }, { id: 'discover', icon: '⌁', label: tx('发现', 'Discover', 'Entdecken'), detail: tx('完整目录、过滤器和排序', 'Full catalog, filters and sorting', 'Vollständiger Katalog, Filter und Sortierung'), run: () => openNoopener(suiteUrl('discover')) }, { id: 'dashboard', icon: '⌂', label: tx('个人首页', 'Personal dashboard', 'Persönliche Startseite'), detail: tx('收藏、动态和快捷入口', 'Favorites, activity and shortcuts', 'Favoriten, Aktivität und Schnellzugriffe'), run: () => openNoopener(suiteUrl('dashboard')) }, { id: 'tags', icon: '#', label: tx('标签浏览器', 'Tag explorer', 'Tag-Explorer'), detail: tx('组合、趋势和相关标签', 'Combinations, trends and related tags', 'Kombinationen, Trends und verwandte Tags'), run: () => openNoopener(suiteUrl('tags')) }, ]], [tx('资料和统计', 'Library and statistics', 'Bibliothek und Statistik'), [ { id: 'stats', icon: '▥', label: tx('统计中心', 'Statistics Center', 'Statistikzentrum'), detail: tx('模型比较和本地统计', 'Model comparison and local statistics', 'Modelvergleich und lokale Statistik'), active: !!root && viewState.view === 'stats', run: () => goHub('stats') }, { id: 'library', icon: '★', label: tx('模型库', 'Model library', 'Modellbibliothek'), detail: tx('资料、标签、群组和备注', 'Profiles, tags, groups and notes', 'Profile, Tags, Gruppen und Notizen'), active: !!root && viewState.view === 'library', run: () => goHub('library') }, { id: 'following', icon: '♥', label: (I18N[LANG]?.followingUi || I18N.en.followingUi).nav, detail: (I18N[LANG]?.followingUi || I18N.en.followingUi).description, active: !!root && viewState.view === 'following', run: () => goHub('following') }, { id: 'user', icon: '◎', label: tx('我的用户', 'My user', 'Mein Nutzer'), detail: tx('偏好、使用情况和账户数据', 'Preferences, usage and account data', 'Vorlieben, Nutzung und Kontodaten'), active: !!root && viewState.view === 'user', run: () => goHub('user') }, { id: 'analytics', icon: '▤', label: tx('分析工作室', 'Analytics Studio', 'Analytics Studio'), detail: tx('时间范围、热图和导出', 'Ranges, heatmaps and exports', 'Zeiträume, Heatmaps und Exporte'), run: () => goControl('analytics') }, ]], [tx('设置和维护', 'Settings and maintenance', 'Einstellungen und Wartung'), [ { id: 'settings', icon: '⚙', label: tx('设置', 'Settings', 'Einstellungen'), detail: tx('所有选项、数据和维护', 'All options, data and maintenance', 'Alle Optionen, Daten und Wartung'), run: () => goControl('homepage') }, ]], ]; globalMenuPanel = $('section', { class: 'rgh-global-menu', dataset: { roomgridGlobalMenu: '1' }, role: 'dialog', ariaLabel: tx('RoomGrid 主菜单', 'RoomGrid main menu', 'RoomGrid-Hauptmenü'), lang: LANG, tabIndex: '-1' }, [ $('div', { class: 'rgh-global-head' }, [ $('div', {}, [$('strong', {}, [$('span', { dataset: { roomgridBrandName: '1' } }, roomGridBrandIdentity(activeStore?.state?.settings).name), ' ', $('span', { dataset: { roomgridBrandVersion: '1' } }, `v${META.version}`)]), $('small', {}, tx('所有页面、设置和工具位于一个位置', 'All pages, settings and tools in one place', 'Alle Seiten, Einstellungen und Werkzeuge an einem Ort'))]), $('button', { type: 'button', ariaLabel: tx('关闭', 'Close', 'Schließen'), onclick: () => closeGlobalMenu() }, '×'), ]), ...groups.map(([title, items]) => $('section', { class: 'rgh-global-section' }, [$('h3', {}, title), $('div', { class: 'rgh-global-grid' }, items.map(menuItem))])), $('div', { class: 'rgh-global-foot' }, tx('菜单会保持在可见窗口内;所有 RoomGrid 数据仍保存在本地。', 'The menu stays inside the visible window; all RoomGrid data remains local.', 'Das Menü bleibt im sichtbaren Fenster; alle RoomGrid-Daten bleiben lokal.')), ]); document.body.appendChild(globalMenuPanel); positionGlobalMenu(); globalMenuOutsideHandler = event => { if (!globalMenuPanel?.contains(event.target) && !globalMenuButton?.contains?.(event.target)) closeGlobalMenu(); }; setTimeout(() => globalMenuOutsideHandler && document.addEventListener('pointerdown', globalMenuOutsideHandler, true), 0); syncHeaderButtons(); requestAnimationFrame(() => globalMenuPanel?.querySelector('.rgh-global-item')?.focus()); } function createHeaderMenu() { const menu = $('div', { class: 'rgh-header-menu', dataset: { roomgridHeaderMenu: '1' }, role: 'navigation', ariaLabel: tx('RoomGrid 页面', 'RoomGrid pages', 'RoomGrid-Seiten'), lang: LANG }); const specs = [ ['stats', '▥', tx('统计', 'Statistics', 'Statistiken')], ['library', '★', tx('模型库', 'Model library', 'Model-Bibliothek')], ['following', '♥', (I18N[LANG]?.followingUi || I18N.en.followingUi).nav], ['user', '◎', tx('我的用户', 'My user', 'Mein Nutzer')], ]; const go = view => { location.href = suiteUrl(view); }; const identity = roomGridBrandIdentity(activeStore?.state?.settings); menu.appendChild($('button', { class: 'rgh-header-brand', type: 'button', dataset: { roomgridGlobalMenuButton: '1' }, onclick: event => openGlobalMenu(event.currentTarget), title: `${tx('RoomGrid 主菜单', 'RoomGrid main menu', 'RoomGrid-Hauptmenü')} · v${META.version}` }, [roomGridBrandMark(activeStore?.state?.settings, { size: 22 }), ' ', $('b', { dataset: { roomgridBrandVersion: '1' } }, identity.showVersion ? `v${META.version}` : '')])); specs.forEach(([id, icon, label]) => menu.appendChild($('button', { type: 'button', dataset: { roomgridHeaderView: id }, onclick: () => go(id), title: label }, [$('b', {}, icon), $('span', {}, label)]))); menu.appendChild($('button', { type: 'button', dataset: { roomgridHeaderHomepage: '1' }, onclick: () => { if (!lightweight && activeStore && activeAnalytics) openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'homepage'); else location.href = new URL('/?roomgrid_home_options=1', location.origin).toString(); }, title: tx('自定义原始首页', 'Customize original homepage', 'Originale Startseite anpassen') }, [$('b', {}, '▦'), $('span', {}, tx('首页', 'Homepage', 'Startseite'))])); groupMenuButton = $('button', { type: 'button', dataset: { roomgridHeaderGroups: '1' }, onclick: event => openHeaderGroupMenu(event.currentTarget), title: tx('按自己的群组筛选主页', 'Filter homepage by your own groups', 'Startseite nach eigenen Gruppen filtern', 'Filtrar la página de inicio por tus propios grupos', 'Filtrar a página inicial por seus próprios grupos') }, [$('span', {}, tx('群组', 'Groups', 'Gruppen', 'Grupos', 'Grupos')), $('b', { class: 'rgh-header-group-count' })]); menu.appendChild(groupMenuButton); menu.appendChild(createLanguageSwitcher('rgh-language-select')); return menu; } function ensureHeaderMenu() { if (headerMenu?.isConnected) return; const target = findHeaderHost(); if (!target) return; closeHeaderGroupMenu(); headerHost?.removeAttribute?.('data-roomgrid-header-host'); headerHost = target.closest('header,[role="banner"],#header,nav') || target; headerHost.dataset.roomgridHeaderHost = '1'; headerMenu = createHeaderMenu(); target.appendChild(headerMenu); syncHeaderButtons(); syncTop(); } function syncHeaderButtons() { document.querySelectorAll('[data-roomgrid-global-menu-button]').forEach(button => button.classList.toggle('is-active', !!globalMenuPanel?.isConnected && button === globalMenuButton)); document.querySelectorAll('[data-roomgrid-header-view]').forEach(button => button.classList.toggle('is-active', !!root && button.dataset.roomgridHeaderView === viewState.view)); document.querySelectorAll('[data-roomgrid-header-groups]').forEach(button => { button.classList.toggle('is-active', !!groupMenuPanel?.isConnected && button === groupMenuButton); button.classList.toggle('has-filter', viewState.homeGroups.length > 0); const count = button.querySelector('.rgh-header-group-count'); if (count) { count.textContent = String(viewState.homeGroups.length); count.hidden = viewState.homeGroups.length === 0; } }); } function syncTop() { if (!root) return; const rect = headerHost?.getBoundingClientRect?.(); const top = rect && rect.bottom > 0 && rect.top < 160 ? Math.min(180, Math.max(0, Math.round(rect.bottom))) : 0; root.style.setProperty('--rgh-top', `${top}px`); } function syncPrivateCacheState() { const next = activeAnalytics?.isPrivateSession?.() === true; if (next !== cachedPrivateSession) { cachedPrivateSession = next; followingCacheChecked = ''; if (next && followingState.cached) followingState = { status: 'idle', rows: [], pages: 0, partial: false, error: '', recognized: false }; mediaCacheGeneration += 1; modelCoverCache.clear(); hubPageCache.clear(); } } function invalidate(_state, path) { syncPrivateCacheState(); // Layout, appearance and filter preferences do not change historical // model/event data. Store supplies a precise settings path for these. if (typeof path === 'string' && (path === 'settings' || path.startsWith('settings:'))) { if (cache) cache.storeRevision = activeStore.revision; if (root) scheduleRender(); return; } cacheGeneration += 1; cacheDirty = true; // Keep an already usable snapshot on screen. Discarding both the cache // and an in-flight promise here used to start another 30k+ model index // build on every embedded page switch or store update. The running build // now finishes exactly once; a newer generation is refreshed quietly. if (cache) scheduleCacheRefresh(); if (root) scheduleRender(); } function invalidateQuietly() { syncPrivateCacheState(); cacheGeneration += 1; cacheDirty = true; // Summaries are keyed by their immutable index object. A dirty index is // still a valid last-known snapshot; a replacement gets a new memo key. if (cache) scheduleCacheRefresh(); } // Building the combined library/statistics index can touch tens of // thousands of models and up to 100k events. Yield regularly so the // native header, close button and page links stay responsive while the // richer views finish preparing in the background. const yieldToUi = () => globalThis.scheduler?.yield ? globalThis.scheduler.yield() : new Promise(resolve => setTimeout(resolve, 0)); async function forEachProgressively(list, visit, budgetMs = 10) { let sliceStartedAt = performance.now(); for (let index = 0; index < list.length; index += 1) { visit(list[index], index); if (index && index % 16 === 0 && performance.now() - sliceStartedAt >= budgetMs) { await yieldToUi(); sliceStartedAt = performance.now(); } } } async function filterHubItems(items, predicate, revision) { const filtered = []; let sliceStartedAt = performance.now(); for (let index = 0; index < items.length; index += 1) { if (revision !== renderRevision || !root?.isConnected) return null; if (predicate(items[index])) filtered.push(items[index]); if (index && index % 128 === 0 && performance.now() - sliceStartedAt >= 8) { await yieldToUi(); if (revision !== renderRevision || !root?.isConnected) return null; sliceStartedAt = performance.now(); } } return filtered; } async function sortHubItemsProgressively(items, compare, revision) { // A background index is not tied to a visible render. It must finish // once even when the user switches pages; publication is guarded by // loadData's generation tracking. Visible sorts still cancel promptly. const cancelled = () => revision != null && (revision !== renderRevision || !root?.isConnected); if (items.length < 2048) return items.sort(compare); // Index and filtered views often contain a few already sorted runs. // Merge those directly; a highly disordered list uses the normal sort. const boundaries = [0]; let checkStartedAt = performance.now(); for (let index = 1; index < items.length; index += 1) { if (compare(items[index - 1], items[index]) > 0) { boundaries.push(index); if (boundaries.length > 64) break; } if ((index & 255) === 0 && performance.now() - checkStartedAt >= 8) { await yieldToUi(); if (cancelled()) return null; checkStartedAt = performance.now(); } } if (cancelled()) return null; if (boundaries.length === 1) return items; let source = items.slice(); let target = new Array(source.length); let operations = 0; let sliceStartedAt = performance.now(); if (boundaries.length <= 64) { boundaries.push(source.length); let runs = boundaries; while (runs.length > 2) { const nextRuns = [0]; for (let run = 0; run < runs.length - 1; run += 2) { const start = runs[run], middle = runs[run + 1], end = runs[Math.min(run + 2, runs.length - 1)]; let left = start, right = middle, output = start; while (left < middle || right < end) { if (right >= end || (left < middle && compare(source[left], source[right]) <= 0)) target[output++] = source[left++]; else target[output++] = source[right++]; operations += 1; if ((operations & 255) === 0 && performance.now() - sliceStartedAt >= 8) { await yieldToUi(); if (cancelled()) return null; sliceStartedAt = performance.now(); } } nextRuns.push(end); } [source, target] = [target, source]; runs = nextRuns; if (cancelled()) return null; } return source; } for (let width = 1; width < source.length; width *= 2) { for (let start = 0; start < source.length; start += width * 2) { const middle = Math.min(start + width, source.length); const end = Math.min(start + width * 2, source.length); let left = start; let right = middle; let output = start; while (left < middle && right < end) { if (compare(source[left], source[right]) <= 0) target[output++] = source[left++]; else target[output++] = source[right++]; operations += 1; if ((operations & 255) === 0 && performance.now() - sliceStartedAt >= 8) { await yieldToUi(); if (cancelled()) return null; sliceStartedAt = performance.now(); } } while (left < middle) target[output++] = source[left++]; while (right < end) target[output++] = source[right++]; } [source, target] = [target, source]; if (cancelled()) return null; } return source; } async function buildDataIndex(options = {}) { // The shell opens immediately, but every rich data view must consume // the same fully hydrated analytics snapshot. Without this barrier, // rapid navigation could cache a partial model list while IndexedDB // was still streaming in the remaining account history. // The first paint deliberately skips that barrier. Analytics hydrates // incrementally, so the synchronous maps already contain a useful // partial snapshot. Waiting for all old events before painting made // Stats/Library/User look frozen for large local histories. The full // refresh still uses the barrier in the idle background path. if (options.waitForReady !== false) await activeAnalytics.whenReady?.(); const historyReady = activeAnalytics.isReady?.() !== false; const analyticsRevision = activeAnalytics.getDataRevision?.(); const storeRevision = activeStore.revision; const [models, events, quality, accountSnapshots] = await Promise.all([ Promise.resolve(activeAnalytics.getModelsSync()), Promise.resolve(activeAnalytics.getEventsSync({ limit: 100000 })), Promise.resolve(activeAnalytics.getQualitySamplesSync({ limit: 20000 })), Promise.resolve(activeAnalytics.getAccountSnapshotsSync({ limit: 20000 })), ]); const rooms = (activeStore.state.rooms || []).slice(); const modelMap = new Map(models.map(model => [model.id, model])); const roomMap = new Map(rooms.map(room => [room.id, room])); const eventMap = new Map(); const qualityMap = new Map(); await forEachProgressively(events, event => { if (!event.id) return; if (!eventMap.has(event.id)) eventMap.set(event.id, []); eventMap.get(event.id).push(event); }); await forEachProgressively(quality, sample => { if (!sample.id) return; if (!qualityMap.has(sample.id)) qualityMap.set(sample.id, []); qualityMap.get(sample.id).push(sample); }); const ids = new Set([...modelMap.keys(), ...roomMap.keys(), ...eventMap.keys()]); const totalWatchMs = [...ids].reduce((sum, id) => sum + Math.max(0, Number(modelMap.get(id)?.totalMs) || 0), 0); let items = []; const indexBuiltAt = Date.now(); await forEachProgressively([...ids], id => { if (!isLikelyUsername(id)) return; const model = modelMap.get(id) || { id, totalMs: 0, sessions: 0, hours: [], days: {}, tags: [] }; const room = roomMap.get(id) || null; const profile = activeAnalytics.getProfileSync(id) || null; const modelEvents = eventMap.get(id) || []; const samples = qualityMap.get(id) || []; const counts = {}; let lastEventAt = 0; for (const event of modelEvents) { counts[event.type] = (counts[event.type] || 0) + 1; lastEventAt = Math.max(lastEventAt, Number(event.at) || 0); } let startupTotal = 0; let stalls = 0; let dropped = 0; let frames = 0; for (const sample of samples) { startupTotal += Number(sample.startupMs) || 0; stalls += Number(sample.stalls) || 0; dropped += Number(sample.droppedFrames) || 0; frames += Number(sample.totalFrames) || 0; } const avgStartup = samples.length ? startupTotal / samples.length : 0; let peakIndex = 0; let peakValue = 0; const observedHours = Array.isArray(model.hours) ? model.hours : []; const observedHourCount = Math.min(168, observedHours.length); for (let index = 0; index < observedHourCount; index += 1) { const value = Math.max(0, Number(observedHours[index]) || 0); if (value > peakValue) { peakValue = value; peakIndex = index; } } const tags = [...new Set([...(room?.localTags || []), ...(model.tags || []), ...(profile?.tags || [])])].slice(0, 80); const status = freshObservedStatus(room, model, profile, indexBuiltAt); const searchText = `${id} ${profile?.displayName || ''} ${profile?.bio || ''} ${tags.join(' ')} ${room?.notes || ''}`.toLocaleLowerCase(); items.push({ id, model, room, profile, events: modelEvents, counts, samples, tags, status, searchText, totalMs: Math.max(0, Number(model.totalMs) || 0), sessions: Math.max(0, Number(model.sessions) || 0), activeDays: Object.values(model.days || {}).filter(value => Number(value) > 0).length, avgSessionMs: model.sessions ? model.totalMs / model.sessions : 0, share: totalWatchMs ? model.totalMs / totalWatchMs : 0, avgStartup, stalls, dropRate: frames ? dropped / frames : 0, peakIndex, lastActivity: Math.max(Number(model.lastObservedAt) || 0, Number(model.lastWatchedAt) || 0, Number(model.lastOnlineAt) || 0, Number(room?.lastSeenOnline) || 0, lastEventAt), }); }, 10); await yieldToUi(); items = await sortHubItemsProgressively(items, (a, b) => b.totalMs - a.totalMs || b.lastActivity - a.lastActivity || a.id.localeCompare(b.id), null); items.forEach((item, index) => { item.rank = index + 1; }); const recentCutoff = Date.now() - 30 * 86400000; const summary = { watched: items.filter(item => item.totalMs > 0), sessions: items.reduce((sum, item) => sum + item.sessions, 0), previews: events.reduce((sum, event) => sum + (event.type === 'preview' ? 1 : 0), 0), recentEvents: events.reduce((sum, event) => sum + (Number(event.at) >= recentCutoff ? 1 : 0), 0), }; summary.top = summary.watched[0] || null; return { models, events, quality, accountSnapshots, rooms, items, totalWatchMs, summary, usage: activeAnalytics.getUsageSummary(), analyticsRevision, storeRevision, historyReady }; } // Discover keeps the complete public catalog in its own IndexedDB cache. // Recover it into the stable analytics store after an update so the native // library does not suddenly shrink to only the models watched locally. async function restorePersistedCatalog() { if (catalogRestorePromise || !activeAnalytics || typeof activeAnalytics.observeModelsBatch !== 'function') return catalogRestorePromise; const bridge = globalThis.RoomGridCatalogBridge; if (!bridge?.loadFullModels) return null; catalogRestorePromise = (async () => { catalogRestoreStatus = 'pending'; try { await activeAnalytics.whenReady?.(); const snapshot = await bridge.loadFullModels(); const rooms = Array.isArray(snapshot?.rooms) ? snapshot.rooms : []; if (!rooms.length) { catalogRestoreStatus = 'empty'; return 0; } const observedAt = Math.max(0, Number(snapshot.fetchedAt || snapshot.savedAt) || Date.now()); const payload = rooms.map(room => ({ id: room.id, currentShow: room.currentShow, viewerCount: room.viewers, tags: room.tags || [], subject: room.subject || '', country: room.country || '', location: room.location || '', gender: room.gender || 'unknown', age: room.age || 0, followers: room.followers || 0, secondsOnline: room.secondsOnline || 0, isHd: !!room.isHd, isNew: !!room.isNew, isGaming: !!room.isGaming, source: 'roomgrid-catalog-cache', observedAt, })); const changed = await activeAnalytics.observeModelsBatch(payload); await activeAnalytics.flush?.(); catalogRestoreStatus = changed > 0 ? 'done' : 'already'; return changed; } catch (_) { catalogRestoreStatus = 'error'; return 0; } finally { catalogRestorePromise = null; } })(); return catalogRestorePromise; } function schedulePersistedCatalogRestore() { if (catalogRestorePromise || !activeAnalytics || !globalThis.RoomGridCatalogBridge?.loadFullModels) return; const run = () => restorePersistedCatalog().then(changed => { if (changed > 0) { invalidateQuietly(); if (root) scheduleRender(); } }).catch(() => {}); if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 5000 }); else setTimeout(run, 1200); } async function loadData(options = {}) { // A cached snapshot remains useful while a newer, complete snapshot is // built. Never make a page switch wait for that background refresh. if (cache) { if (cache.analyticsRevision !== activeAnalytics.getDataRevision?.() || cache.storeRevision !== activeStore.revision) { cacheDirty = true; scheduleCacheRefresh(); } if (cacheDirty) scheduleCacheRefresh(); return cache; } if (cachePromise) return cachePromise; const generation = cacheGeneration; const allowPartial = options.allowPartial === true; const pending = buildDataIndex({ waitForReady: !allowPartial }).then(value => { cache = value; // Partial data is intentionally marked dirty so the idle refresh can // replace it with the fully hydrated index once IndexedDB is ready. cacheDirty = (allowPartial && !value.historyReady) || generation !== cacheGeneration; if (cachePromise === pending) cachePromise = null; if (cacheDirty) scheduleCacheRefresh(); // Keep the first paint fast; recovery runs in an idle slice and then // refreshes the visible count/cards without navigating away. schedulePersistedCatalogRestore(); return cache; }).catch(error => { if (cachePromise === pending) cachePromise = null; throw error; }); cachePromise = pending; return cachePromise; } function requestIdleWork(run, timeout = 2500) { if ('requestIdleCallback' in window) return requestIdleCallback(run, { timeout }); return setTimeout(() => run({ didTimeout: true, timeRemaining: () => 0 }), Math.min(timeout, 500)); } function cancelIdleWork(handle) { if (!handle) return; if ('cancelIdleCallback' in window) cancelIdleCallback(handle); else clearTimeout(handle); } function scheduleCacheWarmup() { if (watchOnlyMode || cache || cachePromise || cacheWarmupHandle || !activeAnalytics) return; cacheWarmupHandle = requestIdleWork(() => { cacheWarmupHandle = 0; // Foreground navigation shares this promise. Never park it behind // complete IndexedDB hydration; the full refresh has its own promise. if (!watchOnlyMode && activeAnalytics && !cache && !cachePromise) loadData({ allowPartial: true }).catch(() => {}); }, 3500); } function scheduleCacheRefresh() { if (!cacheDirty || !cache || !root?.isConnected || cacheRefreshPromise || cacheRefreshHandle || cacheRefreshTimer || !activeAnalytics) return; cacheRefreshTimer = setTimeout(() => { cacheRefreshTimer = 0; cacheRefreshHandle = requestIdleWork(() => { cacheRefreshHandle = 0; if (!cacheDirty || !root?.isConnected || !activeAnalytics || cacheRefreshPromise) return; const generation = cacheGeneration; cacheRefreshPromise = buildDataIndex().then(value => { cache = value; // Old handlers refer to their data snapshot. Drop completed pages // when replacing the index rather than retaining several histories. hubPageCache.clear(); cacheDirty = generation !== cacheGeneration; cacheRefreshPromise = null; if (root) scheduleRender(); if (cacheDirty) scheduleCacheRefresh(); }).catch(() => { cacheRefreshPromise = null; }); }, 5000); }, 1200); } function queueMediaHydration(node, task) { mediaHydrationQueue.push({ node, task }); if (mediaHydrationHandle || mediaHydrationTimer) return; const drain = deadline => { mediaHydrationHandle = 0; let launched = 0; while (mediaHydrationQueue.length && launched < 4 && (deadline.didTimeout || deadline.timeRemaining() > 3)) { const entry = mediaHydrationQueue.shift(); if (entry.node?.isConnected) Promise.resolve().then(entry.task).catch(() => {}); launched += 1; } if (mediaHydrationQueue.length) mediaHydrationHandle = requestIdleWork(drain, 2200); }; mediaHydrationTimer = setTimeout(() => { mediaHydrationTimer = 0; mediaHydrationHandle = requestIdleWork(drain, 2200); }, 500); } function spark(values, limit = 14) { const list = (values || []).slice(-limit); const max = Math.max(1, ...list.map(value => Number(value) || 0)); return $('div', { class: 'rgh-spark', ariaLabel: tx('活动趋势', 'Activity trend', 'Aktivitätstrend') }, list.map(value => $('i', { style: { height: `${Math.max(6, Math.round((Number(value) || 0) / max * 100))}%` } }))); } function bindLiveHover(media, id) { if (!matchMedia('(hover:hover) and (pointer:fine)').matches || activeStore.state.settings.normalHoverLivePreview === false) return; const delay = clampInt(activeStore.state.settings.normalHoverLivePreviewDelay, 0, 1000, 250); media.addEventListener('pointerenter', () => { clearTimeout(hoverTimer); hoverTimer = setTimeout(() => { if (media.matches(':hover')) RoomGridUltimate.startInlineHoverPreview(id, media); }, delay); }); media.addEventListener('pointerleave', () => { clearTimeout(hoverTimer); RoomGridUltimate.stopInlineHoverPreview(media); }); } function publicProfileExtras(item) { const profile = item.profile; const sections = (profile?.sections || []).slice(0, 8); const links = (profile?.links || []).filter(link => { try { const url = new URL(link); return ['http:', 'https:'].includes(url.protocol) && url.hostname !== location.hostname; } catch (_) { return false; } }).slice(0, 16); if (!sections.length && !links.length) return null; const serviceName = raw => { try { const host = new URL(raw).hostname.replace(/^www\./iu, '').toLocaleLowerCase(); if (host === 'x.com' || host.includes('twitter.com')) return 'X / Twitter'; if (host.includes('instagram.com')) return 'Instagram'; if (host.includes('tiktok.com')) return 'TikTok'; if (host.includes('youtube.com') || host === 'youtu.be') return 'YouTube'; if (host.includes('onlyfans.com')) return 'OnlyFans'; if (host.includes('snapchat.com')) return 'Snapchat'; return host; } catch (_) { return tx('链接', 'Link', 'Link'); } }; return $('details', { class: 'rgh-public-profile' }, [ $('summary', {}, tx('公开简介和链接', 'Public profile text and links', 'Öffentliche Profiltexte und Links')), $('div', { class: 'rgh-public-profile-body' }, [ ...sections.map(section => $('section', {}, [$('strong', {}, section.label), $('p', {}, section.text)])), links.length ? $('div', { class: 'rgh-profile-links' }, links.map(link => $('a', { href: link, target: '_blank', rel: 'noopener noreferrer', title: link }, serviceName(link)))) : null, ]), ]); } function modelMedia(item) { const mediaGeneration = mediaCacheGeneration; const saved = [...new Set((item.model.previewUrls || []).filter(Boolean))].reverse(); const candidates = [...new Set([...saved, thumbUrl(item.id), smallThumbUrl(item.id), generatedCoverUrl(item.id)])]; let candidateIndex = 0; const img = $('img', { src: generatedCoverUrl(item.id), alt: tx(`${item.id} 预览`, `${item.id} preview`, `${item.id} Vorschau`, `Vista previa de ${item.id}`, `Prévia de ${item.id}`), loading: 'lazy', referrerPolicy: 'no-referrer', onerror: event => { if (event.currentTarget.dataset.coverFallback === 'generated-local') return; candidateIndex += 1; if (candidates[candidateIndex]) event.currentTarget.src = candidates[candidateIndex]; else { // The final candidate is always the local generated cover. Keep a // visible image even when all public thumbnail requests fail. event.currentTarget.dataset.coverFallback = 'generated-local'; media.dataset.roomgridMediaSource = 'generated-local-cover'; if (sourceLabel) sourceLabel.textContent = tx('Lokales Camclient-Cover', 'Local Camclient cover', 'Lokales Camclient-Cover'); event.currentTarget.classList.remove('is-error'); event.currentTarget.src = generatedCoverUrl(item.id); } }, }); const avatar = $('img', { class: 'rgh-avatar', src: generatedCoverUrl(item.id), alt: '', loading: 'lazy', referrerPolicy: 'no-referrer', onerror: event => { if (!event.currentTarget.dataset.fallback) { event.currentTarget.dataset.fallback = '1'; event.currentTarget.src = thumbUrl(item.id); } else event.currentTarget.classList.add('is-error'); } }); const media = $('div', { class: 'rgh-media', dataset: { initial: item.id.slice(0, 1).toLocaleUpperCase(), roomgridMediaSource: saved.length ? 'saved-public-preview' : 'current-public-thumbnail' } }, [ img, avatar, $('span', { class: `rgh-status ${item.status === 'online' || item.status === 'public' ? 'online' : ''}` }, statusLabel(item.status)), item.rank ? $('span', { class: 'rgh-rank' }, `#${item.rank}`) : null, $('span', { class: 'rgh-media-source' }, saved.length ? tx('最后已知的公开预览', 'Last known public preview', 'Letzte bekannte öffentliche Vorschau') : (libraryStatusBucket(item) === 'live' ? tx('当前公开预览', 'Current public preview', 'Aktuelle öffentliche Vorschau') : tx('离线时可能只有占位图', 'Offline may only provide a placeholder', 'Offline eventuell nur Platzhalter'))), ]); media.style.backgroundImage = `url("${generatedCoverUrl(item.id)}")`; media.style.backgroundSize = 'cover'; bindLiveHover(media, item.id); const sourceLabel = media.querySelector('.rgh-media-source'); let coverRevision = 0; let currentCoverKey = ''; let publicThumbnailStarted = false; const usePublicThumbnail = () => { if (publicThumbnailStarted || currentCoverKey || !media.isConnected || mediaGeneration !== mediaCacheGeneration) return; publicThumbnailStarted = true; candidateIndex = 0; img.src = candidates[0]; avatar.src = smallThumbUrl(item.id); }; const applyArchivedCover = records => { records = Array.isArray(records) ? records.filter(record => record.blob?.type?.startsWith('image/')) : []; if (mediaGeneration !== mediaCacheGeneration || cachedPrivateSession || !media.isConnected || !Array.isArray(records) || !records.length) return; const record = records.find(entry => entry.isCustomCover) || records.find(entry => entry.kind === 'snapshot') || records[0]; const key = `${record.mediaId}:${record.coverUpdatedAt || 0}:${record.capturedAt || 0}`; if (key === currentCoverKey) return; const objectUrl = RoomGridModelMedia.objectUrl(record); if (!objectUrl) return; const revision = ++coverRevision; const preload = new Image(); preload.onerror = () => { URL.revokeObjectURL(objectUrl); usePublicThumbnail(); }; preload.onload = () => { if (revision !== coverRevision || mediaGeneration !== mediaCacheGeneration || cachedPrivateSession || !media.isConnected) { URL.revokeObjectURL(objectUrl); return; } currentCoverKey = key; img.dataset.coverFallback = 'generated-local'; // The probe is eager; keep the card image eager too so off-screen // removals do not leave a lazy blob URL alive indefinitely. img.loading = 'eager'; img.addEventListener('load', () => setTimeout(() => URL.revokeObjectURL(objectUrl), 0), { once: true }); img.src = objectUrl; modelCoverCache.delete(item.id); modelCoverCache.set(item.id, record); let bytes = [...modelCoverCache.values()].reduce((sum, entry) => sum + (entry.blob?.size || 0), 0); while (modelCoverCache.size > 96 || bytes > 32 * 1024 * 1024) { const oldest = modelCoverCache.keys().next().value; bytes -= modelCoverCache.get(oldest)?.blob?.size || 0; modelCoverCache.delete(oldest); } media.dataset.roomgridMediaSource = record.isCustomCover ? 'local-custom-cover' : 'local-public-preview-archive'; sourceLabel?.classList.add('is-archived'); if (sourceLabel) sourceLabel.textContent = record.isCustomCover ? tx(`自己的封面 · ${dateTime(record.capturedAt)}`, `Custom cover · ${dateTime(record.capturedAt)}`, `Eigenes Cover · ${dateTime(record.capturedAt)}`) : tx( `本地保存的封面 · ${dateTime(record.capturedAt)}`, `Locally saved cover · ${dateTime(record.capturedAt)}`, `Lokal gespeichertes Cover · ${dateTime(record.capturedAt)}`, `Portada guardada localmente · ${dateTime(record.capturedAt)}`, `Capa salva localmente · ${dateTime(record.capturedAt)}`, ); }; preload.src = objectUrl; }; // Read the existing cover as soon as the card is attached. Only new // network captures/compression wait for idle time; a local cover must not. const archiveReady = Promise.resolve().then(async () => { if (cachedPrivateSession) { usePublicThumbnail(); return []; } if (mediaGeneration !== mediaCacheGeneration || !media.isConnected) return []; const existingCover = modelCoverCache.get(item.id); if (existingCover?.isCustomCover || (existingCover && Date.now() - Number(existingCover.capturedAt || 0) < 5 * 60 * 1000)) return [existingCover]; const archived = await RoomGridModelMedia.list(item.id); applyArchivedCover(archived); if (cachedPrivateSession || mediaGeneration !== mediaCacheGeneration) return []; if (!archived.some(record => record.blob?.type?.startsWith('image/'))) usePublicThumbnail(); return archived; }); queueMediaHydration(media, async () => { const archived = await archiveReady; if (cachedPrivateSession || mediaGeneration !== mediaCacheGeneration || !media.isConnected) return; applyArchivedCover(archived); if (archived.some(record => record.isCustomCover)) return; const captured = await RoomGridModelMedia.capturePack(item.id, { live: libraryStatusBucket(item) === 'live', urls: saved }); applyArchivedCover(captured); }); // Reuse a previously decoded cover on background re-renders; do not wait // for another IndexedDB/network capture cycle before displaying it. const cachedCover = modelCoverCache.get(item.id); if (cachedCover && !cachedPrivateSession) queueMicrotask(() => applyArchivedCover([cachedCover])); return media; } function modelExampleGallery(item) { const grid = $('div', { class: 'rgh-example-gallery-grid' }); const summary = $('summary', {}, tx('示例图片和封面', 'Example images and covers', 'Beispielbilder und Cover', 'Imágenes de ejemplo y portadas', 'Imagens de exemplo e capas')); const status = $('span', { class: 'rgh-example-gallery-status' }, tx('正在读取本地图片档案…', 'Reading local image archive…', 'Lokales Bildarchiv wird gelesen …', 'Leyendo el archivo de imágenes local…', 'Lendo o arquivo de imagens local…')); const saveButton = $('button', { class: 'rgh-example-gallery-save', type: 'button' }, tx('保存新快照', 'Save new snapshot', 'Neue Momentaufnahme speichern', 'Guardar nueva instantánea', 'Salvar novo instantâneo')); const details = $('details', { class: 'rgh-example-gallery', dataset: { roomgridExampleGallery: item.id } }, [ summary, $('div', { class: 'rgh-example-gallery-controls' }, [status, saveButton]), grid, ]); let loadRevision = 0; let openedOnce = false; const renderImages = async ({ capture = false, force = false } = {}) => { const revision = ++loadRevision; saveButton.disabled = capture; if (capture) status.textContent = tx('正在保存公开预览…', 'Saving public previews…', 'Öffentliche Vorschaubilder werden gespeichert …', 'Guardando vistas previas públicas…', 'Salvando prévias públicas…'); let archived = await RoomGridModelMedia.list(item.id); if (capture) archived = await RoomGridModelMedia.capturePack(item.id, { force, live: libraryStatusBucket(item) === 'live', urls: item.model.previewUrls || [] }); archived = archived.filter(record => record.blob?.type?.startsWith('image/')); if (revision !== loadRevision || !details.isConnected) return; saveButton.disabled = false; summary.textContent = tx( `示例图片和封面(已保存 ${archived.length} 张)`, `Example images and covers (${archived.length} saved)`, `Beispielbilder und Cover (${archived.length} gespeichert)`, `Imágenes y portadas (${archived.length} guardadas)`, `Imagens e capas (${archived.length} salvas)`, ); status.textContent = archived.length ? tx( `永久保存在此设备上 · 最近 ${dateTime(archived[0].capturedAt)}`, `Permanently stored on this device · latest ${dateTime(archived[0].capturedAt)}`, `Dauerhaft auf diesem Gerät · zuletzt ${dateTime(archived[0].capturedAt)}`, `Guardadas en este dispositivo · última ${dateTime(archived[0].capturedAt)}`, `Salvas neste dispositivo · última ${dateTime(archived[0].capturedAt)}`, ) : tx('尚未保存图片;当前公开预览仍会显示。', 'No saved image yet; current public previews are still shown.', 'Noch kein Bild gespeichert; aktuelle öffentliche Vorschauen werden trotzdem angezeigt.', 'Aún no hay imágenes guardadas; se muestran las vistas públicas actuales.', 'Ainda não há imagens salvas; as prévias públicas atuais continuam visíveis.'); const sources = []; const addSource = (url, label, archivedImage = false) => { if (url && !sources.some(entry => entry.url === url)) sources.push({ url, label, archivedImage }); }; archived.slice(0, 6).forEach((record, index) => addSource(RoomGridModelMedia.objectUrl(record), record.isCustomCover ? tx(`自己的封面 · ${dateTime(record.capturedAt)}`, `Custom cover · ${dateTime(record.capturedAt)}`, `Eigenes Cover · ${dateTime(record.capturedAt)}`) : record.userOwned ? tx(`自己的图片 · ${dateTime(record.capturedAt)}`, `Own image · ${dateTime(record.capturedAt)}`, `Eigenes Bild · ${dateTime(record.capturedAt)}`) : tx( `已保存的${record.kind === 'cover' ? '封面' : '快照'} ${archived.length - index} · ${dateTime(record.capturedAt)}`, `Saved ${record.kind === 'cover' ? 'cover' : 'snapshot'} ${archived.length - index} · ${dateTime(record.capturedAt)}`, `Gespeichertes ${record.kind === 'cover' ? 'Cover' : 'Beispielbild'} ${archived.length - index} · ${dateTime(record.capturedAt)}`, `${record.kind === 'cover' ? 'Portada' : 'Instantánea'} guardada · ${dateTime(record.capturedAt)}`, `${record.kind === 'cover' ? 'Capa' : 'Instantâneo'} salvo · ${dateTime(record.capturedAt)}`, ), true)); if (sources.length < 3) { addSource(thumbUrl(item.id), tx('当前公开封面', 'Current public cover', 'Aktuelles öffentliches Cover', 'Portada pública actual', 'Capa pública atual')); addSource(smallThumbUrl(item.id), tx('当前紧凑预览', 'Current compact preview', 'Aktuelle kompakte Vorschau', 'Vista compacta actual', 'Prévia compacta atual')); } if (libraryStatusBucket(item) === 'live') addSource(liveFrameUrl(item.id), tx('当前实时画面', 'Current live image', 'Aktuelles Livebild', 'Imagen en vivo actual', 'Imagem ao vivo atual')); grid.replaceChildren(...sources.slice(0, 6).map(({ url, label, archivedImage }) => { const image = $('img', { src: url, alt: `${item.id} – ${label || tx('预览', 'Preview', 'Vorschau', 'Vista previa', 'Prévia')}`, loading: 'lazy', referrerPolicy: 'no-referrer' }); if (archivedImage) image.addEventListener('load', () => setTimeout(() => URL.revokeObjectURL(url), 0), { once: true }); image.addEventListener('error', () => { if (!archivedImage && !image.dataset.fallback) { image.dataset.fallback = '1'; image.src = thumbUrl(item.id); } else image.classList.add('is-error'); }); return $('div', { class: `rgh-example-shot${archivedImage ? ' is-archived' : ''}` }, [image, $('span', {}, label)]); })); }; details.addEventListener('toggle', () => { if (!details.open) return; const firstOpen = !openedOnce; openedOnce = true; renderImages({ capture: firstOpen, force: firstOpen }).catch(() => {}); }); saveButton.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); renderImages({ capture: true, force: true }).catch(() => {}); }); return details; } function modelActions(item, includeLibrary = false) { const room = item.room; const ensureRoom = () => { if (!activeStore.state.rooms.some(entry => entry.id === item.id)) activeStore.addRoom(item.id); }; const favorite = () => { ensureRoom(); const current = activeStore.state.rooms.find(entry => entry.id === item.id); if (!isFavorite(current)) activeStore.toggleRoomInGroup(item.id, FAVORITE_GROUP_ID); else activeStore.toggleRoomInGroup(item.id, FAVORITE_GROUP_ID); }; return $('div', { class: 'rgh-actions' }, [ action(tx('房间', 'Room', 'Raum öffnen'), () => openNoopener(roomGridSingleCamUrl(item.id, { source: 'hub' })), 'primary'), action(tx('详情', 'Details', 'Alle Details'), () => RoomGridUltimate.openInspector(item.id)), $('details', { class: 'rgh-action-more' }, [ $('summary', { class: 'rgh-action', role: 'button' }, tx('更多', 'More', 'Mehr')), $('div', { class: 'rgh-action-menu' }, [ action(tx('固定预览', 'Pin preview', 'Vorschau anheften'), () => RoomGridUltimate.openPreview(item.id)), action(RoomGridUltimate.comparisonStatus().ids.includes(item.id) ? tx('从比较中移除', 'Remove from comparison', 'Aus Vergleich entfernen') : tx('加入比较', 'Add to comparison', 'Zum Vergleich'), () => RoomGridUltimate.toggleCompare(item.id), RoomGridUltimate.comparisonStatus().ids.includes(item.id) ? 'active' : ''), action(isFavorite(room) ? `★ ${tx('收藏', 'Favorite', 'Favorit')}` : `☆ ${tx('收藏', 'Favorite', 'Favorit')}`, favorite, isFavorite(room) ? 'active' : ''), !room ? action('MultiCam +', () => activeStore.addRoom(item.id)) : null, includeLibrary && room ? action(room.watchlist ? tx('取消观察', 'Unwatch', 'Merkliste aus') : tx('观察列表', 'Watchlist', 'Merkliste'), () => activeStore.patchRoom(item.id, { watchlist: !room.watchlist }), room.watchlist ? 'active' : '') : null, ]), ]), ]); } function lineageRow(item) { const sources = RoomGridDataProvenance.itemSources(item); const freshness = RoomGridDataProvenance.freshness(item); return $('div', { class: 'rgh-lineage', dataset: { roomgridLineage: item.id }, ariaLabel: tx('数据来源和新鲜度', 'Data sources and freshness', 'Datenquellen und Aktualität') }, [ ...sources.map(source => $('span', { dataset: { tone: source.tone }, title: tx('数据来源', 'Data source', 'Datenquelle') }, rawUiText(source.label))), $('span', { dataset: { tone: freshness.tone }, title: freshness.at ? dateTime(freshness.at) : tx('时间未知', 'Time unknown', 'Zeitpunkt unbekannt') }, rawUiText(freshness.label)), ]); } function datasetScope(data, matching, shown, page, pageSize) { const allCoverage = RoomGridDataProvenance.coverage(data.items); const matchCoverage = RoomGridDataProvenance.coverage(matching); const start = matching.length ? (Math.max(1, page) - 1) * pageSize + 1 : 0; const end = matching.length ? Math.min(matching.length, start + shown.length - 1) : 0; const lastAt = data.items.reduce((latest, item) => Math.max(latest, Number(item.model?.lastObservedAt) || 0, Number(item.lastActivity) || 0), 0); const percent = value => allCoverage.total ? `${Math.round(value / allCoverage.total * 100)} %` : '0 %'; const chips = [ [tx('完整本地数据集', 'Full local data set', 'Vollständiger lokaler Datenbestand'), allCoverage.total], [tx('符合筛选', 'Matching filters', 'Passend zum Filter'), matchCoverage.total], [tx('此页显示', 'Shown on this page', 'Auf dieser Seite'), `${start}–${end}`], [tx('个人整理', 'Personally organized', 'Persönlich organisiert'), allCoverage.saved], [tx('最近直播信号', 'Latest live signals', 'Letzte Live-Signale'), allCoverage.live], [tx('有观看记录', 'With watch history', 'Mit Sehverlauf'), allCoverage.watched], [tx('公开资料覆盖', 'Public-profile coverage', 'Profilabdeckung'), percent(allCoverage.profile)], [tx('国家覆盖', 'Country coverage', 'Länderabdeckung'), percent(allCoverage.country)], [tx('年龄覆盖', 'Age coverage', 'Altersabdeckung'), percent(allCoverage.age)], [tx('标签覆盖', 'Tag coverage', 'Tag-Abdeckung'), percent(allCoverage.tags)], ]; return $('section', { class: 'rgh-dataset-scope', dataset: { roomgridDatasetScope: '1' } }, [ $('div', { class: 'rgh-dataset-scope-head' }, [ $('strong', {}, tx('正在搜索哪个数据集?', 'Which data set is being searched?', 'Welcher Datenbestand wird durchsucht?')), $('span', {}, lastAt ? `${tx('最后一次本地观察', 'Last local observation', 'Letzte lokale Beobachtung')}: ${dateTime(lastAt)}` : tx('尚无观察时间', 'No observation time yet', 'Noch kein Beobachtungszeitpunkt')), ]), $('div', { class: 'rgh-dataset-chips' }, chips.slice(0, 3).map(([label, value]) => $('span', { class: 'rgh-dataset-chip' }, [$('b', {}, String(value)), rawUiText(label)]))), $('details', { class: 'rgh-source-details' }, [ $('summary', {}, tx('数据来源与覆盖率', 'Data sources and coverage', 'Datenquellen und Abdeckung')), $('div', { class: 'rgh-dataset-chips' }, chips.slice(3).map(([label, value]) => $('span', { class: 'rgh-dataset-chip' }, [$('b', {}, String(value)), rawUiText(label)]))), $('p', { class: 'rgh-source-legend' }, tx('公共 = 来自当前 Chaturbate 页面或实时目录。个人 = 由你保存、标记或分组。本地 = 仅在此设备上观察到的会话、预览或质量值。未知字段保持未知,不会被猜测。', 'Public = from current Chaturbate pages or live catalog. Personal = saved, tagged or grouped by you. Local = sessions, previews or quality values observed only on this device. Unknown fields remain unknown and are never guessed.', 'Öffentlich = aus aktuellen Chaturbate-Seiten oder dem Livekatalog. Persönlich = von dir gespeichert, markiert oder gruppiert. Lokal = nur auf diesem Gerät beobachtete Sitzungen, Vorschauen oder Qualitätswerte. Unbekannte Felder bleiben unbekannt und werden nicht geraten.')), ]), ]); } function statCard(item, customGroups) { const dayValues = Object.entries(item.model.days || {}).sort(([a], [b]) => a.localeCompare(b)).slice(-14).map(([, value]) => value); const weekdays = [tx('周日', 'Sun', 'So'), tx('周一', 'Mon', 'Mo'), tx('周二', 'Tue', 'Di'), tx('周三', 'Wed', 'Mi'), tx('周四', 'Thu', 'Do'), tx('周五', 'Fri', 'Fr'), tx('周六', 'Sat', 'Sa')]; const peak = item.peakIndex >= 0 && (item.model.hours?.[item.peakIndex] || 0) > 0 ? `${weekdays[Math.floor(item.peakIndex / 24)]} ${item.peakIndex % 24}:00` : '—'; const body = $('div', { class: 'rgh-card-body' }, [ $('div', { class: 'rgh-card-title' }, [$('h2', {}, item.profile?.displayName || item.id), isFavorite(item.room) ? $('span', { class: 'rgh-fav' }, '★') : null]), $('div', { class: 'rgh-bio' }, item.profile?.bio || item.model.subjects?.at?.(-1) || `${tx('本地模型统计', 'Local model statistics', 'Lokale Modelstatistik')} · @${item.id}`), publicProfileExtras(item), $('span', { class: 'rgh-catalog-badge' }, item.room ? tx('自动记录 · 个人整理', 'Automatically recorded · personally organized', 'Automatisch erfasst · persönlich organisiert', 'Registrado automáticamente · organizado personalmente', 'Registrado automaticamente · organizado pessoalmente') : tx('自动记录在不断增长的模型目录中', 'Automatically recorded in the growing model catalog', 'Automatisch im wachsenden Modelkatalog erfasst', 'Registrado automáticamente en el catálogo de modelos creciente', 'Registrado automaticamente no catálogo de modelos em crescimento')), lineageRow(item), $('div', { class: 'rgh-statgrid' }, [ [tx('总计', 'Total', 'Gesamt', 'Total', 'Total'), duration(item.totalMs)], [tx('会话', 'Sessions', 'Sitzungen', 'Sesiones', 'Sessões'), compactNumber(item.sessions)], [tx('平均会话', 'Avg. session', 'Ø Sitzung', 'Promedio por sesión', 'Média por sessão'), duration(item.avgSessionMs)], [tx('活跃天数', 'Active days', 'Aktive Tage', 'Días activos', 'Dias ativos'), compactNumber(item.activeDays)], [tx('占比', 'Share', 'Anteil', 'Proporción', 'Participação'), `${(item.share * 100).toFixed(1).replace('.', ',')} %`], [tx('最长会话', 'Longest session', 'Längste Sitzung', 'Sesión más larga', 'Sessão mais longa'), duration(item.model.longestSessionMs)], [tx('预览', 'Previews', 'Vorschauen', 'Vistas previas', 'Prévias'), compactNumber(item.counts.preview || 0)], [tx('快速信息', 'Quick views', 'Schnellinfos', 'Vistas rápidas', 'Visualizações rápidas'), compactNumber(item.counts.quickview || 0)], [tx('已发现', 'Discovered', 'Entdeckt', 'Descubiertos', 'Descobertos'), compactNumber(item.counts.discovered || 0)], ].map(([label, value]) => $('div', { class: 'rgh-stat' }, [$('small', {}, label), $('strong', {}, value)]))), dayValues.length ? spark(dayValues) : null, $('div', { class: 'rgh-meta' }, [ $('span', {}, tx(`首次观看:${dateTime(item.model.firstWatchedAt)}`, `First seen: ${dateTime(item.model.firstWatchedAt)}`, `Erstmals gesehen: ${dateTime(item.model.firstWatchedAt)}`, `Visto por primera vez: ${dateTime(item.model.firstWatchedAt)}`, `Visto pela primeira vez: ${dateTime(item.model.firstWatchedAt)}`)), $('span', {}, tx(`上次观看:${dateTime(item.model.lastWatchedAt)}`, `Last seen: ${dateTime(item.model.lastWatchedAt)}`, `Zuletzt gesehen: ${dateTime(item.model.lastWatchedAt)}`, `Visto por última vez: ${dateTime(item.model.lastWatchedAt)}`, `Visto pela última vez: ${dateTime(item.model.lastWatchedAt)}`)), $('span', {}, tx(`上次直播:${dateTime(item.model.lastOnlineAt || item.room?.lastSeenOnline)}`, `Last live: ${dateTime(item.model.lastOnlineAt || item.room?.lastSeenOnline)}`, `Zuletzt live: ${dateTime(item.model.lastOnlineAt || item.room?.lastSeenOnline)}`, `Última emisión: ${dateTime(item.model.lastOnlineAt || item.room?.lastSeenOnline)}`, `Última transmissão: ${dateTime(item.model.lastOnlineAt || item.room?.lastSeenOnline)}`)), $('span', {}, tx(`出现:${compactNumber(item.model.appearances || 0)}× · 首次 ${dateTime(item.model.firstObservedAt)} · 最近 ${dateTime(item.model.lastObservedAt)}`, `Appearances: ${compactNumber(item.model.appearances || 0)}× · first ${dateTime(item.model.firstObservedAt)} · latest ${dateTime(item.model.lastObservedAt)}`, `Aufgetaucht: ${compactNumber(item.model.appearances || 0)}× · erstmals ${dateTime(item.model.firstObservedAt)} · zuletzt ${dateTime(item.model.lastObservedAt)}`, `Apariciones: ${compactNumber(item.model.appearances || 0)}× · primera ${dateTime(item.model.firstObservedAt)} · última ${dateTime(item.model.lastObservedAt)}`, `Aparições: ${compactNumber(item.model.appearances || 0)}× · primeira ${dateTime(item.model.firstObservedAt)} · última ${dateTime(item.model.lastObservedAt)}`)), item.model.sources?.length ? $('span', {}, `${tx('数据来源', 'Data sources', 'Datenquellen')}: ${item.model.sources.slice(-4).map(source => RoomGridDataProvenance.sourceInfo(source).label).join(' · ')}`) : null, $('span', {}, tx(`典型时间:${peak}`, `Typical time: ${peak}`, `Typische Zeit: ${peak}`, `Horario habitual: ${peak}`, `Horário habitual: ${peak}`)), $('span', {}, tx( `技术:${item.samples.length ? `${Math.round(item.avgStartup)} 毫秒启动 · ${item.stalls} 次中断 · ${(item.dropRate * 100).toFixed(2).replace('.', ',')} % 丢帧` : '尚无测量值'}`, `Technical: ${item.samples.length ? `${Math.round(item.avgStartup)} ms startup · ${item.stalls} stalls · ${(item.dropRate * 100).toFixed(2).replace('.', ',')}% dropped frames` : 'no measurements yet'}`, `Technik: ${item.samples.length ? `${Math.round(item.avgStartup)} ms Start · ${item.stalls} Unterbrechungen · ${(item.dropRate * 100).toFixed(2).replace('.', ',')} % Bildverlust` : 'noch keine Messwerte'}`, `Técnica: ${item.samples.length ? `${Math.round(item.avgStartup)} ms de inicio · ${item.stalls} interrupciones · ${(item.dropRate * 100).toFixed(2).replace('.', ',')} % de pérdida de fotogramas` : 'aún no hay mediciones'}`, `Técnica: ${item.samples.length ? `${Math.round(item.avgStartup)} ms de início · ${item.stalls} interrupções · ${(item.dropRate * 100).toFixed(2).replace('.', ',')} % de perda de quadros` : 'ainda não há medições'}`, )), ]), item.tags.length ? $('div', { class: 'rgh-tags' }, item.tags.slice(0, 12).map(tag => $('span', { class: 'rgh-tag' }, `#${tag}`))) : null, modelExampleGallery(item), groupPicker(item, customGroups || []), modelActions(item), $('div', { class: 'rgh-actions' }, [action(tx('在资料库中编辑', 'Edit in library', 'In Bibliothek bearbeiten', 'Editar en la biblioteca', 'Editar na biblioteca'), () => { viewState.query = item.id; viewState.libraryPage = 1; setView('library'); })]), ]); return $('article', { class: 'rgh-card', dataset: { roomgridModelStat: item.id } }, [modelMedia(item), body]); } function onlinePattern(item) { const observed = Array.from({ length: 168 }, (_, index) => Math.max(0, Number(item.model.onlineHours?.[index]) || 0)); const watched = Array.from({ length: 168 }, (_, index) => Math.max(0, Number(item.model.hours?.[index]) || 0)); const observedTotal = observed.reduce((sum, value) => sum + value, 0); const watchedTotal = watched.reduce((sum, value) => sum + value, 0); const weights = observedTotal ? observed : watched; const total = observedTotal || watchedTotal; if (!total) return { label: tx('尚无经验数据', 'No experience data yet', 'Noch keine Erfahrungswerte', 'Aún no hay datos de experiencia', 'Ainda não há dados de experiência'), note: tx('从现在起将在本地收集在线观察数据。', 'Online observations will be collected locally from now on.', 'Online-Beobachtungen werden ab jetzt lokal gesammelt.', 'A partir de ahora, las observaciones en línea se recopilarán localmente.', 'A partir de agora, as observações online serão coletadas localmente.'), confidence: tx('待定', 'Open', 'Offen', 'Pendiente', 'Pendente'), samples: 0, }; const dayNames = [ tx('周日', 'Sun', 'So', 'Dom', 'Dom'), tx('周一', 'Mon', 'Mo', 'Lun', 'Seg'), tx('周二', 'Tue', 'Di', 'Mar', 'Ter'), tx('周三', 'Wed', 'Mi', 'Mié', 'Qua'), tx('周四', 'Thu', 'Do', 'Jue', 'Qui'), tx('周五', 'Fri', 'Fr', 'Vie', 'Sex'), tx('周六', 'Sat', 'Sa', 'Sáb', 'Sáb'), ]; const byDay = Array.from({ length: 7 }, (_, day) => weights.slice(day * 24, day * 24 + 24).reduce((sum, value) => sum + value, 0)); const byHour = Array.from({ length: 24 }, (_, hour) => weights.reduce((sum, value, index) => sum + (index % 24 === hour ? value : 0), 0)); const days = byDay.map((value, index) => ({ value, index })).filter(item => item.value > 0).sort((a, b) => b.value - a.value).slice(0, 2).map(item => dayNames[item.index]); const bestHours = byHour.map((value, index) => ({ value, index })).filter(item => item.value > 0).sort((a, b) => b.value - a.value).slice(0, 4).map(item => item.index).sort((a, b) => a - b); const ranges = []; bestHours.forEach(hour => { const last = ranges[ranges.length - 1]; if (last && last[1] + 1 === hour) last[1] = hour; else ranges.push([hour, hour]); }); const hourSuffix = tx('时', '', ' Uhr', ' h', ' h'); const timeText = ranges.slice(0, 3).map(([from, to]) => `${String(from).padStart(2, '0')}–${String((to + 1) % 24).padStart(2, '0')}${hourSuffix}`).join(', '); const samples = Math.max(observedTotal, Number(item.model.onlineSamples) || 0); const confidence = observedTotal >= 36 ? tx('良好趋势', 'Good trend', 'Gute Tendenz', 'Buena tendencia', 'Boa tendência') : observedTotal >= 12 ? tx('中等趋势', 'Moderate trend', 'Mittlere Tendenz', 'Tendencia moderada', 'Tendência moderada') : observedTotal >= 3 ? tx('初步趋势', 'Initial trend', 'Erste Tendenz', 'Tendencia inicial', 'Tendência inicial') : observedTotal ? tx('首次观察', 'First observation', 'Erste Beobachtung', 'Primera observación', 'Primeira observação') : tx('根据观看时间估算', 'Estimated from watch time', 'Aus Sehzeiten geschätzt', 'Estimado a partir del tiempo de visualización', 'Estimado com base no tempo de visualização'); const note = observedTotal ? tx(`${compactNumber(samples)} 次本地直播观察,每 5 分钟最多一次`, `${compactNumber(samples)} local live observations, at most one every 5 minutes`, `${compactNumber(samples)} lokale Live-Beobachtungen, höchstens eine je 5 Minuten`, `${compactNumber(samples)} observaciones locales en vivo, como máximo una cada 5 minutos`, `${compactNumber(samples)} observações locais ao vivo, no máximo uma a cada 5 minutos`) : tx(`${duration(watchedTotal)} 的历史观看时间作为谨慎估计`, `${duration(watchedTotal)} of your previous watch time as a cautious estimate`, `${duration(watchedTotal)} deiner bisherigen Sehzeit als vorsichtige Näherung`, `${duration(watchedTotal)} de tu tiempo de visualización anterior como estimación prudente`, `${duration(watchedTotal)} do seu tempo de visualização anterior como estimativa cautelosa`); return { label: `${days.join('/') || tx('工作日待定', 'weekday open', 'Wochentag offen', 'día pendiente', 'dia pendente')} · ${timeText || tx('时间待定', 'time open', 'Uhrzeit offen', 'hora pendiente', 'horário pendente')}`, note, confidence, samples, }; } function customLibraryGroups() { return (activeStore.state.groups || []).filter(group => !group.system && !group.archived).sort((a, b) => Number(a.order || 0) - Number(b.order || 0) || String(a.name).localeCompare(String(b.name))); } function buildTagSuggestions(rooms) { const counts = new Map(); (rooms || []).forEach(room => (room.localTags || []).forEach(tag => { const clean = parseTags(tag)[0]; if (clean) counts.set(clean, (counts.get(clean) || 0) + 1); })); return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); } function libraryDisplayPanel() { const details = [ ['libraryShowImage', tx('预览图像', 'Preview image', 'Vorschaubild')], ['libraryShowBio', tx('简介 / 房间主题', 'Biography / room subject', 'Biografie / Raumthema')], ['libraryShowCatalogBadge', tx('目录状态', 'Catalog state', 'Katalogstatus')], ['libraryShowStatus', tx('直播状态', 'Live status', 'Live-Status')], ['libraryShowViewers', tx('观众数', 'Viewer count', 'Zuschauerzahl')], ['libraryShowCountry', tx('国家/地区', 'Country/region', 'Land/Region')], ['libraryShowLocation', tx('地点', 'Location', 'Ort')], ['libraryShowAge', tx('年龄', 'Age', 'Alter')], ['libraryShowType', tx('类型', 'Type', 'Typ')], ['libraryShowWatchTime', tx('总观看时间', 'Total watch time', 'Gesamte Sehzeit')], ['libraryShowSessions', tx('会话', 'Sessions', 'Sitzungen')], ['libraryShowAverageSession', tx('平均会话', 'Average session', 'Durchschnittliche Sitzung')], ['libraryShowLongestSession', tx('最长会话', 'Longest session', 'Längste Sitzung')], ['libraryShowActiveDays', tx('活跃天数', 'Active days', 'Aktive Tage')], ['libraryShowFirstSeen', tx('首次观看', 'First seen', 'Erstmals gesehen')], ['libraryShowLastSeen', tx('上次观看', 'Last seen', 'Zuletzt gesehen')], ['libraryShowLastLive', tx('上次直播', 'Last live', 'Zuletzt live')], ['libraryShowPreviews', tx('预览', 'Previews', 'Vorschauen')], ['libraryShowQuickViews', tx('快速信息', 'Quick views', 'Schnellinfos')], ['libraryShowOnlineEstimate', tx('大概在线时间', 'Approximate online times', 'Ungefähre Onlinezeiten')], ['libraryShowActivityDetails', tx('活动、来源和个人资料状态', 'Activity, sources and profile state', 'Aktivität, Quellen und Profilstatus')], ['libraryShowExampleGallery', tx('示例图像', 'Example images', 'Beispielbilder')], ['libraryShowGroups', tx('自己的组', 'Own groups', 'Eigene Gruppen')], ['libraryShowPublicTags', tx('公开/识别标签', 'Public/recognized tags', 'Öffentliche/erkannte Tags')], ['libraryShowEditor', tx('备注、自己的标签和优先级', 'Notes, own tags and priority', 'Notizen, eigene Tags und Priorität')], ['libraryShowRating', tx('本地评分', 'Local rating', 'Lokale Bewertung')], ['libraryShowGroupAssignment', tx('组分配', 'Group assignment', 'Gruppenzuordnung')], ['libraryShowActions', tx('房间和预览操作', 'Room and preview actions', 'Raum- und Vorschauaktionen')], ]; const applyDetails = enabled => { const selected = new Set(enabled); details.forEach(([key]) => { viewState[key] = selected.has(key); }); }; const commit = (rerender = true) => { savePrefs(); applyLibraryLayout(); if (rerender) scheduleRender(); }; const preset = (density, columns, pageSize, scale, enabled) => { viewState.density = density; viewState.libraryColumns = columns; viewState.libraryPageSize = pageSize; viewState.libraryCardScale = scale; viewState.libraryPage = 1; applyDetails(enabled); commit(); }; const compactKeys = ['libraryShowImage', 'libraryShowStatus', 'libraryShowViewers', 'libraryShowCountry', 'libraryShowAge', 'libraryShowActions']; const balancedKeys = ['libraryShowImage', 'libraryShowBio', 'libraryShowCatalogBadge', 'libraryShowStatus', 'libraryShowViewers', 'libraryShowCountry', 'libraryShowLocation', 'libraryShowAge', 'libraryShowType', 'libraryShowWatchTime', 'libraryShowSessions', 'libraryShowAverageSession', 'libraryShowLastSeen', 'libraryShowLastLive', 'libraryShowOnlineEstimate', 'libraryShowGroups', 'libraryShowPublicTags', 'libraryShowRating', 'libraryShowActions']; return $('section', { class: 'rgh-section rgh-library-display', dataset: { roomgridLibraryDisplay: '1' } }, [ $('div', { class: 'rgh-section-head' }, [ $('h2', {}, tx('模型库显示', 'Library display', 'Bibliotheks-Darstellung')), $('span', { class: 'rgh-private' }, tx('保存在此浏览器中', 'Saved in this browser', 'In diesem Browser gespeichert')), ]), $('p', { class: 'rgh-bio' }, tx('选择卡片大小、每行模型数、每页数量以及每张卡片显示的详细信息。', 'Choose card size, models per row, page size and exactly which details each card shows.', 'Wähle Kartengröße, Models pro Reihe, Seitenumfang und ganz genau, welche Details jede Karte zeigt.')), $('div', { class: 'rgh-library-presets' }, [ action(tx('紧凑概览', 'Compact overview', 'Kompakte Übersicht'), () => preset('compact', 5, 120, 85, compactKeys)), action(tx('平衡', 'Balanced', 'Ausgewogen'), () => preset('comfortable', 0, 48, 100, balancedKeys), 'primary'), action(tx('所有详细信息', 'All details', 'Alle Details'), () => preset('large', 2, 24, 110, details.map(([key]) => key))), action(tx('显示所有详细信息', 'Show all details', 'Alle Informationen einblenden'), () => { applyDetails(details.map(([key]) => key)); commit(); }), action(tx('仅核心信息', 'Core details only', 'Nur Kerninfos'), () => { applyDetails(compactKeys); commit(); }), ]), $('div', { class: 'rgh-library-layout-row' }, [ rangeControl(tx('卡片缩放', 'Card zoom', 'Kartenvergrößerung'), viewState.libraryCardScale, value => { viewState.libraryCardScale = value; commit(false); }), ]), $('h3', {}, tx('每个模型的可见信息', 'Visible information per model', 'Sichtbare Informationen pro Model')), $('div', { class: 'rgh-detail-options' }, details.map(([key, label]) => $('label', { class: 'rgh-detail-check' }, [ $('input', { type: 'checkbox', checked: !!viewState[key], onchange: event => { viewState[key] = event.target.checked; commit(); } }), $('span', {}, label), ]))), $('p', { class: 'rgh-library-display-note' }, tx('使用自动列时,卡片缩放决定并排显示多少个模型。精确的列数会覆盖它。所有选择都会保存在此浏览器中。', 'With automatic columns, card zoom controls how many models fit next to each other. An exact column count overrides it. All choices remain saved in this browser.', 'Bei automatischen Spalten bestimmt die Kartenvergrößerung, wie viele Models nebeneinander passen. Eine feste Spaltenzahl hat Vorrang. Alle Auswahlen bleiben in diesem Browser gespeichert.')), ]); } function createTagEditor(room, tagSuggestions, pendingInput = '', onDraftChange = null) { let draft = [...new Set((room.localTags || []).map(tag => String(tag).trim().toLocaleLowerCase()).filter(Boolean))]; const editorTags = value => [...new Set(String(value || '').split(/[,;\n]+/u).map(tag => tag.trim().toLocaleLowerCase().slice(0, 50)).filter(Boolean))]; const chips = $('div', { class: 'rgh-tag-editor-chips' }); const matches = $('div', { class: 'rgh-tag-suggestions' }); const input = $('input', { class: 'rgh-tag-input', type: 'text', value: pendingInput, dataset: { libraryDraftField: 'tags' }, ariaLabel: `${tx('自己的标签', 'Own tags', 'Eigene Tags')} · ${room.id}`, placeholder: 'Eigenen Tag eingeben …', autocomplete: 'off' }); const tagNotice = $('small', { role: 'status', dataset: { tagLimitNotice: '1' } }); const remember = () => onDraftChange?.({ localTags: [...draft], tagInput: input.value }); const drawChips = () => chips.replaceChildren(...(draft.length ? draft.map(tag => $('button', { type: 'button', title: tx(`删除标签 ${tag}`, `Remove tag ${tag}`, `Tag ${tag} entfernen`, `Eliminar etiqueta ${tag}`, `Remover tag ${tag}`), onclick: () => { draft = draft.filter(value => value !== tag); remember(); drawChips(); drawMatches(input.value); } }, rawUiText(`#${tag} ×`))) : [$('span', {}, tx('还没有自定义标签', 'No custom tags yet', 'Noch keine eigenen Tags', 'Todavía no hay etiquetas propias', 'Ainda não há tags próprias'))])); const addValues = value => { const additions = editorTags(value); if (!additions.length) return false; const next = [...new Set([...draft, ...additions])]; if (next.length > Math.max(40, draft.length)) { tagNotice.textContent = tx('最多40个自定义标签。请先删除一个;输入内容已保留。', 'Up to 40 own tags. Remove one first; your input has been kept.', 'Bis zu 40 eigene Tags. Entferne zuerst einen; deine Eingabe bleibt erhalten.'); remember(); return false; } tagNotice.textContent = ''; draft = next; input.value = ''; remember(); drawChips(); matches.replaceChildren(); return true; }; const drawMatches = value => { const query = String(value || '').trim().toLocaleLowerCase(); const options = (tagSuggestions || []).filter(item => !draft.includes(item.tag) && (!query || item.tag.includes(query))).slice(0, 8); matches.replaceChildren(...options.map(item => $('button', { type: 'button', onclick: () => { addValues(item.tag); input.focus(); } }, [$('b', {}, `#${item.tag}`), $('span', {}, tx(`${item.count} 个模型`, `${item.count} model${item.count === 1 ? '' : 's'}`, `${item.count} Model${item.count === 1 ? '' : 's'}`, `${item.count} modelo${item.count === 1 ? '' : 's'}`, `${item.count} modelo${item.count === 1 ? '' : 's'}`))]))); matches.classList.toggle('is-open', !!query && options.length > 0); }; input.addEventListener('input', () => { remember(); drawMatches(input.value); }); input.addEventListener('focus', () => drawMatches(input.value)); input.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ',') { event.preventDefault(); addValues(input.value); } if (event.key === 'Escape') matches.classList.remove('is-open'); }); input.addEventListener('blur', () => setTimeout(() => matches.classList.remove('is-open'), 140)); drawChips(); return { element: $('section', { class: 'rgh-tag-editor' }, [ $('div', { class: 'rgh-editor-label' }, [$('strong', {}, 'Eigene Tags'), $('span', {}, 'lokal · für Suche und Filter')]), chips, $('div', { class: 'rgh-tag-entry' }, [input, action('Tag hinzufügen', () => addValues(input.value))]), matches, tagNotice, $('small', {}, 'Enter oder „Tag hinzufügen“ übernimmt den Entwurf. Dauerhaft gespeichert wird er erst mit „Profil speichern“. Bereits verwendete Tags werden beim Tippen vorgeschlagen.'), ]), values: () => { const next = [...new Set([...draft, ...editorTags(input.value)])]; if (next.length > Math.max(40, draft.length)) { addValues(input.value); return null; } return next; }, }; } function groupPicker(item, groups) { const room = activeStore.state.rooms.find(entry => entry.id === item.id) || item.room || null; const assigned = groups.filter(group => roomInGroup(room, group.id)); const ensureOrganizedRoom = () => { let current = activeStore.state.rooms.find(entry => entry.id === item.id); if (!current) { activeStore.addRoom(item.id); current = activeStore.state.rooms.find(entry => entry.id === item.id); } return current; }; const picker = $('details', { class: 'rgh-group-picker', open: openLibraryGroupPickers.has(item.id), dataset: { roomgridGroupPicker: item.id } }, [ $('summary', {}, tx(`管理自定义组 · 已分配到 ${assigned.length} 个`, `Manage custom groups · assigned to ${assigned.length}`, `Eigene Gruppen verwalten · ${assigned.length} zugeordnet`, `Administrar grupos propios · asignado a ${assigned.length}`, `Gerenciar grupos próprios · atribuído a ${assigned.length}`)), $('div', { class: 'rgh-group-picker-list' }, groups.length ? groups.map(group => $('label', {}, [ $('input', { type: 'checkbox', checked: roomInGroup(room, group.id), onchange: event => { const current = ensureOrganizedRoom(); if (!!current && roomInGroup(current, group.id) !== event.target.checked) activeStore.toggleRoomInGroup(item.id, group.id); } }), $('span', {}, rawUiText(group.name)), ])) : [$('span', { class: 'rgh-muted-copy' }, 'Erstelle oben zuerst eine eigene Gruppe.')]), ]); picker.addEventListener('toggle', () => { if (!picker.isConnected) return; if (picker.open) openLibraryGroupPickers.add(item.id); else openLibraryGroupPickers.delete(item.id); }); return picker; } function libraryGroupPanel(data) { const groups = customLibraryGroups(); const validIds = new Set(groups.map(group => group.id)); viewState.libraryGroups = viewState.libraryGroups.filter(id => validIds.has(id)); const createInput = $('input', { type: 'text', maxlength: 80, placeholder: tx('新群组的名称', 'Name of the new group', 'Name der neuen Gruppe', 'Nombre del nuevo grupo', 'Nome do novo grupo'), onkeydown: event => { if (event.key === 'Enter') { event.preventDefault(); createGroup(); } } }); const createGroup = () => { const name = String(createInput.value || '').trim(); if (!name) { createInput.focus(); return; } activeStore.addGroup(name); createInput.value = ''; viewState.libraryPage = 1; savePrefs(); }; const toggleFilter = id => { viewState.libraryGroups = viewState.libraryGroups.includes(id) ? viewState.libraryGroups.filter(value => value !== id) : [...viewState.libraryGroups, id]; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; const filterButtons = groups.map(group => { const count = data.rooms.filter(room => roomInGroup(room, group.id)).length; return $('button', { class: `rgh-group-filter${viewState.libraryGroups.includes(group.id) ? ' is-active' : ''}`, type: 'button', ariaPressed: String(viewState.libraryGroups.includes(group.id)), onclick: () => toggleFilter(group.id) }, [$('span', {}, rawUiText(group.name)), $('b', {}, String(count))]); }); const managerRows = groups.map(group => { const name = $('input', { type: 'text', value: group.name, maxlength: 80 }); return $('div', { class: 'rgh-group-admin-row' }, [ name, action('Namen speichern', () => activeStore.renameGroup(group.id, name.value)), action('Gruppe löschen', () => { if (!confirm(tx(`删除“${group.name}”组?模型将保留在你的库中。`, `Delete group “${group.name}”? The models will remain in your library.`, `Gruppe „${group.name}“ löschen? Die Models bleiben in deiner Bibliothek.`, `¿Eliminar el grupo «${group.name}»? Los modelos permanecerán en tu biblioteca.`, `Excluir o grupo “${group.name}”? Os modelos permanecerão na sua biblioteca.`))) return; viewState.libraryGroups = viewState.libraryGroups.filter(id => id !== group.id); savePrefs(); RoomGridUx.undoable(activeStore, tx(`群组“${group.name}”已删除`, `Group “${group.name}” deleted`, `Gruppe „${group.name}“ gelöscht`), () => activeStore.removeGroup(group.id), { detail: tx('模型仍保留在库中。', 'Models remain in the library.', 'Die Models bleiben in der Bibliothek.'), onUndo: () => scheduleRender() }); }, 'danger'), ]); }); return $('section', { class: 'rgh-section rgh-group-center', dataset: { roomgridLibraryGroups: '1' } }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, tx('自定义群组', 'Custom groups', 'Eigene Gruppen', 'Grupos propios', 'Grupos próprios')), $('span', { class: 'rgh-private' }, tx(`${groups.length} 个组 · 可多选`, `${groups.length} groups · multiple selection possible`, `${groups.length} Gruppen · Mehrfachauswahl möglich`, `${groups.length} grupos · selección múltiple disponible`, `${groups.length} grupos · seleção múltipla disponível`))]), $('p', { class: 'rgh-bio' }, 'Erstelle frei benannte Sammlungen, ordne Models mehreren Gruppen zu und zeige hier eine oder mehrere Gruppen gemeinsam an.'), $('div', { class: 'rgh-group-create' }, [createInput, action(tx('创建新群组', 'Create new group', 'Neue Gruppe erstellen', 'Crear nuevo grupo', 'Criar novo grupo'), createGroup, 'primary')]), $('div', { class: 'rgh-group-filterbar' }, [ $('button', { class: `rgh-group-filter${viewState.libraryGroups.length ? '' : ' is-active'}`, type: 'button', onclick: () => { viewState.libraryGroups = []; viewState.libraryPage = 1; savePrefs(); scheduleRender(); } }, [$('span', {}, 'Alle Gruppen'), $('b', {}, String(data.items.length))]), ...filterButtons, ]), viewState.libraryGroups.length > 1 ? selectControl('Mehrfachfilter', viewState.libraryGroupMode, [['any', 'Model ist in mindestens einer gewählten Gruppe'], ['all', 'Model ist in allen gewählten Gruppen']], value => { viewState.libraryGroupMode = value; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }) : null, groups.length ? $('details', { class: 'rgh-group-admin' }, [$('summary', {}, 'Gruppen umbenennen oder löschen'), $('div', { class: 'rgh-group-admin-list' }, managerRows)]) : $('div', { class: 'rgh-empty rgh-empty-small' }, 'Noch keine eigene Gruppe. Gib oben einen Namen ein und erstelle deine erste Sammlung.'), ]); } function libraryTagFilterPanel(tagSuggestions) { const validTags = new Set((tagSuggestions || []).map(item => item.tag)); viewState.libraryTags = viewState.libraryTags.filter(tag => validTags.has(tag)); const toggle = tag => { viewState.libraryTags = viewState.libraryTags.includes(tag) ? viewState.libraryTags.filter(value => value !== tag) : [...viewState.libraryTags, tag]; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; return $('section', { class: 'rgh-section rgh-tag-filter-center', dataset: { roomgridLibraryTags: '1' } }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Eigene Tags filtern'), $('span', { class: 'rgh-private' }, tx(`${tagSuggestions.length} 个已保存标签`, `${tagSuggestions.length} saved tags`, `${tagSuggestions.length} gespeicherte Tags`, `${tagSuggestions.length} etiquetas guardadas`, `${tagSuggestions.length} tags salvas`))]), $('p', { class: 'rgh-bio' }, 'Jeder gespeicherte eigene Tag steht automatisch in der Suche, in der Autovervollständigung und hier als direkter Mehrfachfilter bereit.'), $('div', { class: 'rgh-group-filterbar' }, [ $('button', { class: `rgh-group-filter${viewState.libraryTags.length ? '' : ' is-active'}`, type: 'button', onclick: () => { viewState.libraryTags = []; viewState.libraryPage = 1; savePrefs(); scheduleRender(); } }, [$('span', {}, 'Alle eigenen Tags'), $('b', {}, String(tagSuggestions.length))]), ...tagSuggestions.map(item => $('button', { class: `rgh-group-filter${viewState.libraryTags.includes(item.tag) ? ' is-active' : ''}`, type: 'button', ariaPressed: String(viewState.libraryTags.includes(item.tag)), onclick: () => toggle(item.tag) }, [$('span', {}, `#${item.tag}`), $('b', {}, String(item.count))])), ]), viewState.libraryTags.length > 1 ? selectControl('Tag-Mehrfachfilter', viewState.libraryTagMode, [['any', 'Model besitzt mindestens einen gewählten Tag'], ['all', 'Model besitzt alle gewählten Tags']], value => { viewState.libraryTagMode = value; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }) : null, tagSuggestions.length ? null : $('div', { class: 'rgh-empty rgh-empty-small' }, 'Sobald du bei einem Model einen eigenen Tag speicherst, erscheint er hier und bei der Autovervollständigung.'), ]); } const hubPublicTagCache = new WeakMap(); function publicTagsForLibraryItem(item) { const modelTags = item?.model?.tags || []; const profileTags = item?.profile?.tags || []; const cached = item && hubPublicTagCache.get(item); // Exact raw-value checks also catch in-place edits with unchanged length. // Sort comparators reuse normalized tags rather than allocate a Set and // lowercase every string again on every comparison. Weak keys release // replaced historical indices; no persistent data or tag limit changes. if (cached && cached.modelTags.length === modelTags.length && cached.profileTags.length === profileTags.length && cached.modelTags.every((tag, index) => tag === modelTags[index]) && cached.profileTags.every((tag, index) => tag === profileTags[index])) return cached.value; const value = [...new Set([...modelTags, ...profileTags] .map(tag => String(tag || '').trim().toLocaleLowerCase()).filter(Boolean))]; if (item && typeof item === 'object') hubPublicTagCache.set(item, { modelTags: [...modelTags], profileTags: [...profileTags], value }); return value; } function libraryStatusBucket(item) { const value = String(item?.status || 'unknown').toLocaleLowerCase(); if (value === 'online' || value === 'public') return 'live'; if (value === 'private') return 'private'; if (value === 'offline') return 'offline'; if (value === 'error' || value === 'loading') return 'error'; return 'unknown'; } // Facets describe the unfiltered index, not the current search. Build them // once in short slices; paging/filter edits must not rescan every model. // Weak keys release old indices. Nothing here is a persistent data copy. const hubFacetCache = new WeakMap(); async function prepareHubFacets(data, revision) { const key = `${activeStore.revision}:${data.catalogStatusKey || ''}`; const cached = hubFacetCache.get(data); if (cached?.key === key) return cached.value; const genderCounts = { f: 0, m: 0, c: 0, t: 0, unknown: 0 }; const statusCounts = { live: 0, offline: 0, private: 0, unknown: 0, error: 0 }; const countryCounts = new Map(); const tagCounts = new Map(); const groupCounts = new Map(); let sliceStartedAt = performance.now(); for (let index = 0; index < data.items.length; index += 1) { if (revision !== renderRevision || !root?.isConnected) return null; const item = data.items[index]; const gender = String(item.model?.gender || ''); genderCounts[['f', 'm', 'c', 't'].includes(gender) ? gender : 'unknown'] += 1; statusCounts[libraryStatusBucket(item)] += 1; const country = String(item.model?.country || '').trim(); if (country) countryCounts.set(country, (countryCounts.get(country) || 0) + 1); for (const tag of publicTagsForLibraryItem(item)) tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); for (const group of new Set(getRoomGroups(item.room))) groupCounts.set(group, (groupCounts.get(group) || 0) + 1); if (index && index % 128 === 0 && performance.now() - sliceStartedAt >= 8) { await yieldToUi(); sliceStartedAt = performance.now(); } } const options = counts => [...counts].map(([value, count]) => ({ value, label: value, count })); const compare = (a, b) => b.count - a.count || a.label.localeCompare(b.label); const countries = await sortHubItemsProgressively(options(countryCounts), compare, revision); const tags = await sortHubItemsProgressively(options(tagCounts), compare, revision); if (!countries || !tags || revision !== renderRevision || key !== `${activeStore.revision}:${data.catalogStatusKey || ''}`) return null; const value = { genderCounts, statusCounts, countries, publicTags: tags.slice(0, 500).map(option => ({ ...option, label: `#${option.label}` })), groupCounts }; hubFacetCache.set(data, { key, value }); return value; } const HUB_ONLINE_FRESHNESS_MS = 15 * 60 * 1000; async function applyCurrentCatalogStatus(data, revision) { const snapshot = globalThis.RoomGridCatalogBridge?.getCurrentModels?.(); const fresh = snapshot?.fresh && snapshot.fetchedAt && Date.now() - snapshot.fetchedAt <= HUB_ONLINE_FRESHNESS_MS; const statusKey = `${catalogStatusGeneration}:${Math.floor(Date.now() / 60000)}:${snapshot?.fetchedAt || 0}:${snapshot?.rooms?.length || 0}:${!!fresh}:${!!snapshot?.complete}`; if (data.catalogStatusKey === statusKey) return; const liveById = new Map(fresh ? snapshot.rooms.map(room => [room.id, room]) : []); await forEachProgressively(data.items, item => { if (revision !== renderRevision) return; item.status = freshObservedStatus(item.room, item.model, item.profile); const current = liveById.get(item.id); if (current) { item.status = normalizeHubStatus(current.currentShow); item.model = { ...item.model, viewerCount: current.viewers, secondsOnline: current.secondsOnline }; } else if (fresh && snapshot.complete) { // A partial/error/cached page cannot prove that a room is offline. const observed = freshObservedStatus(item.room, item.model, item.profile); const latestOnlineAt = Math.max(Number(item.model.lastOnlineAt) || 0, Number(item.room?.lastSeenOnline) || 0, Number(item.room?.fieldModifiedAt?.lastStatus) || 0, item.profile?.lastStatus === 'online' ? Number(item.profile.observedAt) || 0 : 0); item.status = observed === 'online' && latestOnlineAt <= snapshot.fetchedAt ? 'offline' : observed; } }, 8); if (revision === renderRevision) data.catalogStatusKey = statusKey; } function normalizeHubStatus(value) { // Hydrated records use these canonical values; skip locale conversion // on the large history path without changing legacy-value handling. if (!value || value === 'unknown') return 'unknown'; if (value === 'online' || value === 'public') return 'online'; if (value === 'hidden' || value === 'private') return 'private'; if (value === 'offline' || value === 'error' || value === 'loading') return value; const status = String(value || '').trim().toLocaleLowerCase(); if (status === 'public' || status === 'online') return 'online'; if (status === 'hidden') return 'private'; return ['offline', 'private', 'error', 'loading'].includes(status) ? status : 'unknown'; } function freshObservedStatus(room, model, profile, now = Date.now()) { const roomStatus = normalizeHubStatus(room?.lastStatus); const modelStatus = normalizeHubStatus(model?.lastStatus); const profileStatus = normalizeHubStatus(profile?.lastStatus); let newestStatus = 'unknown'; let newestAt = -1; const consider = (status, at) => { if (status === 'unknown') return; const timestamp = Math.max(0, Number(at) || 0); if (timestamp > newestAt) { newestStatus = status; newestAt = timestamp; } }; consider(roomStatus, roomStatus === 'online' ? Math.max(Number(room?.lastSeenOnline) || 0, Number(room?.fieldModifiedAt?.lastStatus) || 0) : Number(room?.fieldModifiedAt?.lastStatus) || 0); consider(modelStatus, modelStatus === 'online' ? model?.lastOnlineAt : model?.lastObservedAt); consider(profileStatus, profile?.observedAt); if (newestStatus === 'online' && (!newestAt || now - newestAt > HUB_ONLINE_FRESHNESS_MS)) return 'unknown'; return newestStatus; } function hasAdvancedLibraryFilters() { return Object.entries(LIBRARY_FILTER_DEFAULTS).some(([key, fallback]) => Array.isArray(fallback) ? (Array.isArray(viewState[key]) && viewState[key].length > 0) : viewState[key] !== fallback); } function matchesLibraryAdvancedFilters(item) { const model = item.model || {}; const room = item.room || null; const gender = ['f', 'm', 'c', 't'].includes(String(model.gender || '')) ? String(model.gender) : 'unknown'; const status = libraryStatusBucket(item); const country = String(model.country || '').trim(); const age = Math.max(0, Number(model.age) || 0); const viewers = Math.max(0, Number(model.viewerCount) || 0); const followers = Math.max(0, Number(model.followers) || 0); const groups = getRoomGroups(room); const ownTags = room?.localTags || []; const hasNotes = !!String(room?.notes || '').trim(); if (viewState.libraryGenders.length && !viewState.libraryGenders.includes(gender)) return false; if (viewState.libraryStatuses.length && !viewState.libraryStatuses.includes(status)) return false; if (viewState.libraryCountries.length && !viewState.libraryCountries.includes(country)) return false; if (viewState.libraryPublicTags.length) { const publicTags = new Set(publicTagsForLibraryItem(item)); const matches = viewState.libraryPublicTags.map(tag => publicTags.has(tag)); if (viewState.libraryPublicTagMode === 'all' ? !matches.every(Boolean) : !matches.some(Boolean)) return false; } if ((viewState.libraryAgeMin || viewState.libraryAgeMax) && !age) return false; if (viewState.libraryAgeMin && age < viewState.libraryAgeMin) return false; if (viewState.libraryAgeMax && age > viewState.libraryAgeMax) return false; if (viewState.libraryViewerMin && viewers < viewState.libraryViewerMin) return false; if (viewState.libraryViewerMax && viewers > viewState.libraryViewerMax) return false; if (viewState.libraryFollowersMin && followers < viewState.libraryFollowersMin) return false; if (viewState.libraryFollowersMax && followers > viewState.libraryFollowersMax) return false; if (viewState.libraryWatchMinMinutes && item.totalMs < viewState.libraryWatchMinMinutes * 60000) return false; if (viewState.libraryCurrentOnlineMinMinutes && (status !== 'live' || Number(model.secondsOnline || 0) < viewState.libraryCurrentOnlineMinMinutes * 60)) return false; if (viewState.librarySessionsMin && item.sessions < viewState.librarySessionsMin) return false; if (viewState.libraryRatingMin && Number(room?.rating || 0) < viewState.libraryRatingMin) return false; if (viewState.libraryPriorityMin && Number(room?.priority || 0) < viewState.libraryPriorityMin) return false; if (viewState.libraryAppearancesMin && Number(model.appearances || 0) < viewState.libraryAppearancesMin) return false; if (viewState.libraryOnlineSamplesMin && Number(model.onlineSamples || 0) < viewState.libraryOnlineSamplesMin) return false; if (viewState.libraryActivityDays && (!item.lastActivity || item.lastActivity < Date.now() - viewState.libraryActivityDays * 86400000)) return false; if (viewState.libraryOrganization === 'organized' && !room) return false; if (viewState.libraryOrganization === 'catalog' && room) return false; if (viewState.libraryProfileState === 'loaded' && !item.profile) return false; if (viewState.libraryProfileState === 'missing' && item.profile) return false; if (viewState.libraryWatchState === 'watched' && item.totalMs <= 0) return false; if (viewState.libraryWatchState === 'unwatched' && item.totalMs > 0) return false; if (viewState.libraryOwnTagsState === 'with' && !ownTags.length) return false; if (viewState.libraryOwnTagsState === 'without' && ownTags.length) return false; if (viewState.libraryGroupsState === 'with' && !groups.length) return false; if (viewState.libraryGroupsState === 'without' && groups.length) return false; if (viewState.libraryNotesState === 'with' && !hasNotes) return false; if (viewState.libraryNotesState === 'without' && hasNotes) return false; const isHd = !!model.isHd; const isGaming = !!model.isGaming; if (viewState.libraryFeature === 'hd' && !isHd) return false; if (viewState.libraryFeature === 'gaming' && !isGaming) return false; if (viewState.libraryFeature === 'both' && (!isHd || !isGaming)) return false; if (viewState.libraryFeature === 'neither' && (isHd || isGaming)) return false; if (viewState.libraryOnlineHour >= 0 || viewState.libraryOnlineWeekday >= 0) { const observed = Array.from({ length: 168 }, (_, index) => Math.max(0, Number(model.onlineHours?.[index]) || 0)); const fallback = Array.from({ length: 168 }, (_, index) => Math.max(0, Number(model.hours?.[index]) || 0)); const weights = observed.some(Boolean) ? observed : fallback; const matchesTime = weights.some((weight, index) => weight > 0 && (viewState.libraryOnlineWeekday < 0 || Math.floor(index / 24) === viewState.libraryOnlineWeekday) && (viewState.libraryOnlineHour < 0 || index % 24 === viewState.libraryOnlineHour)); if (!matchesTime) return false; } return true; } function libraryActiveFilterCount() { let count = Object.entries(LIBRARY_FILTER_DEFAULTS).reduce((total, [key, fallback]) => { const value = viewState[key]; if (Array.isArray(fallback)) return total + (Array.isArray(value) && value.length ? 1 : 0); return total + (value !== fallback ? 1 : 0); }, 0); if (viewState.libraryMode !== 'active') count += 1; if (viewState.libraryGroups.length) count += 1; if (viewState.libraryTags.length) count += 1; if (viewState.query) count += 1; return count; } function libraryMultiPicker(label, key, options, placeholder) { const selected = Array.isArray(viewState[key]) ? viewState[key] : []; const optionMap = new Map(options.map(option => [option.value, option])); const statsPicker = key.startsWith('stats'); const commit = values => { viewState[key] = [...new Set(values)].slice(0, 80); if (statsPicker) viewState.page = 1; else viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; const available = options.filter(option => !selected.includes(option.value)); const select = $('select', { value: '', ariaLabel: label, onchange: event => { if (event.target.value) commit([...selected, event.target.value]); } }, [ $('option', { value: '' }, placeholder), ...available.map(option => $('option', { value: option.value }, `${option.label} · ${compactNumber(option.count)}`)), ]); return $('div', { class: 'rgh-filter-picker', dataset: statsPicker ? { roomgridStatsPicker: key } : { roomgridLibraryPicker: key } }, [ $('strong', {}, label), $('div', { class: 'rgh-filter-picker-row' }, [select, action(tx('清空', 'Clear', 'Leeren'), () => commit([]), selected.length ? '' : 'disabled')]), $('div', { class: 'rgh-filter-chips' }, selected.length ? selected.map(value => { const option = optionMap.get(value); return $('button', { class: 'rgh-filter-chip', type: 'button', title: tx(`删除筛选器 ${option?.label || value}`, `Remove filter ${option?.label || value}`, `Filter ${option?.label || value} entfernen`), onclick: () => commit(selected.filter(item => item !== value)) }, `${option?.label || value} ×`); }) : [$('span', { class: 'rgh-filter-empty' }, tx('没有选择——显示所有', 'Nothing selected — show all', 'Nichts gewählt – alle anzeigen'))]), ]); } function libraryNumberControl(label, key, maximum = 1000000000, suffix = '') { const value = Number(viewState[key]) || 0; const statsControl = key.startsWith('stats'); const input = $('input', { type: 'number', min: '0', max: String(maximum), step: '1', value: value ? String(value) : '', placeholder: tx('任意', 'Any', 'Beliebig'), onchange: event => { viewState[key] = clampInt(event.target.value, 0, maximum, 0); if (statsControl) viewState.page = 1; else viewState.libraryPage = 1; savePrefs(); scheduleRender(); }, }); return $('label', { class: 'rgh-control' }, [$('span', {}, `${label}${suffix ? ` (${suffix})` : ''}`), input]); } function libraryAdvancedFilterPanel(data, matchingCount, facets) { const commitArrayToggle = (key, value) => { const current = Array.isArray(viewState[key]) ? viewState[key] : []; viewState[key] = current.includes(value) ? current.filter(item => item !== value) : [...current, value]; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; const genderLabels = { f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans', unknown: tx('未知', 'Unknown', 'Unbekannt'), }; const statusLabels = { live: 'Live', offline: 'Offline', private: tx('私人', 'Private', 'Privat'), unknown: tx('未知', 'Unknown', 'Unbekannt'), error: tx('错误/加载中', 'Error/loading', 'Fehler/Laden'), }; const { genderCounts, statusCounts, countries, publicTags } = facets; const setSelect = (key, value) => { viewState[key] = value; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }; const weekdayOptions = [ [-1, tx('所有星期', 'Every weekday', 'Alle Wochentage')], [0, tx('星期日', 'Sunday', 'Sonntag')], [1, tx('星期一', 'Monday', 'Montag')], [2, tx('星期二', 'Tuesday', 'Dienstag')], [3, tx('星期三', 'Wednesday', 'Mittwoch')], [4, tx('星期四', 'Thursday', 'Donnerstag')], [5, tx('星期五', 'Friday', 'Freitag')], [6, tx('星期六', 'Saturday', 'Samstag')], ]; const hourOptions = [[-1, tx('所有时间', 'Every time', 'Alle Uhrzeiten')], ...Array.from({ length: 24 }, (_, hour) => [hour, `${String(hour).padStart(2, '0')}:00–${String((hour + 1) % 24).padStart(2, '0')}:00`])]; const reset = () => { Object.entries(LIBRARY_FILTER_DEFAULTS).forEach(([key, value]) => { viewState[key] = Array.isArray(value) ? [] : value; }); viewState.libraryGroups = []; viewState.libraryTags = []; viewState.libraryMode = 'active'; viewState.query = ''; viewState.libraryPage = 1; savePrefs(); scheduleRender(true); }; const toggleBlock = (label, key, labels, counts) => $('div', { class: 'rgh-filter-toggle-block' }, [ $('strong', {}, label), $('div', { class: 'rgh-filter-toggle-list' }, [ $('button', { class: `rgh-filter-toggle${viewState[key].length ? '' : ' is-active'}`, type: 'button', onclick: () => { viewState[key] = []; viewState.libraryPage = 1; savePrefs(); scheduleRender(); } }, [tx('全部', 'All', 'Alle'), $('b', {}, compactNumber(data.items.length))]), ...Object.entries(labels).map(([value, text]) => $('button', { class: `rgh-filter-toggle${viewState[key].includes(value) ? ' is-active' : ''}`, type: 'button', ariaPressed: String(viewState[key].includes(value)), onclick: () => commitArrayToggle(key, value) }, [text, $('b', {}, compactNumber(counts[value] || 0))])), ]), ]); return $('section', { class: 'rgh-section rgh-library-filter-center', dataset: { roomgridLibraryFilters: '1' } }, [ $('div', { class: 'rgh-section-head' }, [ $('h2', {}, tx('高级筛选和排序', 'Advanced filters and sorting', 'Erweiterte Filter und Sortierung')), $('div', { class: 'rgh-filter-result' }, [ $('b', {}, tx(`${compactNumber(matchingCount)} 个匹配`, `${compactNumber(matchingCount)} matches`, `${compactNumber(matchingCount)} Treffer`)), $('span', { class: 'rgh-private' }, tx(`${libraryActiveFilterCount()} 个筛选器启用`, `${libraryActiveFilterCount()} filters active`, `${libraryActiveFilterCount()} Filter aktiv`)), ]), ]), $('p', { class: 'rgh-bio' }, tx('组合任意数量的筛选器。一个区域内的多个性别、状态、国家或标签以多选方式工作。所有设置都永久保存在此浏览器中。', 'Combine as many filters as you like. Multiple genders, states, countries and tags work as multi-selections. Every setting stays saved in this browser.', 'Kombiniere beliebig viele Filter. Mehrere Geschlechter, Status, Länder und Tags funktionieren als Mehrfachauswahl. Jede Einstellung bleibt in diesem Browser gespeichert.')), toggleBlock(tx('性别/类型', 'Gender/type', 'Geschlecht/Typ'), 'libraryGenders', genderLabels, genderCounts), toggleBlock(tx('直播状态', 'Live status', 'Live-Status'), 'libraryStatuses', statusLabels, statusCounts), $('div', { class: 'rgh-filter-pickers' }, [ libraryMultiPicker(tx('国家/地区(多选)', 'Countries/regions (multiple)', 'Länder/Regionen (mehrfach)'), 'libraryCountries', countries, tx('添加国家…', 'Add country…', 'Land hinzufügen …')), libraryMultiPicker(tx('公开标签(多选)', 'Public tags (multiple)', 'Öffentliche Tags (mehrfach)'), 'libraryPublicTags', publicTags, tx('添加标签…', 'Add tag…', 'Tag hinzufügen …')), ]), $('div', { class: 'rgh-filter-grid' }, [ selectControl(tx('公开标签组合', 'Public-tag matching', 'Verknüpfung öffentlicher Tags'), viewState.libraryPublicTagMode, [['any', tx('至少一个标签', 'At least one tag', 'Mindestens ein Tag')], ['all', tx('所有选定标签', 'Every selected tag', 'Alle gewählten Tags')]], value => setSelect('libraryPublicTagMode', value)), selectControl(tx('目录来源', 'Catalog source', 'Katalogquelle'), viewState.libraryOrganization, [['all', tx('全部模型', 'All models', 'Alle Models')], ['organized', tx('仅个人整理', 'Personally organized only', 'Nur persönlich organisiert')], ['catalog', tx('仅自动发现', 'Automatically discovered only', 'Nur automatisch erfasst')]], value => setSelect('libraryOrganization', value)), selectControl(tx('资料数据', 'Profile data', 'Profildaten'), viewState.libraryProfileState, [['all', tx('有或没有资料', 'Loaded or missing', 'Geladen oder nicht')], ['loaded', tx('仅已加载', 'Loaded only', 'Nur geladen')], ['missing', tx('仅尚未加载', 'Missing only', 'Nur noch nicht geladen')]], value => setSelect('libraryProfileState', value)), selectControl(tx('画面/类别', 'Picture/category', 'Bild/Kategorie'), viewState.libraryFeature, [['all', tx('全部', 'All', 'Alle')], ['hd', 'HD'], ['gaming', 'Gaming'], ['both', 'HD + Gaming'], ['neither', tx('既非 HD 也非游戏', 'Neither HD nor gaming', 'Weder HD noch Gaming')]], value => setSelect('libraryFeature', value)), selectControl(tx('观看历史', 'Watch history', 'Sehverlauf'), viewState.libraryWatchState, [['all', tx('全部', 'All', 'Alle')], ['watched', tx('已经看过', 'Watched', 'Bereits angesehen')], ['unwatched', tx('从未看过', 'Never watched', 'Noch nie angesehen')]], value => setSelect('libraryWatchState', value)), selectControl(tx('自己的标签', 'Own tags', 'Eigene Tags'), viewState.libraryOwnTagsState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('仅带有标签', 'With tags only', 'Nur mit Tags')], ['without', tx('仅不带标签', 'Without tags only', 'Nur ohne Tags')]], value => setSelect('libraryOwnTagsState', value)), selectControl(tx('组分配', 'Group assignment', 'Gruppenzuordnung'), viewState.libraryGroupsState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('至少一个组', 'At least one group', 'Mindestens eine Gruppe')], ['without', tx('没有组', 'No group', 'Ohne Gruppe')]], value => setSelect('libraryGroupsState', value)), selectControl(tx('私人备注', 'Private notes', 'Private Notizen'), viewState.libraryNotesState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('仅带备注', 'With notes only', 'Nur mit Notiz')], ['without', tx('仅不带备注', 'Without notes only', 'Nur ohne Notiz')]], value => setSelect('libraryNotesState', value)), ]), $('h3', {}, tx('数值范围和最低值', 'Numeric ranges and minimums', 'Zahlenbereiche und Mindestwerte')), $('div', { class: 'rgh-filter-grid' }, [ libraryNumberControl(tx('最低年龄', 'Minimum age', 'Mindestalter'), 'libraryAgeMin', 99), libraryNumberControl(tx('最高年龄', 'Maximum age', 'Höchstalter'), 'libraryAgeMax', 99), libraryNumberControl(tx('最少观众', 'Minimum viewers', 'Mindestens Zuschauer'), 'libraryViewerMin'), libraryNumberControl(tx('最多观众', 'Maximum viewers', 'Höchstens Zuschauer'), 'libraryViewerMax'), libraryNumberControl(tx('最少关注者', 'Minimum followers', 'Mindestens Follower'), 'libraryFollowersMin'), libraryNumberControl(tx('最多关注者', 'Maximum followers', 'Höchstens Follower'), 'libraryFollowersMax'), libraryNumberControl(tx('最低总观看时间', 'Minimum total watch time', 'Mindest-Gesamtsehzeit'), 'libraryWatchMinMinutes', 1000000000, 'min'), libraryNumberControl(tx('当前至少在线', 'Currently online for at least', 'Aktuell mindestens online'), 'libraryCurrentOnlineMinMinutes', 1000000000, 'min'), libraryNumberControl(tx('最少会话', 'Minimum sessions', 'Mindestens Sitzungen'), 'librarySessionsMin'), libraryNumberControl(tx('最低评分', 'Minimum rating', 'Mindestbewertung'), 'libraryRatingMin', 5, '1–5'), libraryNumberControl(tx('最低优先级', 'Minimum priority', 'Mindestpriorität'), 'libraryPriorityMin', 5, '1–5'), libraryNumberControl(tx('最近活跃', 'Active within', 'Aktiv innerhalb'), 'libraryActivityDays', 36500, tx('天', 'days', 'Tagen')), libraryNumberControl(tx('最少出现次数', 'Minimum appearances', 'Mindestens aufgetaucht'), 'libraryAppearancesMin'), libraryNumberControl(tx('最少在线观察', 'Minimum live observations', 'Mindestens Live-Beobachtungen'), 'libraryOnlineSamplesMin'), ]), $('h3', {}, tx('根据通常在线时间筛选', 'Filter by typical online time', 'Nach typischer Onlinezeit filtern')), $('div', { class: 'rgh-filter-grid' }, [ selectControl(tx('星期', 'Weekday', 'Wochentag'), viewState.libraryOnlineWeekday, weekdayOptions, value => setSelect('libraryOnlineWeekday', Number(value))), selectControl(tx('小时(本地时间)', 'Hour (local time)', 'Stunde (Ortszeit)'), viewState.libraryOnlineHour, hourOptions, value => setSelect('libraryOnlineHour', Number(value))), ]), $('div', { class: 'rgh-filter-actions' }, [ $('p', { class: 'rgh-filter-note' }, tx('零或空字段表示没有限制。未知年龄在使用年龄限制时被排除。时间筛选使用在此设备上观察到的直播时间;如果尚无此数据,则使用观看时间作为谨慎估计。', 'Zero or empty fields mean no limit. Unknown ages are excluded while an age limit is active. Time filters use live times observed on this device, falling back cautiously to watch times when needed.', 'Null oder leere Felder bedeuten keine Grenze. Unbekanntes Alter wird bei aktiver Altersgrenze ausgeschlossen. Zeitfilter nutzen auf diesem Gerät beobachtete Livezeiten und ersatzweise vorsichtig die Sehzeiten.')), action(tx('重置所有资料库筛选器', 'Reset all library filters', 'Alle Bibliotheksfilter zurücksetzen'), reset, libraryActiveFilterCount() ? 'danger' : 'disabled'), ]), ]); } function libraryCard(item, getTagSuggestions, customGroups) { const room = item.room || { id: item.id, notes: '', localTags: [], rating: 0, priority: 0, watchlist: false, archived: false, groups: [] }; const rememberDraft = fields => libraryDrafts.set(item.id, { ...libraryDrafts.get(item.id), ...fields }); const roomHref = roomGridSingleCamUrl(item.id, { source: 'library' }); const roomIsLive = libraryStatusBucket(item) === 'live'; const roomLinkLabel = roomIsLive ? tx('进入直播间', 'Open live room', '▶ Live-Raum öffnen', 'Abrir sala en vivo', 'Abrir sala ao vivo') : tx('打开房间页面', 'Open room page', 'Raumseite öffnen', 'Abrir página de la sala', 'Abrir página da sala'); const ensureOrganizedRoom = () => { let current = activeStore.state.rooms.find(entry => entry.id === item.id); if (!current) { activeStore.addRoom(item.id); current = activeStore.state.rooms.find(entry => entry.id === item.id); } return current; }; const groups = customGroups.filter(group => roomInGroup(room, group.id)); const estimate = viewState.libraryShowOnlineEstimate ? onlinePattern(item) : null; const publicTags = [...new Set([...(item.model.tags || []), ...(item.profile?.tags || [])])].filter(tag => !(room.localTags || []).includes(tag)).slice(0, 20); const buildEditor = () => { const draft = libraryDrafts.get(item.id) || {}; const notes = $('textarea', { value: draft.notes ?? room.notes ?? '', rows: 3, dataset: { libraryDraftField: 'notes' }, placeholder: tx('私人备注', 'Private notes', 'Private Notizen'), oninput: event => rememberDraft({ notes: event.target.value }) }); const tagEditor = viewState.libraryShowEditor ? createTagEditor({ ...room, localTags: draft.localTags ?? room.localTags }, getTagSuggestions(), draft.tagInput || '', rememberDraft) : null; const priority = $('select', { dataset: { libraryDraftField: 'priority' }, onchange: event => rememberDraft({ priority: Number(event.target.value) || 0 }) }, [0, 1, 2, 3, 4, 5].map(value => $('option', { value: String(value), selected: Number(draft.priority ?? room.priority ?? 0) === value }, value ? `Priorität ${value}` : 'Keine Priorität'))); const save = () => { const localTags = tagEditor.values(); if (!localTags) return; RoomGridUx.undoable(activeStore, tx(`@${item.id} 资料已保存`, `@${item.id} profile saved`, `Profil von @${item.id} gespeichert`), () => { if (ensureOrganizedRoom()) { activeStore.patchRoom(item.id, { notes: notes.value, localTags, priority: Number(priority.value) || 0 }); libraryDrafts.delete(item.id); } }, { onUndo: () => scheduleRender() }); }; return [ viewState.libraryShowEditor ? $('section', { class: 'rgh-profile-editor' }, [ $('label', { class: 'rgh-note-editor' }, [$('span', {}, 'Private Notiz'), notes]), $('label', { class: 'rgh-priority-editor' }, [$('span', {}, 'Eigene Priorität'), priority]), tagEditor.element, ]) : null, viewState.libraryShowRating ? $('div', { class: 'rgh-stars', ariaLabel: tx('本地评分', 'Local rating', 'Lokale Bewertung') }, [1, 2, 3, 4, 5].map(value => $('button', { type: 'button', class: Number(room.rating || 0) >= value ? 'on' : '', onclick: () => { const current = ensureOrganizedRoom(); if (current) activeStore.patchRoom(item.id, { rating: Number(current.rating || 0) === value ? 0 : value }); }, title: `${value} Sterne` }, '★'))) : null, viewState.libraryShowGroupAssignment ? groupPicker(item, customGroups) : null, viewState.libraryShowActions ? modelActions(item, true) : null, viewState.libraryShowEditor ? $('div', { class: 'rgh-actions' }, [action(tx('保存资料', 'Save profile', 'Profil speichern'), save, 'primary'), action(room.archived ? tx('恢复', 'Restore', 'Wiederherstellen') : tx('归档', 'Archive', 'Archivieren'), () => { const archive = !room.archived; RoomGridUx.undoable(activeStore, archive ? tx(`@${item.id} 已归档`, `@${item.id} archived`, `@${item.id} archiviert`) : tx(`@${item.id} 已恢复`, `@${item.id} restored`, `@${item.id} wiederhergestellt`), () => { if (ensureOrganizedRoom()) activeStore.patchRoom(item.id, { archived: archive }); }, { onUndo: () => scheduleRender() }); })]) : null, ].filter(Boolean); }; const editorBody = $('div'); const editor = $('details', { dataset: { libraryEditor: item.id }, open: libraryEditorId === item.id }, [ $('summary', { class: 'rgh-action' }, tx('编辑', 'Edit', 'Bearbeiten')), editorBody, ]); if (editor.open) editorBody.append(...buildEditor()); editor.addEventListener('toggle', () => { if (!editor.isConnected) return; if (!editor.open) { if (libraryEditorId === item.id) libraryEditorId = ''; editorBody.replaceChildren(); return; } libraryEditorId = item.id; for (const other of content.querySelectorAll('[data-library-editor][open]')) { if (other !== editor) other.open = false; } if (!editorBody.childElementCount) editorBody.append(...buildEditor()); }); const genderLabel = ({ f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans' })[String(item.model.gender || '').toLocaleLowerCase()] || '—'; const statEntries = [ [viewState.libraryShowStatus, tx('状态', 'Status', 'Status'), statusLabel(item.status)], [viewState.libraryShowViewers, tx('观众数', 'Viewer count', 'Zuschauerzahl'), item.model.viewerCount ? compactNumber(item.model.viewerCount) : '—'], [viewState.libraryShowCountry, tx('国家/地区', 'Country/region', 'Land/Region'), item.model.country || '—'], [viewState.libraryShowLocation, tx('地点', 'Location', 'Ort'), item.model.location || '—'], [viewState.libraryShowAge, tx('年龄', 'Age', 'Alter'), item.model.age || '—'], [viewState.libraryShowType, tx('类型', 'Type', 'Typ'), genderLabel], [viewState.libraryShowWatchTime, tx('总观看时间', 'Total watch time', 'Gesamte Sehzeit'), duration(item.totalMs)], [viewState.libraryShowSessions, tx('会话', 'Sessions', 'Sitzungen'), compactNumber(item.sessions)], [viewState.libraryShowAverageSession, tx('平均会话', 'Avg. session', 'Ø Sitzung'), duration(item.avgSessionMs)], [viewState.libraryShowLongestSession, tx('最长会话', 'Longest session', 'Längste Sitzung'), duration(item.model.longestSessionMs)], [viewState.libraryShowActiveDays, tx('活跃天数', 'Active days', 'Aktive Tage'), compactNumber(item.activeDays)], [viewState.libraryShowFirstSeen, tx('首次观看', 'First seen', 'Erstmals gesehen'), dateOnly(item.model.firstWatchedAt)], [viewState.libraryShowLastSeen, tx('上次观看', 'Last seen', 'Zuletzt gesehen'), dateOnly(item.model.lastWatchedAt)], [viewState.libraryShowLastLive, tx('上次直播', 'Last live', 'Zuletzt live'), dateTime(item.model.lastOnlineAt || room.lastSeenOnline)], [viewState.libraryShowPreviews, tx('预览', 'Previews', 'Vorschauen'), compactNumber(item.counts.preview || 0)], [viewState.libraryShowQuickViews, tx('快速信息', 'Quick views', 'Schnellinfos'), compactNumber(item.counts.quickview || 0)], ].filter(([enabled]) => enabled); return $('article', { class: `rgh-card${viewState.libraryShowImage ? '' : ' no-media'}`, dataset: { roomgridLibraryModel: item.id } }, [ viewState.libraryShowImage ? modelMedia(item) : null, $('div', { class: 'rgh-card-body' }, [ $('div', { class: 'rgh-card-title' }, [$('h2', {}, $('a', { class: 'rgh-model-room-title', href: roomHref, target: '_blank', rel: 'noopener noreferrer', title: tx('在新标签页中打开模型房间', 'Open model room in a new tab', 'Model-Raum in neuem Tab öffnen') }, item.profile?.displayName || item.id)), isFavorite(room) ? $('span', { class: 'rgh-fav' }, '★') : null]), viewState.libraryShowActions ? $('div', { class: 'rgh-library-quick-actions' }, [ $('a', { class: `rgh-action primary rgh-room-direct${roomIsLive ? ' is-live' : ''}`, href: roomHref, target: '_blank', rel: 'noopener noreferrer', dataset: { roomgridOpenRoom: item.id }, ariaLabel: `${roomLinkLabel}: ${item.id}`, title: tx('在新标签页中打开 Chaturbate 单模型视图', 'Open the Chaturbate single-model view in a new tab', 'Chaturbate-Einzelansicht in neuem Tab öffnen') }, roomLinkLabel), $('button', { type: 'button', class: 'rgh-action', ariaPressed: String(isFavorite(room)), onclick: () => { if (ensureOrganizedRoom()) activeStore.toggleRoomInGroup(item.id, FAVORITE_GROUP_ID); } }, tx('收藏', 'Favorite', 'Favorit')), ]) : null, viewState.libraryShowEditor || viewState.libraryShowRating || viewState.libraryShowGroupAssignment || viewState.libraryShowActions ? editor : null, viewState.libraryShowBio ? $('div', { class: 'rgh-bio' }, item.profile?.bio || item.model.subjects?.at?.(-1) || tx(`@${item.id} · 可通过“所有详细信息”加载公开个人资料。`, `@${item.id} · Public profile information can be loaded through “All details”.`, `@${item.id} · Öffentliche Profilinformationen können über „Alle Details“ geladen werden.`, `@${item.id} · La información pública del perfil se puede cargar mediante «Todos los detalles».`, `@${item.id} · As informações públicas do perfil podem ser carregadas em “Todos os detalhes”.`)) : null, viewState.libraryShowBio ? publicProfileExtras(item) : null, viewState.libraryShowCatalogBadge ? $('span', { class: 'rgh-catalog-badge' }, item.room ? 'Persönlich organisiert' : 'Automatisch erfasst · beim Bearbeiten vollständig organisiert') : null, viewState.libraryShowActivityDetails || viewState.libraryShowCatalogBadge ? lineageRow(item) : null, statEntries.length ? $('div', { class: 'rgh-statgrid' }, statEntries.map(([, label, value]) => $('div', { class: 'rgh-stat' }, [$('small', {}, label), $('strong', {}, String(value ?? '—'))]))) : null, viewState.libraryShowOnlineEstimate ? $('section', { class: 'rgh-online-estimate' }, [ $('div', {}, [$('small', {}, 'Ungefähre Onlinezeiten'), $('strong', {}, estimate.label)]), $('span', {}, `${estimate.confidence} · ${estimate.note}`), ]) : null, viewState.libraryShowActivityDetails ? $('div', { class: 'rgh-meta' }, [ $('span', {}, item.room ? tx(`个人整理自:${dateTime(room.addedAt)}`, `Personally organized since: ${dateTime(room.addedAt)}`, `Persönlich organisiert seit: ${dateTime(room.addedAt)}`, `Organizado personalmente desde: ${dateTime(room.addedAt)}`, `Organizado pessoalmente desde: ${dateTime(room.addedAt)}`) : tx(`自动加入目录自:${dateTime(item.model.firstObservedAt)}`, `Automatically cataloged since: ${dateTime(item.model.firstObservedAt)}`, `Automatisch im Katalog seit: ${dateTime(item.model.firstObservedAt)}`, `Catalogado automáticamente desde: ${dateTime(item.model.firstObservedAt)}`, `Catalogado automaticamente desde: ${dateTime(item.model.firstObservedAt)}`)), $('span', {}, tx(`最近活动:${dateTime(item.lastActivity)}`, `Last activity: ${dateTime(item.lastActivity)}`, `Letzte Aktivität: ${dateTime(item.lastActivity)}`, `Última actividad: ${dateTime(item.lastActivity)}`, `Última atividade: ${dateTime(item.lastActivity)}`)), $('span', {}, tx(`出现:${compactNumber(item.model.appearances || 0)}× · 最近记录 ${dateTime(item.model.lastObservedAt)}`, `Appearances: ${compactNumber(item.model.appearances || 0)}× · last recorded ${dateTime(item.model.lastObservedAt)}`, `Aufgetaucht: ${compactNumber(item.model.appearances || 0)}× · zuletzt erfasst ${dateTime(item.model.lastObservedAt)}`, `Apariciones: ${compactNumber(item.model.appearances || 0)}× · último registro ${dateTime(item.model.lastObservedAt)}`, `Aparições: ${compactNumber(item.model.appearances || 0)}× · último registro ${dateTime(item.model.lastObservedAt)}`)), item.model.sources?.length ? $('span', {}, `${tx('数据来源', 'Data sources', 'Datenquellen')}: ${item.model.sources.slice(-4).map(source => RoomGridDataProvenance.sourceInfo(source).label).join(' · ')}`) : null, $('span', {}, tx(`已发现:${compactNumber(item.counts.discovered || 0)} · 质量测量:${compactNumber(item.samples.length)}`, `Discovered: ${compactNumber(item.counts.discovered || 0)} · quality measurements: ${compactNumber(item.samples.length)}`, `Entdeckt: ${compactNumber(item.counts.discovered || 0)} · Qualitätsmessungen: ${compactNumber(item.samples.length)}`, `Descubiertos: ${compactNumber(item.counts.discovered || 0)} · mediciones de calidad: ${compactNumber(item.samples.length)}`, `Descobertos: ${compactNumber(item.counts.discovered || 0)} · medições de qualidade: ${compactNumber(item.samples.length)}`)), $('span', {}, tx( `个人资料信息:${item.profile ? `加载于 ${dateOnly(item.profile.observedAt)}` : '尚未加载'}`, `Profile information: ${item.profile ? `loaded on ${dateOnly(item.profile.observedAt)}` : 'not loaded yet'}`, `Profilinformationen: ${item.profile ? `geladen am ${dateOnly(item.profile.observedAt)}` : 'noch nicht geladen'}`, `Información del perfil: ${item.profile ? `cargada el ${dateOnly(item.profile.observedAt)}` : 'aún no cargada'}`, `Informações do perfil: ${item.profile ? `carregadas em ${dateOnly(item.profile.observedAt)}` : 'ainda não carregadas'}`, )), ]) : null, viewState.libraryShowExampleGallery ? modelExampleGallery(item) : null, viewState.libraryShowGroups && groups.length ? $('div', { class: 'rgh-groups' }, groups.map(group => $('span', { class: 'rgh-group' }, groupLabel(group)))) : null, viewState.libraryShowPublicTags && publicTags.length ? $('div', { class: 'rgh-public-tags' }, [$('small', {}, 'Öffentliche/erkannte Tags'), $('div', { class: 'rgh-tags' }, publicTags.map(tag => $('span', { class: 'rgh-tag' }, `#${tag}`)))]) : null, ]), ]); } function statsActiveFilterCount() { let count = Object.entries(STATS_FILTER_DEFAULTS).reduce((total, [key, fallback]) => { const value = viewState[key]; if (Array.isArray(fallback)) return total + (Array.isArray(value) && value.length ? 1 : 0); return total + (value !== fallback ? 1 : 0); }, 0); if (viewState.status !== 'all') count += 1; if (viewState.savedOnly) count += 1; if (viewState.query) count += 1; return count; } function matchesStatsAdvancedFilters(item) { const model = item.model || {}; const room = item.room || null; const gender = ['f', 'm', 'c', 't'].includes(String(model.gender || '')) ? String(model.gender) : 'unknown'; const country = String(model.country || '').trim(); const age = Math.max(0, Number(model.age) || 0); const viewers = Math.max(0, Number(model.viewerCount) || 0); const followers = Math.max(0, Number(model.followers) || 0); const groups = getRoomGroups(room); const ownTags = room?.localTags || []; const hasNotes = !!String(room?.notes || '').trim(); const hasSavedMedia = !!(model.previewUrls || []).filter(Boolean).length; if (viewState.statsGenders.length && !viewState.statsGenders.includes(gender)) return false; if (viewState.statsCountries.length && !viewState.statsCountries.includes(country)) return false; if (viewState.statsPublicTags.length) { const publicTags = new Set(publicTagsForLibraryItem(item)); const matches = viewState.statsPublicTags.map(tag => publicTags.has(tag)); if (viewState.statsPublicTagMode === 'all' ? !matches.every(Boolean) : !matches.some(Boolean)) return false; } if (viewState.statsGroups.length) { const matches = viewState.statsGroups.map(id => groups.includes(id)); if (viewState.statsGroupMode === 'all' ? !matches.every(Boolean) : !matches.some(Boolean)) return false; } if ((viewState.statsAgeMin || viewState.statsAgeMax) && !age) return false; if (viewState.statsAgeMin && age < viewState.statsAgeMin) return false; if (viewState.statsAgeMax && age > viewState.statsAgeMax) return false; if (viewState.statsViewerMin && viewers < viewState.statsViewerMin) return false; if (viewState.statsViewerMax && viewers > viewState.statsViewerMax) return false; if (viewState.statsFollowersMin && followers < viewState.statsFollowersMin) return false; if (viewState.statsFollowersMax && followers > viewState.statsFollowersMax) return false; if (viewState.statsWatchMinMinutes && item.totalMs < viewState.statsWatchMinMinutes * 60000) return false; if (viewState.statsWatchMaxMinutes && item.totalMs > viewState.statsWatchMaxMinutes * 60000) return false; if (viewState.statsSessionsMin && item.sessions < viewState.statsSessionsMin) return false; if (viewState.statsSessionsMax && item.sessions > viewState.statsSessionsMax) return false; if (viewState.statsActiveDaysMin && item.activeDays < viewState.statsActiveDaysMin) return false; if (viewState.statsPreviewsMin && Number(item.counts.preview || 0) < viewState.statsPreviewsMin) return false; if (viewState.statsQuickViewsMin && Number(item.counts.quickview || 0) < viewState.statsQuickViewsMin) return false; if (viewState.statsRatingMin && Number(room?.rating || 0) < viewState.statsRatingMin) return false; if (viewState.statsPriorityMin && Number(room?.priority || 0) < viewState.statsPriorityMin) return false; if (viewState.statsAppearancesMin && Number(model.appearances || 0) < viewState.statsAppearancesMin) return false; if (viewState.statsOnlineSamplesMin && Number(model.onlineSamples || 0) < viewState.statsOnlineSamplesMin) return false; if (viewState.statsActivityDays && (!item.lastActivity || item.lastActivity < Date.now() - viewState.statsActivityDays * 86400000)) return false; if (viewState.statsOrganization === 'organized' && !room) return false; if (viewState.statsOrganization === 'catalog' && room) return false; if (viewState.statsProfileState === 'loaded' && !item.profile) return false; if (viewState.statsProfileState === 'missing' && item.profile) return false; if (viewState.statsWatchState === 'watched' && item.totalMs <= 0) return false; if (viewState.statsWatchState === 'unwatched' && item.totalMs > 0) return false; if (viewState.statsMediaState === 'saved' && !hasSavedMedia) return false; if (viewState.statsMediaState === 'missing' && hasSavedMedia) return false; if (viewState.statsMediaState === 'live' && libraryStatusBucket(item) !== 'live') return false; if (viewState.statsOwnTagsState === 'with' && !ownTags.length) return false; if (viewState.statsOwnTagsState === 'without' && ownTags.length) return false; if (viewState.statsGroupsState === 'with' && !groups.length) return false; if (viewState.statsGroupsState === 'without' && groups.length) return false; if (viewState.statsNotesState === 'with' && !hasNotes) return false; if (viewState.statsNotesState === 'without' && hasNotes) return false; const isHd = !!model.isHd; const isGaming = !!model.isGaming; if (viewState.statsFeature === 'hd' && !isHd) return false; if (viewState.statsFeature === 'gaming' && !isGaming) return false; if (viewState.statsFeature === 'both' && (!isHd || !isGaming)) return false; if (viewState.statsFeature === 'neither' && (isHd || isGaming)) return false; return true; } function statsAdvancedFilterPanel(data, matchingCount, facets) { const genderLabels = { f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans', unknown: tx('未知', 'Unknown', 'Unbekannt') }; const { genderCounts, countries, publicTags, groupCounts } = facets; const groups = customLibraryGroups().map(group => ({ value: group.id, label: groupLabel(group), count: groupCounts.get(group.id) || 0 })); const commit = (key, value) => { viewState[key] = value; viewState.page = 1; savePrefs(); scheduleRender(); }; const toggleGender = value => commit('statsGenders', viewState.statsGenders.includes(value) ? viewState.statsGenders.filter(item => item !== value) : [...viewState.statsGenders, value]); const reset = () => { Object.entries(STATS_FILTER_DEFAULTS).forEach(([key, value]) => { viewState[key] = Array.isArray(value) ? [] : value; }); viewState.status = 'all'; viewState.savedOnly = false; viewState.query = ''; viewState.page = 1; savePrefs(); scheduleRender(true); }; return $('section', { class: 'rgh-section rgh-library-filter-center rgh-stats-filter-center', dataset: { roomgridStatsFilters: '1' } }, [ $('div', { class: 'rgh-section-head' }, [ $('h2', {}, tx('统计高级筛选', 'Advanced statistics filters', 'Erweiterte Statistikfilter')), $('div', { class: 'rgh-filter-result' }, [ $('b', {}, tx(`${compactNumber(matchingCount)} 个匹配`, `${compactNumber(matchingCount)} matches`, `${compactNumber(matchingCount)} Treffer`)), $('span', { class: 'rgh-private' }, tx(`${statsActiveFilterCount()} 个筛选器启用`, `${statsActiveFilterCount()} filters active`, `${statsActiveFilterCount()} Filter aktiv`)), ]), ]), $('p', { class: 'rgh-bio' }, tx('所有筛选器可以组合,多个国家、标签、群组和类型支持多选。选择会永久保存。', 'Every filter can be combined; countries, tags, groups and types support multiple selection. Choices are saved permanently.', 'Alle Filter sind kombinierbar; Länder, Tags, Gruppen und Typen erlauben Mehrfachauswahl. Die Auswahl bleibt dauerhaft gespeichert.')), $('div', { class: 'rgh-filter-toggle-block' }, [ $('strong', {}, tx('性别/类型', 'Gender/type', 'Geschlecht/Typ')), $('div', { class: 'rgh-filter-toggle-list' }, [ $('button', { class: `rgh-filter-toggle${viewState.statsGenders.length ? '' : ' is-active'}`, type: 'button', onclick: () => commit('statsGenders', []) }, [tx('全部', 'All', 'Alle'), $('b', {}, compactNumber(data.items.length))]), ...Object.entries(genderLabels).map(([value, label]) => $('button', { class: `rgh-filter-toggle${viewState.statsGenders.includes(value) ? ' is-active' : ''}`, type: 'button', ariaPressed: String(viewState.statsGenders.includes(value)), onclick: () => toggleGender(value) }, [label, $('b', {}, compactNumber(genderCounts[value] || 0))])), ]), ]), $('div', { class: 'rgh-filter-pickers' }, [ libraryMultiPicker(tx('国家/地区(多选)', 'Countries/regions (multiple)', 'Länder/Regionen (mehrfach)'), 'statsCountries', countries, tx('添加国家…', 'Add country…', 'Land hinzufügen …')), libraryMultiPicker(tx('公开标签(多选)', 'Public tags (multiple)', 'Öffentliche Tags (mehrfach)'), 'statsPublicTags', publicTags, tx('添加标签…', 'Add tag…', 'Tag hinzufügen …')), libraryMultiPicker(tx('自己的群组(多选)', 'Own groups (multiple)', 'Eigene Gruppen (mehrfach)'), 'statsGroups', groups, tx('添加群组…', 'Add group…', 'Gruppe hinzufügen …')), ]), $('div', { class: 'rgh-filter-grid' }, [ selectControl(tx('标签组合', 'Tag matching', 'Tag-Verknüpfung'), viewState.statsPublicTagMode, [['any', tx('至少一个', 'At least one', 'Mindestens einer')], ['all', tx('所有选定标签', 'All selected tags', 'Alle gewählten Tags')]], value => commit('statsPublicTagMode', value)), selectControl(tx('群组组合', 'Group matching', 'Gruppen-Verknüpfung'), viewState.statsGroupMode, [['any', tx('至少一个', 'At least one', 'Mindestens eine')], ['all', tx('所有选定群组', 'All selected groups', 'Alle gewählten Gruppen')]], value => commit('statsGroupMode', value)), selectControl(tx('目录来源', 'Catalog source', 'Katalogquelle'), viewState.statsOrganization, [['all', tx('全部模型', 'All models', 'Alle Models')], ['organized', tx('仅个人整理', 'Personally organized only', 'Nur persönlich organisiert')], ['catalog', tx('仅自动发现', 'Automatically discovered only', 'Nur automatisch erfasst')]], value => commit('statsOrganization', value)), selectControl(tx('资料数据', 'Profile data', 'Profildaten'), viewState.statsProfileState, [['all', tx('有或没有资料', 'Loaded or missing', 'Geladen oder nicht')], ['loaded', tx('仅已加载', 'Loaded only', 'Nur geladen')], ['missing', tx('仅尚未加载', 'Missing only', 'Nur noch nicht geladen')]], value => commit('statsProfileState', value)), selectControl(tx('封面/示例图', 'Cover/example image', 'Cover/Beispielbild'), viewState.statsMediaState, [['all', tx('全部', 'All', 'Alle')], ['saved', tx('有已知公开图片', 'Known public image available', 'Bekannte öffentliche Bilder vorhanden')], ['missing', tx('没有已知图片', 'No known image', 'Keine bekannten Bilder')], ['live', tx('当前直播', 'Currently live', 'Gerade live')]], value => commit('statsMediaState', value)), selectControl(tx('画面/类别', 'Picture/category', 'Bild/Kategorie'), viewState.statsFeature, [['all', tx('全部', 'All', 'Alle')], ['hd', 'HD'], ['gaming', 'Gaming'], ['both', 'HD + Gaming'], ['neither', tx('既非 HD 也非游戏', 'Neither HD nor gaming', 'Weder HD noch Gaming')]], value => commit('statsFeature', value)), selectControl(tx('观看历史', 'Watch history', 'Sehverlauf'), viewState.statsWatchState, [['all', tx('全部', 'All', 'Alle')], ['watched', tx('已经看过', 'Watched', 'Bereits angesehen')], ['unwatched', tx('从未看过', 'Never watched', 'Noch nie angesehen')]], value => commit('statsWatchState', value)), selectControl(tx('自己的标签', 'Own tags', 'Eigene Tags'), viewState.statsOwnTagsState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('仅带有标签', 'With tags only', 'Nur mit Tags')], ['without', tx('仅不带标签', 'Without tags only', 'Nur ohne Tags')]], value => commit('statsOwnTagsState', value)), selectControl(tx('群组分配', 'Group assignment', 'Gruppenzuordnung'), viewState.statsGroupsState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('至少一个群组', 'At least one group', 'Mindestens eine Gruppe')], ['without', tx('没有群组', 'No group', 'Ohne Gruppe')]], value => commit('statsGroupsState', value)), selectControl(tx('私人备注', 'Private notes', 'Private Notizen'), viewState.statsNotesState, [['all', tx('带或不带', 'With or without', 'Mit oder ohne')], ['with', tx('仅带备注', 'With notes only', 'Nur mit Notiz')], ['without', tx('仅不带备注', 'Without notes only', 'Nur ohne Notiz')]], value => commit('statsNotesState', value)), ]), $('h3', {}, tx('数值范围和最低值', 'Numeric ranges and minimums', 'Zahlenbereiche und Mindestwerte')), $('div', { class: 'rgh-filter-grid' }, [ libraryNumberControl(tx('最低年龄', 'Minimum age', 'Mindestalter'), 'statsAgeMin', 99), libraryNumberControl(tx('最高年龄', 'Maximum age', 'Höchstalter'), 'statsAgeMax', 99), libraryNumberControl(tx('最少观众', 'Minimum viewers', 'Mindestens Zuschauer'), 'statsViewerMin'), libraryNumberControl(tx('最多观众', 'Maximum viewers', 'Höchstens Zuschauer'), 'statsViewerMax'), libraryNumberControl(tx('最少关注者', 'Minimum followers', 'Mindestens Follower'), 'statsFollowersMin'), libraryNumberControl(tx('最多关注者', 'Maximum followers', 'Höchstens Follower'), 'statsFollowersMax'), libraryNumberControl(tx('最低总观看时间', 'Minimum total watch time', 'Mindest-Gesamtsehzeit'), 'statsWatchMinMinutes', 1000000000, 'min'), libraryNumberControl(tx('最高总观看时间', 'Maximum total watch time', 'Höchste Gesamtsehzeit'), 'statsWatchMaxMinutes', 1000000000, 'min'), libraryNumberControl(tx('最少会话', 'Minimum sessions', 'Mindestens Sitzungen'), 'statsSessionsMin'), libraryNumberControl(tx('最多会话', 'Maximum sessions', 'Höchstens Sitzungen'), 'statsSessionsMax'), libraryNumberControl(tx('最少活跃天数', 'Minimum active days', 'Mindestens aktive Tage'), 'statsActiveDaysMin'), libraryNumberControl(tx('最少预览', 'Minimum previews', 'Mindestens Vorschauen'), 'statsPreviewsMin'), libraryNumberControl(tx('最少快速查看', 'Minimum quick views', 'Mindestens Schnellinfos'), 'statsQuickViewsMin'), libraryNumberControl(tx('最低评分', 'Minimum rating', 'Mindestbewertung'), 'statsRatingMin', 5, '1–5'), libraryNumberControl(tx('最低优先级', 'Minimum priority', 'Mindestpriorität'), 'statsPriorityMin', 5, '1–5'), libraryNumberControl(tx('最近活跃', 'Active within', 'Aktiv innerhalb'), 'statsActivityDays', 36500, tx('天', 'days', 'Tagen')), libraryNumberControl(tx('最少出现次数', 'Minimum appearances', 'Mindestens aufgetaucht'), 'statsAppearancesMin'), libraryNumberControl(tx('最少在线观察', 'Minimum live observations', 'Mindestens Live-Beobachtungen'), 'statsOnlineSamplesMin'), ]), $('div', { class: 'rgh-filter-actions' }, [ $('p', { class: 'rgh-filter-note' }, tx('零或空字段表示没有限制。未知值不会被猜测;年龄范围会排除未知年龄。', 'Zero or empty fields mean no limit. Unknown values are never guessed; age ranges exclude unknown ages.', 'Null oder leere Felder bedeuten keine Grenze. Unbekannte Werte werden nicht geraten; Altersgrenzen schließen unbekanntes Alter aus.')), action(tx('重置所有统计筛选器', 'Reset all statistics filters', 'Alle Statistikfilter zurücksetzen'), reset, statsActiveFilterCount() ? 'danger' : 'disabled'), ]), ]); } // Attach each card before yielding: cover hydration runs in microtasks and // deliberately skips detached media. Keep the grid connected while small // batches give input/navigation a turn instead of constructing 72 editors // in one long task. An old render must never append to the newer page. async function appendHubCards(target, className, shown, createCard, revision, emptyText) { const grid = $('div', { class: className }); target.appendChild(grid); if (!shown.length) { grid.appendChild($('div', { class: 'rgh-empty' }, emptyText)); return true; } let sliceStartedAt = performance.now(); for (let index = 0; index < shown.length; index += 1) { if (revision !== renderRevision || !grid.isConnected) return false; grid.appendChild(createCard(shown[index])); if (index + 1 < shown.length && (index + 1) % 4 === 0 && performance.now() - sliceStartedAt >= 8) { await yieldToUi(); sliceStartedAt = performance.now(); } } return revision === renderRevision && grid.isConnected; } function pagination(total, current, setter, pageSize = viewState.pageSize) { const pages = Math.max(1, Math.ceil(total / pageSize)); const page = Math.max(1, Math.min(pages, current)); return $('div', { class: 'rgh-pagination' }, [ action('‹ Zurück', () => { setter(Math.max(1, page - 1)); scheduleRender(true); }, page <= 1 ? 'disabled' : ''), $('span', {}, tx(`第 ${page} 页,共 ${pages} 页 · ${compactNumber(total)} 个模型`, `Page ${page} of ${pages} · ${compactNumber(total)} models`, `Seite ${page} von ${pages} · ${compactNumber(total)} Models`, `Página ${page} de ${pages} · ${compactNumber(total)} modelos`, `Página ${page} de ${pages} · ${compactNumber(total)} modelos`)), action('Weiter ›', () => { setter(Math.min(pages, page + 1)); scheduleRender(true); }, page >= pages ? 'disabled' : ''), ]); } function mainToolbar(kind) { const search = $('input', { type: 'search', value: viewState.query, dataset: { hubSearch: kind }, placeholder: tx('按名称、标签或备注搜索…', 'Search name, tags or notes…', 'Name, Tags oder Notizen durchsuchen …'), oninput: debounce(event => { viewState.query = event.target.value.trim().toLocaleLowerCase(); viewState.page = 1; viewState.libraryPage = 1; scheduleRender(); }, 160) }); const controls = [$('label', { class: 'rgh-control rgh-search' }, [$('span', {}, tx('搜索', 'Search', 'Suche')), search])]; if (kind === 'stats') { controls.push( selectControl(tx('排序', 'Sorting', 'Sortierung'), viewState.sort, [ ['watch', tx('最多观看时间', 'Most watch time', 'Meiste Sehzeit')], ['watch_low', tx('最少观看时间', 'Least watch time', 'Wenigste Sehzeit')], ['recent', tx('最近活跃', 'Recently active', 'Zuletzt aktiv')], ['oldest', tx('最久未活跃', 'Longest inactive', 'Am längsten inaktiv')], ['sessions', tx('最多会话', 'Most sessions', 'Meiste Sitzungen')], ['sessions_low', tx('最少会话', 'Fewest sessions', 'Wenigste Sitzungen')], ['avg_session', tx('最长平均会话', 'Longest average session', 'Längste Ø-Sitzung')], ['avg_session_low', tx('最短平均会话', 'Shortest average session', 'Kürzeste Ø-Sitzung')], ['longest_session', tx('最长单次会话', 'Longest single session', 'Längste Einzelsitzung')], ['active_days', tx('最多活跃天数', 'Most active days', 'Meiste aktive Tage')], ['share', tx('最高观看占比', 'Highest watch share', 'Höchster Sehzeitanteil')], ['previews', tx('最多预览', 'Most previews', 'Meiste Vorschauen')], ['quickviews', tx('最多快速查看', 'Most quick views', 'Meiste Schnellinfos')], ['discovered', tx('最常发现', 'Most discoveries', 'Am häufigsten entdeckt')], ['viewers', tx('最多观众', 'Most viewers', 'Meiste Zuschauer')], ['viewers_low', tx('最少观众', 'Fewest viewers', 'Wenigste Zuschauer')], ['followers', tx('最多关注者', 'Most followers', 'Meiste Follower')], ['age_young', tx('年龄从小到大', 'Youngest first', 'Alter aufsteigend')], ['age_old', tx('年龄从大到小', 'Oldest first', 'Alter absteigend')], ['rating', tx('最高评分', 'Highest rating', 'Höchste Bewertung')], ['priority', tx('最高优先级', 'Highest priority', 'Höchste Priorität')], ['appearances', tx('出现次数最多', 'Most appearances', 'Am häufigsten aufgetaucht')], ['online_samples', tx('最多在线观察', 'Most live observations', 'Meiste Live-Beobachtungen')], ['last_online', tx('最近直播', 'Last live', 'Zuletzt live')], ['first_observed', tx('最早记录', 'First observed', 'Zuerst erfasst')], ['country', tx('国家 A–Z', 'Country A–Z', 'Land A–Z')], ['gender', tx('性别/类型', 'Gender/type', 'Geschlecht/Typ')], ['tag_count', tx('最多公开标签', 'Most public tags', 'Meiste öffentliche Tags')], ['group_count', tx('最多群组', 'Most groups', 'Meiste Gruppen')], ['quality', tx('最佳技术质量', 'Best technical quality', 'Beste technische Qualität')], ['media', tx('有封面/示例图优先', 'Cover/example images first', 'Cover/Beispielbilder zuerst')], ['name', 'Name A–Z'], ['name_desc', 'Name Z–A'], ], value => { viewState.sort = value; viewState.page = 1; savePrefs(); scheduleRender(); }), selectControl(tx('状态', 'Status', 'Status'), viewState.status, [['all', tx('所有状态', 'All statuses', 'Alle Status')], ['live', tx('仅直播', 'Live only', 'Nur Live')], ['offline', 'Offline'], ['private', tx('私人', 'Private', 'Privat')], ['unknown', tx('未知', 'Unknown', 'Unbekannt')], ['error', tx('错误/加载中', 'Error/loading', 'Fehler/Laden')]], value => { viewState.status = value; viewState.page = 1; savePrefs(); scheduleRender(); }), $('label', { class: 'rgh-check' }, [$('input', { type: 'checkbox', checked: viewState.savedOnly, onchange: event => { viewState.savedOnly = event.target.checked; viewState.page = 1; savePrefs(); scheduleRender(); } }), tx('仅资料库', 'Library only', 'Nur Bibliothek')]), ); } else { controls.push( selectControl(tx('视图', 'View', 'Ansicht'), viewState.libraryMode, [['active', tx('活跃资料库', 'Active library', 'Aktive Bibliothek')], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['watchlist', tx('关注列表', 'Watchlist', 'Merkliste')], ['online', tx('当前直播', 'Live now', 'Gerade live')], ['notes', tx('带备注', 'With notes', 'Mit Notizen')], ['archived', tx('存档', 'Archive', 'Archiv')]], value => { viewState.libraryMode = value; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }), selectControl(tx('排序', 'Sorting', 'Sortierung'), viewState.librarySort, [ ['recent', tx('最近活跃', 'Recently active', 'Zuletzt aktiv')], ['oldest', tx('最久未活跃', 'Longest inactive', 'Am längsten inaktiv')], ['added', tx('最近保存/发现', 'Newest saved/discovered', 'Zuletzt gespeichert/erfasst')], ['added_oldest', tx('最早保存/发现', 'Oldest saved/discovered', 'Zuerst gespeichert/erfasst')], ['watch', tx('最多观看时间', 'Most watch time', 'Meiste Sehzeit')], ['watch_low', tx('最少观看时间', 'Least watch time', 'Wenigste Sehzeit')], ['rating', tx('最高评分', 'Highest rating', 'Höchste Bewertung')], ['rating_low', tx('最低评分', 'Lowest rating', 'Niedrigste Bewertung')], ['priority', tx('最高优先级', 'Highest priority', 'Höchste Priorität')], ['priority_low', tx('最低优先级', 'Lowest priority', 'Niedrigste Priorität')], ['viewers', tx('最多观众', 'Most viewers', 'Meiste Zuschauer')], ['viewers_low', tx('最少观众', 'Fewest viewers', 'Wenigste Zuschauer')], ['followers', tx('最多关注者', 'Most followers', 'Meiste Follower')], ['current_duration', tx('当前在线最久', 'Live longest now', 'Aktuell am längsten online')], ['age_young', tx('年龄从小到大', 'Age youngest first', 'Alter aufsteigend')], ['age_old', tx('年龄从大到小', 'Age oldest first', 'Alter absteigend')], ['sessions', tx('最多会话', 'Most sessions', 'Meiste Sitzungen')], ['avg_session', tx('最长平均会话', 'Longest average session', 'Längste Ø-Sitzung')], ['longest_session', tx('最长单次会话', 'Longest single session', 'Längste Einzelsitzung')], ['active_days', tx('最多活跃天数', 'Most active days', 'Meiste aktive Tage')], ['appearances', tx('最常出现', 'Most appearances', 'Am häufigsten aufgetaucht')], ['online_samples', tx('最多直播观察', 'Most live observations', 'Meiste Live-Beobachtungen')], ['last_online', tx('最近直播', 'Most recently live', 'Zuletzt live')], ['first_observed', tx('最早发现', 'First discovered', 'Am frühesten erfasst')], ['name', 'Name A–Z'], ['name_desc', 'Name Z–A'], ['country', tx('国家 A–Z', 'Country A–Z', 'Land A–Z')], ['gender', tx('性别/类型', 'Gender/type', 'Geschlecht/Typ')], ['tag_count', tx('最多公开标签', 'Most public tags', 'Meiste öffentliche Tags')], ['group_count', tx('最多自己的组', 'Most own groups', 'Meiste eigene Gruppen')], ], value => { viewState.librarySort = value; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }), selectControl(tx('卡片大小', 'Card size', 'Kartengröße'), viewState.density, [['compact', tx('紧凑', 'Compact', 'Kompakt')], ['comfortable', tx('舒适', 'Comfortable', 'Komfortabel')], ['large', tx('大', 'Large', 'Groß')], ['cinema', tx('影院', 'Cinema', 'Kino')], ['focus', tx('超大', 'Extra large', 'Extra groß')]], value => { viewState.density = value; savePrefs(); applyLibraryLayout(); scheduleRender(); }), selectControl(tx('每行模型数', 'Models per row', 'Models pro Reihe'), viewState.libraryColumns, [['0', tx('自动适应窗口', 'Automatic for window', 'Automatisch nach Fenster')], ['1', '1'], ['2', '2'], ['3', '3'], ['4', '4'], ['5', '5'], ['6', '6']], value => { viewState.libraryColumns = Math.max(0, Math.min(6, Number(value) || 0)); viewState.libraryPage = 1; savePrefs(); applyLibraryLayout(); scheduleRender(); }), ); } if (kind === 'library') controls.push(selectControl(tx('每页模型数', 'Models per page', 'Models pro Seite'), viewState.libraryPageSize, LIBRARY_PAGE_SIZES.map(value => [value, String(value)]), value => { viewState.libraryPageSize = Number(value); viewState.libraryPage = 1; savePrefs(); scheduleRender(); })); else controls.push(selectControl(tx('每页模型数', 'Models per page', 'Models pro Seite'), viewState.pageSize, PAGE_SIZES.map(value => [value, String(value)]), value => { viewState.pageSize = Number(value); viewState.page = 1; savePrefs(); scheduleRender(); })); return $('div', { class: 'rgh-toolbar' }, controls); } function librarySubviews() { const views = [['all', tx('全部记录', 'All recorded', 'Alle erfasst')], ['active', tx('活跃资料库', 'Active library', 'Aktiv')], ['collection', tx('我的收藏库', 'My collection', 'Meine Sammlung')], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['watchlist', tx('关注列表', 'Watchlist', 'Merkliste')], ['groups', tx('组', 'Groups', 'Gruppen')], ['archived', tx('存档', 'Archive', 'Archiv')]]; return $('nav', { class: 'rgh-library-subviews', ariaLabel: tx('资料库视图', 'Library views', 'Bibliotheksansichten') }, views.map(([id, label]) => { const button = action(label, () => { viewState.libraryMode = id; viewState.libraryPage = 1; savePrefs(); scheduleRender(); }, viewState.libraryMode === id ? 'is-active' : ''); button.setAttribute('aria-pressed', String(viewState.libraryMode === id)); return button; })); } async function statsPeriodTotals(data, days, now = Date.now(), cancelled = () => false) { const memoKey = `${days}:${Math.floor(now / 60000)}:${data.events.length}`; let memo = statsPeriodMemo.get(data); if (!memo) { memo = new Map(); statsPeriodMemo.set(data, memo); } if (memo.has(memoKey)) return memo.get(memoKey); const current = { ms: 0, visits: 0, previews: 0, models: new Set() }; const previous = { ms: 0, visits: 0, previews: 0, models: new Set() }; const boundary = now - days * 86400000; const start = boundary - days * 86400000; let sliceAt = performance.now(); for (let index = 0; index < data.events.length; index += 1) { if (index % 256 === 0 && cancelled()) return null; const event = data.events[index]; if (event.at >= start && event.at <= now) { const bucket = event.at >= boundary ? current : previous; if (event.type === 'visit' && event.id && Number(event.durationMs) > 0) { bucket.ms += Number(event.durationMs); bucket.visits += 1; bucket.models.add(event.id); } if (event.type === 'preview') bucket.previews += 1; } if (index % 256 === 0 && performance.now() - sliceAt >= 8) { await yieldToUi(); sliceAt = performance.now(); } } const result = { current: { ...current, models: current.models.size }, previous: { ...previous, models: previous.models.size }, boundary, start, now }; if (memo.size > 4) memo.clear(); memo.set(memoKey, result); return result; } function statsPeriodComparison(data) { const body = $('section', { class: 'rgh-period-comparison', dataset: { statsPeriodComparison: '1' } }); if (data.historyReady === false) { body.appendChild($('p', { role: 'status' }, tx('历史仍在加载——比较数据尚不完整。', 'History is still loading — comparison data is not complete yet.', 'Der Verlauf wird noch geladen – die Vergleichsdaten sind noch nicht vollständig.'))); return body; } const values = $('div', { role: 'status' }); let generation = 0; const draw = async () => { const token = ++generation; values.textContent = tx('计算本地时间段…', 'Calculating local periods…', 'Lokale Zeiträume werden berechnet …'); const range = viewState.statsRange; const result = await statsPeriodTotals(data, range, Date.now(), () => token !== generation || !body.isConnected); if (!result || token !== generation || !body.isConnected) return; const diff = (current, previous) => previous ? `${current >= previous ? '+' : ''}${Math.round((current - previous) / previous * 100)} %` : current ? tx('没有比较基准', 'No comparison baseline', 'Kein Vergleichswert') : '0'; values.replaceChildren($('p', {}, `${dateTime(result.boundary)} – ${dateTime(result.now)} · ${tx('对比上一时间段', 'versus previous period', 'gegenüber vorherigem Zeitraum')} ${dateTime(result.start)} – ${dateTime(result.boundary)}`), $('div', { class: 'rgh-metrics' }, [ metric(tx('观看时间', 'Watch time', 'Sehzeit'), duration(result.current.ms), `${tx('以前', 'Previously', 'Vorher')}: ${duration(result.previous.ms)} · ${diff(result.current.ms, result.previous.ms)}`), metric(tx('访问', 'Visits', 'Besuche'), compactNumber(result.current.visits), `${tx('以前', 'Previously', 'Vorher')}: ${compactNumber(result.previous.visits)} · ${diff(result.current.visits, result.previous.visits)}`), metric(tx('观看的模型', 'Watched models', 'Angesehene Models'), compactNumber(result.current.models), `${tx('以前', 'Previously', 'Vorher')}: ${compactNumber(result.previous.models)} · ${diff(result.current.models, result.previous.models)}`), ])); }; body.append(selectControl(tx('比较时间段', 'Comparison period', 'Vergleichszeitraum'), viewState.statsRange, [7, 30, 90, 365].map(days => [days, tx(`${days} 天`, `${days} days`, `${days} Tage`)]), value => { viewState.statsRange = Number(value); savePrefs(); void draw(); }), $('p', {}, tx('仅使用本地保存的访问事件。并行的摄像头时间会相加;未保存的历史不估算。下面模型卡仍显示完整历史。', 'Uses only locally saved visit events. Concurrent cam time is added; unsaved history is not estimated. Model cards below still show all-time history.', 'Nur lokal gespeicherte Besuchsereignisse. Gleichzeitige Cam-Zeiten werden addiert; nicht gespeicherter Verlauf wird nicht geschätzt. Die Modelkarten darunter zeigen weiterhin den gesamten Verlauf.')), values); setTimeout(() => { if (body.isConnected) void draw(); }, 0); return body; } async function renderStats(target, data, revision) { const customGroups = customLibraryGroups(); let list = await filterHubItems(data.items, item => { if (viewState.query && !item.searchText.includes(viewState.query)) return false; if (viewState.savedOnly && !item.room) return false; if (viewState.status !== 'all' && libraryStatusBucket(item) !== viewState.status) return false; if (!matchesStatsAdvancedFilters(item)) return false; return true; }, revision); if (!list || revision !== renderRevision) return; const knownNumber = (item, getter) => { const value = Number(getter(item)) || 0; return value > 0 ? value : null; }; const knownAscending = getter => (a, b) => { const left = knownNumber(a, getter); const right = knownNumber(b, getter); if (left == null) return right == null ? a.id.localeCompare(b.id) : 1; if (right == null) return -1; return left - right || a.id.localeCompare(b.id); }; const knownDescending = getter => (a, b) => { const left = knownNumber(a, getter); const right = knownNumber(b, getter); if (left == null) return right == null ? a.id.localeCompare(b.id) : 1; if (right == null) return -1; return right - left || a.id.localeCompare(b.id); }; const qualityScore = item => item.samples.length ? 100000 - item.dropRate * 50000 - item.stalls * 100 - item.avgStartup : -1; const sorters = { watch: (a, b) => b.totalMs - a.totalMs || b.lastActivity - a.lastActivity, watch_low: (a, b) => a.totalMs - b.totalMs || a.id.localeCompare(b.id), recent: (a, b) => b.lastActivity - a.lastActivity, oldest: (a, b) => (a.lastActivity || Number.MAX_SAFE_INTEGER) - (b.lastActivity || Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id), sessions: (a, b) => b.sessions - a.sessions || b.totalMs - a.totalMs, sessions_low: (a, b) => a.sessions - b.sessions || a.totalMs - b.totalMs, avg_session: (a, b) => b.avgSessionMs - a.avgSessionMs || b.totalMs - a.totalMs, avg_session_low: (a, b) => a.avgSessionMs - b.avgSessionMs || a.totalMs - b.totalMs, longest_session: (a, b) => Number(b.model.longestSessionMs || 0) - Number(a.model.longestSessionMs || 0), active_days: (a, b) => b.activeDays - a.activeDays || b.totalMs - a.totalMs, share: (a, b) => b.share - a.share || b.totalMs - a.totalMs, previews: (a, b) => Number(b.counts.preview || 0) - Number(a.counts.preview || 0) || b.totalMs - a.totalMs, quickviews: (a, b) => Number(b.counts.quickview || 0) - Number(a.counts.quickview || 0) || b.totalMs - a.totalMs, discovered: (a, b) => Number(b.counts.discovered || 0) - Number(a.counts.discovered || 0) || b.lastActivity - a.lastActivity, viewers: (a, b) => (b.model.viewerCount || 0) - (a.model.viewerCount || 0), viewers_low: (a, b) => (a.model.viewerCount || 0) - (b.model.viewerCount || 0), followers: (a, b) => Number(b.model.followers || 0) - Number(a.model.followers || 0) || a.id.localeCompare(b.id), age_young: knownAscending(item => item.model.age), age_old: knownDescending(item => item.model.age), rating: (a, b) => Number(b.room?.rating || 0) - Number(a.room?.rating || 0) || b.totalMs - a.totalMs, priority: (a, b) => Number(b.room?.priority || 0) - Number(a.room?.priority || 0) || b.lastActivity - a.lastActivity, appearances: (a, b) => Number(b.model.appearances || 0) - Number(a.model.appearances || 0) || b.lastActivity - a.lastActivity, online_samples: (a, b) => Number(b.model.onlineSamples || 0) - Number(a.model.onlineSamples || 0) || b.lastActivity - a.lastActivity, last_online: knownDescending(item => item.model.lastOnlineAt || item.room?.lastSeenOnline), first_observed: knownAscending(item => item.model.firstObservedAt || item.room?.addedAt), country: (a, b) => String(a.model.country || '\uffff').localeCompare(String(b.model.country || '\uffff')) || a.id.localeCompare(b.id), gender: (a, b) => String(a.model.gender || 'unknown').localeCompare(String(b.model.gender || 'unknown')) || a.id.localeCompare(b.id), tag_count: (a, b) => publicTagsForLibraryItem(b).length - publicTagsForLibraryItem(a).length || a.id.localeCompare(b.id), group_count: (a, b) => getRoomGroups(b.room).length - getRoomGroups(a.room).length || a.id.localeCompare(b.id), quality: (a, b) => qualityScore(b) - qualityScore(a) || b.samples.length - a.samples.length, media: (a, b) => Number(!!(b.model.previewUrls || []).length) - Number(!!(a.model.previewUrls || []).length) || b.lastActivity - a.lastActivity, name: (a, b) => a.id.localeCompare(b.id), name_desc: (a, b) => b.id.localeCompare(a.id), }; list = await sortHubItemsProgressively(list, sorters[viewState.sort] || sorters.watch, revision); if (!list || revision !== renderRevision) return; const watched = data.summary.watched; const most = data.summary.top; target.replaceChildren(); target.append( hero('stats'), $('div', { class: 'rgh-actions' }, [ action(tx('统计分析', 'Detailed statistics', 'Detaillierte Auswertungen'), () => openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'analytics', { statisticsView: true })), action(tx('历史设置', 'History settings', 'Verlauf einstellen'), () => openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'analytics', { analyticsSection: 'data' })), ]), ...(data.historyReady === false ? [$('p', { class: 'rgh-bio', role: 'status' }, tx('正在补全本地历史。当前结果尚不完整。', 'Local history is still loading. Results are partial.', 'Der lokale Verlauf wird ergänzt. Die angezeigten Ergebnisse sind vorläufig.'))] : []), $('div', { class: 'rgh-metrics' }, [ metric(tx('已记录模型', 'Recorded models', 'Erfasste Models', 'Modelos registrados', 'Modelos registrados'), compactNumber(data.items.length), tx(`${data.rooms.length} 个已保存`, `${data.rooms.length} saved`, `${data.rooms.length} gespeichert`, `${data.rooms.length} guardados`, `${data.rooms.length} salvos`)), metric(tx('总直播时间', 'Total cam time', 'Gesamte Cam-Zeit', 'Tiempo total de cámaras', 'Tempo total de câmeras'), duration(data.totalWatchMs), tx('所有模型', 'across all models', 'über alle Models', 'en todos los modelos', 'em todos os modelos')), metric(tx('会话', 'Sessions', 'Sitzungen', 'Sesiones', 'Sessões'), compactNumber(data.summary.sessions), tx(`${compactNumber(watched.length)} 个已观看`, `${compactNumber(watched.length)} watched`, `${compactNumber(watched.length)} angesehen`, `${compactNumber(watched.length)} vistos`, `${compactNumber(watched.length)} assistidos`)), metric(tx('预览', 'Previews', 'Vorschauen', 'Vistas previas', 'Prévias'), compactNumber(data.summary.previews), tx('固定栏 + 悬停', 'Dock + hover', 'Dock + Hover', 'Fijar + pasar el cursor', 'Fixar + passar o cursor')), metric(tx('最近 30 天', 'Last 30 days', 'Letzte 30 Tage', 'Últimos 30 días', 'Últimos 30 dias'), compactNumber(data.summary.recentEvents), tx('本地事件', 'local events', 'lokale Ereignisse', 'eventos locales', 'eventos locais')), metric(tx('热门模型', 'Top model', 'Top-Model', 'Modelo principal', 'Modelo principal'), most?.id || '—', most ? duration(most.totalMs) : tx('尚无观看时间', 'no watch time yet', 'noch keine Sehzeit', 'aún no hay tiempo de visualización', 'ainda não há tempo de visualização')), ]), mainToolbar('stats'), ); compactMetrics(target, 'stats-metrics'); target.append(activeHubFilters('stats'), $('div', { class: 'rgh-tools-rack' }, [ target.querySelector('[data-hub-tools="stats-metrics"]'), hubTools('stats-filters', tx('筛选结果', 'Filter results', 'Ergebnisse filtern'), async () => { const facets = await prepareHubFacets(data, renderRevision); return facets ? statsAdvancedFilterPanel(data, list.length, facets) : null; }, tx('全部选项已保留', 'All options retained', 'Alle bisherigen Optionen bleiben erhalten')), hubTools('stats-comparison', tx('时间段比较', 'Period comparison', 'Zeitraumvergleich'), () => statsPeriodComparison(data), tx('本地保存的访问', 'Locally saved visits', 'Lokal gespeicherte Besuche'))])); const pages = Math.max(1, Math.ceil(list.length / viewState.pageSize)); viewState.page = Math.min(viewState.page, pages); const shown = list.slice((viewState.page - 1) * viewState.pageSize, viewState.page * viewState.pageSize); target.appendChild(datasetScope(data, list, shown, viewState.page, viewState.pageSize)); if (!await appendHubCards(target, 'rgh-grid', shown, item => statCard(item, customGroups), revision, 'Keine Models passen zu diesen Filtern.')) return; target.appendChild(pagination(list.length, viewState.page, value => { viewState.page = value; })); target.appendChild($('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, tx('更深入的分析', 'Deeper analysis', 'Noch tiefere Auswertung', 'Análisis más profundo', 'Análise mais profunda')), action(tx('打开 Analytics Studio', 'Open Analytics Studio', 'Analytics Studio öffnen', 'Abrir Analytics Studio', 'Abrir Analytics Studio'), () => { close(); openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'analytics'); }, 'primary')]), $('p', { class: 'rgh-bio' }, 'Das Analytics Studio enthält zusätzlich Zeitraumvergleiche, Heatmaps, Discover-Funnel, Workspace- und Qualitätsanalysen sowie Datenexporte.'), ])); } async function renderLibrary(target, data, revision) { await applyCurrentCatalogStatus(data, revision); if (revision !== renderRevision) return; const customGroups = customLibraryGroups(); const validGroupIds = new Set(customGroups.map(group => group.id)); viewState.libraryGroups = viewState.libraryGroups.filter(id => validGroupIds.has(id)); let tagSuggestions = null; const getTagSuggestions = () => tagSuggestions ??= buildTagSuggestions(data.rooms); // Most visits use the default advanced filters. Determine once whether // the expensive per-model filter evaluation can affect the result. const hasAdvancedFilters = hasAdvancedLibraryFilters(); let list = await filterHubItems(data.items, item => { const room = item.room; if (viewState.query && !item.searchText.includes(viewState.query)) return false; if (viewState.libraryGroups.length) { const groupMatches = viewState.libraryGroups.map(groupId => roomInGroup(room, groupId)); if (viewState.libraryGroupMode === 'all' ? !groupMatches.every(Boolean) : !groupMatches.some(Boolean)) return false; } if (viewState.libraryTags.length) { const ownTags = new Set(room?.localTags || []); const tagMatches = viewState.libraryTags.map(tag => ownTags.has(tag)); if (viewState.libraryTagMode === 'all' ? !tagMatches.every(Boolean) : !tagMatches.some(Boolean)) return false; } if (viewState.libraryMode === 'active' && room?.archived) return false; if (viewState.libraryMode === 'collection' && (!room || room.archived)) return false; if (viewState.libraryMode === 'groups' && (!room || !getRoomGroups(room).some(id => validGroupIds.has(id)))) return false; if (viewState.libraryMode === 'archived' && !room?.archived) return false; if (viewState.libraryMode === 'favorites' && !isFavorite(room)) return false; if (viewState.libraryMode === 'watchlist' && !room?.watchlist) return false; if (viewState.libraryMode === 'online' && item.status !== 'online' && item.status !== 'public') return false; if (viewState.libraryMode === 'notes' && !String(room?.notes || '').trim()) return false; if (hasAdvancedFilters && !matchesLibraryAdvancedFilters(item)) return false; return true; }, revision); if (!list || revision !== renderRevision) return; const knownNumber = (item, getter) => { const value = Number(getter(item)) || 0; return value > 0 ? value : null; }; const knownAscending = getter => (a, b) => { const left = knownNumber(a, getter); const right = knownNumber(b, getter); if (left == null) return right == null ? a.id.localeCompare(b.id) : 1; if (right == null) return -1; return left - right || a.id.localeCompare(b.id); }; const knownDescending = getter => (a, b) => { const left = knownNumber(a, getter); const right = knownNumber(b, getter); if (left == null) return right == null ? a.id.localeCompare(b.id) : 1; if (right == null) return -1; return right - left || a.id.localeCompare(b.id); }; const sorters = { recent: (a, b) => b.lastActivity - a.lastActivity, oldest: (a, b) => (a.lastActivity || Number.MAX_SAFE_INTEGER) - (b.lastActivity || Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id), added: (a, b) => (b.room?.addedAt || b.model.firstObservedAt || 0) - (a.room?.addedAt || a.model.firstObservedAt || 0), added_oldest: (a, b) => (a.room?.addedAt || a.model.firstObservedAt || Number.MAX_SAFE_INTEGER) - (b.room?.addedAt || b.model.firstObservedAt || Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id), watch: (a, b) => b.totalMs - a.totalMs, watch_low: (a, b) => a.totalMs - b.totalMs || a.id.localeCompare(b.id), rating: (a, b) => (b.room?.rating || 0) - (a.room?.rating || 0) || b.totalMs - a.totalMs, rating_low: (a, b) => (a.room?.rating || 0) - (b.room?.rating || 0) || a.id.localeCompare(b.id), priority: (a, b) => (b.room?.priority || 0) - (a.room?.priority || 0) || b.lastActivity - a.lastActivity, priority_low: (a, b) => (a.room?.priority || 0) - (b.room?.priority || 0) || a.id.localeCompare(b.id), viewers: (a, b) => Number(b.model.viewerCount || 0) - Number(a.model.viewerCount || 0) || a.id.localeCompare(b.id), viewers_low: (a, b) => Number(a.model.viewerCount || 0) - Number(b.model.viewerCount || 0) || a.id.localeCompare(b.id), followers: (a, b) => Number(b.model.followers || 0) - Number(a.model.followers || 0) || a.id.localeCompare(b.id), current_duration: (a, b) => (libraryStatusBucket(b) === 'live' ? Number(b.model.secondsOnline || 0) : 0) - (libraryStatusBucket(a) === 'live' ? Number(a.model.secondsOnline || 0) : 0) || a.id.localeCompare(b.id), age_young: knownAscending(item => item.model.age), age_old: knownDescending(item => item.model.age), sessions: (a, b) => b.sessions - a.sessions || b.totalMs - a.totalMs, avg_session: (a, b) => b.avgSessionMs - a.avgSessionMs || b.totalMs - a.totalMs, longest_session: (a, b) => Number(b.model.longestSessionMs || 0) - Number(a.model.longestSessionMs || 0) || a.id.localeCompare(b.id), active_days: (a, b) => b.activeDays - a.activeDays || b.totalMs - a.totalMs, appearances: (a, b) => Number(b.model.appearances || 0) - Number(a.model.appearances || 0) || b.lastActivity - a.lastActivity, online_samples: (a, b) => Number(b.model.onlineSamples || 0) - Number(a.model.onlineSamples || 0) || b.lastActivity - a.lastActivity, last_online: knownDescending(item => item.model.lastOnlineAt || item.room?.lastSeenOnline), first_observed: knownAscending(item => item.model.firstObservedAt || item.room?.addedAt), name: (a, b) => a.id.localeCompare(b.id), name_desc: (a, b) => b.id.localeCompare(a.id), country: (a, b) => String(a.model.country || '\uffff').localeCompare(String(b.model.country || '\uffff')) || a.id.localeCompare(b.id), gender: (a, b) => String(a.model.gender || 'unknown').localeCompare(String(b.model.gender || 'unknown')) || a.id.localeCompare(b.id), tag_count: (a, b) => publicTagsForLibraryItem(b).length - publicTagsForLibraryItem(a).length || a.id.localeCompare(b.id), group_count: (a, b) => getRoomGroups(b.room).length - getRoomGroups(a.room).length || a.id.localeCompare(b.id), }; list = await sortHubItemsProgressively(list, sorters[viewState.librarySort] || sorters.recent, revision); if (!list || revision !== renderRevision) return; const activeCatalog = data.items.filter(item => !item.room?.archived); target.replaceChildren(); target.append( hero('library'), ...(data.historyReady === false ? [$('p', { class: 'rgh-bio', role: 'status' }, tx('正在补全本地历史。当前结果尚不完整。', 'Local history is still loading. Results are partial.', 'Der lokale Verlauf wird ergänzt. Die angezeigten Ergebnisse sind vorläufig.'))] : []), $('div', { class: 'rgh-metrics' }, [ metric('Bibliothek gesamt', compactNumber(data.items.length), catalogRestoreStatus === 'pending' ? tx(`${activeCatalog.length} 个活跃 · 正在补全已保存目录`, `${activeCatalog.length} active · restoring saved catalog`, `${activeCatalog.length} aktiv · gespeicherten Katalog wird ergänzt`, `${activeCatalog.length} activos · restaurando catálogo guardado`, `${activeCatalog.length} ativos · restaurando catálogo salvo`) : tx(`${activeCatalog.length} 个活跃 · 自动增长`, `${activeCatalog.length} active · grows automatically`, `${activeCatalog.length} aktiv · wächst automatisch`, `${activeCatalog.length} activos · crece automáticamente`, `${activeCatalog.length} ativos · cresce automaticamente`)), metric('Persönlich organisiert', compactNumber(data.rooms.length), 'Gruppen, Tags oder MultiCam'), metric('Favoriten', compactNumber(data.rooms.filter(isFavorite).length), 'schnell erreichbar'), metric('Merkliste', compactNumber(data.rooms.filter(room => room.watchlist).length), 'für später'), metric('Mit Notizen', compactNumber(data.rooms.filter(room => String(room.notes || '').trim()).length), 'private Profile'), metric('Eigene Tags', compactNumber(new Set(data.rooms.flatMap(room => room.localTags || [])).size), 'lokale Organisation'), metric('Gerade live', compactNumber(data.items.filter(item => item.status === 'online' || item.status === 'public').length), tx('当前目录或最近的观察', 'Current catalog or recent observation', 'Aktueller Katalog oder frische Beobachtung')), ]), mainToolbar('library'), ); const libraryMetrics = target.querySelector(':scope > .rgh-metrics'); libraryMetrics.remove(); target.querySelector('.rgh-hero > div').appendChild($('p', { class: 'rgh-library-summary' }, `${compactNumber(data.items.length)} ${tx('模型', 'models', 'Models')} · ${compactNumber(data.rooms.length)} ${tx('已保存', 'saved', 'gespeichert')} · ${compactNumber(data.rooms.filter(isFavorite).length)} ${tx('收藏', 'favorites', 'Favoriten')}`)); const toolbar = target.querySelector(':scope > .rgh-toolbar'); toolbar.before(librarySubviews()); const viewSelect = toolbar.querySelectorAll('select')[0]; if (viewSelect) { [['all', tx('全部记录', 'All recorded', 'Alle erfasst')], ['collection', tx('我的收藏库', 'My collection', 'Meine Sammlung')], ['groups', tx('组', 'Groups', 'Gruppen')]].forEach(([value, label]) => viewSelect.appendChild($('option', { value, selected: viewState.libraryMode === value }, label))); viewSelect.value = viewState.libraryMode; } const pages = Math.max(1, Math.ceil(list.length / viewState.libraryPageSize)); viewState.libraryPage = Math.min(viewState.libraryPage, pages); const shown = list.slice((viewState.libraryPage - 1) * viewState.libraryPageSize, viewState.libraryPage * viewState.libraryPageSize); target.append(activeHubFilters('library'), $('div', { class: 'rgh-tools-rack' }, [ hubTools('library-metrics', tx('指标与数据来源', 'Metrics and data sources', 'Kennzahlen und Datenquellen'), () => [libraryMetrics, datasetScope(data, list, shown, viewState.libraryPage, viewState.libraryPageSize)]), hubTools('library-filters', tx('筛选结果', 'Filter results', 'Ergebnisse filtern'), async () => { const facets = await prepareHubFacets(data, renderRevision); return facets ? libraryAdvancedFilterPanel(data, list.length, facets) : null; }), hubTools('library-display', tx('显示与卡片', 'Display and cards', 'Ansicht und Karten'), () => libraryDisplayPanel()), hubTools('library-groups', tx('组与自己的标签', 'Groups and own tags', 'Gruppen und eigene Tags'), () => [libraryGroupPanel(data), libraryTagFilterPanel(getTagSuggestions())])])); if (!await appendHubCards(target, 'rgh-grid rgh-library-grid', shown, item => libraryCard(item, getTagSuggestions, customGroups), revision, 'Diese Bibliotheksansicht ist noch leer.')) return; target.appendChild(pagination(list.length, viewState.libraryPage, value => { viewState.libraryPage = value; }, viewState.libraryPageSize)); } function usageStreak(days) { const active = new Set(Object.entries(days || {}).filter(([, value]) => Number(value) > 0).map(([day]) => day)); let cursor = new Date(); if (!active.has(asDay(cursor))) cursor = new Date(cursor.getTime() - 86400000); let count = 0; while (active.has(asDay(cursor)) && count < 5000) { count += 1; cursor = new Date(cursor.getTime() - 86400000); } return count; } function userChart(days) { const rows = []; for (let offset = 13; offset >= 0; offset--) { const date = new Date(Date.now() - offset * 86400000); rows.push({ day: asDay(date), label: date.toLocaleDateString([], { weekday: 'short' }).slice(0, 2), value: Number(days?.[asDay(date)]) || 0 }); } const max = Math.max(1, ...rows.map(row => row.value)); return $('div', { class: 'rgh-chart' }, rows.map(row => $('div', { class: 'rgh-bar', title: `${row.day}: ${duration(row.value)}` }, [$('i', { style: { height: `${Math.max(2, row.value / max * 100)}%` } }), $('span', {}, row.label)]))); } function addInterestValue(map, key, values = {}) { const label = String(key || '').trim(); if (!label) return; const current = map.get(label) || { key: label, ms: 0, sessions: 0, signals: 0, favorites: 0, models: new Set() }; current.ms += Math.max(0, Number(values.ms) || 0); current.sessions += Math.max(0, Number(values.sessions) || 0); current.signals += Math.max(0, Number(values.signals) || 0); current.favorites += Math.max(0, Number(values.favorites) || 0); if (values.id) current.models.add(String(values.id)); map.set(label, current); } function rankedInterest(map, score = 'ms', limit = 12) { return [...map.values()].filter(item => Number(item[score]) > 0).sort((a, b) => Number(b[score]) - Number(a[score]) || b.models.size - a.models.size || a.key.localeCompare(b.key)).slice(0, limit); } function ageBand(age) { const value = Number(age) || 0; if (value < 18) return ''; if (value <= 20) return '18–20'; if (value <= 24) return '21–24'; if (value <= 29) return '25–29'; if (value <= 34) return '30–34'; if (value <= 39) return '35–39'; if (value <= 49) return '40–49'; return '50+'; } function dayPart(hour) { if (hour < 5) return tx('深夜', 'Late night', 'Nachts', 'Madrugada', 'Madrugada'); if (hour < 12) return tx('早晨', 'Morning', 'Morgens', 'Mañana', 'Manhã'); if (hour < 18) return tx('白天', 'Daytime', 'Tagsüber', 'Día', 'Durante o dia'); return tx('晚上', 'Evening', 'Abends', 'Noche', 'Noite'); } let weekdayFormatter = null; let weekdayFormatterLanguage = ''; function userWeekdayName(date) { const language = String(LANG || 'en'); if (!weekdayFormatter || weekdayFormatterLanguage !== language) { weekdayFormatter = new Intl.DateTimeFormat(language, { weekday: 'long' }); weekdayFormatterLanguage = language; } return weekdayFormatter.format(date); } function countryName(value) { const code = String(value || '').trim(); if (!/^[a-z]{2}$/iu.test(code)) return code; try { if (countryName.language !== LANG) { countryName.language = LANG; countryName.displayNames = new Intl.DisplayNames([LANG], { type: 'region' }); countryName.values = new Map(); } const key = code.toLocaleUpperCase(); if (!countryName.values.has(key)) countryName.values.set(key, countryName.displayNames.of(key) || key); return countryName.values.get(key); } catch (_) { return code.toLocaleUpperCase(); } } async function forEachUserSlice(list, visit, revision) { let sliceStartedAt = performance.now(); for (let index = 0; index < list.length; index += 1) { if (revision !== renderRevision || !root?.isConnected) return false; visit(list[index], index); if (index && index % 128 === 0 && performance.now() - sliceStartedAt >= 7) { await yieldToUi(); if (revision !== renderRevision || !root?.isConnected) return false; sliceStartedAt = performance.now(); } } return revision === renderRevision && !!root?.isConnected; } async function buildUserInterests(data, selectedEvents, from, revision) { const itemById = new Map(); if (!await forEachUserSlice(data.items, item => itemById.set(item.id, item), revision)) return null; const rowsById = new Map(); const oldestVisitById = new Map(); const maps = Object.fromEntries(['tags', 'genres', 'countries', 'locations', 'ages', 'exactAges', 'types', 'weekdays', 'dayparts', 'favoriteTags', 'favoriteCountries', 'favoriteAges', 'favoriteTypes', 'ownTags', 'signalTags', 'signalModels'].map(key => [key, new Map()])); const genreDefinitions = [ [tx('游戏', 'Gaming', 'Gaming'), ['gaming', 'gamer', 'videogame', 'games']], [tx('音乐与 DJ', 'Music & DJ', 'Musik & DJ'), ['music', 'sing', 'singer', 'singing', 'dj', 'piano', 'guitar']], [tx('舞蹈', 'Dance', 'Tanz'), ['dance', 'dancing', 'twerk']], [tx('角色扮演与动漫', 'Cosplay & anime', 'Cosplay & Anime'), ['cosplay', 'anime', 'costume']], [tx('健身与运动', 'Fitness & sports', 'Fitness & Sport'), ['fitness', 'gym', 'yoga', 'sport', 'workout']], ['ASMR', ['asmr']], [tx('聊天与社交', 'Talk & social', 'Talk & Social'), ['chat', 'talk', 'social', 'conversation', 'chill']], [tx('户外与旅行', 'Outdoor & travel', 'Outdoor & Reisen'), ['outdoor', 'travel', 'outside']], [tx('角色扮演', 'Roleplay', 'Rollenspiel'), ['roleplay', 'role-play', 'role_play']], [tx('情侣', 'Couples', 'Paare'), ['couple', 'couples']], [tx('表演', 'Performance', 'Performance'), ['show', 'performance', 'interactive']], ]; if (!await forEachUserSlice(selectedEvents, event => { if (event.type !== 'visit' || !event.id) return; if (!oldestVisitById.has(event.id) || Number(event.at) < Number(oldestVisitById.get(event.id)?.at)) oldestVisitById.set(event.id, event); if (!(Number(event.durationMs) > 0)) return; const date = new Date(event.at); const value = { id: event.id, ms: event.durationMs, sessions: 1 }; addInterestValue(maps.weekdays, userWeekdayName(date), value); addInterestValue(maps.dayparts, dayPart(date.getHours()), value); if (!from) return; const item = itemById.get(event.id); let row = rowsById.get(event.id); if (!row) { row = { id: event.id, item, ms: 0, sessions: 0, latestEvent: event, tags: new Set() }; (event.tags || []).forEach(tag => row.tags.add(tag)); (item?.model.tags || []).forEach(tag => row.tags.add(tag)); (item?.profile?.tags || []).forEach(tag => row.tags.add(tag)); rowsById.set(event.id, row); } else if (Number(event.at) < Number(row.latestEvent?.at)) row.latestEvent = event; row.ms += Math.max(0, Number(event.durationMs) || 0); row.sessions += 1; (event.tags || []).forEach(tag => row.tags.add(tag)); }, revision)) return null; if (!from && !await forEachUserSlice(data.items, item => { if (!(item.totalMs > 0)) return; const tags = new Set([...(item.model.tags || []), ...(item.profile?.tags || [])]); rowsById.set(item.id, { id: item.id, item, ms: item.totalMs, sessions: item.sessions, latestEvent: oldestVisitById.get(item.id), tags }); }, revision)) return null; const rows = [...rowsById.values()]; let knownAgeMs = 0; let weightedAge = 0; let knownCountryMs = 0; let taggedMs = 0; let favoriteWatchMs = 0; let hdWatchMs = 0; let totalMs = 0; let totalSessions = 0; const topThree = [0, 0, 0]; if (!await forEachUserSlice(rows, row => { const item = row.item || itemById.get(row.id); const latestEvent = row.latestEvent || {}; const model = item?.model || {}; const tags = [...new Set([...row.tags].map(tag => String(tag || '').trim().toLocaleLowerCase()).filter(tag => tag && tag !== 'my-collection' && !tag.startsWith('roomgrid-')))].slice(0, 80); const values = { id: row.id, ms: row.ms, sessions: row.sessions }; tags.forEach(tag => addInterestValue(maps.tags, tag, values)); if (tags.length) taggedMs += row.ms; const genreHits = genreDefinitions.filter(([, needles]) => needles.some(needle => tags.some(tag => tag === needle || tag.includes(needle)))); if ((latestEvent.isGaming ?? model.isGaming) && !genreHits.some(([, needles]) => needles.includes('gaming'))) genreHits.unshift(genreDefinitions[0]); genreHits.forEach(([label]) => addInterestValue(maps.genres, label, values)); const country = countryName(latestEvent.country || model.country || ''); const location = String(latestEvent.location || model.location || '').trim(); const age = Number(latestEvent.age || model.age) || 0; const gender = String(latestEvent.gender || model.gender || 'unknown'); if (country) { addInterestValue(maps.countries, country, values); knownCountryMs += row.ms; } if (location) addInterestValue(maps.locations, location, values); if (age >= 18) { addInterestValue(maps.ages, ageBand(age), values); addInterestValue(maps.exactAges, String(age), values); knownAgeMs += row.ms; weightedAge += row.ms * age; } const genderLabel = ({ f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans' })[gender]; if (genderLabel) addInterestValue(maps.types, genderLabel, values); const room = item?.room; if (isFavorite(room)) favoriteWatchMs += row.ms; if (latestEvent.isHd ?? model.isHd) hdWatchMs += row.ms; (room?.localTags || []).forEach(tag => addInterestValue(maps.ownTags, tag, values)); totalMs += row.ms; totalSessions += row.sessions; if (row.ms > topThree[0]) { topThree[2] = topThree[1]; topThree[1] = topThree[0]; topThree[0] = row.ms; } else if (row.ms > topThree[1]) { topThree[2] = topThree[1]; topThree[1] = row.ms; } else if (row.ms > topThree[2]) topThree[2] = row.ms; }, revision)) return null; if (!await forEachUserSlice(data.items, item => { if (!isFavorite(item.room)) return; const values = { id: item.id, favorites: 1 }; [...new Set([...(item.model.tags || []), ...(item.profile?.tags || [])])].forEach(tag => addInterestValue(maps.favoriteTags, String(tag || '').trim().toLocaleLowerCase(), values)); if (item.model.country) addInterestValue(maps.favoriteCountries, countryName(item.model.country), values); if (item.model.age >= 18) addInterestValue(maps.favoriteAges, ageBand(item.model.age), values); const label = ({ f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans' })[item.model.gender]; if (label) addInterestValue(maps.favoriteTypes, label, values); }, revision)) return null; if (!await forEachUserSlice(selectedEvents, event => { if (!['preview', 'quickview'].includes(event.type) || !event.id) return; const item = itemById.get(event.id); const values = { id: event.id, signals: 1 }; addInterestValue(maps.signalModels, item?.profile?.displayName || event.id, values); [...new Set([...(event.tags || []), ...(item?.model.tags || []), ...(item?.profile?.tags || [])])].forEach(tag => addInterestValue(maps.signalTags, String(tag || '').trim().toLocaleLowerCase(), values)); }, revision)) return null; const limits = { tags: 12, genres: 10, countries: 10, locations: 10, ages: 8, exactAges: 8, types: 6, weekdays: 7, dayparts: 4, favoriteTags: 10, favoriteCountries: 8, favoriteAges: 8, favoriteTypes: 6, ownTags: 10, signalTags: 10, signalModels: 10, }; const compactMaps = Object.fromEntries(Object.entries(maps).map(([key, map]) => [ key, rankedInterest(map, ['signalTags', 'signalModels'].includes(key) ? 'signals' : ['favoriteTags', 'favoriteCountries', 'favoriteAges', 'favoriteTypes'].includes(key) ? 'favorites' : 'ms', limits[key] || 10) .map(item => ({ ...item, models: { size: item.models.size } })), ])); return { maps: compactMaps, totalMs, totalSessions, averageAge: knownAgeMs ? weightedAge / knownAgeMs : 0, ageCoverage: totalMs ? knownAgeMs / totalMs : 0, countryCoverage: totalMs ? knownCountryMs / totalMs : 0, tagCoverage: totalMs ? taggedMs / totalMs : 0, favoriteShare: totalMs ? favoriteWatchMs / totalMs : 0, hdShare: totalMs ? hdWatchMs / totalMs : 0, topThreeShare: totalMs ? topThree.reduce((sum, value) => sum + value, 0) / totalMs : 0, }; } async function buildUserSummary(data, range, from, revision, onCore) { const events = []; const eventCounts = {}; const uniqueModels = new Set(); let visitCount = 0; let visitMs = 0; let usageCount = 0; if (!await forEachUserSlice(data.events, event => { if (event.at < from) return; events.push(event); eventCounts[event.type] = (eventCounts[event.type] || 0) + 1; if (event.id) uniqueModels.add(event.id); if (event.type === 'visit' && event.id && Number(event.durationMs) > 0) { visitCount += 1; visitMs += Number(event.durationMs) || 0; } if (event.type === 'usage') usageCount += 1; }, revision)) return null; let revisited = 0; if (!await forEachUserSlice(data.items, item => { if (item.sessions > 1) revisited += 1; }, revision)) return null; const days = data.usage.usageDays || {}; let selectedUsage = 0; const dayEntries = Object.entries(days); if (!await forEachUserSlice(dayEntries, ([day, value]) => { if (!from || new Date(`${day}T23:59:59`).getTime() >= from) selectedUsage += Number(value) || 0; }, revision)) return null; const activeDays = dayEntries.reduce((sum, [, value]) => sum + (Number(value) > 0 ? 1 : 0), 0); const peakDays = dayEntries.sort((a, b) => Number(b[1]) - Number(a[1])); const top = data.items.slice(0, 12); let favoriteRooms = 0; if (!await forEachUserSlice(data.rooms, room => { if (isFavorite(room)) favoriteRooms += 1; }, revision)) return null; const latestByUser = new Map(); if (!await forEachUserSlice(data.accountSnapshots, snapshot => { if (snapshot.at < from) return; const previous = latestByUser.get(snapshot.username); if (!previous || Number(snapshot.at) > Number(previous.at)) latestByUser.set(snapshot.username, snapshot); }, revision)) return null; const core = { from, range, eventCount: events.length, eventCounts, uniqueModelCount: uniqueModels.size, visitCount, visitMs, usageCount, revisited, days, selectedUsage, today: days[asDay(new Date())] || 0, activeDays, peakDays, top, favoriteRooms, latestByUser, }; // Show complete basic figures without waiting for the detailed interests. // Only the completed summary below is eligible for memoisation. if (revision === renderRevision) onCore?.(core); await yieldToUi(); if (revision !== renderRevision || !root?.isConnected) return null; const interests = await buildUserInterests(data, events, from, revision); if (!interests || revision !== renderRevision) return null; return { ...core, interests }; } async function getUserSummary(data, revision, onCore) { const range = Math.max(0, Number(viewState.userRange) || 0); const minute = Math.floor(Date.now() / 60000) * 60000; const from = range ? minute - range * 86400000 : 0; let byRange = userSummaryMemo.get(data); if (!byRange) { byRange = new Map(); userSummaryMemo.set(data, byRange); } const key = `${LANG}:${range}:${range ? minute : 0}`; if (byRange.has(key)) return { ...byRange.get(key), cacheHit: true }; const summary = await buildUserSummary(data, range, from, revision, onCore); if (!summary || revision !== renderRevision) return null; byRange.set(key, summary); while (byRange.size > 5) byRange.delete(byRange.keys().next().value); return { ...summary, cacheHit: false }; } const FOLLOWING_PAGE_LIMIT = 500; const FOLLOWING_CONCURRENCY = 4; const FOLLOWING_API_PAGE_SIZE = 90; const followingApiPaths = new Set(); function followingPageUrl(href) { return safeAccountPageUrl(href, 'following'); } function followingPageLinks(doc) { return [...doc.querySelectorAll('a[href]')] .map(link => followingPageUrl(link.getAttribute('href'))) .filter(url => url && /^\d+$/u.test(url.searchParams.get('page') || '')); } async function fetchFollowingApiPage(url, signal) { const page = Math.max(1, Number(url.searchParams.get('page')) || 1); const offset = (page - 1) * FOLLOWING_API_PAGE_SIZE; const apiUrl = new URL('/api/ts/roomlist/room-list/', location.origin); apiUrl.searchParams.set('follow', 'true'); if (/\/offline\/?$/iu.test(url.pathname)) apiUrl.searchParams.set('offline', 'true'); apiUrl.searchParams.set('limit', String(FOLLOWING_API_PAGE_SIZE)); apiUrl.searchParams.set('offset', String(offset)); const response = await fetch(apiUrl.href, { credentials: 'include', cache: 'no-store', headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, signal }); const finalUrl = new URL(response.url || apiUrl.href, location.origin); if (finalUrl.origin !== location.origin || finalUrl.pathname !== apiUrl.pathname) throw Object.assign(new Error('Following API redirected'), { code: /\/(?:auth\/login|accounts\/login)(?:\/|$)/iu.test(finalUrl.pathname) ? 'login' : 'parser' }); if (!response.ok) throw Object.assign(new Error(`HTTP ${response.status}`), { code: response.status === 401 || response.status === 403 ? 'login' : 'http' }); let payload; try { payload = await response.json(); } catch (_) { throw Object.assign(new Error('Following API returned invalid JSON'), { code: 'parser' }); } const total = payload?.total_count; const rooms = payload?.rooms; if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(rooms) || rooms.length > FOLLOWING_API_PAGE_SIZE || rooms.some(room => !room || room.is_following !== true || !isLikelyUsername(normalizeUsername(room.username || room.slug || ''))) || (total > offset && rooms.length === 0) || (total === 0 && rooms.length > 0)) { throw Object.assign(new Error('Following API structure not recognized'), { code: 'parser' }); } const ids = [...new Set(rooms.map(room => normalizeUsername(room.username || room.slug)))]; const metadataRows = rooms.map(room => ({ id: normalizeUsername(room.username || room.slug), metadata: { gender: ['f', 'm', 'c', 't'].includes(room.gender) ? room.gender : null, country: typeof room.country === 'string' ? room.country.slice(0, 80) : '', age: Number.isFinite(Number(room.display_age ?? room.age)) && Number(room.display_age ?? room.age) >= 18 ? Math.min(99, Number(room.display_age ?? room.age)) : null, viewerCount: room.num_users != null || room.num_viewers != null || room.viewers != null ? Math.max(0, Number(room.num_users ?? room.num_viewers ?? room.viewers) || 0) : null, followers: room.num_followers != null || room.followers != null ? Math.max(0, Number(room.num_followers ?? room.followers) || 0) : null, secondsOnline: room.seconds_online != null || room.secondsOnline != null || room.start_timestamp ? Math.max(0, Number(room.seconds_online ?? room.secondsOnline ?? (Date.now() / 1000 - Number(room.start_timestamp))) || 0) : null, tags: (Array.isArray(room.tags) ? room.tags : []).filter(tag => typeof tag === 'string').map(tag => tag.slice(0, 80)).slice(0, 80), }, })); const nextPages = []; // The site offsets by requested slots, not returned rows: its offline // response can omit a row (89 of 90). Advancing by rows skips/duplicates pages. if (offset + FOLLOWING_API_PAGE_SIZE < total) { const next = new URL(url.href); next.searchParams.set('page', String(page + 1)); nextPages.push(next); } return { doc: null, parsed: { ids, rows: metadataRows, recognized: true, explicitEmpty: total === 0, cardCount: rooms.length }, nextPages, path: url.pathname }; } async function fetchFollowingPage(url, signal) { const controller = new AbortController(); let timedOut = false; const timeout = setTimeout(() => { timedOut = true; controller.abort(); }, 12000); const abort = () => controller.abort(); signal?.addEventListener('abort', abort, { once: true }); try { if (followingApiPaths.has(url.pathname)) return await fetchFollowingApiPage(url, controller.signal); const response = await fetch(url.href, { credentials: 'include', cache: 'no-store', headers: { Accept: 'text/html' }, signal: controller.signal }); const finalUrl = new URL(response.url || url.href, location.origin); if (finalUrl.origin !== location.origin) throw Object.assign(new Error('Following redirect left the current origin'), { code: 'parser' }); if (/\/(?:auth\/login|accounts\/login)(?:\/|$)/iu.test(finalUrl.pathname)) throw Object.assign(new Error('login required'), { code: 'login' }); if (!response.ok) throw Object.assign(new Error(`HTTP ${response.status}`), { code: response.status === 401 || response.status === 403 ? 'login' : 'http' }); const doc = new DOMParser().parseFromString(await response.text(), 'text/html'); const parsed = extractFollowingDocument(doc); if (/\blog\s*in\b|\bsign\s*in\b|anmelden/iu.test(doc.title || '') && !parsed.recognized) throw Object.assign(new Error('login required'), { code: 'login' }); if (parsed.spaPlaceholder) { const result = await fetchFollowingApiPage(url, controller.signal); followingApiPaths.add(url.pathname); return result; } return { doc, parsed, path: url.pathname }; } catch (error) { if (signal?.aborted) throw Object.assign(new Error('Following load cancelled'), { code: 'cancelled' }); if (error?.name === 'AbortError') throw Object.assign(new Error(timedOut ? 'Following page timeout' : 'Following request aborted'), { code: timedOut ? 'timeout' : 'cancelled' }); throw error; } finally { clearTimeout(timeout); signal?.removeEventListener('abort', abort); } } async function readFollowingPages(onProgress = () => {}) { const controller = new AbortController(); followingRequest = controller; const queue = ['/followed-cams/', '/followed-cams/offline/'].map(path => new URL(path, location.origin)); const queued = new Set(queue.map(url => url.href)); const rows = new Map(); const failures = []; let pages = 0; let recognized = false; let limitReached = false; try { while (queue.length && pages < FOLLOWING_PAGE_LIMIT) { if (controller.signal.aborted) throw Object.assign(new Error('Following load cancelled'), { code: 'cancelled' }); const batch = queue.splice(0, Math.min(FOLLOWING_CONCURRENCY, FOLLOWING_PAGE_LIMIT - pages)); const results = await Promise.allSettled(batch.map(url => fetchFollowingPage(url, controller.signal))); for (let index = 0; index < results.length; index += 1) { pages += 1; const url = batch[index]; const result = results[index]; if (result.status === 'rejected') { failures.push({ code: result.reason?.code || 'http', message: String(result.reason?.message || result.reason) }); continue; } const { doc, parsed, nextPages } = result.value; if (!parsed.recognized) { failures.push({ code: 'parser', message: 'Following page structure not recognized' }); continue; } recognized = true; const offline = /\/offline\/?$/iu.test(url.pathname); const metadataById = new Map((parsed.rows || []).map(row => [row.id, row.metadata])); parsed.ids.forEach(id => { const existing = rows.get(id); if (!existing || !offline) rows.set(id, { id, status: offline ? 'offline' : 'live', metadata: metadataById.get(id) || null }); }); for (const next of nextPages || followingPageLinks(doc)) { if (queued.has(next.href)) continue; if (queued.size >= FOLLOWING_PAGE_LIMIT) { limitReached = true; continue; } queued.add(next.href); queue.push(next); } } onProgress({ pages, count: rows.size }); } if (queue.length || limitReached) limitReached = true; if (!recognized && failures.length) { const code = failures.some(item => item.code === 'login') ? 'login' : failures.some(item => item.code === 'parser') ? 'parser' : failures[0].code; throw Object.assign(new Error(failures[0].message), { code, pages, recognized }); } return { rows: [...rows.values()], pages, recognized, partial: failures.length > 0 || limitReached, failures, limitReached }; } finally { if (followingRequest === controller) followingRequest = null; } } function followingUiText(value, replacements = {}) { let text = String(value || ''); Object.entries(replacements).forEach(([key, replacement]) => { text = text.replaceAll(`{${key}}`, String(replacement)); }); return text; } function renderFollowingRows(target, rows, itemById, ui) { const query = followingQuery.trim().toLocaleLowerCase(); const viewersLabel = ui.viewers || tx('观众', 'viewers', 'Zuschauer', 'espectadores', 'espectadores'); const models = new Map(rows.map(row => [row.id, { ...itemById.get(row.id)?.model, ...Object.fromEntries(Object.entries(row.metadata || {}).filter(([, value]) => value != null && value !== '' && !(Array.isArray(value) && !value.length))) }])); const metadata = row => models.get(row.id) || {}; const requiredTags = parseTags(followingFilters.tags).map(tag => tag.replace(/^#/u, '')); const visible = rows.filter(row => { if (followingStatusFilter !== 'all' && row.status !== followingStatusFilter) return false; const item = itemById.get(row.id); const model = metadata(row); if (followingFilters.gender !== 'all' && (model.gender || 'unknown') !== followingFilters.gender) return false; if (followingFilters.country && String(model.country || '').toLocaleLowerCase() !== followingFilters.country.toLocaleLowerCase()) return false; const tags = new Set([...(model.tags || []), ...(item?.tags || [])].map(tag => String(tag).toLocaleLowerCase().replace(/^#/u, ''))); if (requiredTags.length && (followingFilters.tagMode === 'all' ? !requiredTags.every(tag => tags.has(tag)) : !requiredTags.some(tag => tags.has(tag)))) return false; for (const [field, minKey, maxKey] of [['viewerCount', 'viewerMin', 'viewerMax'], ['age', 'ageMin', 'ageMax']]) { const value = Number(model[field]); const min = followingFilters[minKey]; const max = followingFilters[maxKey]; if ((min || max) && (model[field] == null || !Number.isFinite(value) || (field === 'age' && value < 18) || (min && value < min) || (max && value > max))) return false; } const local = followingFilters.local; if (local === 'favorites' && !isFavorite(item?.room)) return false; if (local === 'watched' && !(item?.totalMs > 0)) return false; if (local === 'unwatched' && item?.totalMs > 0) return false; if (local === 'organized' && !item?.room) return false; if (local === 'unorganized' && item?.room) return false; if (local === 'notes' && !String(item?.room?.notes || '').trim()) return false; if (followingFilters.group && !roomInGroup(item?.room, followingFilters.group)) return false; return !query || `${row.id} ${item?.profile?.displayName || ''} ${item?.profile?.bio || ''} ${[...tags].join(' ')} ${model.country || ''} ${item?.room?.notes || ''}`.toLocaleLowerCase().includes(query); }); const number = (row, field) => Number(metadata(row)[field]) || 0; const localNumber = (row, field) => Number(itemById.get(row.id)?.[field]) || 0; visible.sort((a, b) => { const compare = { name_desc: () => b.id.localeCompare(a.id), live_first: () => Number(b.status === 'live') - Number(a.status === 'live'), viewers: () => number(b, 'viewerCount') - number(a, 'viewerCount'), viewers_low: () => number(a, 'viewerCount') - number(b, 'viewerCount'), recent: () => number(b, 'lastOnlineAt') - number(a, 'lastOnlineAt'), watch: () => localNumber(b, 'totalMs') - localNumber(a, 'totalMs'), watch_low: () => localNumber(a, 'totalMs') - localNumber(b, 'totalMs'), sessions: () => localNumber(b, 'sessions') - localNumber(a, 'sessions'), rating: () => (itemById.get(b.id)?.room?.rating || 0) - (itemById.get(a.id)?.room?.rating || 0), followers: () => number(b, 'followers') - number(a, 'followers'), current_duration: () => (b.status === 'live' ? number(b, 'secondsOnline') : 0) - (a.status === 'live' ? number(a, 'secondsOnline') : 0), }; return (compare[followingSort]?.() || 0) || a.id.localeCompare(b.id); }); const pages = Math.max(1, Math.ceil(visible.length / followingPageSize)); followingPage = Math.max(1, Math.min(followingPage, pages)); const shown = visible.slice((followingPage - 1) * followingPageSize, followingPage * followingPageSize); target.dataset.followingMatches = String(visible.length); target.replaceChildren(...(shown.length ? shown.map(row => { const item = itemById.get(row.id); const model = metadata(row); const viewers = Number(model.numUsers ?? model.viewers ?? model.viewerCount ?? 0); const statusLabel = row.status === 'live' ? ui.live : ui.offline; return $('article', { class: 'rgh-list-row rgh-following-row', dataset: { followingModel: row.id } }, [ $('strong', {}, rawUiText(item?.profile?.displayName || row.id)), $('span', { class: row.status === 'live' ? 'rgh-following-live' : 'rgh-following-offline' }, statusLabel), $('span', {}, viewers > 0 && row.status === 'live' ? `${compactNumber(viewers)} ${viewersLabel.toLocaleLowerCase()}` : (model.country ? countryName(model.country) : '')), action(ui.open, () => openNoopener(roomGridSingleCamUrl(row.id, { source: 'following' })), 'primary'), ]); }) : rows.length ? [$('div', { class: 'rgh-empty' }, ui.noMatch)] : followingState.status === 'complete' ? [$('div', { class: 'rgh-empty' }, ui.empty)] : [])); const panel = target.parentElement; panel?.querySelector('[data-following-pagination]')?.remove(); const redraw = () => renderFollowingRows(target, rows, itemById, ui); const navigation = $('div', { class: 'rgh-pagination', dataset: { followingPagination: '1' } }, [ action(tx('上一页', 'Back', 'Zurück'), () => { followingPage -= 1; redraw(); }, followingPage <= 1 ? 'disabled' : ''), $('span', {}, tx(`第 ${followingPage}/${pages} 页 · ${visible.length}/${rows.length} 个结果`, `Page ${followingPage}/${pages} · ${visible.length}/${rows.length} matches`, `Seite ${followingPage}/${pages} · ${visible.length}/${rows.length} Treffer`)), action(tx('下一页', 'Next', 'Weiter'), () => { followingPage += 1; redraw(); }, followingPage >= pages ? 'disabled' : ''), ]); panel?.append(navigation); } async function renderFollowing(target, data, revision) { restoreFollowingCache(); const ui = I18N[LANG]?.followingUi || I18N.en.followingUi; const itemById = new Map(data.items.map(item => [item.id, item])); const search = $('input', { type: 'search', value: followingQuery, placeholder: ui.search, ariaLabel: ui.search, dataset: { hubSearch: 'following' } }); const status = $('select', { ariaLabel: ui.status }, [ ['all', ui.all], ['live', ui.live], ['offline', ui.offline], ].map(([value, label]) => $('option', { value, selected: followingStatusFilter === value }, label))); const sort = $('select', { ariaLabel: ui.sortName, dataset: { followingSort: '1' } }, [ ['name', ui.sortName], ['name_desc', tx('名称 Z–A', 'Name Z–A', 'Name Z–A')], ['live_first', tx('直播优先', 'Live first', 'Online zuerst')], ['viewers', ui.sortViewers], ['viewers_low', tx('观众最少', 'Fewest viewers', 'Wenigste Zuschauer')], ['recent', ui.sortRecent], ['watch', tx('观看时间最多', 'Most local watch time', 'Meiste lokale Sehzeit')], ['watch_low', tx('观看时间最少', 'Least local watch time', 'Wenigste lokale Sehzeit')], ['sessions', tx('会话最多', 'Most local sessions', 'Meiste lokale Sitzungen')], ['rating', tx('评分最高', 'Highest local rating', 'Höchste lokale Bewertung')], ['followers', tx('关注者最多', 'Most followers', 'Meiste Follower')], ['current_duration', tx('在线时间最长', 'Longest current broadcast', 'Längste aktuelle Online-Dauer')], ].map(([value, label]) => $('option', { value, selected: followingSort === value }, label))); const statusNode = $('p', { class: 'rgh-bio', role: 'status', dataset: { followingState: '1' } }); const listNode = $('div', { class: 'rgh-list', dataset: { followingList: '1' } }); const statsNode = $('div', { class: 'rgh-metrics', dataset: { followingSummary: '1' } }); const quickButtons = []; const filterChange = () => { followingPage = 1; savePrefs(); drawRows(followingState.rows); }; const filterInput = (key, label, type = 'text') => $('label', { class: 'rgh-control' }, [ $('span', {}, label), $('input', { type, value: followingFilters[key], min: type === 'number' ? 0 : undefined, max: key.startsWith('age') ? 99 : undefined, ariaLabel: label, dataset: { followingFilter: key }, oninput: event => { followingFilters[key] = type === 'number' ? clampInt(event.target.value, 0, key.startsWith('age') ? 99 : 1000000000, 0) : event.target.value.slice(0, 500); filterChange(); } }), ]); const filterSelect = (key, label, choices) => $('label', { class: 'rgh-control' }, [$('span', {}, label), $('select', { ariaLabel: label, dataset: { followingFilter: key }, onchange: event => { followingFilters[key] = event.target.value; filterChange(); } }, choices.map(([value, text]) => $('option', { value, selected: followingFilters[key] === value }, text)))]); const advanced = $('details', { class: 'rgh-section' }, [ $('summary', {}, tx('更多筛选', 'More filters', 'Weitere Filter')), $('div', { class: 'rgh-toolbar' }, [ filterSelect('gender', tx('类型', 'Model type', 'Modeltyp'), [['all', ui.all], ['f', tx('女性', 'Women', 'Frauen')], ['m', tx('男性', 'Men', 'Männer')], ['c', tx('情侣', 'Couples', 'Paare')], ['t', 'Trans'], ['unknown', tx('未知', 'Unknown', 'Unbekannt')]]), filterInput('country', tx('国家代码', 'Country code', 'Ländercode')), filterInput('tags', tx('标签,用逗号分隔', 'Tags, comma separated', 'Tags, mit Komma trennen')), filterSelect('tagMode', tx('标签匹配', 'Tag matching', 'Tag-Verknüpfung'), [['all', tx('全部标签', 'All tags (AND)', 'Alle Tags (UND)')], ['any', tx('任意标签', 'Any tag (OR)', 'Ein Tag (ODER)')]]), filterInput('viewerMin', tx('最少观众', 'Minimum viewers', 'Zuschauer mindestens'), 'number'), filterInput('viewerMax', tx('最多观众', 'Maximum viewers', 'Zuschauer höchstens'), 'number'), filterInput('ageMin', tx('最低年龄', 'Minimum age', 'Alter mindestens'), 'number'), filterInput('ageMax', tx('最高年龄', 'Maximum age', 'Alter höchstens'), 'number'), filterSelect('local', tx('本地选择', 'Local selection', 'Lokale Auswahl'), [['all', ui.all], ['favorites', tx('收藏', 'Local favorites', 'Lokale Favoriten')], ['watched', tx('已观看', 'Watched locally', 'Lokal angesehen')], ['unwatched', tx('未观看', 'Never watched locally', 'Lokal nie angesehen')], ['organized', tx('已整理', 'Organized locally', 'Lokal organisiert')], ['unorganized', tx('未整理', 'Not organized locally', 'Lokal nicht organisiert')], ['notes', tx('有备注', 'With local notes', 'Mit lokalen Notizen')]]), filterSelect('group', tx('本地分组', 'Local group', 'Lokale Gruppe'), [['', ui.all], ...customLibraryGroups().map(group => [group.id, groupLabel(group)])]), action(tx('重置筛选', 'Reset filters', 'Filter zurücksetzen'), () => { followingFilters = { ...FOLLOWING_FILTER_DEFAULTS }; followingQuery = ''; followingStatusFilter = 'all'; followingPage = 1; savePrefs(); scheduleRender(); }), ]), ]); const countryInput = advanced.querySelector('[data-following-filter="country"]'); countryInput.setAttribute('list', 'rgh-following-countries'); const tagInput = advanced.querySelector('[data-following-filter="tags"]'); tagInput.setAttribute('list', 'rgh-following-tags'); const countries = $('datalist', { id: 'rgh-following-countries' }); const tagsList = $('datalist', { id: 'rgh-following-tags' }); advanced.append(countries, tagsList); const refreshButton = action(ui.refresh, () => { followingState = { ...followingState, status: 'idle', error: '', partial: false }; void startFollowingLoad(revision, statusNode, listNode, refreshButton, cancelButton, rows => drawRows(rows), () => drawState()); }, 'primary'); const cancelButton = action(ui.cancel, () => followingRequest?.abort(), ''); cancelButton.hidden = true; const controls = $('div', { class: 'rgh-toolbar rgh-following-toolbar' }, [ $('label', { class: 'rgh-control' }, [$('span', {}, ui.search), search]), $('label', { class: 'rgh-control' }, [$('span', {}, ui.status), status]), $('label', { class: 'rgh-control' }, [$('span', {}, ui.sortName), sort]), $('label', { class: 'rgh-control' }, [$('span', {}, tx('每页模型数', 'Models per page', 'Models pro Seite')), $('select', { dataset: { followingPageSize: '1' }, onchange: event => { followingPageSize = Number(event.target.value); filterChange(); } }, PAGE_SIZES.map(size => $('option', { value: size, selected: size === followingPageSize }, String(size))))]), refreshButton, cancelButton, ]); const section = $('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, ui.title), $('span', { class: 'rgh-private' }, ui.profile)]), $('div', { class: 'rgh-actions' }, [['all', ui.all], ['live', tx('仅在线', 'Online only', 'Nur online')], ['offline', tx('仅离线', 'Offline only', 'Nur offline')]].map(([value, label]) => { const button = action(label, () => { followingStatusFilter = value; status.value = value; filterChange(); }); button.dataset.followingQuickStatus = value; quickButtons.push(button); return button; })), controls, statsNode, advanced, $('p', { class: 'rgh-bio' }, tx('官方关注列表;观看时间、备注和收藏仅来自此设备。缺少元数据的模型不会匹配具体筛选。', 'Official Following list; watch time, notes and favorites are local to this device. Models with missing metadata do not match specific filters.', 'Offizielle Following-Liste; Sehzeit, Notizen und Favoriten stammen nur von diesem Gerät. Models ohne passende Metadaten erfüllen konkrete Filter nicht.')), statusNode, $('p', { class: 'rgh-bio', dataset: { followingCacheNotice: '1' } }), listNode, ]); target.append(hero('following'), section); let suggestionRows = null; const drawRows = rows => { renderFollowingRows(listNode, rows, itemById, ui); quickButtons.forEach(button => { const active = button.dataset.followingQuickStatus === followingStatusFilter; button.classList.toggle('active', active); button.setAttribute('aria-pressed', String(active)); }); const online = rows.filter(row => row.status === 'live').length; const hasResult = followingState.recognized; const value = number => hasResult ? compactNumber(number) : '…'; statsNode.replaceChildren( metric(ui.all, value(rows.length), followingState.status === 'complete' ? tx('完整列表', 'Complete list', 'Vollständige Liste') : tx('上次或部分结果', 'Previous or partial result', 'Letzter Stand oder Teilergebnis')), metric(ui.live, value(online)), metric(ui.offline, value(rows.length - online)), metric(tx('在线比例', 'Online share', 'Online-Anteil'), hasResult && rows.length ? `${Math.round(online / rows.length * 100)} %` : '—'), metric(tx('已本地观看', 'Watched locally', 'Lokal angesehen'), value(rows.filter(row => itemById.get(row.id)?.totalMs > 0).length)), metric(tx('本地收藏', 'Local favorites', 'Lokale Favoriten'), value(rows.filter(row => isFavorite(itemById.get(row.id)?.room)).length)), metric(tx('列表更新时间', 'List updated', 'Liste aktualisiert'), followingState.updatedAt ? dateTime(followingState.updatedAt) : '—'), ); const extraMetrics = [...statsNode.children].slice(3); const extraRow = $('div', { class: 'rgh-metrics' }, extraMetrics); const oldMore = section.querySelector('[data-hub-tools="following-metrics"]'); const more = hubTools('following-metrics', tx('更多指标与更新时间', 'More metrics and freshness', 'Weitere Kennzahlen und Aktualität'), () => extraRow); if (oldMore) oldMore.replaceWith(more); else statsNode.after(more); if (suggestionRows === rows) return; suggestionRows = rows; const models = rows.map(row => row.metadata || itemById.get(row.id)?.model || {}); countries.replaceChildren(...[...new Set(models.map(model => model.country).filter(Boolean))].sort().map(code => $('option', { value: code }, countryName(code)))); tagsList.replaceChildren(...[...new Set(models.flatMap(model => model.tags || []))].sort().slice(0, 2000).map(tag => $('option', { value: tag }))); }; const drawState = () => { const notice = section.querySelector('[data-following-cache-notice]'); const freshness = followingState.updatedAt ? `${tx('最后成功同步', 'Last successful sync', 'Letzter erfolgreicher Abgleich')} · ${dateTime(followingState.updatedAt)}. ` : ''; notice.textContent = `${freshness}${followingState.saveFailed ? t('followingCacheSaveFailed') : followingState.cached ? t('followingCachePrevious') : followingState.saved ? t('followingCacheSaved') : !followingState.recognized ? tx('列表尚未验证——这不表示你没有关注任何模型。', 'The list has not been verified yet — this does not mean you follow nobody.', 'Die Liste ist noch nicht geprüft – das bedeutet nicht, dass du niemandem folgst.') : ''}`; notice.hidden = !notice.textContent; section.dataset.followingCache = followingState.cached ? 'previous' : followingState.saved ? 'saved' : 'none'; const pages = Number(followingState.pages) || 0; if (followingState.status === 'loading') { statusNode.textContent = `${ui.loading} ${followingUiText(ui.loadingPages, { done: pages })}`; refreshButton.disabled = true; cancelButton.hidden = false; cancelButton.disabled = false; } else { refreshButton.disabled = false; cancelButton.hidden = true; const countText = followingUiText(ui.results, { count: compactNumber(followingState.rows.length), pages: compactNumber(pages) }); if (followingState.status === 'partial') statusNode.textContent = `${followingUiText(ui.partial, { count: compactNumber(followingState.rows.length), pages: compactNumber(pages) })}${followingState.error ? ` ${followingState.error}` : ''}`; else if (followingState.status === 'login') statusNode.textContent = ui.login; else if (followingState.status === 'parser') statusNode.textContent = ui.parser; else if (followingState.status === 'error' || followingState.status === 'cancelled') statusNode.textContent = ui.failed; else statusNode.textContent = countText; } drawRows(followingState.rows); }; search.addEventListener('input', () => { followingQuery = search.value; filterChange(); }); status.value = followingStatusFilter; status.addEventListener('change', () => { followingStatusFilter = status.value; filterChange(); }); sort.value = followingSort; sort.addEventListener('change', () => { followingSort = sort.value; filterChange(); }); drawState(); if (followingState.status === 'idle' || (followingState.status === 'complete' && followingState.cached && Date.now() - followingState.updatedAt > FOLLOWING_FRESH_MS)) void startFollowingLoad(revision, statusNode, listNode, refreshButton, cancelButton, drawRows, drawState); } // Only a complete, recognized list is durable. Never replace it with a // failed/partial refresh, and never reuse another account's private list. function restoreFollowingCache() { const owner = sessionAccount.username; if (!owner || activeAnalytics?.isPrivateSession?.() || followingCacheChecked === owner) return; followingCacheChecked = owner; if (followingState.status !== 'idle') return; try { const value = JSON.parse(localStorage.getItem(FOLLOWING_CACHE_KEY) || 'null'); if (!value || value.schema !== 1 || value.owner !== owner || !Number.isFinite(value.updatedAt) || value.updatedAt <= 0 || value.updatedAt > Date.now() + 60000 || !Number.isSafeInteger(value.pages) || value.pages < 0 || !Array.isArray(value.rows)) return; const ids = new Set(); const rows = value.rows.map(row => { if (!row || !isLikelyUsername(row.id) || normalizeUsername(row.id) !== row.id || ids.has(row.id) || !['live', 'offline'].includes(row.status)) throw new Error('Invalid Following cache row'); ids.add(row.id); const metadata = {}; const source = row.metadata || {}; for (const key of ['country', 'gender']) if (typeof source[key] === 'string') metadata[key] = source[key].slice(0, 80); for (const key of ['age', 'viewerCount', 'followers', 'secondsOnline']) if (typeof source[key] === 'number' && Number.isFinite(source[key]) && source[key] >= 0) metadata[key] = source[key]; if (Array.isArray(source.tags)) metadata.tags = source.tags.filter(tag => typeof tag === 'string').map(tag => tag.slice(0, 80)).slice(0, 80); return { id: row.id, status: row.status, metadata }; }); followingState = { status: 'complete', rows, pages: value.pages, updatedAt: value.updatedAt, partial: false, recognized: true, error: '', cached: true, saved: true }; } catch (_) { /* Corrupt cache cannot block fresh read-only recovery. */ } } function persistFollowingCache() { if (!sessionAccount.username || activeAnalytics?.isPrivateSession?.() || followingState.status !== 'complete' || !followingState.recognized) return; try { localStorage.setItem(FOLLOWING_CACHE_KEY, JSON.stringify({ schema: 1, owner: sessionAccount.username, updatedAt: followingState.updatedAt, pages: followingState.pages, rows: followingState.rows })); followingState.saved = true; } catch (error) { followingState.saveFailed = true; StorageHealth.report('following-cache-save', error); } } function followingSnapshot() { restoreFollowingCache(); return { ...followingState, rows: followingState.rows.map(row => ({ ...row, metadata: row.metadata ? { ...row.metadata, tags: [...(row.metadata.tags || [])] } : null })) }; } function publishFollowing() { window.dispatchEvent(new Event('roomgrid:following-updated')); } function startFollowingLoad(...args) { if (followingLoadPromise) return followingLoadPromise.then(() => { const [revision, statusNode, , , , , drawState] = args; if (revision === renderRevision && statusNode?.isConnected) drawState?.(); }); followingLoadPromise = performFollowingLoad(...args).finally(() => { followingLoadPromise = null; }); return followingLoadPromise; } async function loadFollowingSnapshot({ refresh = false } = {}) { restoreFollowingCache(); if (!refresh && followingState.recognized) { if (!followingLoadPromise && followingState.status === 'complete' && Date.now() - Number(followingState.updatedAt || 0) > FOLLOWING_FRESH_MS) void startFollowingLoad(-1, null, null, null, null, () => {}); return followingSnapshot(); } if (followingLoadPromise) await followingLoadPromise; else if (refresh || followingState.status === 'idle' || Date.now() - Number(followingState.updatedAt || 0) > FOLLOWING_FRESH_MS) { await startFollowingLoad(-1, null, null, null, null, () => {}); } return followingSnapshot(); } async function performFollowingLoad(revision, statusNode, listNode, refreshButton, cancelButton, drawRows, drawState = null) { if (followingRequest) return; const generation = followingAccountGeneration; const previous = followingState; const previousRows = followingState.rows || []; const previousUpdatedAt = followingState.updatedAt || 0; followingState = { ...previous, status: 'loading', rows: previousRows, pages: 0, partial: previousRows.length > 0, error: '', recognized: followingState.recognized, updatedAt: previousUpdatedAt }; const ui = I18N[LANG]?.followingUi || I18N.en.followingUi; const progressTask = activeNotifications?.beginProgress?.(`${ui.loading} ${followingUiText(ui.loadingPages, { done: 0 })}`, { delayMs: 280 }); const update = () => { publishFollowing(); progressTask?.update({ label: `${ui.loading} ${followingUiText(ui.loadingPages, { done: followingState.pages })}`, done: followingState.pages, total: null }); if (drawState) { drawState(); return; } if (!statusNode?.isConnected || revision !== renderRevision) return; statusNode.textContent = `${ui.loading} ${followingUiText(ui.loadingPages, { done: followingState.pages })}`; refreshButton.disabled = true; cancelButton.hidden = false; drawRows(followingState.rows); }; update(); try { const result = await readFollowingPages(progress => { if (generation !== followingAccountGeneration) return; followingState.pages = progress.pages; update(); }); if (generation !== followingAccountGeneration) { progressTask?.finish(); return; } const rows = result.partial && previous.saved ? [...new Map([...previousRows, ...result.rows].map(row => [row.id, row])).values()] : result.rows; followingState = { status: result.partial ? 'partial' : 'complete', rows, pages: result.pages, partial: result.partial, recognized: result.recognized, cached: result.partial && !!previous.saved, saved: result.partial && !!previous.saved, updatedAt: Date.now(), error: result.limitReached ? `Page safety limit (${FOLLOWING_PAGE_LIMIT}) reached.` : result.failures.map(item => item.message).join('; '), }; persistFollowingCache(); } catch (error) { if (generation !== followingAccountGeneration) { progressTask?.finish(); return; } const code = error?.code || 'http'; followingState = { ...previous, status: code === 'cancelled' ? 'cancelled' : code === 'login' ? 'login' : code === 'parser' ? 'parser' : 'error', rows: previousRows, pages: Number(error?.pages) || followingState.pages || 0, partial: previousRows.length > 0, recognized: !!error?.recognized || followingState.recognized, updatedAt: previousUpdatedAt, error: String(error?.message || error), }; } publishFollowing(); if (revision === renderRevision && statusNode?.isConnected) { if (drawState) drawState(); else { refreshButton.disabled = false; cancelButton.hidden = true; const pages = Number(followingState.pages) || 0; const countText = followingUiText(ui.results, { count: compactNumber(followingState.rows.length), pages: compactNumber(pages) }); if (followingState.status === 'partial') statusNode.textContent = `${followingUiText(ui.partial, { count: compactNumber(followingState.rows.length), pages: compactNumber(pages) })}${followingState.error ? ` ${followingState.error}` : ''}`; else if (followingState.status === 'login') statusNode.textContent = ui.login; else if (followingState.status === 'parser') statusNode.textContent = ui.parser; else if (followingState.status === 'error' || followingState.status === 'cancelled') statusNode.textContent = ui.failed; else statusNode.textContent = countText; drawRows(followingState.rows); } } if (revision !== renderRevision && root?.isConnected && viewState.view === 'following') scheduleRender(); progressTask?.finish(); } function interestPanel(id, title, entries, score, valueText, emptyText, metaText = null) { const max = Math.max(1, ...entries.map(item => Number(item[score]) || 0)); const tagPrefix = String(id).includes('tags') ? '#' : ''; return $('article', { class: 'rgh-interest-card', dataset: { interestPanel: id } }, [ $('h3', {}, title), entries.length ? $('div', { class: 'rgh-interest-list' }, entries.map((item, index) => $('div', { class: 'rgh-interest-row' }, [ $('div', { class: 'rgh-interest-line' }, [ $('strong', {}, `${index + 1}. ${tagPrefix}${item.key}`), $('span', {}, valueText(item)), ]), $('div', { class: 'rgh-interest-track' }, $('i', { style: { width: `${Math.max(3, Math.round((Number(item[score]) || 0) / max * 100))}%` } })), $('small', {}, metaText ? metaText(item) : tx(`${item.models.size} 个模型 · ${compactNumber(item.sessions)} 次会话`, `${item.models.size} models · ${compactNumber(item.sessions)} sessions`, `${item.models.size} Models · ${compactNumber(item.sessions)} Sitzungen`, `${item.models.size} modelos · ${compactNumber(item.sessions)} sesiones`, `${item.models.size} modelos · ${compactNumber(item.sessions)} sessões`)), ]))) : $('div', { class: 'rgh-empty rgh-empty-small' }, emptyText), ]); } function userHeader() { return [ hero('user'), sessionAccountPanel(), $('div', { class: 'rgh-toolbar' }, [ selectControl('Zeitraum', viewState.userRange, [[7, '7 Tage'], [30, '30 Tage'], [90, '90 Tage'], [365, '1 Jahr'], [0, 'Gesamter Verlauf']], value => { viewState.userRange = Number(value); scheduleRender(); }), action('Broadcaster-Verbindung verwalten', () => { close(); openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'broadcaster'); }, 'primary'), action('Datenschutz & Export', () => { close(); openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'privacy'); }), ]), ]; } function pendingUserHistory(target) { target.replaceChildren(...userHeader(), $('section', { class: 'rgh-section', dataset: { userHistoryPending: '1' } }, [ $('p', { role: 'status' }, tx( '本地历史仍在加载。账户功能可用;统计数据尚不完整。', 'Local history is still loading. Account controls are available; statistics are not complete yet.', 'Der lokale Verlauf wird noch geladen. Die Kontofunktionen sind bereits bedienbar; die Statistik ist noch nicht vollständig.', 'El historial local sigue cargándose. Las funciones de cuenta están disponibles; las estadísticas aún no están completas.', 'O histórico local ainda está carregando. Os controles da conta estão disponíveis; as estatísticas ainda não estão completas.', )), action(tx('重试', 'Check again', 'Erneut prüfen', 'Comprobar de nuevo', 'Verificar novamente'), () => { invalidateQuietly(); scheduleRender(); }), ])); } function userCoreMetrics(data, summary) { const { eventCounts, uniqueModelCount, visitCount, visitMs, usageCount, revisited, days, selectedUsage, today, activeDays, favoriteRooms } = summary; return $('div', { class: 'rgh-metrics', dataset: { userCoreSummary: '1' } }, [ metric('Heute aktiv', duration(today), 'RoomGrid-Nutzungszeit'), metric('Lokal erfasst seit', data.usage.startedAt ? dateOnly(data.usage.startedAt) : 'Noch nicht', 'ältester RoomGrid-Messpunkt'), metric('Gewählter Zeitraum', duration(selectedUsage), viewState.userRange ? tx(`${viewState.userRange} 天`, `${viewState.userRange} days`, `${viewState.userRange} Tage`, `${viewState.userRange} días`, `${viewState.userRange} dias`) : 'gesamter Verlauf'), metric('Aktive Tage', compactNumber(activeDays), tx(`连续 ${usageStreak(days)} 天`, `${usageStreak(days)}-day streak`, `${usageStreak(days)} Tage Serie`, `Racha de ${usageStreak(days)} días`, `Sequência de ${usageStreak(days)} dias`)), metric('Model-Kontakte', compactNumber(uniqueModelCount), `${compactNumber(visitCount)} Besuche`), metric('Wiederkehr-Quote', `${data.items.length ? Math.round(revisited / data.items.length * 100) : 0} %`, tx(`${revisited} 个重复观看`, `${revisited} watched repeatedly`, `${revisited} wiederholt angesehen`, `${revisited} vistos repetidamente`, `${revisited} assistidos repetidamente`)), metric('Bibliothek', compactNumber(data.rooms.length), `${favoriteRooms} Favoriten`), metric('Live-Vorschauen', compactNumber(eventCounts.preview || 0), `${compactNumber(eventCounts.quickview || 0)} Schnellinfos`), metric('Sehzeit im Zeitraum', duration(visitMs), `${usageCount} Nutzungssitzungen`), ]); } async function renderUser(target, data, revision) { if (activeAnalytics?.isReady?.() === false || data.historyReady === false) { pendingUserHistory(target); return; } const header = userHeader(); const interestPending = () => $('div', { class: 'rgh-loading', role: 'status', dataset: { userInterestPending: '1' } }, tx('正在分批计算本地兴趣数据…', 'Preparing your local interest summary…', 'Deine lokalen Interessendaten werden in kurzen Abschnitten berechnet …')); target.replaceChildren(...header, interestPending()); const summary = await getUserSummary(data, revision, core => { if (revision !== renderRevision || !target.isConnected) return; target.replaceChildren(...header, userCoreMetrics(data, core), interestPending()); }); if (!summary || revision !== renderRevision || !target.isConnected) return; target.replaceChildren(...header); target.dataset.userSummaryCache = summary.cacheHit ? 'hit' : 'miss'; target.dataset.userEventCount = String(summary.eventCount); const { from, eventCount, eventCounts, uniqueModelCount, visitCount, visitMs, usageCount, revisited, days, selectedUsage, today, activeDays, peakDays, top, favoriteRooms, latestByUser, interests } = summary; const statsService = RoomGridUltimate.getAccountStats(activeAnalytics); const official = statsService.status(); const importedAccount = captureHeaderAccountSummary(); const topTags = interests.maps.tags; const topGenres = interests.maps.genres; const topCountries = interests.maps.countries; const topLocations = interests.maps.locations; const topAges = interests.maps.ages; const topExactAges = interests.maps.exactAges; const topTypes = interests.maps.types; const topWeekdays = interests.maps.weekdays; const topDayparts = interests.maps.dayparts; const topOwnTags = interests.maps.ownTags; const topSignalTags = interests.maps.signalTags; const topSignalModels = interests.maps.signalModels; const favoriteTags = interests.maps.favoriteTags; const favoriteCountries = interests.maps.favoriteCountries; const favoriteAges = interests.maps.favoriteAges; const favoriteTypes = interests.maps.favoriteTypes; const percentage = value => `${Math.round(Math.max(0, Number(value) || 0) * 100)} %`; const watchValue = item => duration(item.ms); const favoriteValue = item => tx(`${compactNumber(item.favorites)} 个收藏`, `${compactNumber(item.favorites)} favorites`, `${compactNumber(item.favorites)} Favoriten`, `${compactNumber(item.favorites)} favoritos`, `${compactNumber(item.favorites)} favoritos`); const favoriteMeta = item => tx(`${item.models.size} 个不同模型`, `${item.models.size} distinct models`, `${item.models.size} unterschiedliche Models`, `${item.models.size} modelos diferentes`, `${item.models.size} modelos diferentes`); const signalValue = item => tx(`${compactNumber(item.signals)} 次预览`, `${compactNumber(item.signals)} previews`, `${compactNumber(item.signals)} Vorschauen`, `${compactNumber(item.signals)} vistas previas`, `${compactNumber(item.signals)} prévias`); const signalMeta = item => tx(`${item.models.size} 个模型的预览/快速信息`, `Preview/quick view across ${item.models.size} models`, `Vorschau/Schnellinfo bei ${item.models.size} Models`, `Vista previa/rápida en ${item.models.size} modelos`, `Prévia/visualização rápida em ${item.models.size} modelos`); target.append(userCoreMetrics(data, summary)); target.appendChild($('section', { class: 'rgh-section rgh-interest-center', dataset: { roomgridUserInterests: '1' } }, [ $('div', { class: 'rgh-section-head' }, [ $('h2', {}, tx('我的兴趣统计', 'My interest statistics', 'Meine Interessen-Statistik', 'Mis estadísticas de intereses', 'Minhas estatísticas de interesses')), $('span', { class: 'rgh-private' }, viewState.userRange ? tx(`${viewState.userRange} 天本地数据`, `${viewState.userRange} days of local data`, `${viewState.userRange} Tage lokale Daten`, `${viewState.userRange} días de datos locales`, `${viewState.userRange} dias de dados locais`) : tx('所有本地数据', 'All local data', 'Alle lokalen Daten', 'Todos los datos locales', 'Todos os dados locais')), ]), $('p', { class: 'rgh-bio' }, tx('根据实际本地观看时间、会话、预览、快速信息、收藏和当前已知的公开模型信息计算。', 'Calculated from actual local watch time, sessions, previews, quick views, favorites, and currently known public model information.', 'Berechnet aus tatsächlicher lokaler Sehzeit, Sitzungen, Vorschauen, Schnellinfos, Favoriten und den aktuell bekannten öffentlichen Modelinformationen.', 'Calculado a partir del tiempo de visualización local, sesiones, vistas previas, vistas rápidas, favoritos e información pública conocida de los modelos.', 'Calculado com base no tempo de visualização local, sessões, prévias, visualizações rápidas, favoritos e informações públicas conhecidas dos modelos.')), $('div', { class: 'rgh-interest-summary' }, [ metric(tx('最喜欢的标签', 'Favorite tag', 'Lieblings-Tag'), topTags[0] ? `#${topTags[0].key}` : '—', topTags[0] ? duration(topTags[0].ms) : tx('数据不足', 'not enough data', 'noch zu wenig Daten')), metric(tx('最喜欢的主题', 'Favorite genre', 'Lieblings-Genre'), topGenres[0]?.key || '—', topGenres[0] ? duration(topGenres[0].ms) : tx('来自公开标签', 'from public tags', 'aus öffentlichen Tags')), metric(tx('最常看的年龄', 'Most-watched age', 'Meistgesehenes Alter'), topExactAges[0]?.key || '—', interests.averageAge ? tx(`加权平均 ${interests.averageAge.toFixed(1)}`, `weighted average ${interests.averageAge.toFixed(1)}`, `gewichteter Schnitt ${interests.averageAge.toFixed(1).replace('.', ',')}`) : tx('没有年龄数据', 'no age data', 'keine Altersdaten')), metric(tx('最常看的国家', 'Most-watched country', 'Meistgesehenes Land'), topCountries[0]?.key || '—', topCountries[0] ? duration(topCountries[0].ms) : tx('没有国家数据', 'no country data', 'keine Länderdaten')), metric(tx('最喜欢的类型', 'Favorite model type', 'Bevorzugter Modeltyp'), topTypes[0]?.key || '—', topTypes[0] ? duration(topTypes[0].ms) : tx('没有类型数据', 'no type data', 'keine Typdaten')), metric(tx('最常观看时段', 'Favorite viewing time', 'Bevorzugte Sehzeit'), topDayparts[0]?.key || '—', topDayparts[0] ? duration(topDayparts[0].ms) : tx('没有时间数据', 'no time data', 'keine Zeitdaten')), metric(tx('前三名集中度', 'Top-three concentration', 'Top-3-Konzentration'), percentage(interests.topThreeShare), tx('观看时间用于前三个模型', 'watch time on the top three models', 'Sehzeit bei den drei Top-Models')), metric(tx('收藏匹配度', 'Favorite affinity', 'Favoriten-Anteil'), percentage(interests.favoriteShare), tx('观看时间用于当前收藏', 'watch time on current favorites', 'Sehzeit bei aktuellen Favoriten')), metric('HD-Anteil', percentage(interests.hdShare), tx('高清模型的观看时间', 'watch time on HD models', 'Sehzeit bei HD-Models')), metric(tx('数据覆盖率', 'Metadata coverage', 'Metadaten-Abdeckung'), `${percentage(interests.tagCoverage)} Tags`, `${percentage(interests.ageCoverage)} ${tx('年龄', 'age', 'Alter')} · ${percentage(interests.countryCoverage)} ${tx('国家', 'country', 'Land')}`), ]), $('h3', { class: 'rgh-interest-section-title' }, tx('标签、主题和浏览兴趣', 'Tags, genres and browsing interests', 'Tags, Genres und Stöberinteressen')), $('div', { class: 'rgh-interest-grid' }, [ interestPanel('tags', tx('按观看时间排名的标签', 'Tags by watch time', 'Tags nach Sehzeit'), topTags, 'ms', watchValue, tx('当观看过的模型带有公开标签时,此处会显示你的排名。', 'Your ranking appears when watched models have public tags.', 'Sobald angesehene Models öffentliche Tags besitzen, erscheint hier deine Rangliste.')), interestPanel('genres', tx('最喜欢的主题世界', 'Favorite genre worlds', 'Bevorzugte Themenwelten'), topGenres, 'ms', watchValue, tx('尚无法从公开标签分配主题世界。', 'No genre world can yet be assigned from public tags.', 'Aus öffentlichen Tags lässt sich noch keine Themenwelt zuordnen.')), interestPanel('own-tags', tx('你自己的标签', 'Your own tags', 'Deine eigenen Tags'), topOwnTags, 'ms', watchValue, tx('保存模型的自己的标签也会在此按观看时间评估。', 'Your saved custom tags are also evaluated here by watch time.', 'Deine gespeicherten eigenen Tags werden hier ebenfalls nach Sehzeit ausgewertet.')), interestPanel('signal-tags', tx('预览兴趣标签', 'Preview-interest tags', 'Vorschau-Interessen nach Tags'), topSignalTags, 'signals', signalValue, tx('当你使用实时预览或快速信息时,将显示浏览兴趣。', 'Browsing interests appear when you use live previews or quick views.', 'Stöberinteressen erscheinen, sobald du Live-Vorschauen oder Schnellinfos nutzt.'), signalMeta), interestPanel('signal-models', tx('最常预览的模型', 'Most-previewed models', 'Am häufigsten vorgeschaut'), topSignalModels, 'signals', signalValue, tx('尚未在此期间记录任何预览。', 'No previews have been recorded in this period yet.', 'In diesem Zeitraum wurden noch keine Vorschauen erfasst.'), signalMeta), ]), $('h3', { class: 'rgh-interest-section-title' }, tx('年龄、来源和模型类型', 'Age, origin and model type', 'Alter, Herkunft und Modeltyp')), $('div', { class: 'rgh-interest-grid' }, [ interestPanel('ages', tx('按观看时间划分的年龄段', 'Age groups by watch time', 'Altersgruppen nach Sehzeit'), topAges, 'ms', watchValue, tx('没有足够的公开年龄数据。', 'Not enough public age data is available.', 'Noch nicht genügend öffentliche Altersdaten vorhanden.')), interestPanel('countries', tx('按观看时间划分的国家', 'Countries by watch time', 'Länder nach Sehzeit'), topCountries, 'ms', watchValue, tx('没有足够的公开国家数据。', 'Not enough public country data is available.', 'Noch nicht genügend öffentliche Länderdaten vorhanden.')), interestPanel('locations', tx('最常看的地方', 'Most-watched places', 'Meistgesehene Orte'), topLocations, 'ms', watchValue, tx('没有足够的公开位置信息。', 'Not enough public location data is available.', 'Noch nicht genügend öffentliche Ortsangaben vorhanden.')), interestPanel('types', tx('按观看时间划分的模型类型', 'Model types by watch time', 'Modeltypen nach Sehzeit'), topTypes, 'ms', watchValue, tx('没有足够的公开类型数据。', 'Not enough public model-type data is available.', 'Noch nicht genügend öffentliche Typdaten vorhanden.')), ]), $('h3', { class: 'rgh-interest-section-title' }, tx('观看节奏和收藏信号', 'Viewing rhythm and favorite signals', 'Sehrhythmus und Favoriten-Signale')), $('div', { class: 'rgh-interest-grid' }, [ interestPanel('weekdays', tx('最活跃的星期几', 'Most active weekdays', 'Aktivste Wochentage'), topWeekdays, 'ms', watchValue, tx('没有足够的访问事件。', 'Not enough visit events are available.', 'Noch nicht genügend Besuchsereignisse vorhanden.')), interestPanel('dayparts', tx('一天中的时间', 'Times of day', 'Tageszeiten'), topDayparts, 'ms', watchValue, tx('没有足够的访问事件。', 'Not enough visit events are available.', 'Noch nicht genügend Besuchsereignisse vorhanden.')), interestPanel('favorite-tags', tx('收藏中的标签', 'Tags among favorites', 'Tags deiner Favoriten'), favoriteTags, 'favorites', favoriteValue, tx('当你将模型标记为收藏时,将显示喜欢信号。', 'Preference signals appear when you mark models as favorites.', 'Sobald du Models als Favorit markierst, erscheinen hier Vorlieben.'), favoriteMeta), interestPanel('favorite-countries', tx('收藏中的国家', 'Countries among favorites', 'Länder deiner Favoriten'), favoriteCountries, 'favorites', favoriteValue, tx('收藏中没有已知的国家。', 'No known countries among favorites yet.', 'Noch keine bekannten Länder bei Favoriten.'), favoriteMeta), interestPanel('favorite-ages', tx('收藏中的年龄组', 'Age groups among favorites', 'Altersgruppen deiner Favoriten'), favoriteAges, 'favorites', favoriteValue, tx('收藏中没有已知的年龄。', 'No known ages among favorites yet.', 'Noch keine bekannten Altersangaben bei Favoriten.'), favoriteMeta), interestPanel('favorite-types', tx('收藏中的模型类型', 'Model types among favorites', 'Modeltypen deiner Favoriten'), favoriteTypes, 'favorites', favoriteValue, tx('收藏中没有已知的模型类型。', 'No known model types among favorites yet.', 'Noch keine bekannten Modeltypen bei Favoriten.'), favoriteMeta), ]), $('p', { class: 'rgh-interest-note' }, tx('这些结果不会发送到服务器。只有实际观看的时间才计入观看偏好;预览和收藏单独显示。缺少年龄、国家、地点或标签的数据不会被猜测。', 'These results are not sent to a server. Only actual watch time counts toward viewing preferences; previews and favorites are shown separately. Missing age, country, location, or tag data is never guessed.', 'Diese Ergebnisse werden an keinen Server gesendet. Für Sehvorlieben zählt nur tatsächliche Sehzeit; Vorschauen und Favoriten werden getrennt ausgewiesen. Fehlende Angaben zu Alter, Land, Ort oder Tags werden nicht geraten.', 'Estos resultados no se envían a ningún servidor. Solo el tiempo de visualización real cuenta para las preferencias; las vistas previas y los favoritos se muestran por separado. Los datos que falten no se inventan.', 'Esses resultados não são enviados a nenhum servidor. Apenas o tempo real de visualização conta para as preferências; prévias e favoritos são mostrados separadamente. Dados ausentes não são inventados.')), ])); target.appendChild($('section', { class: 'rgh-section', dataset: { roomgridAccountImport: '1' } }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Bestehende Chaturbate-Kontodaten übernehmen'), $('span', { class: 'rgh-private' }, importedAccount.importedAt ? `Zuletzt: ${dateTime(importedAccount.importedAt)}` : 'Noch nicht eingelesen')]), $('p', { class: 'rgh-bio' }, 'Damit bist du nicht auf Daten seit der Script-Installation beschränkt: RoomGrid kann deine bereits vorhandene Following-Liste und die sicher erkennbaren Einträge aus My Collection lesend übernehmen. Es werden keine Nachrichten, Zahlungen, Medien oder Passwörter kopiert.'), $('div', { class: 'rgh-metrics' }, [ metric('Zuletzt erfasstes Konto', importedAccount.username ? `@${importedAccount.username}` : 'Nicht erkennbar', 'Lokaler Importstand – keine Bestätigung der aktuellen Anmeldung'), metric('Following gesamt', compactNumber(importedAccount.followedTotal), tx(`${compactNumber(importedAccount.followedOnline)} 个正在直播`, `${compactNumber(importedAccount.followedOnline)} live now`, `${compactNumber(importedAccount.followedOnline)} gerade live`, `${compactNumber(importedAccount.followedOnline)} en vivo ahora`, `${compactNumber(importedAccount.followedOnline)} ao vivo agora`)), metric('Following übernommen', compactNumber(importedAccount.followedImported), 'auch von vor der Installation'), metric('My Collection', importedAccount.collectionRecognized ? compactNumber(importedAccount.collectionItems) : '—', tx(`${compactNumber(importedAccount.collectionModels.length)} 个模型已识别`, `${compactNumber(importedAccount.collectionModels.length)} models recognized`, `${compactNumber(importedAccount.collectionModels.length)} Models erkannt`, `${compactNumber(importedAccount.collectionModels.length)} modelos reconocidos`, `${compactNumber(importedAccount.collectionModels.length)} modelos reconhecidos`)), ]), $('div', { class: 'rgh-actions' }, [ action(accountImportBusy ? 'Konto-Daten werden eingelesen …' : 'Vorhandene Konto-Daten jetzt einlesen', () => importExistingAccountData(), 'primary'), action(tx('在 Chaturbate 打开关注列表', 'Open Following on Chaturbate', 'Following bei Chaturbate öffnen', 'Abrir Seguidos en Chaturbate', 'Abrir Seguindo no Chaturbate'), () => openNoopener(new URL('/followed-cams/', location.origin).toString())), action('My Collection öffnen', () => openNoopener(new URL('/my_collection/', location.origin).toString())), action('Offizielle Statistik verbinden', () => { close(); openControlCenter(activeStore, activeAnalytics, activeNotifications, activeWindows, 'broadcaster'); }), action('Ältere Chaturbate-Daten anfordern', () => openNoopener(new URL('/security/privacy/data_export/', location.origin).toString())), ]), importedAccount.lastMessage ? $('p', { style: { color: '#8ee9ed', marginBottom: 0 } }, importedAccount.lastMessage) : null, importedAccount.lastError ? $('p', { style: { color: '#fda4af', marginBottom: 0 } }, tx(`无法读取所有内容:${importedAccount.lastError}`, `Not everything could be read: ${importedAccount.lastError}`, `Nicht alles konnte gelesen werden: ${importedAccount.lastError}`, `No se pudo leer todo: ${importedAccount.lastError}`, `Nem tudo pôde ser lido: ${importedAccount.lastError}`)) : null, $('p', { class: 'rgh-bio', style: { marginBottom: 0 } }, tx('无法直接追溯获取完整的观看时长或访问历史:Chaturbate 没有提供有文档说明的个人历史 API。你可以通过 Chaturbate 的官方数据导出申请已保存个人数据的副本。如果导出中包含可用的历史字段,RoomGrid 可以在检查文件格式后提供安全导入。官方 Stats API 只提供当前主播数值;RoomGrid 只能从首次连接开始建立自己的历史记录。', 'A complete watch-time or visit history cannot be retrieved retroactively because Chaturbate provides no documented personal-history API. You can request a copy of your stored personal data through Chaturbate’s official data export. If that export contains usable history fields, RoomGrid can support a safe import after validating the file format. The official Stats API supplies current broadcaster values; RoomGrid can build its own history only from the first connection onward.', 'Nicht direkt rückwirkend abrufbar ist eine vollständige Sehzeit- oder Besuchshistorie: Chaturbate stellt dafür keine dokumentierte persönliche Verlaufs-API bereit. Über Chaturbates offiziellen Datenexport kannst du eine Kopie deiner gespeicherten personenbezogenen Daten anfordern. Falls der Export nutzbare Verlaufsfelder enthält, kann RoomGrid dafür nach Prüfung des Dateiformats einen sicheren Import erhalten. Die offizielle Stats-API liefert aktuelle Broadcaster-Werte; RoomGrid kann daraus erst ab der ersten Verbindung einen eigenen Verlauf bilden.', 'No se puede recuperar retroactivamente un historial completo de tiempo de visualización o visitas porque Chaturbate no ofrece una API documentada de historial personal. Puedes solicitar una copia de tus datos personales guardados mediante la exportación oficial de datos de Chaturbate. Si contiene campos de historial utilizables, RoomGrid podrá importarlos de forma segura después de validar el formato. La API oficial de estadísticas proporciona valores actuales; RoomGrid solo puede crear su propio historial desde la primera conexión.', 'Não é possível recuperar retroativamente um histórico completo de tempo assistido ou visitas porque o Chaturbate não oferece uma API documentada de histórico pessoal. Você pode solicitar uma cópia dos seus dados pessoais armazenados pela exportação oficial de dados do Chaturbate. Se ela contiver campos de histórico utilizáveis, o RoomGrid poderá importá-los com segurança após validar o formato. A API oficial de estatísticas fornece valores atuais; o RoomGrid só pode criar seu próprio histórico a partir da primeira conexão.')), ])); target.appendChild($('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Deine Aktivität der letzten 14 Tage'), $('span', { class: 'rgh-private' }, 'Nur auf diesem Gerät')]), userChart(days), $('p', { class: 'rgh-bio' }, peakDays.length ? tx( `最活跃的一天:${peakDays[0][0]},时长 ${duration(peakDays[0][1])}。保留期:原始事件 ${data.usage.retentionDays} 天;较早的值可保留为每日汇总。`, `Most active day: ${peakDays[0][0]} with ${duration(peakDays[0][1])}. Retention: ${data.usage.retentionDays} days for raw events; older values can remain as daily totals.`, `Aktivster Tag: ${peakDays[0][0]} mit ${duration(peakDays[0][1])}. Aufbewahrung: ${data.usage.retentionDays} Tage für Rohereignisse; ältere Werte können als Tageswerte erhalten bleiben.`, `Día más activo: ${peakDays[0][0]} con ${duration(peakDays[0][1])}. Conservación: ${data.usage.retentionDays} días para eventos sin procesar; los valores anteriores pueden conservarse como totales diarios.`, `Dia mais ativo: ${peakDays[0][0]} com ${duration(peakDays[0][1])}. Retenção: ${data.usage.retentionDays} dias para eventos brutos; valores anteriores podem permanecer como totais diários.`, ) : 'Noch keine ausreichenden lokalen Nutzungswerte.'), ])); target.appendChild($('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Deine meistgesehenen Models'), action('Vollständige Modelstatistik', () => setView('stats'))]), $('div', { class: 'rgh-list' }, top.length ? top.map(item => $('div', { class: 'rgh-list-row' }, [ $('strong', {}, rawUiText(item.id)), $('span', {}, duration(item.totalMs)), $('span', {}, tx(`${item.sessions} 次会话`, `${item.sessions} sessions`, `${item.sessions} Sitzungen`, `${item.sessions} sesiones`, `${item.sessions} sessões`)), $('span', {}, tx(`${item.activeDays} 天`, `${item.activeDays} days`, `${item.activeDays} Tage`, `${item.activeDays} días`, `${item.activeDays} dias`)), action(tx('打开', 'Open', 'Öffnen', 'Abrir', 'Abrir'), () => openNoopener(roomGridSingleCamUrl(item.id, { source: 'statistics' }))), ])) : [$('div', { class: 'rgh-empty' }, 'Noch keine Model-Sehzeit erfasst.')]), ])); target.appendChild($('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Ereignisse im Zeitraum'), $('span', { class: 'rgh-private' }, tx(`${compactNumber(eventCount)} 个本地保存`, `${compactNumber(eventCount)} saved locally`, `${compactNumber(eventCount)} lokal gespeichert`, `${compactNumber(eventCount)} guardados localmente`, `${compactNumber(eventCount)} salvos localmente`))]), $('div', { class: 'rgh-metrics' }, Object.entries({ Besuche: eventCounts.visit || 0, Vorschauen: eventCounts.preview || 0, Schnellinfos: eventCounts.quickview || 0, Entdeckt: eventCounts.discovered || 0, Filter: eventCounts.filter || 0, Qualität: eventCounts.quality || 0 }).map(([label, value]) => metric(label, compactNumber(value)))), ])); const accountCards = [...latestByUser.values()].map(snapshot => $('article', { class: 'rgh-account-card' }, [ $('h3', {}, `@${snapshot.username}`), $('dl', {}, [ $('dt', {}, 'Token-Guthaben'), $('dd', {}, compactNumber(snapshot.tokenBalance)), $('dt', {}, 'Tipps letzte Stunde'), $('dd', {}, compactNumber(snapshot.tipsLastHour)), $('dt', {}, 'Follower'), $('dd', {}, compactNumber(snapshot.followers)), $('dt', {}, tx('观众', 'Viewers', 'Zuschauer', 'Espectadores', 'Espectadores')), $('dd', {}, compactNumber(snapshot.viewers)), $('dt', {}, 'Zufriedenheit'), $('dd', {}, `${snapshot.satisfactionScore || 0} %`), $('dt', {}, tx('好评(90天)', 'Positive votes (90 days)', 'Positive Bewertungen (90 Tage)')), $('dd', {}, compactNumber(snapshot.votesUp)), $('dt', {}, tx('差评(90天)', 'Negative votes (90 days)', 'Negative Bewertungen (90 Tage)')), $('dd', {}, compactNumber(snapshot.votesDown)), $('dt', {}, tx('上次直播', 'Last broadcast', 'Letzte Sendung')), $('dd', {}, rawUiText(snapshot.lastBroadcast || '—')), $('dt', {}, tx('在线分钟', 'Online minutes', 'Online-Minuten')), $('dd', {}, snapshot.timeOnline >= 0 ? compactNumber(snapshot.timeOnline) : '—'), $('dt', {}, tx('已注册观众', 'Registered viewers', 'Registrierte Zuschauer')), $('dd', {}, compactNumber(snapshot.registeredViewers)), $('dt', {}, tx('持有代币的观众', 'Viewers with tokens', 'Zuschauer mit Token-Guthaben')), $('dd', {}, compactNumber(snapshot.tokenedViewers)), $('dt', {}, 'Letzte Messung'), $('dd', {}, dateTime(snapshot.at)), ]), ])); target.appendChild($('section', { class: 'rgh-section' }, [ $('div', { class: 'rgh-section-head' }, [$('h2', {}, 'Optionale offizielle Kontostatistik'), $('span', { class: 'rgh-private' }, official.unlocked ? 'Verbunden' : 'Nicht verbunden')]), $('p', { class: 'rgh-bio' }, 'RoomGrid kann persönliche Chaturbate-Kontowerte nicht erraten. Diese Karten erscheinen nur, wenn du die offizielle Broadcaster-Statistik bewusst mit deinem eigenen Benutzernamen und Token verbunden hast.'), $('div', { class: 'rgh-account-grid' }, accountCards.length ? accountCards : [$('div', { class: 'rgh-empty' }, official.latest ? 'Die aktuelle Verbindung ist aktiv; nach der nächsten Messung erscheint hier der lokale Verlauf.' : 'Noch keine offiziellen Konto-Snapshots vorhanden.')]), ])); const interestsNode = target.querySelector('[data-roomgrid-user-interests]'); foldInterestTopics(interestsNode); const importedNode = target.querySelector('[data-roomgrid-account-import]'); const officialNode = target.lastElementChild; const localSections = [...target.children].filter(node => node.classList.contains('rgh-section') && node !== interestsNode && node !== importedNode && node !== officialNode && !node.querySelector('[data-session-account]')); const localHost = $('div', {}, localSections); const interestHost = $('div', {}, interestsNode ? [interestsNode] : []); const accountHost = $('div', {}, [importedNode, officialNode].filter(Boolean)); compactMetrics(target, 'user-metrics', 3); target.append( hubTools('user-usage', tx('本地使用情况', 'My local usage', 'Meine lokale Nutzung'), () => localHost, tx('仅本设备记录', 'Recorded on this device only', 'Nur auf diesem Gerät erfasst')), hubTools('user-interests', tx('我的兴趣', 'My interests', 'Meine Interessen'), () => interestHost, tx('标签、收藏与观看偏好', 'Tags, favorites and watch preferences', 'Tags, Favoriten und Sehvorlieben')), hubTools('user-account', tx('账户与导入数据', 'Account and imported information', 'Konto und übernommene Informationen'), () => accountHost, tx('官方账户与本地使用分开', 'Official account separate from local usage', 'Offizielles Konto getrennt von lokaler Nutzung'))); } function hero(view) { const copy = { stats: [ tx('统计中心', 'Statistics Center', 'Statistikzentrum', 'Centro de estadísticas', 'Central de estatísticas'), tx('比较所有在本地观察、保存和观看的模型,包括观看时间、会话、活动、预览、质量和排名。', 'Compare every locally observed, saved, and watched model—with watch time, sessions, activity, previews, quality, and rankings.', 'Vergleiche alle lokal beobachteten, gespeicherten und angesehenen Models – mit Sehzeit, Sitzungen, Aktivität, Vorschauen, Qualität und Ranglisten.', 'Compara todos los modelos observados, guardados y vistos localmente, con tiempo de visualización, sesiones, actividad, vistas previas, calidad y clasificaciones.', 'Compare todos os modelos observados, salvos e assistidos localmente, com tempo de visualização, sessões, atividade, prévias, qualidade e classificações.'), ], library: [ tx('大型模型库', 'Large model library', 'Große Modellbibliothek', 'Gran biblioteca de modelos', 'Grande biblioteca de modelos'), tx('将你的模型整理成包含丰富图片的便捷档案,提供直播状态、备注、标签、评分、优先级、分组和直接操作。', 'Your models as image-rich, convenient profiles with live status, notes, tags, ratings, priority, groups, and direct actions.', 'Deine Models als bildreiche, komfortable Profile mit Live-Status, Notizen, Tags, Bewertung, Priorität, Gruppen und direkten Aktionen.', 'Tus modelos como perfiles cómodos y visuales, con estado en vivo, notas, etiquetas, valoración, prioridad, grupos y acciones directas.', 'Seus modelos como perfis práticos e visuais, com status ao vivo, notas, tags, avaliação, prioridade, grupos e ações diretas.'), ], user: [ tx('我的用户面板', 'My user dashboard', 'Mein Nutzer-Dashboard', 'Panel de mi usuario', 'Painel do meu usuário'), tx('你的私人本地使用历史、习惯、库数据,以及仅在主动连接时显示的官方主播账户数据。', 'Your private local usage history, habits, library data, and—only when deliberately connected—official broadcaster account data.', 'Dein privater lokaler Nutzungsverlauf, Gewohnheiten, Bibliothekswerte und – nur bei bewusster Verbindung – offizielle Broadcaster-Kontodaten.', 'Tu historial de uso local privado, hábitos, datos de la biblioteca y, solo si conectas la cuenta de forma consciente, datos oficiales de la cuenta de broadcaster.', 'Seu histórico de uso local privado, hábitos, dados da biblioteca e, somente quando você conecta a conta de forma consciente, dados oficiais da conta de broadcaster.'), ], following: [ (I18N[LANG]?.followingUi || I18N.en.followingUi).title, (I18N[LANG]?.followingUi || I18N.en.followingUi).description, ], }[view]; return $('section', { class: `rgh-hero${view === 'library' ? ' rgh-library-heading' : ''}` }, [ $('div', {}, view === 'library' ? [$('h1', {}, copy[0])] : [$('p', { class: 'rgh-kicker' }, `RoomGrid v${META.version}`), $('h1', {}, copy[0]), $('p', {}, copy[1])]), $('span', { class: 'rgh-private' }, tx('🔒 本地且仅存于此设备', '🔒 Local & private on this device', '🔒 Lokal & privat auf diesem Gerät', '🔒 Local y privado en este dispositivo', '🔒 Local e privado neste dispositivo')), ]); } function captureHubSearchFocus() { const active = document.activeElement; if (!content?.contains(active)) return null; const draftField = active?.dataset?.libraryDraftField; const model = draftField ? active.closest('[data-roomgrid-library-model]')?.dataset.roomgridLibraryModel : null; if (active?.dataset?.hubSearch !== viewState.view && !(viewState.view === 'library' && model)) return null; try { return { view: viewState.view, model, draftField, start: active.selectionStart, end: active.selectionEnd, direction: active.selectionDirection, }; } catch (_) { return { view: viewState.view, model, draftField, start: null, end: null, direction: 'none' }; } } function restoreHubSearchFocus(snapshot, revision) { if (!snapshot || revision !== renderRevision || snapshot.view !== viewState.view || !content) return; const active = document.activeElement; if (active && active !== document.body && active.isConnected) return; const card = snapshot.model ? [...content.querySelectorAll('[data-roomgrid-library-model]')].find(node => node.dataset.roomgridLibraryModel === snapshot.model) : null; const input = snapshot.model ? card?.querySelector(`[data-library-draft-field="${snapshot.draftField}"]`) : content.querySelector(`input[data-hub-search="${snapshot.view}"]`); if (!input) return; try { input.focus({ preventScroll: true }); } catch (_) { input.focus(); } if (snapshot.start == null || typeof input.setSelectionRange !== 'function') return; const start = Math.max(0, Math.min(input.value.length, Number(snapshot.start) || 0)); const end = Math.max(start, Math.min(input.value.length, Number(snapshot.end) || 0)); try { input.setSelectionRange(start, end, snapshot.direction || 'none'); } catch (_) {} } async function render() { if (!root || !content) return; syncPrivateCacheState(); const searchFocus = captureHubSearchFocus(); const revision = ++renderRevision; const progressTask = activeNotifications?.beginProgress?.(tx('统计与资料视图正在准备', 'Preparing statistics and library view', 'Statistik- und Bibliotheksansicht wird vorbereitet'), { delayMs: 280 }); applyLibraryLayout(); syncHeaderButtons(); content.setAttribute('aria-busy', 'true'); // Keep the last complete page visible while another hub view is built. // The shared data index is already warm in the common case, so replacing // useful content with a blocking loader only creates a visible flash. if (!content.childElementCount) { const loading = $('div', { class: 'rgh-loading', role: 'status' }, tx('正在准备数据…', 'Preparing data…', 'Daten werden vorbereitet …', 'Preparando datos…', 'Preparando dados…')); if (viewState.view === 'stats' || viewState.view === 'library') content.replaceChildren(hero(viewState.view), mainToolbar(viewState.view), loading); else content.replaceChildren(loading); } else content.classList.add('rgh-refreshing'); // Identity/account actions do not depend on the full historical index. // Keep them available even during the very first index preparation. if (viewState.view === 'user' && !content.querySelector('[data-session-account]')) pendingUserHistory(content); try { // Paint from the currently available synchronous snapshot. The full // analytics/index build is scheduled by loadData and replaces this // view quietly when it finishes. const data = await loadData({ allowPartial: true }); if (!root || revision !== renderRevision) return; // A previously completed DOM keeps form handlers, expanded sections, // decoded covers and scrollable content. It is reused only for the same // index, preferences and freshness window; Following retains its own // request lifecycle and deliberately does not enter this cache. const pageKey = `${JSON.stringify(viewState)}:${activeStore.revision}:${LANG}:${Math.floor(Date.now() / 60000)}:${catalogStatusGeneration}:${mediaCacheGeneration}:${sessionAccount.checkedAt}`; const savedPage = hubPageCache.get(viewState.view); if (viewState.view !== 'following' && savedPage?.data === data && savedPage.key === pageKey) { content.replaceChildren(...savedPage.nodes); content.dataset.hubPageCache = 'hit'; if (viewState.view === 'user') { content.dataset.userSummaryCache = 'hit'; content.dataset.userEventCount = savedPage.userEventCount; } content.classList.remove('rgh-refreshing'); content.setAttribute('aria-busy', 'false'); restoreHubSearchFocus(searchFocus, revision); return; } content.dataset.hubPageCache = 'miss'; if (viewState.view !== 'library' && viewState.view !== 'stats') content.replaceChildren(); if (viewState.view === 'library') await renderLibrary(content, data, revision); else if (viewState.view === 'user') await renderUser(content, data, revision); else if (viewState.view === 'following') await renderFollowing(content, data, revision); else await renderStats(content, data, revision); if (root && revision === renderRevision) { restoreHubSearchFocus(searchFocus, revision); content.classList.remove('rgh-refreshing'); content.setAttribute('aria-busy', 'false'); if (viewState.view !== 'following' && !content.querySelector('[data-user-history-pending]')) { const completeKey = `${JSON.stringify(viewState)}:${activeStore.revision}:${LANG}:${Math.floor(Date.now() / 60000)}:${catalogStatusGeneration}:${mediaCacheGeneration}:${sessionAccount.checkedAt}`; hubPageCache.set(viewState.view, { key: completeKey, data, nodes: [...content.childNodes], userEventCount: content.dataset.userEventCount }); } } } catch (error) { if (revision === renderRevision && content) { content.classList.remove('rgh-refreshing'); content.setAttribute('aria-busy', 'false'); content.replaceChildren($('div', { class: 'rgh-empty', role: 'alert' }, tx( `无法加载本地数据:${String(error?.message || error)}`, `Local data could not be loaded: ${String(error?.message || error)}`, `Die lokalen Daten konnten nicht geladen werden: ${String(error?.message || error)}`, `No se pudieron cargar los datos locales: ${String(error?.message || error)}`, `Não foi possível carregar os dados locais: ${String(error?.message || error)}`, ))); } } finally { progressTask?.finish(); } } const scheduleRender = debounce(scrollTop => { if (scrollTop && root) root.scrollTop = 0; render(); }, 80); function syncInternalTabs() { let activeButton = null; root?.querySelectorAll('[data-hub-tab]').forEach(button => { const active = button.dataset.hubTab === viewState.view; button.classList.toggle('is-active', active); button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1; if (active) activeButton = button; }); if (activeButton && content) content.setAttribute('aria-labelledby', activeButton.id); } function trapFocus(container, event) { if (event.key !== 'Tab' || !container?.isConnected) return false; const focusable = [...container.querySelectorAll('button:not(:disabled),summary,input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href],[tabindex]:not([tabindex="-1"])')] .filter(node => node.offsetParent !== null && node.checkVisibility?.() !== false); if (!focusable.length) { event.preventDefault(); container.focus?.(); return true; } const first = focusable[0]; const last = focusable[focusable.length - 1]; if (!container.contains(document.activeElement)) { event.preventDefault(); (event.shiftKey ? last : first).focus(); return true; } if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); return true; } if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); return true; } return false; } function setView(view) { const next = ['stats', 'library', 'user', 'following'].includes(view) ? view : 'stats'; if (embedded && embeddedViewHandler) { embeddedViewHandler(next); return; } viewState.view = next; syncHeaderButtons(); syncInternalTabs(); if (root) { root.scrollTop = 0; render(); } else open(viewState.view); } function createRoot(target = document.body, embeddedMode = false) { embedded = !!embeddedMode; embeddedTarget = embedded ? target : null; root = $('div', { class: `rgh-root${embedded ? ' rgh-root-embedded' : ''}`, dataset: { roomgridNativeHub: '1', roomgridEmbeddedHub: embedded ? '1' : '0', density: viewState.density }, role: embedded ? 'region' : 'dialog', ariaModal: embedded ? null : 'true', ariaLabel: tx('RoomGrid 舒适中心', 'RoomGrid Comfort Center', 'RoomGrid Komfortzentrum', 'Centro RoomGrid', 'Central RoomGrid'), lang: LANG, tabIndex: '-1' }); const tabs = $('nav', { class: 'rgh-tabs', role: 'tablist', ariaLabel: tx('RoomGrid 页面', 'RoomGrid pages', 'RoomGrid-Seiten', 'Páginas de RoomGrid', 'Páginas do RoomGrid') }, [ ['stats', `▥ ${tx('统计中心', 'Statistics Center', 'Statistikzentrum', 'Centro de estadísticas', 'Central de estatísticas')}`], ['library', `★ ${tx('模型库', 'Model library', 'Modellbibliothek', 'Biblioteca de modelos', 'Biblioteca de modelos')}`], ['following', `♥ ${(I18N[LANG]?.followingUi || I18N.en.followingUi).nav}`], ['user', `◎ ${tx('我的用户', 'My user', 'Mein Nutzer', 'Mi usuario', 'Meu usuário')}`], ].map(([id, label]) => $('button', { type: 'button', id: `roomgrid-native-hub-tab-${id}`, role: 'tab', 'aria-controls': 'roomgrid-native-hub-panel', class: viewState.view === id ? 'is-active' : '', ariaSelected: String(viewState.view === id), tabIndex: viewState.view === id ? '0' : '-1', onclick: () => setView(id), dataset: { hubTab: id } }, label))); tabs.addEventListener('keydown', event => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; const buttons = [...tabs.querySelectorAll('[data-hub-tab]')]; const current = Math.max(0, buttons.indexOf(document.activeElement)); const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (current + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length; event.preventDefault(); buttons[next]?.click(); buttons[next]?.focus(); }); const topbar = $('div', { class: 'rgh-topbar' }, [ $('div', { class: 'rgh-logo' }, 'RG'), $('div', { class: 'rgh-title' }, [ $('strong', {}, tx('RoomGrid 舒适中心', 'RoomGrid Comfort Center', 'RoomGrid Komfortzentrum', 'Centro RoomGrid', 'Central RoomGrid')), $('small', {}, `${tx('位于标准 Chaturbate 页面中', 'inside the standard Chaturbate page', 'in der normalen Chaturbate-Seite', 'dentro de la página normal de Chaturbate', 'dentro da página normal do Chaturbate')} · v${META.version}`), ]), tabs, $('button', { class: 'rgh-menu-button', type: 'button', dataset: { roomgridHubMenuButton: '1' }, onclick: event => openGlobalMenu(event.currentTarget), title: tx('所有 RoomGrid 页面和设置', 'All RoomGrid pages and settings', 'Alle RoomGrid-Seiten und Einstellungen') }, '☰'), $('button', { class: 'rgh-close', type: 'button', onclick: close, title: tx('关闭', 'Close', 'Schließen') }, '×'), ]); content = $('main', { class: 'rgh-content', id: 'roomgrid-native-hub-panel', role: 'tabpanel', ariaBusy: 'false' }); root.appendChild($('div', { class: 'rgh-shell' }, embedded ? [content] : [topbar, content])); target.appendChild(root); applyLibraryLayout(); if (!embedded) { previousOverflow = document.documentElement.style.overflow; document.documentElement.style.overflow = 'hidden'; syncTop(); } render(); syncInternalTabs(); if (!embedded) requestAnimationFrame(() => root?.querySelector('[data-hub-tab][aria-selected="true"]')?.focus()); } function open(view = 'stats') { viewState.view = ['stats', 'library', 'user', 'following'].includes(view) ? view : 'stats'; if (detectPageType() !== 'discover') { location.href = suiteUrl(viewState.view); return true; } closeHeaderGroupMenu(); installStyles(); ensureHeaderMenu(); if (embedded) close(); if (!root?.isConnected) { previousHubFocus = document.activeElement; createRoot(document.body, false); } else { root.scrollTop = 0; syncInternalTabs(); render(); } syncHeaderButtons(); return true; } function mount(target, view, store, analytics, notifications, windows, onView = null) { if (!target) return false; lightweight = false; const bindingsChanged = (!!activeStore && activeStore !== store) || (!!activeAnalytics && activeAnalytics !== analytics); activeStore = store; activeAnalytics = analytics; activeNotifications = notifications; activeWindows = windows; readPrefs(); installStyles(); const next = ['stats', 'library', 'user', 'following'].includes(view) ? view : 'stats'; if (root?.isConnected && (!embedded || embeddedTarget !== target)) close(); embedded = true; embeddedTarget = target; embeddedViewHandler = typeof onView === 'function' ? onView : null; viewState.view = next; if (bindingsChanged) { libraryDrafts.clear(); libraryEditorId = ''; embeddedSubscriptions.forEach(unsubscribe => unsubscribe()); embeddedSubscriptions = []; cache = null; hubPageCache.clear(); modelCoverCache.clear(); mediaCacheGeneration += 1; } if (!embeddedSubscriptions.length) { embeddedSubscriptions = [store.subscribe(invalidate), analytics.subscribe(invalidateQuietly)]; } if (!embeddedCatalogHandler) { embeddedCatalogHandler = debounce(() => { catalogStatusGeneration += 1; if (root?.isConnected && viewState.view === 'library') scheduleRender(); }, 750); window.addEventListener('roomgrid:live-catalog-updated', embeddedCatalogHandler); embeddedCoverHandler = event => { mediaCacheGeneration += 1; modelCoverCache.delete(event.detail?.id); hubPageCache.clear(); if (root?.isConnected) scheduleRender(); }; window.addEventListener('roomgrid:model-cover-changed', embeddedCoverHandler); window.addEventListener('roomgrid:model-media-removed', embeddedCoverHandler); } // Switching Stats -> Library -> User changes only the presentation. // Store/analytics subscriptions invalidate real data changes already. // Reusing the warmed index makes all three embedded pages immediate. if (bindingsChanged) invalidate(); if (!root?.isConnected) createRoot(target, true); else { applyLibraryLayout(); render(); } return true; } function unmountEmbedded() { if (embedded) close(); } function close() { const wasOpen = !!root; const wasEmbedded = embedded; if (wasEmbedded) { // Keep bounded covers/pages across menu navigation. The media/catalog // listeners stay registered so changes made in SingleCam while this // view is absent cannot revive a deleted or replaced cover on return. // Subscriptions only mark unseen data dirty; the expensive refresh // scheduler stays dormant while no hub view is mounted. } closeGlobalMenu(false); clearTimeout(hoverTimer); RoomGridUltimate.stopInlineHoverPreview(); cancelIdleWork(mediaHydrationHandle); clearTimeout(mediaHydrationTimer); mediaHydrationHandle = 0; mediaHydrationTimer = 0; mediaHydrationQueue = []; renderRevision += 1; root?.remove(); root = null; content = null; if (wasOpen && !wasEmbedded) document.documentElement.style.overflow = previousOverflow; embedded = false; embeddedTarget = null; embeddedViewHandler = null; syncHeaderButtons(); const returnTarget = previousHubFocus; previousHubFocus = null; if (wasOpen && returnTarget?.isConnected) requestAnimationFrame(() => returnTarget.focus?.()); } function init(store, analytics, notifications, windows) { lightweight = false; activeStore = store; activeAnalytics = analytics; activeNotifications = notifications; activeWindows = windows; readPrefs(); installStyles(); ensureHeaderMenu(); const ensureSoon = debounce(() => { ensureHeaderMenu(); scheduleHomeGroupFilter(); }, 180); observer = new MutationObserver(ensureSoon); observer.observe(document.body, { childList: true, subtree: true }); const onResize = () => { syncTop(); positionHeaderGroupMenu(); positionGlobalMenu(); scheduleHomeGroupFilter(); }; const onScroll = () => { syncTop(); positionHeaderGroupMenu(); }; const onRoute = () => scheduleHomeGroupFilter(); const onEnsureUi = () => { ensureHeaderMenu(); syncTop(); scheduleHomeGroupFilter(); }; window.addEventListener('resize', onResize, { passive: true }); window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('popstate', onRoute); window.addEventListener('pageshow', onRoute); window.addEventListener('roomgrid:navigation', onEnsureUi); window.addEventListener('roomgrid:ensure-ui', onEnsureUi); window.addEventListener('camclient:watch-only', scheduleCacheWarmup); const onKey = event => { if (event.key === 'Escape') { if (globalMenuPanel) { event.preventDefault(); closeGlobalMenu(); } else if (groupMenuPanel) { event.preventDefault(); closeHeaderGroupMenu(); } else if (root) { event.preventDefault(); close(); } return; } if (globalMenuPanel) trapFocus(globalMenuPanel, event); else if (groupMenuPanel) trapFocus(groupMenuPanel, event); else if (root) trapFocus(root, event); }; document.addEventListener('keydown', onKey, true); unsubscribeStore = store.subscribe((state, path) => { invalidate(state, path); scheduleHomeGroupFilter(); if (groupMenuPanel) renderHeaderGroupMenu(); }); unsubscribeAnalytics = analytics.subscribe(invalidateQuietly); scheduleCacheWarmup(); const params = new URLSearchParams(location.search); const requested = params.get('roomgrid_hub'); if (['stats', 'library', 'user', 'following'].includes(requested)) setTimeout(() => { location.replace(suiteUrl(requested)); }, 160); if (params.get('roomgrid_groups') === '1') setTimeout(() => openHeaderGroupMenu(groupMenuButton), 180); setTimeout(applyHomeGroupFilter, 120); return () => { closeGlobalMenu(false); closeHeaderGroupMenu(); close(); observer?.disconnect(); observer = null; unsubscribeStore?.(); unsubscribeAnalytics?.(); window.removeEventListener('camclient:watch-only', scheduleCacheWarmup); cancelIdleWork(cacheWarmupHandle); cancelIdleWork(cacheRefreshHandle); clearTimeout(cacheRefreshTimer); cacheWarmupHandle = 0; cacheRefreshHandle = 0; cacheRefreshTimer = 0; window.removeEventListener('roomgrid:live-catalog-updated', embeddedCatalogHandler); window.removeEventListener('roomgrid:model-cover-changed', embeddedCoverHandler); window.removeEventListener('roomgrid:model-media-removed', embeddedCoverHandler); embeddedCatalogHandler = null; embeddedCoverHandler = null; embeddedSubscriptions.forEach(unsubscribe => unsubscribe()); embeddedSubscriptions = []; hubPageCache.clear(); modelCoverCache.clear(); mediaCacheGeneration += 1; unsubscribeStore = null; unsubscribeAnalytics = null; document.removeEventListener('keydown', onKey, true); window.removeEventListener('resize', onResize); window.removeEventListener('scroll', onScroll); window.removeEventListener('popstate', onRoute); window.removeEventListener('pageshow', onRoute); window.removeEventListener('roomgrid:navigation', onEnsureUi); window.removeEventListener('roomgrid:ensure-ui', onEnsureUi); document.querySelectorAll('.rgh-home-group-hidden').forEach(card => card.classList.remove('rgh-home-group-hidden')); document.body?.classList?.remove('rgh-home-group-filter-active'); removeHomeFilterBanner(); headerMenu?.remove(); headerMenu = null; headerHost?.removeAttribute?.('data-roomgrid-header-host'); headerHost = null; style?.remove(); style = null; groupMenuButton = null; activeStore = null; }; } function lightweightHeaderLinks() { lightweight = true; installStyles(); ensureHeaderMenu(); const ensureSoon = debounce(ensureHeaderMenu, 180); observer = new MutationObserver(ensureSoon); observer.observe(document.body, { childList: true, subtree: true }); const onEnsureUi = () => ensureHeaderMenu(); window.addEventListener('roomgrid:navigation', onEnsureUi); window.addEventListener('roomgrid:ensure-ui', onEnsureUi); return () => { closeGlobalMenu(false); closeHeaderGroupMenu(); observer?.disconnect(); observer = null; window.removeEventListener('roomgrid:navigation', onEnsureUi); window.removeEventListener('roomgrid:ensure-ui', onEnsureUi); headerMenu?.remove(); headerMenu = null; headerHost?.removeAttribute?.('data-roomgrid-header-host'); headerHost = null; style?.remove(); style = null; groupMenuButton = null; lightweight = false; }; } return { init, open, close, mount, unmountEmbedded, captureSessionAccount, followingSnapshot, loadFollowingSnapshot, openMenu: openGlobalMenu, lightweightHeaderLinks }; })(); /* ============================================================= * 3.5. Benachrichtigungen, Datenschutz und Fensterkoordination * Lokal, begrenzt und ohne Cloud-/Telemetriepfad. * ============================================================= */ const PRIVACY_STORE_KEY = 'ryujo_multicam_privacy_v1'; const NOTIFICATION_STORE_KEY = 'ryujo_multicam_notifications_v1'; const NOTIFICATION_LEADER_KEY = 'ryujo_multicam_notification_leader_v1'; const WINDOW_COORDINATION_KEY = 'ryujo_multicam_windows_v1'; function safeLocalJson(key, fallback) { try { const parsed = JSON.parse(localStorage.getItem(key) || 'null'); return parsed && typeof parsed === 'object' ? parsed : fallback; } catch (_) { return fallback; } } function saveLocalJson(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { StorageHealth.report('control-center-save', error); return false; } } // Explicit Camclient-owned preferences only. Never enumerate website // storage, account-token vaults, filesystem handles or live coordination. const COMPLETE_BACKUP_RESOURCE_KEYS = Object.freeze([ 'ryujo_multicam_discover_v1', 'ryujo_multicam_modules_v1', 'ryujo_multicam_privacy_v1', 'ryujo_multicam_notifications_v1', 'ryujo_multicam_optional_tools_v1', 'ryujo_multicam_group_user_collapsed_v1', 'roomgrid_audio_silencer_v1', 'roomgrid_preview_layout_v1', 'roomgrid_native_hub_prefs_v1', 'roomgrid_native_account_import_v1', 'roomgrid_following_snapshot_v1', 'multicam_lang', 'ryujo_multicam_tip_sound_muted_v1', 'ryujo_fab_collapsed', ]); const COMPLETE_BACKUP_SCALAR_KEYS = new Set(['multicam_lang', 'ryujo_multicam_tip_sound_muted_v1', 'ryujo_fab_collapsed']); const COMPLETE_BACKUP_MAX_BYTES = 512 * 1024 * 1024; function validateCompleteBackupResources(input) { if (!input || input.schema !== 1 || !input.local || typeof input.local !== 'object' || Array.isArray(input.local)) throw new Error('Invalid supplementary preferences'); const clean = {}; const validateJson = (value, depth = 0) => { if (depth > 30) throw new Error('Supplementary preferences are nested too deeply'); if (value && typeof value === 'object') for (const [key, item] of Object.entries(value)) { if (['__proto__', 'prototype', 'constructor'].includes(key)) throw new Error('Unsafe preference property'); validateJson(item, depth + 1); } }; for (const [key, raw] of Object.entries(input.local)) { if (!COMPLETE_BACKUP_RESOURCE_KEYS.includes(key) || typeof raw !== 'string') throw new Error('Unknown or invalid supplementary preference'); if (COMPLETE_BACKUP_SCALAR_KEYS.has(key)) { if (key === 'multicam_lang' ? !/^[a-z]{2,3}(?:-[a-zA-Z]{2,4})?$/u.test(raw) : !['0', '1', 'true', 'false'].includes(raw)) throw new Error('Invalid scalar preference'); } else { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid supplementary preference object'); validateJson(parsed); if (key === 'ryujo_multicam_privacy_v1' && Object.keys(parsed).some(field => !['neutralText', 'shortcut'].includes(field))) throw new Error('Privacy credentials must not be imported'); } clean[key] = raw; } return { schema: 1, local: clean }; } function exportCompleteBackupResources() { const local = {}; for (const key of COMPLETE_BACKUP_RESOURCE_KEYS) { const raw = localStorage.getItem(key); // Fail visibly on unreadable storage. if (raw == null) continue; if (key === 'ryujo_multicam_privacy_v1') { const parsed = sanitizePrivacyState(JSON.parse(raw)); local[key] = JSON.stringify({ neutralText: parsed.neutralText, shortcut: parsed.shortcut }); } else local[key] = raw; } return validateCompleteBackupResources({ schema: 1, local }); } function applyCompleteBackupResources(input, strategy = 'merge') { const resources = validateCompleteBackupResources(input); const merge = (current, incoming) => { if (Array.isArray(current) && Array.isArray(incoming)) { const keyed = [...current, ...incoming].every(item => item && typeof item === 'object' && typeof item.id === 'string'); if (keyed) { const items = new Map(current.map(item => [item.id, item])); incoming.forEach(item => items.set(item.id, items.has(item.id) ? merge(items.get(item.id), item) : item)); return [...items.values()]; } return [...new Map([...current, ...incoming].map(item => [JSON.stringify(item), item])).values()]; } if (current && incoming && typeof current === 'object' && typeof incoming === 'object' && !Array.isArray(current) && !Array.isArray(incoming)) { const result = { ...current }; for (const [key, value] of Object.entries(incoming)) result[key] = merge(current[key], value); return result; } return incoming; }; const previous = new Map(); try { for (const [key, raw] of Object.entries(resources.local)) { const current = localStorage.getItem(key); previous.set(key, current); const keepPrivacyPin = key === 'ryujo_multicam_privacy_v1'; const value = key === 'roomgrid_following_snapshot_v1' && current && strategy === 'merge' ? current : !COMPLETE_BACKUP_SCALAR_KEYS.has(key) && current && (strategy === 'merge' || keepPrivacyPin) ? JSON.stringify(merge(JSON.parse(current), JSON.parse(raw))) : raw; localStorage.setItem(key, value); } } catch (error) { let rollbackFailed = false; for (const [key, value] of previous) try { if (value == null) localStorage.removeItem(key); else localStorage.setItem(key, value); } catch (_) { rollbackFailed = true; } throw new Error(`${String(error?.message || error)}${rollbackFailed ? ' · Supplementary preference rollback incomplete' : ' · Supplementary preferences restored'}`); } return previous.size; } function pseudonymizeStatistics(source) { const aliases = new Map(); const alias = id => { if (typeof id !== 'string' || !id) return ''; if (!aliases.has(id)) aliases.set(id, `model_${String(aliases.size + 1).padStart(4, '0')}`); return aliases.get(id); }; const numbers = (item, keys) => Object.fromEntries(keys.filter(key => typeof item?.[key] === 'number' && Number.isFinite(item[key])).map(key => [key, Math.max(0, item[key])])); const numericArray = values => (Array.isArray(values) ? values : []).slice(0, 168).map(value => typeof value === 'number' && Number.isFinite(value) ? Math.max(0, value) : 0); const dateMap = values => Object.fromEntries(Object.entries(values && typeof values === 'object' ? values : {}).filter(([key, value]) => /^\d{4}-\d{2}-\d{2}$/u.test(key) && typeof value === 'number' && Number.isFinite(value)).map(([key, value]) => [key, Math.max(0, value)])); const types = ['visit', 'usage', 'workspace', 'filter', 'tag', 'discovered', 'online', 'connection', 'quality', 'preview', 'quickview', 'chat_focus', 'account', 'player']; const numericTypeMap = values => Object.fromEntries(types.filter(type => typeof values?.[type] === 'number' && Number.isFinite(values[type])).map(type => [type, Math.max(0, values[type])])); return { kind: 'roomgrid-statistics', schema: 4, pseudonymized: true, sensitive: true, exportedAt: Date.now(), meta: { ...numbers(source.meta, ['startedAt', 'totalUsageMs', 'totalVisits', 'usageSessions', 'lastUsageAt']), usageDays: dateMap(source.meta?.usageDays) }, ...numbers(source, ['startedAt']), models: (source.models || []).map(model => ({ id: alias(model.id), ...numbers(model, ['totalMs', 'sessions', 'firstWatchedAt', 'lastWatchedAt', 'longestSessionMs', 'lastOnlineAt', 'viewerCount', 'onlineSamples', 'lastOnlineSampleAt', 'firstObservedAt', 'lastObservedAt', 'appearances', 'followers', 'secondsOnline']), hours: numericArray(model.hours), onlineHours: numericArray(model.onlineHours), days: dateMap(model.days) })), events: (source.events || []).map((event, index) => ({ eventId: `event_${index + 1}`, id: alias(event.id), type: types.includes(event.type) ? event.type : 'usage', ...numbers(event, ['at', 'endAt', 'durationMs', 'quality']) })), qualitySamples: (source.qualitySamples || []).map((sample, index) => ({ sampleId: `sample_${index + 1}`, id: alias(sample.id), ...numbers(sample, ['at', 'startupMs', 'stallMs', 'stalls', 'reconnects', 'totalFrames', 'droppedFrames', 'width', 'height', 'bufferSeconds', 'activeStreams']) })), dailyRollups: (source.dailyRollups || []).filter(item => /^\d{4}-\d{2}-\d{2}$/u.test(String(item.day || ''))).map(item => ({ day: item.day, counts: numericTypeMap(item.counts), durations: numericTypeMap(item.durations), ...numbers(item, ['updatedAt']) })), profiles: [], accountSnapshots: [], }; } let nativeTagAutocompleteRevision = 0; let nativeTagAutocompleteInput = null; let nativeTagAutocompleteOriginalValue = ''; async function queryNativeWebsiteTags(query, cleaner) { const cleanQuery = cleaner(query).toLocaleLowerCase(); if (cleanQuery.length < 2) return []; const input = document.querySelector('input.tagSearchInput[data-testid="filter-tag-search-input"],input.tagSearchInput'); if (!(input instanceof HTMLInputElement)) return []; const host = input.closest('.tagSearchDiv')?.querySelector('.tagSuggestionsDiv'); if (!host) return []; if (nativeTagAutocompleteInput !== input) { nativeTagAutocompleteInput = input; nativeTagAutocompleteOriginalValue = input.value; } const revision = ++nativeTagAutocompleteRevision; const setNativeValue = value => { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; if (setter) setter.call(input, value); else input.value = value; input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); }; try { input.dispatchEvent(new FocusEvent('focusin', { bubbles: true, composed: true })); setNativeValue(cleanQuery); await new Promise(resolve => setTimeout(resolve, 550)); if (revision !== nativeTagAutocompleteRevision) return []; const matches = [...host.querySelectorAll('[data-testid="filter-hashtag-suggestion"][title],.suggestion[title]')] .map(node => cleaner(node.getAttribute('title')).toLocaleLowerCase()) .filter(tag => tag && tag.includes(cleanQuery)); return [...new Set(matches)].slice(0, 40); } catch (_) { return []; } finally { if (revision === nativeTagAutocompleteRevision && input.isConnected) { setNativeValue(nativeTagAutocompleteOriginalValue); nativeTagAutocompleteInput = null; nativeTagAutocompleteOriginalValue = ''; } } } function sanitizePrivacyState(input) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; return { pinHash: /^[a-f0-9]{64}$/u.test(String(source.pinHash || '')) ? String(source.pinHash) : '', shortcut: 'ctrl+shift+x', neutralText: String(source.neutralText || 'RoomGrid · Neutrale Ansicht').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 80) || 'RoomGrid · Neutrale Ansicht', }; } const PrivacyShield = (() => { let config = sanitizePrivacyState(safeLocalJson(PRIVACY_STORE_KEY, {})); let active = false; let overlay = null; let mediaState = []; let installed = false; let activeStore = null; let lastActivityAt = Date.now(); let idleTimer = 0; async function hashPin(pin) { const bytes = new TextEncoder().encode(String(pin || '')); const digest = await crypto.subtle.digest('SHA-256', bytes); return [...new Uint8Array(digest)].map(value => value.toString(16).padStart(2, '0')).join(''); } async function setPin(pin) { pin = String(pin || ''); config.pinHash = pin ? await hashPin(pin) : ''; saveLocalJson(PRIVACY_STORE_KEY, config); return !!config.pinHash; } function activate() { if (active) return; active = true; mediaState = [...document.querySelectorAll('video,audio')].map(media => ({ media, muted: !!media.muted, volume: Number(media.volume), paused: !!media.paused })); mediaState.forEach(item => { try { item.media.muted = true; item.media.pause(); } catch (_) {} }); document.documentElement.dataset.roomgridPrivacy = 'active'; overlay = $('div', { dataset: { roomgridPrivacyOverlay: '1' }, style: { position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center', background: '#eef2f6', color: '#334155', fontFamily: 'system-ui,sans-serif', } }, [$('div', { style: { textAlign: 'center', padding: '24px' } }, [ $('div', { style: { fontSize: '20px', fontWeight: '750', marginBottom: '8px' } }, config.neutralText), $('div', { style: { fontSize: '12px', color: '#64748b', marginBottom: '16px' } }, tx('媒体已暂停,名称和图片已遮盖。', 'Media paused; names and images are covered.', 'Medien pausiert; Namen und Bilder sind verdeckt.')), $('button', { type: 'button', onclick: () => restore(), style: { minHeight: '40px', padding: '8px 14px', border: '1px solid #cbd5e1', borderRadius: '10px', background: '#fff', color: '#334155', cursor: 'pointer', fontWeight: '700' } }, tx('恢复之前的视图', 'Restore previous view', 'Vorherige Ansicht wiederherstellen')), $('div', { style: { marginTop: '14px', fontSize: '10px', color: '#94a3b8' } }, 'Ctrl + Shift + X'), ])]); document.body.appendChild(overlay); } async function restore() { if (!active) return true; if (config.pinHash) { const pin = prompt(tx('输入本地 PIN', 'Enter local PIN', 'Lokale PIN eingeben')); if (pin == null || await hashPin(pin) !== config.pinHash) return false; } active = false; delete document.documentElement.dataset.roomgridPrivacy; try { overlay?.remove(); } catch (_) {} overlay = null; for (const item of mediaState) { if (!item.media?.isConnected) continue; try { item.media.volume = Number.isFinite(item.volume) ? item.volume : item.media.volume; item.media.muted = item.muted; if (!item.paused) item.media.play().catch(() => {}); } catch (_) {} } mediaState = []; lastActivityAt = Date.now(); return true; } function init(store = null) { if (store) activeStore = store; if (installed) return; installed = true; const noteActivity = () => { if (!active) lastActivityAt = Date.now(); }; ['pointerdown', 'keydown', 'wheel', 'touchstart'].forEach(type => window.addEventListener(type, noteActivity, { capture: true, passive: true })); window.addEventListener('keydown', event => { if (!(event.ctrlKey && event.shiftKey && String(event.key).toLocaleLowerCase() === 'x')) return; event.preventDefault(); if (active) restore(); else activate(); }, true); idleTimer = setInterval(() => { if (active || document.hidden) return; const minutes = Number(activeStore?.state?.settings?.privacyIdleMinutes) || 0; if (minutes > 0 && Date.now() - lastActivityAt >= minutes * 60000) activate(); }, 15000); } async function storageSummary() { let localBytes = 0; try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) || ''; localBytes += (key.length + (localStorage.getItem(key) || '').length) * 2; } } catch (_) {} let estimate = {}; try { estimate = await navigator.storage?.estimate?.() || {}; } catch (_) {} return { localBytes, usage: Math.max(0, Number(estimate.usage) || 0), quota: Math.max(0, Number(estimate.quota) || 0) }; } return { init, activate, restore, setPin, storageSummary, isActive: () => active, hasPin: () => !!config.pinHash, getConfig: () => ({ ...config }) }; })(); function notificationDefaults() { return { schema: 2, master: false, neutral: true, delivery: 'inpage', inPagePosition: 'header', inPageStyle: 'comfortable', inPageDuration: 8, inPageMaxVisible: 2, quietStart: '22:00', quietEnd: '08:00', cooldownMinutes: 15, maxPerHour: 6, favoriteThreshold: 0, destination: 'details', models: {}, groups: {}, events: [], }; } function sanitizeNotificationRule(rule) { const source = rule && typeof rule === 'object' ? rule : {}; const inferred = source.online || source.offline || source.profile ? (source.offline || source.profile ? 'always' : 'live') : 'off'; const mode = ['off', 'live', 'personalized', 'always'].includes(source.mode) ? source.mode : inferred; return { mode, online: mode !== 'off', offline: mode === 'always', profile: mode === 'always', inactiveDays: clampInt(source.inactiveDays, 0, 3650, 0) }; } function sanitizeNotificationState(input) { const base = notificationDefaults(); const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const cleanRules = (raw, model) => Object.fromEntries(Object.entries(raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}).slice(0, 200).map(([id, rule]) => { const key = model ? normalizeUsername(id) : String(id).slice(0, 80); return [(model && !isLikelyUsername(key)) || !key ? '' : key, sanitizeNotificationRule(rule)]; }).filter(([id]) => id)); return { ...base, schema: 2, master: !!source.master, neutral: source.neutral !== false, delivery: ['inpage', 'browser', 'both', 'off'].includes(source.delivery) ? source.delivery : (Number(source.schema) === 1 && source.master ? 'browser' : base.delivery), inPagePosition: ['header', 'top-center', 'top-right'].includes(source.inPagePosition) ? source.inPagePosition : base.inPagePosition, inPageStyle: ['compact', 'comfortable', 'detailed'].includes(source.inPageStyle) ? source.inPageStyle : base.inPageStyle, inPageDuration: [0, 3, 5, 8, 12, 20, 30].includes(Number(source.inPageDuration)) ? Number(source.inPageDuration) : base.inPageDuration, inPageMaxVisible: clampInt(source.inPageMaxVisible, 1, 4, base.inPageMaxVisible), quietStart: /^\d{2}:\d{2}$/u.test(source.quietStart) ? source.quietStart : base.quietStart, quietEnd: /^\d{2}:\d{2}$/u.test(source.quietEnd) ? source.quietEnd : base.quietEnd, cooldownMinutes: clampInt(source.cooldownMinutes, 1, 1440, base.cooldownMinutes), maxPerHour: clampInt(source.maxPerHour, 1, 60, base.maxPerHour), favoriteThreshold: clampInt(source.favoriteThreshold, 0, 100, 0), destination: ['room', 'details', 'workspace'].includes(source.destination) ? source.destination : base.destination, models: cleanRules(source.models, true), groups: cleanRules(source.groups, false), events: (Array.isArray(source.events) ? source.events : []).slice(-200).map(item => ({ id: String(item?.id || '').slice(0, 32), at: Math.max(0, Number(item?.at) || 0), type: String(item?.type || '').slice(0, 30), model: normalizeUsername(item?.model), text: String(item?.text || '').slice(0, 180), read: !!item?.read, })).filter(item => item.id && item.at), }; } function createNotificationCenter(store) { const instanceId = `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`; let state = sanitizeNotificationState(safeLocalJson(NOTIFICATION_STORE_KEY, {})); let timer = 0; let destroyed = false; let leader = false; const statuses = new Map(); const statusCheckedAt = new Map(); const probeControllers = new Set(); let checkInFlight = null; let ruleCursor = 0; let ruleRevision = 0; const lastSent = new Map(); const inactiveSent = new Set(); const listeners = new Set(); const dismissTimers = new Map(); const progressTasks = new Map(); let surface = null; let notificationContent = null; let progressContent = null; let progressSignature = ''; let preferredHost = null; let preferredBefore = null; let mountObserver = null; let mountTimer = 0; function installSurfaceStyles() { if (document.getElementById('roomgrid-notification-surface-style')) return; document.head.appendChild($('style', { id: 'roomgrid-notification-surface-style', html: trustedHtml(` .roomgrid-notification-surface{--rgn-bg:var(--rg-panel,rgba(15,29,42,.96));--rgn-surface:var(--rg-surface,#1d3243);--rgn-text:var(--rg-text,#eef7fb);--rgn-muted:var(--rg-muted,#9eb4c2);--rgn-line:var(--rg-border,#405467);--rgn-accent:var(--rg-accent,#4f8cff);box-sizing:border-box;display:flex;flex:1 1 260px;align-items:center;justify-content:center;gap:6px;min-width:190px;max-width:720px;margin-inline:auto;color:var(--rgn-text);font:700 11px/1.25 Inter,system-ui,sans-serif;z-index:2147481500} .roomgrid-notification-surface[hidden]{display:none!important}.roomgrid-notification-surface[data-position="top-center"],.roomgrid-notification-surface[data-position="top-right"]{position:fixed;top:10px;width:min(680px,calc(100vw - 24px));max-height:min(46vh,420px);overflow:auto;padding:7px;border:1px solid var(--rgn-line);border-radius:12px;background:var(--rgn-bg);box-shadow:0 18px 50px rgba(2,6,23,.38);backdrop-filter:blur(16px)} .roomgrid-notification-surface[data-position="top-center"]{left:50%;transform:translateX(-50%)}.roomgrid-notification-surface[data-position="top-right"]{right:12px;width:min(440px,calc(100vw - 24px));margin:0}.roomgrid-notification-surface[data-position="header"]{align-self:stretch;min-height:34px;padding:2px 6px} .roomgrid-notification-content[hidden],.roomgrid-progress-content[hidden]{display:none!important}.roomgrid-notification-empty,.roomgrid-notification-message{box-sizing:border-box;width:100%;min-height:32px;border:1px solid var(--rgn-line);border-radius:8px;background:var(--rgn-surface);color:var(--rgn-text)}.roomgrid-notification-empty{display:flex;align-items:center;justify-content:center;gap:7px;padding:6px 10px;color:var(--rgn-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .roomgrid-notification-list{display:grid;gap:5px;width:100%;max-height:220px;overflow:auto}.roomgrid-notification-message{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:8px;padding:6px 7px;text-align:left;box-shadow:inset 3px 0 0 var(--rgn-accent)}.roomgrid-notification-message-main{min-width:0;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer}.roomgrid-notification-message-main strong,.roomgrid-notification-message-main span,.roomgrid-notification-message-main time{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.roomgrid-notification-message-main strong{font-size:10px}.roomgrid-notification-message-main span{color:var(--rgn-muted);font-size:10px}.roomgrid-notification-message-main time{margin-top:2px;color:var(--rgn-muted);font-size:8px}.roomgrid-notification-dismiss{width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--rgn-muted);cursor:pointer}.roomgrid-notification-dismiss:hover{background:rgba(148,163,184,.16);color:var(--rgn-text)}.roomgrid-notification-count{min-width:20px;padding:2px 5px;border-radius:999px;background:var(--rgn-accent);color:#fff;font-size:8px;text-align:center} .roomgrid-progress-content{box-sizing:border-box;width:100%;min-width:0;padding:7px 9px;border:1px solid var(--rgn-line);border-radius:8px;background:var(--rgn-surface);color:var(--rgn-text);display:grid;gap:5px}.roomgrid-progress-copy{display:flex;align-items:center;justify-content:space-between;gap:9px;min-width:0;font-size:10px}.roomgrid-progress-copy span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.roomgrid-progress-copy strong{flex:none;color:var(--rgn-text);font-variant-numeric:tabular-nums}.roomgrid-progress-track{height:5px;overflow:hidden;border-radius:999px;background:rgba(148,163,184,.22)}.roomgrid-progress-track i{display:block;width:0;height:100%;border-radius:inherit;background:var(--rgn-accent);transition:width .18s ease}.roomgrid-progress-content[data-mode="indeterminate"] .roomgrid-progress-track i{width:34%;animation:roomgrid-progress-sweep 1.25s ease-in-out infinite}@keyframes roomgrid-progress-sweep{0%{transform:translateX(-115%)}100%{transform:translateX(310%)}}@media(prefers-reduced-motion:reduce){.roomgrid-progress-content[data-mode="indeterminate"] .roomgrid-progress-track i{animation:none;width:100%;opacity:.55}.roomgrid-progress-track i{transition:none}} .roomgrid-notification-surface[data-style="compact"] .roomgrid-notification-message{min-height:29px;padding-block:3px}.roomgrid-notification-surface[data-style="compact"] .roomgrid-notification-message-main strong,.roomgrid-notification-surface[data-style="compact"] .roomgrid-notification-message-main time{display:none}.roomgrid-notification-surface[data-style="detailed"] .roomgrid-notification-message{padding:9px}.roomgrid-notification-surface[data-style="detailed"] .roomgrid-notification-message-main span{white-space:normal;line-height:1.35} @media(max-width:900px){.roomgrid-notification-surface[data-position="header"]{order:4;flex:1 1 100%;max-width:none;min-height:32px}.roomgrid-notification-surface[data-position="header"] .roomgrid-notification-list{grid-template-columns:1fr}.roomgrid-notification-surface[data-style="detailed"] .roomgrid-notification-message-main span{white-space:nowrap}} `) })); } function surfaceHost() { if (preferredHost?.isConnected) return preferredHost; return document.querySelector('.discover-header') || document.querySelector('.app-shell main > header') || document.querySelector('[data-testid="header-top-row"]') || document.querySelector('[data-testid="header-nav-bar"]') || document.querySelector('#desktop-spa-header nav') || document.querySelector('header nav') || document.querySelector('[role="banner"]') || document.querySelector('header'); } function ensureSurface() { installSurfaceStyles(); if (!surface) { notificationContent = $('div', { class: 'roomgrid-notification-content', dataset: { roomgridNotificationContent: '1' } }); progressContent = $('div', { class: 'roomgrid-progress-content', dataset: { roomgridProgress: '1' }, role: 'status', ariaLive: 'polite', hidden: true }); surface = $('section', { class: 'roomgrid-notification-surface', dataset: { roomgridNotificationSurface: '1' }, ariaLabel: tx('页面通知与加载进度', 'In-page notifications and loading progress', 'Meldungen und Ladefortschritt in der Seite', 'Notificaciones y progreso en la página', 'Notificações e progresso na página') }, [notificationContent, progressContent]); } return surface; } function autoMountSurface() { if (destroyed) return; const node = ensureSurface(); const position = state.inPagePosition; node.dataset.position = position; node.dataset.style = state.inPageStyle; if (position !== 'header') { if (node.parentElement !== document.body) document.body.appendChild(node); return; } const host = surfaceHost(); if (!host) { if (node.parentElement !== document.body) document.body.appendChild(node); return; } const before = preferredHost === host && preferredBefore?.parentElement === host ? preferredBefore : host.querySelector('.discover-header-actions,.toolbar-spacer,[data-roomgrid-header-actions]'); if (node.parentElement !== host || (before && node.nextSibling !== before)) host.insertBefore(node, before || null); } function markEventRead(id) { if (dismissTimers.has(id)) { clearTimeout(dismissTimers.get(id)); dismissTimers.delete(id); } let changed = false; state.events = state.events.map(event => { if (event.id !== id || event.read) return event; changed = true; return { ...event, read: true }; }); if (changed) save(); } function renderProgressContent() { const active = [...progressTasks.values()].filter(task => task.visible); const task = active.sort((left, right) => Number(left.background) - Number(right.background) || right.updatedAt - left.updatedAt)[0] || null; progressContent.hidden = !task; if (!task) { progressSignature = ''; if (progressContent.childElementCount) progressContent.replaceChildren(); return false; } const determinate = Number.isFinite(task.total) && task.total > 0; const total = determinate ? Math.max(1, Math.floor(task.total)) : 0; const done = determinate ? Math.min(total, Math.max(0, Math.floor(task.done))) : Math.max(0, Math.floor(task.done)); const percent = determinate ? Math.max(0, Math.min(100, Math.round(done / total * 100))) : 0; const extra = active.length > 1 ? ` · +${active.length - 1}` : ''; const backgroundSuffix = task.background ? ` · ${tx('后台进行中', 'running in background', 'läuft im Hintergrund')}` : ''; const displayLabel = `${task.label}${backgroundSuffix}${extra}`; const signature = JSON.stringify([displayLabel, determinate, percent, determinate ? 0 : done]); if (progressSignature === signature && progressContent.childElementCount) return true; progressSignature = signature; const barProps = { class: 'roomgrid-progress-track', role: 'progressbar', ariaLabel: displayLabel, ariaValueMin: '0', ariaValueMax: '100' }; if (determinate) barProps.ariaValueNow = String(percent); progressContent.dataset.mode = determinate ? 'determinate' : 'indeterminate'; progressContent.setAttribute('aria-label', displayLabel); progressContent.replaceChildren( $('div', { class: 'roomgrid-progress-copy' }, [ $('span', { dataset: { progressLabel: '1' } }, displayLabel), $('strong', { dataset: { progressValue: '1' } }, determinate ? `${percent}%` : done > 0 ? String(done) : ''), ]), $('div', barProps, $('i', { style: determinate ? { width: `${percent}%` } : {} })), ); return true; } function renderSurface(progressOnly = false) { const node = ensureSurface(); autoMountSurface(); const inPage = ['inpage', 'both'].includes(state.delivery); // Loading updates do not change notification messages or their timers. // Keep those DOM nodes (and keyboard focus) instead of rebuilding them. if (progressOnly === true) { const active = renderProgressContent(); node.hidden = !inPage && !active; return; } notificationContent.hidden = !inPage; if (!inPage) { notificationContent.replaceChildren(); node.hidden = !renderProgressContent(); return; } node.hidden = false; node.dataset.style = state.inPageStyle; node.dataset.position = state.inPagePosition; const unread = state.events.filter(event => !event.read); const visible = unread.slice(-state.inPageMaxVisible).reverse(); if (!visible.length) { notificationContent.replaceChildren($('div', { class: 'roomgrid-notification-empty', dataset: { notificationEmpty: '1' } }, [ $('span', {}, state.master ? '🔔' : '🔕'), $('span', {}, state.master ? tx('没有新通知', 'No new notifications', 'Keine neuen Meldungen', 'No hay notificaciones nuevas', 'Nenhuma notificação nova') : tx('通知已关闭', 'Notifications are off', 'Meldungen sind ausgeschaltet', 'Las notificaciones están desactivadas', 'As notificações estão desativadas')), ])); renderProgressContent(); return; } const list = $('div', { class: 'roomgrid-notification-list' }); for (const event of visible) { const main = $('button', { class: 'roomgrid-notification-message-main', type: 'button' }, [ $('strong', {}, event.model ? `@${event.model}` : tx('RoomGrid 通知', 'RoomGrid notification', 'RoomGrid-Meldung', 'Notificación de RoomGrid', 'Notificação do RoomGrid')), $('span', {}, event.text), $('time', {}, new Date(event.at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })), ]); main.addEventListener('click', () => { markEventRead(event.id); if (event.model) location.href = targetUrl(event.model); }); const dismiss = $('button', { class: 'roomgrid-notification-dismiss', type: 'button', title: tx('标为已读并隐藏', 'Dismiss as read', 'Als gelesen ausblenden', 'Descartar como leída', 'Dispensar como lida'), ariaLabel: tx('标为已读并隐藏', 'Dismiss as read', 'Als gelesen ausblenden', 'Descartar como leída', 'Dispensar como lida') }, '×'); dismiss.addEventListener('click', () => markEventRead(event.id)); list.appendChild($('article', { class: 'roomgrid-notification-message', dataset: { notificationEventId: event.id } }, [ $('span', {}, event.type === 'online' ? '●' : event.type === 'offline' ? '○' : event.type === 'test' ? '✓' : '◆'), main, dismiss, ])); if (state.inPageDuration > 0 && !dismissTimers.has(event.id)) { dismissTimers.set(event.id, window.setTimeout(() => { dismissTimers.delete(event.id); markEventRead(event.id); }, state.inPageDuration * 1000)); } } if (unread.length > visible.length) list.prepend($('span', { class: 'roomgrid-notification-count', title: tx('更多未读通知', 'More unread notifications', 'Weitere ungelesene Meldungen', 'Más notificaciones sin leer', 'Mais notificações não lidas') }, `+${unread.length - visible.length}`)); notificationContent.replaceChildren(list); renderProgressContent(); } function mount(host = null, before = null) { preferredHost = host || preferredHost; preferredBefore = before || null; renderSurface(); return surface; } function beginProgress(label, options = {}) { if (destroyed) return null; const id = `p_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; const delay = Math.max(0, Math.min(2000, Number(options.delayMs ?? 280) || 0)); const task = { id, label: String(label || tx('Lädt …', 'Loading…', 'Wird geladen …')).slice(0, 180), total: Number.isFinite(Number(options.total)) && Number(options.total) > 0 ? Math.floor(Number(options.total)) : null, done: Math.max(0, Number(options.done) || 0), background: !!options.background, updatedAt: Date.now(), visible: delay === 0, showTimer: 0, expiryTimer: 0, }; const remove = () => { clearTimeout(task.showTimer); clearTimeout(task.expiryTimer); progressTasks.delete(id); if (!destroyed) renderSurface(true); }; const keepAlive = () => { clearTimeout(task.expiryTimer); const timeout = Math.max(5000, Math.min(300000, Number(options.timeoutMs) || 120000)); task.expiryTimer = window.setTimeout(remove, timeout); }; progressTasks.set(id, task); if (!task.visible) task.showTimer = window.setTimeout(() => { task.visible = true; renderSurface(true); }, delay); keepAlive(); if (task.visible) renderSurface(true); let finished = false; return { update(next = {}) { if (finished || destroyed || !progressTasks.has(id)) return; if (Object.hasOwn(next, 'label')) task.label = String(next.label || task.label).slice(0, 180); if (Object.hasOwn(next, 'total')) task.total = Number.isFinite(Number(next.total)) && Number(next.total) > 0 ? Math.floor(Number(next.total)) : null; if (Object.hasOwn(next, 'done')) task.done = Math.max(0, Number(next.done) || 0); if (Object.hasOwn(next, 'background')) task.background = !!next.background; task.updatedAt = Date.now(); keepAlive(); if (task.visible) renderSurface(true); }, finish() { if (finished) return; finished = true; remove(); }, }; } const save = () => { ruleRevision += 1; saveLocalJson(NOTIFICATION_STORE_KEY, state); renderSurface(); listeners.forEach(listener => { try { listener(state); } catch (_) {} }); }; const patch = next => { state = sanitizeNotificationState({ ...state, ...(next || {}) }); if (!state.master) probeControllers.forEach(controller => controller.abort()); save(); }; const setModelRule = (id, rule) => { id = normalizeUsername(id); if (!isLikelyUsername(id)) return; state.models[id] = sanitizeNotificationRule(rule); save(); }; const setGroupRule = (id, rule) => { id = String(id || '').slice(0, 80); if (!id) return; state.groups[id] = sanitizeNotificationRule(rule); save(); }; function acquireLeader() { const now = Date.now(); const current = safeLocalJson(NOTIFICATION_LEADER_KEY, {}); leader = !current.id || current.id === instanceId || Number(current.expiresAt || 0) < now; if (leader) saveLocalJson(NOTIFICATION_LEADER_KEY, { id: instanceId, expiresAt: now + 130000 }); return leader; } function inQuietHours() { const minutes = value => { const [h, m] = value.split(':').map(Number); return h * 60 + m; }; const now = new Date(); const value = now.getHours() * 60 + now.getMinutes(); const start = minutes(state.quietStart); const end = minutes(state.quietEnd); return start === end ? false : start < end ? value >= start && value < end : value >= start || value < end; } function targetUrl(model) { if (state.destination === 'room') return roomGridSingleCamUrl(model, { source: 'notification' }); if (state.destination === 'workspace') return new URL('/?multicam_mode=1', location.origin).toString(); return roomGridSingleCamUrl(model, { source: 'notification' }); } function deliverBrowser(event) { if (!['browser', 'both'].includes(state.delivery)) return; Notify.fire(tx('RoomGrid 更新', 'RoomGrid update', 'RoomGrid-Aktualisierung'), event.text, `roomgrid-${event.type}:${event.model}`, event.model ? () => { location.href = targetUrl(event.model); } : null); } function appendEvent(type, model, text) { const now = Date.now(); const event = { id: `e_${now.toString(36)}_${Math.random().toString(36).slice(2, 7)}`, at: now, type, model: normalizeUsername(model), text: String(text || '').slice(0, 180), read: false }; state.events = [...state.events, event].slice(-200); save(); deliverBrowser(event); return event; } function emit(type, model, descriptiveText) { const now = Date.now(); const key = `${type}:${model}`; const recent = state.events.filter(event => event.at >= now - 3600000); if (inQuietHours() || recent.length >= state.maxPerHour || now - Number(lastSent.get(key) || 0) < state.cooldownMinutes * 60000) return false; lastSent.set(key, now); const text = state.neutral ? tx('Eine beobachtete Quelle hat ihren Status geändert.', 'A monitored source changed status.', 'Eine beobachtete Quelle hat ihren Status geändert.') : descriptiveText; appendEvent(type, model, text); return true; } function effectiveRules() { const result = new Map(); for (const [id, rule] of Object.entries(state.models)) result.set(id, rule); const rank = { off: 0, personalized: 1, live: 2, always: 3 }; for (const [groupId, rule] of Object.entries(state.groups)) { for (const room of store.state.rooms.filter(item => roomInGroup(item, groupId))) { const previous = result.get(room.id) || {}; const mode = rank[rule.mode] > rank[previous.mode] ? rule.mode : (previous.mode || rule.mode || 'off'); result.set(room.id, sanitizeNotificationRule({ mode, inactiveDays: Math.max(Number(previous.inactiveDays) || 0, Number(rule.inactiveDays) || 0) })); } } return result; } function personalizedEligible(id) { const room = store.state.rooms.find(item => item.id === id); if (!room) return false; return roomInGroup(room, FAVORITE_GROUP_ID) || Number(room.rating || 0) >= 4 || Number(room.priority || 0) >= 3 || (Array.isArray(room.localTags) && room.localTags.length > 0); } async function probe(id) { const controller = new AbortController(); probeControllers.add(controller); let timeout = 0; try { return await Promise.race([ (async () => { const response = await fetch(`/api/chatvideocontext/${encodeURIComponent(id)}/`, { credentials: 'include', cache: 'no-store', referrerPolicy: 'strict-origin-when-cross-origin', signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const raw = String(data?.room_status || data?.current_show || '').toLocaleLowerCase(); if (!raw) throw new Error('Missing room status'); return ['public', 'online'].includes(raw) ? 'online' : ['private', 'group', 'hidden'].includes(raw) ? 'private' : 'offline'; })(), new Promise((_, reject) => { timeout = setTimeout(() => { controller.abort(); reject(new Error('Notification probe timed out')); }, 8000); }), new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error('Notification probe aborted')), { once: true })), ]); } finally { clearTimeout(timeout); probeControllers.delete(controller); } } function check() { if (checkInFlight) return checkInFlight; if (destroyed || !state.master || !acquireLeader()) return Promise.resolve(); checkInFlight = runCheck().finally(() => { checkInFlight = null; }); return checkInFlight; } async function runCheck() { let currentRules = effectiveRules(); let currentRuleRevision = ruleRevision; let currentStoreState = store.state; const entries = [...currentRules].filter(([, rule]) => rule.mode !== 'off'); if (!entries.length) { ruleCursor = 0; return; } const roomsById = new Map(store.state.rooms.map(room => [room.id, room])); const freshOnline = new Set(); // Reuse only actually observed public rooms, never infer offline/private // from a missing entry in the public catalog or from an offline cache. try { const snapshot = globalThis.RoomGridCatalogBridge?.getCurrentModels?.(); if (snapshot?.fresh && Date.now() - Number(snapshot.fetchedAt || 0) <= 90000) { for (const room of snapshot.rooms || []) { const raw = String(room.currentShow || room.current_show || room.room_status || room.status || '').toLocaleLowerCase(); if (['public', 'online'].includes(raw)) freshOnline.add(normalizeUsername(room.id || room.username)); } } } catch (_) {} const start = ruleCursor % entries.length; const count = Math.min(100, entries.length); const deadline = Date.now() + 45000; let nextIndex = 0; const worker = async () => { while (!destroyed && state.master && Date.now() < deadline && nextIndex < count) { const index = nextIndex++; ruleCursor = (start + nextIndex) % entries.length; const [id] = entries[(start + index) % entries.length]; try { const next = freshOnline.has(id) ? 'online' : await probe(id); if (currentRuleRevision !== ruleRevision || currentStoreState !== store.state) { currentRules = effectiveRules(); currentRuleRevision = ruleRevision; currentStoreState = store.state; } const rule = currentRules.get(id); if (destroyed || !state.master || !rule || rule.mode === 'off') continue; const previous = statuses.get(id); statuses.set(id, next); statusCheckedAt.set(id, Date.now()); if (previous && previous !== next) { if (next === 'online' && rule.online && (rule.mode !== 'personalized' || personalizedEligible(id))) emit('online', id, tx(`${id} 已上线。`, `${id} is online.`, `${id} ist online.`, `${id} está en directo.`, `${id} está ao vivo.`)); if (previous === 'online' && next === 'offline' && rule.mode === 'always') emit('offline', id, tx(`${id} 已离线。`, `${id} is offline.`, `${id} ist offline.`, `${id} está desconectado.`, `${id} está offline.`)); } if (rule.inactiveDays > 0) { const room = roomsById.get(id); const due = Date.now() - Number(room?.lastSeenOnline || 0) >= rule.inactiveDays * 86400000; if (due && !inactiveSent.has(id)) { inactiveSent.add(id); emit('inactive', id, tx(`${id} 已有较长时间未观察到在线。`, `${id} has not been observed online for a while.`, `${id} wurde längere Zeit nicht online beobachtet.`, `No se ha observado a ${id} en directo desde hace tiempo.`, `${id} não é observado ao vivo há algum tempo.`)); } } } catch (_) {} } }; await Promise.all(Array.from({ length: Math.min(3, count) }, worker)); if (destroyed || !state.master) return; const onlineFavorites = entries.filter(([id]) => statuses.get(id) === 'online' && Date.now() - Number(statusCheckedAt.get(id) || 0) <= 90000 && roomInGroup(roomsById.get(id), FAVORITE_GROUP_ID)).map(([id]) => id); if (state.favoriteThreshold > 0 && onlineFavorites.length >= state.favoriteThreshold) emit('favorites', onlineFavorites[0] || '', tx(`${onlineFavorites.length} 个收藏同时在线。`, `${onlineFavorites.length} favorites are online together.`, `${onlineFavorites.length} Favoriten sind gleichzeitig online.`, `${onlineFavorites.length} favoritos están en directo a la vez.`, `${onlineFavorites.length} favoritos estão ao vivo ao mesmo tempo.`)); } function observeProfileChange(id) { id = normalizeUsername(id); const rule = effectiveRules().get(id); if (state.master && rule?.mode === 'always') emit('profile', id, tx(`${id} 的公开资料已更改。`, `${id}'s public profile changed.`, `Das öffentliche Profil von ${id} hat sich geändert.`, `El perfil público de ${id} ha cambiado.`, `O perfil público de ${id} foi alterado.`)); } function start() { if (timer) return; Notify.init(); renderSurface(); mountObserver = new MutationObserver(() => { clearTimeout(mountTimer); mountTimer = window.setTimeout(autoMountSurface, 80); }); try { mountObserver.observe(document.body, { childList: true, subtree: true }); } catch (_) {} acquireLeader(); timer = setInterval(check, 60000); setTimeout(check, 1500); window.addEventListener('storage', onStorage); } function onStorage(event) { if (event.key === NOTIFICATION_STORE_KEY) { state = sanitizeNotificationState(safeLocalJson(NOTIFICATION_STORE_KEY, {})); renderSurface(); listeners.forEach(listener => listener(state)); } } function destroy() { destroyed = true; probeControllers.forEach(controller => controller.abort()); probeControllers.clear(); clearInterval(timer); timer = 0; clearTimeout(mountTimer); mountTimer = 0; mountObserver?.disconnect(); mountObserver = null; dismissTimers.forEach(value => clearTimeout(value)); dismissTimers.clear(); progressTasks.forEach(task => { clearTimeout(task.showTimer); clearTimeout(task.expiryTimer); }); progressTasks.clear(); try { surface?.remove(); } catch (_) {} window.removeEventListener('storage', onStorage); const lease = safeLocalJson(NOTIFICATION_LEADER_KEY, {}); if (lease.id === instanceId) localStorage.removeItem(NOTIFICATION_LEADER_KEY); } function markAllRead() { state.events = state.events.map(event => ({ ...event, read: true })); save(); } function clearEvents() { dismissTimers.forEach(value => clearTimeout(value)); dismissTimers.clear(); state.events = []; save(); } function preview() { const text = state.neutral ? tx('这是一条本地 RoomGrid 测试通知。', 'This is a local RoomGrid test notification.', 'Dies ist eine lokale RoomGrid-Testmeldung.', 'Esta es una notificación de prueba local de RoomGrid.', 'Esta é uma notificação de teste local do RoomGrid.') : tx('测试成功:通知可以直接显示在 RoomGrid 栏中。', 'Test successful: notifications can appear directly in the RoomGrid bar.', 'Test erfolgreich: Meldungen können direkt in der RoomGrid-Leiste erscheinen.', 'Prueba correcta: las notificaciones pueden aparecer directamente en la barra de RoomGrid.', 'Teste concluído: as notificações podem aparecer diretamente na barra do RoomGrid.'); return appendEvent('test', '', text); } return { start, destroy, check, patch, mount, beginProgress, preview, setModelRule, setGroupRule, observeProfileChange, markAllRead, clearEvents, getState: () => JSON.parse(JSON.stringify(state)), isLeader: () => leader, monitoredCount: () => effectiveRules().size, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } }; } function createWindowCoordinator(store) { const params = new URLSearchParams(location.search); const id = String(params.get('window_id') || `w_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`).slice(0, 80); let timer = 0; const update = () => { const source = safeLocalJson(WINDOW_COORDINATION_KEY, { peers: {}, recent: [] }); const now = Date.now(); const peers = source.peers && typeof source.peers === 'object' ? source.peers : {}; for (const [peerId, peer] of Object.entries(peers)) if (now - Number(peer?.lastSeenAt || 0) > 20000) delete peers[peerId]; const entry = { id, name: String(store.state.settings.windowName || params.get('window_name') || 'RoomGrid').slice(0, 60), workspaceId: String(store.state.settings.activeWorkspaceId || params.get('suite_workspace') || DEFAULT_WORKSPACE_ID).slice(0, 80), groupId: String(store.state.settings.activeGroup || params.get('workspace') || DEFAULT_GROUP_ID).slice(0, 80), profile: store.state.settings.performanceProfile || 'balanced', openedAt: Number(peers[id]?.openedAt || now), lastSeenAt: now }; peers[id] = entry; const recent = [entry, ...(Array.isArray(source.recent) ? source.recent : []).filter(item => item?.id !== id)].slice(0, 10); saveLocalJson(WINDOW_COORDINATION_KEY, { peers, recent }); }; function start() { if (timer) return; update(); timer = setInterval(update, 5000); } function status() { const state = safeLocalJson(WINDOW_COORDINATION_KEY, { peers: {}, recent: [] }); return { id, peers: Object.values(state.peers || {}), recent: Array.isArray(state.recent) ? state.recent : [] }; } function openWindow(groupId, name, options = {}) { const url = new URL(location.origin + '/'); url.searchParams.set('multicam_mode', '1'); if (options.workspace) { url.searchParams.set('suite_workspace', String(groupId || DEFAULT_WORKSPACE_ID)); url.searchParams.delete('workspace'); } else { url.searchParams.set('workspace', String(groupId || DEFAULT_GROUP_ID)); url.searchParams.delete('suite_workspace'); } url.searchParams.set('window_name', String(name || 'RoomGrid').slice(0, 60)); url.searchParams.set('window_id', `w_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`); return window.open(url.toString(), '_blank', 'noopener,noreferrer'); } function destroy() { clearInterval(timer); timer = 0; const state = safeLocalJson(WINDOW_COORDINATION_KEY, { peers: {}, recent: [] }); if (state.peers) delete state.peers[id]; saveLocalJson(WINDOW_COORDINATION_KEY, state); } function resourceStatus() { const memory = performance?.memory; return { videos: document.querySelectorAll('video').length, playing: [...document.querySelectorAll('video')].filter(video => !video.paused && !video.ended).length, heapUsed: Math.max(0, Number(memory?.usedJSHeapSize) || 0), heapLimit: Math.max(0, Number(memory?.jsHeapSizeLimit) || 0) }; } return { id, start, status, openWindow, resourceStatus, destroy }; } function openControlCenter(store, analytics, notifications, windows, initialTab = 'workspaces', hooks = null) { // Resolve intrinsic helpers once, not through Tampermonkey's global proxy // for every historical model, tag or hour bucket. const { Math, Number, String, Array, Object, Date, Set, Map, Promise, JSON, URL } = globalThis; try { window.__roomGridCloseControlCenter?.(); } catch (_) { document.querySelector('[data-roomgrid-control-center]')?.remove(); } let tab = ['workspaces', 'library', 'data', 'homepage', 'appearance', 'analytics', 'broadcaster', 'notifications', 'privacy', 'performance', 'extras', 'diagnostics'].includes(initialTab) ? initialTab : 'workspaces'; const statisticsView = tab === 'analytics' && hooks?.statisticsView === true; let renderRevision = 0; const expandedSettingsGroups = new Set(); const dataManager = createSuiteDataManager(store); const librarySelection = new Set(); const exportSections = new Set(dataManager.sections); let importReview = null; let importStrategy = 'merge'; let analyticsImportReview = null; let analyticsImportStrategy = 'merge'; let backgroundEditorScope = 'global'; const analyticsImportSections = new Set(); const broadcasterStats = RoomGridUltimate.getAccountStats(analytics); if (!statisticsView) broadcasterStats.init().catch(() => {}); applySuiteAppearance(store); const styleId = 'roomgrid-control-center-style'; if (!document.getElementById(styleId)) document.head.appendChild($('style', { id: styleId, html: trustedHtml(` .rgcc-overlay{position:fixed;inset:0;z-index:2147483000;display:grid;place-items:center;padding:18px;background:rgba(5,12,18,.72);font-family:Arial,Helvetica,sans-serif;color:#f1f5f8} .rgcc{width:min(1500px,100%);height:min(900px,94vh);display:grid;grid-template-rows:auto auto minmax(0,1fr);overflow:hidden;border:1px solid #405061;border-radius:8px;background:#0e1a26;box-shadow:0 24px 70px rgba(0,0,0,.42)} .rgcc-head{display:flex;align-items:center;gap:12px;padding:14px 16px;background:#1d2b39;border-bottom:1px solid #405061}.rgcc-head strong{font-size:18px}.rgcc-head span{color:#aeb9c3;font-size:11px}.rgcc-close{margin-left:auto} .rgcc-tabs{display:flex;gap:0;padding:0 12px;overflow:auto;background:#1d2b39;border-bottom:1px solid #405061}.rgcc-tabs button.active{color:#78d4fb;background:#263746;border-color:#405061;border-bottom-color:#0b9ddd} .rgcc-body{overflow:auto;padding:16px}.rgcc-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.rgcc-two{display:grid;grid-template-columns:1fr 1fr;gap:12px}.rgcc-card{padding:13px;border:1px solid #405061;border-radius:6px;background:#172533}.rgcc-card h3{margin:0 0 9px;font-size:14px}.rgcc-card p{margin:5px 0;color:#aeb9c3;font-size:11px;line-height:1.5}.rgcc-value{display:block;font-size:23px;font-weight:850}.rgcc-label{display:block;color:#aeb9c3;font-size:10px;font-weight:800;text-transform:uppercase}.rgcc-btn{min-height:36px;padding:7px 10px;border:1px solid #445465;border-radius:5px;background:#233443;color:#f1f5f8;font-weight:750;font-size:11px;cursor:pointer}.rgcc-btn:hover{border-color:#69c9f2;background:#2b4052}.rgcc-btn.primary{color:#fff;background:#f47321;border-color:#f58a47}.rgcc-btn.danger{color:#ff9d9d}.rgcc-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:8px 0}.rgcc-field{display:grid;gap:4px;min-width:120px;flex:1;color:#aeb9c3;font-size:10px;font-weight:800}.rgcc-input{min-height:36px;width:100%;padding:7px 9px;border:1px solid #405061;border-radius:5px;background:#101c28;color:#f1f5f8}.rgcc-range-control{display:grid;grid-template-columns:minmax(0,1fr) 52px;align-items:center;gap:10px}.rgcc-range-control .rgcc-input{padding-inline:3px}.rgcc-range-value{display:grid;place-items:center;min-height:30px;padding:3px 7px;border:1px solid #405061;border-radius:5px;background:#101c28;color:#f1f5f8;font-size:11px;font-variant-numeric:tabular-nums}.rgcc-check{display:inline-flex;align-items:center;gap:6px;color:#cbd5dd;font-size:11px;font-weight:700}.rgcc-list{display:grid;gap:6px}.rgcc-item{display:grid;grid-template-columns:minmax(120px,1fr) 2fr auto;gap:8px;align-items:center;padding:8px;border-radius:5px;background:#1c2c3a;font-size:11px}.rgcc-muted{color:#aeb9c3}.rgcc-heat{display:grid;grid-template-columns:repeat(24,1fr);gap:2px}.rgcc-heat i{aspect-ratio:1;border-radius:2px;background:#24465a}.rgcc-note{padding:9px 10px;border-left:3px solid #f47321;border-radius:4px;background:#352719;color:#ffd4b9;font-size:11px;line-height:1.5}.rgcc-good{color:#6fd6a2}.rgcc-warn{color:#ffba6f}.rgcc-danger{color:#ff9d9d}.rgcc-section{margin-top:12px}.rgcc-section>h2{margin:0 0 8px;font-size:16px} .rgcc-tabs button{border-radius:0;border-bottom:3px solid transparent;background:transparent}.rgcc-tabs button.active{color:#78d4fb!important;background:#263746!important;border-color:transparent!important;border-bottom-color:#0b9ddd!important}.rgcc-btn.primary{color:#fff!important;background:#f47321!important;border-color:#f58a47!important}.rgcc-btn:hover{border-color:#69c9f2}.rgcc-toolbar{position:sticky;top:-16px;z-index:3;padding:10px 0;background:#0e1a26}.rgcc-workspace{display:grid;grid-template-columns:minmax(180px,1.2fr) minmax(240px,2fr) auto;gap:10px;align-items:start}.rgcc-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.rgcc-chip{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border-radius:4px;background:#243d4e;color:#9addf8;font-size:10px;font-weight:800}.rgcc-library-row{display:grid;grid-template-columns:auto minmax(140px,1.2fr) minmax(220px,2fr) minmax(120px,1fr) auto;gap:8px;align-items:center;padding:9px;border-radius:5px;background:#1c2c3a}.rgcc-stars{display:flex;gap:2px}.rgcc-stars button{border:0;background:transparent;color:#72808c;cursor:pointer;padding:1px}.rgcc-stars button.on{color:#f4ad42}.rgcc-color{width:34px;height:34px;border:2px solid #fff;border-radius:999px;box-shadow:0 0 0 1px #5c6d7d;cursor:pointer}.rgcc-color.active{box-shadow:0 0 0 3px #0b9ddd} .rgcc-subtabs{display:flex;gap:6px;overflow:auto;padding:2px 0 10px}.rgcc-subtabs .rgcc-btn{white-space:nowrap}.rgcc-chart{display:flex;align-items:end;gap:5px;min-height:150px;padding:12px 8px 24px;border:1px solid #405061;border-radius:6px;background:#121f2b}.rgcc-bar{position:relative;flex:1;min-width:8px;border-radius:3px 3px 0 0;background:#0b9ddd;transition:height .2s ease}.rgcc-bar span{position:absolute;left:50%;bottom:-19px;transform:translateX(-50%);font-size:9px;color:#aeb9c3;white-space:nowrap}.rgcc-progress{height:8px;overflow:hidden;border-radius:999px;background:#263746}.rgcc-progress>i{display:block;height:100%;border-radius:inherit;background:#0b9ddd}.rgcc-preset{min-width:140px;text-align:left}.rgcc-preset strong,.rgcc-preset small{display:block}.rgcc-preset small{margin-top:3px;color:#aeb9c3;font-weight:500}.rgcc-secret{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.08em}.rgcc-spark{display:flex;align-items:end;gap:2px;height:42px}.rgcc-spark i{flex:1;min-width:3px;border-radius:2px;background:#0b9ddd} .rgcc-analytics-dashboard{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:start}.rgcc-dashboard-widget{position:relative;min-width:0}.rgcc-dashboard-widget.size-small{grid-column:span 1}.rgcc-dashboard-widget.size-wide{grid-column:span 2}.rgcc-dashboard-widget.size-full{grid-column:1/-1}.rgcc-dashboard-widget>.rgcc-provenance{position:absolute;right:8px;bottom:7px}.rgcc-provenance{display:inline-flex;align-items:center;min-height:20px;max-width:240px;padding:3px 7px;border:1px solid var(--rg-border,#405061);border-radius:999px;background:var(--rg-surface,#1c2c3a);color:var(--rg-muted,#aeb9c3);font-size:9px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rgcc-provenance-list{display:grid;gap:6px}.rgcc-provenance-list>div{display:grid;grid-template-columns:minmax(120px,1fr) auto auto;gap:7px;align-items:center}.rgcc-widget-editor{display:grid;gap:6px;margin-top:10px}.rgcc-widget-editor .rgcc-item{grid-template-columns:minmax(170px,1fr) minmax(150px,.7fr) auto}.rgcc-widget-inner{grid-template-columns:repeat(3,minmax(0,1fr))}.rgcc-widget-inner .rgcc-card{padding:9px} .rgcc-multi-row{align-items:start}.rgcc-multi{display:grid;gap:6px;min-width:0;flex:1}.rgcc-multi>span{color:#aeb9c3;font-size:10px;font-weight:800}.rgcc-multi-chips{display:flex;flex-wrap:wrap;gap:5px;min-height:0}.rgcc-multi-chip{display:inline-flex;align-items:center;gap:6px;max-width:210px;padding:4px 6px 4px 8px;border:1px solid #315d75;border-radius:999px;background:#173246;color:#b9e8fb;font:800 10px/1.2 Arial,Helvetica,sans-serif}.rgcc-multi-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rgcc-multi-chip button{width:18px;height:18px;padding:0;border:0;border-radius:50%;background:#2c4c60;color:#fff;font-weight:900;cursor:pointer}.rgcc-multi-picker{min-width:0}.rgcc-multi-picker>summary{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:36px;padding:7px 10px;border:1px solid #405061;border-radius:5px;background:#101c28;color:#f1f5f8;font-size:11px;font-weight:750;cursor:pointer;list-style:none}.rgcc-multi-picker>summary::-webkit-details-marker{display:none}.rgcc-multi-picker>summary::after{content:'▾';color:#78d4fb}.rgcc-multi-picker[open]>summary{border-color:#0b9ddd;border-radius:5px 5px 0 0}.rgcc-multi-menu{display:grid;gap:7px;padding:9px;border:1px solid #0b9ddd;border-top:0;border-radius:0 0 6px 6px;background:#101c28;box-shadow:0 14px 34px rgba(0,0,0,.28)}.rgcc-multi-tools{display:flex;gap:6px}.rgcc-multi-tools .rgcc-input{flex:1}.rgcc-multi-tools .rgcc-btn{min-height:36px;white-space:nowrap}.rgcc-multi-options{display:grid;gap:3px;max-height:min(250px,25vh);overflow:auto;padding-right:3px}.rgcc-multi-group{position:sticky;top:0;z-index:1;padding:6px 5px 4px;background:#101c28;color:#78d4fb;font-size:9px;font-weight:900;text-transform:uppercase}.rgcc-multi-option{display:flex;align-items:center;gap:7px;min-height:29px;padding:5px 7px;border-radius:4px;color:#dce6ee;font-size:11px;font-weight:650;cursor:pointer}.rgcc-multi-option:hover{background:#233443}.rgcc-multi-option input{accent-color:#f47321}.rgcc-multi-empty{padding:14px 7px;color:#aeb9c3;text-align:center;font-size:11px} .rgcc-multi-suggestions{display:flex;flex-wrap:wrap;gap:5px;padding:7px;border:1px solid #315d75;border-radius:5px;background:#132432}.rgcc-multi-suggestions[hidden]{display:none}.rgcc-multi-suggestion{min-height:28px;padding:4px 8px;border:1px solid #315d75;border-radius:999px;background:#1c3546;color:#b9e8fb;font:800 10px/1.2 Arial,Helvetica,sans-serif;cursor:pointer}.rgcc-multi-suggestion:hover,.rgcc-multi-suggestion:focus-visible{border-color:#78d4fb;background:#24465a;outline:none}.rgcc-multi-suggestion[aria-pressed="true"]{border-color:#f47321;color:#fff} .rgcc-brand-studio{display:grid;grid-template-columns:minmax(210px,.7fr) minmax(0,2fr);gap:14px;align-items:stretch}.rgcc-brand-preview{display:flex;align-items:center;gap:12px;min-height:128px;padding:18px;border:1px solid var(--rg-border,#405061);border-radius:var(--rg-radius,10px);background:var(--rg-brand-gradient);color:var(--rg-accent-text,#fff)}.rgcc-brand-preview .rgcc-brand-mark{display:grid;place-items:center;min-width:52px;min-height:52px;border-radius:var(--rg-radius);background:rgba(255,255,255,.16);font-weight:950;font-size:20px}.rgcc-brand-preview .rgcc-brand-mark:has(img[data-roomgrid-brand-logo]){min-width:0;min-height:0;background:transparent}.rgcc-brand-preview strong,.rgcc-brand-preview span,.rgcc-brand-preview small{display:block;color:inherit}.rgcc-brand-fields{min-width:0}.rgcc-brand-note{padding:9px 11px;border:1px solid var(--rg-border,#405061);border-radius:9px;background:var(--rg-surface,#172033);color:var(--rg-muted,#a9b8c8);font-size:11px;line-height:1.45}.rgcc-palette-grid{display:grid;grid-template-columns:repeat(3,minmax(100px,1fr));gap:8px}.rgcc-palette-grid .rgcc-color-input{min-height:42px;padding:4px} .rgcc-background-studio{display:grid;grid-template-columns:minmax(260px,.8fr) minmax(0,2fr);gap:14px;align-items:start}.rgcc-background-preview{position:relative;min-height:250px;overflow:hidden;border:1px solid var(--rg-border);border-radius:var(--rg-radius);background:var(--rg-page-bg);box-shadow:var(--rg-shadow)}.rgcc-background-preview>img,.rgcc-background-preview>.rgcc-background-fill,.rgcc-background-preview>.rgcc-background-dim{position:absolute;inset:0;width:100%;height:100%}.rgcc-background-preview>img{display:block}.rgcc-background-preview>.rgcc-background-dim{pointer-events:none}.rgcc-background-preview-label{position:absolute;left:10px;right:10px;bottom:10px;z-index:3;padding:8px 10px;border:1px solid rgba(255,255,255,.24);border-radius:7px;background:rgba(2,6,23,.74);color:#fff;font-size:10px;font-weight:850;backdrop-filter:blur(10px)}.rgcc-background-fields{display:grid;gap:9px}.rgcc-background-fields .rgcc-grid{grid-template-columns:repeat(3,minmax(130px,1fr))} .rgcc-design-preview{display:grid;place-items:center;min-height:260px;padding:16px;overflow:auto;border:1px solid var(--rg-border);border-radius:var(--rg-radius);background:color-mix(in srgb,var(--rg-page-bg) 86%,#000 14%)}.rgcc-preview-browser{width:100%;max-width:660px;overflow:hidden;border:1px solid var(--rg-border);border-radius:var(--rg-radius);background:var(--rg-page-bg);box-shadow:var(--rg-shadow);transition:max-width .2s ease}.rgcc-design-preview.device-tablet .rgcc-preview-browser{max-width:480px}.rgcc-design-preview.device-mobile .rgcc-preview-browser{max-width:290px}.rgcc-preview-header{display:flex;align-items:center;gap:8px;min-height:42px;padding:7px 10px;border-bottom:1px solid var(--rg-border);background:var(--rg-surface-strong)}.rgcc-preview-header span{margin-right:auto;color:var(--rg-muted);font-size:9px}.rgcc-preview-header button{border:0;background:transparent;color:var(--rg-text)}.rgcc-preview-layout{display:grid;grid-template-columns:auto minmax(0,1fr);min-height:190px}.rgcc-preview-layout>aside{width:54px;display:grid;align-content:start;gap:7px;padding:10px 7px;border-right:1px solid var(--rg-border);background:var(--rg-panel)}.rgcc-preview-layout>aside i{height:8px;border-radius:9px;background:var(--rg-border)}.rg-sidebar-right .rgcc-preview-layout{direction:rtl}.rgcc-preview-layout>main{min-width:0;padding:10px;direction:ltr}.rgcc-preview-toolbar{display:flex;gap:6px;margin-bottom:8px}.rgcc-preview-toolbar span,.rgcc-preview-toolbar b{height:9px;border-radius:9px;background:var(--rg-border)}.rgcc-preview-toolbar span{width:44px}.rgcc-preview-toolbar b{margin-left:auto;width:58px;background:var(--rg-accent)}.rgcc-preview-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--rg-card-gap)}.device-mobile .rgcc-preview-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.surface-workspace .rgcc-preview-cards{grid-template-columns:repeat(2,minmax(0,1fr))}.rgcc-preview-cards article{min-width:0;padding:6px;border:1px solid var(--rg-border);border-radius:var(--rg-radius-small);background:var(--rg-panel);color:var(--rg-text)}.rgcc-preview-media{display:grid;place-items:center;aspect-ratio:16/9;margin-bottom:5px;border-radius:calc(var(--rg-radius-small) - 2px);background:linear-gradient(135deg,var(--rg-accent-soft),var(--rg-secondary-soft));color:var(--rg-success);font-size:7px;font-weight:950}.rgcc-preview-cards strong,.rgcc-preview-cards span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:8px}.rgcc-preview-cards span{color:var(--rg-muted);font-size:6px} .rgcc-contrast-audit{display:grid;gap:6px;margin:8px 0}.rgcc-contrast-row{display:grid;grid-template-columns:minmax(0,1fr) auto 48px;gap:8px;align-items:center;padding:8px;border-radius:var(--rg-radius-small);background:var(--rg-surface);font-size:11px}.rgcc-contrast-row em{font-style:normal;font-weight:900;text-align:right}.rgcc-contrast-row.pass em{color:var(--rg-success,#22c55e)}.rgcc-contrast-row.fail em{color:var(--rg-danger,#ef4444)}.rgcc-responsive-profiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px;margin-top:10px}.rgcc-responsive-profile{padding:8px;border:1px solid var(--rg-border);border-radius:var(--rg-radius-small);background:var(--rg-surface)}.rgcc-responsive-profile>summary{cursor:pointer;font-weight:850}.rgcc-responsive-profile .rgcc-grid{grid-template-columns:1fr;margin-top:8px} body.discover-dark .rgcc-overlay,.dark .rgcc-overlay,.rg-dark-mode .rgcc-overlay{color:#f1f5f8}.discover-dark .rgcc,.dark .rgcc,.rg-dark-mode .rgcc{border-color:#405061;background:#0e1a26}.discover-dark .rgcc-head,.discover-dark .rgcc-tabs,.discover-dark .rgcc-card,.discover-dark .rgcc-btn,.discover-dark .rgcc-input,.dark .rgcc-head,.dark .rgcc-tabs,.dark .rgcc-card,.dark .rgcc-btn,.dark .rgcc-input,.rg-dark-mode .rgcc-head,.rg-dark-mode .rgcc-tabs,.rg-dark-mode .rgcc-card,.rg-dark-mode .rgcc-btn,.rg-dark-mode .rgcc-input{color:#f1f5f8;border-color:#405061;background:#172533}.discover-dark .rgcc-item,.dark .rgcc-item,.rg-dark-mode .rgcc-item{background:#1c2c3a}.discover-dark .rgcc-card p,.discover-dark .rgcc-muted,.dark .rgcc-card p,.dark .rgcc-muted,.rg-dark-mode .rgcc-card p,.rg-dark-mode .rgcc-muted{color:#aeb9c3}.rgcc-btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none!important} .discover-dark .rgcc-library-row,.rg-dark-mode .rgcc-library-row{background:#1c2c3a}.discover-dark .rgcc-toolbar,.rg-dark-mode .rgcc-toolbar{background:#0e1a26} .rgcc-overlay{color:var(--rg-text,#f1f5f8)}.rgcc-overlay .rgcc{border-color:var(--rg-border,#405061);background:var(--rg-page-bg,#0e1a26);color:var(--rg-text,#f1f5f8)}.rgcc .rgcc-head,.rgcc .rgcc-tabs{border-color:var(--rg-border,#405061);background:var(--rg-surface-strong,#1d2b39)}.rgcc .rgcc-head span,.rgcc .rgcc-card p,.rgcc .rgcc-muted,.rgcc .rgcc-label,.rgcc .rgcc-field,.rgcc .rgcc-check,.rgcc .rgcc-multi>span{color:var(--rg-muted,#aeb9c3)}.rgcc .rgcc-body,.rgcc .rgcc-toolbar{background:var(--rg-page-bg,#0e1a26)}.rgcc .rgcc-card{border-color:var(--rg-border,#405061);background:var(--rg-panel,#172533);color:var(--rg-text,#f1f5f8)}.rgcc .rgcc-item,.rgcc .rgcc-library-row,.rgcc .rgcc-detail-check{background:var(--rg-surface,#1c2c3a);color:var(--rg-text,#f1f5f8)}.rgcc .rgcc-btn,.rgcc .rgcc-input,.rgcc .rgcc-range-value,.rgcc .rgcc-multi-picker>summary,.rgcc .rgcc-multi-menu{border-color:var(--rg-border,#405061);background:var(--rg-input,#101c28);color:var(--rg-text,#f1f5f8)}.rgcc .rgcc-btn:hover{border-color:var(--rg-secondary,#69c9f2);background:var(--rg-secondary-soft,rgba(105,201,242,.15))}.rgcc .rgcc-btn.primary{border-color:var(--rg-accent,#f47321)!important;background:var(--rg-accent,#f47321)!important;color:var(--rg-accent-text,#fff)!important}.rgcc .rgcc-tabs button.active{border-bottom-color:var(--rg-accent,#0b9ddd)!important;background:var(--accent-soft,rgba(11,157,221,.13))!important;color:var(--rg-accent,#78d4fb)!important}.rgcc .rgcc-color.active{box-shadow:0 0 0 3px var(--rg-accent,#0b9ddd)}.rgcc .rgcc-note{border-left-color:var(--rg-secondary,#f47321);background:var(--rg-secondary-soft,rgba(244,115,33,.14));color:var(--rg-text,#ffd4b9)}.rgcc .rgcc-chip,.rgcc .rgcc-multi-chip{border-color:var(--rg-secondary,#315d75);background:var(--rg-secondary-soft,#173246);color:var(--rg-text,#b9e8fb)}.rgcc .rgcc-bar,.rgcc .rgcc-spark i,.rgcc .rgcc-progress>i{background:var(--rg-accent,#0b9ddd)}.rgcc .rgcc-multi-picker[open]>summary,.rgcc .rgcc-multi-menu{border-color:var(--rg-accent,#0b9ddd)}.rgcc .rgcc-multi-option input{accent-color:var(--rg-accent,#f47321)} .rgcc .rgcc-multi-suggestions{border-color:var(--rg-secondary,#315d75);background:var(--rg-surface,#132432)}.rgcc .rgcc-multi-suggestion{border-color:var(--rg-secondary,#315d75);background:var(--rg-input,#1c3546);color:var(--rg-text,#b9e8fb)}.rgcc .rgcc-multi-suggestion:hover,.rgcc .rgcc-multi-suggestion:focus-visible{border-color:var(--rg-secondary,#78d4fb);background:var(--rg-secondary-soft,#24465a)}.rgcc .rgcc-multi-suggestion[aria-pressed="true"]{border-color:var(--rg-accent,#f47321)} @media(max-width:760px){.rgcc-overlay{padding:0}.rgcc{height:100vh;border-radius:0}.rgcc-grid,.rgcc-two,.rgcc-workspace,.rgcc-brand-studio,.rgcc-background-studio,.rgcc-responsive-profiles,.rgcc-analytics-dashboard{grid-template-columns:1fr}.rgcc-background-fields .rgcc-grid{grid-template-columns:1fr}.rgcc-dashboard-widget.size-small,.rgcc-dashboard-widget.size-wide,.rgcc-dashboard-widget.size-full{grid-column:1}.rgcc-item,.rgcc-library-row,.rgcc-widget-editor .rgcc-item,.rgcc-provenance-list>div{grid-template-columns:1fr}.rgcc-body{padding:11px}.rgcc-actions{justify-content:flex-start}.rgcc-multi-menu{position:static}.rgcc-palette-grid{grid-template-columns:repeat(2,minmax(100px,1fr))}} .rgcc-settings-tools{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--rg-border,#405061)}.rgcc-settings-tools p{flex:1;min-width:180px;margin:0;color:var(--rg-muted,#aeb9c3);font-size:11px;line-height:1.5}.rgcc-settings-card{padding:0;min-width:0;align-self:start}.rgcc-settings-group>summary{display:flex;align-items:center;gap:12px;min-height:55px;padding:12px 13px;cursor:pointer;list-style:none;border-radius:inherit}.rgcc-settings-group>summary::-webkit-details-marker{display:none}.rgcc-settings-group>summary:after{content:'▸';margin-left:auto;color:var(--rg-accent,#78d4fb);font-size:16px}.rgcc-settings-group[open]>summary:after{content:'▾'}.rgcc-settings-group>summary:hover{background:var(--rg-secondary-soft,rgba(105,201,242,.1))}.rgcc-settings-group>summary:focus-visible{outline:2px solid var(--rg-accent,#78d4fb);outline-offset:-2px}.rgcc-settings-summary{min-width:0;flex:1}.rgcc-settings-summary h3{margin:0;font-size:13px;line-height:1.4}.rgcc-settings-summary small{display:block;margin-top:4px;color:var(--rg-muted,#aeb9c3);font-size:11px;font-weight:400;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rgcc-settings-content{padding:12px 13px;border-top:1px solid var(--rg-border,#405061);min-width:0}.rgcc-body:has(.rgcc-settings-tools)>.rgcc-two{align-items:start} `) })); const settingsLayoutStyle = 'roomgrid-settings-layout-style'; if (!document.getElementById(settingsLayoutStyle)) document.head.appendChild($('style', { id: settingsLayoutStyle, html: trustedHtml(` .rgcc{grid-template-rows:auto auto auto minmax(0,1fr)}.rgcc-head{flex-wrap:wrap}.rgcc-head>div:first-child{flex:1;min-width:180px}.rgcc-head strong{display:block}.rgcc-head>div:first-child span{display:block;margin-top:4px;line-height:1.5}.rgcc-head .rgcc-input{width:auto;max-width:280px;flex:0 1 220px}.rgcc-tabs button{white-space:nowrap} .rgcc-topic-select{display:none}.rgcc-tab-switcher{min-width:0}@media(max-width:700px){.rgcc .rgcc-tab-switcher .rgcc-tabs{display:none}.rgcc .rgcc-topic-select{display:block;width:calc(100% - 24px);margin:6px 12px}} .rgcc-settings-search-area{min-width:0;padding:8px 12px;border-bottom:1px solid var(--rg-border,#405061);background:var(--rg-panel,#172533)}.rgcc-settings-search-bar{display:flex;flex-wrap:wrap;align-items:center;gap:7px}.rgcc-settings-search-bar>input{flex:1;min-width:170px}.rgcc-settings-results{max-height:min(38vh,330px);overflow:auto;margin-top:8px}.rgcc-settings-results[hidden]{display:none}.rgcc-setting-result{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;padding:7px;border-bottom:1px solid var(--rg-border,#405061)}.rgcc-setting-result small{display:block;margin-top:4px;color:var(--rg-muted,#aeb9c3);line-height:1.4;overflow-wrap:anywhere}.rgcc-setting-open{max-width:100%;text-align:left}.rgcc-setting-target{outline:2px solid var(--rg-accent,#4f8cff);outline-offset:4px;border-radius:4px} .rgcc-settings-help{margin-top:8px;color:var(--rg-muted,#aeb9c3);font-size:11px;line-height:1.5}.rgcc-settings-help>summary{cursor:pointer;min-height:32px;display:flex;align-items:center;gap:7px;color:var(--rg-accent,#78d4fb)}.rgcc-settings-help>summary::before{content:'▸'}.rgcc-settings-help[open]>summary::before{content:'▾'}.rgcc-settings-help>summary:focus-visible{outline:2px solid var(--rg-accent,#78d4fb);outline-offset:2px}.rgcc-settings-help p{max-width:85ch;margin:6px 0;overflow-wrap:anywhere} .rgcc-reset-preview{position:fixed;inset:0;z-index:2;display:grid;place-items:center;padding:16px;background:rgba(5,12,18,.86)}.rgcc-reset-preview>.rgcc-card{width:min(660px,100%);max-height:85vh;overflow:auto;box-sizing:border-box}.rgcc-reset-changes{max-height:38vh;overflow:auto;display:grid;gap:5px}.rgcc-reset-preview .rgcc-item{grid-template-columns:minmax(0,1fr) minmax(0,1fr);overflow-wrap:anywhere} @media(max-width:760px){.rgcc-head>div:first-child{min-width:140px}.rgcc-head>div:first-child span{display:none}.rgcc-head .rgcc-input{order:4;max-width:none;flex:1 0 100%;width:100%}} `) })); const previousFocus = document.activeElement; const overlay = $('div', { class: 'rgcc-overlay', dataset: { roomgridControlCenter: '1' }, lang: LANG }); const body = $('div', { class: 'rgcc-body', id: 'roomgrid-control-center-panel', role: statisticsView ? 'region' : 'tabpanel', ariaBusy: 'false' }); if (tab === 'analytics' && hooks?.analyticsSection) body.dataset.analyticsSection = hooks.analyticsSection; let keydownHandler = null; let renderProgressTask = null; let settingsSearchTimer = 0; let pendingSettingTarget = null; let changedSettingsOnly = false; let favoriteSettingsOnly = false; let designUndoState = null; const close = () => { clearTimeout(settingsSearchTimer); renderProgressTask?.finish(); renderProgressTask = null; if (keydownHandler) document.removeEventListener('keydown', keydownHandler, true); overlay.remove(); void RoomGridBackgrounds.apply(store); if (window.__roomGridCloseControlCenter === close) delete window.__roomGridCloseControlCenter; const returnFocus = previousFocus?.closest('[data-camclient-nav-more]')?.querySelector('summary') || previousFocus; if (returnFocus?.isConnected) requestAnimationFrame(() => returnFocus.focus?.()); }; window.__roomGridCloseControlCenter = close; overlay.addEventListener('click', event => { if (event.target === overlay) close(); }); const privacyStateBadge = $('span', { class: 'rgcc-chip', dataset: { analyticsStateBadge: '1' } }, ''); const head = $('div', { class: 'rgcc-head' }, [ $('div', {}, [$('strong', { id: 'roomgrid-control-center-title' }, statisticsView ? tx('统计分析', 'Detailed statistics', 'Detaillierte Auswertungen') : tx('设置', 'Settings', 'Einstellungen')), $('span', {}, statisticsView ? tx('本地历史和质量数据', 'Local history and playback quality', 'Lokaler Verlauf und Wiedergabequalität') : tx('工作区、模型库、设计、数据、历史、通知、隐私与性能', 'Workspaces, library, design, data, history, notifications, privacy and performance', 'Arbeitsbereiche, Bibliothek, Design, Daten, Verlauf, Meldungen, Datenschutz und Leistung'))]), privacyStateBadge, createLanguageSwitcher('rgcc-input'), $('button', { class: 'rgcc-btn rgcc-close', type: 'button', onclick: close }, tx('关闭', 'Close', 'Schließen')), ]); const tabSpecs = [ ['workspaces', tx('工作区', 'Workspaces', 'Arbeitsbereiche')], ['library', tx('模型库', 'Model library', 'Modellbibliothek')], ['data', tx('数据', 'Data', 'Daten')], ['homepage', tx('首页', 'Homepage', 'Startseite')], ['appearance', tx('外观', 'Appearance', 'Design')], ['analytics', tx('历史与统计', 'History & statistics', 'Verlauf & Statistik')], ['broadcaster', tx('主播统计', 'Broadcaster stats', 'Broadcaster-Statistik')], ['notifications', tx('通知', 'Notifications', 'Benachrichtigungen')], ['privacy', tx('隐私', 'Privacy', 'Datenschutz')], ['performance', tx('窗口与性能', 'Windows & performance', 'Fenster & Leistung')], ['extras', tx('可选工具', 'Optional tools', 'Nebenmodule')], ['diagnostics', tx('诊断', 'Diagnostics', 'Diagnose')], ]; const tabButtons = new Map(); const tabBar = $('div', { class: 'rgcc-tabs', role: 'tablist', ariaLabel: tx('设置页面', 'Settings pages', 'Einstellungsseiten') }, tabSpecs.map(([id, label]) => { const button = $('button', { class: 'rgcc-btn', id: `roomgrid-control-center-tab-${id}`, type: 'button', role: 'tab', 'aria-controls': body.id, ariaSelected: String(id === tab), tabIndex: id === tab ? '0' : '-1', onclick: () => { tab = id; render(); } }, label); tabButtons.set(id, button); return button; })); tabBar.addEventListener('keydown', event => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; const buttons = [...tabButtons.values()]; const current = Math.max(0, buttons.indexOf(document.activeElement)); const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (current + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length; event.preventDefault(); buttons[next]?.click(); buttons[next]?.focus(); }); const topicSelect = $('select', { class: 'rgcc-input rgcc-topic-select', ariaLabel: tx('设置页面', 'Settings pages', 'Einstellungsseiten'), onchange: event => { tab = event.target.value; render(); } }, tabSpecs.map(([id, label]) => $('option', { value: id }, label))); const settingsSearch = $('input', { class: 'rgcc-input', type: 'search', placeholder: tx('搜索所有设置…', 'Search all settings…', 'Alle Einstellungen durchsuchen…'), ariaLabel: tx('搜索所有设置', 'Search all settings', 'Alle Einstellungen durchsuchen'), dataset: { settingsSearch: '1' } }); const settingsSearchResults = $('div', { class: 'rgcc-settings-results', dataset: { settingsSearchResults: '1' }, hidden: true }); const settingsSearchStatus = $('span', { class: 'rgcc-muted', role: 'status', ariaLive: 'polite' }); const favoritesToggle = $('button', { class: 'rgcc-btn', type: 'button', ariaPressed: 'false', dataset: { settingsFavoritesOnly: '1' }, onclick: () => { favoriteSettingsOnly = !favoriteSettingsOnly; favoritesToggle.setAttribute('aria-pressed', String(favoriteSettingsOnly)); favoritesToggle.classList.toggle('primary', favoriteSettingsOnly); refreshSettingsSearch(); } }, tx('收藏设置', 'Favorite settings', 'Favoriten')); const changedToggle = $('button', { class: 'rgcc-btn', type: 'button', ariaPressed: 'false', dataset: { settingsChangedOnly: '1' }, onclick: () => { changedSettingsOnly = !changedSettingsOnly; changedToggle.setAttribute('aria-pressed', String(changedSettingsOnly)); changedToggle.classList.toggle('primary', changedSettingsOnly); refreshSettingsSearch(); } }, tx('只显示已更改', 'Changed values only', 'Nur geänderte Werte')); const designUndoButton = $('button', { class: 'rgcc-btn', type: 'button', hidden: true, dataset: { designResetUndo: '1' }, onclick: () => { if (!designUndoState) return; if (Object.entries(designUndoState.reset).some(([key, value]) => JSON.stringify(store.state.settings[key]) !== JSON.stringify(value))) { settingsSearchStatus.textContent = tx('外观在此后又改变,未覆盖新值。配置备份仍可用。', 'Appearance changed again; newer values were not overwritten. The configuration backup remains available.', 'Das Design wurde inzwischen erneut geändert. Neue Werte werden nicht überschrieben; die Konfigurationssicherung bleibt verfügbar.'); return; } store.patchSettings(designUndoState.before); designUndoState = null; designUndoButton.hidden = true; applySuiteAppearance(store); render(); } }, tx('撤销外观重置', 'Undo design reset', 'Designreset rückgängig')); const settingsSearchArea = $('section', { class: 'rgcc-settings-search-area', ariaLabel: tx('设置查找器', 'Settings finder', 'Einstellungsfinder') }, [ $('div', { class: 'rgcc-settings-search-bar' }, [settingsSearch, favoritesToggle, changedToggle, designUndoButton, settingsSearchStatus]), settingsSearchResults, $('details', { class: 'rgcc-settings-help', dataset: { settingsHelp: '1' } }, [ $('summary', {}, tx('如何使用设置?', 'How do settings work?', 'Wie finde und ändere ich eine Einstellung?')), $('p', { class: 'rgcc-note' }, tx('先选择主题,再打开需要的部分。搜索可以直接找到选项。收藏保存快捷入口;已更改只列出与默认值不同的设置。关闭部分不会撤销更改。', 'Choose a topic, then open only the section you need. Search jumps directly to an option. Favorites keep shortcuts; changed values lists differences from defaults. Closing a section does not undo changes.', 'Wähle oben einen Bereich und öffne darunter nur das passende Thema. Oder suche direkt nach einer Option, etwa „Lautstärke“ oder „Cover“. Mit dem Stern speicherst du einen Schnellzugriff. „Nur geänderte Werte“ zeigt Abweichungen vom Standard. Zuklappen blendet Einstellungen nur aus – deine Änderungen bleiben erhalten.')), ]), ]); settingsSearch.addEventListener('input', () => { clearTimeout(settingsSearchTimer); settingsSearchTimer = setTimeout(refreshSettingsSearch, 100); }); const panel = $('section', { class: 'rgcc', role: 'dialog', ariaModal: 'true', ariaLabel: statisticsView ? tx('统计分析', 'Detailed statistics', 'Detaillierte Auswertungen') : tx('RoomGrid 控制中心', 'RoomGrid control center', 'RoomGrid-Zentrale'), lang: LANG, style: statisticsView ? { gridTemplateRows: 'auto minmax(0,1fr)' } : undefined }, [ head, statisticsView ? null : $('div', { class: 'rgcc-tab-switcher' }, [tabBar, topicSelect]), statisticsView ? null : settingsSearchArea, body, ]); overlay.append(panel); document.body.appendChild(overlay); void RoomGridBackgrounds.apply(store, { scope: 'settings' }); keydownHandler = event => { if (event.key === 'Escape') { event.preventDefault(); const resetPreview = panel.querySelector('[data-design-reset-preview]'); if (resetPreview) { resetPreview.remove(); return; } close(); return; } if (event.key !== 'Tab') return; const focusable = [...panel.querySelectorAll('summary,button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href]')].filter(node => node.getClientRects().length && !node.closest('details:not([open]) .rgcc-settings-content')); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }; document.addEventListener('keydown', keydownHandler, true); requestAnimationFrame(() => head.querySelector('button')?.focus()); const btn = (label, onclick, extra = '') => $('button', { class: `rgcc-btn ${extra}`.trim(), type: 'button', disabled: String(extra).split(/\s+/u).includes('disabled'), onclick }, label); const field = (label, control) => $('label', { class: 'rgcc-field' }, [$('span', {}, label), control]); function markSettingControl(node, handler) { const key = String(handler).match(/(?:patchLive|patch|patchSettings)\(\{\s*([A-Za-z]\w*)\s*:/u)?.[1]; if (key) node.dataset.settingKey = tab === 'notifications' ? `notifications.${key}` : key; return node; } const input = (type, value, onchange, attrs = {}) => { const accessible = attrs.placeholder && !attrs.ariaLabel && !attrs['aria-label'] ? { ariaLabel: attrs.placeholder } : {}; const node = markSettingControl($('input', { class: 'rgcc-input', type, value, ...accessible, ...attrs }), onchange); node.addEventListener('change', () => onchange(node.value, node)); return node; }; const colorInput = (value, onlive) => { const node = $('input', { class: 'rgcc-input rgcc-color-input', type: 'color', value }); let last = String(value); const sync = () => { if (node.value === last) return; last = node.value; onlive(node.value, node); }; node.addEventListener('input', sync); node.addEventListener('change', () => { sync(); render(); }); return node; }; const range = (value, onchange, attrs = {}, format = next => String(next)) => { // Range bounds must exist before assigning value. Brave otherwise clamps values // against the temporary HTML default range (0..100), which caused the thumb to // jump back to 100% while the control center was being rendered. const node = markSettingControl($('input', { class: 'rgcc-input', type: 'range', ...attrs, value }), onchange); const output = $('output', { class: 'rgcc-range-value' }, format(Number(value))); let lastSent = Number(value); const sync = () => { const next = Number(node.value); output.value = format(next); output.textContent = output.value; node.setAttribute('aria-valuetext', output.value); if (next === lastSent) return; lastSent = next; onchange(next, node); }; node.addEventListener('input', sync); node.addEventListener('change', sync); sync(); return $('div', { class: 'rgcc-range-control' }, [node, output]); }; const select = (value, options, onchange) => { const isPreviewFpsList = options.some(([id]) => String(id) === '0.1') && options.some(([id]) => String(id) === '5'); const normalizedOptions = isPreviewFpsList && !options.some(([id]) => String(id) === '6') ? [...options, ...PREVIEW_FPS_VALUES.filter(fps => fps > 5).map(fps => [String(fps), `${fps} FPS${fps === 240 ? ' · MAX' : ''}`])] : options; const node = markSettingControl($('select', { class: 'rgcc-input' }, normalizedOptions.map(([id, label]) => $('option', { value: id, selected: String(id) === String(value) }, label))), onchange); node.addEventListener('change', () => onchange(node.value)); return node; }; const check = (label, checked, onchange) => { const node = markSettingControl($('input', { type: 'checkbox', checked: !!checked }), onchange); node.addEventListener('change', () => onchange(node.checked)); return $('label', { class: 'rgcc-check' }, [node, label]); }; const fmtDuration = ms => { const total = Math.max(0, Math.round(Number(ms) || 0)); const hours = Math.floor(total / 3600000); const minutes = Math.floor((total % 3600000) / 60000); return hours ? `${hours} h ${minutes} min` : `${minutes} min`; }; const fmtBytes = value => value >= 1073741824 ? `${(value / 1073741824).toFixed(1)} GB` : value >= 1048576 ? `${(value / 1048576).toFixed(1)} MB` : `${Math.round(value / 1024)} KB`; // A label-only index avoids rendering twelve pages (and their optional // network panels) just to find one option. Values are read on demand. const settingsOptionSeeds = [ ["workspaces", "", tx('名称', 'Name', 'Name'), "Name"], ["workspaces", "", tx('说明', 'Description', 'Beschreibung'), "Description"], ["workspaces", "", tx('离线时自动替换', 'Replace offline models', 'Offline-Models automatisch ersetzen'), "Replace offline models"], ["workspaces", "", tx('替换组', 'Replacement group', 'Ersatzgruppe'), "Replacement group"], ["workspaces", "", tx('延迟秒数', 'Delay seconds', 'Verzögerung (Sek.)'), "Delay seconds"], ["workspaces", "", tx('上级组', 'Parent group', 'Übergeordnete Gruppe'), "Parent group"], ["library", "", tx('优先级', 'Priority', 'Priorität'), "Priority"], ["library", "", tx('搜索', 'Search', 'Suche'), "Search"], ["library", "", tx('名称', 'Name', 'Name'), "Name"], ["library", "", tx('标签', 'Tags', 'Tags'), "Tags"], ["library", "", tx('国家', 'Countries', 'Länder'), "Countries"], ["library", "", tx('类型', 'Type', 'Typ'), "Type"], ["library", "", tx('最少会话', 'Minimum sessions', 'Mindestsitzungen'), "Minimum sessions"], ["library", "", tx('国家', 'Country', 'Land'), "Country"], ["library", "", tx('最少观众', 'Minimum viewers', 'Mindestzuschauer'), "Minimum viewers"], ["library", "", tx('动作', 'Action', 'Aktion'), "Action"], ["library", "", tx('目标组', 'Target group', 'Zielgruppe'), "Target group"], ["data", "", tx('同时导入统计', 'Import statistics too', 'Statistik ebenfalls importieren'), "Import statistics too"], ["data", "", tx('合并媒体(不删除现有媒体)', 'Merge media (keep existing media)', 'Medien zusammenführen (vorhandene bleiben erhalten)'), "Merge media (keep existing media)"], ["data", "", tx('导入独立设置', 'Import supplementary preferences', 'Getrennte Einstellungen importieren'), "Import supplementary preferences"], ["data", "", tx('导入方式', 'Import method', 'Importart'), "Import method"], ["appearance", "", tx('编辑范围', 'Edit area', 'Bereich bearbeiten'), "Edit area"], ["appearance", "", tx('背景类型', 'Background type', 'Hintergrundart'), "Background type"], ["appearance", "", tx('背景颜色', 'Background color', 'Hintergrundfarbe'), "Background color"], ["appearance", "", tx('渐变起点', 'Gradient start', 'Verlaufsanfang'), "Gradient start"], ["appearance", "", tx('渐变终点', 'Gradient end', 'Verlaufsende'), "Gradient end"], ["appearance", "", tx('渐变角度', 'Gradient angle', 'Verlaufswinkel'), "Gradient angle"], ["appearance", "", tx('图像适配', 'Image fit', 'Bildanpassung'), "Image fit"], ["appearance", "", tx('缩放', 'Zoom', 'Zoom'), "Zoom"], ["appearance", "", tx('水平位置', 'Horizontal position', 'Horizontale Position'), "Horizontal position"], ["appearance", "", tx('垂直位置', 'Vertical position', 'Vertikale Position'), "Vertical position"], ["appearance", "", tx('图片不透明度', 'Image opacity', 'Bilddeckkraft'), "Image opacity"], ["appearance", "", tx('背景变暗', 'Darken background', 'Hintergrund abdunkeln'), "Darken background"], ["appearance", "", tx('背景模糊', 'Background blur', 'Hintergrund weichzeichnen'), "Background blur"], ["appearance", "", tx('面板不透明度', 'Panel opacity', 'Panel-Deckkraft'), "Panel opacity"], ["appearance", "", tx('滚动时固定', 'Keep fixed while scrolling', 'Beim Scrollen fixieren'), "Keep fixed while scrolling"], ["appearance", "brandName", tx('名称', 'Name', 'Name'), "Name"], ["appearance", "brandMonogram", tx('缩写', 'Monogram', 'Kürzel'), "Monogram"], ["appearance", "brandTagline", tx('口号', 'Tagline', 'Unterzeile'), "Tagline"], ["appearance", "brandDisplayMode", tx('标志显示', 'Brand display', 'Markenanzeige'), "Brand display"], ["appearance", "brandLogoShape", tx('徽标形状', 'Logo shape', 'Logoform'), "Logo shape"], ["appearance", "brandPageTitleMode", tx('页面标题', 'Page title', 'Seitentitel'), "Page title"], ["appearance", "brandCustomPageTitle", tx('自定义页面标题', 'Custom page title', 'Eigener Seitentitel'), "Custom page title"], ["appearance", "brandLogoWidth", tx('徽标宽度', 'Logo width', 'Logobreite'), "Logo width"], ["appearance", "brandLogoHeight", tx('徽标高度', 'Logo height', 'Logohöhe'), "Logo height"], ["appearance", "brandLogoFit", tx('图像适配', 'Image fit', 'Bildanpassung'), "Image fit"], ["appearance", "brandLogoBackground", tx('徽标背景', 'Logo background', 'Logohintergrund'), "Logo background"], ["appearance", "brandLogoPositionX", tx('水平位置', 'Horizontal position', 'Horizontale Position'), "Horizontal position"], ["appearance", "brandLogoPositionY", tx('垂直位置', 'Vertical position', 'Vertikale Position'), "Vertical position"], ["appearance", "brandLogoPadding", tx('内边距', 'Inner spacing', 'Innenabstand'), "Inner spacing"], ["appearance", "brandShowVersion", tx('显示版本', 'Show version', 'Version anzeigen'), "Show version"], ["appearance", "brandFaviconEnabled", tx('在 RoomGrid 页面使用徽标作为标签图标', 'Use logo as tab icon on RoomGrid pages', 'Logo als Tab-Symbol auf RoomGrid-Seiten verwenden'), "Use logo as tab icon on RoomGrid pages"], ["appearance", "brandReplaceNativeLogo", tx('在原始页眉中替换网站徽标(仅本地)', 'Replace website logo in the original header (local only)', 'Website-Logo in der Original-Kopfzeile ersetzen (nur lokal)'), "Replace website logo in the original header (local only)"], ["appearance", "layoutCardMinWidth", tx('最小卡片宽度', 'Minimum card width', 'Minimale Kartenbreite'), "Minimum card width"], ["appearance", "layoutSectionGap", tx('区块间距', 'Section spacing', 'Abschnittsabstand'), "Section spacing"], ["appearance", "layoutContentPadding", tx('内容内边距', 'Content padding', 'Inhaltsabstand'), "Content padding"], ["appearance", "layoutHeaderHeight", tx('页眉高度', 'Header height', 'Kopfzeilenhöhe'), "Header height"], ["appearance", "layoutMaxColumns", tx('最多列数', 'Maximum columns', 'Maximale Spalten'), "Maximum columns"], ["appearance", "layoutCardAspect", tx('卡片比例', 'Card aspect', 'Kartenformat'), "Card aspect"], ["appearance", "layoutSidebarPosition", tx('侧栏位置', 'Sidebar position', 'Seitenleistenposition'), "Sidebar position"], ["appearance", "layoutStickyHeader", tx('滚动时固定页眉', 'Keep header visible while scrolling', 'Kopfzeile beim Scrollen sichtbar halten'), "Keep header visible while scrolling"], ["appearance", "highContrast", tx('高对比度模式', 'High-contrast mode', 'Hoher Kontrast'), "High-contrast mode"], ["appearance", "underlineLinks", tx('链接加下划线', 'Underline links', 'Links unterstreichen'), "Underline links"], ["appearance", "reduceTransparency", tx('减少透明效果', 'Reduce transparency', 'Transparenz reduzieren'), "Reduce transparency"], ["appearance", "largeTextBoost", tx('放大基础文字', 'Boost base text size', 'Grundtext vergrößern'), "Boost base text size"], ["appearance", "focusRingWidth", tx('焦点环宽度', 'Focus ring width', 'Fokusringbreite'), "Focus ring width"], ["appearance", "minimumTargetSize", tx('最小操作区域', 'Minimum target size', 'Minimale Bedienfläche'), "Minimum target size"], ["appearance", "respectSystemMotion", tx('系统einstellung für reduzierte Bewegung beachten', 'Respect system reduced-motion setting', 'Systemeinstellung für reduzierte Bewegung beachten'), "Respect system reduced-motion setting"], ["appearance", "responsiveAppearanceProfilesEnabled", tx('启用响应式设计配置', 'Enable responsive design profiles', 'Responsive Designprofile aktivieren'), "Enable responsive design profiles"], ["appearance", "appearanceBreakpoints", tx('手机最大宽度', 'Mobile maximum width', 'Mobil bis'), "Mobile maximum width"], ["appearance", "accentColor", tx('主强调色', 'Primary accent', 'Primärakzent'), "Primary accent"], ["appearance", "secondaryAccentColor", tx('次强调色', 'Secondary accent', 'Sekundärakzent'), "Secondary accent"], ["appearance", "contentWidth", tx('内容宽度', 'Content width', 'Inhaltsbreite'), "Content width"], ["appearance", "cardStyle", tx('卡片风格', 'Card style', 'Kartenstil'), "Card style"], ["appearance", "dashboardWidgetDensity", tx('组件密度', 'Widget density', 'Widget-Dichte'), "Widget density"], ["appearance", "surfaceStyle", tx('表面风格', 'Surface style', 'Oberflächenstil'), "Surface style"], ["appearance", "backgroundStyle", tx('页面背景', 'Page background', 'Seitenhintergrund'), "Page background"], ["appearance", "surfaceOpacity", tx('表面不透明度', 'Surface opacity', 'Flächendeckkraft'), "Surface opacity"], ["appearance", "fontFamily", tx('字体样式', 'Font style', 'Schriftstil'), "Font style"], ["appearance", "density", tx('密度', 'Density', 'Dichte'), "Density"], ["appearance", "fontScale", tx('字体缩放', 'Font scale', 'Schriftgröße'), "Font scale"], ["appearance", "controlScale", tx('控件大小', 'Control size', 'Bedienflächengröße'), "Control size"], ["appearance", "cornerRadius", tx('圆角', 'Corner radius', 'Eckenrundung'), "Corner radius"], ["appearance", "cardGap", tx('卡片间距', 'Card spacing', 'Kartenabstand'), "Card spacing"], ["appearance", "shadowStrength", tx('阴影强度', 'Shadow strength', 'Schattenstärke'), "Shadow strength"], ["appearance", "sidebarWidth", tx('侧栏宽度', 'Sidebar width', 'Seitenleistenbreite'), "Sidebar width"], ["appearance", "animations", tx('启用动画', 'Enable animations', 'Animationen aktivieren'), "Enable animations"], ["appearance", "pageAppearanceProfilesEnabled", tx('启用每页设计', 'Enable per-page design', 'Seitenspezifisches Design aktivieren'), "Enable per-page design"], ["appearance", "normalPageDesign", tx('启用RoomGrid页面设计', 'Enable RoomGrid page design', 'RoomGrid-Seitendesign aktivieren'), "Enable RoomGrid page design"], ["appearance", "normalDesignHome", tx('主页', 'Home', 'Startseite'), "Home"], ["appearance", "normalDesignListings", tx('标签和分类页', 'Tag and category pages', 'Tag- und Kategorieseiten'), "Tag and category pages"], ["appearance", "normalDesignRoom", tx('模型页', 'Model pages', 'Modelseiten'), "Model pages"], ["appearance", "normalPageView", tx('视图', 'View', 'Ansicht'), "View"], ["appearance", "normalPageCardScale", tx('卡片大小', 'Card size', 'Kartengröße'), "Card size"], ["appearance", "normalPageDensity", tx('页面密度', 'Page density', 'Seitendichte'), "Page density"], ["appearance", "normalPolishNavigation", tx('整理导航', 'Polish navigation', 'Navigation übersichtlicher gestalten'), "Polish navigation"], ["appearance", "normalPolishSidebar", tx('整理侧栏', 'Polish sidebars', 'Seitenleisten übersichtlicher gestalten'), "Polish sidebars"], ["appearance", "normalHidePromos", tx('隐藏可识别的广告和商品区', 'Hide recognized promo and merch areas', 'Erkannte Werbe- und Merchbereiche ausblenden'), "Hide recognized promo and merch areas"], ["appearance", "normalCompactFooter", tx('压缩页脚', 'Compact footer', 'Kompakter Footer'), "Compact footer"], ["appearance", "normalMarkSaved", tx('标记已保存模型', 'Mark saved models', 'Gespeicherte Models markieren'), "Mark saved models"], ["appearance", "normalMarkFavorites", tx('单独标记收藏', 'Mark favorites separately', 'Favoriten gesondert markieren'), "Mark favorites separately"], ["appearance", "normalMarkVisited", tx('标记已访问模型', 'Mark visited models', 'Besuchte Models markieren'), "Mark visited models"], ["appearance", "normalQuickActions", tx('显示卡片快捷操作', 'Show card quick actions', 'Schnellaktionen auf Karten anzeigen'), "Show card quick actions"], ["appearance", "normalLivePreviews", tx('首页和列表页的动画实时预览', 'Animated live previews on home and listing pages', 'Animierte Live-Vorschauen auf Start- und Listenseiten'), "Animated live previews on home and listing pages"], ["appearance", "normalLivePreviewFps", tx('预览刷新率', 'Preview refresh rate', 'Vorschau-Aktualisierung'), "Preview refresh rate"], ["appearance", "normalLivePreviewMode", tx('更新方式', 'Update mode', 'Aktualisierungsart'), "Update mode"], ["appearance", "normalLivePreviewConcurrency", tx('分批并行限制', 'Batch concurrency limit', 'Parallelitätsgrenze bei Staffelung'), "Batch concurrency limit"], ["appearance", "normalHoverLivePreview", tx('鼠标悬停时播放实时视频', 'Play real-time video while hovering', 'Beim Darüberfahren echtes Livevideo abspielen'), "Play real-time video while hovering"], ["appearance", "normalHoverLivePreviewDelay", tx('启动延迟', 'Start delay', 'Startverzögerung'), "Start delay"], ["appearance", "normalHoverLivePreviewScale", tx('悬停放大', 'Hover enlargement', 'Vergrößerung beim Darüberfahren'), "Hover enlargement"], ["appearance", "previewDockEnabled", tx('启用低负载实时预览', 'Enable low-load live previews', 'Ressourcenschonende Live-Vorschauen aktivieren'), "Enable low-load live previews"], ["appearance", "previewDockMax", tx('同时预览', 'Concurrent previews', 'Gleichzeitige Vorschauen'), "Concurrent previews"], ["appearance", "previewDockSize", tx('窗口大小', 'Window size', 'Fenstergröße'), "Window size"], ["appearance", "previewDockOpacity", tx('不透明度', 'Opacity', 'Deckkraft'), "Opacity"], ["appearance", "inspectorEnabled", tx('启用模型快速信息面板', 'Enable model Quick View', 'Model-Quick-View aktivieren'), "Enable model Quick View"], ["appearance", "chatFocusEnabled", tx('聊天专注模式(可选)', 'Chat focus mode (opt-in)', 'Chat-Fokus-Modus (optional)'), "Chat focus mode (opt-in)"], ["appearance", "chatTimestampEnabled", tx('显示本地时间戳', 'Show local timestamps', 'Lokale Zeitstempel anzeigen'), "Show local timestamps"], ["appearance", "chatSystemMode", tx('系统消息', 'System messages', 'Systemmeldungen'), "System messages"], ["appearance", "chatFilterLevel", tx('聊天过滤强度', 'Chat filter strength', 'Chat-Filterstärke'), "Chat filter strength"], ["appearance", "chatHighlightWords", tx('突出显示词(逗号分隔)', 'Highlight words (comma separated)', 'Hervorheben (kommagetrennt)'), "Highlight words (comma separated)"], ["appearance", "chatMutedWords", tx('静音词(逗号分隔)', 'Muted words (comma separated)', 'Ausgeblendete Wörter (kommagetrennt)'), "Muted words (comma separated)"], ["appearance", "chatCollapseRepeats", tx('折叠重复消息', 'Collapse repeated messages', 'Wiederholte Nachrichten einklappen'), "Collapse repeated messages"], ["appearance", "", tx('正常页面工具栏和卡片操作', 'Normal-page dock and card actions', 'Dock und Kartenaktionen auf normalen Seiten'), "Normal-page dock and card actions"], ["appearance", "", tx('真实音频电平', 'Real audio meters', 'Echte Audiopegel'), "Real audio meters"], ["appearance", "", tx('媒体、应用和比赛目录', 'Media, app and contest directories', 'Medien-, App- und Contest-Verzeichnisse'), "Media, app and contest directories"], ["homepage", "normalHomeToolbar", tx('在模型上方显示快速工具栏', 'Show quick toolbar above models', 'Schnellleiste über den Models anzeigen'), "Show quick toolbar above models"], ["homepage", "normalPageView", tx('视图', 'View', 'Ansicht'), "View"], ["homepage", "normalHomePageSize", tx('每页模型数', 'Models per page', 'Models pro Seite'), "Models per page"], ["homepage", "normalHomeColumns", tx('每行卡片数', 'Cards per row', 'Karten pro Reihe'), "Cards per row"], ["homepage", "normalPageCardScale", tx('卡片大小', 'Card size', 'Kartengröße'), "Card size"], ["homepage", "normalHomeImageRatio", tx('图像比例', 'Image ratio', 'Bildformat'), "Image ratio"], ["homepage", "normalHomeMetaStyle", tx('信息显示', 'Information display', 'Informationsdarstellung'), "Information display"], ["homepage", "normalHomeSort", tx('主要排序', 'Primary sort', 'Hauptsortierung'), "Primary sort"], ["homepage", "normalHomeSearch", tx('姓名、主题、标签或地点', 'Name, subject, tag or place', 'Name, Thema, Tag oder Ort'), "Name, subject, tag or place"], ["homepage", "normalHomeStatus", tx('状态', 'Status', 'Status'), "Status"], ["homepage", "normalHomeGender", tx('类型', 'Type', 'Typ'), "Type"], ["homepage", "normalHomeTagMatchMode", tx('选择多个标签时', 'When several tags are selected', 'Bei mehreren ausgewählten Tags', 'Cuando se seleccionan varias etiquetas', 'Quando várias tags são selecionadas'), "When several tags are selected"], ["homepage", "normalHomeMinViewers", tx('最少观众', 'Minimum viewers', 'Mindestens Zuschauer'), "Minimum viewers"], ["homepage", "normalHomeMaxViewers", tx('最多观众', 'Maximum viewers', 'Höchstens Zuschauer'), "Maximum viewers"], ["homepage", "normalHomeMinAge", tx('最低年龄', 'Minimum age', 'Mindestalter'), "Minimum age"], ["homepage", "normalHomeMaxAge", tx('最高年龄', 'Maximum age', 'Höchstalter'), "Maximum age"], ["homepage", "normalHomeOnlyHd", tx('仅高清模型', 'HD models only', 'Nur HD-Models'), "HD models only"], ["homepage", "normalHomeOnlyNew", tx('仅新模型', 'New models only', 'Nur neue Models'), "New models only"], ["homepage", "normalHomeOnlyGaming", tx('仅游戏模型', 'Gaming models only', 'Nur Gaming-Models'), "Gaming models only"], ["homepage", "normalHomeOnlySaved", tx('仅保存在 RoomGrid 中', 'Only saved in RoomGrid', 'Nur in RoomGrid gespeichert'), "Only saved in RoomGrid"], ["homepage", "normalHomeOnlyFavorites", tx('仅自己的收藏', 'Only own favorites', 'Nur eigene Favoriten'), "Only own favorites"], ["homepage", "normalHomeOnlyFollowing", tx('仅已关注模型', 'Following only', 'Nur gefolgte Models'), "Following only"], ["homepage", "normalHomeHideVisited", tx('隐藏已看过的模型', 'Hide previously watched models', 'Bereits gesehene Models ausblenden'), "Hide previously watched models"], ["analytics", "", tx('时间范围', 'Range', 'Zeitraum'), "Range"], ["analytics", "", tx('记录已启用', 'Recording enabled', 'Aufzeichnung aktiv'), "Recording enabled"], ["analytics", "", tx('私人会话(不记录)', 'Private session (do not record)', 'Private Sitzung (nicht aufzeichnen)'), "Private session (do not record)"], ["analytics", "", tx('保留天数', 'Retention days', 'Aufbewahrungstage'), "Retention days"], ["analytics", "analyticsDashboardPreset", tx('预设', 'Preset', 'Voreinstellung'), "Preset"], ["analytics", "", tx('大小', 'Size', 'Größe'), "Size"], ["analytics", "", tx('导入方式', 'Import method', 'Importart'), "Import method"], ["analytics", "", tx('记录使用统计', 'Record usage statistics', 'Nutzungsstatistik erfassen'), "Record usage statistics"], ["analytics", "", tx('原始事件(天)', 'Raw events (days)', 'Rohereignisse (Tage)'), "Raw events (days)"], ["analytics", "", tx('质量样本(天)', 'Quality samples (days)', 'Qualitätsproben (Tage)'), "Quality samples (days)"], ["analytics", "", tx('账户快照(天)', 'Account snapshots (days)', 'Konto-Snapshots (Tage)'), "Account snapshots (days)"], ["analytics", "", tx('每日汇总(天)', 'Daily rollups (days)', 'Tageswerte (Tage)'), "Daily rollups (days)"], ["broadcaster", "", tx('用户名', 'Username', 'Benutzername'), "Username"], ["broadcaster", "", tx('统计令牌', 'Stats token', 'Statistik-Token'), "Stats token"], ["broadcaster", "", tx('Tresor-Passwort', 'Vault password', 'Tresor-Passwort'), "Vault password"], ["broadcaster", "", tx('密码', 'Password', 'Passwort'), "Password"], ["broadcaster", "", tx('每 5 分钟自动刷新', 'Auto-refresh every 5 minutes', 'Alle 5 Minuten automatisch aktualisieren'), "Auto-refresh every 5 minutes"], ["notifications", "", tx('通知配置', 'Notification profile', 'Benachrichtigungsprofil'), "Notification profile"], ["notifications", "notifications.delivery", tx('输出方式', 'Delivery', 'Ausgabeart', 'Entrega', 'Entrega'), "Delivery"], ["notifications", "notifications.inPagePosition", tx('页面内位置', 'In-page position', 'Position in der Seite', 'Posición en la página', 'Posição na página'), "In-page position"], ["notifications", "notifications.inPageStyle", tx('显示样式', 'Display style', 'Darstellung', 'Estilo de visualización', 'Estilo de exibição'), "Display style"], ["notifications", "notifications.inPageDuration", tx('显示时长', 'Display duration', 'Einblenddauer', 'Duración', 'Duração'), "Display duration"], ["notifications", "notifications.inPageMaxVisible", tx('同时可见', 'Visible at once', 'Gleichzeitig sichtbar', 'Visibles a la vez', 'Visíveis ao mesmo tempo'), "Visible at once"], ["notifications", "notifications.master", tx('启用通知', 'Enable notifications', 'Benachrichtigungen aktivieren'), "Enable notifications"], ["notifications", "notifications.neutral", tx('中性文本', 'Neutral text', 'Neutrale Texte'), "Neutral text"], ["notifications", "notifications.quietStart", tx('安静时间开始', 'Quiet time starts', 'Ruhezeit ab'), "Quiet time starts"], ["notifications", "notifications.quietEnd", tx('安静时间结束', 'Quiet time ends', 'Ruhezeit bis'), "Quiet time ends"], ["notifications", "notifications.cooldownMinutes", tx('冷却(分钟)', 'Cooldown (minutes)', 'Abklingzeit (Min.)'), "Cooldown (minutes)"], ["notifications", "notifications.maxPerHour", tx('每小时上限', 'Hourly limit', 'Limit pro Stunde'), "Hourly limit"], ["notifications", "notifications.favoriteThreshold", tx('收藏在线阈值', 'Favorite threshold', 'Favoriten-Schwelle'), "Favorite threshold"], ["notifications", "notifications.destination", tx('点击后打开', 'Open on click', 'Beim Klick öffnen'), "Open on click"], ["notifications", "notifications.groups", tx('组', 'Group', 'Gruppe'), "Group"], ["privacy", "privacyIdleMinutes", tx('自动遮盖(无操作后)', 'Auto-shield after inactivity', 'Automatisch ausblenden nach Inaktivität'), "Auto-shield after inactivity"], ["privacy", "", tx('设置或更改本地 PIN', 'Set or change local PIN', 'Lokale PIN setzen/ändern'), "Set or change local PIN"], ["privacy", "", tx('使用统计', 'Usage statistics', 'Nutzungsstatistik'), "Usage statistics"], ["performance", "performanceMaxStreams", tx('同时播放上限', 'Concurrent stream limit', 'Gleichzeitige Streams'), "Concurrent stream limit"], ["performance", "adaptiveQuality", tx('自动降低质量', 'Adaptive quality', 'Adaptive Qualität'), "Adaptive quality"], ["performance", "gradualStart", tx('逐步启动', 'Gradual startup', 'Gestaffelter Start'), "Gradual startup"], ["performance", "autoPerformanceMode", tx('自动性能模式', 'Automatic performance mode', 'Automatischer Leistungsmodus'), "Automatic performance mode"], ["performance", "autoPerformanceMemoryPct", tx('内存阈值', 'Memory threshold', 'Speicherschwelle'), "Memory threshold"], ["performance", "autoPerformanceStreamThreshold", tx('流数量阈值', 'Stream-count threshold', 'Stream-Anzahl-Schwelle'), "Stream-count threshold"], ["performance", "windowName", tx('窗口名称', 'Window name', 'Fenstername'), "Window name"], ["extras", "", tx('搜索保存的条目和当前页面', 'Search saved items and current page', 'Gespeicherte Einträge und aktuelle Seite durchsuchen'), "Search saved items and current page"], ["extras", "", tx('收藏', 'Favorite', 'Favorit'), "Favorite"], ["workspaces", "", tx('布局模板', 'Layout templates', 'Layoutvorlagen'), "Layout templates"], ["workspaces", "", tx('组和子组', 'Groups and subgroups', 'Gruppen und Untergruppen'), "Groups and subgroups"], ["library", "", tx('智能集合', 'Smart collections', 'Intelligente Sammlungen'), "Smart collections"], ["library", "", tx('规则与自动化中心', 'Rules & automation center', 'Regel- und Automationszentrum'), "Rules & automation center"], ["data", "", tx('导入预览(尚未更改)', 'Import preview (nothing changed yet)', 'Importvorschau (noch nichts geändert)'), "Import preview (nothing changed yet)"], ["data", "", tx('选择性导出', 'Selective export', 'Selektiver Export'), "Selective export"], ["data", "", tx('存储和备份中心', 'Storage and backup center', 'Speicher- und Sicherungszentrale'), "Storage and backup center"], ["data", "", tx('旧数据清理', 'Legacy cleanup', 'Altdaten bereinigen'), "Legacy cleanup"], ["appearance", "", tx('页面和模型背景', 'Page and model backgrounds', 'Seiten- und Model-Hintergründe'), "Page and model backgrounds"], ["appearance", "", tx('品牌与徽标工作室', 'Brand and logo studio', 'Brand- und Logo-Studio'), "Brand and logo studio"], ["appearance", "", tx('完整颜色系统', 'Complete color system', 'Vollständiges Farbsystem'), "Complete color system"], ["appearance", "", tx('布局编辑器', 'Layout editor', 'Layout-Editor'), "Layout editor"], ["appearance", "", tx('实时设备预览', 'Live device preview', 'Live-Gerätevorschau'), "Live device preview"], ["appearance", "", tx('对比度与无障碍', 'Contrast and accessibility', 'Kontrast und Barrierefreiheit'), "Contrast and accessibility"], ["appearance", "", tx('按页面和屏幕尺寸使用配置', 'Profiles by page and screen size', 'Profile je Seite und Bildschirmgröße'), "Profiles by page and screen size"], ["appearance", "", tx('RoomGrid 设计系统', 'RoomGrid design system', 'RoomGrid-Designsystem'), "RoomGrid design system"], ["appearance", "", tx('主题和表面', 'Theme and surfaces', 'Thema und Oberflächen'), "Theme and surfaces"], ["appearance", "", tx('尺寸和动画', 'Sizing and motion', 'Größen und Animationen'), "Sizing and motion"], ["appearance", "", tx('外观配置', 'Appearance profiles', 'Designprofile'), "Appearance profiles"], ["appearance", "", tx('即时预览', 'Instant preview', 'Sofortvorschau'), "Instant preview"], ["appearance", "", tx('每页设计配置', 'Per-page design profiles', 'Designprofile je Seite'), "Per-page design profiles"], ["appearance", "", tx('统一主导航', 'Unified main navigation', 'Einheitliche Hauptnavigation'), "Unified main navigation"], ["appearance", "", tx('正常网站页面', 'Normal website pages', 'Normale Website-Seiten'), "Normal website pages"], ["appearance", "", tx('悬停实时视频', 'Live video on hover', 'Echtzeitvideo beim Darüberfahren'), "Live video on hover"], ["appearance", "", tx('实时预览停靠栏', 'Live Preview Dock', 'Live-Vorschau-Dock'), "Live Preview Dock"], ["appearance", "", tx('快速信息与聊天专注', 'Quick info & chat focus', 'Quick Info & Chat-Fokus'), "Quick info & chat focus"], ["appearance", "", tx('快捷键', 'Shortcuts', 'Tastaturkürzel'), "Shortcuts"], ["appearance", "", tx('可选模块', 'Optional modules', 'Optionale Module'), "Optional modules"], ["homepage", "", tx('Camclient 作为真正的主页', 'Camclient as the real homepage', 'Camclient als echte Startseite'), "Camclient as the real homepage"], ["homepage", "", tx('首页工作室', 'Homepage Studio', 'Startseiten-Studio'), "Homepage Studio"], ["homepage", "", tx('布局和分页', 'Layout and pagination', 'Layout und Seiteneinteilung'), "Layout and pagination"], ["homepage", "", tx('强大的排序', 'Powerful sorting', 'Leistungsfähige Sortierung'), "Powerful sorting"], ["homepage", "", tx('搜索和范围筛选', 'Search and range filters', 'Suche und Bereichsfilter'), "Search and range filters"], ["homepage", "", tx('快速条件', 'Quick conditions', 'Schnellbedingungen'), "Quick conditions"], ["homepage", "", tx('智能筛选视图', 'Smart filter views', 'Intelligente Filteransichten'), "Smart filter views"], ["homepage", "", tx('每个模型显示的内容', 'What to show for each model', 'Was pro Model angezeigt wird'), "What to show for each model"], ["analytics", "", tx('每周热图', 'Weekly heatmap', 'Wochen-Heatmap'), "Weekly heatmap"], ["analytics", "", tx('观看排名', 'Watch ranking', 'Sehzeit-Rangliste'), "Watch ranking"], ["analytics", "", tx('工作区、筛选与质量事件', 'Workspace, filter and quality events', 'Arbeitsbereich-, Filter- und Qualitätsereignisse'), "Workspace, filter and quality events"], ["analytics", "", tx('最近的时间线', 'Recent timeline', 'Letzte Chronik'), "Recent timeline"], ["analytics", "", tx('每日使用趋势', 'Daily usage trend', 'Täglicher Nutzungstrend'), "Daily usage trend"], ["analytics", "", tx('会话时长分布', 'Session-duration distribution', 'Verteilung der Sitzungsdauer'), "Session-duration distribution"], ["analytics", "", tx('模型排名', 'Model ranking', 'Model-Rangliste'), "Model ranking"], ["analytics", "", tx('预计下一次活跃时间', 'Likely next active times', 'Voraussichtlich nächste aktive Zeiten'), "Likely next active times"], ["analytics", "", tx('本地交互漏斗', 'Local interaction funnel', 'Lokaler Interaktions-Funnel'), "Local interaction funnel"], ["analytics", "", tx('前四个工作区比较', 'Top-four workspace comparison', 'Vergleich der vier stärksten Workspaces'), "Top-four workspace comparison"], ["analytics", "", tx('工作区使用', 'Workspace usage', 'Workspace-Nutzung'), "Workspace usage"], ["analytics", "", tx('按模型的播放质量', 'Playback quality by model', 'Wiedergabequalität je Model'), "Playback quality by model"], ["analytics", "", tx('统计导入助手 · 预览', 'Statistics import assistant · preview', 'Statistik-Importassistent · Vorschau'), "Statistics import assistant · preview"], ["analytics", "", tx('记录、暂停和私人会话', 'Recording, pauses and private sessions', 'Erfassung, Pausen und private Sitzungen'), "Recording, pauses and private sessions"], ["analytics", "", tx('自动删除和保留', 'Automatic deletion and retention', 'Automatische Löschung und Aufbewahrung'), "Automatic deletion and retention"], ["analytics", "", tx('统计备份中心', 'Statistics backup center', 'Statistik-Sicherungszentrale'), "Statistics backup center"], ["analytics", "", tx('数据清单和来源', 'Data inventory and provenance', 'Datenbestand und Herkunft'), "Data inventory and provenance"], ["analytics", "", tx('指标定义与数据质量', 'Metric definitions & data quality', 'Messwert-Definitionen & Datenqualität'), "Metric definitions & data quality"], ["broadcaster", "", tx('安全连接官方统计 API', 'Secure official statistics connection', 'Sichere Verbindung zur offiziellen Statistik-API'), "Secure official statistics connection"], ["broadcaster", "", tx('解锁本地 Tresor', 'Unlock local vault', 'Lokalen Tresor entsperren'), "Unlock local vault"], ["broadcaster", "", tx('账户健康中心', 'Account Health Center', 'Account Health Center'), "Account Health Center"], ["notifications", "", tx('通知输出和显示', 'Notification delivery and display', 'Ausgabe und Anzeige', 'Entrega y visualización de notificaciones', 'Entrega e exibição de notificações'), "Notification delivery and display"], ["notifications", "", tx('通用规则', 'General rules', 'Allgemeine Regeln'), "General rules"], ["notifications", "", tx('每个模型', 'Per model', 'Pro Model'), "Per model"], ["notifications", "", tx('每个组', 'Per group', 'Pro Gruppe'), "Per group"], ["notifications", "", tx('本地通知列表', 'Local notification list', 'Lokale Meldungsliste'), "Local notification list"], ["privacy", "", tx('快速遮盖', 'Quick shield', 'Schnell-Ausblendung'), "Quick shield"], ["privacy", "", tx('记录控制', 'History controls', 'Verlauf steuern'), "History controls"], ["privacy", "", tx('数据边界', 'Data boundary', 'Datengrenze'), "Data boundary"], ["privacy", "", tx('敏感度与同意中心', 'Sensitivity & consent center', 'Sensitivitäts- und Einwilligungscenter'), "Sensitivity & consent center"], ["performance", "", tx('流性能', 'Stream performance', 'Stream-Leistung'), "Stream performance"], ["performance", "", tx('多窗口工作区', 'Multi-window workspaces', 'Mehrfenster-Arbeitsbereiche'), "Multi-window workspaces"], ["performance", "", tx('活动与最近的窗口', 'Active and recent windows', 'Aktive und letzte Fenster'), "Active and recent windows"], ["extras", "", tx('可选工具已关闭', 'Optional tools are disabled', 'Nebenmodule sind ausgeschaltet'), "Optional tools are disabled"], ["extras", "", tx('公开比赛结果', 'Public contest results', 'Öffentliche Contest-Ergebnisse'), "Public contest results"], ["extras", "", tx('原生账户、帮助和播主链接', 'Native account, help and broadcaster links', 'Native Konto-, Hilfe- und Broadcaster-Links'), "Native account, help and broadcaster links"], ["diagnostics", "", tx('修复中心', 'Repair center', 'Reparaturzentrum'), "Repair center"], ["diagnostics", "", tx('兼容性自检', 'Compatibility self-check', 'Kompatibilitäts-Selbsttest'), "Compatibility self-check"], ["diagnostics", "", tx('模块和页面状态', 'Module and page status', 'Modul- und Seitenstatus'), "Module and page status"], ["diagnostics", "", tx('页面路由测试矩阵', 'Page route test matrix', 'Seiten-Routen-Testmatrix'), "Page route test matrix"], ["diagnostics", "", tx('功能能力层', 'Capability layer', 'Capability Layer'), "Capability layer"], ["diagnostics", "", tx('备份和恢复状态', 'Backup and recovery status', 'Backup- und Wiederherstellungsstatus'), "Backup and recovery status"], ["diagnostics", "", tx('已清理的本地错误日志', 'Sanitized local error log', 'Bereinigtes lokales Fehlerprotokoll'), "Sanitized local error log"], ["diagnostics", "", tx('此设置页面无法加载', 'This settings page could not be loaded', 'Diese Einstellungsseite konnte nicht geladen werden'), "This settings page could not be loaded"], ]; const optionCatalog = new Map(); for (const [page, key, label, stableLabel] of settingsOptionSeeds) { const id = `${page}:${key || stableLabel}`; if (!optionCatalog.has(id)) optionCatalog.set(id, { id, page, key, label }); } for (const [page, label] of tabSpecs) optionCatalog.set(`page:${page}`, { id: `page:${page}`, page, key: '', label }); const settingsDefaults = defaultState().settings; let settingsResultPage = 0; const savedSettingFavorites = () => new Set((Array.isArray(store.state.settings.settingsMenuFavorites) ? store.state.settings.settingsMenuFavorites : []).filter(id => typeof id === 'string' && id.length <= 240)); const settingState = (option, notificationSnapshot) => { if (!option.key) return { known: false }; const notificationKey = option.key.startsWith('notifications.') ? option.key.slice(14) : ''; const source = notificationKey ? notificationSnapshot.state() : store.state.settings; const defaults = notificationKey ? notificationSnapshot.defaults() : settingsDefaults; const key = notificationKey || option.key; if (!Object.prototype.hasOwnProperty.call(defaults, key) || !Object.prototype.hasOwnProperty.call(source, key)) return { known: false }; const value = source[key]; const standard = defaults[key]; return { known: true, value, standard, changed: value !== standard && JSON.stringify(value) !== JSON.stringify(standard) }; }; const formatSettingValue = value => { if (typeof value === 'boolean') return value ? tx('开启', 'On', 'An') : tx('关闭', 'Off', 'Aus'); if (value == null || value === '') return tx('未设置', 'Not set', 'Nicht gesetzt'); if (typeof value === 'string') return value.startsWith('data:') ? tx('自有图像', 'Custom image', 'Eigenes Bild') : value.length > 64 ? `${value.slice(0, 61)}…` : value; if (Array.isArray(value)) return `${value.length} ${tx('条目', 'entries', 'Einträge')}`; if (typeof value === 'object') return `${Object.keys(value).length} ${tx('选项', 'options', 'Optionen')}`; return String(value); }; const settingScope = option => option.key.startsWith('notifications.') ? tx('本地通知设置 · 所有 Camclient 标签页', 'Local notifications · all Camclient tabs', 'Lokale Benachrichtigungen · alle Camclient-Tabs') : option.key ? tx('本地全局偏好 · 相关页面使用', 'Local global preference · used by related pages', 'Lokale globale Vorgabe · gilt auf den betreffenden Seiten') : tx('此页面的上下文 · 没有统一默认值', 'Context on this page · no single default value', 'Kontext dieser Seite · kein einheitlicher Standardwert'); function previewDesignReset(keys) { panel.querySelector('[data-design-reset-preview]')?.remove(); const defaults = defaultState().settings; const changes = [...new Set(keys)].filter(key => Object.prototype.hasOwnProperty.call(defaults, key) && JSON.stringify(store.state.settings[key]) !== JSON.stringify(defaults[key])); const previous = document.activeElement; const closePreview = () => { preview.remove(); previous?.focus?.(); }; const warning = $('p', { class: 'rgcc-note', role: 'status' }, tx('只重置下列外观值。会先保存配置备份;模型、笔记、历史及图库文件保持不变。图片关联可能被清除,但原始图片不会删除。', 'Only the appearance values below will be reset after a configuration backup. Models, notes, history and gallery files stay unchanged. Image assignments may be cleared; original images are not deleted.', 'Nur die folgenden Designwerte werden nach einer Konfigurationssicherung zurückgesetzt. Models, Notizen, Verlauf und Galeriedateien bleiben erhalten. Bildzuordnungen können entfernt werden, die Originalbilder werden nicht gelöscht.')); const preview = $('div', { class: 'rgcc-reset-preview', dataset: { designResetPreview: '1' } }, $('section', { class: 'rgcc-card', role: 'alertdialog', ariaLabel: tx('外观重置预览', 'Design reset preview', 'Designreset-Vorschau') }, [ $('h3', {}, tx('外观重置预览', 'Design reset preview', 'Designreset-Vorschau')), warning, $('p', {}, `${changes.length} ${tx('更改的值', 'changed values', 'geänderte Werte')}`), $('div', { class: 'rgcc-reset-changes' }, changes.map(key => { const label = [...optionCatalog.values()].find(option => option.key === key)?.label || key; return $('div', { class: 'rgcc-item' }, [$('strong', {}, label), $('span', {}, `${formatSettingValue(store.state.settings[key])} → ${formatSettingValue(defaults[key])}`)]); })), $('div', { class: 'rgcc-row' }, [btn(tx('取消', 'Cancel', 'Abbrechen'), closePreview), btn(tx('确认重置', 'Confirm reset', 'Zurücksetzen bestätigen'), () => { try { Storage.backupCurrentConfig(); const before = Object.fromEntries(changes.map(key => [key, jsonClone(store.state.settings[key], store.state.settings[key])])); const reset = Object.fromEntries(changes.map(key => [key, jsonClone(defaults[key], defaults[key])])); store.patchSettings(reset); closePreview(); applySuiteAppearance(store); render(); // Sanitization can normalize a default (for example branding). // Compare undo against the actual committed values, not raw defaults. designUndoState = { before, reset: Object.fromEntries(changes.map(key => [key, jsonClone(store.state.settings[key], store.state.settings[key])])) }; designUndoButton.hidden = false; } catch (error) { warning.textContent = tx('重置失败。未删除数据。请打开诊断检查备份或存储。', 'Reset failed. No data was deleted. Check backup/storage in Diagnostics.', 'Zurücksetzen fehlgeschlagen. Es wurden keine Daten gelöscht. Prüfe Sicherung und Speicher in der Diagnose.'); StorageHealth.report('design-reset', error); } }, changes.length ? 'primary' : 'disabled')]), ])); preview.addEventListener('click', event => { if (event.target === preview) closePreview(); }); preview.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closePreview(); } }); panel.appendChild(preview); preview.querySelector('button')?.focus?.(); } function refreshSettingsSearch(resetPage = true) { if (resetPage) settingsResultPage = 0; const query = settingsSearch.value.trim().toLocaleLowerCase(); settingsSearchResults.hidden = !query && !favoriteSettingsOnly && !changedSettingsOnly; if (settingsSearchResults.hidden) { settingsSearchResults.replaceChildren(); settingsSearchStatus.textContent = ''; return; } const favorites = savedSettingFavorites(); // One detached notification snapshot per search pass, not a deep clone of // the complete event/model history for every matching option and row. let notificationState; let notificationStandards; const notificationSnapshot = { state: () => notificationState || (notificationState = notifications?.getState?.() || {}), defaults: () => notificationStandards || (notificationStandards = notificationDefaults()), }; const candidates = [...optionCatalog.values()].filter(option => { const pageLabel = tabButtons.get(option.page)?.textContent || option.page; return (!query || `${option.label} ${pageLabel} ${option.key}`.toLocaleLowerCase().includes(query)) && (!favoriteSettingsOnly || favorites.has(option.id)) && (!changedSettingsOnly || settingState(option, notificationSnapshot).changed); }).sort((left, right) => Number(right.label.toLocaleLowerCase() === query) - Number(left.label.toLocaleLowerCase() === query) || Number(!!right.key) - Number(!!left.key)); const pages = Math.max(1, Math.ceil(candidates.length / 24)); settingsResultPage = Math.min(settingsResultPage, pages - 1); settingsSearchStatus.textContent = `${candidates.length} ${tx('结果', 'results', 'Treffer')} · ${changedSettingsOnly ? tx('仅具有可比较默认值的偏好', 'preferences with comparable defaults only', 'nur Vorgaben mit vergleichbarem Standard') : tx('所有设置页', 'all settings pages', 'alle Einstellungsreiter')}`; const rows = candidates.slice(settingsResultPage * 24, settingsResultPage * 24 + 24).map(option => { const value = settingState(option, notificationSnapshot); const favorite = favorites.has(option.id); const open = btn(option.label, () => { pendingSettingTarget = option; if (option.page === 'analytics' && /raw events|quality samples|account snapshots|daily rollups|usage statistics|privacy|retention|import method/i.test(option.id)) body.dataset.analyticsSection = 'data'; tab = option.page; render(); }); open.classList.add('rgcc-setting-open'); const favoriteButton = btn(favorite ? '★' : '☆', () => { const next = savedSettingFavorites(); if (next.has(option.id)) next.delete(option.id); else next.add(option.id); store.patchSettings({ settingsMenuFavorites: [...next] }); refreshSettingsSearch(false); }); favoriteButton.setAttribute('aria-label', `${favorite ? tx('从收藏移除', 'Remove favorite', 'Favorit entfernen') : tx('收藏设置', 'Favorite setting', 'Einstellung merken')}: ${option.label}`); favoriteButton.setAttribute('aria-pressed', String(favorite)); favoriteButton.dataset.settingsFavorite = option.id; return $('div', { class: 'rgcc-setting-result', dataset: { settingsOption: option.id } }, [ $('div', {}, [open, $('small', {}, `${tabButtons.get(option.page)?.textContent || option.page} · ${settingScope(option)}`), value.known ? $('small', {}, `${tx('当前', 'Current', 'Aktuell')}: ${formatSettingValue(value.value)} · ${tx('默认', 'Default', 'Standard')}: ${formatSettingValue(value.standard)}${value.changed ? ` · ${tx('已更改', 'Changed', 'Geändert')}` : ''}`) : null]), favoriteButton, ]); }); const resultNodes = rows.length ? rows : [$('p', {}, tx('没有匹配的设置。尝试其他词或关闭过滤。', 'No matching settings. Try another word or turn off a filter.', 'Keine passenden Einstellungen. Versuche einen anderen Begriff oder deaktiviere einen Filter.'))]; if (pages > 1) resultNodes.push($('div', { class: 'rgcc-row' }, [btn(tx('上一页', 'Previous', 'Zurück'), () => { settingsResultPage = Math.max(0, settingsResultPage - 1); refreshSettingsSearch(false); }, settingsResultPage === 0 ? 'disabled' : ''), $('span', {}, `${settingsResultPage + 1} / ${pages}`), btn(tx('下一页', 'Next', 'Weiter'), () => { settingsResultPage += 1; refreshSettingsSearch(false); }, settingsResultPage >= pages - 1 ? 'disabled' : '')])); settingsSearchResults.replaceChildren(...resultNodes); } function revealSettingTarget() { const option = pendingSettingTarget; if (!option || option.page !== tab) return; pendingSettingTarget = null; const findTarget = root => { const controls = [...root.querySelectorAll('.rgcc-field,.rgcc-check,h3')]; const keyedControl = option.key ? [...root.querySelectorAll('[data-setting-key]')].find(node => node.dataset.settingKey === option.key) : null; return keyedControl?.closest('.rgcc-field,.rgcc-check') || controls.find(node => (node.querySelector(':scope > span')?.textContent || node.textContent || '').trim() === option.label) || controls.find(node => node.textContent?.trim().startsWith(option.label)); }; let target = findTarget(body); if (!target) { for (const group of body.querySelectorAll('[data-settings-group]')) { const build = deferredSettingGroups.get(group); if (!build) continue; build(); target = findTarget(group); if (target) break; } } if (!target) { settingsSearchStatus.textContent = tx('页面已打开;此选项依赖当前选项卡或上下文。', 'Page opened; this option depends on its selected subtab or context.', 'Seite geöffnet; diese Option hängt vom gewählten Unterreiter oder Kontext ab.'); return; } const group = target.closest('[data-settings-group]'); if (group) { group.open = true; expandedSettingsGroups.add(group.dataset.settingsGroup); } settingsSearchResults.hidden = true; target.classList.add('rgcc-setting-target'); target.scrollIntoView?.({ block: 'center', behavior: 'auto' }); const control = target.querySelector('input:not([type="hidden"]),select,textarea,button') || target.closest('summary'); control?.focus?.({ preventScroll: true }); settingsSearchStatus.textContent = `${tx('已打开', 'Opened', 'Geöffnet')}: ${option.label}`; } const stat = (label, value, tone = '') => $('div', { class: 'rgcc-card' }, [$('span', { class: 'rgcc-label' }, label), $('strong', { class: `rgcc-value ${tone}` }, value)]); function rangeStart(value) { const days = value === 'day' ? 1 : value === 'week' ? 7 : value === 'month' ? 31 : value === 'year' ? 366 : 0; return days ? Date.now() - days * 86400000 : 0; } function renderWorkspaces() { const state = store.state; const currentId = state.settings.activeWorkspaceId || DEFAULT_WORKSPACE_ID; const workspaces = [...(state.workspaces || [])].sort((left, right) => Number(left.archived) - Number(right.archived) || left.order - right.order); const groups = state.groups.filter(group => !group.system); const groupOptions = [['', tx('任何在线模型', 'Any online model', 'Beliebiges Online-Model')], ...groups.map(group => [group.id, group.name])]; const createWorkspace = () => { const name = prompt(tx('工作区名称', 'Workspace name', 'Name des Arbeitsbereichs')); if (!name) return; const description = prompt(tx('可选说明', 'Optional description', 'Optionale Beschreibung')) || ''; const id = store.addWorkspace(name, description); store.activateWorkspace(id); render(); }; const workspaceRows = workspaces.map((item, index) => { const replacement = item.replacement || {}; const device = workspaceDeviceClass(); const layout = item.deviceLayouts?.[device] || item.deviceLayouts?.desktop || {}; return $('section', { class: 'rgcc-card rgcc-workspace' }, [ $('div', {}, [ $('div', { class: 'rgcc-row' }, [$('strong', {}, rawUiText(item.name)), item.id === currentId ? $('span', { class: 'rgcc-chip' }, tx('活动', 'Active', 'Aktiv')) : null, item.archived ? $('span', { class: 'rgcc-chip' }, tx('已归档', 'Archived', 'Archiviert')) : null]), $('p', {}, item.description || tx('没有说明', 'No description', 'Keine Beschreibung')), $('span', { class: 'rgcc-muted' }, `${device} · ${layout.useCustomLayout ? `${layout.customLayoutRows || 2}×${layout.customLayoutCols || 2}` : `${layout.layoutSize || 4}`} · ${layout.viewMode || 'grid'}`), ]), $('div', {}, [ field(tx('名称', 'Name', 'Name'), input('text', item.name, value => { store.patchWorkspace(item.id, { name: value }); render(); }, { maxlength: 80 })), field(tx('说明', 'Description', 'Beschreibung'), input('text', item.description || '', value => store.patchWorkspace(item.id, { description: value }), { maxlength: 300 })), $('div', { class: 'rgcc-row' }, [ check(tx('离线时自动替换', 'Replace offline models', 'Offline-Models automatisch ersetzen'), !!replacement.enabled, value => { store.patchWorkspace(item.id, { replacement: { ...replacement, enabled: value } }); render(); }), field(tx('替换组', 'Replacement group', 'Ersatzgruppe'), select(replacement.groupId || '', groupOptions, value => store.patchWorkspace(item.id, { replacement: { ...replacement, groupId: value } }))), field(tx('延迟秒数', 'Delay seconds', 'Verzögerung (Sek.)'), input('number', replacement.delaySeconds ?? 20, value => store.patchWorkspace(item.id, { replacement: { ...replacement, delaySeconds: value } }), { min: 0, max: 600 })), ]), ]), $('div', { class: 'rgcc-actions' }, [ btn('↑', () => { const ids = workspaces.map(value => value.id); if (index > 0) [ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]; store.reorderWorkspaces(ids); render(); }), btn('↓', () => { const ids = workspaces.map(value => value.id); if (index < ids.length - 1) [ids[index + 1], ids[index]] = [ids[index], ids[index + 1]]; store.reorderWorkspaces(ids); render(); }), !item.archived ? btn(item.id === currentId ? tx('保存当前状态', 'Save current state', 'Aktuellen Stand speichern') : tx('打开', 'Open', 'Öffnen'), () => { if (item.id === currentId) store.saveActiveWorkspace(); else store.activateWorkspace(item.id); render(); }, item.id === currentId ? 'primary' : '') : null, !item.archived ? btn(tx('新窗口', 'New window', 'Neues Fenster'), () => windows.openWindow(item.id, item.name, { workspace: true })) : null, btn(tx('导出此工作区', 'Export this workspace', 'Diesen Arbeitsbereich exportieren'), () => { const payload = dataManager.exportData(['groups','workspaces','layouts']); payload.workspaces = [item]; downloadBlob(new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }), `roomgrid-workspace-${item.id}-${stampForFile()}.json`); }), btn(tx('复制', 'Duplicate', 'Duplizieren'), () => { store.duplicateWorkspace(item.id); render(); }), item.id !== DEFAULT_WORKSPACE_ID ? btn(item.archived ? tx('恢复', 'Restore', 'Wiederherstellen') : tx('归档', 'Archive', 'Archivieren'), () => { RoomGridUx.undoable(store, item.archived ? tx('工作区已恢复', 'Workspace restored', 'Arbeitsbereich wiederhergestellt') : tx('工作区已归档', 'Workspace archived', 'Arbeitsbereich archiviert'), () => store.patchWorkspace(item.id, { archived: !item.archived }), { onUndo: render }); render(); }) : null, item.id !== DEFAULT_WORKSPACE_ID && item.archived ? btn(tx('永久删除', 'Delete permanently', 'Dauerhaft löschen'), () => { if (confirm(tx('永久删除此工作区?', 'Permanently delete this workspace?', 'Diesen Arbeitsbereich dauerhaft löschen?'))) { store.removeWorkspace(item.id); render(); } }, 'danger') : null, ]), ]); }); const templateRows = (state.layoutTemplates || []).map(template => $('div', { class: 'rgcc-item' }, [ $('strong', {}, template.name), $('span', {}, template.description || tx('保存的布局和面板尺寸', 'Saved layout and panel sizes', 'Gespeichertes Raster und Panelgrößen')), $('span', { class: 'rgcc-actions' }, [btn(tx('应用', 'Apply', 'Anwenden'), () => { store.applyLayoutTemplate(template.id); render(); }), btn('×', () => { RoomGridUx.undoable(store, tx('布局模板已删除', 'Layout template deleted', 'Layoutvorlage gelöscht'), () => store.removeLayoutTemplate(template.id), { onUndo: render }); render(); }, 'danger')]), ])); const customGroups = [...groups].sort((a, b) => a.order - b.order); const groupRows = customGroups.map((group, index) => $('div', { class: 'rgcc-item' }, [ $('strong', {}, `${group.parentId ? '↳ ' : ''}${group.name}`), $('span', {}, [ field(tx('上级组', 'Parent group', 'Übergeordnete Gruppe'), select(group.parentId || '', [['', tx('无', 'None', 'Keine')], ...customGroups.filter(parent => parent.id !== group.id).map(parent => [parent.id, parent.name])], value => store.patchGroup(group.id, { parentId: value }))), input('text', group.description || '', value => store.patchGroup(group.id, { description: value }), { placeholder: tx('组说明', 'Group description', 'Gruppenbeschreibung'), maxlength: 240 }), ]), $('span', { class: 'rgcc-actions' }, [ btn('↑', () => { const ids = customGroups.map(item => item.id); if (index > 0) [ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]; store.reorderGroups(ids); render(); }), btn('↓', () => { const ids = customGroups.map(item => item.id); if (index < ids.length - 1) [ids[index + 1], ids[index]] = [ids[index], ids[index + 1]]; store.reorderGroups(ids); render(); }), btn(group.archived ? tx('恢复', 'Restore', 'Wiederherstellen') : tx('归档', 'Archive', 'Archivieren'), () => { store.patchGroup(group.id, { archived: !group.archived }); render(); }), ]), ])); body.replaceChildren( $('div', { class: 'rgcc-toolbar rgcc-row' }, [btn(tx('新建工作区', 'New workspace', 'Neuer Arbeitsbereich'), createWorkspace, 'primary'), btn(tx('保存当前工作区', 'Save current workspace', 'Aktuellen Arbeitsbereich speichern'), () => { store.saveActiveWorkspace(); render(); }), btn(tx('保存布局模板', 'Save layout template', 'Layoutvorlage speichern'), () => { const name = prompt(tx('模板名称', 'Template name', 'Name der Vorlage')); if (name) { store.addLayoutTemplate(name); render(); } })]), $('div', { class: 'rgcc-grid' }, [stat(tx('工作区', 'Workspaces', 'Arbeitsbereiche'), String(workspaces.length)), stat(tx('布局模板', 'Layout templates', 'Layoutvorlagen'), String(state.layoutTemplates?.length || 0)), stat(tx('设备布局', 'Device layout', 'Gerätelayout'), workspaceDeviceClass()), stat(tx('组和子组', 'Groups and subgroups', 'Gruppen und Untergruppen'), String(groups.length))]), $('p', { class: 'rgcc-note' }, tx('每个工作区分别保存桌面和紧凑设备布局。自动替换默认关闭;只有你在工作区中启用后,离线槽位才会使用所选组中的在线模型。', 'Each workspace stores separate desktop and compact layouts. Automatic replacement is off by default and only fills offline slots after you enable it for that workspace.', 'Jeder Arbeitsbereich speichert ein Desktop- und ein Kompaktlayout. Der automatische Ersatz ist standardmäßig aus und füllt Offline-Slots erst, wenn du ihn für den Arbeitsbereich aktivierst.')), $('section', { class: 'rgcc-section rgcc-list' }, workspaceRows), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('布局模板', 'Layout templates', 'Layoutvorlagen')), $('div', { class: 'rgcc-list' }, templateRows.length ? templateRows : [$('p', {}, tx('尚无模板。', 'No templates yet.', 'Noch keine Vorlagen.'))])]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('组和子组', 'Groups and subgroups', 'Gruppen und Untergruppen')), $('div', { class: 'rgcc-row' }, [btn(tx('新建组', 'New group', 'Neue Gruppe'), () => { const name = prompt(tx('组名称', 'Group name', 'Gruppenname')); if (name) { store.addGroup(name); render(); } }, 'primary'), btn(tx('新建子组', 'New subgroup', 'Neue Untergruppe'), () => { const parent = prompt(tx('输入上级组名称', 'Enter parent group name', 'Name der übergeordneten Gruppe')); const parentItem = customGroups.find(item => item.name.toLocaleLowerCase() === String(parent || '').toLocaleLowerCase()); const name = prompt(tx('子组名称', 'Subgroup name', 'Name der Untergruppe')); if (name) { store.addGroup(name, parentItem?.id || ''); render(); } })]), $('div', { class: 'rgcc-list' }, groupRows)]), ); } function renderLibrary() { const settings = store.state.settings; const query = String(body.dataset.libraryQuery || '').trim().toLocaleLowerCase(); const mode = String(body.dataset.libraryMode || 'all'); const smartId = String(body.dataset.smartCollection || ''); const followedId = settings.followedGroupId; const tomorrow = new Date(); tomorrow.setHours(24, 0, 0, 0); const matchesCollection = (room, collection) => { const model = analytics.getModelSync?.(room.id) || {}; const tags = new Set([...(room.localTags || []), ...(model.tags || [])].map(value => String(value).toLocaleLowerCase())); const tests = []; if (collection.tags?.length) tests.push(collection.matchMode === 'any' ? collection.tags.some(tag => tags.has(tag)) : collection.tags.every(tag => tags.has(tag))); if (collection.countries?.length) tests.push(collection.countries.some(value => String(model.country || '').toLocaleLowerCase().includes(value))); if (collection.genders?.length) tests.push(collection.genders.includes(model.gender || 'unknown')); if (collection.minAge) tests.push(Number(model.age || 0) >= collection.minAge); if (collection.maxAge) tests.push(Number(model.age || 0) > 0 && Number(model.age || 0) <= collection.maxAge); if (collection.minWatchMs) tests.push(Number(model.totalMs || 0) >= collection.minWatchMs); if (collection.minSessions) tests.push(Number(model.sessions || 0) >= collection.minSessions); if (collection.favoritesOnly) tests.push(roomInGroup(room, FAVORITE_GROUP_ID)); if (collection.onlineOnly) tests.push(room.lastStatus === 'online' || model.lastStatus === 'online'); return tests.length ? tests.every(Boolean) : true; }; let rooms = [...store.state.rooms]; if (mode === 'favorites') rooms = rooms.filter(room => roomInGroup(room, FAVORITE_GROUP_ID)); else if (mode === 'following') rooms = followedId ? rooms.filter(room => roomInGroup(room, followedId)) : []; else if (mode === 'watchlist') rooms = rooms.filter(room => room.watchlist); else if (mode === 'archived') rooms = rooms.filter(room => room.archived); else if (mode === 'paused') rooms = rooms.filter(room => Number(room.pausedUntil) > Date.now() || Number(room.hiddenUntil) > Date.now()); else rooms = rooms.filter(room => !room.archived); const activeSmart = settings.smartCollections.find(item => item.id === smartId); if (activeSmart) rooms = rooms.filter(room => matchesCollection(room, activeSmart)); if (query) rooms = rooms.filter(room => `${room.id} ${room.notes || ''} ${(room.localTags || []).join(' ')}`.toLocaleLowerCase().includes(query)); rooms.sort((left, right) => Number(right.priority || 0) - Number(left.priority || 0) || Number(right.rating || 0) - Number(left.rating || 0) || left.id.localeCompare(right.id)); const selectedRooms = () => store.state.rooms.filter(room => librarySelection.has(room.id)); const bulkPatch = patch => { const count = librarySelection.size; if (!count) return; RoomGridUx.undoable(store, tx(`${count} 个模型已更新`, `${count} models updated`, `${count} Models aktualisiert`), () => selectedRooms().forEach(room => store.patchRoom(room.id, patch)), { onUndo: render }); render(); }; const search = input('search', query, value => { body.dataset.libraryQuery = value; render(); }, { placeholder: tx('名称、备注或本地标签', 'Name, note or local tag', 'Name, Notiz oder lokaler Tag') }); search.addEventListener('input', debounce(() => { body.dataset.libraryQuery = search.value; render(); }, 180)); const groupSelect = select('', [['', tx('选择组…', 'Choose group…', 'Gruppe wählen…')], ...store.state.groups.filter(group => !group.system).map(group => [group.id, group.name])], value => { if (!value) return; selectedRooms().forEach(room => store.moveToGroup(room.id, value)); render(); }); groupSelect.setAttribute('aria-label', tx('将所选模型添加到组', 'Add selected models to group', 'Ausgewählte Models einer Gruppe zuordnen')); const modeButtons = [['all', tx('全部', 'All', 'Alle')], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['following', 'Following'], ['watchlist', tx('稍后观看', 'Watch later', 'Später ansehen')], ['paused', tx('暂停', 'Paused', 'Pausiert')], ['archived', tx('归档', 'Archive', 'Archiv')]].map(([id, label]) => btn(label, () => { body.dataset.libraryMode = id; render(); }, mode === id ? 'primary' : '')); const collectionButtons = [btn(tx('所有模型', 'All models', 'Alle Models'), () => { delete body.dataset.smartCollection; render(); }, activeSmart ? '' : 'primary'), ...settings.smartCollections.map(collection => btn(`${collection.name} (${store.state.rooms.filter(room => matchesCollection(room, collection)).length})`, () => { body.dataset.smartCollection = collection.id; render(); }, smartId === collection.id ? 'primary' : ''))]; const smartName = input('text', '', () => {}, { placeholder: tx('集合名称', 'Collection name', 'Sammlungsname'), maxlength: 60 }); const smartTags = input('text', '', () => {}, { placeholder: tx('标签,用逗号分隔', 'Tags, comma separated', 'Tags, kommagetrennt'), maxlength: 300 }); const smartCountries = input('text', '', () => {}, { placeholder: tx('国家,用逗号分隔', 'Countries, comma separated', 'Länder, kommagetrennt'), maxlength: 300 }); const smartGender = select('', [['', tx('所有类型', 'All types', 'Alle Typen')], ['f', tx('女性', 'Women', 'Frauen')], ['m', tx('男性', 'Men', 'Männer')], ['c', tx('情侣', 'Couples', 'Paare')], ['t', 'Trans']], () => {}); const smartMinSessions = input('number', 0, () => {}, { min: 0, max: 1000000 }); const smartFavorite = $('input', { type: 'checkbox' }); const smartOnline = $('input', { type: 'checkbox' }); const saveSmart = () => { const name = String(smartName.value || '').trim(); if (!name) return; const terms = value => [...new Set(String(value || '').split(/[,;|]+/u).map(item => item.trim().toLocaleLowerCase()).filter(Boolean))]; const item = { id: `smart_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, name, tags: terms(smartTags.value), countries: terms(smartCountries.value), genders: smartGender.value ? [smartGender.value] : [], minSessions: Number(smartMinSessions.value) || 0, favoritesOnly: smartFavorite.checked, onlineOnly: smartOnline.checked, matchMode: 'all', createdAt: Date.now() }; store.patchSettings({ smartCollections: [...settings.smartCollections, item] }); body.dataset.smartCollection = item.id; render(); }; const ruleName = input('text', '', () => {}, { placeholder: tx('规则名称', 'Rule name', 'Regelname'), maxlength: 60 }); const ruleTags = input('text', '', () => {}, { placeholder: tx('所需标签', 'Required tags', 'Benötigte Tags'), maxlength: 300 }); const ruleCountry = input('text', '', () => {}, { placeholder: tx('国家', 'Country', 'Land'), maxlength: 120 }); const ruleViewers = input('number', 0, () => {}, { min: 0, max: 10000000 }); const ruleAction = select('notify', [['notify', tx('本地提示', 'Local alert', 'Lokaler Hinweis')], ['favorite', tx('加入收藏', 'Add to favorites', 'Zu Favoriten')], ['group', tx('加入组', 'Add to group', 'Zu Gruppe')]], () => {}); const ruleGroup = select('', [['', tx('选择组…', 'Choose group…', 'Gruppe wählen…')], ...store.state.groups.filter(group => !group.system).map(group => [group.id, group.name])], () => {}); const saveAutomationRule = () => { const name = String(ruleName.value || '').trim(); if (!name) return; const terms = value => [...new Set(String(value || '').split(/[,;|]+/u).map(item => item.trim().toLocaleLowerCase()).filter(Boolean))]; store.patchSettings({ automationRules: [...settings.automationRules, { id: `rule_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, name, enabled: true, tags: terms(ruleTags.value), countries: terms(ruleCountry.value), minViewers: Number(ruleViewers.value) || 0, action: ruleAction.value, groupId: ruleGroup.value }].slice(-30) }); render(); }; const automationRows = settings.automationRules.map(rule => $('div', { class: 'rgcc-item' }, [$('strong', {}, rule.name), $('span', {}, `${(rule.tags || []).map(tag => `#${tag}`).join(' ') || tx('所有标签', 'Any tag', 'Beliebige Tags')} · ${rule.countries?.join(', ') || tx('所有国家', 'Any country', 'Beliebiges Land')} · ${rule.action}`), $('span', { class: 'rgcc-actions' }, [btn(rule.enabled ? tx('暂停', 'Pause', 'Pausieren') : tx('启用', 'Enable', 'Aktivieren'), () => { store.patchSettings({ automationRules: settings.automationRules.map(item => item.id === rule.id ? { ...item, enabled: !item.enabled } : item) }); render(); }), btn(tx('删除', 'Delete', 'Löschen'), () => { RoomGridUx.undoable(store, tx('自动化规则已删除', 'Automation rule deleted', 'Automationsregel gelöscht'), () => store.patchSettings({ automationRules: settings.automationRules.filter(item => item.id !== rule.id) }), { onUndo: render }); render(); }, 'danger')]) ])); const rows = rooms.slice(0, 500).map(room => { const chosen = librarySelection.has(room.id); const tags = (room.localTags || []).join(', '); const favorite = roomInGroup(room, FAVORITE_GROUP_ID); const following = followedId && roomInGroup(room, followedId); const modelStat = analytics.getModelSync?.(room.id) || {}; const lastVisited = Number(modelStat.lastWatchedAt || 0); const lastOnline = Number(room.lastSeenOnline || modelStat.lastOnlineAt || 0); const checkbox = $('input', { type: 'checkbox', ariaLabel: rawUiText(`${tx('选择模型', 'Select model', 'Model auswählen')}: ${room.id}`), checked: chosen, onchange: event => { if (event.target.checked) librarySelection.add(room.id); else librarySelection.delete(room.id); render(); } }); const rating = $('div', { class: 'rgcc-stars' }, [1,2,3,4,5].map(value => $('button', { class: value <= Number(room.rating || 0) ? 'on' : '', type: 'button', title: `${value}/5`, onclick: () => { store.patchRoom(room.id, { rating: value === room.rating ? 0 : value }); render(); } }, '★'))); return $('div', { class: 'rgcc-library-row' }, [ checkbox, $('div', {}, [$('strong', {}, rawUiText(room.id)), $('div', { class: 'rgcc-row' }, [favorite ? $('span', { class: 'rgcc-chip' }, '★') : null, following ? $('span', { class: 'rgcc-chip' }, 'Following') : null, room.watchlist ? $('span', { class: 'rgcc-chip' }, tx('稍后', 'Later', 'Später')) : null, $('span', { class: room.lastStatus === 'online' ? 'rgcc-good' : 'rgcc-muted' }, room.lastStatus), !lastVisited ? $('span', { class: 'rgcc-chip' }, tx('从未访问', 'Never visited', 'Nie besucht')) : null]), $('div', { class: 'rgcc-muted', style: { marginTop: '5px', fontSize: '10px' } }, `${tx('最后在线', 'Last online', 'Zuletzt online')}: ${lastOnline ? new Date(lastOnline).toLocaleString() : '—'} · ${tx('最后访问', 'Last visited', 'Zuletzt besucht')}: ${lastVisited ? new Date(lastVisited).toLocaleString() : '—'}`)]), $('div', {}, [input('text', room.notes || '', value => store.patchRoom(room.id, { notes: value }), { placeholder: tx('本地备注', 'Local note', 'Lokale Notiz'), maxlength: 500 }), input('text', tags, value => store.patchRoom(room.id, { localTags: String(value).split(/[,;|]+/u) }), { placeholder: tx('本地标签,用逗号分隔', 'Local tags, comma separated', 'Lokale Tags, mit Komma getrennt'), maxlength: 400 })]), $('div', {}, [rating, field(tx('优先级', 'Priority', 'Priorität'), select(room.priority || 0, [[0,'0'],[1,'1'],[2,'2'],[3,'3'],[4,'4'],[5,'5']], value => store.patchRoom(room.id, { priority: value })))]), $('div', { class: 'rgcc-actions' }, [ btn(favorite ? '★' : '☆', () => { store.toggleRoomInGroup(room.id, FAVORITE_GROUP_ID); render(); }), btn(room.watchlist ? tx('移出稍后', 'Remove later', 'Nicht später') : tx('稍后', 'Later', 'Später'), () => { store.patchRoom(room.id, { watchlist: !room.watchlist }); render(); }), btn(tx('今天隐藏', 'Hide today', 'Heute ausblenden'), () => { store.patchRoom(room.id, { hiddenUntil: tomorrow.getTime() }); render(); }), btn(tx('暂停7天', 'Pause 7 days', '7 Tage pausieren'), () => { store.patchRoom(room.id, { pausedUntil: Date.now() + 7 * 86400000 }); render(); }), btn(room.archived ? tx('恢复', 'Restore', 'Wiederherstellen') : tx('归档', 'Archive', 'Archivieren'), () => { store.patchRoom(room.id, { archived: !room.archived }); render(); }), btn(tx('不再建议', 'Never suggest', 'Nicht mehr vorschlagen'), () => { store.patchRoom(room.id, { suggestionHidden: true }); render(); }), ]), ]); }); body.replaceChildren( $('div', { class: 'rgcc-toolbar' }, [$('div', { class: 'rgcc-row' }, [field(tx('搜索', 'Search', 'Suche'), search), ...modeButtons]), $('div', { class: 'rgcc-row' }, [btn(tx('选择当前结果', 'Select results', 'Treffer auswählen'), () => { rooms.forEach(room => librarySelection.add(room.id)); render(); }), btn(tx('清除选择', 'Clear selection', 'Auswahl leeren'), () => { librarySelection.clear(); render(); }), $('span', { class: 'rgcc-chip' }, `${librarySelection.size} ${tx('已选择', 'selected', 'ausgewählt')}`), groupSelect, btn(tx('收藏', 'Favorite', 'Favorisieren'), () => { selectedRooms().forEach(room => { if (!roomInGroup(room, FAVORITE_GROUP_ID)) store.toggleRoomInGroup(room.id, FAVORITE_GROUP_ID); }); render(); }, librarySelection.size ? '' : 'disabled'), btn(tx('加入稍后观看', 'Add to watch later', 'Später ansehen'), () => bulkPatch({ watchlist: true })), btn(tx('归档', 'Archive', 'Archivieren'), () => bulkPatch({ archived: true })), btn(tx('永久删除', 'Delete permanently', 'Dauerhaft löschen'), () => { if (librarySelection.size && confirm(tx('永久删除所选模型?', 'Permanently delete selected models?', 'Ausgewählte Models dauerhaft löschen?'))) { [...librarySelection].forEach(id => store.removeRoom(id)); librarySelection.clear(); render(); } }, 'danger')])]), $('div', { class: 'rgcc-grid' }, [stat(tx('全部模型', 'All models', 'Alle Models'), String(store.state.rooms.length)), stat(tx('收藏', 'Favorites', 'Favoriten'), String(store.state.rooms.filter(room => roomInGroup(room, FAVORITE_GROUP_ID)).length)), stat('Following', String(followedId ? store.state.rooms.filter(room => roomInGroup(room, followedId)).length : 0)), stat(tx('归档', 'Archived', 'Archiviert'), String(store.state.rooms.filter(room => room.archived).length))]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('智能集合', 'Smart collections', 'Intelligente Sammlungen')), $('p', {}, tx('集合会随着本地资料和统计自动增长;模型不会被复制。', 'Collections update automatically as local metadata and statistics grow; models are not duplicated.', 'Sammlungen wachsen automatisch mit lokalen Metadaten und Statistiken; Models werden nicht dupliziert.')), $('div', { class: 'rgcc-row' }, collectionButtons), $('div', { class: 'rgcc-row' }, [field(tx('名称', 'Name', 'Name'), smartName), field(tx('标签', 'Tags', 'Tags'), smartTags), field(tx('国家', 'Countries', 'Länder'), smartCountries), field(tx('类型', 'Type', 'Typ'), smartGender), field(tx('最少会话', 'Minimum sessions', 'Mindestsitzungen'), smartMinSessions)]), $('div', { class: 'rgcc-row' }, [$('label', { class: 'rgcc-check' }, [smartFavorite, tx('仅收藏', 'Favorites only', 'Nur Favoriten')]), $('label', { class: 'rgcc-check' }, [smartOnline, tx('仅在线', 'Online only', 'Nur online')]), btn(tx('保存智能集合', 'Save smart collection', 'Intelligente Sammlung speichern'), saveSmart, 'primary'), activeSmart ? btn(tx('删除当前集合', 'Delete current collection', 'Aktuelle Sammlung löschen'), () => { if (confirm(tx('删除此智能集合?模型数据保持不变。', 'Delete this smart collection? Model data stays unchanged.', 'Diese intelligente Sammlung löschen? Modeldaten bleiben erhalten.'))) { store.patchSettings({ smartCollections: settings.smartCollections.filter(item => item.id !== activeSmart.id) }); delete body.dataset.smartCollection; render(); } }, 'danger') : null])]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('规则与自动化中心', 'Rules & automation center', 'Regel- und Automationszentrum')), $('p', {}, tx('当本地模型数据匹配时,可显示一次提示、加入收藏或加入指定组。', 'When local model data matches, show one alert, add a favorite, or add the model to a chosen group.', 'Wenn lokale Modeldaten passen, wird einmal hingewiesen oder das Model zu Favoriten bzw. einer Gruppe hinzugefügt.')), $('div', { class: 'rgcc-row' }, [field(tx('名称', 'Name', 'Name'), ruleName), field(tx('标签', 'Tags', 'Tags'), ruleTags), field(tx('国家', 'Country', 'Land'), ruleCountry), field(tx('最少观众', 'Minimum viewers', 'Mindestzuschauer'), ruleViewers), field(tx('动作', 'Action', 'Aktion'), ruleAction), field(tx('目标组', 'Target group', 'Zielgruppe'), ruleGroup), btn(tx('保存规则', 'Save rule', 'Regel speichern'), saveAutomationRule, 'primary')]), $('div', { class: 'rgcc-list' }, automationRows.length ? automationRows : [$('p', { class: 'rgcc-muted' }, tx('还没有自动化规则。', 'No automation rules yet.', 'Noch keine Automationsregeln.'))])]), $('p', { class: 'rgcc-note' }, tx('用户名完全匹配后去重。搜索同时使用名称、备注和本地标签。归档、从视图移除和永久删除是三个不同操作。', 'Usernames are deduplicated by exact normalized match. Search covers names, notes and local tags. Archive, remove from a view and permanent deletion remain separate actions.', 'Benutzernamen werden nach exakter normalisierter Übereinstimmung dedupliziert. Die Suche umfasst Name, Notizen und lokale Tags. Archivieren, aus einer Ansicht entfernen und dauerhaft löschen bleiben getrennte Aktionen.')), $('div', { class: 'rgcc-list rgcc-section' }, rows.length ? rows : [$('section', { class: 'rgcc-card' }, tx('没有匹配模型。', 'No matching models.', 'Keine passenden Models.'))]), $('div', { class: 'rgcc-row' }, [btn('CSV ' + tx('导出', 'export', 'exportieren'), () => downloadBlob(new Blob(['\ufeff' + dataManager.exportCsv(rooms)], { type: 'text/csv;charset=utf-8' }), `roomgrid-models-${stampForFile()}.csv`)), btn('JSON ' + tx('导出', 'export', 'exportieren'), () => downloadBlob(new Blob([JSON.stringify(dataManager.exportData(['models','groups']), null, 2)], { type: 'application/json' }), `roomgrid-models-${stampForFile()}.json`))]), ); } async function renderData(revision) { const [storageSummary, statisticsInventory, statisticsBackups] = await Promise.all([PrivacyShield.storageSummary(), analytics.measureDataInventory(), analytics.getBackups()]); if (revision !== renderRevision || tab !== 'data') return; const names = { models: tx('模型和备注', 'Models and notes', 'Models und Notizen'), groups: tx('组', 'Groups', 'Gruppen'), workspaces: tx('工作区', 'Workspaces', 'Arbeitsbereiche'), layouts: tx('布局模板', 'Layout templates', 'Layoutvorlagen'), settings: tx('设置', 'Settings', 'Einstellungen') }; const exportCompletePackage = async () => { const progress = notifications?.beginProgress?.(tx('正在准备完整备份', 'Preparing complete backup', 'Vollständige Sicherung wird vorbereitet'), { delayMs: 0 }); try { const resources = exportCompleteBackupResources(); const [statistics, media] = await Promise.all([analytics.exportData(), RoomGridModelMedia.exportData()]); RoomGridModelMedia.validateImport(media); const payload = { kind: 'roomgrid-complete-backup', schema: 2, softwareVersion: META.version, exportedAt: Date.now(), sensitive: true, config: dataManager.exportData(), statistics, media, resources }; const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' }); if (blob.size > COMPLETE_BACKUP_MAX_BYTES) throw new Error('Backup exceeds the 512 MiB import/export limit; back up large media files separately'); downloadBlob(blob, `roomgrid-complete-backup-${stampForFile()}.json`); } catch (error) { alert(`${tx('无法创建完整备份。未下载不完整文件,当前数据保持不变。', 'The complete backup could not be created. No incomplete file was downloaded; current data was not changed.', 'Die vollständige Sicherung konnte nicht erstellt werden. Keine unvollständige Datei wurde heruntergeladen; aktuelle Daten bleiben unverändert.')}\n${String(error?.message || error)}`); } finally { progress?.finish(); } }; const fileInput = $('input', { type: 'file', accept: 'application/json', style: { display: 'none' }, onchange: async event => { const file = event.target.files?.[0]; if (!file) return; try { if (file.size > COMPLETE_BACKUP_MAX_BYTES) throw new Error('Backup exceeds the 512 MiB import/export limit'); const parsed = JSON.parse(await file.text()); const complete = parsed?.kind === 'roomgrid-complete-backup'; if (complete && ![1, 2].includes(parsed.schema)) throw new Error('Unsupported complete-backup schema'); const review = dataManager.inspect(complete ? parsed.config : parsed); if (complete && parsed.statistics) { review.statistics = analytics.inspectImport(parsed.statistics); review.includeStatistics = true; } if (complete && parsed.schema === 2) { // Validate every additional section before offering any write. RoomGridModelMedia.validateImport(parsed.media); review.media = parsed.media; review.resources = validateCompleteBackupResources(parsed.resources); review.includeMedia = true; review.includeResources = true; } review.selected = new Set(review.sections); importReview = review; render(); } catch (error) { importReview = null; alert(`${tx('无法读取此备份。当前数据保持不变。', 'This backup could not be read. Current data was not changed.', 'Diese Sicherung konnte nicht gelesen werden. Die aktuellen Daten wurden nicht verändert.')}\n${String(error?.message || error)}`); render(); } } }); const reviewNode = importReview ? $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('导入预览(尚未更改)', 'Import preview (nothing changed yet)', 'Importvorschau (noch nichts geändert)')), $('div', { class: 'rgcc-grid' }, dataManager.sections.map(section => stat(names[section], `${importReview.counts[section] || 0} · +${importReview.newItems[section] || 0} · ${importReview.conflicts[section] || 0} ${tx('冲突', 'conflicts', 'Konflikte')}`))), importReview.statistics ? $('div', { class: 'rgcc-card rgcc-section' }, [$('strong', {}, tx('包含敏感统计', 'Includes sensitive statistics', 'Enthält sensible Statistik')), $('p', {}, `${importReview.statistics.counts.models || 0} ${tx('模型', 'models', 'Models')} · ${importReview.statistics.counts.events || 0} ${tx('事件', 'events', 'Ereignisse')} · ${importReview.statistics.counts.qualitySamples || 0} ${tx('质量样本', 'quality samples', 'Qualitätsproben')}`), check(tx('同时导入统计', 'Import statistics too', 'Statistik ebenfalls importieren'), importReview.includeStatistics, value => { importReview.includeStatistics = value; })]) : null, importReview.media ? $('section', { class: 'rgcc-card rgcc-section', dataset: { completeBackupExtras: '1' } }, [ $('strong', {}, tx('媒体和独立设置', 'Media and supplementary preferences', 'Medien und getrennte Einstellungen')), $('p', {}, `${importReview.media.records.length} ${tx('媒体记录', 'media records', 'Medieneinträge')} · ${Object.keys(importReview.resources.local).length} ${tx('设置区域', 'preference areas', 'Einstellungsbereiche')}`), check(tx('合并媒体(不删除现有媒体)', 'Merge media (keep existing media)', 'Medien zusammenführen (vorhandene bleiben erhalten)'), importReview.includeMedia, value => { importReview.includeMedia = value; }), check(tx('导入独立设置', 'Import supplementary preferences', 'Getrennte Einstellungen importieren'), importReview.includeResources, value => { importReview.includeResources = value; }), $('p', { class: 'rgcc-note' }, tx('文件夹授权、登录信息、PIN和账户令牌不包含在内。保存在电脑上、没有浏览器副本的文件需要单独备份。独立设置在重新加载后完整生效。', 'Folder permissions, login information, PINs and account tokens are excluded. Files saved only on your computer need a separate backup. Supplementary preferences take full effect after reloading.', 'Ordnerberechtigungen, Anmeldedaten, PINs und Konto-Token sind ausgeschlossen. Nur auf dem PC gespeicherte Dateien ohne Browserkopie müssen separat gesichert werden. Getrennte Einstellungen wirken nach dem Neuladen vollständig.')), ]) : null, $('div', { class: 'rgcc-row' }, dataManager.sections.map(section => check(names[section], importReview.selected.has(section), value => { if (value) importReview.selected.add(section); else importReview.selected.delete(section); }))), $('div', { class: 'rgcc-row' }, [field(tx('导入方式', 'Import method', 'Importart'), select(importStrategy, [['merge', tx('合并(推荐)', 'Merge (recommended)', 'Zusammenführen (empfohlen)')], ['replace', tx('替换所选区域', 'Replace selected sections', 'Gewählte Bereiche ersetzen')]], value => { importStrategy = value; })), btn(tx('应用导入', 'Apply import', 'Import anwenden'), async () => { const completed = []; try { const review = importReview; if (review.includeMedia) RoomGridModelMedia.validateImport(review.media); if (review.includeResources) validateCompleteBackupResources(review.resources); if (review.includeResources) localStorage.setItem(`ryujo_multicam_extra_resources_backup_${Date.now()}`, JSON.stringify(exportCompleteBackupResources())); dataManager.apply(review, [...review.selected], importStrategy); completed.push(tx('配置', 'Configuration', 'Konfiguration')); if (review.statistics && review.includeStatistics) { await analytics.importData(review.statistics, { strategy: importStrategy, sections: review.statistics.sections }); completed.push(tx('统计', 'Statistics', 'Statistik')); } if (review.includeMedia) { await RoomGridModelMedia.importData(review.media); completed.push(tx('媒体', 'Media', 'Medien')); } if (review.includeResources) { applyCompleteBackupResources(review.resources, importStrategy); completed.push(tx('独立设置', 'Supplementary preferences', 'Getrennte Einstellungen')); } importReview = null; alert(`${tx('导入完成。之前的配置和统计已分别备份。', 'Import complete. Previous configuration and statistics were backed up separately.', 'Import abgeschlossen. Vorherige Konfiguration und Statistik wurden getrennt gesichert.')}\n${completed.join(' · ')}${review.includeResources ? `\n${tx('请重新加载以应用所有设置。', 'Reload to apply all preferences.', 'Bitte neu laden, damit alle Einstellungen übernommen werden.')}` : ''}`); render(); } catch (error) { alert(`${tx('导入未完整完成。已导入的区域不会自动撤销;配置与统计回滚点仍可恢复。', 'Import did not complete. Imported sections are not automatically reverted; configuration and statistics restore points remain available.', 'Import nicht vollständig abgeschlossen. Bereits importierte Bereiche werden nicht automatisch zurückgesetzt; Konfigurations- und Statistik-Rücksetzpunkte bleiben verfügbar.')}\n${tx('已完成', 'Completed', 'Bereits abgeschlossen')}: ${completed.join(' · ') || '—'}\n${String(error?.message || error)}`); } }, 'primary'), btn(tx('取消预览', 'Cancel preview', 'Vorschau abbrechen'), () => { importReview = null; render(); })]), $('p', { class: 'rgcc-note' }, importReview.sensitive ? tx('此文件含模型、备注或工作区数据,应视为敏感文件。', 'This file contains model, note or workspace data and should be treated as sensitive.', 'Diese Datei enthält Model-, Notiz- oder Arbeitsbereichsdaten und ist sensibel.') : tx('此导入未标记为敏感。', 'This import is not marked sensitive.', 'Dieser Import ist nicht als sensibel markiert.')), ]) : null; body.replaceChildren( $('div', { class: 'rgcc-grid' }, [stat(tx('数据版本', 'Data version', 'Datenversion'), String(store.state.dataVersion || 1)), stat(tx('软件版本', 'Software version', 'Softwareversion'), META.version), stat(tx('修订', 'Revision', 'Revision'), String(store.state.revision || 0)), stat(tx('本地模型', 'Local models', 'Lokale Models'), String(store.state.rooms.length))]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('选择性导出', 'Selective export', 'Selektiver Export')), $('div', { class: 'rgcc-row' }, dataManager.sections.map(section => check(names[section], exportSections.has(section), value => { if (value) exportSections.add(section); else exportSections.delete(section); }))), $('div', { class: 'rgcc-row' }, [btn(tx('导出所选', 'Export selected', 'Auswahl exportieren'), () => downloadBlob(new Blob([JSON.stringify(dataManager.exportData([...exportSections]), null, 2)], { type: 'application/json' }), `roomgrid-backup-${stampForFile()}.json`), 'primary'), btn(tx('完整导出', 'Full export', 'Vollständiger Export'), () => downloadBlob(new Blob([JSON.stringify(dataManager.exportData(), null, 2)], { type: 'application/json' }), `roomgrid-full-backup-${stampForFile()}.json`)), btn(tx('完整安全包', 'Complete safety package', 'Komplettes Sicherungspaket'), exportCompletePackage, 'primary'), btn(tx('单独导出统计', 'Export statistics separately', 'Statistik separat exportieren'), async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `roomgrid-statistics-${stampForFile()}.json`); }), btn(tx('选择文件并预览', 'Choose file and preview', 'Datei wählen und prüfen'), () => { document.body.appendChild(fileInput); fileInput.click(); })]), $('p', {}, tx('完整安全包包含配置、统计、浏览器图库和独立设置。电脑文件、文件夹权限、登录信息、PIN及账户令牌不包含在内;它们需要单独备份或重新授权。', 'The complete safety package contains configuration, statistics, browser gallery and supplementary preferences. Computer-only files, folder permissions, login information, PINs and account tokens are excluded and require a separate backup or authorization.', 'Das komplette Sicherungspaket enthält Konfiguration, Statistik, Browsergalerie und getrennte Einstellungen. Nur auf dem PC gespeicherte Dateien, Ordnerberechtigungen, Anmeldedaten, PINs und Konto-Token sind ausgeschlossen und benötigen eine separate Sicherung oder erneute Freigabe.'))]), ...(reviewNode ? [reviewNode] : []), $('p', { class: 'rgcc-note' }, tx('JSON备份上限为512 MiB,适用于导入和完整导出。请将较大的媒体文件单独保存,不会下载超出限制或缺少媒体的不完整包。', 'JSON backups support up to 512 MiB for import and complete export. Back up larger media files separately; incomplete packages that exceed the limit or omit unavailable media are not downloaded.', 'JSON-Sicherungen unterstützen bis zu 512 MiB beim Import und vollständigen Export. Größere Mediendateien bitte separat sichern; unvollständige Pakete über dem Limit oder mit nicht lesbaren Medien werden nicht heruntergeladen.')), $('section', { class: 'rgcc-card rgcc-section', dataset: { storageBackupCenter: '1' } }, [ $('h3', {}, tx('存储和备份中心', 'Storage and backup center', 'Speicher- und Sicherungszentrale')), $('div', { class: 'rgcc-grid' }, [stat(tx('本地存储', 'Local storage', 'Lokaler Speicher'), fmtBytes(storageSummary.localBytes)), stat(tx('站点存储', 'Site storage', 'Website-Speicher'), storageSummary.usage ? fmtBytes(storageSummary.usage) : '—'), stat(tx('可用配额', 'Available quota', 'Verfügbares Kontingent'), storageSummary.quota ? fmtBytes(storageSummary.quota) : '—'), stat(tx('内部统计备份', 'Internal statistics backups', 'Interne Statistik-Sicherungen'), `${statisticsBackups.length}/3`)]), $('div', { class: 'rgcc-list rgcc-section' }, [ $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('配置', 'Configuration', 'Konfiguration')), $('span', {}, `${store.state.rooms.length} ${tx('模型', 'models', 'Models')} · ${store.state.workspaces.length} ${tx('工作区', 'workspaces', 'Arbeitsbereiche')}`), $('span', {}, tx('LocalStorage + update vault', 'LocalStorage + update vault', 'LocalStorage + Update-Tresor'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('统计数据库', 'Statistics database', 'Statistikdatenbank')), $('span', {}, `${statisticsInventory.models} ${tx('模型', 'models', 'Models')} · ${statisticsInventory.events} ${tx('事件', 'events', 'Ereignisse')} · ${fmtBytes(statisticsInventory.estimatedBytes)}`), $('span', {}, 'IndexedDB')]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('秘密', 'Secrets', 'Geheimnisse')), $('span', {}, tx('官方统计令牌永远不包含在普通或完整备份中。', 'The official statistics token is never included in normal or complete backups.', 'Das offizielle Statistik-Token ist niemals in normalen oder vollständigen Sicherungen enthalten.')), $('span', { class: 'rgcc-good' }, tx('排除', 'Excluded', 'Ausgeschlossen'))]), ]), $('div', { class: 'rgcc-row' }, [btn(tx('创建配置回滚点', 'Create configuration restore point', 'Konfigurations-Rücksetzpunkt erstellen'), () => { Storage.backupCurrentConfig(); render(); }), btn(tx('创建统计回滚点', 'Create statistics restore point', 'Statistik-Rücksetzpunkt erstellen'), async () => { await analytics.createBackup('manual'); render(); })]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('旧数据清理', 'Legacy cleanup', 'Altdaten bereinigen')), $('p', {}, tx('旧版本数据不会自动删除。请仅在成功检查后有意识地删除早期的v6/v7数据。', 'Legacy data is never deleted automatically. Remove the old v6/v7 keys only after a successful review.', 'Alte Versionsdaten werden nie automatisch gelöscht. Entferne die früheren v6/v7-Schlüssel erst nach erfolgreicher Kontrolle.')), btn(tx('删除旧v6/v7副本', 'Remove old v6/v7 copies', 'Alte v6/v7-Kopien löschen'), () => { if (confirm(tx('仅删除旧v6/v7副本?当前数据和备份将保留。', 'Remove only old v6/v7 copies? Current data and backups remain.', 'Nur alte v6/v7-Kopien löschen? Aktuelle Daten und Sicherungen bleiben erhalten.'))) alert(`${dataManager.cleanupLegacy()} ${tx('项已删除', 'items removed', 'Einträge entfernt')}`); }, 'danger')]), ); } function renderAppearance() { const settings = store.state.settings; const moduleSettings = loadSuiteModuleSettings(); const patch = next => { store.patchSettings(next); applySuiteAppearance(store); render(); }; const patchLive = next => { store.patchSettings(next); applySuiteAppearance(store); }; const accents = ['#2563eb','#7c3aed','#db2777','#dc2626','#d97706','#059669','#0891b2','#475569']; const presets = [ ['aurora', tx('极光', 'Aurora', 'Aurora'), tx('蓝色与青绿色,RoomGrid 标准', 'Blue and teal, the RoomGrid signature', 'Blau und Türkis, die RoomGrid-Signatur'), '#4f8cff', '#27c7b8'], ['ember', tx('余烬', 'Ember', 'Ember'), tx('暖橙与玫红', 'Warm orange and rose', 'Warmes Orange und Rosé'), '#f97316', '#ec4899'], ['frost', tx('冰霜', 'Frost', 'Frost'), tx('清晰的青色与紫色', 'Crisp cyan and violet', 'Klares Cyan und Violett'), '#06b6d4', '#8b5cf6'], ['custom', tx('自定义', 'Custom', 'Eigene Farben'), tx('使用下方选择的颜色', 'Use the colors selected below', 'Die unten gewählten Farben verwenden'), settings.accentColor, settings.secondaryAccentColor], ]; const appearanceProfileKeys = ['theme', 'brandPreset', 'accentColor', 'secondaryAccentColor', 'paletteMode', 'customPalette', 'surfaceStyle', 'density', 'contentWidth', 'cardStyle', 'dashboardWidgetDensity', 'fontFamily', 'fontScale', 'controlScale', 'cornerRadius', 'cardGap', 'shadowStrength', 'surfaceOpacity', 'backgroundStyle', 'layoutPreset', 'layoutCardMinWidth', 'layoutSectionGap', 'layoutContentPadding', 'layoutHeaderHeight', 'layoutMaxColumns', 'layoutSidebarPosition', 'layoutStickyHeader', 'layoutCardAspect', 'highContrast', 'focusRingWidth', 'minimumTargetSize', 'underlineLinks', 'reduceTransparency', 'respectSystemMotion', 'largeTextBoost']; const appearanceSnapshot = () => Object.fromEntries(appearanceProfileKeys.map(key => [key, jsonClone(store.state.settings[key], store.state.settings[key])])); const saveAppearanceProfile = () => { const proposed = prompt(tx('为当前外观命名', 'Name the current appearance', 'Aktuelles Erscheinungsbild benennen'), tx('我的外观', 'My appearance', 'Mein Design')); const name = String(proposed || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60); if (!name) return; const profiles = Array.isArray(store.state.settings.appearanceProfiles) ? [...store.state.settings.appearanceProfiles] : []; const match = profiles.findIndex(profile => String(profile.name || '').toLocaleLowerCase() === name.toLocaleLowerCase()); const saved = { id: match >= 0 ? profiles[match].id : `appearance_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, name, createdAt: Date.now(), values: appearanceSnapshot() }; if (match >= 0) profiles.splice(match, 1, saved); else profiles.push(saved); patch({ appearanceProfiles: profiles.slice(-32) }); }; const removeAppearanceProfile = profile => { if (!confirm(`${tx('删除外观配置', 'Delete appearance profile', 'Designprofil löschen')} “${rawUiText(profile.name)}”?`)) return; patch({ appearanceProfiles: settings.appearanceProfiles.filter(item => item.id !== profile.id) }); }; const importAppearanceProfile = () => { const picker = $('input', { type: 'file', accept: 'application/json,.json', style: { display: 'none' } }); picker.addEventListener('change', async () => { try { const parsed = JSON.parse(await picker.files?.[0]?.text?.()); const source = parsed?.profile || parsed; const name = String(source?.name || '').trim().slice(0, 60); if (!name || !source?.values || typeof source.values !== 'object') throw new Error('invalid profile'); patch({ appearanceProfiles: [...settings.appearanceProfiles, { id: `appearance_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, name, createdAt: Date.now(), values: source.values }].slice(-32) }); } catch (_) { alert(tx('无法导入此外观配置。', 'This appearance profile could not be imported.', 'Dieses Designprofil konnte nicht importiert werden.')); } picker.remove(); }, { once: true }); document.body.appendChild(picker); picker.click(); }; const appearanceProfileRows = settings.appearanceProfiles.map(profile => $('div', { class: 'rgcc-item' }, [ $('strong', {}, rawUiText(profile.name)), $('span', { class: 'rgcc-muted' }, `${profile.values?.theme === 'light' ? tx('浅色', 'Light', 'Hell') : tx('深色', 'Dark', 'Dunkel')} · ${profile.values?.backgroundStyle || 'solid'} · ${profile.values?.cornerRadius ?? 10}px`), $('span', { class: 'rgcc-actions' }, [btn(tx('应用', 'Apply', 'Anwenden'), () => patch(profile.values || {}), 'primary'), btn(tx('导出', 'Export', 'Exportieren'), () => downloadBlob(new Blob([JSON.stringify({ schema: 1, profile }, null, 2)], { type: 'application/json' }), `roomgrid-design-${String(profile.name || 'profile').replace(/[^a-z0-9_-]+/giu, '-')}.json`)), btn(tx('删除', 'Delete', 'Löschen'), () => removeAppearanceProfile(profile), 'danger')]), ])); const pageLabels = { home: tx('主页', 'Home', 'Startseite'), listing: tx('列表和标签页', 'Listings and tags', 'Listen und Tags'), room: tx('模型页', 'Model pages', 'Modelseiten'), workspace: 'MultiCam', discover: tx('发现', 'Discover', 'Entdecken'), hub: tx('模型库和统计', 'Library and statistics', 'Bibliothek und Statistik') }; const profileChoices = [['', tx('使用全局外观', 'Use global appearance', 'Globales Design verwenden')], ...settings.appearanceProfiles.map(profile => [profile.id, profile.name])]; const pageProfileFields = Object.entries(pageLabels).map(([key, label]) => field(label, select(settings.pageAppearanceProfiles?.[key] || '', profileChoices, value => patch({ pageAppearanceProfiles: { ...settings.pageAppearanceProfiles, [key]: value } })))); const backgroundScopeLabels = { global: tx('所有页面(全局)', 'All pages (global)', 'Alle Seiten (global)'), dashboard: tx('主页', 'Home dashboard', 'Start'), discover: tx('发现', 'Discover', 'Entdecken'), tags: tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer'), stats: tx('统计中心', 'Statistics center', 'Statistikzentrum'), library: tx('模型库', 'Model library', 'Modellbibliothek'), user: tx('我的用户', 'My user', 'Mein Nutzer'), settings: tx('设置', 'Settings', 'Einstellungen'), multicam: 'MultiCam', single: 'SingleCam · Standard', home: tx('原始主页', 'Native homepage', 'Website-Startseite'), listing: tx('原始列表页', 'Native listing pages', 'Website-Listen'), room: tx('原始模型页', 'Native model pages', 'Website-Modelseiten'), }; const backgroundScopeOptions = Object.entries(backgroundScopeLabels); const backgroundConfig = backgroundEditorScope === 'global' ? sanitizeRoomGridBackground(settings.backgroundGlobal, 'theme') : sanitizeRoomGridBackground(settings.backgroundPages?.[backgroundEditorScope], 'inherit'); const backgroundPreviewConfig = backgroundConfig.mode === 'inherit' ? sanitizeRoomGridBackground(settings.backgroundGlobal, 'theme') : backgroundConfig; const patchBackground = (next, rerender = true) => { RoomGridBackgrounds.patch(store, backgroundEditorScope, next); if (rerender) render(); }; const backgroundPicker = $('input', { type: 'file', accept: 'image/jpeg,image/png,image/webp', hidden: true, dataset: { backgroundFilePicker: '1' } }); backgroundPicker.addEventListener('change', async () => { const file = backgroundPicker.files?.[0]; if (!file) return; try { await RoomGridBackgrounds.importImage(store, backgroundEditorScope, file); render(); } catch (_) { alert(tx('背景图片无法处理。请选择不超过 20 MB 的 PNG、JPEG 或 WebP。', 'The background image could not be processed. Choose a PNG, JPEG, or WebP up to 20 MB.', 'Das Hintergrundbild konnte nicht verarbeitet werden. Wähle PNG, JPEG oder WebP bis 20 MB.')); } }); const backgroundPreviewFill = $('div', { class: 'rgcc-background-fill' }); const backgroundPreviewImage = $('img', { alt: '', hidden: true }); const backgroundPreviewDim = $('div', { class: 'rgcc-background-dim' }); const backgroundPreview = $('div', { class: 'rgcc-background-preview', dataset: { backgroundPreview: '1', scope: backgroundEditorScope } }, [ backgroundPreviewFill, backgroundPreviewImage, backgroundPreviewDim, $('span', { class: 'rgcc-background-preview-label' }, `${backgroundScopeLabels[backgroundEditorScope]} · ${backgroundConfig.mode === 'inherit' ? tx('继承全局背景', 'inherits the global background', 'übernimmt den globalen Hintergrund') : backgroundPreviewConfig.mode}`), ]); const paintBackgroundPreview = config => { const zoom = Math.max(.5, Math.min(2.4, Number(config.zoom) / 100 || 1)); backgroundPreviewFill.style.background = config.mode === 'gradient' ? `linear-gradient(${config.gradientAngle}deg,${config.gradientStart},${config.gradientEnd})` : config.color; backgroundPreviewFill.style.transform = `scale(${zoom})`; backgroundPreviewFill.style.opacity = String(Number(config.opacity) / 100); backgroundPreviewImage.style.objectFit = config.fit === 'stretch' ? 'fill' : config.fit === 'original' ? 'none' : config.fit === 'tile' ? 'contain' : config.fit; backgroundPreviewImage.style.objectPosition = `${config.positionX}% ${config.positionY}%`; backgroundPreviewImage.style.transform = `scale(${zoom})`; backgroundPreviewImage.style.opacity = String(Number(config.opacity) / 100); backgroundPreviewImage.style.filter = config.blur ? `blur(${config.blur}px)` : ''; backgroundPreviewDim.style.background = `rgba(0,0,0,${Number(config.dim) / 100})`; if (config.mode === 'image') RoomGridBackgrounds.previewUrl(config).then(url => { if (!url || !backgroundPreview.isConnected) { if (url) URL.revokeObjectURL(url); return; } backgroundPreviewImage.hidden = false; backgroundPreviewImage.addEventListener('load', () => setTimeout(() => URL.revokeObjectURL(url), 0), { once: true }); backgroundPreviewImage.src = url; }).catch(() => {}); }; paintBackgroundPreview(backgroundPreviewConfig); const copyBackgroundToAll = () => { const selected = backgroundConfig.mode === 'inherit' ? sanitizeRoomGridBackground(settings.backgroundGlobal, 'theme') : sanitizeRoomGridBackground(backgroundConfig, 'theme'); store.patchSettings({ backgroundGlobal: selected, backgroundPages: Object.fromEntries(ROOMGRID_BACKGROUND_PAGE_KEYS.map(key => [key, defaultRoomGridBackground('inherit')])), }); backgroundEditorScope = 'global'; applySuiteAppearance(store); render(); }; const resetBackground = () => patchBackground(backgroundEditorScope === 'global' ? defaultRoomGridBackground('theme') : defaultRoomGridBackground('inherit')); const backgroundModeOptions = backgroundEditorScope === 'global' ? [['theme', tx('设计主题', 'Design theme', 'Design-Standard')], ['color', tx('纯色', 'Solid color', 'Einfarbig')], ['gradient', tx('渐变', 'Gradient', 'Farbverlauf')], ['image', tx('图片', 'Image', 'Bild')]] : [['inherit', tx('使用全局背景', 'Use global background', 'Globalen Hintergrund übernehmen')], ['color', tx('纯色', 'Solid color', 'Einfarbig')], ['gradient', tx('渐变', 'Gradient', 'Farbverlauf')], ['image', tx('图片', 'Image', 'Bild')]]; const backgroundStudio = () => $('section', { class: 'rgcc-card rgcc-section', dataset: { designBackgroundStudio: '1' } }, [ $('h3', {}, tx('页面和模型背景', 'Page and model backgrounds', 'Seiten- und Model-Hintergründe')), $('p', {}, tx('设置全局背景,或为每个菜单设置自己的背景。单个模型的背景可直接在 SingleCam 中选择。图片仅保存在此浏览器中。', 'Set a global background or a separate background for every menu. Individual model backgrounds are selected directly in SingleCam. Images stay in this browser.', 'Lege einen globalen Hintergrund oder für jedes Menü einen eigenen fest. Hintergründe einzelner Models wählst du direkt in SingleCam. Bilder bleiben in diesem Browser.')), $('div', { class: 'rgcc-background-studio' }, [ backgroundPreview, $('div', { class: 'rgcc-background-fields' }, [ $('div', { class: 'rgcc-row' }, [ field(tx('编辑范围', 'Edit area', 'Bereich bearbeiten'), select(backgroundEditorScope, backgroundScopeOptions, value => { backgroundEditorScope = value; render(); })), field(tx('背景类型', 'Background type', 'Hintergrundart'), select(backgroundConfig.mode, backgroundModeOptions, value => patchBackground({ mode: value }))), ]), $('div', { class: 'rgcc-grid' }, [ field(tx('背景颜色', 'Background color', 'Hintergrundfarbe'), colorInput(backgroundConfig.color, value => patchBackground({ color: value }, false))), field(tx('渐变起点', 'Gradient start', 'Verlaufsanfang'), colorInput(backgroundConfig.gradientStart, value => patchBackground({ gradientStart: value }, false))), field(tx('渐变终点', 'Gradient end', 'Verlaufsende'), colorInput(backgroundConfig.gradientEnd, value => patchBackground({ gradientEnd: value }, false))), field(tx('渐变角度', 'Gradient angle', 'Verlaufswinkel'), range(backgroundConfig.gradientAngle, value => patchBackground({ gradientAngle: value }, false), { min: 0, max: 360, step: 5 }, value => `${value}°`)), field(tx('图像适配', 'Image fit', 'Bildanpassung'), select(backgroundConfig.fit, [['cover', tx('填满并裁剪', 'Fill and crop', 'Ausfüllen')], ['contain', tx('完整显示', 'Show completely', 'Einpassen')], ['stretch', tx('拉伸', 'Stretch', 'Strecken')], ['original', tx('原始大小', 'Original size', 'Originalgröße')], ['tile', tx('平铺', 'Tile', 'Kacheln')]], value => patchBackground({ fit: value }))), field(tx('缩放', 'Zoom', 'Zoom'), range(backgroundConfig.zoom, value => patchBackground({ zoom: value }, false), { min: 50, max: 240, step: 5 }, value => `${value} %`)), field(tx('水平位置', 'Horizontal position', 'Horizontale Position'), range(backgroundConfig.positionX, value => patchBackground({ positionX: value }, false), { min: 0, max: 100, step: 1 }, value => `${value} %`)), field(tx('垂直位置', 'Vertical position', 'Vertikale Position'), range(backgroundConfig.positionY, value => patchBackground({ positionY: value }, false), { min: 0, max: 100, step: 1 }, value => `${value} %`)), field(tx('图片不透明度', 'Image opacity', 'Bilddeckkraft'), range(backgroundConfig.opacity, value => patchBackground({ opacity: value }, false), { min: 10, max: 100, step: 1 }, value => `${value} %`)), field(tx('背景变暗', 'Darken background', 'Hintergrund abdunkeln'), range(backgroundConfig.dim, value => patchBackground({ dim: value }, false), { min: 0, max: 90, step: 1 }, value => `${value} %`)), field(tx('背景模糊', 'Background blur', 'Hintergrund weichzeichnen'), range(backgroundConfig.blur, value => patchBackground({ blur: value }, false), { min: 0, max: 32, step: 1 }, value => `${value} px`)), field(tx('面板不透明度', 'Panel opacity', 'Panel-Deckkraft'), range(backgroundConfig.panelOpacity, value => patchBackground({ panelOpacity: value }, false), { min: 55, max: 100, step: 1 }, value => `${value} %`)), ]), $('div', { class: 'rgcc-row' }, [ btn(tx('从电脑选择图片', 'Choose image from computer', 'Bild vom PC auswählen'), () => backgroundPicker.click(), 'primary'), btn(tx('用于所有页面', 'Use on every page', 'Für alle Seiten übernehmen'), copyBackgroundToAll), btn(tx('重置此背景', 'Reset this background', 'Diesen Hintergrund zurücksetzen'), resetBackground, 'danger'), check(tx('滚动时固定', 'Keep fixed while scrolling', 'Beim Scrollen fixieren'), backgroundConfig.fixed, value => patchBackground({ fixed: value })), backgroundPicker, ]), $('p', { class: 'rgcc-note' }, tx('优先级:模型背景 → 当前菜单背景 → 全局背景。高分辨率图片会自动压缩到最高 4K,以保持界面流畅。', 'Priority: model background → current menu background → global background. High-resolution images are automatically compressed to at most 4K to keep the interface smooth.', 'Priorität: Model-Hintergrund → Hintergrund des aktuellen Menüs → globaler Hintergrund. Hochauflösende Bilder werden für eine flüssige Oberfläche automatisch auf maximal 4K optimiert.')), ]), ]), ]); const brand = roomGridBrandIdentity(settings); const logoPicker = mode => { const picker = $('input', { type: 'file', accept: 'image/png,image/jpeg,image/webp,image/svg+xml', style: { display: 'none' } }); picker.addEventListener('change', () => { const file = picker.files?.[0]; if (!file || file.size > 1150000) { alert(tx('请选择小于 1.1 MB 的 PNG、JPEG、WebP 或安全 SVG。', 'Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.', 'Wähle PNG, JPEG, WebP oder ein sicheres SVG unter 1,1 MB.')); picker.remove(); return; } const reader = new FileReader(); reader.onload = () => { const key = mode === 'light' ? 'brandLogoLight' : 'brandLogoDark'; // Selecting a logo is an explicit request to use it. Previously the // image was stored while the display mode silently stayed on the // default monogram, which made a successful upload look broken. store.patchSettings({ [key]: String(reader.result || ''), brandDisplayMode: 'logo' }); if (!store.state.settings[key]) alert(tx('此徽标格式不安全或不受支持。', 'This logo format is unsafe or unsupported.', 'Dieses Logoformat ist unsicher oder wird nicht unterstützt.')); applySuiteAppearance(store); render(); picker.remove(); }; reader.onerror = () => { picker.remove(); alert(tx('无法读取徽标文件。', 'The logo file could not be read.', 'Die Logodatei konnte nicht gelesen werden.')); }; reader.readAsDataURL(file); }, { once: true }); document.body.appendChild(picker); picker.click(); }; const logoPreview = () => $('div', { class: 'rgcc-brand-preview', dataset: { designBrandPreview: '1' } }, [ $('div', { class: 'rgcc-brand-mark' }, roomGridBrandMark(settings)), $('div', {}, [$('strong', { dataset: { roomgridBrandName: '1' } }, brand.name), $('span', { dataset: { roomgridBrandTagline: '1', roomgridBrandTaglineFallback: tx('多摄像头工具', 'MultiCam tools', 'MultiCam-Werkzeuge') } }, brand.tagline || tx('多摄像头工具', 'MultiCam tools', 'MultiCam-Werkzeuge')), brand.showVersion ? $('small', {}, `v${META.version}`) : null]), ]); const activePaletteMode = settings.theme === 'light' ? 'light' : 'dark'; const paletteLabels = { page: tx('页面', 'Page', 'Seite'), panel: tx('面板', 'Panel', 'Panel'), surface: tx('卡片表面', 'Card surface', 'Kartenfläche'), input: tx('输入区域', 'Input', 'Eingabefläche'), border: tx('边框', 'Border', 'Rahmen'), text: tx('正文', 'Text', 'Text'), muted: tx('次要文本', 'Muted text', 'Sekundärtext'), success: tx('成功', 'Success', 'Erfolg'), warning: tx('警告', 'Warning', 'Warnung'), danger: tx('危险', 'Danger', 'Gefahr'), info: tx('信息与焦点', 'Info and focus', 'Info und Fokus') }; const paletteFields = Object.entries(paletteLabels).map(([key, label]) => field(label, colorInput(settings.customPalette?.[activePaletteMode]?.[key] || DEFAULT_CUSTOM_PALETTE[activePaletteMode][key], value => patchLive({ paletteMode: 'custom', customPalette: { ...settings.customPalette, [activePaletteMode]: { ...(settings.customPalette?.[activePaletteMode] || {}), [key]: value } } })))); const applyLayoutPreset = id => { const values = id === 'compact' ? { layoutPreset: id, layoutCardMinWidth: 180, layoutSectionGap: 8, layoutContentPadding: 10, layoutHeaderHeight: 48, layoutMaxColumns: 0, layoutCardAspect: '16/9' } : id === 'cinema' ? { layoutPreset: id, layoutCardMinWidth: 340, layoutSectionGap: 22, layoutContentPadding: 22, layoutHeaderHeight: 64, layoutMaxColumns: 4, layoutCardAspect: '16/9' } : { layoutPreset: id, layoutCardMinWidth: 260, layoutSectionGap: 16, layoutContentPadding: 18, layoutHeaderHeight: 56, layoutMaxColumns: 0, layoutCardAspect: 'auto' }; patch(values); }; const previewDevice = settings.designPreviewDevice || 'desktop'; const previewSurface = settings.designPreviewSurface || 'home'; const previewCards = previewSurface === 'workspace' ? 4 : previewSurface === 'hub' ? 3 : 6; const livePreview = () => $('div', { class: `rgcc-design-preview device-${previewDevice} surface-${previewSurface}`, dataset: { designLivePreview: '1', device: previewDevice, surface: previewSurface } }, [ $('div', { class: 'rgcc-preview-browser' }, [ $('div', { class: 'rgcc-preview-header' }, [roomGridBrandMark(settings, { size: 20 }), $('strong', { dataset: { roomgridBrandName: '1' } }, brand.name), $('span', {}, previewSurface === 'workspace' ? 'MultiCam' : previewSurface === 'hub' ? tx('资料中心', 'Data hub', 'Datenzentrum') : tx('主页', 'Home', 'Startseite')), $('button', { type: 'button' }, '•••')]), $('div', { class: 'rgcc-preview-layout' }, [ previewSurface !== 'home' ? $('aside', {}, [$('i'), $('i'), $('i'), $('i')]) : null, $('main', {}, [ $('div', { class: 'rgcc-preview-toolbar' }, [$('span'), $('span'), $('b')]), $('div', { class: 'rgcc-preview-cards' }, Array.from({ length: previewCards }, (_, index) => $('article', {}, [ $('div', { class: 'rgcc-preview-media' }, index === 0 ? 'LIVE' : ''), $('strong', {}, `model_${index + 1}`), $('span', {}, '● 1.2k · #tag'), ]))), ]), ]), ]), ]); const palette = resolvedAppearancePalette(settings, settings.theme !== 'light'); const contrastChecks = () => [ [tx('正文 / 页面', 'Text / page', 'Text / Seite'), palette.text, palette.page, 4.5], [tx('正文 / 面板', 'Text / panel', 'Text / Panel'), palette.text, palette.panel, 4.5], [tx('次要文本 / 面板', 'Muted text / panel', 'Sekundärtext / Panel'), palette.muted, palette.panel, 4.5], [tx('强调色文字 / 强调色', 'Accent text / accent', 'Akzenttext / Akzent'), readableAccent(settings.accentColor), settings.accentColor, 4.5], ].map(([label, foreground, background, target]) => { const ratio = contrastRatio(foreground, background); const pass = ratio >= target; return $('div', { class: `rgcc-contrast-row ${pass ? 'pass' : 'fail'}` }, [$('span', {}, label), $('b', {}, `${ratio.toFixed(2)}:1`), $('em', {}, pass ? 'AA ✓' : 'AA ×')]); }); const deviceLabels = { desktop: tx('桌面', 'Desktop', 'Desktop'), tablet: tx('平板', 'Tablet', 'Tablet'), mobile: tx('手机', 'Mobile', 'Mobil') }; const responsiveProfileBlocks = () => Object.entries(deviceLabels).map(([device, label]) => $('details', { class: 'rgcc-responsive-profile', open: device === previewDevice }, [ $('summary', {}, label), $('div', { class: 'rgcc-grid' }, Object.entries(pageLabels).map(([key, pageLabel]) => field(pageLabel, select(settings.responsiveAppearanceProfiles?.[device]?.[key] || '', profileChoices, value => patch({ responsiveAppearanceProfiles: { ...settings.responsiveAppearanceProfiles, [device]: { ...(settings.responsiveAppearanceProfiles?.[device] || {}), [key]: value } } }))))), ])); body.replaceChildren( deferredSettingsCard(tx('品牌与徽标工作室', 'Brand and logo studio', 'Brand- und Logo-Studio'), () => $('section', { class: 'rgcc-card', dataset: { designBrandStudio: '1' } }, [ $('h3', {}, tx('品牌与徽标工作室', 'Brand and logo studio', 'Brand- und Logo-Studio')), $('p', {}, tx('自定义 Camclient 名称、缩写、口号和本地徽标。徽标只保存在此浏览器中。', 'Customize the Camclient name, mark, tagline and local logo. Logos stay in this browser.', 'Passe Camclient-Name, Kürzel, Unterzeile und lokales Logo an. Logos bleiben in diesem Browser.')), $('div', { class: 'rgcc-brand-studio' }, [logoPreview(), $('div', { class: 'rgcc-brand-fields' }, [ $('div', { class: 'rgcc-row' }, [field(tx('名称', 'Name', 'Name'), input('text', settings.brandName, value => patch({ brandName: value }), { maxlength: 40 })), field(tx('缩写', 'Monogram', 'Kürzel'), input('text', settings.brandMonogram, value => patch({ brandMonogram: value }), { maxlength: 5 })), field(tx('口号', 'Tagline', 'Unterzeile'), input('text', settings.brandTagline, value => patch({ brandTagline: value }), { maxlength: 80 }))]), $('div', { class: 'rgcc-row' }, [field(tx('标志显示', 'Brand display', 'Markenanzeige'), select(settings.brandDisplayMode, [['monogram', tx('缩写', 'Monogram', 'Kürzel')], ['text', tx('完整名称', 'Full name', 'Vollständiger Name')], ['logo', tx('上传的徽标', 'Uploaded logo', 'Hochgeladenes Logo')]], value => patch({ brandDisplayMode: value }))), field(tx('徽标形状', 'Logo shape', 'Logoform'), select(settings.brandLogoShape, [['rounded', tx('圆角', 'Rounded', 'Abgerundet')], ['circle', tx('圆形', 'Circle', 'Kreis')], ['square', tx('方形', 'Square', 'Eckig')], ['pill', tx('胶囊', 'Pill', 'Pille')]], value => patch({ brandLogoShape: value }))), field(tx('页面标题', 'Page title', 'Seitentitel'), select(settings.brandPageTitleMode, [['native', tx('网站原始', 'Website original', 'Website-Original')], ['brand', tx('名称加原标题', 'Brand plus original', 'Marke plus Original')], ['custom', tx('自定义标题', 'Custom title', 'Eigener Titel')]], value => patch({ brandPageTitleMode: value })))]), settings.brandPageTitleMode === 'custom' ? field(tx('自定义页面标题', 'Custom page title', 'Eigener Seitentitel'), input('text', settings.brandCustomPageTitle, value => patch({ brandCustomPageTitle: value }), { maxlength: 80 })) : null, $('div', { class: 'rgcc-row' }, [ field(tx('徽标宽度', 'Logo width', 'Logobreite'), range(settings.brandLogoWidth, value => patchLive({ brandLogoWidth: value }), { min: 20, max: 240, step: 1 }, value => `${value} px`)), field(tx('徽标高度', 'Logo height', 'Logohöhe'), range(settings.brandLogoHeight, value => patchLive({ brandLogoHeight: value }), { min: 20, max: 96, step: 1 }, value => `${value} px`)), field(tx('图像适配', 'Image fit', 'Bildanpassung'), select(settings.brandLogoFit, [['contain', tx('完整显示', 'Fit completely', 'Vollständig einpassen')], ['cover', tx('填满并裁剪', 'Fill and crop', 'Ausfüllen und beschneiden')], ['fill', tx('拉伸', 'Stretch', 'Strecken')], ['scale-down', tx('不放大', 'Do not enlarge', 'Nicht vergrößern')]], value => patch({ brandLogoFit: value }))), field(tx('徽标背景', 'Logo background', 'Logohintergrund'), select(settings.brandLogoBackground, [['transparent', tx('透明', 'Transparent', 'Transparent')], ['accent', tx('强调色', 'Accent gradient', 'Akzentverlauf')], ['panel', tx('面板', 'Panel', 'Panel')], ['white', tx('白色', 'White', 'Weiß')], ['black', tx('黑色', 'Black', 'Schwarz')]], value => patch({ brandLogoBackground: value }))), ]), $('div', { class: 'rgcc-row' }, [ field(tx('水平位置', 'Horizontal position', 'Horizontale Position'), range(settings.brandLogoPositionX, value => patchLive({ brandLogoPositionX: value }), { min: 0, max: 100, step: 1 }, value => `${value} %`)), field(tx('垂直位置', 'Vertical position', 'Vertikale Position'), range(settings.brandLogoPositionY, value => patchLive({ brandLogoPositionY: value }), { min: 0, max: 100, step: 1 }, value => `${value} %`)), field(tx('内边距', 'Inner spacing', 'Innenabstand'), range(settings.brandLogoPadding, value => patchLive({ brandLogoPadding: value }), { min: 0, max: 20, step: 1 }, value => `${value} px`)), ]), $('p', { class: 'rgcc-brand-note' }, tx('透明背景请使用 PNG、WebP 或安全 SVG,并将“徽标背景”设为透明。JPEG 不支持透明度。“拉伸”会独立使用所选宽度和高度。', 'For transparency, use PNG, WebP or safe SVG and choose Transparent as the logo background. JPEG has no transparency. Stretch uses the chosen width and height independently.', 'Für einen transparenten Hintergrund PNG, WebP oder ein sicheres SVG verwenden und „Logohintergrund: Transparent“ wählen. JPEG unterstützt keine Transparenz. „Strecken“ verwendet Breite und Höhe unabhängig voneinander.')), $('div', { class: 'rgcc-row' }, [btn(tx('上传浅色模式徽标', 'Upload light-mode logo', 'Logo für helles Design'), () => logoPicker('light')), btn(tx('上传深色模式徽标', 'Upload dark-mode logo', 'Logo für dunkles Design'), () => logoPicker('dark')), btn(tx('移除徽标', 'Remove logos', 'Logos entfernen'), () => patch({ brandLogoLight: '', brandLogoDark: '', brandDisplayMode: 'monogram', brandReplaceNativeLogo: false, brandFaviconEnabled: false }), 'danger')]), $('div', { class: 'rgcc-row' }, [check(tx('显示版本', 'Show version', 'Version anzeigen'), settings.brandShowVersion, value => patch({ brandShowVersion: value })), check(tx('在 RoomGrid 页面使用徽标作为标签图标', 'Use logo as tab icon on RoomGrid pages', 'Logo als Tab-Symbol auf RoomGrid-Seiten verwenden'), settings.brandFaviconEnabled, value => patch({ brandFaviconEnabled: value })), check(tx('在原始页眉中替换网站徽标(仅本地)', 'Replace website logo in the original header (local only)', 'Website-Logo in der Original-Kopfzeile ersetzen (nur lokal)'), settings.brandReplaceNativeLogo, value => patch({ brandReplaceNativeLogo: value }))]), ])]), ])), deferredSettingsCard(tx('页面与模型背景', 'Page and model backgrounds', 'Seiten- und Model-Hintergründe'), backgroundStudio), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('完整颜色系统', 'Complete color system', 'Vollständiges Farbsystem'), () => $('section', { class: 'rgcc-card', dataset: { designPaletteEditor: '1' } }, [$('h3', {}, tx('完整颜色系统', 'Complete color system', 'Vollständiges Farbsystem')), $('p', {}, tx('除了强调色,还可以控制页面、面板、卡片、输入、边框、文字和状态颜色。', 'Control page, panel, card, input, border, text and status colors in addition to accents.', 'Steuere neben Akzenten auch Seiten-, Panel-, Karten-, Eingabe-, Rahmen-, Text- und Statusfarben.')), $('div', { class: 'rgcc-row' }, [btn(tx('自动派生', 'Derived automatically', 'Automatisch abgeleitet'), () => patch({ paletteMode: 'derived' }), settings.paletteMode === 'derived' ? 'primary' : ''), btn(tx('完整自定义', 'Fully custom', 'Vollständig benutzerdefiniert'), () => patch({ paletteMode: 'custom' }), settings.paletteMode === 'custom' ? 'primary' : ''), $('span', { class: 'rgcc-chip' }, activePaletteMode === 'dark' ? tx('正在编辑深色调色板', 'Editing dark palette', 'Dunkle Palette bearbeiten') : tx('正在编辑浅色调色板', 'Editing light palette', 'Helle Palette bearbeiten'))]), $('div', { class: 'rgcc-palette-grid' }, paletteFields), $('div', { class: 'rgcc-row' }, [btn(tx('重置当前调色板', 'Reset current palette', 'Aktuelle Palette zurücksetzen'), () => patch({ paletteMode: 'custom', customPalette: { ...settings.customPalette, [activePaletteMode]: jsonClone(DEFAULT_CUSTOM_PALETTE[activePaletteMode], {}) } }), 'danger')])])), deferredSettingsCard(tx('布局编辑器', 'Layout editor', 'Layout-Editor'), () => $('section', { class: 'rgcc-card', dataset: { designLayoutEditor: '1' } }, [$('h3', {}, tx('布局编辑器', 'Layout editor', 'Layout-Editor')), $('p', {}, tx('在所有 RoomGrid 页面上统一设置内容间距、卡片宽度、页眉和侧栏。', 'Set content spacing, card width, header and sidebar consistently across RoomGrid pages.', 'Stelle Inhaltsabstand, Kartenbreite, Kopfzeile und Seitenleiste auf allen RoomGrid-Seiten einheitlich ein.')), $('div', { class: 'rgcc-row' }, [['balanced', tx('平衡', 'Balanced', 'Ausgewogen')], ['compact', tx('紧凑', 'Compact', 'Kompakt')], ['cinema', tx('影院', 'Cinema', 'Kino')]].map(([id, label]) => btn(label, () => applyLayoutPreset(id), settings.layoutPreset === id ? 'primary' : ''))), field(tx('最小卡片宽度', 'Minimum card width', 'Minimale Kartenbreite'), range(settings.layoutCardMinWidth, value => patchLive({ layoutCardMinWidth: value, layoutPreset: 'custom' }), { min: 160, max: 480, step: 10 }, value => `${value} px`)), field(tx('区块间距', 'Section spacing', 'Abschnittsabstand'), range(settings.layoutSectionGap, value => patchLive({ layoutSectionGap: value, layoutPreset: 'custom' }), { min: 4, max: 40, step: 1 }, value => `${value} px`)), field(tx('内容内边距', 'Content padding', 'Inhaltsabstand'), range(settings.layoutContentPadding, value => patchLive({ layoutContentPadding: value, layoutPreset: 'custom' }), { min: 0, max: 48, step: 1 }, value => `${value} px`)), field(tx('页眉高度', 'Header height', 'Kopfzeilenhöhe'), range(settings.layoutHeaderHeight, value => patchLive({ layoutHeaderHeight: value, layoutPreset: 'custom' }), { min: 40, max: 96, step: 2 }, value => `${value} px`)), $('div', { class: 'rgcc-row' }, [field(tx('最多列数', 'Maximum columns', 'Maximale Spalten'), select(String(settings.layoutMaxColumns), [['0', tx('自动', 'Automatic', 'Automatisch')], ...Array.from({ length: 11 }, (_, index) => [String(index + 2), String(index + 2)])], value => patch({ layoutMaxColumns: Number(value), layoutPreset: 'custom' }))), field(tx('卡片比例', 'Card aspect', 'Kartenformat'), select(settings.layoutCardAspect, [['auto', tx('自动', 'Automatic', 'Automatisch')], ['16/9', '16:9'], ['4/3', '4:3'], ['1/1', '1:1'], ['3/4', '3:4']], value => patch({ layoutCardAspect: value, layoutPreset: 'custom' }))), field(tx('侧栏位置', 'Sidebar position', 'Seitenleistenposition'), select(settings.layoutSidebarPosition, [['left', tx('左侧', 'Left', 'Links')], ['right', tx('右侧', 'Right', 'Rechts')]], value => patch({ layoutSidebarPosition: value, layoutPreset: 'custom' })))]), check(tx('滚动时固定页眉', 'Keep header visible while scrolling', 'Kopfzeile beim Scrollen sichtbar halten'), settings.layoutStickyHeader, value => patch({ layoutStickyHeader: value, layoutPreset: 'custom' }))])), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('实时设备预览', 'Live device preview', 'Live-Gerätevorschau'), () => $('section', { class: 'rgcc-card', dataset: { designPreviewStudio: '1' } }, [$('h3', {}, tx('实时设备预览', 'Live device preview', 'Live-Gerätevorschau')), $('div', { class: 'rgcc-row' }, Object.entries(deviceLabels).map(([id, label]) => btn(label, () => patch({ designPreviewDevice: id }), previewDevice === id ? 'primary' : ''))), $('div', { class: 'rgcc-row' }, [['home', tx('主页', 'Home', 'Startseite')], ['workspace', 'MultiCam'], ['hub', tx('资料中心', 'Data hub', 'Datenzentrum')]].map(([id, label]) => btn(label, () => patch({ designPreviewSurface: id }), previewSurface === id ? 'primary' : ''))), livePreview()])), deferredSettingsCard(tx('对比度与无障碍', 'Contrast and accessibility', 'Kontrast und Barrierefreiheit'), () => $('section', { class: 'rgcc-card', dataset: { designAccessibility: '1' } }, [$('h3', {}, tx('对比度与无障碍', 'Contrast and accessibility', 'Kontrast und Barrierefreiheit')), $('div', { class: 'rgcc-contrast-audit', dataset: { designContrastAudit: '1' } }, contrastChecks()), $('div', { class: 'rgcc-row' }, [check(tx('高对比度模式', 'High-contrast mode', 'Hoher Kontrast'), settings.highContrast, value => patch({ highContrast: value })), check(tx('链接加下划线', 'Underline links', 'Links unterstreichen'), settings.underlineLinks, value => patch({ underlineLinks: value })), check(tx('减少透明效果', 'Reduce transparency', 'Transparenz reduzieren'), settings.reduceTransparency, value => patch({ reduceTransparency: value })), check(tx('放大基础文字', 'Boost base text size', 'Grundtext vergrößern'), settings.largeTextBoost, value => patch({ largeTextBoost: value }))]), field(tx('焦点环宽度', 'Focus ring width', 'Fokusringbreite'), range(settings.focusRingWidth, value => patchLive({ focusRingWidth: value }), { min: 1, max: 6, step: 1 }, value => `${value} px`)), field(tx('最小操作区域', 'Minimum target size', 'Minimale Bedienfläche'), range(settings.minimumTargetSize, value => patchLive({ minimumTargetSize: value }), { min: 28, max: 52, step: 2 }, value => `${value} px`)), check(tx('系统einstellung für reduzierte Bewegung beachten', 'Respect system reduced-motion setting', 'Systemeinstellung für reduzierte Bewegung beachten'), settings.respectSystemMotion, value => patch({ respectSystemMotion: value })), btn(tx('无障碍安全值', 'Apply accessible defaults', 'Barrierearme Werte anwenden'), () => patch({ highContrast: true, reduceTransparency: true, surfaceOpacity: 100, focusRingWidth: 3, minimumTargetSize: 40, respectSystemMotion: true }), 'primary')])), ]), deferredSettingsCard(tx('按页面和屏幕尺寸使用配置', 'Profiles by page and screen size', 'Profile je Seite und Bildschirmgröße'), () => $('section', { class: 'rgcc-card rgcc-section', dataset: { designResponsiveProfiles: '1' } }, [$('h3', {}, tx('按页面和屏幕尺寸使用配置', 'Profiles by page and screen size', 'Profile je Seite und Bildschirmgröße')), $('p', {}, tx('先保存一个或多个外观,然后为桌面、平板和手机的每种页面类型分配它们。屏幕尺寸变化时会自动切换。', 'Save one or more appearances, then assign them per page type for desktop, tablet and mobile. Profiles switch automatically when the screen size changes.', 'Speichere ein oder mehrere Designs und ordne sie je Seitentyp für Desktop, Tablet und Mobil zu. Beim Ändern der Bildschirmgröße wird automatisch gewechselt.')), check(tx('启用响应式设计配置', 'Enable responsive design profiles', 'Responsive Designprofile aktivieren'), settings.responsiveAppearanceProfilesEnabled, value => patch({ responsiveAppearanceProfilesEnabled: value })), $('div', { class: 'rgcc-row' }, [field(tx('手机最大宽度', 'Mobile maximum width', 'Mobil bis'), input('number', settings.appearanceBreakpoints?.mobileMax || 767, value => patch({ appearanceBreakpoints: { ...settings.appearanceBreakpoints, mobileMax: Number(value) } }), { min: 480, max: 1000 })), field(tx('平板最大宽度', 'Tablet maximum width', 'Tablet bis'), input('number', settings.appearanceBreakpoints?.tabletMax || 1199, value => patch({ appearanceBreakpoints: { ...settings.appearanceBreakpoints, tabletMax: Number(value) } }), { min: 580, max: 1800 }))]), $('div', { class: 'rgcc-responsive-profiles' }, responsiveProfileBlocks())])), deferredSettingsCard(tx('RoomGrid 设计系统', 'RoomGrid design system', 'RoomGrid-Designsystem'), () => $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('RoomGrid 设计系统', 'RoomGrid design system', 'RoomGrid-Designsystem')), $('p', {}, tx('选择品牌方向、内容宽度和卡片信息密度。所有页面立即同步。', 'Choose a brand direction, content width and card information density. Every page follows immediately.', 'Wähle Markenrichtung, Inhaltsbreite und Karten-Informationsdichte. Alle Seiten folgen sofort.')), $('div', { class: 'rgcc-row' }, presets.map(([id, label, note, primary, secondary]) => $('button', { class: `rgcc-btn rgcc-preset${settings.brandPreset === id ? ' primary' : ''}`, type: 'button', onclick: () => patch(id === 'custom' ? { brandPreset: id } : { brandPreset: id, accentColor: primary, secondaryAccentColor: secondary }) }, [$('strong', {}, label), $('small', {}, note)]))), $('div', { class: 'rgcc-row' }, [ field(tx('主强调色', 'Primary accent', 'Primärakzent'), colorInput(settings.accentColor, value => patchLive({ accentColor: value, brandPreset: 'custom' }))), field(tx('次强调色', 'Secondary accent', 'Sekundärakzent'), colorInput(settings.secondaryAccentColor, value => patchLive({ secondaryAccentColor: value, brandPreset: 'custom' }))), field(tx('内容宽度', 'Content width', 'Inhaltsbreite'), select(settings.contentWidth, [['centered', tx('居中', 'Centered', 'Zentriert')], ['wide', tx('宽', 'Wide', 'Breit')], ['full', tx('全宽', 'Full width', 'Volle Breite')]], value => patch({ contentWidth: value }))), field(tx('卡片风格', 'Card style', 'Kartenstil'), select(settings.cardStyle, [['visual', tx('视觉优先', 'Visual', 'Visuell')], ['balanced', tx('平衡', 'Balanced', 'Ausgewogen')], ['data', tx('数据优先', 'Data-rich', 'Datenreich')], ['micro', tx('微型', 'Micro', 'Mikro')]], value => patch({ cardStyle: value }))), field(tx('组件密度', 'Widget density', 'Widget-Dichte'), select(settings.dashboardWidgetDensity, [['compact', tx('紧凑', 'Compact', 'Kompakt')], ['comfortable', tx('舒适', 'Comfortable', 'Komfortabel')], ['spacious', tx('宽松', 'Spacious', 'Großzügig')]], value => patch({ dashboardWidgetDensity: value }))), ]), ])), $('div', { class: 'rgcc-two' }, [ deferredSettingsCard(tx('主题和表面', 'Theme and surfaces', 'Thema und Oberflächen'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('主题和表面', 'Theme and surfaces', 'Thema und Oberflächen')), $('div', { class: 'rgcc-row' }, [btn(tx('浅色', 'Light', 'Hell'), () => patch({ theme: 'light' }), settings.theme === 'light' ? 'primary' : ''), btn(tx('深色', 'Dark', 'Dunkel'), () => patch({ theme: 'dark' }), settings.theme === 'dark' ? 'primary' : '')]), field(tx('表面风格', 'Surface style', 'Oberflächenstil'), select(settings.surfaceStyle, [['soft', tx('柔和', 'Soft', 'Weich')], ['flat', tx('平面', 'Flat', 'Flach')], ['glass', tx('玻璃', 'Glass', 'Glas')]], value => patch({ surfaceStyle: value }))), field(tx('页面背景', 'Page background', 'Seitenhintergrund'), select(settings.backgroundStyle, [['solid', tx('纯色', 'Solid', 'Einfarbig')], ['gradient', tx('渐变', 'Gradient', 'Farbverlauf')], ['ambient', tx('氛围光', 'Ambient glow', 'Umgebungsleuchten')]], value => patch({ backgroundStyle: value }))), field(tx('表面不透明度', 'Surface opacity', 'Flächendeckkraft'), range(settings.surfaceOpacity, value => patchLive({ surfaceOpacity: value }), { min: 65, max: 100, step: 1 }, value => `${value} %`)), $('div', { class: 'rgcc-row' }, accents.map(color => $('button', { class: `rgcc-color${settings.accentColor === color ? ' active' : ''}`, type: 'button', title: color, style: { background: color }, onclick: () => patch({ accentColor: color, brandPreset: 'custom' }) }))), field(tx('自定义强调色', 'Custom accent color', 'Eigene Akzentfarbe'), colorInput(settings.accentColor, value => patchLive({ accentColor: value, brandPreset: 'custom' })))])), deferredSettingsCard(tx('尺寸和动画', 'Sizing and motion', 'Größen und Animationen'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('尺寸和动画', 'Sizing and motion', 'Größen und Animationen')), field(tx('字体样式', 'Font style', 'Schriftstil'), select(settings.fontFamily, [['system', tx('系统', 'System', 'System')], ['rounded', tx('圆润', 'Rounded', 'Abgerundet')], ['classic', tx('经典衬线', 'Classic serif', 'Klassische Serife')], ['mono', tx('等宽', 'Monospace', 'Monospace')]], value => patch({ fontFamily: value }))), field(tx('密度', 'Density', 'Dichte'), select(settings.density, [['compact', tx('紧凑', 'Compact', 'Kompakt')], ['comfortable', tx('舒适', 'Comfortable', 'Komfortabel')], ['spacious', tx('宽松', 'Spacious', 'Großzügig')]], value => patch({ density: value }))), field(tx('字体缩放', 'Font scale', 'Schriftgröße'), range(settings.fontScale, value => patchLive({ fontScale: value }), { min: .8, max: 1.35, step: .05 }, value => `${Math.round(value * 100)} %`)), field(tx('控件大小', 'Control size', 'Bedienflächengröße'), range(settings.controlScale, value => patchLive({ controlScale: value }), { min: .8, max: 1.4, step: .05 }, value => `${Math.round(value * 100)} %`)), field(tx('圆角', 'Corner radius', 'Eckenrundung'), range(settings.cornerRadius, value => patchLive({ cornerRadius: value }), { min: 0, max: 24, step: 1 }, value => `${value} px`)), field(tx('卡片间距', 'Card spacing', 'Kartenabstand'), range(settings.cardGap, value => patchLive({ cardGap: value }), { min: 4, max: 28, step: 1 }, value => `${value} px`)), field(tx('阴影强度', 'Shadow strength', 'Schattenstärke'), range(settings.shadowStrength, value => patchLive({ shadowStrength: value }), { min: 0, max: 100, step: 5 }, value => `${value} %`)), field(tx('侧栏宽度', 'Sidebar width', 'Seitenleistenbreite'), range(settings.sidebarWidth, value => patchLive({ sidebarWidth: value }), { min: 180, max: 460, step: 10 }, value => `${value} px`)), check(tx('启用动画', 'Enable animations', 'Animationen aktivieren'), settings.animations, value => patch({ animations: value }))])), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('外观配置', 'Appearance profiles', 'Designprofile'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('外观配置', 'Appearance profiles', 'Designprofile')), $('p', {}, tx('将完整外观保存为可重复使用的配置。使用脚本更新后,这些配置仍保留。', 'Save the complete appearance as a reusable profile. Profiles remain available after script updates.', 'Speichere das komplette Erscheinungsbild als wiederverwendbares Profil. Die Profile bleiben auch nach Script-Updates erhalten.')), $('div', { class: 'rgcc-row' }, [btn(tx('保存当前外观', 'Save current appearance', 'Aktuelles Design speichern'), saveAppearanceProfile, 'primary'), btn(tx('导入配置', 'Import profile', 'Profil importieren'), importAppearanceProfile)]), $('div', { class: 'rgcc-list', style: { marginTop: '10px' } }, appearanceProfileRows.length ? appearanceProfileRows : [$('p', { class: 'rgcc-muted' }, tx('尚未保存配置。', 'No profiles saved yet.', 'Noch keine Profile gespeichert.'))])])), deferredSettingsCard(tx('即时预览', 'Instant preview', 'Sofortvorschau'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('即时预览', 'Instant preview', 'Sofortvorschau')), $('p', {}, tx('颜色、字体、间距、圆角、阴影和背景立即应用于设置、工作区、发现页和模型库。', 'Colors, typography, spacing, corners, shadows and backgrounds apply immediately to Settings, the workspace, Discover, and the model library.', 'Farben, Typografie, Abstände, Rundungen, Schatten und Hintergründe wirken sofort in den Einstellungen, im Arbeitsbereich, in Entdecken und in der Modellbibliothek.')), $('div', { style: { minHeight: '120px', border: '1px solid var(--rg-border)', borderRadius: 'var(--rg-radius)', background: 'var(--rg-panel)', boxShadow: 'var(--rg-shadow)', padding: 'var(--rg-card-gap)', display: 'grid', alignContent: 'center', gap: '8px' } }, [$('strong', {}, tx('示例卡片', 'Example card', 'Beispielkarte')), $('span', { class: 'rgcc-muted' }, tx('这就是当前视觉设置的组合效果。', 'This is the combined result of the current visual settings.', 'So wirken die aktuellen visuellen Einstellungen zusammen.')), btn(tx('示例操作', 'Example action', 'Beispielaktion'), () => {}, 'primary')])])), ]), deferredSettingsCard(tx('每页设计配置', 'Per-page design profiles', 'Designprofile je Seite'), () => $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('每页设计配置', 'Per-page design profiles', 'Designprofile je Seite')), $('p', {}, tx('可以为主页、列表、模型页、MultiCam、发现页和资料中心分别选择外观。', 'Choose a different saved appearance for Home, listings, model pages, MultiCam, Discover and the data hub.', 'Wähle für Startseite, Listen, Modelseiten, MultiCam, Entdecken und das Datenzentrum jeweils ein gespeichertes Design.')), check(tx('启用每页设计', 'Enable per-page design', 'Seitenspezifisches Design aktivieren'), settings.pageAppearanceProfilesEnabled, value => patch({ pageAppearanceProfilesEnabled: value })), $('div', { class: 'rgcc-grid' }, pageProfileFields)])), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('统一主导航', 'Unified main navigation', 'Einheitliche Hauptnavigation'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('统一主导航', 'Unified main navigation', 'Einheitliche Hauptnavigation')), $('p', {}, tx('顶部显示主页、多画面、发现、标签浏览器、统计中心、模型库、关注、我的用户和设置。所有配置区域都作为设置中的子页面提供。', 'The header shows Home, MultiCam, Discover, Tag Explorer, Statistics Center, Model Library, Following, My User, and Settings. Every configuration area is available as a Settings subpage.', 'Oben stehen Start, MultiCam, Entdecken, Tag-Explorer, Statistikzentrum, Modelbibliothek, Gefolgt, Mein Nutzer und Einstellungen. Alle Konfigurationsbereiche befinden sich als Unterseiten in Einstellungen.'))])), deferredSettingsCard(tx('正常网站页面', 'Normal website pages', 'Normale Website-Seiten'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('正常网站页面', 'Normal website pages', 'Normale Website-Seiten')), check(tx('启用RoomGrid页面设计', 'Enable RoomGrid page design', 'RoomGrid-Seitendesign aktivieren'), settings.normalPageDesign, value => patch({ normalPageDesign: value })), $('div', { class: 'rgcc-row' }, [check(tx('主页', 'Home', 'Startseite'), settings.normalDesignHome, value => patch({ normalDesignHome: value })), check(tx('标签和分类页', 'Tag and category pages', 'Tag- und Kategorieseiten'), settings.normalDesignListings, value => patch({ normalDesignListings: value })), check(tx('模型页', 'Model pages', 'Modelseiten'), settings.normalDesignRoom, value => patch({ normalDesignRoom: value }))]), field(tx('视图', 'View', 'Ansicht'), select(settings.normalPageView, [['grid', tx('卡片', 'Cards', 'Karten')], ['list', tx('列表', 'List', 'Liste')]], value => patch({ normalPageView: value }))), field(tx('卡片大小', 'Card size', 'Kartengröße'), range(settings.normalPageCardScale, value => patchLive({ normalPageCardScale: value }), { min: 70, max: 150, step: 5 }, value => `${value} %`)), field(tx('页面密度', 'Page density', 'Seitendichte'), select(settings.normalPageDensity, [['compact', tx('紧凑', 'Compact', 'Kompakt')], ['comfortable', tx('舒适', 'Comfortable', 'Komfortabel')], ['spacious', tx('宽松', 'Spacious', 'Großzügig')]], value => patch({ normalPageDensity: value }))), check(tx('整理导航', 'Polish navigation', 'Navigation übersichtlicher gestalten'), settings.normalPolishNavigation, value => patch({ normalPolishNavigation: value })), check(tx('整理侧栏', 'Polish sidebars', 'Seitenleisten übersichtlicher gestalten'), settings.normalPolishSidebar, value => patch({ normalPolishSidebar: value })), check(tx('隐藏可识别的广告和商品区', 'Hide recognized promo and merch areas', 'Erkannte Werbe- und Merchbereiche ausblenden'), settings.normalHidePromos, value => patch({ normalHidePromos: value })), check(tx('压缩页脚', 'Compact footer', 'Kompakter Footer'), settings.normalCompactFooter, value => patch({ normalCompactFooter: value })), check(tx('标记已保存模型', 'Mark saved models', 'Gespeicherte Models markieren'), settings.normalMarkSaved, value => patch({ normalMarkSaved: value })), check(tx('单独标记收藏', 'Mark favorites separately', 'Favoriten gesondert markieren'), settings.normalMarkFavorites, value => patch({ normalMarkFavorites: value })), check(tx('标记已访问模型', 'Mark visited models', 'Besuchte Models markieren'), settings.normalMarkVisited, value => patch({ normalMarkVisited: value })), check(tx('显示卡片快捷操作', 'Show card quick actions', 'Schnellaktionen auf Karten anzeigen'), settings.normalQuickActions, value => patch({ normalQuickActions: value })), $('hr'), check(tx('首页和列表页的动画实时预览', 'Animated live previews on home and listing pages', 'Animierte Live-Vorschauen auf Start- und Listenseiten'), settings.normalLivePreviews, value => patch({ normalLivePreviews: value })), field(tx('预览刷新率', 'Preview refresh rate', 'Vorschau-Aktualisierung'), select(String(settings.normalLivePreviewFps), [['0', tx('关闭', 'Off', 'Aus')], ['0.1', '0,1 FPS · 10 s'], ['0.2', '0,2 FPS · 5 s'], ['0.25', '0,25 FPS · 4 s'], ['0.33', '0,33 FPS · 3 s'], ['0.5', '0,5 FPS · 2 s'], ['0.75', '0,75 FPS'], ['1', '1 FPS'], ['1.5', '1,5 FPS'], ['2', '2 FPS'], ['3', '3 FPS'], ['4', '4 FPS'], ['5', '5 FPS']], value => patch({ normalLivePreviewFps: Number(value), normalLivePreviews: Number(value) > 0 }))), field(tx('更新方式', 'Update mode', 'Aktualisierungsart'), select(settings.normalLivePreviewMode, [['simultaneous', tx('全部可见内容同时更新', 'All visible cards simultaneously', 'Alle sichtbaren gleichzeitig')], ['bounded', tx('性能受限的分批更新', 'Performance-bounded batches', 'Leistungsgestaffelt')]], value => patch({ normalLivePreviewMode: value }))), field(tx('分批并行限制', 'Batch concurrency limit', 'Parallelitätsgrenze bei Staffelung'), select(String(settings.normalLivePreviewConcurrency), [['0', tx('所有可见内容', 'All visible', 'Alle sichtbaren')], ['1', '1'], ['2', '2'], ['4', '4'], ['6', '6'], ['8', '8'], ['12', '12'], ['16', '16'], ['24', '24']], value => patch({ normalLivePreviewConcurrency: Number(value) }))), $('p', { class: 'rgcc-note' }, tx('只有当前可见的卡片会以静音 JPEG 单帧更新;离开视口或切换标签页后立即暂停。同步模式会以一波同时更新所有可见卡片;高帧率会明显增加带宽和 CPU 使用。', 'Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.', 'Nur sichtbare Karten erhalten stumme JPEG-Einzelbilder; außerhalb des Sichtbereichs oder in einem ausgeblendeten Tab pausieren sie sofort. Der synchrone Modus aktualisiert alle sichtbaren Karten in einer gemeinsamen Welle; hohe Bildraten erhöhen Bandbreite und CPU-Last deutlich.'))])), $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('悬停实时视频', 'Live video on hover', 'Echtzeitvideo beim Darüberfahren')), check(tx('鼠标悬停时播放实时视频', 'Play real-time video while hovering', 'Beim Darüberfahren echtes Livevideo abspielen'), settings.normalHoverLivePreview, value => patch({ normalHoverLivePreview: value })), field(tx('启动延迟', 'Start delay', 'Startverzögerung'), select(String(settings.normalHoverLivePreviewDelay), [['0', tx('立即', 'Immediately', 'Sofort')], ['150', '0,15 s'], ['250', '0,25 s'], ['400', '0,4 s'], ['600', '0,6 s'], ['1000', '1 s']], value => patch({ normalHoverLivePreviewDelay: Number(value) }))), field(tx('悬停放大', 'Hover enlargement', 'Vergrößerung beim Darüberfahren'), range(settings.normalHoverLivePreviewScale, value => store.patchSettings({ normalHoverLivePreviewScale: value }), { min: 100, max: 600, step: 1 }, value => `${value} %`)), $('p', { class: 'rgcc-note' }, tx('只会播放鼠标下方的一张卡片,始终静音。移开鼠标或切换标签页会立即停止视频。', 'Only the card under the pointer plays, always muted. Moving away or hiding the tab stops the stream immediately.', 'Nur die Karte unter dem Mauszeiger spielt – immer stumm. Beim Verlassen oder Wechseln des Tabs wird der Stream sofort vollständig beendet.'))]), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('实时预览停靠栏', 'Live Preview Dock', 'Live-Vorschau-Dock'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('实时预览停靠栏', 'Live Preview Dock', 'Live-Vorschau-Dock')), check(tx('启用低负载实时预览', 'Enable low-load live previews', 'Ressourcenschonende Live-Vorschauen aktivieren'), settings.previewDockEnabled, value => patch({ previewDockEnabled: value })), field(tx('同时预览', 'Concurrent previews', 'Gleichzeitige Vorschauen'), input('number', settings.previewDockMax, value => patch({ previewDockMax: value }), { min: 1, max: 6 })), field(tx('窗口大小', 'Window size', 'Fenstergröße'), select(settings.previewDockSize, [['small', tx('小', 'Small', 'Klein')], ['medium', tx('中', 'Medium', 'Mittel')], ['large', tx('大', 'Large', 'Groß')]], value => patch({ previewDockSize: value }))), field(tx('不透明度', 'Opacity', 'Deckkraft'), range(settings.previewDockOpacity, value => patchLive({ previewDockOpacity: value }), { min: .55, max: 1, step: .05 }, value => `${Math.round(value * 100)} %`)), $('p', {}, tx('预览静音启动;隐藏标签页时自动暂停。声音始终需要手动开启。', 'Previews start muted and pause in hidden tabs. Audio always requires an explicit click.', 'Vorschauen starten stumm und pausieren in ausgeblendeten Tabs. Ton erfordert immer einen bewussten Klick.'))])), deferredSettingsCard(tx('快速信息与聊天专注', 'Quick info & chat focus', 'Quick Info & Chat-Fokus'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('快速信息与聊天专注', 'Quick info & chat focus', 'Quick Info & Chat-Fokus')), check(tx('启用模型快速信息面板', 'Enable model Quick View', 'Model-Quick-View aktivieren'), settings.inspectorEnabled, value => patch({ inspectorEnabled: value })), check(tx('聊天专注模式(可选)', 'Chat focus mode (opt-in)', 'Chat-Fokus-Modus (optional)'), settings.chatFocusEnabled, value => { patch({ chatFocusEnabled: value }); RoomGridUltimate.refreshChatFocus(); }), check(tx('显示本地时间戳', 'Show local timestamps', 'Lokale Zeitstempel anzeigen'), settings.chatTimestampEnabled, value => { patch({ chatTimestampEnabled: value }); RoomGridUltimate.refreshChatFocus(); }), field(tx('系统消息', 'System messages', 'Systemmeldungen'), select(settings.chatSystemMode, [['show', tx('显示', 'Show', 'Anzeigen')], ['dim', tx('变暗', 'Dim', 'Abblenden')], ['hide', tx('隐藏', 'Hide', 'Ausblenden')]], value => { patch({ chatSystemMode: value }); RoomGridUltimate.refreshChatFocus(); })), field(tx('聊天过滤强度', 'Chat filter strength', 'Chat-Filterstärke'), select(settings.chatFilterLevel, [['off', tx('关闭', 'Off', 'Aus')], ['low', tx('轻度', 'Low', 'Leicht')], ['medium', tx('中等', 'Medium', 'Mittel')], ['strict', tx('严格', 'Strict', 'Streng')]], value => { patch({ chatFilterLevel: value }); RoomGridUltimate.refreshChatFocus(); })), field(tx('突出显示词(逗号分隔)', 'Highlight words (comma separated)', 'Hervorheben (kommagetrennt)'), input('text', settings.chatHighlightWords, value => { store.patchSettings({ chatHighlightWords: value }); RoomGridUltimate.refreshChatFocus(); }, { maxlength: 500 })), field(tx('静音词(逗号分隔)', 'Muted words (comma separated)', 'Ausgeblendete Wörter (kommagetrennt)'), input('text', settings.chatMutedWords, value => { store.patchSettings({ chatMutedWords: value }); RoomGridUltimate.refreshChatFocus(); }, { maxlength: 500 })), check(tx('折叠重复消息', 'Collapse repeated messages', 'Wiederholte Nachrichten einklappen'), settings.chatCollapseRepeats, value => { patch({ chatCollapseRepeats: value }); RoomGridUltimate.refreshChatFocus(); }), $('p', {}, tx('聊天增强只改变当前页面的显示,不保存聊天内容,也不自动发送消息或小费。', 'Chat enhancements only change the current display. They never store chat, send messages, or tip automatically.', 'Chat-Verbesserungen ändern nur die aktuelle Anzeige. Sie speichern keinen Chat und senden weder Nachrichten noch Trinkgeld automatisch.'))])), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ deferredSettingsCard(tx('快捷键', 'Shortcuts', 'Tastaturkürzel'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('快捷键', 'Shortcuts', 'Tastaturkürzel')), $('p', {}, `${tx('命令菜单', 'Command menu', 'Befehlsmenü')}: ${shortcutLabel(settings.shortcuts?.commandPalette || 'ctrl+k')} · ${tx('全局模型搜索', 'Global model search', 'Globale Modellsuche')}: ${shortcutLabel(settings.shortcuts?.globalSearch || 'ctrl+shift+k')}`), btn(tx('恢复设计默认值', 'Reset design defaults', 'Design-Vorgaben wiederherstellen'), () => { const keys = [...appearanceProfileKeys, 'backgroundGlobal', 'backgroundPages', 'backgroundModels', 'brandName', 'brandMonogram', 'brandTagline', 'brandDisplayMode', 'brandLogoLight', 'brandLogoDark', 'brandLogoShape', 'brandLogoSize', 'brandLogoWidth', 'brandLogoHeight', 'brandLogoFit', 'brandLogoPositionX', 'brandLogoPositionY', 'brandLogoPadding', 'brandLogoBackground', 'brandShowVersion', 'brandReplaceNativeLogo', 'brandPageTitleMode', 'brandCustomPageTitle', 'brandFaviconEnabled', 'responsiveAppearanceProfilesEnabled', 'responsiveAppearanceProfiles', 'appearanceBreakpoints', 'designPreviewDevice', 'designPreviewSurface', 'animations', 'navOrder', 'navHidden', 'sidebarWidth', 'previewDockEnabled', 'previewDockMax', 'previewDockOpacity', 'previewDockSize', 'inspectorEnabled', 'chatFocusEnabled', 'chatTimestampEnabled', 'chatSystemMode']; previewDesignReset(keys); })])), deferredSettingsCard(tx('可选模块', 'Optional modules', 'Optionale Module'), () => $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('可选模块', 'Optional modules', 'Optionale Module')), check(tx('正常页面工具栏和卡片操作', 'Normal-page dock and card actions', 'Dock und Kartenaktionen auf normalen Seiten'), moduleSettings.pageTools, value => { setSuiteModuleEnabled('pageTools', value); render(); }), check(tx('真实音频电平', 'Real audio meters', 'Echte Audiopegel'), moduleSettings.audioMeters, value => { setSuiteModuleEnabled('audioMeters', value); render(); }), check(tx('媒体、应用和比赛目录', 'Media, app and contest directories', 'Medien-, App- und Contest-Verzeichnisse'), moduleSettings.optionalTools, value => { setSuiteModuleEnabled('optionalTools', value); render(); }), $('p', { class: 'rgcc-note' }, tx('模块开关在重新加载页面后完全生效。核心数据和备份模块保持启用,以避免数据丢失。', 'Module switches take full effect after reloading the page. Core data and backup modules remain enabled to prevent data loss.', 'Modulschalter wirken vollständig nach dem Neuladen. Kernmodule für Daten und Sicherungen bleiben aktiv, damit keine Daten verloren gehen.'))])), ]), ); } function homepagePreviewOptions(settings, patch) { return $('section', { class: 'rgcc-card rgcc-section', dataset: { homepagePreviewSettings: '1' } }, [ $('h3', {}, tx('实时预览', 'Live previews', 'Live-Vorschauen')), check(tx('首页和列表页的动画实时预览', 'Animated live previews on home and listing pages', 'Animierte Live-Vorschauen auf Start- und Listenseiten'), settings.normalLivePreviews, value => patch({ normalLivePreviews: value })), field(tx('预览刷新率', 'Preview refresh rate', 'Vorschau-Aktualisierung'), select(String(settings.normalLivePreviewFps), [['0', tx('关闭', 'Off', 'Aus')], ['0.1', '0,1 FPS · 10 s'], ['0.2', '0,2 FPS · 5 s'], ['0.25', '0,25 FPS · 4 s'], ['0.33', '0,33 FPS · 3 s'], ['0.5', '0,5 FPS · 2 s'], ['0.75', '0,75 FPS'], ['1', '1 FPS'], ['1.5', '1,5 FPS'], ['2', '2 FPS'], ['3', '3 FPS'], ['4', '4 FPS'], ['5', '5 FPS']], value => patch({ normalLivePreviewFps: Number(value), normalLivePreviews: Number(value) > 0 }))), field(tx('更新方式', 'Update mode', 'Aktualisierungsart'), select(settings.normalLivePreviewMode, [['simultaneous', tx('全部可见内容同时更新', 'All visible cards simultaneously', 'Alle sichtbaren gleichzeitig')], ['bounded', tx('性能受限的分批更新', 'Performance-bounded batches', 'Leistungsgestaffelt')]], value => patch({ normalLivePreviewMode: value }))), field(tx('分批并行限制', 'Batch concurrency limit', 'Parallelitätsgrenze bei Staffelung'), select(String(settings.normalLivePreviewConcurrency), [['0', tx('所有可见内容', 'All visible', 'Alle sichtbaren')], ['1', '1'], ['2', '2'], ['4', '4'], ['6', '6'], ['8', '8'], ['12', '12'], ['16', '16'], ['24', '24']], value => patch({ normalLivePreviewConcurrency: Number(value) }))), $('p', { class: 'rgcc-note' }, tx('只有当前可见的卡片会以静音 JPEG 单帧更新;离开视口或切换标签页后立即暂停。同步模式会以一波同时更新所有可见卡片;高帧率会明显增加带宽和 CPU 使用。', 'Only visible cards receive muted JPEG frame updates; they pause immediately offscreen or in a hidden tab. Synchronized mode refreshes every visible card in one wave; high rates noticeably increase bandwidth and CPU use.', 'Nur sichtbare Karten erhalten stumme JPEG-Einzelbilder; außerhalb des Sichtbereichs oder in einem ausgeblendeten Tab pausieren sie sofort. Der synchrone Modus aktualisiert alle sichtbaren Karten in einer gemeinsamen Welle; hohe Bildraten erhöhen Bandbreite und CPU-Last deutlich.')) ]); } async function renderHomepage(revision) { let knownModels = []; try { knownModels = analytics.getModelsSync(); } catch (_) {} if (revision !== renderRevision || tab !== 'homepage') return; const settings = store.state.settings; const notifyHomepageSettings = () => { try { hooks?.onHomepageSettings?.(store.state.settings); } catch (error) { StorageHealth.report('homepage-settings-hook', error); } }; const patch = (next, resetPage = true) => { store.patchSettings({ ...next, ...(resetPage ? { normalHomePage: 1 } : {}) }); notifyHomepageSettings(); applySuiteAppearance(store); render(); }; const patchLive = next => { store.patchSettings(next); notifyHomepageSettings(); applySuiteAppearance(store); }; const clearFilters = () => patch({ normalHomeSearch: '', normalHomeStatus: 'all', normalHomeGender: 'all', normalHomeCountryFilter: '', normalHomeTagFilter: '', normalHomeMinViewers: 0, normalHomeMaxViewers: 0, normalHomeMinAge: 0, normalHomeMaxAge: 0, normalHomeOnlyHd: false, normalHomeOnlyNew: false, normalHomeOnlyGaming: false, normalHomeOnlySaved: false, normalHomeOnlyFavorites: false, normalHomeOnlyFollowing: false, normalHomeHideVisited: false }); const homeViewKeys = ['normalHomeSearch', 'normalHomeStatus', 'normalHomeGender', 'normalHomeCountryFilter', 'normalHomeTagFilter', 'normalHomeTagMatchMode', 'normalHomeMinViewers', 'normalHomeMaxViewers', 'normalHomeMinAge', 'normalHomeMaxAge', 'normalHomeOnlyHd', 'normalHomeOnlyNew', 'normalHomeOnlyGaming', 'normalHomeOnlySaved', 'normalHomeOnlyFavorites', 'normalHomeOnlyFollowing', 'normalHomeHideVisited', 'normalHomeSort']; const saveHomeView = () => { const proposed = prompt(tx('为当前筛选和排序命名', 'Name the current filters and sorting', 'Aktuelle Filter und Sortierung benennen'), tx('我的智能视图', 'My smart view', 'Meine intelligente Ansicht')); const name = String(proposed || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60); if (!name) return; const presets = Array.isArray(store.state.settings.normalHomeFilterPresets) ? [...store.state.settings.normalHomeFilterPresets] : []; const match = presets.findIndex(item => String(item.name || '').toLocaleLowerCase() === name.toLocaleLowerCase()); const saved = { id: match >= 0 ? presets[match].id : `home_view_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, name, createdAt: Date.now(), values: Object.fromEntries(homeViewKeys.map(key => [key, store.state.settings[key]])) }; if (match >= 0) presets.splice(match, 1, saved); else presets.push(saved); patch({ normalHomeFilterPresets: presets.slice(-24) }, false); }; const removeHomeView = preset => { if (!confirm(`${tx('删除智能视图', 'Delete smart view', 'Intelligente Ansicht löschen')} “${rawUiText(preset.name)}”?`)) return; RoomGridUx.undoable(store, tx('智能视图已删除', 'Smart view deleted', 'Intelligente Ansicht gelöscht'), () => store.patchSettings({ normalHomeFilterPresets: settings.normalHomeFilterPresets.filter(item => item.id !== preset.id) }), { onUndo: render }); render(); }; const homeViewRows = settings.normalHomeFilterPresets.map(preset => $('div', { class: 'rgcc-item' }, [ $('strong', {}, rawUiText(preset.name)), $('span', { class: 'rgcc-muted' }, `${String(preset.values?.normalHomeTagFilter || '').split(',').filter(Boolean).length} ${tx('标签', 'tags', 'Tags')} · ${String(preset.values?.normalHomeCountryFilter || '').split(',').filter(Boolean).length} ${tx('国家', 'countries', 'Länder')} · ${preset.values?.normalHomeSort || 'native'}`), $('span', { class: 'rgcc-actions' }, [btn(tx('应用', 'Apply', 'Anwenden'), () => patch(preset.values || {}), 'primary'), btn(tx('删除', 'Delete', 'Löschen'), () => removeHomeView(preset), 'danger')]), ])); const normalizedTags = new Map(); const cleanTag = value => { if (normalizedTags.has(value)) return normalizedTags.get(value); const clean = String(value || '').trim().replace(/^#+/u, '').replace(/[\u0000-\u001f\u007f,;|]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 80); normalizedTags.set(value, clean); return clean; }; const cleanCountry = value => { const clean = String(value || '').replace(/[\u0000-\u001f\u007f,;|]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 80); return /^[a-z]{2}$/iu.test(clean) ? clean.toUpperCase() : clean; }; const ownTags = new Map(); const websiteTags = new Map(); const observedCountries = new Map(); const addOption = (map, value, cleaner) => { const clean = cleaner(value); if (clean && !map.has(clean.toLocaleLowerCase())) map.set(clean.toLocaleLowerCase(), clean); }; store.state.rooms.forEach(room => (room.localTags || []).forEach(tag => addOption(ownTags, tag, cleanTag))); const addModelOptions = model => { (model.tags || []).forEach(tag => addOption(websiteTags, tag, cleanTag)); addOption(observedCountries, model.country, cleanCountry); }; knownModels.forEach(addModelOptions); // Public filter choices must not depend on recording personal history // or on which cards happen to be mounted after a menu switch. globalThis.RoomGridCatalogBridge?.getCurrentModels?.()?.rooms?.forEach(addModelOptions); document.querySelectorAll('[data-tags],[data-room-tags],[data-broadcast-tags],[data-tag-list],[data-tag]').forEach(node => { ['data-tags', 'data-room-tags', 'data-broadcast-tags', 'data-tag-list', 'data-tag'].forEach(name => { if (!node.hasAttribute(name)) return; const raw = String(node.getAttribute(name) || '').trim(); if (!raw) return; try { const parsed = JSON.parse(raw); (Array.isArray(parsed) ? parsed : []).forEach(tag => addOption(websiteTags, tag, cleanTag)); } catch (_) { raw.split(/[,;|]+/u).forEach(tag => addOption(websiteTags, tag, cleanTag)); } }); }); document.querySelectorAll('a[href*="/tag/"],a[href*="/tags/"]').forEach(link => { try { const match = new URL(link.href, location.href).pathname.match(/^\/tags?\/([^/?#]+)/iu); if (match) addOption(websiteTags, decodeURIComponent(match[1]).replace(/[-_]+/gu, ' '), cleanTag); } catch (_) {} }); document.querySelectorAll('[data-country],[data-country-code]').forEach(node => addOption(observedCountries, node.getAttribute('data-country') || node.getAttribute('data-country-code'), cleanCountry)); const countryCodes = 'AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW'.split(' '); let regionNames = null; try { regionNames = new Intl.DisplayNames([LANG === 'pt' ? 'pt-BR' : LANG], { type: 'region' }); } catch (_) {} const countryLabel = code => /^[A-Z]{2}$/u.test(code) ? `${regionNames?.of(code) || code} · ${code}` : code; const optionComparer = new Intl.Collator(LANG === 'pt' ? 'pt-BR' : LANG, { sensitivity: 'base', numeric: true }); const sortValues = values => [...values].sort(optionComparer.compare); const observedCountryOptions = sortValues([...observedCountries.values()]).map(value => ({ value, label: countryLabel(value) })); const observedCountryKeys = new Set(observedCountryOptions.map(option => option.value.toLocaleLowerCase())); const countryComparer = new Intl.Collator(LANG === 'pt' ? 'pt-BR' : LANG, { sensitivity: 'base' }); const allCountryOptions = countryCodes.filter(code => !observedCountryKeys.has(code.toLocaleLowerCase())).map(value => ({ value, label: countryLabel(value) })).sort((left, right) => countryComparer.compare(left.label, right.label)); const ownTagOptions = sortValues([...ownTags.values()]).map(value => ({ value: cleanTag(value).toLocaleLowerCase(), label: value })); const websiteTagOptions = sortValues([...websiteTags.values()].filter(value => !ownTags.has(cleanTag(value).toLocaleLowerCase()))).map(value => ({ value: cleanTag(value).toLocaleLowerCase(), label: value })); const multiPicker = ({ id, label, value, cleaner, groups, placeholder, searchPlaceholder, settingKey, valueLabel = item => item, autocompleteProvider = null }) => { const selected = new Map(); String(value || '').split(/[,;|]+/u).map(cleaner).filter(Boolean).forEach(item => selected.set(item.toLocaleLowerCase(), item)); const known = new Map(); groups.forEach(group => group.options.forEach(option => { const clean = cleaner(option.value); if (clean && !known.has(clean.toLocaleLowerCase())) known.set(clean.toLocaleLowerCase(), { ...option, value: clean }); })); const missing = [...selected.values()].filter(item => !known.has(item.toLocaleLowerCase())).map(item => ({ value: item, label: valueLabel(item) })); const effectiveGroups = missing.length ? [{ label: tx('已保存的选择', 'Saved selection', 'Gespeicherte Auswahl', 'Selección guardada', 'Seleção salva'), options: missing }, ...groups] : groups; const summaryText = $('span'); const summaryCount = $('b', { class: 'rgcc-chip' }); const summary = $('summary', {}, [summaryText, summaryCount]); const chips = $('div', { class: 'rgcc-multi-chips', dataset: { roomgridMultiChips: id } }); const search = $('input', { class: 'rgcc-input', type: 'search', placeholder: searchPlaceholder, ariaLabel: searchPlaceholder }); const suggestionsHost = $('div', { class: 'rgcc-multi-suggestions', hidden: true, role: 'listbox', ariaLabel: tx('建议', 'Suggestions', 'Vorschläge', 'Sugerencias', 'Sugestões') }); const optionsHost = $('div', { class: 'rgcc-multi-options' }); const clear = btn(tx('取消全部选择', 'Clear selection', 'Auswahl löschen', 'Borrar selección', 'Limpar seleção'), () => {}); const details = $('details', { class: 'rgcc-multi-picker', dataset: { roomgridHomePicker: id } }, [summary, $('div', { class: 'rgcc-multi-menu' }, [$('div', { class: 'rgcc-multi-tools' }, [search, clear]), suggestionsHost, optionsHost])]); const rows = []; const rowKeys = new Set(); let autocompleteTimer = 0; let autocompleteRevision = 0; let remoteSuggestions = []; const save = () => { const next = [...selected.values()].slice(0, settingKey === 'normalHomeTagFilter' ? 120 : 80).join(','); patchLive({ [settingKey]: next, normalHomePage: 1 }); }; const draw = () => { summaryText.textContent = selected.size ? tx('已选择', 'Selected', 'Ausgewählt', 'Seleccionados', 'Selecionados') : placeholder; summaryCount.textContent = selected.size ? String(selected.size) : ''; summaryCount.hidden = !selected.size; chips.replaceChildren(...[...selected.values()].map(item => { const key = item.toLocaleLowerCase(); const knownOption = known.get(key); const remove = $('button', { type: 'button', title: tx('移除', 'Remove', 'Entfernen', 'Quitar', 'Remover'), ariaLabel: `${tx('移除', 'Remove', 'Entfernen', 'Quitar', 'Remover')} ${knownOption?.label || valueLabel(item)}` }, '×'); remove.addEventListener('click', () => { selected.delete(key); save(); draw(); }); return $('span', { class: 'rgcc-multi-chip' }, [$('span', {}, knownOption?.label || valueLabel(item)), remove]); })); rows.forEach(({ input: checkbox, key }) => { checkbox.checked = selected.has(key); }); }; const suggestionCandidates = query => { if (!query) return []; const merged = new Map(); const add = (value, optionLabel = value, source = 'known') => { const clean = cleaner(value); const key = clean.toLocaleLowerCase(); if (!clean || !key.includes(query) || merged.has(key)) return; merged.set(key, { value: clean, label: optionLabel, key, source, prefix: key.startsWith(query) }); }; known.forEach(option => add(option.value, option.label, 'known')); remoteSuggestions.forEach(item => add(item, valueLabel(item), 'website')); return [...merged.values()].sort((left, right) => Number(right.prefix) - Number(left.prefix) || left.key.localeCompare(right.key, LANG === 'pt' ? 'pt-BR' : LANG, { sensitivity: 'base', numeric: true })).slice(0, 40); }; const drawSuggestions = () => { const query = cleaner(search.value).toLocaleLowerCase(); const suggestions = suggestionCandidates(query); suggestionsHost.hidden = !query || !suggestions.length; suggestionsHost.replaceChildren(...suggestions.map(item => { const button = $('button', { class: 'rgcc-multi-suggestion', type: 'button', role: 'option', ariaPressed: selected.has(item.key), dataset: { autocompleteValue: item.value }, title: item.source === 'website' ? tx('网站标签', 'Website tags', 'Website-Tags', 'Etiquetas del sitio web', 'Tags do site') : '' }, `#${item.label}`); button.addEventListener('click', () => { if (selected.has(item.key)) selected.delete(item.key); else selected.set(item.key, item.value); save(); draw(); drawSuggestions(); search.focus(); }); return button; })); }; let rowsBuilt = false; const buildRows = () => { if (rowsBuilt) return; rowsBuilt = true; effectiveGroups.forEach(group => { const groupTitle = $('div', { class: 'rgcc-multi-group' }, group.label); const groupRows = []; for (const option of group.options) { const clean = cleaner(option.value); const key = clean.toLocaleLowerCase(); if (!clean || rowKeys.has(key)) continue; rowKeys.add(key); const checkbox = $('input', { type: 'checkbox' }); const optionRow = $('label', { class: 'rgcc-multi-option', dataset: { value: clean, search: `${option.label} ${clean}`.toLocaleLowerCase() } }, [checkbox, $('span', {}, option.label)]); checkbox.addEventListener('change', () => { if (checkbox.checked) selected.set(key, clean); else selected.delete(key); save(); draw(); }); rows.push({ input: checkbox, key, node: optionRow, groupTitle }); groupRows.push(optionRow); } if (groupRows.length) optionsHost.append(groupTitle, ...groupRows); }); }; const empty = $('div', { class: 'rgcc-multi-empty' }, tx('没有匹配项', 'No matches', 'Keine Treffer', 'Sin resultados', 'Nenhum resultado')); const filterRows = () => { const query = search.value.trim().toLocaleLowerCase(); let visible = 0; rows.forEach(row => { const show = !query || row.node.dataset.search.includes(query); row.node.hidden = !show; if (show) visible += 1; }); new Set(rows.map(row => row.groupTitle)).forEach(title => { title.hidden = !rows.some(row => row.groupTitle === title && !row.node.hidden); }); empty.remove(); if (!visible) optionsHost.append(empty); if (!autocompleteProvider || query.length < 2) remoteSuggestions = []; drawSuggestions(); clearTimeout(autocompleteTimer); const revision = ++autocompleteRevision; if (autocompleteProvider && query.length >= 2) autocompleteTimer = setTimeout(async () => { const results = await autocompleteProvider(query); if (revision !== autocompleteRevision || cleaner(search.value).toLocaleLowerCase() !== query) return; remoteSuggestions = Array.isArray(results) ? results : []; drawSuggestions(); }, 140); }; search.addEventListener('input', filterRows); search.addEventListener('keydown', event => { if (event.key === 'Escape' && search.value) { event.preventDefault(); search.value = ''; filterRows(); return; } if (event.key === 'ArrowDown') { const first = suggestionsHost.querySelector('button'); if (first) { event.preventDefault(); first.focus(); } return; } if (event.key !== 'Enter') return; const first = suggestionsHost.querySelector('button'); if (first) { event.preventDefault(); first.click(); } }); clear.onclick = () => { selected.clear(); save(); draw(); filterRows(); search.focus(); }; // Closed pickers expose their saved chips, not hundreds of unused // checkbox nodes. Build the complete list once on actual first use. details.addEventListener('toggle', () => { if (!details.open) return; buildRows(); draw(); filterRows(); body.querySelectorAll('.rgcc-multi-picker[open]').forEach(other => { if (other !== details) other.open = false; }); setTimeout(() => search.focus(), 0); }); draw(); return $('div', { class: 'rgcc-multi' }, [$('span', {}, label), chips, details]); }; const countryPicker = multiPicker({ id: 'countries', label: tx('国家(可多选)', 'Countries (multiple selection)', 'Länder (Mehrfachauswahl)', 'Países (selección múltiple)', 'Países (seleção múltipla)'), value: settings.normalHomeCountryFilter, cleaner: cleanCountry, settingKey: 'normalHomeCountryFilter', valueLabel: countryLabel, placeholder: tx('选择国家', 'Select countries', 'Länder auswählen', 'Seleccionar países', 'Selecionar países'), searchPlaceholder: tx('搜索国家…', 'Search countries…', 'Länder suchen …', 'Buscar países…', 'Pesquisar países…'), groups: [{ label: tx('当前模型中已识别', 'Recognized in current models', 'In aktuellen Models erkannt', 'Detectados en los modelos actuales', 'Reconhecidos nos modelos atuais'), options: observedCountryOptions }, { label: tx('所有国家', 'All countries', 'Alle Länder', 'Todos los países', 'Todos os países'), options: allCountryOptions }], }); const tagPicker = multiPicker({ id: 'tags', label: tx('标签(可多选)', 'Tags (multiple selection)', 'Tags (Mehrfachauswahl)', 'Etiquetas (selección múltiple)', 'Tags (seleção múltipla)'), value: settings.normalHomeTagFilter, cleaner: value => cleanTag(value).toLocaleLowerCase(), settingKey: 'normalHomeTagFilter', placeholder: tx('选择标签', 'Select tags', 'Tags auswählen', 'Seleccionar etiquetas', 'Selecionar tags'), searchPlaceholder: tx('搜索标签…', 'Search tags…', 'Tags suchen …', 'Buscar etiquetas…', 'Pesquisar tags…'), groups: [{ label: tx('自己的标签', 'Own tags', 'Eigene Tags', 'Etiquetas propias', 'Tags próprias'), options: ownTagOptions }, { label: tx('网站标签', 'Website tags', 'Website-Tags', 'Etiquetas del sitio web', 'Tags do site'), options: websiteTagOptions }], autocompleteProvider: query => queryNativeWebsiteTags(query, cleanTag), }); const detailChecks = [ ['normalHomeShowCountry', tx('国家/地区', 'Country/region', 'Land/Region')], ['normalHomeShowLocation', tx('地点', 'Location', 'Ort')], ['normalHomeShowViewers', tx('观众数', 'Viewer count', 'Zuschauerzahl')], ['normalHomeShowAge', tx('年龄', 'Age', 'Alter')], ['normalHomeShowStatus', tx('直播状态', 'Live status', 'Live-Status')], ['normalHomeShowTags', 'Tags'], ['normalHomeShowSubject', tx('房间主题', 'Room subject', 'Raumthema')], ['normalHomeShowOnlineTime', tx('当前在线时长', 'Current online duration', 'Aktuelle Online-Dauer')], ['normalHomeShowLocalStats', tx('本地观看时间和会话', 'Local watch time and sessions', 'Lokale Sehzeit und Sitzungen')], ['normalHomeShowBadges', tx('HD、新模型、游戏和收藏徽章', 'HD, new, gaming and favorite badges', 'HD-, Neu-, Gaming- und Favoriten-Abzeichen')], ].map(([key, label]) => check(label, settings[key], value => patch({ [key]: value }, false))); body.replaceChildren( $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('Camclient 作为真正的主页', 'Camclient as the real homepage', 'Camclient als echte Startseite')), $('p', {}, tx( '访问 Chaturbate 首页时始终打开完整的 Camclient 主页。所有主要区域都保留在同一个自定义界面中。', 'Visiting the Chaturbate homepage always opens the complete Camclient homepage. Every main area stays inside the same custom interface.', 'Beim Besuch der Chaturbate-Startseite öffnet sich immer die vollständige Camclient-Startseite. Alle Hauptbereiche bleiben in derselben eigenen Oberfläche.' )), $('div', { class: 'rgcc-row' }, [ $('span', { class: 'rgcc-note' }, tx('始终启用', 'Always active', 'Dauerhaft aktiv')), btn(tx('打开 Camclient 主页', 'Open Camclient homepage', 'Camclient-Start öffnen'), () => { const url = new URL('/', location.origin); url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', 'dashboard'); location.href = url.toString(); }, 'primary'), ]), $('p', { class: 'rgcc-note' }, tx('所有主页筛选、布局、预览及分页设置都保存在稳定设置保管库中,并在脚本更新后保留。', 'All homepage filters, layout, preview, and pagination settings are stored in the stable settings vault and survive script updates.', 'Alle Startseitenfilter, Layout-, Vorschau- und Seiteneinstellungen werden im stabilen Einstellungstresor gespeichert und bleiben nach Script-Updates erhalten.')), ]), homepagePreviewOptions(settings, next => { store.patchSettings(next); render(); }), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('首页工作室', 'Homepage Studio', 'Startseiten-Studio')), $('p', {}, tx('这些设置控制 Camclient 主页的布局、分页、模型信息、筛选和性能。', 'These settings control the Camclient homepage layout, pagination, model information, filters, and performance.', 'Diese Einstellungen steuern Layout, Seiteneinteilung, Modelinformationen, Filter und Leistung der Camclient-Startseite.')), $('div', { class: 'rgcc-row' }, [ btn(tx('平衡预设', 'Balanced preset', 'Ausgewogen'), () => patch({ normalPageView: 'grid', normalPageCardScale: 100, normalHomeColumns: 0, normalHomePageSize: 24, normalHomeImageRatio: 'auto', normalHomeMetaStyle: 'compact', normalHomeShowCountry: true, normalHomeShowLocation: true, normalHomeShowViewers: true, normalHomeShowAge: true, normalHomeShowStatus: true, normalHomeShowTags: true, normalHomeShowSubject: true, normalHomeShowOnlineTime: true, normalHomeShowLocalStats: false, normalHomeShowBadges: true }), 'primary'), btn(tx('紧凑预设', 'Compact preset', 'Kompakt'), () => patch({ normalPageView: 'grid', normalPageCardScale: 78, normalHomeColumns: 6, normalHomePageSize: 48, normalHomeImageRatio: '16/9', normalHomeMetaStyle: 'compact', normalHomeShowCountry: true, normalHomeShowLocation: false, normalHomeShowViewers: true, normalHomeShowAge: false, normalHomeShowStatus: true, normalHomeShowTags: false, normalHomeShowSubject: false, normalHomeShowOnlineTime: false, normalHomeShowLocalStats: false, normalHomeShowBadges: true })), btn(tx('数据预设', 'Data-rich preset', 'Datenreich'), () => patch({ normalPageView: 'grid', normalPageCardScale: 115, normalHomeColumns: 4, normalHomePageSize: 24, normalHomeImageRatio: '4/3', normalHomeMetaStyle: 'detailed', normalHomeShowCountry: true, normalHomeShowLocation: true, normalHomeShowViewers: true, normalHomeShowAge: true, normalHomeShowStatus: true, normalHomeShowTags: true, normalHomeShowSubject: true, normalHomeShowOnlineTime: true, normalHomeShowLocalStats: true, normalHomeShowBadges: true })), btn(tx('影院预设', 'Cinema preset', 'Kino'), () => patch({ normalPageView: 'grid', normalPageCardScale: 145, normalHomeColumns: 3, normalHomePageSize: 12, normalHomeImageRatio: '16/9', normalHomeMetaStyle: 'overlay', normalHomeShowCountry: true, normalHomeShowLocation: true, normalHomeShowViewers: true, normalHomeShowAge: false, normalHomeShowStatus: true, normalHomeShowTags: false, normalHomeShowSubject: true, normalHomeShowOnlineTime: true, normalHomeShowLocalStats: false, normalHomeShowBadges: true })), ]), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('布局和分页', 'Layout and pagination', 'Layout und Seiteneinteilung')), check(tx('在模型上方显示快速工具栏', 'Show quick toolbar above models', 'Schnellleiste über den Models anzeigen'), settings.normalHomeToolbar, value => patch({ normalHomeToolbar: value }, false)), field(tx('视图', 'View', 'Ansicht'), select(settings.normalPageView, [['grid', tx('卡片', 'Cards', 'Karten')], ['list', tx('列表', 'List', 'Liste')]], value => patch({ normalPageView: value }, false))), field(tx('每页模型数', 'Models per page', 'Models pro Seite'), select(String(settings.normalHomePageSize), [['12', '12'], ['24', '24'], ['36', '36'], ['48', '48'], ['60', '60'], ['90', '90'], ['0', tx('全部已加载', 'All loaded', 'Alle geladen')]], value => patch({ normalHomePageSize: Number(value) }))), field(tx('每行卡片数', 'Cards per row', 'Karten pro Reihe'), select(String(settings.normalHomeColumns), [['0', tx('自动适应窗口', 'Automatic for window', 'Automatisch nach Fenster')], ['2', '2'], ['3', '3'], ['4', '4'], ['5', '5'], ['6', '6'], ['7', '7'], ['8', '8']], value => patch({ normalHomeColumns: Number(value) }, false))), field(tx('卡片大小', 'Card size', 'Kartengröße'), range(settings.normalPageCardScale, value => patchLive({ normalPageCardScale: value }), { min: 70, max: 150, step: 5 }, value => `${value} %`)), field(tx('图像比例', 'Image ratio', 'Bildformat'), select(settings.normalHomeImageRatio, [['auto', tx('网站原始', 'Website original', 'Website-Original')], ['16/9', '16:9'], ['4/3', '4:3'], ['1/1', '1:1'], ['3/4', '3:4']], value => patch({ normalHomeImageRatio: value }, false))), field(tx('信息显示', 'Information display', 'Informationsdarstellung'), select(settings.normalHomeMetaStyle, [['compact', tx('紧凑', 'Compact', 'Kompakt')], ['detailed', tx('详细', 'Detailed', 'Ausführlich')], ['overlay', tx('图像叠加', 'Overlay on image', 'Über dem Bild')]], value => patch({ normalHomeMetaStyle: value }, false))), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('强大的排序', 'Powerful sorting', 'Leistungsfähige Sortierung')), field(tx('主要排序', 'Primary sort', 'Hauptsortierung'), select(settings.normalHomeSort, [ ['native', tx('网站顺序', 'Website order', 'Website-Reihenfolge')], ['viewers_desc', tx('观众最多', 'Most viewers', 'Meiste Zuschauer')], ['viewers_asc', tx('观众最少', 'Fewest viewers', 'Wenigste Zuschauer')], ['name_asc', 'Name A–Z'], ['name_desc', 'Name Z–A'], ['online_desc', tx('在线时间最长', 'Longest online', 'Am längsten online')], ['online_asc', tx('刚刚上线', 'Recently online', 'Gerade erst online')], ['age_asc', tx('年龄升序', 'Age ascending', 'Alter aufsteigend')], ['age_desc', tx('年龄降序', 'Age descending', 'Alter absteigend')], ['new_first', tx('新模型优先', 'New models first', 'Neue Models zuerst')], ['hd_first', 'HD zuerst'], ['gaming_first', 'Gaming zuerst'], ['saved_first', tx('已保存优先', 'Saved first', 'Gespeicherte zuerst')], ['favorite_first', tx('收藏优先', 'Favorites first', 'Favoriten zuerst')], ['watched_desc', tx('观看最多', 'Most watched', 'Meistgesehen')], ['recent_desc', tx('最近观看', 'Recently watched', 'Zuletzt gesehen')], ['random', tx('随机混合', 'Random mix', 'Zufällige Mischung')], ], value => patch({ normalHomeSort: value }))), $('p', { class: 'rgcc-note' }, tx('排序只改变已加载模型卡片的显示顺序,不会更改 Chaturbate 账户中的关注或收藏。', 'Sorting only changes the display order of loaded model cards. It does not change follows or favorites in your Chaturbate account.', 'Die Sortierung ändert nur die Anzeige der geladenen Modelkarten. Following und Favoriten deines Chaturbate-Kontos bleiben unverändert.')), $('div', { class: 'rgcc-row' }, [btn(tx('恢复网站顺序', 'Restore website order', 'Website-Reihenfolge wiederherstellen'), () => patch({ normalHomeSort: 'native' })), btn(tx('清除所有筛选', 'Clear all filters', 'Alle Filter löschen'), clearFilters, 'danger')]), ]), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('搜索和范围筛选', 'Search and range filters', 'Suche und Bereichsfilter')), field(tx('姓名、主题、标签或地点', 'Name, subject, tag or place', 'Name, Thema, Tag oder Ort'), input('search', settings.normalHomeSearch, value => patch({ normalHomeSearch: value }), { maxlength: 120 })), $('div', { class: 'rgcc-row' }, [ field(tx('状态', 'Status', 'Status'), select(settings.normalHomeStatus, [['all', tx('全部', 'All', 'Alle')], ['online', 'Live'], ['away', tx('离开', 'Away', 'Abwesend')], ['private', tx('私人', 'Private', 'Privat')]], value => patch({ normalHomeStatus: value }))), field(tx('类型', 'Type', 'Typ'), select(settings.normalHomeGender, [['all', tx('全部', 'All', 'Alle')], ['f', tx('女性', 'Women', 'Frauen')], ['m', tx('男性', 'Men', 'Männer')], ['c', tx('情侣', 'Couples', 'Paare')], ['t', 'Trans']], value => patch({ normalHomeGender: value }))), ]), $('div', { class: 'rgcc-row rgcc-multi-row' }, [countryPicker, tagPicker]), $('div', { class: 'rgcc-row' }, [field(tx('选择多个标签时', 'When several tags are selected', 'Bei mehreren ausgewählten Tags', 'Cuando se seleccionan varias etiquetas', 'Quando várias tags são selecionadas'), select(settings.normalHomeTagMatchMode, [['all', tx('模型必须包含所有标签', 'Model must contain every tag', 'Model muss alle Tags enthalten', 'El modelo debe contener todas', 'O modelo deve conter todas')], ['any', tx('至少一个标签匹配', 'At least one tag matches', 'Mindestens ein Tag passt', 'Coincide al menos una', 'Pelo menos uma tag corresponde')]], value => patch({ normalHomeTagMatchMode: value })))]), $('p', {}, tx('国家筛选会显示来自任一所选国家的模型。自己的标签和已识别的网站标签会保留在列表中,并可通过搜索快速找到。', 'Country filtering shows models from any selected country. Own tags and recognized website tags stay available in the list and can be found quickly with search.', 'Der Länderfilter zeigt Models aus jedem der gewählten Länder. Eigene Tags und erkannte Website-Tags bleiben in der Liste verfügbar und sind schnell durchsuchbar.', 'El filtro de países muestra modelos de cualquiera de los países seleccionados. Las etiquetas propias y las detectadas en el sitio permanecen disponibles y se pueden buscar.', 'O filtro de países mostra modelos de qualquer país selecionado. Tags próprias e reconhecidas no site continuam disponíveis e podem ser pesquisadas.')), $('p', { class: 'rgcc-note' }, tx('选择标签或国家后,RoomGrid 会搜索当前完整直播目录。在同一结果页面上先显示 120 个匹配项,滚动时再分批加载,以便即使是非常常见的标签也能保持页面流畅。主页快速栏中的重新加载按钮可刷新直播列表。', 'After selecting tags or countries, RoomGrid searches the complete current live catalog. The same result page initially shows 120 matches and loads more in chunks while scrolling, so even very common tags stay responsive. The reload button in the homepage toolbar refreshes the live list.', 'Sobald Tags oder Länder gewählt sind, durchsucht RoomGrid den vollständigen aktuellen Live-Katalog. Auf derselben Ergebnisseite werden zuerst 120 Treffer angezeigt und beim Scrollen weitere in Blöcken geladen, damit auch sehr häufige Tags flüssig bleiben. Der Neuladen-Knopf in der Startseitenleiste aktualisiert die Live-Liste.', 'Después de seleccionar etiquetas o países, RoomGrid busca en todo el catálogo en vivo actual. La misma página de resultados muestra primero 120 coincidencias y carga más por bloques al desplazarse para mantener la fluidez incluso con etiquetas muy frecuentes.', 'Depois de selecionar tags ou países, o RoomGrid pesquisa todo o catálogo ao vivo atual. A mesma página de resultados mostra primeiro 120 correspondências e carrega mais em blocos ao rolar, mantendo a fluidez mesmo com tags muito frequentes.')), $('div', { class: 'rgcc-row' }, [field(tx('最少观众', 'Minimum viewers', 'Mindestens Zuschauer'), input('number', settings.normalHomeMinViewers, value => patch({ normalHomeMinViewers: value }), { min: 0, max: 10000000 })), field(tx('最多观众', 'Maximum viewers', 'Höchstens Zuschauer'), input('number', settings.normalHomeMaxViewers, value => patch({ normalHomeMaxViewers: value }), { min: 0, max: 10000000 }))]), $('div', { class: 'rgcc-row' }, [field(tx('最低年龄', 'Minimum age', 'Mindestalter'), input('number', settings.normalHomeMinAge, value => patch({ normalHomeMinAge: value }), { min: 0, max: 99 })), field(tx('最高年龄', 'Maximum age', 'Höchstalter'), input('number', settings.normalHomeMaxAge, value => patch({ normalHomeMaxAge: value }), { min: 0, max: 99 }))]), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('快速条件', 'Quick conditions', 'Schnellbedingungen')), check(tx('仅高清模型', 'HD models only', 'Nur HD-Models'), settings.normalHomeOnlyHd, value => patch({ normalHomeOnlyHd: value })), check(tx('仅新模型', 'New models only', 'Nur neue Models'), settings.normalHomeOnlyNew, value => patch({ normalHomeOnlyNew: value })), check(tx('仅游戏模型', 'Gaming models only', 'Nur Gaming-Models'), settings.normalHomeOnlyGaming, value => patch({ normalHomeOnlyGaming: value })), check(tx('仅保存在 RoomGrid 中', 'Only saved in RoomGrid', 'Nur in RoomGrid gespeichert'), settings.normalHomeOnlySaved, value => patch({ normalHomeOnlySaved: value })), check(tx('仅自己的收藏', 'Only own favorites', 'Nur eigene Favoriten'), settings.normalHomeOnlyFavorites, value => patch({ normalHomeOnlyFavorites: value })), check(tx('仅已关注模型', 'Following only', 'Nur gefolgte Models'), settings.normalHomeOnlyFollowing, value => patch({ normalHomeOnlyFollowing: value })), check(tx('隐藏已看过的模型', 'Hide previously watched models', 'Bereits gesehene Models ausblenden'), settings.normalHomeHideVisited, value => patch({ normalHomeHideVisited: value })), $('p', { class: 'rgcc-note' }, tx('国家、年龄和在线时长来自当前公开卡片或公开房间目录。如果 Chaturbate 没有提供某个字段,RoomGrid 不会猜测它。', 'Country, age and online duration come from the current public cards or public room directory. If Chaturbate does not provide a field, RoomGrid does not guess it.', 'Land, Alter und Online-Dauer stammen aus den aktuell öffentlichen Karten oder dem öffentlichen Raumverzeichnis. Wenn Chaturbate ein Feld nicht liefert, rät RoomGrid nicht.')), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('智能筛选视图', 'Smart filter views', 'Intelligente Filteransichten')), $('p', {}, tx('将当前筛选和排序保存为动态视图。以后上线并符合条件的新模型会自动出现在该视图中。', 'Save the current filters and sorting as a dynamic view. Models that come online later and match the rules appear automatically.', 'Speichere die aktuellen Filter und die Sortierung als dynamische Ansicht. Später online kommende Models, die zu den Regeln passen, erscheinen automatisch.')), btn(tx('保存当前筛选视图', 'Save current filter view', 'Aktuelle Filteransicht speichern'), saveHomeView, 'primary'), $('div', { class: 'rgcc-list', style: { marginTop: '10px' } }, homeViewRows.length ? homeViewRows : [$('p', { class: 'rgcc-muted' }, tx('尚未保存智能视图。', 'No smart views saved yet.', 'Noch keine intelligenten Ansichten gespeichert.'))]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('每个模型显示的内容', 'What to show for each model', 'Was pro Model angezeigt wird')), $('div', { class: 'rgcc-row' }, detailChecks), ]), ); } async function renderAnalytics(revision) { const range = String(body.dataset.range || 'month'); const from = rangeStart(range); const [models, events] = [analytics.getModelsSync(), analytics.getEventsSync({ from, limit: 1000 })]; if (revision !== renderRevision || tab !== 'analytics') return; const summary = analytics.getUsageSummary(); const visits = events.filter(event => event.type === 'visit'); const usageEvents = timeAnalysis ? events.filter(event => event.type === 'usage') : []; const camTime = visits.reduce((sum, event) => sum + event.durationMs, 0) || models.reduce((sum, model) => sum + Number(model.totalMs || 0), 0); const usageTime = usageEvents.reduce((sum, event) => sum + event.durationMs, 0) || Number(summary.totalUsageMs || 0); const ranked = [...models].sort((a, b) => Number(b.totalMs || 0) - Number(a.totalMs || 0)).slice(0, 20); const heat = Array(168).fill(0); models.forEach(model => (model.hours || []).forEach((value, index) => { heat[index] += Number(value) || 0; })); const maxHeat = Math.max(1, ...heat); const usageKinds = ['workspace', 'filter', 'tag', 'discovered', 'quality', 'connection'].map(type => [type, events.filter(event => event.type === type).length]); const importInput = $('input', { type: 'file', accept: 'application/json', style: { display: 'none' }, onchange: async event => { const file = event.target.files?.[0]; if (!file) return; try { if (file.size > 20 * 1024 * 1024) throw new Error('large'); const incoming = JSON.parse(await file.text()); if (!Array.isArray(incoming?.models) || incoming.models.length > ROOMGRID_DATA_LIMITS.modelRecords) throw new Error('invalid'); const preview = tx( `统计导入预览(尚未更改):\n${incoming.models.length} 个模型\n${Array.isArray(incoming.events) ? incoming.events.length : 0} 个事件\n${Array.isArray(incoming.profiles) ? incoming.profiles.length : 0} 个公开资料\n\n继续将替换现有统计数据。`, `Statistics import preview (nothing changed yet):\n${incoming.models.length} models\n${Array.isArray(incoming.events) ? incoming.events.length : 0} events\n${Array.isArray(incoming.profiles) ? incoming.profiles.length : 0} public profiles\n\nContinuing replaces the existing statistics.`, `Statistik-Importvorschau (noch nichts geändert):\n${incoming.models.length} Models\n${Array.isArray(incoming.events) ? incoming.events.length : 0} Ereignisse\n${Array.isArray(incoming.profiles) ? incoming.profiles.length : 0} öffentliche Profile\n\nBeim Fortfahren werden die vorhandenen Statistiken ersetzt.`, `Vista previa de la importación de estadísticas (todavía no se ha cambiado nada):\n${incoming.models.length} modelos\n${Array.isArray(incoming.events) ? incoming.events.length : 0} eventos\n${Array.isArray(incoming.profiles) ? incoming.profiles.length : 0} perfiles públicos\n\nAl continuar se reemplazarán las estadísticas existentes.`, `Prévia da importação de estatísticas (nada foi alterado ainda):\n${incoming.models.length} modelos\n${Array.isArray(incoming.events) ? incoming.events.length : 0} eventos\n${Array.isArray(incoming.profiles) ? incoming.profiles.length : 0} perfis públicos\n\nContinuar substituirá as estatísticas existentes.`, ); if (!confirm(preview)) return; await analytics.importData(incoming); alert(tx('导入完成', 'Import complete', 'Import abgeschlossen')); render(); } catch (_) { alert(tx('文件无效', 'Invalid file', 'Ungültige Datei')); } } }); body.replaceChildren( $('div', { class: 'rgcc-row' }, [ field(tx('时间范围', 'Range', 'Zeitraum'), select(range, [['day', tx('今天', 'Today', 'Tag')], ['week', tx('周', 'Week', 'Woche')], ['month', tx('月', 'Month', 'Monat')], ['year', tx('年', 'Year', 'Jahr')], ['all', tx('全部', 'All', 'Alles')]], value => { body.dataset.range = value; render(); })), check(tx('记录已启用', 'Recording enabled', 'Aufzeichnung aktiv'), analytics.isEnabled(), value => { store.patchSettings({ analyticsEnabled: value }); analytics.setEnabled(value); render(); }), check(tx('私人会话(不记录)', 'Private session (do not record)', 'Private Sitzung (nicht aufzeichnen)'), analytics.isPrivateSession(), value => { analytics.setPrivateSession(value); render(); }), field(tx('保留天数', 'Retention days', 'Aufbewahrungstage'), input('number', summary.retentionDays, value => { analytics.setRetentionDays(value); render(); }, { min: 7, max: 3650 })), ]), $('div', { class: 'rgcc-grid' }, [ stat(tx('使用时间', 'Usage time', 'Nutzungszeit'), fmtDuration(usageTime)), stat(tx('摄像头总时间', 'Summed camera time', 'Summierte Cam-Zeit'), fmtDuration(camTime)), stat(tx('访问', 'Visits', 'Besuche'), String(visits.length || ranked.reduce((sum, model) => sum + Number(model.sessions || 0), 0))), stat(tx('已知模型', 'Known models', 'Bekannte Models'), String(models.length)), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('每周热图', 'Weekly heatmap', 'Wochen-Heatmap')), $('p', {}, tx('按星期和小时汇总;颜色越深表示观察时间越长。', 'Aggregated by weekday and hour; darker means more observed time.', 'Nach Wochentag und Stunde; dunkler bedeutet mehr beobachtete Zeit.')), $('div', { class: 'rgcc-heat', title: tx('星期日到星期六,每行 24 小时', 'Sunday to Saturday, 24 hours per row', 'Sonntag bis Samstag, je 24 Stunden') }, heat.map((value, index) => $('i', { title: `${['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][Math.floor(index / 24)]} ${String(index % 24).padStart(2, '0')}:00 · ${fmtDuration(value)}`, style: { background: `rgba(37,99,235,${0.08 + (value / maxHeat) * 0.92})` } }))), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('观看排名', 'Watch ranking', 'Sehzeit-Rangliste')), $('div', { class: 'rgcc-list' }, ranked.length ? ranked.map((model, index) => { const hour = (model.hours || []).map((value, i) => [Number(value) || 0, i]).sort((a, b) => b[0] - a[0])[0]?.[1]; const typical = Number.isInteger(hour) ? `${['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][Math.floor(hour / 24)]} ${String(hour % 24).padStart(2, '0')}:00` : '—'; return $('div', { class: 'rgcc-item' }, [$('strong', {}, `${index + 1}. ${model.id}`), $('span', {}, `${fmtDuration(model.totalMs)} · ${model.sessions} ${tx('次会话', 'sessions', 'Sitzungen')} · ${typical}${model.sessions < 3 ? ` · ${tx('数据较少', 'little data', 'wenig Daten')}` : ''}`), $('span', { class: 'rgcc-muted' }, `${model.firstWatchedAt ? new Date(model.firstWatchedAt).toLocaleDateString() : '—'} → ${model.lastWatchedAt ? new Date(model.lastWatchedAt).toLocaleDateString() : '—'}`)]); }) : [$('p', {}, tx('尚无观看记录。', 'No watch history yet.', 'Noch kein Verlauf.'))])]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('工作区、筛选与质量事件', 'Workspace, filter and quality events', 'Arbeitsbereich-, Filter- und Qualitätsereignisse')), $('div', { class: 'rgcc-list' }, usageKinds.map(([type, count]) => $('div', { class: 'rgcc-item' }, [ $('strong', {}, type), $('span', {}, String(count)), $('span', { class: 'rgcc-muted' }, tx('本地事件', 'local events', 'lokale Ereignisse')), ]))), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('最近的时间线', 'Recent timeline', 'Letzte Chronik')), $('div', { class: 'rgcc-list' }, events.slice(0, 30).map(event => $('div', { class: 'rgcc-item' }, [ $('strong', {}, event.id || event.type), $('span', {}, event.detail || event.type), $('span', { class: 'rgcc-row' }, [$('time', { class: 'rgcc-muted' }, new Date(event.at).toLocaleString()), btn('×', async () => { await analytics.deleteEvent(event.eventId); render(); }, 'danger')]), ]))), $('div', { class: 'rgcc-row' }, [ btn(tx('导出敏感统计', 'Export sensitive statistics', 'Sensible Statistik exportieren'), async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `roomgrid-sensitive-statistics-${stampForFile()}.json`); }), btn(tx('导入统计', 'Import statistics', 'Statistik importieren'), () => { document.body.appendChild(importInput); importInput.click(); }), btn(tx('删除所选时间范围', 'Delete selected range', 'Gewählten Zeitraum löschen'), async () => { if (confirm(tx('删除所选时间范围内的本地事件?', 'Delete local events in the selected range?', 'Lokale Ereignisse im gewählten Zeitraum löschen?'))) { await analytics.deleteRange(from, Date.now()); render(); } }, 'danger'), btn(tx('清除所有统计', 'Clear all statistics', 'Alle Statistiken löschen'), async () => { if (confirm(tx('永久清除所有本地统计?', 'Permanently clear all local statistics?', 'Alle lokalen Statistiken dauerhaft löschen?'))) { await analytics.clearAll(); render(); } }, 'danger'), ]), $('p', { class: 'rgcc-note' }, tx('统计导出包含模型名称和时间戳,应视为敏感文件。旧数据会按保留期限自动汇总和清理。', 'Statistics exports contain model names and timestamps and should be treated as sensitive. Old data is cleaned according to retention.', 'Statistik-Exporte enthalten Modelnamen und Zeitstempel und sind sensible Dateien. Alte Daten werden gemäß Aufbewahrungsfrist bereinigt.')), ]), ); } let modelHeatCache = null; async function prepareModelHeat(models, revision) { const dataRevision = analytics.getDataRevision(); if (modelHeatCache?.dataRevision === dataRevision && modelHeatCache.modelCount === models.length && analytics.isReady?.() !== false) return modelHeatCache.heat; const heat = Array(168).fill(0); let sliceAt = performance.now(); for (const model of models) { (model.hours || []).forEach((value, index) => { heat[index] += Number(value) || 0; }); if (performance.now() - sliceAt >= 8) { await new Promise(resolve => setTimeout(resolve, 0)); if (revision !== renderRevision || tab !== 'analytics') return null; sliceAt = performance.now(); } } // A partial or superseded calculation must never become a warm result. if (revision !== renderRevision || tab !== 'analytics') return null; if (analytics.getDataRevision() === dataRevision && analytics.isReady?.() !== false) modelHeatCache = { dataRevision, modelCount: models.length, heat }; return heat; } let plannerCache = null; async function prepareOnlinePlanner(models, revision) { const now = new Date(); const hourKey = Math.floor(now.getTime() / 3600000); const dataRevision = analytics.getDataRevision(); if (plannerCache?.hourKey === hourKey && plannerCache.dataRevision === dataRevision && plannerCache.modelCount === models.length && analytics.isReady?.() !== false) return plannerCache.items; const currentIndex = now.getDay() * 24 + now.getHours(); const items = []; let sliceAt = performance.now(); for (const model of models) { let observedTotal = 0; const observedHours = model.onlineHours; const observedHourCount = Array.isArray(observedHours) ? Math.min(168, observedHours.length) : observedHours ? 168 : 0; for (let index = 0; index < observedHourCount; index += 1) observedTotal += Math.max(0, Number(observedHours[index]) || 0); const weights = observedTotal > 0 ? model.onlineHours : model.hours; let best = { offset: 0, score: 0 }; for (let offset = 0; offset < 48; offset += 1) { const score = Math.max(0, Number(weights?.[(currentIndex + offset) % 168]) || 0); if (score > best.score) best = { offset, score }; } if (best.score > 0) items.push({ model, best, samples: Math.max(Number(model.onlineSamples || 0), observedTotal) }); if (performance.now() - sliceAt >= 8) { await new Promise(resolve => setTimeout(resolve, 0)); if (revision !== renderRevision || tab !== 'analytics') return null; sliceAt = performance.now(); } } items.sort((a, b) => b.best.score - a.best.score || b.samples - a.samples); const result = items.slice(0, 40); if (analytics.getDataRevision() === dataRevision && analytics.isReady?.() !== false) plannerCache = { hourKey, dataRevision, modelCount: models.length, items: result }; return result; } async function renderAnalyticsStudio(revision) { const range = String(body.dataset.range || 'month'); const section = String(body.dataset.analyticsSection || 'overview'); const from = rangeStart(range); const periodMs = range === 'day' ? 86400000 : range === 'week' ? 7 * 86400000 : range === 'month' ? 31 * 86400000 : range === 'year' ? 366 * 86400000 : 0; const previousFrom = periodMs ? Math.max(0, from - periodMs) : 0; const timeAnalysis = section === 'overview' || section === 'time'; const eventAnalysis = timeAnalysis || section === 'discover' || section === 'workspaces'; const modelAnalysis = timeAnalysis || section === 'models' || section === 'planner'; const [models, events, previousEvents, quality, allRollups, inventory, analyticsBackups] = await Promise.all([ modelAnalysis ? analytics.getModelsSync() : [], eventAnalysis ? analytics.getEventsSync({ from, limit: 50000 }) : [], section === 'overview' && periodMs ? analytics.getEventsSync({ from: previousFrom, to: from - 1, limit: 50000 }) : [], section === 'quality' || section === 'overview' ? analytics.getQualitySamplesSync({ from, limit: 20000 }) : [], timeAnalysis ? analytics.getDailyRollups() : [], section === 'data' ? analytics.measureDataInventory() : section === 'overview' ? analytics.dataInventory() : null, section === 'data' ? analytics.getBackups() : Promise.resolve([]), ]); if (revision !== renderRevision || tab !== 'analytics') return; const summary = analytics.getUsageSummary(); const visits = events.filter(event => event.type === 'visit'); const usageEvents = events.filter(event => event.type === 'usage'); const duration = list => list.reduce((sum, event) => sum + Math.max(0, Number(event.durationMs) || 0), 0); const rollups = allRollups.filter(item => new Date(`${item.day}T00:00:00`).getTime() >= from); const rolledUsage = rollups.reduce((sum, item) => sum + Number(item.durations?.usage || 0), 0); const rolledCam = rollups.reduce((sum, item) => sum + Number(item.durations?.visit || 0), 0); const usageTime = duration(usageEvents) + rolledUsage || (range === 'all' ? Number(summary.totalUsageMs || 0) : 0); const camTime = duration(visits) + rolledCam || (range === 'all' ? models.reduce((sum, model) => sum + Number(model.totalMs || 0), 0) : 0); const previousUsage = duration(previousEvents.filter(event => event.type === 'usage')) + allRollups.filter(item => { const at = new Date(`${item.day}T00:00:00`).getTime(); return at >= previousFrom && at < from; }).reduce((sum, item) => sum + Number(item.durations?.usage || 0), 0); const comparison = (current, previous) => previous > 0 ? `${current >= previous ? '+' : ''}${Math.round(((current - previous) / previous) * 100)}%` : tx('新', 'new', 'neu'); const visitDurations = section === 'time' ? visits.map(event => Math.max(0, Number(event.durationMs) || 0)).filter(Boolean).sort((a, b) => a - b) : []; const median = visitDurations.length ? visitDurations[Math.floor(visitDurations.length / 2)] : 0; const activeDays = timeAnalysis ? new Set([...events.map(event => new Date(event.at).toISOString().slice(0, 10)), ...rollups.map(item => item.day)]).size : 0; const sessionBuckets = [ [tx('少于 1 分钟', 'Under 1 minute', 'Unter 1 Minute'), 0, 60000], [tx('1–5 分钟', '1–5 minutes', '1–5 Minuten'), 60000, 300000], [tx('5–15 分钟', '5–15 minutes', '5–15 Minuten'), 300000, 900000], [tx('15–30 分钟', '15–30 minutes', '15–30 Minuten'), 900000, 1800000], [tx('30 分钟以上', '30+ minutes', '30+ Minuten'), 1800000, Infinity], ].map(([label, minimum, maximum]) => [label, visitDurations.filter(value => value >= minimum && value < maximum).length]); const ranked = section === 'models' ? [...models].sort((a, b) => Number(b.totalMs || 0) - Number(a.totalMs || 0)).slice(0, 30) : []; const repeatModels = section === 'overview' || section === 'models' ? models.filter(model => Number(model.sessions || 0) > 1).length : 0; let heat = Array(168).fill(0); if (section === 'overview' || section === 'time') { visits.forEach(event => { const date = new Date(event.at); heat[date.getDay() * 24 + date.getHours()] += Math.max(60000, Number(event.durationMs) || 0); }); if (!visits.length) { heat = await prepareModelHeat(models, revision); if (heat === null || revision !== renderRevision || tab !== 'analytics') return; } } const maxHeat = Math.max(1, ...heat); const dayMap = new Map(); rollups.forEach(item => dayMap.set(item.day, Number(item.durations?.usage || item.durations?.visit || 0))); if (timeAnalysis) [...events].reverse().forEach(event => { const key = new Date(event.at).toISOString().slice(0, 10); if (!dayMap.has(key)) dayMap.set(key, 0); dayMap.set(key, dayMap.get(key) + Math.max(0, Number(event.durationMs) || (event.type === 'visit' ? 60000 : 0))); }); const daily = [...dayMap.entries()].slice(-31); const countsByType = new Map(); if (section === 'overview' || section === 'discover' || section === 'workspaces') { for (const event of events) countsByType.set(event.type, (countsByType.get(event.type) || 0) + 1); } const eventKinds = ['preview', 'quickview', 'visit', 'workspace', 'filter', 'tag', 'discovered', 'quality', 'connection', 'player'].map(type => [type, countsByType.get(type) || 0]); const totals = quality.reduce((out, sample) => ({ stalls: out.stalls + sample.stalls, stallMs: out.stallMs + sample.stallMs, reconnects: out.reconnects + sample.reconnects, frames: out.frames + sample.totalFrames, dropped: out.dropped + sample.droppedFrames, streams: out.streams + sample.activeStreams, startup: out.startup + sample.startupMs }), { stalls: 0, stallMs: 0, reconnects: 0, frames: 0, dropped: 0, streams: 0, startup: 0 }); const dropRate = totals.frames ? totals.dropped / totals.frames * 100 : 0; const avgStreams = quality.length ? totals.streams / quality.length : 0; const avgStartup = quality.length ? totals.startup / quality.length : 0; const barChart = (values, labelFormatter = value => value[0].slice(5)) => { const maximum = Math.max(1, ...values.map(value => Number(value[1]) || 0)); return $('div', { class: 'rgcc-chart' }, values.map(value => $('i', { class: 'rgcc-bar', title: `${value[0]} · ${fmtDuration(value[1])}`, style: { height: `${Math.max(3, (Number(value[1]) || 0) / maximum * 100)}%` } }, $('span', {}, labelFormatter(value))))); }; const progressList = items => $('div', { class: 'rgcc-list' }, items.map(([label, value, max = 1, note = '']) => $('div', { class: 'rgcc-card' }, [$('div', { class: 'rgcc-row' }, [$('strong', {}, label), $('span', { class: 'rgcc-muted', style: { marginLeft: 'auto' } }, note || String(value))]), $('div', { class: 'rgcc-progress' }, $('i', { style: { width: `${Math.max(1, Math.min(100, Number(value) / Math.max(1, Number(max)) * 100))}%` } }))]))); const provenanceChip = (label, detail) => $('span', { class: 'rgcc-provenance', title: detail, dataset: { roomgridProvenance: '1' } }, label); const provenanceName = value => ({ 'local-observation-and-user-activity': tx('本地观察与自己的使用', 'Local observations and your usage', 'Lokale Beobachtungen und eigene Nutzung'), 'user-requested-public-profile': tx('按需读取的公共资料', 'Public profile loaded on request', 'Auf Wunsch geladenes öffentliches Profil'), 'local-roomgrid-interaction': tx('本地 RoomGrid 操作', 'Local RoomGrid interaction', 'Lokale RoomGrid-Aktion'), 'local-browser-player': tx('本地浏览器播放器', 'Local browser player', 'Lokaler Browser-Player'), 'optional-official-stats-api': tx('可选官方统计 API', 'Optional official statistics API', 'Optionale offizielle Statistik-API'), 'local-aggregation': tx('本地每日汇总', 'Local daily aggregation', 'Lokale Tageszusammenfassung'), }[value] || tx('本地数据', 'Local data', 'Lokale Daten')); const statisticsSourceTitle = value => ({ models: tx('模型摘要', 'Model summaries', 'Model-Zusammenfassungen'), profiles: tx('公共资料', 'Public profiles', 'Öffentliche Profile'), events: tx('使用事件', 'Usage events', 'Nutzungsereignisse'), qualitySamples: tx('质量样本', 'Quality samples', 'Qualitätsproben'), accountSnapshots: tx('官方账户快照', 'Official account snapshots', 'Offizielle Konto-Snapshots'), dailyRollups: tx('每日汇总', 'Daily rollups', 'Tageszusammenfassungen'), }[value] || tx('本地数据', 'Local data', 'Lokale Daten')); const widgetLabels = { usage: tx('使用时间', 'Usage time', 'Nutzungszeit'), camera: tx('摄像头时间', 'Camera time', 'Cam-Zeit'), activeDays: tx('活动天数', 'Active days', 'Aktive Tage'), comparison: tx('与上期相比', 'vs previous period', 'zum Vorzeitraum'), trends: tx('趋势', 'Trends', 'Trends'), daily: tx('每日活动', 'Daily activity', 'Tägliche Aktivität'), heatmap: tx('一周热图', 'Weekly heatmap', 'Wochen-Heatmap'), glance: tx('快速概览', 'At a glance', 'Auf einen Blick'), models: tx('已知模型', 'Known models', 'Bekannte Models'), quality: tx('播放质量', 'Playback quality', 'Wiedergabequalität'), privacy: tx('隐私状态', 'Privacy status', 'Privatsphäre-Status'), provenance: tx('数据来源', 'Data provenance', 'Datenherkunft'), }; const dashboardPresets = { balanced: ['usage','camera','activeDays','comparison','trends','daily','heatmap','glance','privacy','provenance'], minimal: ['usage','camera','comparison','daily','privacy'], quality: ['usage','camera','quality','daily','glance','provenance'], privacy: ['usage','activeDays','privacy','provenance'], }; const dashboardWidgets = (Array.isArray(store.state.settings.analyticsDashboardWidgets) ? store.state.settings.analyticsDashboardWidgets : []).map(item => ({ ...item })); const saveDashboardWidgets = (widgets, preset = 'custom') => { store.patchSettings({ analyticsDashboardWidgets: widgets, analyticsDashboardPreset: preset }); render(); }; const applyDashboardPreset = preset => { const enabled = new Set(dashboardPresets[preset] || dashboardPresets.balanced); const ordered = [...enabled, ...dashboardWidgets.map(item => item.id).filter(id => !enabled.has(id))]; saveDashboardWidgets(ordered.map(id => ({ ...(dashboardWidgets.find(item => item.id === id) || { id, size: 'wide' }), visible: enabled.has(id) })), preset); }; const dashboardWidget = item => { const sourceLocal = tx('本地 RoomGrid 使用', 'Local RoomGrid usage', 'Lokale RoomGrid-Nutzung'); let node = null; if (item.id === 'usage') node = stat(widgetLabels.usage, fmtDuration(usageTime)); else if (item.id === 'camera') node = stat(widgetLabels.camera, fmtDuration(camTime)); else if (item.id === 'activeDays') node = stat(widgetLabels.activeDays, String(activeDays)); else if (item.id === 'comparison') node = stat(widgetLabels.comparison, comparison(usageTime, previousUsage), usageTime >= previousUsage ? 'rgcc-good' : 'rgcc-warn'); else if (item.id === 'models') node = stat(widgetLabels.models, String(models.length)); else if (item.id === 'trends') node = $('section', { class: 'rgcc-card' }, [ $('h3', {}, widgetLabels.trends), $('div', { class: 'rgcc-grid rgcc-widget-inner' }, [[7, tx('7天', '7 days', '7 Tage')], [30, tx('30天', '30 days', '30 Tage')], [90, tx('90天', '90 days', '90 Tage')]].map(([days, label]) => stat(label, fmtDuration(usageForDays(days))))), ]); else if (item.id === 'daily') node = $('section', { class: 'rgcc-card' }, [$('h3', {}, widgetLabels.daily), daily.length ? barChart(daily) : $('p', {}, tx('尚无足够数据。', 'Not enough data yet.', 'Noch nicht genug Daten.'))]); else if (item.id === 'heatmap') node = $('section', { class: 'rgcc-card' }, [ $('h3', {}, widgetLabels.heatmap), $('div', { class: 'rgcc-heat' }, heat.map((value, index) => $('i', { title: `${['So','Mo','Di','Mi','Do','Fr','Sa'][Math.floor(index / 24)]} ${String(index % 24).padStart(2, '0')}:00 · ${fmtDuration(value)}`, style: { background: `rgba(var(--rg-accent-rgb,37,99,235),${0.06 + value / maxHeat * .94})` } }))), ]); else if (item.id === 'glance') node = $('section', { class: 'rgcc-card' }, [$('h3', {}, widgetLabels.glance), progressList([[tx('重复访问的模型', 'Models revisited', 'Wiederholt besuchte Models'), repeatModels, models.length, `${repeatModels}/${models.length}`], [tx('预览打开', 'Previews opened', 'Geöffnete Vorschauen'), eventKinds[0][1], Math.max(1, ...eventKinds.map(entry => entry[1]))], [tx('快速信息', 'Quick Views', 'Quick Views'), eventKinds[1][1], Math.max(1, ...eventKinds.map(entry => entry[1]))]])]); else if (item.id === 'quality') node = $('section', { class: 'rgcc-card' }, [ $('h3', {}, widgetLabels.quality), $('div', { class: 'rgcc-grid rgcc-widget-inner' }, [stat(tx('样本', 'Samples', 'Samples'), String(quality.length)), stat(tx('平均启动时间', 'Average startup', 'Ø Startzeit'), `${Math.round(avgStartup)} ms`), stat(tx('掉帧率', 'Dropped frames', 'Verlorene Frames'), `${dropRate.toFixed(2)}%`), stat(tx('卡顿', 'Stalls', 'Pufferstopps'), String(totals.stalls))]), ]); else if (item.id === 'privacy') node = $('section', { class: 'rgcc-card' }, [ $('h3', {}, widgetLabels.privacy), $('div', { class: 'rgcc-row' }, [provenanceChip(summary.privateSession ? tx('私人会话', 'Private session', 'Private Sitzung') : summary.historyPaused ? tx('历史已暂停', 'History paused', 'Verlauf pausiert') : analytics.isEnabled() ? tx('记录中', 'Recording locally', 'Lokale Erfassung') : tx('已关闭', 'Disabled', 'Ausgeschaltet'), tx('没有云同步或遥测。', 'No cloud sync or telemetry.', 'Keine Cloud-Synchronisierung oder Telemetrie.')), $('span', { class: 'rgcc-muted' }, `${tx('原始事件', 'Raw events', 'Rohereignisse')}: ${summary.retentionPolicy?.eventsDays || summary.retentionDays} ${tx('天', 'days', 'Tage')}`)]), ]); else if (item.id === 'provenance') node = $('section', { class: 'rgcc-card' }, [$('h3', {}, widgetLabels.provenance), $('div', { class: 'rgcc-provenance-list' }, inventory.sources.map(entry => $('div', {}, [$('strong', {}, widgetLabels[entry.id] || statisticsSourceTitle(entry.id)), provenanceChip(provenanceName(entry.source), provenanceName(entry.source)), $('span', { class: 'rgcc-muted' }, String(entry.count))])))]) ; if (!node) return null; node.classList.add('rgcc-dashboard-widget', `size-${item.size || 'wide'}`); node.dataset.widgetId = item.id; if (!node.querySelector('[data-roomgrid-provenance]')) node.appendChild(provenanceChip(sourceLocal, tx('仅来自此浏览器中的本地观察和操作。', 'Only local observations and actions from this browser.', 'Nur lokale Beobachtungen und Aktionen aus diesem Browser.'))); return node; }; const ranges = [['day', tx('今天', 'Today', 'Heute')], ['week', tx('周', 'Week', 'Woche')], ['month', tx('月', 'Month', 'Monat')], ['year', tx('年', 'Year', 'Jahr')], ['all', tx('全部', 'All', 'Alles')]]; const sections = [['overview', tx('概览', 'Overview', 'Übersicht')], ['time', tx('时间', 'Time', 'Zeit')], ['models', tx('模型', 'Models', 'Models')], ['planner', tx('在线计划', 'Online planner', 'Online-Planer')], ['discover', tx('发现', 'Discover', 'Entdecken')], ['workspaces', tx('工作区', 'Workspaces', 'Arbeitsbereiche')], ['quality', tx('质量', 'Quality', 'Qualität')], ['data', tx('数据与隐私', 'Data & privacy', 'Daten & Privatsphäre')]]; const usageForDays = days => { const cutoff = Date.now() - days * 86400000; return Object.entries(summary.usageDays || {}).filter(([day]) => new Date(`${day}T23:59:59`).getTime() >= cutoff).reduce((sum, [, value]) => sum + Number(value || 0), 0); }; const planner = section === 'planner' ? await prepareOnlinePlanner(models, revision) : []; if (planner === null || revision !== renderRevision || tab !== 'analytics') return; const content = []; if (section === 'overview') { const moveWidget = (index, delta) => { const next = dashboardWidgets.map(item => ({ ...item })); const target = index + delta; if (target < 0 || target >= next.length) return; [next[index], next[target]] = [next[target], next[index]]; saveDashboardWidgets(next); }; const dashboardEditor = $('details', { class: 'rgcc-card rgcc-section', dataset: { analyticsDashboardEditor: '1' } }, [ $('summary', {}, tx('配置统计仪表板', 'Configure statistics dashboard', 'Statistik-Dashboard konfigurieren')), $('div', { class: 'rgcc-row' }, [field(tx('预设', 'Preset', 'Voreinstellung'), select(store.state.settings.analyticsDashboardPreset || 'balanced', [['balanced', tx('平衡', 'Balanced', 'Ausgewogen')], ['minimal', tx('简约', 'Minimal', 'Minimal')], ['quality', tx('质量重点', 'Quality focus', 'Qualitätsfokus')], ['privacy', tx('隐私重点', 'Privacy focus', 'Privatsphäre-Fokus')], ['custom', tx('自定义', 'Custom', 'Benutzerdefiniert')]], value => value !== 'custom' && applyDashboardPreset(value))), btn(tx('重置仪表板', 'Reset dashboard', 'Dashboard zurücksetzen'), () => applyDashboardPreset('balanced'))]), $('div', { class: 'rgcc-widget-editor' }, dashboardWidgets.map((item, index) => $('div', { class: 'rgcc-item' }, [ check(widgetLabels[item.id] || item.id, item.visible, value => { const next = dashboardWidgets.map(entry => entry.id === item.id ? { ...entry, visible: value } : entry); saveDashboardWidgets(next); }), field(tx('大小', 'Size', 'Größe'), select(item.size || 'wide', [['small', tx('小', 'Small', 'Klein')], ['wide', tx('宽', 'Wide', 'Breit')], ['full', tx('全宽', 'Full width', 'Volle Breite')]], value => { const next = dashboardWidgets.map(entry => entry.id === item.id ? { ...entry, size: value } : entry); saveDashboardWidgets(next); })), $('span', { class: 'rgcc-actions' }, [btn('↑', () => moveWidget(index, -1), index ? '' : 'disabled'), btn('↓', () => moveWidget(index, 1), index < dashboardWidgets.length - 1 ? '' : 'disabled')]), ]))), $('p', { class: 'rgcc-note' }, tx('顺序、可见性和宽度会保存在更新安全设置中。每个组件都会显示其数据来源。', 'Order, visibility, and width are stored in update-safe settings. Every widget shows its data source.', 'Reihenfolge, Sichtbarkeit und Breite werden updatesicher gespeichert. Jedes Widget zeigt seine Datenherkunft.')), ]); const renderedWidgets = dashboardWidgets.filter(item => item.visible).map(dashboardWidget).filter(Boolean); content.push( dashboardEditor, renderedWidgets.length ? $('div', { class: 'rgcc-analytics-dashboard rgcc-section', dataset: { analyticsDashboard: '1' } }, renderedWidgets) : $('section', { class: 'rgcc-card rgcc-section' }, [tx('没有可见的仪表板组件。打开配置并选择至少一个组件。', 'No dashboard widgets are visible. Open configuration and select at least one widget.', 'Keine Dashboard-Widgets sind sichtbar. Öffne die Konfiguration und wähle mindestens ein Widget.')]), ); } else if (section === 'time') { const maxSessions = Math.max(1, ...sessionBuckets.map(item => item[1])); content.push( $('div', { class: 'rgcc-grid' }, [stat(tx('会话', 'Sessions', 'Sitzungen'), String(visits.length)), stat(tx('平均会话', 'Average session', 'Ø Sitzung'), fmtDuration(visits.length ? camTime / visits.length : 0)), stat(tx('中位会话', 'Median session', 'Median Sitzung'), fmtDuration(median)), stat(tx('活跃天数', 'Active days', 'Aktive Tage'), String(activeDays))]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('每日使用趋势', 'Daily usage trend', 'Täglicher Nutzungstrend')), daily.length ? barChart(daily) : $('p', {}, tx('开始使用后会在这里显示趋势。', 'Your trend appears here after you start using RoomGrid.', 'Nach der Nutzung erscheint hier dein Trend.'))]), $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('会话时长分布', 'Session-duration distribution', 'Verteilung der Sitzungsdauer')), visits.length ? progressList(sessionBuckets.map(([label, value]) => [label, value, maxSessions, String(value)])) : $('p', {}, tx('尚无会话时长数据。', 'No session-duration data yet.', 'Noch keine Daten zur Sitzungsdauer.'))]), ]), ); } else if (section === 'models') { const compared = [...new Set(String(body.dataset.compareModels || '').split(',').map(normalizeUsername).filter(isLikelyUsername))].slice(0, 4); const toggleCompare = id => { const next = compared.includes(id) ? compared.filter(value => value !== id) : [...compared, id].slice(-4); body.dataset.compareModels = next.join(','); render(); }; content.push( $('div', { class: 'rgcc-grid' }, [stat(tx('已知模型', 'Known models', 'Bekannte Models'), String(models.length)), stat(tx('重复访问率', 'Return rate', 'Wiederkehrrate'), `${models.length ? Math.round(repeatModels / models.length * 100) : 0}%`), stat(tx('总会话', 'Total sessions', 'Sitzungen gesamt'), String(models.reduce((sum, model) => sum + Number(model.sessions || 0), 0))), stat(tx('最长观察', 'Longest observed', 'Längste Beobachtung'), fmtDuration(ranked[0]?.totalMs || 0))]), compared.length ? $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, `${tx('模型比较', 'Model comparison', 'Model-Vergleich')} · ${compared.length}/4`), $('div', { class: 'rgcc-grid' }, compared.map(id => { const model = models.find(item => item.id === id); return stat(id, model ? `${fmtDuration(model.totalMs)} · ${model.sessions || 0}×` : '—'); }))]) : null, $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('模型排名', 'Model ranking', 'Model-Rangliste')), $('p', {}, tx('最多选择四个模型进行本地对比。', 'Select up to four models for a local comparison.', 'Wähle bis zu vier Models für einen lokalen Vergleich.')), $('div', { class: 'rgcc-list' }, ranked.length ? ranked.map((model, index) => $('div', { class: 'rgcc-item' }, [$('strong', {}, `${index + 1}. ${model.id}`), $('span', {}, `${fmtDuration(model.totalMs)} · ${model.sessions || 0}× · Ø ${fmtDuration(model.sessions ? model.totalMs / model.sessions : 0)}`), $('span', { class: 'rgcc-actions' }, [btn(compared.includes(model.id) ? tx('移除比较', 'Remove', 'Entfernen') : tx('比较', 'Compare', 'Vergleichen'), () => toggleCompare(model.id), compared.includes(model.id) ? 'primary' : ''), btn(tx('快速信息', 'Quick View', 'Quick View'), () => RoomGridUltimate.openInspector(model.id))])])) : $('p', {}, tx('尚无模型数据。', 'No model data yet.', 'Noch keine Modeldaten.')))]), ); } else if (section === 'planner') { const dayNames = [tx('周日', 'Sun', 'So'), tx('周一', 'Mon', 'Mo'), tx('周二', 'Tue', 'Di'), tx('周三', 'Wed', 'Mi'), tx('周四', 'Thu', 'Do'), tx('周五', 'Fri', 'Fr'), tx('周六', 'Sat', 'Sa')]; content.push( $('div', { class: 'rgcc-grid' }, [stat(tx('有时间模式的模型', 'Models with time patterns', 'Models mit Zeitmustern'), String(planner.length)), stat(tx('高可信度', 'High confidence', 'Hohe Sicherheit'), String(planner.filter(item => item.samples >= 36).length)), stat(tx('中等可信度', 'Moderate confidence', 'Mittlere Sicherheit'), String(planner.filter(item => item.samples >= 12 && item.samples < 36).length)), stat(tx('计划范围', 'Planning horizon', 'Planungshorizont'), '48 h')]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('预计下一次活跃时间', 'Likely next active times', 'Voraussichtlich nächste aktive Zeiten')), $('p', {}, tx('从你本地观察到的在线样本计算,不保证实际上线。数据越多,可信度越高。', 'Calculated from locally observed online samples; it does not guarantee a broadcast. Confidence improves with more data.', 'Aus lokal beobachteten Online-Samples berechnet; ein Stream wird nicht garantiert. Mehr Daten erhöhen die Aussagekraft.')), $('div', { class: 'rgcc-list' }, planner.length ? planner.map(item => { const date = new Date(Date.now() + item.best.offset * 3600000); const confidence = item.samples >= 36 ? tx('高', 'High', 'Hoch') : item.samples >= 12 ? tx('中', 'Medium', 'Mittel') : tx('低', 'Low', 'Niedrig'); return $('div', { class: 'rgcc-item' }, [$('strong', {}, item.model.id), $('span', {}, `${dayNames[date.getDay()]} ${String(date.getHours()).padStart(2, '0')}:00 · ${confidence} · ${item.samples} ${tx('样本', 'samples', 'Samples')}`), $('span', { class: 'rgcc-actions' }, [btn(tx('快速信息', 'Quick View', 'Quick View'), () => RoomGridUltimate.openInspector(item.model.id)), btn(tx('打开房间', 'Open room', 'Raum öffnen'), () => openNoopener(roomGridSingleCamUrl(item.model.id, { source: 'planner' })))])]); }) : [$('p', {}, tx('在线观察数据还不够。随着资料库增长,计划会自动出现。', 'Not enough online observations yet. The planner appears automatically with the library.', 'Noch nicht genug Online-Beobachtungen. Der Planer füllt sich automatisch mit der Bibliothek.'))])]), ); } else if (section === 'discover') { const maximum = Math.max(1, ...eventKinds.map(item => item[1])); content.push($('div', { class: 'rgcc-grid' }, [stat(tx('预览', 'Previews', 'Vorschauen'), String(eventKinds.find(item => item[0] === 'preview')?.[1] || 0)), stat(tx('快速信息', 'Quick Views', 'Quick Views'), String(eventKinds.find(item => item[0] === 'quickview')?.[1] || 0)), stat(tx('访问', 'Visits', 'Besuche'), String(visits.length)), stat(tx('发现事件', 'Discovery events', 'Entdecken-Ereignisse'), String(eventKinds.find(item => item[0] === 'discovered')?.[1] || 0))]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('本地交互漏斗', 'Local interaction funnel', 'Lokaler Interaktions-Funnel')), progressList(eventKinds.filter(item => ['discovered','preview','quickview','visit'].includes(item[0])).map(([label, value]) => [label, value, maximum, String(value)])), $('p', { class: 'rgcc-note' }, tx('仅显示你在本设备上的操作;不跟踪聊天内容或付款。', 'Only your actions on this device are counted; chat content and payments are never tracked.', 'Gezählt werden nur deine Aktionen auf diesem Gerät; Chatinhalte und Zahlungen werden nie verfolgt.'))])); } else if (section === 'workspaces') { const workspaceCounts = new Map(); events.filter(event => event.type === 'workspace' || event.workspaceId).forEach(event => { const key = event.workspaceId || event.id || tx('未知', 'Unknown', 'Unbekannt'); workspaceCounts.set(key, (workspaceCounts.get(key) || 0) + 1); }); const workspaceRanking = [...workspaceCounts.entries()].sort((a, b) => b[1] - a[1]); content.push( $('div', { class: 'rgcc-grid' }, [stat(tx('工作区', 'Workspaces', 'Arbeitsbereiche'), String(store.state.workspaces.length)), stat(tx('工作区事件', 'Workspace events', 'Workspace-Ereignisse'), String(eventKinds.find(item => item[0] === 'workspace')?.[1] || 0)), stat(tx('筛选事件', 'Filter events', 'Filter-Ereignisse'), String(eventKinds.find(item => item[0] === 'filter')?.[1] || 0)), stat(tx('标签事件', 'Tag events', 'Tag-Ereignisse'), String(eventKinds.find(item => item[0] === 'tag')?.[1] || 0))]), workspaceRanking.length ? $('section', { class: 'rgcc-section' }, [$('h3', {}, tx('前四个工作区比较', 'Top-four workspace comparison', 'Vergleich der vier stärksten Workspaces')), $('div', { class: 'rgcc-grid' }, workspaceRanking.slice(0, 4).map(([id, value]) => stat(store.state.workspaces.find(item => item.id === id)?.name || id, String(value))))]) : null, $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('工作区使用', 'Workspace usage', 'Workspace-Nutzung')), workspaceRanking.length ? progressList(workspaceRanking.slice(0, 20).map(([label, value]) => [store.state.workspaces.find(item => item.id === label)?.name || label, value, Math.max(1, ...workspaceCounts.values()), String(value)])) : $('p', {}, tx('尚无工作区数据。', 'No workspace data yet.', 'Noch keine Workspace-Daten.'))]), ); } else if (section === 'quality') { const qualityByModel = new Map(); quality.forEach(sample => { const current = qualityByModel.get(sample.id || tx('未知', 'Unknown', 'Unbekannt')) || { count: 0, stalls: 0, dropped: 0, frames: 0 }; current.count += 1; current.stalls += sample.stalls; current.dropped += sample.droppedFrames; current.frames += sample.totalFrames; qualityByModel.set(sample.id || tx('未知', 'Unknown', 'Unbekannt'), current); }); content.push($('div', { class: 'rgcc-grid' }, [stat(tx('质量样本', 'Quality samples', 'Qualitätsproben'), String(quality.length)), stat(tx('平均启动时间', 'Average startup', 'Ø Startzeit'), `${Math.round(avgStartup)} ms`), stat(tx('掉帧率', 'Dropped frames', 'Verlorene Frames'), `${dropRate.toFixed(2)}%`, dropRate > 5 ? 'rgcc-danger' : dropRate > 1 ? 'rgcc-warn' : 'rgcc-good'), stat(tx('平均并发流', 'Average active streams', 'Ø aktive Streams'), avgStreams.toFixed(1))]), $('div', { class: 'rgcc-two rgcc-section' }, [stat(tx('卡顿', 'Stalls', 'Pufferstopps'), String(totals.stalls)), stat(tx('重新连接', 'Reconnects', 'Neu verbunden'), String(totals.reconnects))]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('按模型的播放质量', 'Playback quality by model', 'Wiedergabequalität je Model')), $('div', { class: 'rgcc-list' }, [...qualityByModel.entries()].sort((a,b) => b[1].stalls - a[1].stalls).slice(0, 20).map(([id, value]) => $('div', { class: 'rgcc-item' }, [$('strong', {}, id), $('span', {}, `${value.count} ${tx('样本', 'samples', 'Proben')} · ${value.stalls} ${tx('卡顿', 'stalls', 'Stopps')}`), $('span', { class: 'rgcc-muted' }, `${value.frames ? (value.dropped / value.frames * 100).toFixed(2) : '0.00'}%`)])) || $('p', {}, tx('尚无质量数据。', 'No quality data yet.', 'Noch keine Qualitätsdaten.')))])); } else { const importInput = $('input', { type: 'file', accept: 'application/json', style: { display: 'none' }, onchange: async event => { const file = event.target.files?.[0]; if (!file) return; try { if (file.size > 40 * 1024 * 1024) throw new Error('large'); analyticsImportReview = analytics.inspectImport(JSON.parse(await file.text())); analyticsImportSections.clear(); analyticsImportReview.sections.forEach(key => analyticsImportSections.add(key)); render(); } catch (_) { analyticsImportReview = null; analyticsImportSections.clear(); alert(tx('统计文件无效或超过安全限制。当前数据未更改。', 'The statistics file is invalid or exceeds safe limits. Current data was not changed.', 'Die Statistikdatei ist ungültig oder überschreitet sichere Grenzen. Aktuelle Daten wurden nicht verändert.')); } } }); const exportCsv = () => { const quote = value => `"${String(value ?? '').replace(/"/gu, '""')}"`; const rows = [['model','watch_ms','sessions','first_seen','last_seen'], ...analytics.getModelsSync().map(model => [model.id, model.totalMs, model.sessions, model.firstWatchedAt, model.lastWatchedAt])]; downloadBlob(new Blob(['\ufeff' + rows.map(row => row.map(quote).join(',')).join('\n')], { type: 'text/csv;charset=utf-8' }), `roomgrid-statistics-${stampForFile()}.csv`); }; const exportPseudonymous = async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(pseudonymizeStatistics(data), null, 2)], { type: 'application/json' }), `roomgrid-pseudonymous-statistics-${stampForFile()}.json`); }; const statisticsSectionLabels = { models: tx('模型摘要', 'Model summaries', 'Model-Zusammenfassungen'), profiles: tx('公共资料', 'Public profiles', 'Öffentliche Profile'), events: tx('使用事件', 'Usage events', 'Nutzungsereignisse'), qualitySamples: tx('质量样本', 'Quality samples', 'Qualitätsproben'), accountSnapshots: tx('官方账户快照', 'Official account snapshots', 'Offizielle Konto-Snapshots'), dailyRollups: tx('每日汇总', 'Daily rollups', 'Tageszusammenfassungen'), }; const importPreview = analyticsImportReview ? $('section', { class: 'rgcc-card rgcc-section', dataset: { analyticsImportPreview: '1' } }, [ $('h3', {}, tx('统计导入助手 · 预览', 'Statistics import assistant · preview', 'Statistik-Importassistent · Vorschau')), $('p', {}, tx('尚未更改任何数据。选择要合并的区域并检查冲突。', 'Nothing has changed yet. Select the sections to import and review conflicts.', 'Noch wurden keine Daten geändert. Wähle die zu importierenden Bereiche und prüfe Konflikte.')), $('div', { class: 'rgcc-grid' }, analyticsImportReview.sections.map(key => stat(statisticsSectionLabels[key] || key, `${analyticsImportReview.counts[key] || 0} · ${analyticsImportReview.conflicts[key] || 0} ${tx('冲突', 'conflicts', 'Konflikte')}`))), $('div', { class: 'rgcc-row' }, analyticsImportReview.sections.map(key => check(statisticsSectionLabels[key] || key, analyticsImportSections.has(key), value => { if (value) analyticsImportSections.add(key); else analyticsImportSections.delete(key); }))), $('div', { class: 'rgcc-row' }, [ field(tx('导入方式', 'Import method', 'Importart'), select(analyticsImportStrategy, [['merge', tx('安全合并(推荐)', 'Safe merge (recommended)', 'Sicher zusammenführen (empfohlen)')], ['replace', tx('替换所选区域', 'Replace selected sections', 'Gewählte Bereiche ersetzen')]], value => { analyticsImportStrategy = value; })), btn(tx('应用导入', 'Apply import', 'Import anwenden'), async () => { try { await analytics.importData(analyticsImportReview, { strategy: analyticsImportStrategy, sections: [...analyticsImportSections] }); analyticsImportReview = null; analyticsImportSections.clear(); alert(tx('导入完成。之前的统计已自动备份。', 'Import complete. The previous statistics were backed up automatically.', 'Import abgeschlossen. Die vorherige Statistik wurde automatisch gesichert.')); render(); } catch (_) { alert(tx('无法安全应用导入。当前数据和备份保持不变。', 'The import could not be applied safely. Current data and backups remain unchanged.', 'Der Import konnte nicht sicher angewendet werden. Aktuelle Daten und Sicherungen bleiben unverändert.')); } }, 'primary'), btn(tx('取消预览', 'Cancel preview', 'Vorschau abbrechen'), () => { analyticsImportReview = null; analyticsImportSections.clear(); render(); }), ]), $('p', { class: 'rgcc-note' }, `${tx('文件架构', 'File schema', 'Dateischema')}: ${analyticsImportReview.schema || '—'} · ${tx('时间范围', 'Date range', 'Zeitraum')}: ${analyticsImportReview.earliestAt ? new Date(analyticsImportReview.earliestAt).toLocaleDateString() : '—'} – ${analyticsImportReview.latestAt ? new Date(analyticsImportReview.latestAt).toLocaleDateString() : '—'} · ${analyticsImportReview.sensitive ? tx('敏感数据', 'Sensitive data', 'Sensible Daten') : tx('无敏感标记', 'Not marked sensitive', 'Nicht als sensibel markiert')}`), ]) : null; const untilLabel = value => Number(value) === -1 ? tx('直到手动恢复', 'Until manually resumed', 'Bis zur manuellen Fortsetzung') : Number(value) > Date.now() ? new Date(Number(value)).toLocaleString() : tx('未暂停', 'Not paused', 'Nicht pausiert'); const retention = summary.retentionPolicy || { eventsDays: summary.retentionDays, qualityDays: summary.retentionDays, accountDays: summary.retentionDays, rollupDays: 3650 }; const setRetention = (key, value) => analytics.setRetentionPolicy({ ...retention, [key]: value }).then(() => render()); const backupRows = analyticsBackups.map(item => $('div', { class: 'rgcc-item' }, [ $('strong', {}, new Date(item.at).toLocaleString()), $('span', {}, `${item.reason} · ${item.counts.models} ${tx('模型', 'models', 'Models')} · ${item.counts.events} ${tx('事件', 'events', 'Ereignisse')}`), $('span', { class: 'rgcc-actions' }, [btn(tx('恢复', 'Restore', 'Wiederherstellen'), async () => { if (confirm(tx('恢复此统计备份?当前状态会先另行备份。', 'Restore this statistics backup? The current state will be backed up first.', 'Diese Statistik-Sicherung wiederherstellen? Der aktuelle Stand wird vorher separat gesichert.'))) { await analytics.restoreBackup(item.backupId); render(); } }, 'primary'), btn(tx('删除', 'Delete', 'Löschen'), async () => { if (confirm(tx('永久删除此备份?', 'Permanently delete this backup?', 'Diese Sicherung dauerhaft löschen?'))) { await analytics.deleteBackup(item.backupId); render(); } }, 'danger')]), ])); content.push( $('div', { class: 'rgcc-grid' }, [stat(tx('事件', 'Events', 'Ereignisse'), String(inventory.events)), stat(tx('模型', 'Models', 'Models'), String(inventory.models)), stat(tx('质量样本', 'Quality samples', 'Qualitätsproben'), String(inventory.qualitySamples)), stat(tx('统计存储估计', 'Estimated statistics storage', 'Geschätzter Statistik-Speicher'), fmtBytes(inventory.estimatedBytes))]), $('section', { class: 'rgcc-card rgcc-section', dataset: { analyticsPrivacyControls: '1' } }, [ $('h3', {}, tx('记录、暂停和私人会话', 'Recording, pauses and private sessions', 'Erfassung, Pausen und private Sitzungen')), check(tx('记录使用统计', 'Record usage statistics', 'Nutzungsstatistik erfassen'), analytics.isEnabled(), value => { store.patchSettings({ analyticsEnabled: value }); analytics.setEnabled(value); render(); }), $('div', { class: 'rgcc-row' }, [ btn(tx('暂停 15 分钟', 'Pause 15 minutes', '15 Minuten pausieren'), () => analytics.setHistoryPause(15).then(render)), btn(tx('暂停 1 小时', 'Pause 1 hour', '1 Stunde pausieren'), () => analytics.setHistoryPause(60).then(render)), btn(tx('暂停 8 小时', 'Pause 8 hours', '8 Stunden pausieren'), () => analytics.setHistoryPause(480).then(render)), btn(tx('无限期暂停', 'Pause indefinitely', 'Unbegrenzt pausieren'), () => analytics.setHistoryPause(-1).then(render)), summary.historyPaused ? btn(tx('继续记录', 'Resume history', 'Verlauf fortsetzen'), () => analytics.setHistoryPause(0).then(render), 'primary') : null, ]), $('p', { class: 'rgcc-note' }, `${tx('历史状态', 'History status', 'Verlaufsstatus')}: ${untilLabel(summary.historyPausedUntil)}`), $('div', { class: 'rgcc-row' }, [ btn(tx('私人 30 分钟', 'Private for 30 minutes', '30 Minuten privat'), () => analytics.setPrivateSession(true, 30).then(render)), btn(tx('私人 2 小时', 'Private for 2 hours', '2 Stunden privat'), () => analytics.setPrivateSession(true, 120).then(render)), btn(tx('私人直到恢复', 'Private until resumed', 'Privat bis zur Fortsetzung'), () => analytics.setPrivateSession(true).then(render)), summary.privateSession ? btn(tx('结束私人会话', 'End private session', 'Private Sitzung beenden'), () => analytics.setPrivateSession(false).then(render), 'primary') : null, ]), $('p', {}, `${tx('私人会话', 'Private session', 'Private Sitzung')}: ${summary.privateSession ? untilLabel(summary.privateSessionUntil) : tx('未启用', 'Not active', 'Nicht aktiv')}. ${tx('私人会话也会停止公共模型观察;普通历史暂停只停止个人使用和质量记录。', 'A private session also stops public model observations; a normal history pause stops personal usage and quality records.', 'Eine private Sitzung stoppt auch öffentliche Model-Beobachtungen; eine normale Verlaufspause stoppt persönliche Nutzungs- und Qualitätsdaten.')}`), ]), $('section', { class: 'rgcc-card rgcc-section', dataset: { analyticsRetention: '1' } }, [ $('h3', {}, tx('自动删除和保留', 'Automatic deletion and retention', 'Automatische Löschung und Aufbewahrung')), $('div', { class: 'rgcc-grid' }, [ field(tx('原始事件(天)', 'Raw events (days)', 'Rohereignisse (Tage)'), input('number', retention.eventsDays, value => setRetention('eventsDays', value), { min: 7, max: 3650 })), field(tx('质量样本(天)', 'Quality samples (days)', 'Qualitätsproben (Tage)'), input('number', retention.qualityDays, value => setRetention('qualityDays', value), { min: 7, max: 3650 })), field(tx('账户快照(天)', 'Account snapshots (days)', 'Konto-Snapshots (Tage)'), input('number', retention.accountDays, value => setRetention('accountDays', value), { min: 7, max: 3650 })), field(tx('每日汇总(天)', 'Daily rollups (days)', 'Tageswerte (Tage)'), input('number', retention.rollupDays, value => setRetention('rollupDays', value), { min: 30, max: 3650 })), ]), $('p', {}, tx('过期的原始事件会先汇总为每日数值,然后自动删除。质量和账户快照按各自期限删除。', 'Expired raw events are rolled into daily totals before automatic deletion. Quality and account snapshots use their own limits.', 'Abgelaufene Rohereignisse werden vor der automatischen Löschung zu Tageswerten verdichtet. Qualitäts- und Konto-Snapshots besitzen eigene Fristen.')), ]), $('section', { class: 'rgcc-card rgcc-section', dataset: { analyticsBackupCenter: '1' } }, [ $('h3', {}, tx('统计备份中心', 'Statistics backup center', 'Statistik-Sicherungszentrale')), $('div', { class: 'rgcc-row' }, [ btn(tx('立即创建内部备份', 'Create internal backup now', 'Jetzt interne Sicherung erstellen'), async () => { await analytics.createBackup('manual'); render(); }, 'primary'), btn(tx('导出敏感统计', 'Export sensitive statistics', 'Sensible Statistik exportieren'), async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `roomgrid-sensitive-statistics-${stampForFile()}.json`); }), btn('CSV ' + tx('导出', 'export', 'exportieren'), exportCsv), btn(tx('匿名化导出', 'Pseudonymized export', 'Pseudonymisierter Export'), exportPseudonymous), btn(tx('打印报告', 'Print report', 'Bericht drucken'), () => window.print()), btn(tx('选择导入文件', 'Choose import file', 'Importdatei wählen'), () => { document.body.appendChild(importInput); importInput.click(); }), ]), $('p', { class: 'rgcc-note' }, tx('假名导出仅包含别名、时间和统计值,不包含姓名、文本、标签、链接、资料或账户。时间模式仍可能具有识别性,因此不是匿名数据。', 'Pseudonymized exports contain aliases, times and statistical values only, without names, text, tags, links, profiles or accounts. Time patterns may still be identifiable; this is not anonymous data.', 'Pseudonymisierte Exporte enthalten nur Alias-IDs, Zeiten und Statistikwerte, keine Namen, Texte, Tags, Links, Profile oder Kontodaten. Zeitmuster können weiterhin identifizierbar sein; die Daten sind nicht anonym.')), $('div', { class: 'rgcc-list' }, backupRows.length ? backupRows : [$('p', { class: 'rgcc-muted' }, tx('尚无内部统计备份。导入、恢复或手动创建时最多保留三个。', 'No internal statistics backups yet. Up to three are kept after imports, restores, or manual creation.', 'Noch keine internen Statistik-Sicherungen. Nach Importen, Wiederherstellungen oder manueller Erstellung werden bis zu drei aufbewahrt.'))]), $('div', { class: 'rgcc-row' }, [ btn(tx('删除所选范围', 'Delete selected range', 'Gewählten Zeitraum löschen'), async () => { if (confirm(tx('删除此范围内的本地统计?删除前会创建备份。', 'Delete local statistics in this range? A backup will be created first.', 'Lokale Statistik in diesem Zeitraum löschen? Zuvor wird eine Sicherung erstellt.'))) { await analytics.createBackup('before-range-delete'); await analytics.deleteRange(from, Date.now()); render(); } }, 'danger'), btn(tx('清除全部', 'Clear everything', 'Alles löschen'), async () => { if (confirm(tx('永久清除所有本地统计?删除前会创建备份。', 'Permanently clear all local statistics? A backup will be created first.', 'Alle lokalen Statistiken dauerhaft löschen? Zuvor wird eine Sicherung erstellt.'))) { await analytics.createBackup('before-clear'); await analytics.clearAll(); render(); } }, 'danger'), ]), ]), importPreview, $('section', { class: 'rgcc-card rgcc-section', dataset: { analyticsProvenance: '1' } }, [ $('h3', {}, tx('数据清单和来源', 'Data inventory and provenance', 'Datenbestand und Herkunft')), $('div', { class: 'rgcc-list' }, inventory.sources.map(entry => $('div', { class: 'rgcc-item' }, [$('strong', {}, statisticsSectionLabels[entry.id] || statisticsSourceTitle(entry.id)), $('span', {}, tx('仅存储在此浏览器中', 'Stored only in this browser', 'Nur in diesem Browser gespeichert')), $('span', { class: 'rgcc-actions' }, [provenanceChip(provenanceName(entry.source), provenanceName(entry.source)), $('span', { class: 'rgcc-muted' }, String(entry.count))])]))), $('p', {}, `${tx('覆盖范围', 'Coverage', 'Abdeckung')}: ${inventory.earliestAt ? new Date(inventory.earliestAt).toLocaleString() : '—'} – ${inventory.latestAt ? new Date(inventory.latestAt).toLocaleString() : '—'}. ${tx('未知或缺失的数据不会被推断。', 'Unknown or missing data is never inferred.', 'Unbekannte oder fehlende Daten werden nicht erfunden.')}`), ]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('指标定义与数据质量', 'Metric definitions & data quality', 'Messwert-Definitionen & Datenqualität')), $('div', { class: 'rgcc-list' }, [ $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('使用时间', 'Usage time', 'Nutzungszeit')), $('span', {}, tx('RoomGrid 页面处于活动状态时本地累计。', 'Accumulated locally while a RoomGrid page is active.', 'Wird lokal summiert, solange eine RoomGrid-Seite aktiv ist.'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('摄像头时间', 'Camera time', 'Cam-Zeit')), $('span', {}, tx('所有观察到的摄像头会话时长之和;并行播放会相加。', 'Sum of all observed camera sessions; concurrent streams are added.', 'Summe aller beobachteten Cam-Sitzungen; parallele Streams werden addiert.'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('播放质量', 'Playback quality', 'Wiedergabequalität')), $('span', {}, `${quality.length} ${tx('个浏览器样本;不插值。', 'browser samples; no interpolation.', 'Browser-Proben; keine Hochrechnung.')}`)]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('隐私边界', 'Privacy boundary', 'Privatsphäre-Grenze')), $('span', {}, tx('不保存聊天内容、支付内容或媒体文件。私人会话不会添加事件。', 'No chat content, payments, or media files are saved. Private sessions add no events.', 'Chatinhalte, Zahlungen und Mediendateien werden nicht gespeichert. Private Sitzungen erzeugen keine Ereignisse.'))]), ])]), ); } body.replaceChildren( $('div', { class: 'rgcc-toolbar' }, [$('div', { class: 'rgcc-row' }, [field(tx('时间范围', 'Range', 'Zeitraum'), select(range, ranges, value => { body.dataset.range = value; render(); })), $('span', { class: 'rgcc-chip' }, `${events.length} ${tx('本地事件', 'local events', 'lokale Ereignisse')}`)]), $('div', { class: 'rgcc-subtabs' }, sections.map(([id, label]) => btn(label, () => { body.dataset.analyticsSection = id; render(); }, id === section ? 'primary' : '')))]), ...content.filter(Boolean), ); } async function renderBroadcaster(revision) { await broadcasterStats.init(); const state = broadcasterStats.status(); const snapshots = await analytics.getAccountSnapshots({ username: state.username, limit: 1000 }); if (revision !== renderRevision || tab !== 'broadcaster') return; const latest = state.latest || snapshots[snapshots.length - 1] || null; const trend = snapshots.slice(-24); const previous = trend.length > 1 ? trend[trend.length - 2] : null; const deltaText = key => { if (!latest || !previous) return ''; const difference = (Number(latest[key]) || 0) - (Number(previous[key]) || 0); return difference ? ` (${difference > 0 ? '+' : ''}${difference})` : ' (±0)'; }; const spark = (key, title) => { const max = Math.max(1, ...trend.map(item => Number(item[key]) || 0)); return $('section', { class: 'rgcc-card' }, [$('h3', {}, title), $('div', { class: 'rgcc-spark' }, trend.length ? trend.map(item => $('i', { title: `${new Date(item.at).toLocaleString()} · ${item[key]}`, style: { height: `${Math.max(4, (Number(item[key]) || 0) / max * 100)}%` } })) : $('span', { class: 'rgcc-muted' }, tx('尚无历史', 'No history yet', 'Noch kein Verlauf')))]); }; const statusText = state.loading ? tx('加载中', 'Loading', 'Lädt') : state.unlocked ? tx('已连接', 'Connected', 'Verbunden') : state.locked ? tx('已锁定', 'Locked', 'Gesperrt') : tx('未连接', 'Not connected', 'Nicht verbunden'); const setup = () => { const username = input('text', state.username, () => {}, { placeholder: tx('用户名', 'Username', 'Benutzername'), autocomplete: 'username', maxlength: 80 }); const token = input('password', '', () => {}, { placeholder: tx('24 位统计令牌', '24-character stats token', '24-stelliges Statistik-Token'), autocomplete: 'off', maxlength: 24, class: 'rgcc-input rgcc-secret' }); const passphrase = input('password', '', () => {}, { placeholder: tx('至少 8 个字符', 'At least 8 characters', 'Mindestens 8 Zeichen'), autocomplete: 'new-password', minlength: 8 }); return $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('安全连接官方统计 API', 'Secure official statistics connection', 'Sichere Verbindung zur offiziellen Statistik-API')), $('p', {}, tx('可选功能。令牌不会进入普通设置、日志或导出。会话模式只保存在内存中;加密模式使用本地隔离存储。', 'Optional. The token never enters normal settings, logs, or exports. Session mode keeps it only in memory; encrypted mode uses isolated local storage.', 'Optional. Das Token gelangt nie in normale Einstellungen, Protokolle oder Exporte. Der Sitzungsmodus hält es nur im Arbeitsspeicher; der verschlüsselte Modus nutzt isolierten lokalen Speicher.')), $('div', { class: 'rgcc-row' }, [field(tx('用户名', 'Username', 'Benutzername'), username), field(tx('统计令牌', 'Stats token', 'Statistik-Token'), token), field(tx('Tresor-Passwort', 'Vault password', 'Tresor-Passwort'), passphrase)]), $('div', { class: 'rgcc-row' }, [btn(tx('仅此会话连接', 'Connect for this session', 'Nur für diese Sitzung verbinden'), async () => { try { await broadcasterStats.setSession(username.value, token.value); token.value = ''; await broadcasterStats.fetchNow(); render(); } catch (_) { alert(tx('用户名或令牌无效。', 'Username or token is invalid.', 'Benutzername oder Token ist ungültig.')); } }, 'primary'), btn(tx('加密保存并连接', 'Encrypt, save & connect', 'Verschlüsselt speichern & verbinden'), async () => { try { await broadcasterStats.saveEncrypted(username.value, token.value, passphrase.value); token.value = ''; passphrase.value = ''; await broadcasterStats.fetchNow(); render(); } catch (_) { alert(tx('无法加密保存。请检查令牌、密码和用户脚本管理器权限。', 'Could not save encrypted. Check the token, password and userscript-manager permissions.', 'Verschlüsseltes Speichern nicht möglich. Prüfe Token, Passwort und Berechtigungen des Userscript-Managers.')); } }), btn(tx('获取官方令牌', 'Get official token', 'Offizielles Token abrufen'), () => window.open('https://chaturbate.com/statsapi/authtoken/', '_blank', 'noopener,noreferrer'))]), $('p', { class: 'rgcc-note' }, tx('自动更新最多每 5 分钟一次。此模块仅使用你主动提供的官方令牌。', 'Automatic refresh runs at most once every five minutes. This module only uses an official token you provide deliberately.', 'Die automatische Aktualisierung läuft höchstens alle fünf Minuten. Dieses Modul nutzt nur ein bewusst von dir bereitgestelltes offizielles Token.'))]); }; const nodes = [$('div', { class: 'rgcc-grid' }, [stat(tx('连接状态', 'Connection', 'Verbindung'), statusText, state.unlocked ? 'rgcc-good' : state.lastError ? 'rgcc-danger' : ''), stat(tx('用户', 'Account', 'Konto'), state.username || '—'), stat(tx('历史快照', 'History snapshots', 'Verlaufs-Snapshots'), String(snapshots.length)), stat(tx('最后更新', 'Last refresh', 'Letzte Aktualisierung'), state.lastSuccessAt ? new Date(state.lastSuccessAt).toLocaleString() : '—')])]; if (state.locked) { const password = input('password', '', () => {}, { placeholder: tx('Tresor-Passwort', 'Vault password', 'Tresor-Passwort'), autocomplete: 'current-password' }); nodes.push($('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('解锁本地 Tresor', 'Unlock local vault', 'Lokalen Tresor entsperren')), $('div', { class: 'rgcc-row' }, [field(tx('密码', 'Password', 'Passwort'), password), btn(tx('解锁', 'Unlock', 'Entsperren'), async () => { try { await broadcasterStats.unlock(password.value); await broadcasterStats.fetchNow(); render(); } catch (_) { alert(tx('解锁失败。', 'Unlock failed.', 'Entsperren fehlgeschlagen.')); } }, 'primary')])])); } else if (!state.unlocked) nodes.push(setup()); if (state.unlocked) { nodes.push($('div', { class: 'rgcc-row rgcc-section' }, [btn(state.loading ? tx('加载中…', 'Loading…', 'Lädt…') : tx('立即刷新', 'Refresh now', 'Jetzt aktualisieren'), async () => { try { await broadcasterStats.fetchNow(); render(); } catch (_) { render(); } }, 'primary'), check(tx('每 5 分钟自动刷新', 'Auto-refresh every 5 minutes', 'Alle 5 Minuten automatisch aktualisieren'), state.autoRefresh, async value => { await broadcasterStats.patch({ autoRefresh: value }); render(); }), btn(tx('断开并删除令牌', 'Disconnect and delete token', 'Trennen und Token löschen'), async () => { if (confirm(tx('删除本地连接和加密令牌?历史统计保留。', 'Delete the local connection and encrypted token? Historical statistics remain.', 'Lokale Verbindung und verschlüsseltes Token löschen? Der Statistikverlauf bleibt erhalten.'))) { await broadcasterStats.clear(); render(); } }, 'danger')])); if (latest) nodes.push($('div', { class: 'rgcc-grid' }, [ stat(tx('直播状态', 'Broadcast status', 'Broadcast-Status'), latest.timeOnline > 0 ? tx('直播中', 'Live now', 'Jetzt live') : tx('离线', 'Offline', 'Offline'), latest.timeOnline > 0 ? 'rgcc-good' : ''), stat(tx('代币余额', 'Token balance', 'Token-Guthaben'), `${latest.tokenBalance}${deltaText('tokenBalance')}`), stat(tx('最近一小时小费', 'Tips last hour', 'Trinkgeld letzte Stunde'), `${latest.tipsLastHour}${deltaText('tipsLastHour')}`), stat(tx('关注者', 'Followers', 'Follower'), `${latest.followers}${deltaText('followers')}`), stat(tx('好评', 'Up votes', 'Positive Stimmen'), `${latest.votesUp}${deltaText('votesUp')}`), stat(tx('差评', 'Down votes', 'Negative Stimmen'), `${latest.votesDown}${deltaText('votesDown')}`), stat(tx('满意度', 'Satisfaction', 'Zufriedenheit'), latest.satisfactionScore >= 0 ? `${latest.satisfactionScore}%` : '—'), stat(tx('观众', 'Viewers', 'Zuschauer'), `${latest.viewers}${deltaText('viewers')}`), stat(tx('注册观众', 'Registered viewers', 'Registrierte Zuschauer'), String(latest.registeredViewers)), stat(tx('持币观众', 'Tokened viewers', 'Zuschauer mit Token'), String(latest.tokenedViewers)), stat(tx('在线时间', 'Time online', 'Onlinezeit'), latest.timeOnline >= 0 ? fmtDuration(latest.timeOnline * 1000) : '—'), stat(tx('上次直播', 'Last broadcast', 'Letzter Broadcast'), latest.lastBroadcast ? new Date(latest.lastBroadcast).toLocaleString() : '—'), ])); } if (trend.length) nodes.push($('div', { class: 'rgcc-grid rgcc-section' }, [spark('followers', tx('关注者趋势', 'Follower trend', 'Follower-Trend')), spark('tipsLastHour', tx('小费趋势', 'Tips trend', 'Trinkgeld-Trend')), spark('tokenBalance', tx('代币余额趋势', 'Token-balance trend', 'Token-Guthaben-Trend')), spark('viewers', tx('观众趋势', 'Viewer trend', 'Zuschauer-Trend'))])); nodes.push($('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('账户健康中心', 'Account Health Center', 'Account Health Center')), $('div', { class: 'rgcc-row' }, [btn(tx('官方 My Collection', 'Official My Collection', 'Offizielle My Collection'), () => window.open('https://chaturbate.com/my_collection/', '_blank', 'noopener,noreferrer')), btn(tx('官方支持', 'Official support', 'Offizieller Support'), () => window.open('https://support.chaturbate.com/', '_blank', 'noopener,noreferrer')), btn(tx('导出本地统计', 'Export local statistics', 'Lokale Statistik exportieren'), async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `roomgrid-account-statistics-${stampForFile()}.json`); }), snapshots.length ? btn(tx('删除快照历史', 'Delete snapshot history', 'Snapshot-Verlauf löschen'), async () => { if (confirm(tx('删除此账户的本地官方统计快照?', 'Delete the local official-statistics snapshots for this account?', 'Lokale Snapshots der offiziellen Statistik für dieses Konto löschen?'))) { await analytics.clearAccountSnapshots(state.username); render(); } }, 'danger') : null]), $('p', { class: 'rgcc-note' }, state.lastError ? `${tx('上次错误', 'Last error', 'Letzter Fehler')}: ${state.lastError}` : tx('令牌状态、更新节奏、本地历史和官方入口集中在此。RoomGrid 不会自动发布、发送消息或执行交易。', 'Token state, refresh cadence, local history and official links live here. RoomGrid never publishes, messages, or performs transactions automatically.', 'Token-Status, Aktualisierung, lokaler Verlauf und offizielle Links sind hier gebündelt. RoomGrid veröffentlicht nichts, sendet keine Nachrichten und führt keine Transaktionen automatisch aus.'))])); body.replaceChildren(...nodes); } function renderNotifications() { const state = notifications.getState(); const patch = next => { notifications.patch(next); render(); }; const modelIds = [...new Set([...store.state.rooms.map(room => room.id), ...Object.keys(state.models)])].sort(); const chosenModel = String(body.dataset.notificationModel || modelIds[0] || ''); const modelRule = state.models[chosenModel] || { mode: 'off', online: false, offline: false, profile: false, inactiveDays: 0 }; const chosenGroup = String(body.dataset.notificationGroup || store.state.groups[0]?.id || ''); const groupRule = state.groups[chosenGroup] || { mode: 'off', online: false, offline: false, profile: false, inactiveDays: 0 }; const browserDelivery = value => ['browser', 'both'].includes(value); const changeDelivery = async value => { if (state.master && browserDelivery(value) && !await Notify.request()) { alert(tx('浏览器未授予通知权限。已改用页面内通知。', 'The browser did not grant notification permission. In-page notifications remain active.', 'Der Browser hat keine Benachrichtigungsberechtigung erteilt. Meldungen in der Seite bleiben aktiv.', 'El navegador no concedió permiso. Las notificaciones en la página siguen activas.', 'O navegador não concedeu permissão. As notificações na página continuam ativas.')); patch({ delivery: 'inpage' }); return; } patch({ delivery: value }); }; const ruleEditor = (kind, id, rule) => $('div', { class: 'rgcc-row' }, [ field(tx('通知配置', 'Notification profile', 'Benachrichtigungsprofil'), select(rule.mode || 'off', [['off', tx('关闭', 'Off', 'Aus')], ['live', tx('仅上线', 'Go-live only', 'Nur Live-Start')], ['personalized', tx('个性化', 'Personalized', 'Personalisiert')], ['always', tx('始终(全部事件)', 'Always (all events)', 'Immer (alle Ereignisse)')]], value => { notifications[kind](id, { ...rule, mode: value }); render(); })), field(tx('未上线天数', 'Inactive days', 'Tage nicht online'), input('number', rule.inactiveDays, value => { notifications[kind](id, { ...rule, inactiveDays: value }); render(); }, { min: 0, max: 3650 })), $('span', { class: 'rgcc-muted' }, rule.mode === 'personalized' ? tx('收藏、评分、优先级和自己的标签决定是否重要。', 'Favorites, rating, priority and your tags decide relevance.', 'Favoriten, Bewertung, Priorität und eigene Tags bestimmen die Relevanz.') : rule.mode === 'always' ? tx('包括上线、离线和公开资料变化。', 'Includes go-live, offline and public-profile changes.', 'Umfasst Live-Start, Offline und Änderungen am öffentlichen Profil.') : ''), ]); body.replaceChildren( $('div', { class: 'rgcc-grid' }, [ stat(tx('总开关', 'Master switch', 'Hauptschalter'), state.master ? tx('开启', 'On', 'An') : tx('关闭(默认)', 'Off (default)', 'Aus (Standard)'), state.master ? 'rgcc-good' : ''), stat(tx('监控模型', 'Monitored models', 'Überwachte Models'), String(notifications.monitoredCount())), stat(tx('活动标签页', 'Active tab', 'Aktiver Tab'), notifications.isLeader() ? tx('此标签页', 'This tab', 'Dieser Tab') : tx('另一个标签页', 'Another tab', 'Anderer Tab')), stat(tx('本地通知', 'Local notifications', 'Lokale Meldungen'), String(state.events.length)), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('通知输出和显示', 'Notification delivery and display', 'Ausgabe und Anzeige', 'Entrega y visualización de notificaciones', 'Entrega e exibição de notificações')), $('p', {}, tx('页面内通知会显示在 RoomGrid 顶栏的空闲区域,不需要浏览器权限。位置、显示时间和密度都可以调整。', 'In-page notifications use the free area in the RoomGrid top bar and need no browser permission. Position, duration, and density are configurable.', 'Meldungen in der Seite nutzen das freie Feld der oberen RoomGrid-Leiste und benötigen keine Browser-Berechtigung. Position, Dauer und Dichte sind einstellbar.', 'Las notificaciones en la página usan el espacio libre de la barra superior de RoomGrid y no requieren permiso del navegador. La posición, duración y densidad son configurables.', 'As notificações na página usam o espaço livre da barra superior do RoomGrid e não precisam de permissão do navegador. Posição, duração e densidade são configuráveis.')), $('div', { class: 'rgcc-row' }, [ field(tx('输出方式', 'Delivery', 'Ausgabeart', 'Entrega', 'Entrega'), select(state.delivery, [ ['inpage', tx('仅在 RoomGrid 顶栏中', 'RoomGrid bar only', 'Nur in der RoomGrid-Leiste', 'Solo en la barra de RoomGrid', 'Somente na barra do RoomGrid')], ['browser', tx('仅浏览器通知', 'Browser notifications only', 'Nur Browser-Benachrichtigungen', 'Solo notificaciones del navegador', 'Somente notificações do navegador')], ['both', tx('RoomGrid 顶栏和浏览器', 'RoomGrid bar and browser', 'RoomGrid-Leiste und Browser', 'Barra de RoomGrid y navegador', 'Barra do RoomGrid e navegador')], ['off', tx('不显示,仅保留历史记录', 'No display, history only', 'Keine Ausgabe, nur Verlauf', 'Sin visualización, solo historial', 'Sem exibição, apenas histórico')], ], changeDelivery)), field(tx('页面内位置', 'In-page position', 'Position in der Seite', 'Posición en la página', 'Posição na página'), select(state.inPagePosition, [ ['header', tx('顶栏空闲区域', 'Free top-bar area', 'Freies Feld in der Kopfleiste', 'Espacio libre de la barra superior', 'Espaço livre da barra superior')], ['top-center', tx('顶部居中浮动', 'Floating top center', 'Oben mittig schwebend', 'Flotante arriba al centro', 'Flutuante no topo central')], ['top-right', tx('右上角浮动', 'Floating top right', 'Oben rechts schwebend', 'Flotante arriba a la derecha', 'Flutuante no canto superior direito')], ], value => patch({ inPagePosition: value }))), field(tx('显示样式', 'Display style', 'Darstellung', 'Estilo de visualización', 'Estilo de exibição'), select(state.inPageStyle, [ ['compact', tx('紧凑单行', 'Compact single line', 'Kompakt einzeilig', 'Compacto en una línea', 'Compacto em uma linha')], ['comfortable', tx('舒适', 'Comfortable', 'Komfortabel', 'Cómodo', 'Confortável')], ['detailed', tx('详细', 'Detailed', 'Ausführlich', 'Detallado', 'Detalhado')], ], value => patch({ inPageStyle: value }))), field(tx('显示时长', 'Display duration', 'Einblenddauer', 'Duración', 'Duração'), select(String(state.inPageDuration), [['0', tx('直到手动关闭', 'Until dismissed', 'Bis zum Schließen', 'Hasta cerrar', 'Até fechar')], ['3', '3 s'], ['5', '5 s'], ['8', '8 s'], ['12', '12 s'], ['20', '20 s'], ['30', '30 s']], value => patch({ inPageDuration: Number(value) }))), field(tx('同时可见', 'Visible at once', 'Gleichzeitig sichtbar', 'Visibles a la vez', 'Visíveis ao mesmo tempo'), select(String(state.inPageMaxVisible), [['1', '1'], ['2', '2'], ['3', '3'], ['4', '4']], value => patch({ inPageMaxVisible: Number(value) }))), btn(tx('测试通知', 'Test notification', 'Testmeldung anzeigen', 'Probar notificación', 'Testar notificação'), async () => { if (browserDelivery(state.delivery) && !await Notify.request()) { await changeDelivery('inpage'); return; } if (!state.master) notifications.patch({ master: true }); notifications.preview(); render(); }, 'primary'), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('通用规则', 'General rules', 'Allgemeine Regeln')), $('div', { class: 'rgcc-row' }, [ check(tx('启用通知', 'Enable notifications', 'Benachrichtigungen aktivieren'), state.master, async value => { if (value && browserDelivery(state.delivery) && !await Notify.request()) { alert(tx('浏览器未授予通知权限。', 'The browser did not grant notification permission.', 'Der Browser hat keine Benachrichtigungsberechtigung erteilt.')); patch({ master: false }); return; } patch({ master: value }); }), check(tx('中性文本', 'Neutral text', 'Neutrale Texte'), state.neutral, value => patch({ neutral: value })), field(tx('安静时间开始', 'Quiet time starts', 'Ruhezeit ab'), input('time', state.quietStart, value => patch({ quietStart: value }))), field(tx('安静时间结束', 'Quiet time ends', 'Ruhezeit bis'), input('time', state.quietEnd, value => patch({ quietEnd: value }))), field(tx('冷却(分钟)', 'Cooldown (minutes)', 'Abklingzeit (Min.)'), input('number', state.cooldownMinutes, value => patch({ cooldownMinutes: value }), { min: 1, max: 1440 })), field(tx('每小时上限', 'Hourly limit', 'Limit pro Stunde'), input('number', state.maxPerHour, value => patch({ maxPerHour: value }), { min: 1, max: 60 })), field(tx('收藏在线阈值', 'Favorite threshold', 'Favoriten-Schwelle'), input('number', state.favoriteThreshold, value => patch({ favoriteThreshold: value }), { min: 0, max: 100 })), field(tx('点击后打开', 'Open on click', 'Beim Klick öffnen'), select(state.destination, [['details', tx('详情', 'Details', 'Details')], ['room', tx('房间', 'Room', 'Raum')], ['workspace', tx('工作区', 'Workspace', 'Arbeitsbereich')]], value => patch({ destination: value }))), ]), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('每个模型', 'Per model', 'Pro Model')), field(tx('模型', 'Model', 'Model'), select(chosenModel, modelIds.map(id => [id, id]), value => { body.dataset.notificationModel = value; render(); })), chosenModel ? ruleEditor('setModelRule', chosenModel, modelRule) : $('p', {}, tx('资料库为空。', 'Library is empty.', 'Bibliothek ist leer.'))]), $('section', { class: 'rgcc-card' }, [$('h3', {}, tx('每个组', 'Per group', 'Pro Gruppe')), field(tx('组', 'Group', 'Gruppe'), select(chosenGroup, store.state.groups.map(group => [group.id, rawUiText(group.name)]), value => { body.dataset.notificationGroup = value; render(); })), chosenGroup ? ruleEditor('setGroupRule', chosenGroup, groupRule) : $('p', {}, tx('没有组。', 'No groups.', 'Keine Gruppen.'))]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('本地通知列表', 'Local notification list', 'Lokale Meldungsliste')), $('div', { class: 'rgcc-list' }, [...state.events].reverse().slice(0, 50).map(event => $('div', { class: 'rgcc-item' }, [$('strong', {}, state.neutral ? event.type : (event.model || event.type)), $('span', {}, event.text), $('time', { class: event.read ? 'rgcc-muted' : '' }, new Date(event.at).toLocaleString())]))), $('div', { class: 'rgcc-row' }, [btn(tx('全部标为已读', 'Mark all read', 'Alle als gelesen markieren'), () => { notifications.markAllRead(); render(); }), btn(tx('清除列表', 'Clear list', 'Liste leeren'), () => { notifications.clearEvents(); render(); }, 'danger')]), $('p', { class: 'rgcc-note' }, tx('后台检查只读取状态,不启动视频。用户脚本只有在打开相关网站标签页时才能运行;多个标签页会自动选出一个检查者。', 'Background checks read status only and start no video. A userscript can run only while a related site tab is open; multiple tabs automatically choose one checker.', 'Hintergrundprüfungen lesen nur den Status und starten kein Video. Ein Userscript läuft nur, solange ein passender Website-Tab geöffnet ist; mehrere Tabs bestimmen automatisch einen Prüfer.')), ]), ); } async function renderPrivacy(revision) { const storage = await PrivacyShield.storageSummary(); if (revision !== renderRevision || tab !== 'privacy') return; const summary = analytics.getUsageSummary(); const privacyUntil = value => Number(value) === -1 ? tx('直到手动恢复', 'Until manually resumed', 'Bis zur manuellen Fortsetzung') : Number(value) > Date.now() ? new Date(Number(value)).toLocaleString() : tx('未启用', 'Not active', 'Nicht aktiv'); body.replaceChildren( $('div', { class: 'rgcc-grid' }, [ stat(tx('保护状态', 'Shield status', 'Schutzstatus'), PrivacyShield.isActive() ? tx('活动', 'Active', 'Aktiv') : tx('准备就绪', 'Ready', 'Bereit'), PrivacyShield.isActive() ? 'rgcc-good' : ''), stat(tx('本地 PIN', 'Local PIN', 'Lokale PIN'), PrivacyShield.hasPin() ? tx('已设置', 'Set', 'Gesetzt') : tx('未设置', 'Not set', 'Nicht gesetzt')), stat(tx('本地存储', 'Local storage', 'Lokaler Speicher'), fmtBytes(storage.localBytes)), stat(tx('站点存储', 'Site storage', 'Website-Speicher'), storage.usage ? fmtBytes(storage.usage) : '—'), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('快速遮盖', 'Quick shield', 'Schnell-Ausblendung')), $('p', {}, tx('立即遮盖名称和图片,并暂停、静音所有媒体。恢复时还原之前的媒体状态。', 'Immediately covers names and images and pauses/mutes all media. Restoring returns the previous media state.', 'Verdeckt sofort Namen und Bilder und pausiert/schaltet alle Medien stumm. Beim Wiederherstellen wird der vorherige Medienzustand zurückgesetzt.')), $('div', { class: 'rgcc-row' }, [btn(tx('立即激活', 'Activate now', 'Jetzt aktivieren'), () => PrivacyShield.activate(), 'primary'), $('span', { class: 'rgcc-muted' }, 'Ctrl + Shift + X')]), field(tx('自动遮盖(无操作后)', 'Auto-shield after inactivity', 'Automatisch ausblenden nach Inaktivität'), select(store.state.settings.privacyIdleMinutes, [[0, tx('关闭', 'Off', 'Aus')], [1, '1 min'], [2, '2 min'], [5, '5 min'], [10, '10 min'], [15, '15 min'], [30, '30 min'], [60, '60 min']], value => { store.patchSettings({ privacyIdleMinutes: value }); render(); })), field(tx('设置或更改本地 PIN', 'Set or change local PIN', 'Lokale PIN setzen/ändern'), input('password', '', async value => { await PrivacyShield.setPin(value); render(); }, { placeholder: tx('留空以移除', 'Leave empty to remove', 'Leer lassen zum Entfernen'), autocomplete: 'new-password' })), $('p', { class: 'rgcc-note' }, tx('PIN 只是本地访问障碍,不是加密,也不能替代操作系统账户保护。', 'The PIN is only a local access barrier, not encryption, and does not replace operating-system account protection.', 'Die PIN ist nur eine lokale Zugriffshürde, keine Verschlüsselung, und ersetzt nicht den Schutz des Betriebssystemkontos.')), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('记录控制', 'History controls', 'Verlauf steuern')), check(tx('使用统计', 'Usage statistics', 'Nutzungsstatistik'), analytics.isEnabled(), value => { store.patchSettings({ analyticsEnabled: value }); analytics.setEnabled(value); render(); }), $('div', { class: 'rgcc-row' }, [btn(tx('暂停 1 小时', 'Pause history 1 hour', 'Verlauf 1 Stunde pausieren'), () => analytics.setHistoryPause(60).then(render)), btn(tx('无限期暂停', 'Pause indefinitely', 'Unbegrenzt pausieren'), () => analytics.setHistoryPause(-1).then(render)), summary.historyPaused ? btn(tx('继续记录', 'Resume history', 'Verlauf fortsetzen'), () => analytics.setHistoryPause(0).then(render), 'primary') : null]), $('p', {}, `${tx('历史状态', 'History status', 'Verlaufsstatus')}: ${privacyUntil(summary.historyPausedUntil)}`), $('div', { class: 'rgcc-row' }, [btn(tx('私人 30 分钟', 'Private for 30 minutes', '30 Minuten privat'), () => analytics.setPrivateSession(true, 30).then(render)), btn(tx('私人 2 小时', 'Private for 2 hours', '2 Stunden privat'), () => analytics.setPrivateSession(true, 120).then(render)), btn(tx('私人直到恢复', 'Private until resumed', 'Privat bis zur Fortsetzung'), () => analytics.setPrivateSession(true).then(render)), summary.privateSession ? btn(tx('结束私人会话', 'End private session', 'Private Sitzung beenden'), () => analytics.setPrivateSession(false).then(render), 'primary') : null]), $('p', { class: 'rgcc-note' }, `${tx('私人会话', 'Private session', 'Private Sitzung')}: ${summary.privateSession ? privacyUntil(summary.privateSessionUntil) : tx('未启用', 'Not active', 'Nicht aktiv')}`), $('div', { class: 'rgcc-row' }, [ btn(tx('清除统计', 'Clear statistics', 'Statistik löschen'), async () => { if (confirm(tx('清除所有本地统计?删除前会创建备份。', 'Clear all local statistics? A backup will be created first.', 'Alle lokalen Statistiken löschen? Zuvor wird eine Sicherung erstellt.'))) { await analytics.createBackup('before-clear'); await analytics.clearAll(); render(); } }, 'danger'), btn(tx('清除发现设置', 'Clear Discover settings', 'Entdecken-Einstellungen löschen'), () => { if (confirm(tx('清除本地发现筛选与动态?', 'Clear local Discover filters and feeds?', 'Lokale Entdecken-Filter und Feeds löschen?'))) { localStorage.removeItem(DISCOVER_STORE_KEY); } }, 'danger'), btn(tx('清除通知列表', 'Clear notification list', 'Meldungsliste löschen'), () => { notifications.clearEvents(); render(); }, 'danger'), ]), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('数据边界', 'Data boundary', 'Datengrenze')), $('p', {}, tx('配置、观看统计、通知列表和发现偏好保存在此浏览器的本地存储中。RoomGrid 不包含云同步、账户上传或遥测。', 'Configuration, watch statistics, notification history and Discover preferences are stored locally in this browser. RoomGrid contains no cloud sync, account upload or telemetry.', 'Konfiguration, Sehstatistik, Meldungsliste und Entdecken-Vorlieben liegen lokal in diesem Browser. RoomGrid enthält keine Cloud-Synchronisierung, Konto-Uploads oder Telemetrie.')), $('div', { class: 'rgcc-row' }, [$('span', {}, `${tx('配额', 'Quota', 'Kontingent')}: ${storage.quota ? fmtBytes(storage.quota) : '—'}`), $('span', {}, `${tx('使用', 'Used', 'Belegt')}: ${storage.usage ? fmtBytes(storage.usage) : '—'}`)]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('敏感度与同意中心', 'Sensitivity & consent center', 'Sensitivitäts- und Einwilligungscenter')), $('div', { class: 'rgcc-list' }, [ $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('普通配置', 'Standard configuration', 'Normale Konfiguration')), $('span', {}, tx('主题、布局、筛选', 'Theme, layout, filters', 'Thema, Layout, Filter')), $('span', { class: 'rgcc-good' }, tx('本地', 'Local', 'Lokal'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('敏感数据', 'Sensitive data', 'Sensible Daten')), $('span', {}, tx('模型名、备注、时间戳、统计', 'Model names, notes, timestamps, statistics', 'Modelnamen, Notizen, Zeitstempel, Statistik')), $('span', { class: 'rgcc-warn' }, tx('导出时警告', 'Warn on export', 'Warnung beim Export'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('秘密', 'Secret', 'Geheimnis')), $('span', {}, tx('可选官方统计令牌', 'Optional official statistics token', 'Optionales offizielles Statistik-Token')), $('span', { class: 'rgcc-good' }, tx('仅会话或加密 Tresor', 'Session-only or encrypted vault', 'Nur Sitzung oder verschlüsselter Tresor'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('聊天内容', 'Chat content', 'Chatinhalt')), $('span', {}, tx('不持久化', 'Never persisted', 'Nie dauerhaft gespeichert')), $('span', { class: 'rgcc-good' }, tx('临时', 'Ephemeral', 'Flüchtig'))]), $('div', { class: 'rgcc-item' }, [$('strong', {}, tx('流录制/截图', 'Stream recording/screenshots', 'Stream-Aufnahme/Screenshots')), $('span', {}, tx('未提供', 'Not provided', 'Nicht angeboten')), $('span', { class: 'rgcc-good' }, tx('规则保护', 'Policy guard', 'Regelschutz'))]), ])]), ); } function renderPerformance() { const settings = store.state.settings; const status = windows.status(); const resource = windows.resourceStatus(); const heapRatio = resource.heapLimit ? resource.heapUsed / resource.heapLimit : 0; const warning = resource.playing >= settings.performanceMaxStreams || heapRatio >= 0.75; const setProfile = profile => { const values = profile === 'eco' ? { performanceProfile: 'eco', performanceMaxStreams: 4, maxStreamHeight: 480, adaptiveQuality: true, gradualStart: true } : profile === 'power' ? { performanceProfile: 'power', performanceMaxStreams: 16, maxStreamHeight: 0, adaptiveQuality: true, gradualStart: true } : { performanceProfile: 'balanced', performanceMaxStreams: 8, maxStreamHeight: 720, adaptiveQuality: true, gradualStart: true }; store.patchSettings(values); render(); }; body.replaceChildren( $('div', { class: 'rgcc-grid' }, [ stat(tx('性能配置', 'Performance profile', 'Leistungsprofil'), settings.performanceProfile), stat(tx('正在播放', 'Playing streams', 'Laufende Streams'), `${resource.playing} / ${settings.performanceMaxStreams}`, warning ? 'rgcc-warn' : 'rgcc-good'), stat(tx('视频元素', 'Video elements', 'Videoelemente'), String(resource.videos)), stat(tx('内存测量', 'Measured memory', 'Gemessener Speicher'), resource.heapUsed ? fmtBytes(resource.heapUsed) : tx('浏览器不可用', 'Unavailable in browser', 'Im Browser nicht verfügbar')), ]), ...(warning ? [$('p', { class: 'rgcc-note' }, tx('当前测量值接近设定上限。建议暂停不使用的摄像头或选择节能模式。', 'Current measurements are near the configured limit. Pause unused cameras or choose Eco mode.', 'Die aktuellen Messwerte liegen nahe an der eingestellten Grenze. Pausiere ungenutzte Cams oder wähle Sparsam.'))] : []), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('流性能', 'Stream performance', 'Stream-Leistung')), $('div', { class: 'rgcc-row' }, [btn(tx('节能', 'Eco', 'Sparsam'), () => setProfile('eco'), settings.performanceProfile === 'eco' ? 'primary' : ''), btn(tx('平衡', 'Balanced', 'Ausgewogen'), () => setProfile('balanced'), settings.performanceProfile === 'balanced' ? 'primary' : ''), btn(tx('高性能', 'Power', 'Leistung'), () => setProfile('power'), settings.performanceProfile === 'power' ? 'primary' : '')]), field(tx('同时播放上限', 'Concurrent stream limit', 'Gleichzeitige Streams'), input('number', settings.performanceMaxStreams, value => { store.patchSettings({ performanceMaxStreams: value }); render(); }, { min: 1, max: MAX_LAYOUT_SLOTS })), $('div', { class: 'rgcc-row' }, [check(tx('自动降低质量', 'Adaptive quality', 'Adaptive Qualität'), settings.adaptiveQuality, value => store.patchSettings({ adaptiveQuality: value })), check(tx('逐步启动', 'Gradual startup', 'Gestaffelter Start'), settings.gradualStart, value => store.patchSettings({ gradualStart: value }))]), check(tx('自动性能模式', 'Automatic performance mode', 'Automatischer Leistungsmodus'), settings.autoPerformanceMode, value => { store.patchSettings({ autoPerformanceMode: value }); render(); }), field(tx('内存阈值', 'Memory threshold', 'Speicherschwelle'), range(settings.autoPerformanceMemoryPct, value => store.patchSettings({ autoPerformanceMemoryPct: value }), { min: 50, max: 95, step: 1 }, value => `${value} %`)), field(tx('流数量阈值', 'Stream-count threshold', 'Stream-Anzahl-Schwelle'), input('number', settings.autoPerformanceStreamThreshold, value => store.patchSettings({ autoPerformanceStreamThreshold: value }), { min: 2, max: MAX_LAYOUT_SLOTS })), $('span', { class: 'rgcc-chip' }, `${tx('当前自动状态', 'Current auto state', 'Aktueller Auto-Status')}: ${document.documentElement.dataset.roomgridAutoPerformance || 'off'}`), $('p', {}, tx('未使用的预览会停止。达到上限的新视频会保持暂停,直到有容量。高数量会逐步启动;不存在“无限摄像头”保证。', 'Unused previews are stopped. New video remains paused at the limit until capacity is available. High counts start gradually; there is no “unlimited cameras” guarantee.', 'Ungenutzte Vorschauen werden gestoppt. Neue Videos bleiben an der Grenze pausiert, bis Kapazität frei ist. Viele Cams starten gestaffelt; eine Garantie für „unbegrenzt viele Cams“ gibt es nicht.')), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('多窗口工作区', 'Multi-window workspaces', 'Mehrfenster-Arbeitsbereiche')), field(tx('窗口名称', 'Window name', 'Fenstername'), input('text', settings.windowName || '', value => store.patchSettings({ windowName: String(value).slice(0, 60) }), { maxlength: 60 })), $('div', { class: 'rgcc-list' }, store.state.groups.filter(group => ![ONLINE_GROUP_ID, LIBRARY_GROUP_ID].includes(group.id)).map(group => $('div', { class: 'rgcc-item' }, [ $('strong', {}, rawUiText(group.name)), $('span', { class: 'rgcc-muted' }, `${store.state.rooms.filter(room => roomInGroup(room, group.id)).length} ${tx('个模型', 'models', 'Models')}`), btn(tx('新窗口', 'New window', 'Neues Fenster'), () => windows.openWindow(group.id, group.name)), ]))), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('活动与最近的窗口', 'Active and recent windows', 'Aktive und letzte Fenster')), $('div', { class: 'rgcc-list' }, status.peers.map(peer => $('div', { class: 'rgcc-item' }, [ $('strong', {}, peer.name), $('span', {}, `${peer.workspaceId || peer.groupId} · ${peer.profile}`), $('span', { class: 'rgcc-good' }, tx('活动', 'Active', 'Aktiv')), ]))), $('div', { class: 'rgcc-row' }, status.recent.filter(peer => !status.peers.some(active => active.id === peer.id)).slice(0, 5).map(peer => btn(`${tx('恢复', 'Restore', 'Wiederherstellen')}: ${peer.name}`, () => windows.openWindow(peer.workspaceId || peer.groupId, peer.name, { workspace: !!peer.workspaceId })))), $('p', {}, tx('模型资料库通过本地合并同步;每个窗口保留自己的工作区和视图设置。浏览器允许时,最近的窗口关联可在重启后恢复。', 'The model library is synchronized through local merging; each window keeps its own workspace and view settings. Recent window associations can be restored after restart where the browser allows it.', 'Die Model-Bibliothek wird lokal zusammengeführt; jedes Fenster behält seinen Arbeitsbereich und seine Ansicht. Letzte Fensterzuordnungen können – soweit vom Browser erlaubt – nach einem Neustart wiederhergestellt werden.')), ]), ); } function renderExtras() { if (!suiteModuleEnabled('optionalTools')) { body.replaceChildren($('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('可选工具已关闭', 'Optional tools are disabled', 'Nebenmodule sind ausgeschaltet')), $('p', {}, tx('媒体、应用和比赛目录不会读取当前页面。', 'Media, app and contest directories will not scan the current page.', 'Medien-, App- und Contest-Verzeichnisse lesen die aktuelle Seite nicht ein.')), btn(tx('启用可选工具', 'Enable optional tools', 'Nebenmodule aktivieren'), () => { setSuiteModuleEnabled('optionalTools', true); OptionalTools.capture(document); render(); }, 'primary'), ])); return; } let data = OptionalTools.overview(); if (!data.capturedAt) { OptionalTools.capture(document); data = OptionalTools.overview(); } const query = String(body.dataset.extraSearch || ''); const media = OptionalTools.search('media', query); const apps = OptionalTools.search('apps', query).sort((left, right) => Number(right.favorite) - Number(left.favorite) || left.title.localeCompare(right.title)); const link = item => $('a', { href: item.url, target: '_blank', rel: 'noopener noreferrer', class: 'rgcc-btn' }, item.title || new URL(item.url).pathname); const searchInput = input('search', query, value => { body.dataset.extraSearch = value; render(); }, { placeholder: tx('本地搜索', 'Local search', 'Lokal suchen') }); searchInput.addEventListener('input', debounce(() => { body.dataset.extraSearch = searchInput.value; render(); }, 180)); body.replaceChildren( $('div', { class: 'rgcc-row' }, [ field(tx('搜索保存的条目和当前页面', 'Search saved items and current page', 'Gespeicherte Einträge und aktuelle Seite durchsuchen'), searchInput), btn(tx('重新读取当前页面', 'Rescan current page', 'Aktuelle Seite neu einlesen'), () => { OptionalTools.capture(document); render(); }), btn(tx('关闭可选工具', 'Disable optional tools', 'Nebenmodule ausschalten'), () => { setSuiteModuleEnabled('optionalTools', false); render(); }), btn(tx('清除本地旁注', 'Clear local side data', 'Lokale Zusatzdaten löschen'), () => { if (confirm(tx('清除应用收藏、分类和媒体旁注?', 'Clear app favorites, categories and media notes?', 'App-Favoriten, Kategorien und Mediennotizen löschen?'))) { OptionalTools.clearLocal(); render(); } }, 'danger'), ]), $('p', { class: 'rgcc-note' }, tx('这些模块只整理当前可访问页面上的公开链接和文本。不复制媒体、不自动下载,也不操作聊天、代币或付款功能。登录、年龄验证、安全和付款表单保持不变。', 'These modules only organize public links and text already accessible on the current page. They do not copy media, download automatically, or operate chat, token or payment functions. Login, age verification, security and payment forms remain untouched.', 'Diese Module ordnen nur öffentliche Links und Texte, die auf der aktuellen Seite bereits zugänglich sind. Sie kopieren keine Medien, laden nichts automatisch herunter und bedienen keine Chat-, Token- oder Zahlungsfunktionen. Login, Altersprüfung, Sicherheit und Zahlungsformulare bleiben unverändert.')), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, `${tx('照片和视频区域', 'Photo and video areas', 'Foto- und Videobereiche')} · ${media.length}`), $('div', { class: 'rgcc-list' }, media.length ? media.map(item => $('div', { class: 'rgcc-item' }, [ link(item), input('text', item.note || '', value => OptionalTools.saveMediaNote(item.url, value), { placeholder: tx('本地旁注', 'Local note', 'Lokale Notiz'), maxlength: 500 }), $('span', { class: 'rgcc-muted' }, new URL(item.url).pathname), ])) : [$('p', {}, tx('当前页面没有可识别的公开媒体区域。请在原生照片或视频页面重新读取。', 'No recognizable public media area is present on this page. Rescan while on a native photo or video page.', 'Auf dieser Seite ist kein erkennbarer öffentlicher Medienbereich vorhanden. Lies die Seite auf einer nativen Foto- oder Videoseite neu ein.'))]), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, `${tx('应用目录', 'Apps directory', 'Apps-Verzeichnis')} · ${apps.length}`), $('div', { class: 'rgcc-list' }, apps.length ? apps.map(item => $('div', { class: 'rgcc-item' }, [ $('span', {}, [link(item), check(tx('收藏', 'Favorite', 'Favorit'), item.favorite, value => { OptionalTools.upsertApp({ url: item.url, title: item.title, favorite: value }); render(); })]), $('span', { class: 'rgcc-row' }, [ input('text', item.category || '', value => OptionalTools.upsertApp({ url: item.url, title: item.title, category: value }), { placeholder: tx('本地分类', 'Local category', 'Lokale Kategorie'), maxlength: 50 }), input('text', item.note || '', value => OptionalTools.upsertApp({ url: item.url, title: item.title, note: value }), { placeholder: tx('本地旁注', 'Local note', 'Lokale Notiz'), maxlength: 500 }), ]), item.modifiedAt ? btn('×', () => { OptionalTools.removeApp(item.url); render(); }, 'danger') : $('span', { class: 'rgcc-muted' }, tx('在页面上发现', 'Found on page', 'Auf Seite gefunden')), ])) : [$('p', {}, tx('当前页面没有可识别的应用链接。', 'No recognizable app links are present on this page.', 'Auf dieser Seite wurden keine erkennbaren App-Links gefunden.'))]), ]), ]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('公开比赛结果', 'Public contest results', 'Öffentliche Contest-Ergebnisse')), $('div', { class: 'rgcc-list' }, data.contests.length ? data.contests.map(block => $('div', {}, [ $('strong', {}, block.title), ...(block.links || []).map(link), ...block.rows.slice(0, 30).map((row, index) => $('div', { class: 'rgcc-item' }, [$('strong', {}, String(index + 1)), $('span', {}, row), $('span', {})])), ])) : [$('p', {}, tx('当前页面没有可读的公开比赛表格。', 'No readable public contest table is present on this page.', 'Auf dieser Seite ist keine lesbare öffentliche Contest-Tabelle vorhanden.'))]), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('原生账户、帮助和播主链接', 'Native account, help and broadcaster links', 'Native Konto-, Hilfe- und Broadcaster-Links')), $('div', { class: 'rgcc-row' }, data.native.length ? data.native.map(link) : [$('p', {}, tx('当前页面没有找到安全的原生链接;付款和登录路径会被有意过滤。', 'No safe native links were found on this page; payment and login paths are deliberately filtered.', 'Auf dieser Seite wurden keine sicheren nativen Links gefunden; Zahlungs- und Loginpfade werden absichtlich ausgefiltert.'))]), ]), ]), ); } async function renderDiagnostics(revision) { const checks = await Diagnostics.runSelfCheck(); if (revision !== renderRevision || tab !== 'diagnostics') return; const snapshot = Diagnostics.snapshot(); const capabilities = RoomGridUltimate.capabilities(); const stateClass = status => status === 'ok' ? 'rgcc-good' : status === 'error' ? 'rgcc-danger' : 'rgcc-warn'; const errorRows = Diagnostics.getErrors().slice(0, 50); const okChecks = checks.filter(item => item.status === 'ok').length; const capabilityScore = Object.values(capabilities).filter(Boolean).length / Math.max(1, Object.keys(capabilities).length); const healthScore = Math.max(0, Math.min(100, Math.round((okChecks / Math.max(1, checks.length)) * 70 + capabilityScore * 25 + (errorRows.length ? Math.max(0, 5 - errorRows.length) : 5)))); const withBackup = operation => { Storage.backupCurrentConfig(); operation(); }; const resetHomeFilters = () => withBackup(() => RoomGridUx.undoable(store, tx('主页筛选器已重置', 'Homepage filters reset', 'Startseitenfilter zurückgesetzt'), () => { const defaults = defaultState().settings; const keys = ['normalHomeSearch','normalHomeStatus','normalHomeGender','normalHomeCountryFilter','normalHomeTagFilter','normalHomeTagMatchMode','normalHomeMinViewers','normalHomeMaxViewers','normalHomeMinAge','normalHomeMaxAge','normalHomeOnlyHd','normalHomeOnlyNew','normalHomeOnlyGaming','normalHomeOnlySaved','normalHomeOnlyFavorites','normalHomeOnlyFollowing','normalHomeHideVisited','normalHomePage']; store.patchSettings(Object.fromEntries(keys.map(key => [key, defaults[key]]))); render(); }, { onUndo: render })); const resetAppearance = () => { const keys = ['theme','brandPreset','accentColor','secondaryAccentColor','surfaceStyle','density','contentWidth','cardStyle','dashboardWidgetDensity','fontFamily','fontScale','controlScale','cornerRadius','cardGap','shadowStrength','surfaceOpacity','backgroundStyle','animations','pageAppearanceProfilesEnabled','pageAppearanceProfiles']; previewDesignReset(keys); }; body.replaceChildren( $('div', { class: 'rgcc-grid' }, [ stat(tx('版本', 'Version', 'Version'), snapshot.version), stat(tx('健康评分', 'Health score', 'Gesundheitswert'), `${healthScore}/100`, healthScore >= 90 ? 'rgcc-good' : healthScore >= 70 ? 'rgcc-warn' : 'rgcc-danger'), stat(tx('页面类型', 'Page type', 'Seitentyp'), snapshot.pageType), stat(tx('路由代次', 'Route generation', 'Routen-Generation'), String(snapshot.navigation?.generation || 0)), stat(tx('运行模块', 'Running modules', 'Laufende Module'), String(snapshot.runtime.running.length)), stat(tx('本地错误', 'Local errors', 'Lokale Fehler'), String(errorRows.length), errorRows.length ? 'rgcc-warn' : 'rgcc-good'), ]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('修复中心', 'Repair center', 'Reparaturzentrum')), $('p', {}, tx('每次修复前都会创建配置备份。统计和模型资料不会被这些操作删除。', 'A configuration backup is created before every repair. Statistics and model data are not deleted by these actions.', 'Vor jeder Reparatur wird eine Konfigurationssicherung erstellt. Statistik und Modeldaten werden dabei nicht gelöscht.')), $('div', { class: 'rgcc-row' }, [btn(tx('重置主页筛选', 'Reset home filters', 'Startseitenfilter zurücksetzen'), () => { if (confirm(tx('重置所有主页筛选?', 'Reset all home-page filters?', 'Alle Startseitenfilter zurücksetzen?'))) resetHomeFilters(); }), btn(tx('重置全局外观', 'Reset global appearance', 'Globales Design zurücksetzen'), () => { if (confirm(tx('重置全局外观?已保存的设计配置会保留。', 'Reset the global appearance? Saved design profiles remain.', 'Globales Design zurücksetzen? Gespeicherte Designprofile bleiben erhalten.'))) resetAppearance(); }), btn(tx('重新规范化配置', 'Normalize configuration', 'Konfiguration neu normalisieren'), () => { if (!confirm(tx('验证并重写当前配置?会先创建备份。', 'Validate and rewrite the current configuration? A backup is created first.', 'Aktuelle Konfiguration prüfen und neu schreiben? Zuvor wird eine Sicherung erstellt.'))) return; withBackup(() => { Storage.save(sanitizeState(store.state)); location.reload(); }); }, 'primary'), btn(tx('关闭所有预览', 'Close all previews', 'Alle Vorschauen schließen'), () => { RoomGridUltimate.closeAllPreviews(); render(); })])]), $('div', { class: 'rgcc-two rgcc-section' }, [ $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('兼容性自检', 'Compatibility self-check', 'Kompatibilitäts-Selbsttest')), $('div', { class: 'rgcc-list' }, checks.map(checkItem => $('div', { class: 'rgcc-item' }, [$('strong', {}, checkItem.id), $('span', {}, checkItem.detail), $('span', { class: stateClass(checkItem.status) }, checkItem.status)]))), ]), $('section', { class: 'rgcc-card' }, [ $('h3', {}, tx('模块和页面状态', 'Module and page status', 'Modul- und Seitenstatus')), $('div', { class: 'rgcc-list' }, [ ...snapshot.runtime.running.map(id => $('div', { class: 'rgcc-item' }, [$('strong', {}, id), $('span', {}, snapshot.pageType), $('span', { class: 'rgcc-good' }, 'running')])), ...snapshot.modules.map(item => $('div', { class: 'rgcc-item' }, [$('strong', {}, rawUiText(item.id)), $('span', {}, rawUiText(item.detail || '—')), $('span', { class: stateClass(item.status) }, rawUiText(item.status))])), ]), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('页面路由测试矩阵', 'Page route test matrix', 'Seiten-Routen-Testmatrix')), $('p', { class: 'rgcc-note' }, [tx('当前路由', 'Current route', 'Aktuelle Route'), ': ', rawUiText(snapshot.navigation?.routeKey || '—'), ' · ', tx('触发器', 'Trigger', 'Auslöser'), ': ', rawUiText(snapshot.navigation?.lastReason || '—')]), $('div', { class: 'rgcc-list' }, checks.filter(item => item.id.startsWith('route:')).map(item => $('div', { class: 'rgcc-item' }, [ $('strong', {}, rawUiText(item.id.replace(/^route:/u, ''))), $('span', {}, rawUiText(item.detail)), $('span', { class: stateClass(item.status) }, rawUiText(item.status)), ]))), $('details', {}, [ $('summary', {}, tx('最近的页面切换', 'Recent page transitions', 'Letzte Seitenwechsel')), $('div', { class: 'rgcc-list' }, (snapshot.navigation?.routeHistory || []).slice().reverse().map(item => $('div', { class: 'rgcc-item' }, [ $('strong', {}, rawUiText(`${item.from || '—'} → ${item.to || '—'}`)), $('span', {}, rawUiText(`${item.reason || '—'} · ${item.route || '—'}`)), $('time', { class: 'rgcc-muted' }, item.at ? new Date(item.at).toLocaleTimeString() : '—'), ]))), ]), ]), $('section', { class: 'rgcc-card rgcc-section' }, [$('h3', {}, tx('功能能力层', 'Capability layer', 'Capability Layer')), $('div', { class: 'rgcc-grid' }, Object.entries(capabilities).map(([name, available]) => stat(name, available ? tx('可用', 'Available', 'Verfügbar') : tx('备用模式', 'Fallback', 'Fallback'), available ? 'rgcc-good' : 'rgcc-warn'))), $('p', {}, tx('缺少 API 时只停用相关模块;其余 RoomGrid 功能继续运行。重复失败的网络适配器会暂时进入冷却。', 'When an API is missing, only the affected module falls back. Repeated network failures temporarily open a circuit breaker.', 'Fehlt eine API, wechselt nur das betroffene Modul in den Fallback. Wiederholte Netzwerkfehler öffnen vorübergehend einen Schutzschalter.'))]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('备份和恢复状态', 'Backup and recovery status', 'Backup- und Wiederherstellungsstatus')), $('div', { class: 'rgcc-row' }, [ $('span', { class: snapshot.workspace.primary ? 'rgcc-good' : 'rgcc-danger' }, `${tx('主数据', 'Primary data', 'Hauptdaten')}: ${snapshot.workspace.primary ? 'OK' : '—'}`), $('span', { class: snapshot.workspace.archive ? 'rgcc-good' : 'rgcc-warn' }, `${tx('稳定归档', 'Stable archive', 'Stabiles Archiv')}: ${snapshot.workspace.archive ? 'OK' : '—'}`), $('span', {}, `${tx('滚动备份', 'Rolling backups', 'Rollierende Backups')}: ${snapshot.workspace.backups}`), ]), $('p', {}, tx('自检只验证可读性和浏览器能力,不会自动覆盖当前数据。恢复继续使用现有的带确认备份流程。', 'The self-check only verifies readability and browser capabilities; it never overwrites current data automatically. Recovery continues to use the existing confirmed backup flow.', 'Der Selbsttest prüft nur Lesbarkeit und Browserfähigkeiten und überschreibt niemals automatisch aktuelle Daten. Die Wiederherstellung verwendet weiterhin den vorhandenen bestätigten Backup-Ablauf.')), ]), $('section', { class: 'rgcc-card rgcc-section' }, [ $('h3', {}, tx('已清理的本地错误日志', 'Sanitized local error log', 'Bereinigtes lokales Fehlerprotokoll')), $('div', { class: 'rgcc-list' }, errorRows.length ? errorRows.map(entry => $('div', { class: 'rgcc-item' }, [$('strong', {}, entry.module), $('span', {}, entry.message), $('time', { class: 'rgcc-muted' }, new Date(entry.at).toLocaleString())])) : [$('p', { class: 'rgcc-good' }, tx('没有记录到 RoomGrid 错误。', 'No RoomGrid errors recorded.', 'Keine RoomGrid-Fehler aufgezeichnet.'))]), $('div', { class: 'rgcc-row' }, [ btn(tx('重新运行自检', 'Run self-check again', 'Selbsttest erneut ausführen'), () => render(), 'primary'), btn(tx('导出诊断', 'Export diagnostics', 'Diagnose exportieren'), () => Diagnostics.exportReport()), btn(tx('清除错误日志', 'Clear error log', 'Fehlerprotokoll löschen'), () => { Diagnostics.clear(); render(); }, 'danger'), btn(tx('返回正常网站', 'Return to normal site', 'Zur normalen Website'), () => { stopAllPageMedia(); location.href = Diagnostics.normalSiteUrl(); }), ]), $('p', { class: 'rgcc-note' }, tx('诊断导出不包含工作区、模型名称、备注或统计数据。错误文本中的URL查询参数和可能的密钥值会被清理。', 'The diagnostic export contains no workspace, model names, notes or statistics. URL query parameters and possible secret values are removed from error text.', 'Der Diagnoseexport enthält keine Workspaces, Modelnamen, Notizen oder Statistikdaten. URL-Abfragen und mögliche Geheimwerte werden aus Fehlertexten entfernt.')), ]), ); } const deferredSettingCards = new WeakMap(); const deferredSettingGroups = new WeakMap(); function deferredSettingsCard(label, build) { const card = $('section', { class: 'rgcc-card' }, $('h3', {}, label)); deferredSettingCards.set(card, build); return card; } function rememberSettingsGroups() { for (const group of body.querySelectorAll('[data-settings-group]')) { if (group.open) expandedSettingsGroups.add(group.dataset.settingsGroup); else expandedSettingsGroups.delete(group.dataset.settingsGroup); } } function organizeSettingsGroups() { // Move existing nodes, never clone controls: listeners, drafts and IDs stay intact. const groups = []; const cards = [...body.querySelectorAll('section.rgcc-card')]; for (const card of cards) { const heading = card.querySelector(':scope > h3'); if (!heading || card.closest('[data-settings-group]')) continue; const key = `${tab}:${heading.textContent.trim()}`; const deferredBuild = deferredSettingCards.get(card); const description = card.querySelector(':scope > p')?.textContent?.trim() || ''; const optionCount = card.querySelectorAll('input,select,textarea').length; const shortHint = optionCount ? tx(`${optionCount} 个选项 · 点击打开`, `${optionCount} ${optionCount === 1 ? 'option' : 'options'} · open when needed`, `${optionCount} ${optionCount === 1 ? 'Option' : 'Optionen'} · bei Bedarf öffnen`) : tx('操作与信息 · 点击打开', 'Actions and information · open when needed', 'Aktionen und Informationen · bei Bedarf öffnen'); const summary = $('summary', {}, $('span', { class: 'rgcc-settings-summary' }, [heading, $('small', { title: description || shortHint }, shortHint)])); const content = $('div', { class: 'rgcc-settings-content' }, [...card.childNodes]); const moveGuidance = () => { // Keep lengthy explanations available, without placing a wall of text // before the actual controls. Move nodes so links and handlers survive. const guidance = [...content.querySelectorAll(':scope > p')].filter(node => node.textContent.trim().length > 180 && !node.querySelector('input,select,textarea,button')); if (guidance.length) content.append($('details', { class: 'rgcc-settings-help', dataset: { settingsGuidance: '1' } }, [ $('summary', {}, tx('说明与注意事项', 'Explanation and important notes', 'Erklärung und wichtige Hinweise')), ...guidance, ])); }; if (!deferredBuild) moveGuidance(); const group = $('details', { class: 'rgcc-settings-group', dataset: { settingsGroup: key }, open: expandedSettingsGroups.has(key) }, [summary, content]); if (deferredBuild) { const build = () => { deferredSettingGroups.delete(group); const complete = deferredBuild(); for (const className of complete.classList) card.classList.add(className); Object.assign(card.dataset, complete.dataset); complete.querySelector(':scope > h3')?.remove(); content.append(...complete.childNodes); moveGuidance(); }; deferredSettingGroups.set(group, build); if (group.open) build(); } group.addEventListener('toggle', () => { if (!group.isConnected) return; if (group.open) { deferredSettingGroups.get(group)?.(); expandedSettingsGroups.add(key); } else expandedSettingsGroups.delete(key); }); card.classList.add('rgcc-settings-card'); card.replaceChildren(group); groups.push(group); } if (!groups.length) return; const setExpanded = open => { groups.forEach(group => { group.open = open; if (open) deferredSettingGroups.get(group)?.(); }); rememberSettingsGroups(); }; body.prepend($('div', { class: 'rgcc-settings-tools', dataset: { settingsFolds: '1' } }, [ $('p', {}, tx('点击主题以打开设置。所有选项仍然可用。', 'Open a topic to adjust its settings. All options remain available.', 'Öffne nur das Thema, das du gerade brauchst. Alle Optionen bleiben verfügbar.')), btn(tx('全部展开', 'Expand all', 'Alle aufklappen'), () => setExpanded(true)), btn(tx('全部折叠', 'Collapse all', 'Alle einklappen'), () => setExpanded(false)), ])); } function render() { rememberSettingsGroups(); renderRevision += 1; const revision = renderRevision; renderProgressTask?.finish(); const recordingState = analytics.getUsageSummary(); privacyStateBadge.textContent = recordingState.privateSession ? tx('私人会话', 'Private session', 'Private Sitzung') : recordingState.historyPaused ? tx('历史已暂停', 'History paused', 'Verlauf pausiert') : analytics.isEnabled() ? tx('本地记录', 'Local history on', 'Lokaler Verlauf an') : tx('记录已关闭', 'History off', 'Verlauf aus'); privacyStateBadge.classList.toggle('rgcc-warn', recordingState.privateSession || recordingState.historyPaused); let activeButton = null; tabButtons.forEach((button, id) => { const active = id === tab; button.classList.toggle('active', active); button.setAttribute('aria-selected', String(active)); button.tabIndex = active ? 0 : -1; if (active) activeButton = button; }); if (activeButton) body.setAttribute('aria-labelledby', statisticsView ? 'roomgrid-control-center-title' : activeButton.id); topicSelect.value = tab; const progressTask = notifications?.beginProgress?.(`${tx('正在加载设置页面', 'Loading settings page', 'Einstellungsseite wird geladen')}: ${activeButton?.textContent?.trim() || tab}`, { delayMs: 280 }); renderProgressTask = progressTask; body.setAttribute('aria-busy', 'true'); body.replaceChildren($('div', { class: 'rgcc-card' }, tx('加载中…', 'Loading…', 'Wird geladen…'))); const finish = () => { progressTask?.finish(); if (renderProgressTask === progressTask) renderProgressTask = null; if (revision !== renderRevision || !overlay.isConnected) return; organizeSettingsGroups(); refreshSettingsSearch(false); revealSettingTarget(); body.setAttribute('aria-busy', 'false'); activeButton?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); }; const fail = error => { progressTask?.finish(); if (renderProgressTask === progressTask) renderProgressTask = null; if (revision !== renderRevision || !overlay.isConnected) return; try { Diagnostics.report('control-center', error, `render ${tab}`); } catch (_) {} body.setAttribute('aria-busy', 'false'); body.replaceChildren($('section', { class: 'rgcc-card', role: 'alert' }, [ $('h3', {}, tx('此设置页面无法加载', 'This settings page could not be loaded', 'Diese Einstellungsseite konnte nicht geladen werden')), $('p', {}, tx('你的数据不会更改。请重试;如果问题仍然存在,请打开诊断页面。', 'Your data was not changed. Try again; if the problem remains, open Diagnostics.', 'Deine Daten wurden nicht verändert. Versuche es erneut; falls das Problem bleibt, öffne die Diagnose.')), $('div', { class: 'rgcc-row' }, [ btn(tx('重试', 'Try again', 'Erneut versuchen'), () => render(), 'primary'), tab !== 'diagnostics' ? btn(tx('打开诊断', 'Open Diagnostics', 'Diagnose öffnen'), () => { tab = 'diagnostics'; render(); }) : null, ]), ])); }; let pending = null; try { if (tab === 'workspaces') pending = renderWorkspaces(); else if (tab === 'library') pending = renderLibrary(); else if (tab === 'data') pending = renderData(revision); else if (tab === 'homepage') pending = renderHomepage(revision); else if (tab === 'appearance') pending = renderAppearance(); else if (tab === 'analytics') pending = renderAnalyticsStudio(revision); else if (tab === 'broadcaster') pending = renderBroadcaster(revision); else if (tab === 'notifications') pending = renderNotifications(); else if (tab === 'privacy') pending = renderPrivacy(revision); else if (tab === 'performance') pending = renderPerformance(); else if (tab === 'extras') pending = renderExtras(); else pending = renderDiagnostics(revision); } catch (error) { fail(error); return; } if (pending && typeof pending.then === 'function') pending.then(finish, fail); else finish(); } render(); return { close, setTab(value) { if (tabSpecs.some(([id]) => id === value)) { tab = value; render(); } } }; } /* ============================================================= * 3.6. Optionale Nebenmodule * Rein lokale Verzeichnisse und lesende DOM-Übersichten. Keine * Medienkopie, kein Download und keine Chat-/Token-/Zahlungsaktion. * ============================================================= */ const OPTIONAL_TOOLS_STORE_KEY = 'ryujo_multicam_optional_tools_v1'; const OptionalTools = (() => { const MAX_ITEMS = 250; const sensitivePath = /\/(?:billing|payment|payments|checkout|purchase|tokens?|credits?|age-verification|login|signup)(?:\/|$)/iu; let captured = { media: [], apps: [], contests: [], native: [], capturedAt: 0, pageType: 'other' }; function clean(value, max = 180) { return String(value ?? '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, max); } function safeUrl(value) { try { const url = new URL(String(value || ''), location.origin); if (!['http:', 'https:'].includes(url.protocol) || url.origin !== location.origin || sensitivePath.test(url.pathname)) return ''; url.search = ''; url.hash = ''; return url.toString(); } catch (_) { return ''; } } function cleanItem(input) { const url = safeUrl(input?.url); if (!url) return null; return { url, title: clean(input?.title || new URL(url).pathname, 120), category: clean(input?.category || '', 50), favorite: !!input?.favorite, note: clean(input?.note || '', 500), modifiedAt: Math.max(0, Number(input?.modifiedAt) || Date.now()) }; } function sanitizeState(input) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const apps = (Array.isArray(source.apps) ? source.apps : []).slice(-MAX_ITEMS).map(cleanItem).filter(Boolean); const notes = {}; for (const [rawUrl, note] of Object.entries(source.notes && typeof source.notes === 'object' && !Array.isArray(source.notes) ? source.notes : {}).slice(-MAX_ITEMS)) { const url = safeUrl(rawUrl); if (url) notes[url] = clean(note, 500); } return { schema: 1, apps: [...new Map(apps.map(item => [item.url, item])).values()], notes }; } function load() { try { return sanitizeState(JSON.parse(localStorage.getItem(OPTIONAL_TOOLS_STORE_KEY) || '{}')); } catch (_) { return sanitizeState({}); } } function save(state) { try { localStorage.setItem(OPTIONAL_TOOLS_STORE_KEY, JSON.stringify(sanitizeState(state))); return true; } catch (error) { Diagnostics.report('optional-tools', error, 'local save'); return false; } } function anchors(root) { try { return [...root.querySelectorAll('a[href]')].slice(0, 5000).map(anchor => { const url = safeUrl(anchor.href || anchor.getAttribute('href')); return url ? { url, title: clean(anchor.textContent || anchor.getAttribute('aria-label') || anchor.title || new URL(url).pathname, 120) } : null; }).filter(Boolean); } catch (error) { Diagnostics.report('optional-scanner', error, 'anchor scan'); return []; } } function unique(items, max = 120) { return [...new Map(items.filter(item => item?.url).map(item => [item.url, item])).values()].slice(0, max); } function scanContests(root, pageAnchors) { const output = []; try { const pageIsContest = /contest|winner|leaderboard|ranking|wettbewerb|gewinn/iu.test(`${location.pathname} ${document.title || ''}`); const nodes = [...root.querySelectorAll('table,ol,[class*="contest" i],[class*="leaderboard" i],[class*="ranking" i],[id*="contest" i],[id*="leaderboard" i]')].slice(0, 40); for (const node of nodes) { const context = clean(`${node.previousElementSibling?.textContent || ''} ${node.getAttribute?.('aria-label') || ''} ${node.id || ''} ${node.className || ''}`, 300); if (!pageIsContest && !/contest|winner|leader|ranking|rang|gewinn|wettbewerb|排名|比赛/iu.test(context)) continue; const rows = [...node.querySelectorAll('tr,li')].slice(0, 100).map(row => { const cells = [...row.querySelectorAll('th,td')].map(cell => clean(cell.textContent, 100)).filter(Boolean); return cells.length ? cells.join(' · ') : clean(row.textContent, 220); }).filter(Boolean); if (rows.length) output.push({ title: context || tx('公开结果', 'Public results', 'Öffentliche Ergebnisse'), rows }); } } catch (error) { Diagnostics.report('contest-view', error, 'contest DOM scan'); } if (!output.length) { const links = pageAnchors.filter(item => /contest|winner|leaderboard|ranking|wettbewerb|gewinn/iu.test(`${item.url} ${item.title}`)).slice(0, 30); if (links.length) output.push({ title: tx('公开比赛链接', 'Public contest links', 'Öffentliche Contest-Links'), rows: links.map(item => item.title), links }); } return output.slice(0, 20); } function capture(root = document) { if (!root?.querySelectorAll) { Diagnostics.mark('optional-scanner', 'disabled', 'DOM root unavailable'); return captured; } const all = anchors(root); const media = unique(all.filter(item => /(?:^|\/)(?:photos?|videos?|gallery|galleries|albums?|media)(?:\/|$)|foto|photo|video|galerie/iu.test(`${new URL(item.url).pathname} ${item.title}`))); const apps = unique(all.filter(item => /(?:^|\/)apps?(?:\/|$)|\bapps?\b/iu.test(`${new URL(item.url).pathname} ${item.title}`))); const native = unique(all.filter(item => /account|settings|help|support|broadcaster|broadcast center|hilfe|konto|发送|帮助/iu.test(`${new URL(item.url).pathname} ${item.title}`))); const contests = scanContests(root, all); captured = { media, apps, contests, native, capturedAt: Date.now(), pageType: detectPageType() }; Diagnostics.mark('optional-scanner', all.length || ['workstation', 'discover'].includes(detectPageType()) ? 'ok' : 'degraded', `${media.length} media; ${apps.length} apps; ${contests.length} contest blocks`); return captured; } function saveMediaNote(url, note) { url = safeUrl(url); if (!url) return false; const state = load(); const value = clean(note, 500); if (value) state.notes[url] = value; else delete state.notes[url]; return save(state); } function upsertApp(input) { const url = safeUrl(input?.url); if (!url) return false; const state = load(); const previous = state.apps.find(entry => entry.url === url) || { url, title: new URL(url).pathname, category: '', favorite: false, note: '' }; const next = { ...previous, url, title: Object.hasOwn(input || {}, 'title') ? clean(input.title, 120) || previous.title : previous.title, category: Object.hasOwn(input || {}, 'category') ? clean(input.category, 50) : previous.category, favorite: Object.hasOwn(input || {}, 'favorite') ? !!input.favorite : previous.favorite, note: Object.hasOwn(input || {}, 'note') ? clean(input.note, 500) : previous.note, modifiedAt: Date.now(), }; state.apps = [...state.apps.filter(entry => entry.url !== url), next].slice(-MAX_ITEMS); return save(state); } function removeApp(url) { url = safeUrl(url); const state = load(); state.apps = state.apps.filter(item => item.url !== url); return save(state); } function overview() { const state = load(); const apps = [...new Map([...captured.apps.map(item => ({ ...item, category: '', favorite: false, note: '', modifiedAt: 0 })), ...state.apps].map(item => [item.url, item])).values()]; return { ...captured, media: captured.media.map(item => ({ ...item, note: state.notes[item.url] || '' })), apps, state }; } function search(kind, query = '') { const value = clean(query, 120).toLocaleLowerCase(); const source = overview()[kind] || []; return value ? source.filter(item => JSON.stringify(item).toLocaleLowerCase().includes(value)) : source; } function clearLocal() { try { localStorage.removeItem(OPTIONAL_TOOLS_STORE_KEY); return true; } catch (_) { return false; } } return { capture, overview, search, saveMediaNote, upsertApp, removeApp, clearLocal, safeUrl, sanitizeState }; })(); /* ============================================================= * 4. 房间服务 / RoomService —— API + HLS + 重连 + 智能轮询 * ============================================================= */ function createRoomService(store) { const sessions = new Map(); // id -> { hls, video, status, retryCount, pollTimer, userPaused } const videoSessions = new Map(); const domain = safeChaturbateHost(window.location.hostname) ? window.location.hostname : 'chaturbate.com'; const QUALITY_HEIGHT_STEPS = [2160, 1440, 1080, 720, 480, 360, 240]; const appliedQualityLevels = new WeakMap(); function streamHealth(metrics = {}) { const connection = String(metrics.connection || 'idle'); if (connection === 'paused') return { healthScore: 100, healthState: 'paused' }; let score = connection === 'playing' ? 100 : connection === 'connecting' || connection === 'idle' ? 82 : ['buffering', 'stalled', 'recovering'].includes(connection) ? 58 : connection.includes('error') ? 22 : 72; score -= Math.min(28, Math.max(0, Number(metrics.stalls) || 0) * 5); score -= Math.min(24, Math.max(0, Number(metrics.reconnects) || 0) * 7); const totalFrames = Math.max(0, Number(metrics.totalFrames) || 0); const droppedFrames = Math.max(0, Number(metrics.droppedFrames) || 0); if (totalFrames >= 60) score -= Math.min(28, (droppedFrames / totalFrames) * 180); const bufferSeconds = Math.max(0, Number(metrics.bufferSeconds) || 0); if (connection === 'playing' && bufferSeconds < .6) score -= 12; const startupMs = Math.max(0, Number(metrics.startupMs) || 0); if (startupMs > 8000) score -= 15; else if (startupMs > 4000) score -= 7; score = Math.max(0, Math.min(100, Math.round(score))); return { healthScore: score, healthState: score >= 80 ? 'good' : score >= 55 ? 'warning' : 'critical' }; } function publishMetrics(id, patch = {}) { const session = sessions.get(normalizeUsername(id)); if (!session) return; let frameMetrics = {}; try { const quality = session.video?.getVideoPlaybackQuality?.(); if (quality) frameMetrics = { totalFrames: Math.max(0, Number(quality.totalVideoFrames) || 0), droppedFrames: Math.max(0, Number(quality.droppedVideoFrames) || 0) }; } catch (_) {} let activeStreams = 0; for (const item of videoSessions.values()) { if (!item.video.paused && item.status === 'online') activeStreams += 1; } const combined = { connection: 'idle', bufferSeconds: 0, width: 0, height: 0, levelHeight: 0, startupMs: 0, stallMs: 0, stalls: 0, reconnects: 0, totalFrames: 0, droppedFrames: 0, ...(session.metrics || {}), ...frameMetrics, ...patch, activeStreams, updatedAt: Date.now() }; if (session.playBlocked && !session.userPaused) combined.connection = 'play-blocked'; session.metrics = { ...combined, ...streamHealth(combined) }; EventBus.emit('room:metrics', { id: normalizeUsername(id), ...session.metrics }); } function clearPoll(s) { if (s?.pollTimer) { clearTimeout(s.pollTimer); s.pollTimer = null; } } function onlinePollMs() { return Math.max(45000, Number(store.state.settings.pollMs?.online) || 120000); } async function fetchContext(id, signal) { const req = new AbortController(); const timeout = setTimeout(() => { try { req.abort(); } catch (_) {} }, 15000); const abortFromParent = () => { try { req.abort(); } catch (_) {} }; try { if (signal) { if (signal.aborted) abortFromParent(); else signal.addEventListener('abort', abortFromParent, { once: true }); } const res = await fetch(`https://${domain}/api/chatvideocontext/${encodeURIComponent(id)}/`, { credentials: 'include', signal: req.signal, referrer: `https://${domain}/${encodeURIComponent(id)}/`, referrerPolicy: 'strict-origin-when-cross-origin', }); if (!res.ok) throw new Error('http ' + res.status); const data = await res.json(); if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('Ungültige Raumantwort'); return data; } finally { clearTimeout(timeout); try { signal?.removeEventListener?.('abort', abortFromParent); } catch (_) {} } } function roomViewerCount(data) { if (!data || typeof data !== 'object') return null; const keys = ['num_viewers', 'viewer_count', 'viewers', 'numViewers', 'viewers_count']; for (const key of keys) { const value = Number(data[key]); if (Number.isFinite(value) && value >= 0) return Math.round(value); } return null; } function setStatus(id, status, extra = {}) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) return; const room = store.state.rooms.find(r => r.id === id); const prev = s.status || room?.lastStatus || 'unknown'; const opts = extra && typeof extra === 'object' ? extra : {}; const transient = !!opts.transient; const hadObservedStableStatus = !!s.observedStableStatus; const safeExtra = { ...opts }; delete safeExtra.transient; // v15.5: 后台探测中的 loading / 临时请求错误不覆盖稳定状态。 // 否则 hideOffline / onlyOnline / 分页会短时间重算,表现为离线房间闪出、在线房间消失后又回来。 if ((status === 'loading' || (status === 'error' && transient)) && isStableRoomStatus(prev)) { s.status = prev; s.pendingStatus = status; if (safeExtra.errorMsg) s.lastTransientError = safeExtra.errorMsg; return; } s.status = status; delete s.pendingStatus; const patch = { lastStatus: status, ...safeExtra }; if (status === 'online') { const lastSeen = numeric(room?.lastSeenOnline, 0); if (prev !== 'online' || !lastSeen || Date.now() - lastSeen > 60000) patch.lastSeenOnline = Date.now(); } store.patchRoom(id, patch); if (prev !== status && isStableRoomStatus(status)) addRoomStatusHistory(id, status, safeExtra); // Statusmeldungen erst nach der stillen ersten Referenzprüfung senden. // Private/unknown/error werden nicht als Offline interpretiert. if (hadObservedStableStatus && prev !== status && status === 'online' && store.state.settings.notifyOnline) { Notify.fire(t('notifyTitleText'), t('notifyBody', id), `multicam-${id}-online`); EventBus.emit('room:flash', id); } if (hadObservedStableStatus && prev === 'online' && status === 'offline' && store.state.settings.notifyOffline) { Notify.fire(t('notifyOfflineTitleText'), t('notifyOfflineBody', id), `multicam-${id}-offline`); EventBus.emit('room:flash', id); } if (isStableRoomStatus(status)) s.observedStableStatus = true; } function hardStopVideo(video) { stopMediaElement(video, true); } function hardStopRoomVideos(id) { id = normalizeUsername(id); // DOM 兜底:即使 cardMap/session 已经丢失,也按 room-id 把残留 video 杀掉。 try { const safe = String(id).replace(/"/g, ''); document.querySelectorAll(`video[data-multicam-room-id="${safe}"],video[data-multicam-room="${safe}"],audio[data-multicam-room-id="${safe}"],audio[data-multicam-room="${safe}"]`).forEach(hardStopVideo); } catch (_) {} } function clearFatalRecovery(s, recovered = false) { if (!s) return; if (s.fatalRecoveryTimer) { clearTimeout(s.fatalRecoveryTimer); s.fatalRecoveryTimer = null; } if (recovered) s.fatalRecoveryAttempts = 0; } function destroyHls(s) { clearFatalRecovery(s); if (!s || !s.hls) return; try { s.hls.stopLoad(); } catch (_) {} try { s.hls.detachMedia(); } catch (_) {} try { s.hls.destroy(); } catch (_) {} s.hls = null; } function destroyPlayer(id, opts = {}) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) { hardStopRoomVideos(id); return; } clearFatalRecovery(s); if (opts.abort !== false && s.abortController) { try { s.abortController.abort(); } catch (_) {} s.abortController = null; } clearPoll(s) if (s.hls) { try { s.hls.stopLoad(); } catch (_) {} } hardStopVideo(s.video); hardStopRoomVideos(id); if (s.hls) { try { s.hls.detachMedia(); } catch (_) {} try { s.hls.destroy(); } catch (_) {} s.hls = null; } s.video = null; videoSessions.delete(id); s.hlsSource = null; } function detachVideo(id) { id = normalizeUsername(id); const s = sessions.get(id); if (s) { clearFatalRecovery(s); if (s.hls) { try { s.hls.stopLoad(); } catch (_) {} try { s.hls.detachMedia(); } catch (_) {} try { s.hls.destroy(); } catch (_) {} s.hls = null; } hardStopVideo(s.video); s.video = null; videoSessions.delete(id); s.hlsSource = null; if (s.status === 'online' && store.state.rooms.some(r => r.id === id)) schedulePoll(id, onlinePollMs()); } hardStopRoomVideos(id); queueMicrotask(() => refreshQuality()); } function attachVideo(id, video) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) { hardStopVideo(video); return; } clearPoll(s); s.video = video; videoSessions.set(id, s); s.playBlocked = false; s.metrics = { ...(s.metrics || {}), startupStartedAt: Date.now(), stallStartedAt: 0 }; publishMetrics(id, { connection: 'connecting' }); const buffered = () => { if (sessions.get(id)?.video !== video) return; let seconds = 0; try { if (video.buffered?.length) seconds = Math.max(0, video.buffered.end(video.buffered.length - 1) - video.currentTime); } catch (_) {} publishMetrics(id, { bufferSeconds: seconds, width: Number(video.videoWidth) || 0, height: Number(video.videoHeight) || 0 }); }; video.addEventListener('loadedmetadata', buffered); video.addEventListener('progress', buffered); video.addEventListener('timeupdate', buffered); const markStall = connection => { const current = sessions.get(id); if (current?.video !== video) return; const metrics = current.metrics || {}; const first = !metrics.stallStartedAt; publishMetrics(id, { connection, stallStartedAt: metrics.stallStartedAt || Date.now(), stalls: Math.max(0, Number(metrics.stalls) || 0) + (first ? 1 : 0) }); }; video.addEventListener('waiting', () => markStall('buffering')); video.addEventListener('stalled', () => markStall('stalled')); video.addEventListener('playing', () => { const current = sessions.get(id); if (current?.video !== video) return; current.playBlocked = false; current.playerRetryCount = 0; const metrics = current?.metrics || {}; const stallDelta = metrics.stallStartedAt ? Date.now() - metrics.stallStartedAt : 0; const startupMs = metrics.startupMs || (metrics.startupStartedAt ? Date.now() - metrics.startupStartedAt : 0); buffered(); publishMetrics(id, { connection: 'playing', startupMs, stallMs: Math.max(0, Number(metrics.stallMs) || 0) + stallDelta, stallStartedAt: 0 }); }); video.addEventListener('error', () => { if (sessions.get(id)?.video === video) publishMetrics(id, { connection: 'media-error' }); }); queueMicrotask(() => refreshQuality()); } function schedulePoll(id, ms) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) return; clearPoll(s); // 错峰:±20% const jitter = ms * (0.8 + Math.random() * 0.4); s.pollTimer = setTimeout(() => { if (sessions.has(id)) connect(id); }, jitter); } function pause(id) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) return false; s.userPaused = true; clearFatalRecovery(s); try { s.hls?.stopLoad?.(); } catch (_) {} try { s.video?.pause?.(); } catch (_) {} publishMetrics(id, { connection: 'paused' }); queueMicrotask(() => refreshQuality()); return true; } function resume(id) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) return false; s.userPaused = false; try { s.hls?.startLoad?.(); } catch (_) {} playVideo(id, s.video); if (!s.video && store.state.rooms.some(r => r.id === id)) refresh(id); queueMicrotask(() => refreshQuality()); return true; } function playVideo(id, video) { if (!video) return; if (sessions.get(id)?.video === video) sessions.get(id).playBlocked = false; const failed = error => { if (sessions.get(id)?.video !== video || sessions.get(id)?.userPaused) return; sessions.get(id).playBlocked = error?.name === 'NotAllowedError'; publishMetrics(id, { connection: error?.name === 'NotAllowedError' ? 'play-blocked' : 'media-error' }); }; try { video.play()?.catch?.(failed); } catch (error) { failed(error); } } function isPaused(id) { id = normalizeUsername(id); const s = sessions.get(id); return !!(s?.userPaused || s?.video?.paused); } function togglePause(id) { return isPaused(id) ? (resume(id), false) : (pause(id), true); } function pauseAll(ids = null) { const list = ids && ids.length ? ids.map(normalizeUsername) : [...sessions.keys()]; list.forEach(id => pause(id)); } function resumeAll(ids = null) { const list = ids && ids.length ? ids.map(normalizeUsername) : [...sessions.keys()]; list.forEach(id => resume(id)); } function activeStreamIds() { return [...videoSessions.entries()].filter(([, session]) => !session.video.paused && !session.video.ended).map(([id]) => id); } function qualityBudgetCap(id = '') { const total = Number(store.state.settings.qualityBudgetHeight) || 0; if (!total) return 0; const normalized = normalizeUsername(id); const activeIds = activeStreamIds(); if (normalized && !activeIds.includes(normalized)) activeIds.push(normalized); if (!activeIds.length) return 0; const explicit = new Map(activeIds.map(roomId => [roomId, Number(store.state.rooms.find(room => room.id === roomId)?.qualityHeight) || 0])); const selected = explicit.get(normalized) || 0; if (selected) return selected; const reserved = [...explicit.values()].reduce((sum, height) => sum + Math.max(0, height), 0); const automaticIds = activeIds.filter(roomId => !(explicit.get(roomId) || 0)); if (!automaticIds.length) return 0; const focusId = store.state.settings.qualityPrioritizeFocused !== false && store.state.settings.viewMode === 'focus' ? normalizeUsername(store.state.settings.focusedRoomId) : ''; const totalWeight = automaticIds.reduce((sum, roomId) => sum + (roomId === focusId ? 2 : 1), 0) || 1; const share = Math.max(240, (Math.max(240, total - reserved) / totalWeight) * (normalized === focusId ? 2 : 1)); return QUALITY_HEIGHT_STEPS.find(height => height <= share) || 240; } function streamMaxHeight(id = '') { const configured = Number(store.state.settings.maxStreamHeight) || 0; const roomConfigured = Number(store.state.rooms.find(room => room.id === normalizeUsername(id))?.qualityHeight) || 0; // Eine explizite Kamera-Auswahl hat Vorrang vor der globalen/adaptiven // Begrenzung. So bleibt z. B. die auf 1080p gesetzte Hauptkamera auch // dann bei ihrer eigenen Grenze, wenn viele weitere Kameras laufen. if (roomConfigured) return roomConfigured; const budgetCap = qualityBudgetCap(id); if (store.state.settings.adaptiveQuality === false) return configured && budgetCap ? Math.min(configured, budgetCap) : (configured || budgetCap); const active = activeStreamIds().length; const adaptive = active >= 13 ? 360 : active >= 9 ? 480 : active >= 5 ? 720 : 0; const limits = [configured, adaptive, budgetCap].filter(value => Number(value) > 0); return limits.length ? Math.min(...limits) : 0; } function hlsLevelForMaxHeight(hls, maxHeight = streamMaxHeight()) { const levels = Array.isArray(hls?.levels) ? hls.levels : []; if (!levels.length || !maxHeight) return -1; let best = -1; let bestHeight = 0; let smallest = 0; let smallestHeight = Number(levels[0]?.height) || Infinity; levels.forEach((level, idx) => { const height = Number(level?.height) || 0; if (height && height < smallestHeight) { smallest = idx; smallestHeight = height; } if (height && height <= maxHeight && height >= bestHeight) { best = idx; bestHeight = height; } }); return best >= 0 ? best : smallest; } function applyHlsQuality(hls, id = '') { if (!hls) return; const level = hlsLevelForMaxHeight(hls, streamMaxHeight(id)); if (appliedQualityLevels.get(hls) === level) return; // currentLevel performs an immediate full buffer flush, even for the // same level. Set the next quality conservatively and only when changed. try { hls.autoLevelCapping = level; } catch (_) {} try { hls.loadLevel = level; appliedQualityLevels.set(hls, level); } catch (_) {} } function refreshQuality(id = '') { const normalized = normalizeUsername(id); if (normalized) { const session = sessions.get(normalized); if (session?.hls) applyHlsQuality(session.hls, normalized); return; } videoSessions.forEach((s, sessionId) => { if (s.hls) applyHlsQuality(s.hls, sessionId); }); } function getQualityStatus(id) { id = normalizeUsername(id); const session = sessions.get(id); const selected = Number(store.state.rooms.find(room => room.id === id)?.qualityHeight) || 0; const available = [...new Set((session?.hls?.levels || []).map(level => Number(level?.height) || 0).filter(Boolean))].sort((a, b) => a - b); return { selected, effective: streamMaxHeight(id), current: Number(session?.metrics?.levelHeight) || Number(session?.video?.videoHeight) || 0, available, }; } function getQualityBudgetStatus() { const ids = activeStreamIds(); const total = Number(store.state.settings.qualityBudgetHeight) || 0; const assignments = ids.map(id => { const selected = Number(store.state.rooms.find(room => room.id === id)?.qualityHeight) || 0; return { id, selected, effective: streamMaxHeight(id), current: Number(sessions.get(id)?.metrics?.levelHeight) || Number(sessions.get(id)?.video?.videoHeight) || 0 }; }); return { enabled: total > 0, total, active: ids.length, assigned: assignments.reduce((sum, item) => sum + Math.max(0, item.effective), 0), assignments }; } function getHealthSnapshot(roomIds = null) { // Status probes also have sessions, but are not displayed video streams. // The control center requests its visible cameras directly instead of // sorting/rebuilding a whole account's background probes every second. const entries = roomIds == null ? [...sessions.entries()] : [...new Set(roomIds.map(normalizeUsername))].filter(id => sessions.has(id)).map(id => [id, sessions.get(id)]); return entries.map(([id, session]) => { const metrics = session.metrics || {}; const connection = String(metrics.connection || (session.userPaused ? 'paused' : session.video && !session.video.ended ? 'starting' : 'connecting')); return { id, ...streamHealth({ ...metrics, connection }), ...metrics, connection, paused: !!session.userPaused }; }) .sort((left, right) => Number(left.healthScore) - Number(right.healthScore) || left.id.localeCompare(right.id)); } function sameStreamEndpoint(previous, current) { if (!previous || !current) return false; if (previous === current) return true; try { const oldUrl = new URL(previous); const newUrl = new URL(current); if (oldUrl.origin !== newUrl.origin || oldUrl.pathname !== newUrl.pathname || oldUrl.username !== newUrl.username || oldUrl.password !== newUrl.password || oldUrl.hash !== newUrl.hash) return false; // Ignore only recognized signing fields. Unknown parameters may // identify a different feed even when the path is unchanged. const signingField = /^(?:token|auth|signature|sig|expires|exp|policy|key-pair-id|hdnts|hdnea|auth_key|wssecret|wstime)$/i; const stableParams = url => [...url.searchParams].filter(([key]) => !signingField.test(key)).sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey) || aValue.localeCompare(bValue)); return JSON.stringify(stableParams(oldUrl)) === JSON.stringify(stableParams(newUrl)); } catch (_) { return false; } } async function connect(id) { id = normalizeUsername(id); let s = sessions.get(id); if (!s) { s = { retryCount: 0 }; sessions.set(id, s); } // 新请求开始前中止旧请求。删除房间或快速刷新时,旧请求返回也不会再创建 video。 if (s.abortController) { try { s.abortController.abort(); } catch (_) {} } const ac = new AbortController(); s.abortController = ac; const publishCheck = status => EventBus.emit('room:checked', { id, status, at: Date.now() }); EventBus.emit('room:loading', id); setStatus(id, 'loading'); let data; try { data = await fetchContext(id, ac.signal); } catch (e) { if (ac.signal.aborted || !sessions.has(id) || sessions.get(id) !== s) return; if (s.abortController === ac) s.abortController = null; setStatus(id, 'error', { errorMsg: 'request failed', transient: true }); if (!s.video || s.video.readyState < 2) publishMetrics(id, { connection: 'request-error' }); publishCheck('error'); s.retryCount = (s.retryCount || 0) + 1; const wait = Math.min(60000, store.state.settings.pollMs.error * Math.pow(1.6, s.retryCount)); schedulePoll(id, wait); return; } if (ac.signal.aborted || !sessions.has(id) || sessions.get(id) !== s) return; if (s.abortController === ac) s.abortController = null; s.retryCount = 0; const cfg = store.state.settings.pollMs; const viewers = roomViewerCount(data); const viewerPatch = viewers == null ? {} : { viewerCount: viewers }; if (data.room_status === 'offline') { setStatus(id, 'offline', viewerPatch); publishCheck('offline'); destroyPlayer(id); s = sessions.get(id) || s; s.video = null; sessions.set(id, s); schedulePoll(id, cfg.offline); return; } if (data.room_status === 'private' || data.room_status === 'hidden' || data.room_status === 'away') { setStatus(id, 'private', { privateLabel: data.room_status, ...viewerPatch }); publishCheck('private'); destroyPlayer(id); sessions.set(id, sessions.get(id) || s); schedulePoll(id, cfg.private); return; } if (!data.hls_source) { const playing = s.status === 'online' && s.video && !s.video.ended && s.video.readyState >= 2 && !s.video.error && (s.hls || s.video.src || s.video.srcObject); setStatus(id, 'error', { errorMsg: 'no stream', transient: !!playing, ...viewerPatch }); publishCheck('error'); schedulePoll(id, cfg.error); return; } if (!isSafeStreamUrl(data.hls_source)) { const playing = s.status === 'online' && s.video && !s.video.ended && s.video.readyState >= 2 && !s.video.error && (s.hls || s.video.src || s.video.srcObject); setStatus(id, 'error', { errorMsg: 'invalid stream url', transient: !!playing, ...viewerPatch }); publishCheck('error'); schedulePoll(id, cfg.error); return; } // 在线 —— 触发 UI 创建 video,再回调 attach。 // v15.5: 如果只是例行探测且流地址没变、video 仍在播放,不重建 video/HLS。 // 这能消除多窗口/多房间场景中周期性 attach/detach 造成的闪屏。 if (!sessions.has(id) || sessions.get(id) !== s) return; const prevSource = s.hlsSource; const hasLiveVideo = !!s.video && !s.video.ended && (s.hls || s.video.src || s.video.srcObject); // Signed query parameters can change on each routine status check. A // healthy video on the same endpoint must not lose its buffer just for // that rotation. A changed endpoint or unhealthy video still reconnects. const sameActiveStream = s.status === 'online' && hasLiveVideo && !['media-error', 'stream-error'].includes(s.metrics?.connection) && (prevSource === data.hls_source || ( s.video.readyState >= 2 && !s.video.error && sameStreamEndpoint(prevSource, data.hls_source) )); s.hlsSource = data.hls_source; s.contextValidatedAt = Date.now(); setStatus(id, 'online', viewerPatch); publishCheck('online'); if (!sessions.has(id) || sessions.get(id) !== s) return; if (!sameActiveStream) EventBus.emit('room:online', { id, hlsSource: data.hls_source }); if (sessions.has(id) && sessions.get(id) === s) schedulePoll(id, cfg.online || onlinePollMs()); } function startHls(id, hlsSource) { id = normalizeUsername(id); const s = sessions.get(id); if (!s || !s.video || !isSafeStreamUrl(hlsSource)) return; const video = s.video; s.hlsSource = hlsSource; destroyHls(s); s.playerRetryCount = 0; if (window.Hls && Hls.isSupported()) { const hls = new Hls({ liveDurationInfinity: true, lowLatencyMode: true, maxBufferLength: 10 }); hls.loadSource(hlsSource); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (sessions.get(id)?.hls === hls) { applyHlsQuality(hls, id); if (sessions.get(id)?.userPaused) video.pause(); else playVideo(id, video); } }); if (Hls.Events.LEVELS_UPDATED) { hls.on(Hls.Events.LEVELS_UPDATED, () => { if (sessions.get(id)?.hls === hls) applyHlsQuality(hls, id); }); } if (Hls.Events.LEVEL_SWITCHED) hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => { if (sessions.get(id)?.hls !== hls) return; const level = hls.levels?.[Number(data?.level)]; publishMetrics(id, { levelHeight: Number(level?.height) || 0, connection: 'playing' }); }); if (Hls.Events.FRAG_BUFFERED) hls.on(Hls.Events.FRAG_BUFFERED, () => { if (sessions.get(id)?.hls === hls) { clearFatalRecovery(s, true); publishMetrics(id, { connection: 'playing' }); } }); hls.on(Hls.Events.ERROR, (_, data) => { if (sessions.get(id)?.hls !== hls) return; publishMetrics(id, { connection: data?.fatal ? 'stream-error' : 'recovering', reconnects: Math.max(0, Number(s.metrics?.reconnects) || 0) + (data?.fatal ? 1 : 0) }); if (!data.fatal) return; // 致命错误:尝试 recover,多次失败后回到状态轮询 if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { // hls.js has already exhausted its internal retries at a fatal // network error. Avoid an immediate startLoad loop against an old // URL; obtain a fresh context with a bounded exponential backoff. if (!s.fatalRecoveryTimer && !s.userPaused) { s.fatalRecoveryAttempts = Math.min(5, (s.fatalRecoveryAttempts || 0) + 1); const wait = Math.min(30000, 2000 * Math.pow(2, s.fatalRecoveryAttempts - 1)); s.fatalRecoveryTimer = setTimeout(() => { s.fatalRecoveryTimer = null; if (sessions.get(id) === s && s.hls === hls && !s.userPaused) connect(id); }, wait); } return; } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { s.playerRetryCount = (s.playerRetryCount || 0) + 1; if (s.playerRetryCount <= 3) { try { hls.recoverMediaError(); } catch (_) {} } } else { s.playerRetryCount = (s.playerRetryCount || 0) + 1; } if (s.playerRetryCount > 3) { destroyPlayer(id); // 退避后重新走 connect 流程(重新拉接口) const wait = Math.min(30000, 2000 * Math.pow(1.5, s.playerRetryCount)); if (sessions.has(id)) schedulePoll(id, wait); setStatus(id, 'error', { errorMsg: 'stream broken' }); } }); s.hls = hls; } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = hlsSource; video.addEventListener('loadedmetadata', () => { if (sessions.get(id)?.userPaused) video.pause(); else playVideo(id, video); }, { once: true }); } else publishMetrics(id, { connection: 'media-error' }); } function refresh(id) { id = normalizeUsername(id); // 强制重连:清理旧 hls + video + timer,再走 connect,避免刷新时双音轨。 const s = sessions.get(id); if (s) { clearPoll(s) destroyHls(s); hardStopVideo(s.video); s.video = null; videoSessions.delete(id); s.hlsSource = null; } if (!sessions.has(id)) sessions.set(id, { retryCount: 0 }); connect(id); } function refreshAll() { for (const id of [...sessions.keys()]) refresh(id); } // Layout/status redraws are not explicit reconnects. Join an outstanding // request, or deliver its just-validated public source to a newly built card. // Detached cards clear hlsSource; no persisted/cached authorization is used. function ensure(id) { id = normalizeUsername(id); const s = sessions.get(id); if (!s) { start(id); return; } if (s.video || (s.abortController && !s.abortController.signal.aborted)) return; if (s.status === 'online' && isSafeStreamUrl(s.hlsSource) && Date.now() - s.contextValidatedAt < 15000) { EventBus.emit('room:online', { id, hlsSource: s.hlsSource }); } else connect(id); } function start(id) { id = normalizeUsername(id); // 幂等:已有 session 就跳过(避免切分组时重复启动轮询) if (sessions.has(id)) return; sessions.set(id, { retryCount: 0 }); connect(id); } function stop(id) { id = normalizeUsername(id); destroyPlayer(id); sessions.delete(id); } function stopAll() { for (const id of [...sessions.keys()]) stop(id); stopAllPageMedia(); } function has(id) { id = normalizeUsername(id); return sessions.has(id); } function getMetrics(id) { return { ...(sessions.get(normalizeUsername(id))?.metrics || {}) }; } return { start, ensure, stop, stopAll, refresh, refreshAll, attachVideo, detachVideo, startHls, has, pause, resume, togglePause, isPaused, pauseAll, resumeAll, refreshQuality, getQualityStatus, getQualityBudgetStatus, getHealthSnapshot, fetchContext, getMetrics }; } /* ============================================================= * 4.5. 用户名过滤 / Username validation * Following-Sync uses the same validator, while generic page imports * still reject reserved site paths. * ============================================================= */ const USERNAME_EXCLUDE = new Set([ 'tags', 'tag', 'auth', 'followed-cams', 'multicam', 'events', 'jobs', 'terms', 'privacy', 'support', 'billing', 'accounts', 'b', 'p', 'apps', 'affiliates', 'static', 'feedback', 'sitemap', 'home', 'about', 'rules', 'login', 'logout', 'signup', 'female-cams', 'male-cams', 'couple-cams', 'trans-cams', 's', 'shortcuts', 'roomlist', 'photo_videos', 'in-private-show', 'external_link', 'dmca', 'contact', 'mobile', 'tipping', 'tokens', 'token-purchase', 'social', 'wiki', 'directory', 'spy-on-cams', 'private-shows', 'app', 'photos-videos', 'pm', 'inbox', 'find-friends', 'broadcasters', 'broadcast', 'discover', 'top', 'new', 'gold-shows', 'language', 'settings', 'en', 'es', 'de', 'fr', 'it', 'ja', 'ko', 'pt', 'ru', 'zh', ]); function isLikelyUsername(name) { name = normalizeUsername(name); if (!name) return false; if (USERNAME_EXCLUDE.has(name.toLowerCase())) return false; return usernameSyntaxOk(name); } function roomGridSingleCamUrl(model, options = {}) { const id = normalizeUsername(model); if (!isLikelyUsername(id)) return new URL('/', location.origin).toString(); const url = new URL('/', location.origin); url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', 'single'); url.searchParams.set('model', id); const source = String(options.source || '').replace(/[^a-z0-9_-]/giu, '').slice(0, 32); if (source) url.searchParams.set('single_source', source); return url.toString(); } function roomGridNativeRoomId(input = location.href) { try { const url = input instanceof URL ? input : new URL(String(input || location.href), location.origin); if (url.origin !== location.origin || url.searchParams.get('roomgrid_official') === '1') return ''; const parts = url.pathname.split('/').filter(Boolean); if (parts.length !== 1) return ''; const id = normalizeUsername(parts[0]); return isLikelyUsername(id) ? id : ''; } catch (_) { return ''; } } /* ============================================================= * 5. Event bus * ============================================================= */ const EventBus = (() => { const listeners = new Map(); return { on(event, fn) { (listeners.get(event) || listeners.set(event, new Set()).get(event)).add(fn); return () => listeners.get(event)?.delete(fn); }, emit(event, payload) { (listeners.get(event) || []).forEach(fn => { try { fn(payload); } catch (_) {} }); }, }; })(); /* ============================================================= * 6. Central navigation and module lifecycle * * Chaturbate replaces large parts of its document without necessarily * reloading the userscript. Page type, running modules and pending work * therefore have to follow the URL instead of being frozen at startup. * ============================================================= */ const MODULE_SETTINGS_KEY = 'ryujo_multicam_modules_v1'; const MODULE_DEFAULTS = Object.freeze({ pageTools: true, audioMeters: true, optionalTools: true }); const ROOMGRID_NORMAL_PAGE_TYPES = Object.freeze([ 'home', 'nativeDiscover', 'tags', 'tag', 'following', 'privateShows', 'collection', 'messages', 'room', 'other', ]); const ROOMGRID_RESERVED_ROOTS = new Set([ 'accounts', 'apps', 'app', 'billing', 'contest', 'contests', 'discover', 'followed-cams', 'help', 'login', 'messages', 'my_collection', 'privacy', 'search', 'spy-on-cams', 'support', 'tag', 'tags', 'terms', 'tipping', ]); function loadSuiteModuleSettings() { try { const parsed = JSON.parse(localStorage.getItem(MODULE_SETTINGS_KEY) || '{}'); return { ...MODULE_DEFAULTS, ...(parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}) }; } catch (_) { return { ...MODULE_DEFAULTS }; } } function suiteModuleEnabled(id) { return loadSuiteModuleSettings()[id] !== false; } function setSuiteModuleEnabled(id, enabled) { if (!Object.hasOwn(MODULE_DEFAULTS, id)) return false; try { const next = loadSuiteModuleSettings(); next[id] = !!enabled; localStorage.setItem(MODULE_SETTINGS_KEY, JSON.stringify(next)); return true; } catch (error) { StorageHealth.report('module-settings', error); return false; } } function routeUrl(input = null) { const current = `${location.origin || ''}${location.pathname || '/'}${location.search || ''}${location.hash || ''}` || location.href; try { return input instanceof URL ? input : new URL(String(input || current), location.origin); } catch (_) { return new URL(location.href); } } function detectPageType(input = null) { const url = routeUrl(input); if (url.searchParams.get('multicam_discover') === '1') return 'discover'; if (url.searchParams.get('multicam_mode') === '1') return 'workstation'; const path = url.pathname.replace(/\/+$/u, '') || '/'; if (path === '/') return 'home'; if (path === '/discover') return 'nativeDiscover'; if (path === '/tags') return 'tags'; if (/^\/(?:tag|tags)\//u.test(path)) return 'tag'; if (/^\/followed-cams(?:\/|$)/u.test(path)) return 'following'; if (/^\/spy-on-cams(?:\/|$)/u.test(path)) return 'privateShows'; if (/^\/my_collection(?:\/|$)/u.test(path)) return 'collection'; if (/^\/messages(?:\/|$)/u.test(path)) return 'messages'; const first = path.split('/').filter(Boolean)[0] || ''; if (!ROOMGRID_RESERVED_ROOTS.has(first.toLocaleLowerCase()) && isLikelyUsername(first)) return 'room'; return 'other'; } function normalizedRouteKey(input = null) { const url = routeUrl(input); return `${url.pathname}${url.search}${url.hash}`; } const ROOMGRID_ROUTE_MATRIX = Object.freeze([ { id: 'home-roomgrid-redirect', sample: '/', expected: 'home', module: 'customHomeRedirect' }, { id: 'discover-native', sample: '/discover/', expected: 'nativeDiscover', module: 'pageTools' }, { id: 'tags-index', sample: '/tags/', expected: 'tags', module: 'pageTools' }, { id: 'tag-detail', sample: '/tag/music/', expected: 'tag', module: 'pageTools' }, { id: 'following', sample: '/followed-cams/', expected: 'following', module: 'pageTools' }, { id: 'private-shows', sample: '/spy-on-cams/', expected: 'privateShows', module: 'pageTools' }, { id: 'collection', sample: '/my_collection/', expected: 'collection', module: 'pageTools' }, { id: 'messages', sample: '/messages/', expected: 'messages', module: 'pageTools' }, { id: 'room', sample: '/fixture_model/', expected: 'room', module: 'pageTools' }, { id: 'discover-suite', sample: '/?multicam_discover=1&suite_view=discover', expected: 'discover', module: 'discover' }, { id: 'home-roomgrid-suite', sample: '/?multicam_discover=1&suite_view=dashboard', expected: 'discover', module: 'discover' }, { id: 'workstation-suite', sample: '/?multicam_mode=1', expected: 'workstation', module: 'workstation' }, ]); function initNativeSiteShell() { let fullCleanup = null; let lightweightCleanup = null; let disposed = false; const adapt = () => { if (disposed) return; const type = detectPageType(); if (type === 'room') { const model = roomGridNativeRoomId(location.href); if (model) { disposed = true; stopAllPageMedia(); location.replace(roomGridSingleCamUrl(model, { source: 'direct-room' })); return; } } if (type === 'nativeDiscover' && !fullCleanup) { if (!lightweightCleanup) lightweightCleanup = initNativeDiscoverGuard(); return; } if (!ROOMGRID_NORMAL_PAGE_TYPES.includes(type)) return; if (lightweightCleanup) { try { lightweightCleanup(); } catch (_) {} lightweightCleanup = null; } if (!fullCleanup) fullCleanup = initInjector(); }; const onNavigation = () => adapt(); window.addEventListener('roomgrid:navigation', onNavigation); adapt(); return () => { disposed = true; window.removeEventListener('roomgrid:navigation', onNavigation); try { lightweightCleanup?.(); } catch (_) {} try { fullCleanup?.(); } catch (_) {} lightweightCleanup = null; fullCleanup = null; }; } function initCustomHomeRedirect() { if (detectPageType() !== 'home') return null; const url = new URL(location.href); stopAllPageMedia(); url.searchParams.delete('roomgrid_native_home'); url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', 'dashboard'); url.hash = ''; location.replace(url.toString()); return null; } const SuiteRuntime = (() => { const definitions = []; const running = new Map(); const errors = []; const routeHistory = []; const retryTimers = new Map(); let generation = 0; let lastRouteKey = ''; let lastReason = 'startup'; let lastNavigationAt = 0; let started = false; const wantedDefinitions = pageType => definitions.filter(definition => definition.pages.includes(pageType) && (!definition.optional || suiteModuleEnabled(definition.id))); const startDefinition = (definition, attempt = 0) => { if (running.has(definition.id)) return; try { const cleanup = definition.start(); running.set(definition.id, { cleanup: typeof cleanup === 'function' ? cleanup : null, startedAt: Date.now(), generation }); retryTimers.delete(definition.id); try { Diagnostics.mark(definition.id, 'ok', `running on ${api.pageType}`); } catch (_) {} } catch (error) { errors.push({ module: definition.id, message: String(error?.message || error), at: Date.now(), route: normalizedRouteKey() }); try { Diagnostics.report(definition.id, error, `start on ${api.pageType}; attempt ${attempt + 1}`); } catch (_) {} if (attempt < 2 && !retryTimers.has(definition.id)) { const scheduledGeneration = generation; const timer = setTimeout(() => { retryTimers.delete(definition.id); if (scheduledGeneration !== generation || !wantedDefinitions(api.pageType).some(item => item.id === definition.id)) return; startDefinition(definition, attempt + 1); }, 450 * (2 ** attempt)); retryTimers.set(definition.id, timer); } } }; const stopDefinition = id => { const entry = running.get(id); if (!entry) return; try { entry.cleanup?.(); Diagnostics.mark(id, 'disabled', `stopped on ${api.pageType}`); } catch (error) { errors.push({ module: id, message: String(error?.message || error), at: Date.now(), route: normalizedRouteKey() }); try { Diagnostics.report(id, error, 'cleanup'); } catch (_) {} } running.delete(id); }; const reconcile = (reason = 'navigation', force = false) => { const key = normalizedRouteKey(); const nextType = detectPageType(); if (!force && key === lastRouteKey && nextType === api.pageType) return false; const previousType = api.pageType; lastRouteKey = key; lastReason = String(reason || 'navigation'); lastNavigationAt = Date.now(); generation += 1; api.pageType = nextType; routeHistory.push({ at: lastNavigationAt, from: previousType, to: nextType, route: key.slice(0, 240), reason: lastReason }); if (routeHistory.length > 30) routeHistory.splice(0, routeHistory.length - 30); const wanted = new Map(wantedDefinitions(nextType).map(definition => [definition.id, definition])); for (const id of [...running.keys()]) if (!wanted.has(id)) stopDefinition(id); for (const definition of wanted.values()) startDefinition(definition); const detail = { generation, pageType: nextType, previousPageType: previousType, routeKey: key, reason: lastReason }; try { window.dispatchEvent(new CustomEvent('roomgrid:navigation', { detail })); } catch (_) {} EventBus.emit('navigation', detail); try { Diagnostics.mark('navigation', 'ok', `${previousType}->${nextType}; ${lastReason}`); } catch (_) {} return true; }; const api = { pageType: detectPageType(), register(definition) { definitions.push(definition); }, start() { if (started) return; started = true; reconcile('startup', true); }, reconcile, ensureUi(reason = 'health-check') { try { window.dispatchEvent(new CustomEvent('roomgrid:ensure-ui', { detail: { generation, pageType: api.pageType, reason } })); } catch (_) {} }, stop() { retryTimers.forEach(timer => clearTimeout(timer)); retryTimers.clear(); for (const id of [...running.keys()].reverse()) stopDefinition(id); started = false; }, routeMatrix() { return ROOMGRID_ROUTE_MATRIX.map(item => ({ ...item, actual: detectPageType(new URL(item.sample, location.origin)), ok: detectPageType(new URL(item.sample, location.origin)) === item.expected })); }, status() { return { pageType: api.pageType, generation, routeKey: lastRouteKey || normalizedRouteKey(), lastReason, lastNavigationAt, running: [...running.keys()], errors: errors.slice(-20), routeHistory: routeHistory.slice(-12), }; }, }; return api; })(); const RoomGridNavigation = (() => { let installed = false; let routePollTimer = 0; let mutationObserver = null; let scheduled = 0; let lastObservedKey = ''; const originals = new Map(); const handlers = {}; const schedule = (reason = 'navigation', delay = 0) => { clearTimeout(scheduled); scheduled = setTimeout(() => { scheduled = 0; const key = normalizedRouteKey(); if (key === lastObservedKey && reason !== 'startup' && reason !== 'ensure') return; lastObservedKey = key; SuiteRuntime.reconcile(reason, reason === 'ensure'); }, Math.max(0, delay)); }; const schedulePoll = () => { clearTimeout(routePollTimer); routePollTimer = setTimeout(() => { if (normalizedRouteKey() !== lastObservedKey) schedule('poll'); schedulePoll(); }, document.hidden ? 5000 : 1000); }; const install = () => { if (installed) return; installed = true; lastObservedKey = normalizedRouteKey(); const historyApi = window.history; if (historyApi) ['pushState', 'replaceState'].forEach(method => { const original = historyApi[method]; if (typeof original !== 'function') return; originals.set(method, original); historyApi[method] = function roomGridHistoryNavigation(...args) { const result = original.apply(this, args); schedule(`history.${method}`); return result; }; }); handlers.popstate = () => schedule('popstate'); handlers.hashchange = () => schedule('hashchange'); handlers.pageshow = () => schedule('pageshow', 20); handlers.visibility = () => { schedulePoll(); if (!document.hidden) schedule('visibility', 20); }; handlers.click = event => { const link = event.target?.closest?.('a[href]'); if (!link) return; let url = null; try { url = new URL(link.href, location.href); } catch (_) {} const model = url ? roomGridNativeRoomId(url) : ''; if (model) { const target = roomGridSingleCamUrl(model, { source: 'model-link' }); event.preventDefault(); event.stopImmediatePropagation(); if (event.ctrlKey || event.metaKey || event.shiftKey || link.target === '_blank') openNoopener(target); else location.href = target; return; } if (url?.origin === location.origin) { setTimeout(() => schedule('same-origin-link'), 0); setTimeout(() => schedule('same-origin-link-late'), 500); } }; handlers.auxclick = event => { if (event.button !== 1) return; const link = event.target?.closest?.('a[href]'); if (!link) return; let url = null; try { url = new URL(link.href, location.href); } catch (_) {} const model = url ? roomGridNativeRoomId(url) : ''; if (!model) return; event.preventDefault(); event.stopImmediatePropagation(); openNoopener(roomGridSingleCamUrl(model, { source: 'model-link' })); }; window.addEventListener('popstate', handlers.popstate); window.addEventListener('hashchange', handlers.hashchange); window.addEventListener('pageshow', handlers.pageshow); document.addEventListener('visibilitychange', handlers.visibility); document.addEventListener('click', handlers.click, true); document.addEventListener('auxclick', handlers.auxclick, true); if (typeof MutationObserver === 'function' && document.documentElement) { mutationObserver = new MutationObserver(() => { if (normalizedRouteKey() !== lastObservedKey) schedule('dom-route-change', 20); }); mutationObserver.observe(document.documentElement, { childList: true, subtree: true }); } schedulePoll(); }; const stop = () => { if (!installed) return; installed = false; clearTimeout(scheduled); clearTimeout(routePollTimer); mutationObserver?.disconnect(); mutationObserver = null; window.removeEventListener('popstate', handlers.popstate); window.removeEventListener('hashchange', handlers.hashchange); window.removeEventListener('pageshow', handlers.pageshow); document.removeEventListener('visibilitychange', handlers.visibility); document.removeEventListener('click', handlers.click, true); document.removeEventListener('auxclick', handlers.auxclick, true); originals.forEach((original, method) => { if (window.history?.[method]?.name === 'roomGridHistoryNavigation') window.history[method] = original; }); originals.clear(); }; return { install, stop, schedule }; })(); const RoomGridRouteHealth = (() => { const selectors = Object.freeze({ home: ['[data-testid="room-list"]', '#roomlist_root', '.room_list', '[data-testid="room-card"]'], nativeDiscover: ['[data-testid="room-list"]', '[data-testid="room-card"]', 'main [class*="discover" i]', '#native-discover'], tags: ['main table', 'table a[href*="/tag"]', '[data-testid*="tag" i]'], tag: ['[data-testid="room-list"]', '[data-testid="room-card"]', '.room_list'], following: ['[data-testid="room-list"]', '[data-testid="room-card"]', 'main [data-testid*="empty" i]', 'main [class*="empty" i]'], privateShows: ['[data-testid="room-list"]', '[data-testid="room-card"]', 'main [data-testid*="empty" i]', 'main [class*="empty" i]'], }); let timer = 0; let secondPassTimer = 0; let banner = null; let style = null; let last = { status: 'pending', pageType: detectPageType(), detail: '', checkedAt: 0 }; const removeBanner = () => { banner?.remove(); banner = null; }; const ensureStyle = () => { if (style?.isConnected) return; style = $('style', { id: 'roomgrid-route-health-style', html: trustedHtml(` .roomgrid-route-health{position:fixed;left:50%;top:110px;z-index:2147482400;width:min(680px,calc(100vw - 24px));transform:translateX(-50%);padding:13px 14px;border:1px solid #9a5b1f;border-left:5px solid #f47321;border-radius:7px;background:#172533;color:#f1f5f8;box-shadow:0 16px 44px rgba(0,0,0,.4);font:12px/1.45 Arial,Helvetica,sans-serif}.roomgrid-route-health strong{display:block;margin-bottom:4px;font-size:14px}.roomgrid-route-health p{margin:0;color:#c5d0d9}.roomgrid-route-health-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.roomgrid-route-health button{min-height:32px;padding:6px 10px;border:1px solid #526476;border-radius:5px;background:#233443;color:#f1f5f8;cursor:pointer;font-weight:700}.roomgrid-route-health button.primary{border-color:#f47321;background:#f47321;color:#fff}.roomgrid-route-health button:hover{border-color:#73cef7} `) }); document.head?.appendChild(style); }; const meaningfulContent = pageType => { const list = selectors[pageType] || []; return list.some(selector => { try { return !!document.querySelector(selector); } catch (_) { return false; } }); }; const showBanner = pageType => { ensureStyle(); removeBanner(); const title = tx('页面内容仍未出现', 'Page content has not appeared yet', 'Seiteninhalt ist noch nicht erschienen'); const copy = tx('RoomGrid 已重新检查页面,但仍未识别到 Chaturbate 的内容区域。页面可能仍在加载,或者网站返回了空白视图。', 'RoomGrid checked the page again but still cannot find Chaturbate\'s content area. The page may still be loading or the site returned an empty view.', 'RoomGrid hat die Seite erneut geprüft, erkennt aber weiterhin keinen Chaturbate-Inhaltsbereich. Die Seite lädt möglicherweise noch oder die Website hat eine leere Ansicht geliefert.'); const suiteUrl = new URL(location.href); suiteUrl.searchParams.delete('multicam_mode'); suiteUrl.searchParams.set('multicam_discover', '1'); suiteUrl.searchParams.set('suite_view', 'discover'); suiteUrl.hash = ''; banner = $('section', { class: 'roomgrid-route-health', role: 'alert', dataset: { roomgridRouteHealth: pageType } }, [ $('strong', {}, title), $('p', {}, copy), $('div', { class: 'roomgrid-route-health-actions' }, [ $('button', { type: 'button', class: 'primary', onclick: () => probe(true) }, tx('再次检查', 'Check again', 'Erneut prüfen')), $('button', { type: 'button', onclick: () => { location.href = suiteUrl.toString(); } }, tx('打开 RoomGrid Entdecken', 'Open RoomGrid Discover', 'RoomGrid Entdecken öffnen')), $('button', { type: 'button', onclick: () => location.reload() }, tx('重新加载页面', 'Reload page', 'Seite neu laden')), $('button', { type: 'button', onclick: removeBanner }, tx('隐藏提示', 'Hide notice', 'Hinweis ausblenden')), ]), ]); document.body?.appendChild(banner); }; const probe = (manual = false) => { clearTimeout(secondPassTimer); const pageType = detectPageType(); if (!Object.hasOwn(selectors, pageType)) { removeBanner(); last = { status: 'not-applicable', pageType, detail: 'no native content probe', checkedAt: Date.now() }; return; } SuiteRuntime.ensureUi(manual ? 'manual-route-health' : 'route-health'); if (meaningfulContent(pageType)) { removeBanner(); last = { status: 'ok', pageType, detail: 'native content anchor found', checkedAt: Date.now() }; Diagnostics.mark(`route:${pageType}`, 'ok', last.detail); return; } last = { status: 'checking', pageType, detail: 'waiting for native content anchor', checkedAt: Date.now() }; secondPassTimer = setTimeout(() => { if (detectPageType() !== pageType) return; SuiteRuntime.ensureUi('route-health-second-pass'); if (meaningfulContent(pageType)) { removeBanner(); last = { status: 'ok', pageType, detail: 'native content anchor found after retry', checkedAt: Date.now() }; Diagnostics.mark(`route:${pageType}`, 'ok', last.detail); } else { last = { status: 'degraded', pageType, detail: 'native content anchor unavailable after retry', checkedAt: Date.now() }; Diagnostics.mark(`route:${pageType}`, 'degraded', last.detail); showBanner(pageType); } }, manual ? 700 : 1400); }; const schedule = () => { clearTimeout(timer); clearTimeout(secondPassTimer); removeBanner(); last = { status: 'pending', pageType: detectPageType(), detail: 'scheduled', checkedAt: Date.now() }; timer = setTimeout(() => probe(false), 5200); }; const init = () => { window.addEventListener('roomgrid:navigation', schedule); schedule(); }; const stop = () => { clearTimeout(timer); clearTimeout(secondPassTimer); window.removeEventListener('roomgrid:navigation', schedule); removeBanner(); style?.remove(); style = null; }; return { init, stop, probe, status: () => ({ ...last }) }; })(); SuiteRuntime.register({ id: 'workstation', pages: ['workstation'], start: initWorkstation }); SuiteRuntime.register({ id: 'discover', pages: ['discover'], start: initDiscover }); SuiteRuntime.register({ id: 'customHomeRedirect', pages: ['home'], start: initCustomHomeRedirect }); SuiteRuntime.register({ id: 'pageTools', pages: ROOMGRID_NORMAL_PAGE_TYPES, optional: true, start: initNativeSiteShell }); /* ============================================================= * 7. 普通页面注入:浮动按钮 + 快捷键 * ============================================================= */ function initNativeDiscoverGuard() { const nativeHubLinksCleanup = RoomGridNativeHub.lightweightHeaderLinks(); const catalogAnalytics = createAnalytics(); let catalogObserver = null; let disposed = false; const observedCards = new WeakSet(); const rememberVisibleModels = () => { if (disposed || document.hidden) return; let checked = 0; const candidates = document.querySelectorAll('[data-testid="room-card"],[data-username],li.RoomCard,li.roomCard,li.room_list_room'); for (const card of candidates) { if (++checked > 1600) break; if (observedCards.has(card)) continue; let id = normalizeUsername(card.getAttribute?.('data-username') || card.getAttribute?.('data-room') || ''); if (!isLikelyUsername(id)) { const link = [...(card.querySelectorAll?.('a[href]') || [])].find(item => /^\/[a-zA-Z0-9_-]+\/?(?:[?#].*)?$/u.test(item.getAttribute('href') || '')); id = normalizeUsername((link?.getAttribute('href') || '').split(/[/?#]/u).filter(Boolean)[0] || ''); } if (!isLikelyUsername(id)) continue; observedCards.add(card); const image = card.querySelector?.('img[src]'); const subject = card.querySelector?.('[data-testid*="subject" i],[class*="subject" i]')?.textContent || card.getAttribute?.('data-room-subject') || ''; const tags = [...(card.querySelectorAll?.('a[href*="/tag/"]') || [])].map(link => String(link.textContent || '').replace(/^#/u, '').trim()).filter(Boolean).slice(0, 20); catalogAnalytics.observeModel({ id, status: card.getAttribute?.('data-status') || card.getAttribute?.('data-room-status') || '', subject, imageUrl: image?.currentSrc || image?.src || '', tags, source: 'native-discover', observedAt: Date.now() }); } catalogAnalytics.flush().catch(() => {}); }; const rememberSoon = debounce(rememberVisibleModels, 650); catalogAnalytics.init(Storage.load().settings.analyticsEnabled !== false).then(() => { if (disposed) return; rememberVisibleModels(); catalogObserver = new MutationObserver(rememberSoon); catalogObserver.observe(document.body, { childList: true, subtree: true }); }).catch(error => Diagnostics.report('analytics', error, 'native-discover catalog')); const style = $('style', { id: 'roomgrid-native-discover-guard-style', html: trustedHtml(` .roomgrid-native-discover-version{position:fixed;top:8px;left:168px;z-index:2147481200;display:inline-flex;align-items:center;min-height:24px;padding:4px 9px;border:1px solid rgba(125,211,252,.45);border-radius:999px;background:rgba(15,23,42,.9);color:#e0f2fe;box-shadow:0 5px 18px rgba(2,6,23,.25);backdrop-filter:blur(10px);font:800 10px/1.2 Inter,system-ui,sans-serif;letter-spacing:.02em;pointer-events:none;white-space:nowrap} @media(max-width:700px){.roomgrid-native-discover-version{top:52px;left:8px}} `) }); const badge = $('div', { id: 'roomgrid-active-version', class: 'roomgrid-native-discover-version', dataset: { roomgridActiveVersion: META.version, roomgridNativeDiscoverGuard: '1' }, title: tx('当前启用的 RoomGrid 版本', 'Currently active RoomGrid version', 'Aktuell aktive RoomGrid-Version'), ariaLabel: tx('当前启用的 RoomGrid 版本', 'Currently active RoomGrid version', 'Aktuell aktive RoomGrid-Version'), }, [$('span', { dataset: { roomgridBrandName: '1' } }, 'RoomGrid'), ' ', $('span', { dataset: { roomgridBrandVersion: '1' } }, `v${META.version}`)]); document.head.appendChild(style); document.body.appendChild(badge); return () => { disposed = true; catalogObserver?.disconnect(); catalogObserver = null; catalogAnalytics.flush().catch(() => {}); nativeHubLinksCleanup?.(); badge.remove(); style.remove(); }; } function initInjector() { const ROOM_PATH = /^\/([a-zA-Z0-9_-]+)\/?$/; let injectorDisposed = false; const idleJobs = []; const deferNonCritical = (callback, timeout = 700) => { const run = () => { if (!injectorDisposed) callback(); }; if ('requestIdleCallback' in window) { const id = requestIdleCallback(run, { timeout }); idleJobs.push(() => cancelIdleCallback?.(id)); } else { const id = setTimeout(run, 0); idleJobs.push(() => clearTimeout(id)); } }; const suiteStore = createStore(); const suiteAnalytics = createAnalytics(); const audioSilencerCleanup = RoomGridAudioSilencer.init(); const suiteNotifications = createNotificationCenter(suiteStore); const suiteWindows = createWindowCoordinator(suiteStore); installSuiteExperienceStyles(); applySuiteAppearance(suiteStore); const normalPageCleanup = installNormalPageExperience(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows); PrivacyShield.init(suiteStore); Diagnostics.init(); const ultimateCleanup = RoomGridUltimate.init(suiteStore, suiteAnalytics); const nativeHubCleanup = RoomGridNativeHub.init(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows); ROOMGRID_BOOT_METRICS.criticalUiReadyAt = performance.now(); deferNonCritical(() => { const progressTask = suiteNotifications.beginProgress?.(tx('本地使用数据正在准备', 'Preparing local usage data', 'Lokale Nutzungsdaten werden vorbereitet'), { delayMs: 280, background: true }); suiteAnalytics.init(suiteStore.state.settings.analyticsEnabled !== false) .catch(error => Diagnostics.report('analytics', error, 'normal-page initialization')) .finally(() => progressTask?.finish()); }, 320); const requestedControlTab = new URLSearchParams(location.search).get('roomgrid_control') || (new URLSearchParams(location.search).get('roomgrid_home_options') === '1' ? 'homepage' : ''); if (['workspaces', 'library', 'data', 'homepage', 'appearance', 'analytics', 'broadcaster', 'notifications', 'privacy', 'performance', 'extras', 'diagnostics'].includes(requestedControlTab)) setTimeout(() => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, requestedControlTab), 220); suiteNotifications.start(); suiteWindows.start(); deferNonCritical(() => OptionalTools.capture(document), 1200); // ---- 当前房间(响应式:URL / canonical / DOM 变化时自动重算)---- let currentRoom = null; const currentRoomSubs = new Set(); function extractRoomFromPath(pathname) { const m = (pathname || '').match(ROOM_PATH); const name = m ? normalizeUsername(m[1]) : null; return isLikelyUsername(name) ? name : null; } function extractRoomFromUrl(url) { if (!url) return null; try { return extractRoomFromPath(new URL(url, location.origin).pathname); } catch (_) { return null; } } function detectCurrentRoom() { // 1. URL 路径:// let next = extractRoomFromPath(location.pathname); if (next) return next; // 2. SPA 场景:URL/DOM 被异步替换时,canonical/og:url 往往先更新。 const candidates = [ document.querySelector('link[rel="canonical"]')?.href, document.querySelector('meta[property="og:url"]')?.content, document.querySelector('meta[name="twitter:url"]')?.content, ]; for (const href of candidates) { next = extractRoomFromUrl(href); if (next) return next; } return null; } function recalcCurrentRoom() { const next = detectCurrentRoom(); if (next !== currentRoom) { currentRoom = next; currentRoomSubs.forEach(fn => { try { fn(currentRoom); } catch (_) {} }); } } const recalcCurrentRoomSoon = debounce(recalcCurrentRoom, 180); recalcCurrentRoom(); // hook history pushState/replaceState(chaturbate 是 SPA,URL 变化时不刷新) ['pushState', 'replaceState'].forEach(method => { const orig = history[method]; history[method] = function () { const ret = orig.apply(this, arguments); setTimeout(recalcCurrentRoom, 0); setTimeout(recalcCurrentRoom, 250); setTimeout(recalcCurrentRoom, 900); return ret; }; }); window.addEventListener('popstate', () => setTimeout(recalcCurrentRoom, 50)); window.addEventListener('hashchange', () => setTimeout(recalcCurrentRoom, 50)); // 监听同标签内的异步切换,解决「换人后加入状态不变」。 try { const routeMo = new MutationObserver(recalcCurrentRoomSoon); if (document.head) routeMo.observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'content'] }); if (document.body) routeMo.observe(document.body, { childList: true, subtree: true }); } catch (_) {} // poll 兜底(万一还有别的 navigation 路径漏了);隐藏标签页降低频率。 let routePollTimer = 0; function scheduleRoutePoll() { clearTimeout(routePollTimer); const ms = document.hidden ? INJECTOR_ROUTE_POLL_HIDDEN_MS : INJECTOR_ROUTE_POLL_VISIBLE_MS; routePollTimer = setTimeout(() => { recalcCurrentRoom(); scheduleRoutePoll(); }, ms); } scheduleRoutePoll(); document.addEventListener('visibilitychange', scheduleRoutePoll); // 跨标签页同步 const storageSubs = new Set(); const fireStorageSubs = () => storageSubs.forEach(fn => { try { fn(); } catch (_) {} }); window.addEventListener('storage', (e) => { if (e.key === STORE_KEY) fireStorageSubs(); }); window.addEventListener('ryujo_multicam_storage', fireStorageSubs); function refreshInjectorState() { recalcCurrentRoom(); fireStorageSubs(); } // SPA 路由兜底:CB 有些入口不会稳定走 pushState/popstate。 // 用点击、hash、pageshow、DOM 变化做低成本监听,保证同标签切换主播后“已加入/加入”状态实时变。 const routeCheckSoon = debounce(() => recalcCurrentRoom(), 80); document.addEventListener('click', (e) => { const a = e.target && e.target.closest ? e.target.closest('a[href]') : null; if (!a) return; setTimeout(routeCheckSoon, 0); setTimeout(routeCheckSoon, 160); setTimeout(routeCheckSoon, 650); }, true); window.addEventListener('hashchange', routeCheckSoon); window.addEventListener('pageshow', routeCheckSoon); document.addEventListener('visibilitychange', routeCheckSoon); try { const routeMo = new MutationObserver(routeCheckSoon); routeMo.observe(document.body, { childList: true, subtree: true }); } catch (_) {} const buildWorkstationUrl = () => { const u = new URL(location.href); u.searchParams.set('multicam_mode', '1'); u.searchParams.delete('multicam_discover'); u.hash = ''; return u.toString(); }; const buildDiscoverUrl = () => { const u = new URL(location.href); u.searchParams.delete('multicam_mode'); u.searchParams.set('multicam_discover', '1'); u.searchParams.set('suite_view', 'discover'); u.hash = ''; return u.toString(); }; const openWorkstationNew = () => { // 新开工作台前静音并停止当前页面媒体,避免“新工作台已开但旧页面还在出声”。 stopAllPageMedia(); openNoopener(buildWorkstationUrl()); }; const openWorkstationHere = () => { // 把工作台直接挂到当前页面(破坏性,但有用户要求) // 实现:跳转到 ?multicam_mode=1 同 tab;跳转前先停掉当前页面媒体。 stopAllPageMedia(); location.href = buildWorkstationUrl(); }; const pageCommandPalette = createCommandPalette(suiteStore, () => [ { icon: '▦', title: tx('打开MultiCam', 'Open MultiCam', 'MultiCam öffnen'), detail: tx('在新窗口中打开工作台', 'Open workstation in a new window', 'Arbeitsbereich in neuem Fenster öffnen'), shortcut: 'Alt+M', run: openWorkstationNew }, { icon: '⌁', title: tx('打开发现', 'Open Discover', 'Entdecken öffnen'), run: () => openNoopener(buildDiscoverUrl()) }, { icon: '▥', title: tx('统计中心', 'Statistics center', 'Statistikzentrum'), run: () => RoomGridNativeHub.open('stats') }, { icon: '★', title: tx('模型库', 'Model library', 'Modellbibliothek'), run: () => RoomGridNativeHub.open('library') }, { icon: '◎', title: tx('我的用户', 'My user', 'Mein Nutzer'), run: () => RoomGridNativeHub.open('user') }, { icon: '◫', title: tx('工作区', 'Workspaces', 'Arbeitsbereiche'), run: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'workspaces') }, { icon: '⇅', title: tx('备份和导入', 'Backup and import', 'Sicherung und Import'), run: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'data') }, { icon: '◐', title: tx('页面和设计选项', 'Page and design options', 'Seiten- und Designoptionen'), run: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'appearance') }, { icon: '×', title: tx('立即隐藏', 'Hide immediately', 'Sofort ausblenden'), shortcut: 'Ctrl+Shift+X', run: () => PrivacyShield.activate() }, ], { onModel: id => { location.href = roomGridSingleCamUrl(id, { source: 'command' }); } }); function getPrimaryPageVideo() { const videos = [...document.querySelectorAll('video')] .filter(v => v && v.offsetWidth > 120 && v.offsetHeight > 80 && !v.ended); return videos.sort((a, b) => (b.offsetWidth * b.offsetHeight) - (a.offsetWidth * a.offsetHeight))[0] || null; } function captureCurrentPageVideo() { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; const video = getPrimaryPageVideo(); if (!video || !video.videoWidth || !video.videoHeight) { toast(t('dockVideoMissing')); return; } try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob(blob => { if (!blob) { toast(t('captureFailed')); return; } downloadBlob(blob, `roomgrid-page-${safeFilePart(currentRoom || 'video')}-${stampForFile()}.png`); toast(t('screenshotSaved')); }, 'image/png'); } catch (_) { toast(t('captureFailed')); } } async function toggleCurrentPagePiP() { const video = getPrimaryPageVideo(); if (!video) { toast(t('dockVideoMissing')); return; } try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await video.requestPictureInPicture(); } catch (_) { toast(t('dockVideoMissing')); } } function toggleCurrentPagePlayback() { const video = getPrimaryPageVideo(); if (!video) { toast(t('dockVideoMissing')); return; } if (video.paused) video.play?.().catch?.(() => {}); else video.pause?.(); } function toggleCurrentPageMute() { const video = getPrimaryPageVideo(); if (!video) { toast(t('dockVideoMissing')); return; } video.muted = !video.muted; if (!video.muted && video.volume === 0) video.volume = 0.5; } function queueCurrentRoomRecording() { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; if (!currentRoom) { toast(t('dockNoRoom')); return; } if (!Storage.has(currentRoom)) Storage.add(currentRoom); setRecordingIntent(currentRoom, true); toast(t('dockRecordQueued')); openWorkstationNew(); } // ---- RoomGrid 工具坞 ---- const dockStyle = $('style', { html: trustedHtml(` .roomgrid-dock { position:fixed; right:18px; bottom:18px; z-index:99999; width:292px; font-family:Arial,Helvetica,sans-serif; color:var(--rg-text,#f1f5f8); user-select:none; } .roomgrid-dock-card { border:1px solid var(--rg-border,#405061); border-radius:8px; background:var(--rg-panel,#172533); box-shadow:0 12px 34px rgba(0,0,0,.32); overflow:hidden; } .roomgrid-dock-head { width:100%; border:0; border-bottom:3px solid var(--rg-accent,#0b9ddd); display:flex; align-items:center; gap:10px; padding:10px 12px; cursor:pointer; color:inherit; background:var(--rg-surface-strong,#1f2e3d); text-align:left; } .roomgrid-dock-mark { width:34px; height:34px; border:1px solid var(--rg-border,#4a5b6c); border-radius:5px; display:grid; place-items:center; background:var(--rg-secondary-soft,#26394a); color:var(--rg-secondary,#73cef7); font-weight:900; letter-spacing:.02em; } .roomgrid-dock-title { font-size:14px; font-weight:850; line-height:1.1; } .roomgrid-dock-sub { margin-top:2px; font-size:11px; color:var(--rg-muted,#aeb9c3); } .roomgrid-dock-chevron { margin-left:auto; font-size:16px; opacity:.82; } .roomgrid-dock-body { display:grid; gap:10px; padding:10px; } .roomgrid-dock-room { font-size:12px; color:var(--rg-muted,#b9c3cc); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .roomgrid-dock-actions { display:grid; grid-template-columns:1fr 1fr; gap:7px; } .roomgrid-dock-action { min-height:34px; border:1px solid var(--rg-border,#445465); border-radius:5px; background:var(--rg-surface,#233443); color:var(--rg-text,#f1f5f8); cursor:pointer; font-size:12px; font-weight:700; text-align:left; padding:7px 9px; } .roomgrid-dock-action:hover { background:var(--rg-secondary-soft,#2b4052); border-color:var(--rg-secondary,#69c9f2); } .roomgrid-dock-action.primary { background:var(--rg-accent,#f47321); border-color:var(--rg-accent,#f58a47); color:var(--rg-accent-text,#fff); } .roomgrid-dock-action.success { background:#287b55; border-color:#40936b; } .roomgrid-dock-action.warn { background:#9a5b1f; border-color:#b87330; } .roomgrid-dock-foot { display:flex; justify-content:space-between; gap:8px; border-top:1px solid var(--rg-border,#364655); padding-top:9px; } .roomgrid-dock-link { border:0; background:transparent; color:var(--rg-secondary,#75cdf4); cursor:pointer; font-size:11px; padding:2px 0; } .roomgrid-dock-link:hover { color:#b9e9ff; text-decoration:underline; } .roomgrid-dock.is-collapsed { width:auto; } .roomgrid-dock.is-collapsed .roomgrid-dock-card { border-radius:999px; } .roomgrid-dock.is-collapsed .roomgrid-dock-body { display:none; } .roomgrid-dock.is-collapsed .roomgrid-dock-head { border-radius:999px; padding:8px 10px; } .roomgrid-dock.is-collapsed .roomgrid-dock-sub, .roomgrid-dock.is-collapsed .roomgrid-dock-chevron { display:none; } `)}); document.head.appendChild(dockStyle); const root = $('div', { class: 'roomgrid-dock' }); let collapsed = localStorage.getItem('ryujo_fab_collapsed') === '1'; let dragged = false, sx, sy, ox, oy; const roomLine = $('div', { class: 'roomgrid-dock-room' }, t('dockNoRoom')); const addBtn = $('button', { class: 'roomgrid-dock-action success', onclick: () => toggleCurrentRoomSaved() }, t('dockAdd')); const head = $('button', { class: 'roomgrid-dock-head', title: 'Alt+M / Alt+A', onclick: () => { if (!dragged) toggleDock(); } }, [ $('span', { class: 'roomgrid-dock-mark' }, roomGridBrandMark(suiteStore.state.settings, { size: 22 })), $('span', {}, [ $('div', { class: 'roomgrid-dock-title', dataset: { roomgridBrandName: '1' } }, roomGridBrandIdentity(suiteStore.state.settings).name), $('div', { class: 'roomgrid-dock-sub', dataset: { roomgridBrandTagline: '1', roomgridBrandTaglineFallback: t('dockSubtitle') } }, roomGridBrandIdentity(suiteStore.state.settings).tagline || t('dockSubtitle')), ]), $('span', { class: 'roomgrid-dock-chevron' }, '▾'), ]); const body = $('div', { class: 'roomgrid-dock-body' }, [ roomLine, $('div', { class: 'roomgrid-dock-actions' }, [ $('button', { class: 'roomgrid-dock-action primary', dataset: { roomgridDockMainMenu: '1' }, onclick: event => RoomGridNativeHub.openMenu(event.currentTarget) }, tx('所有工具', 'All tools', 'Alle Werkzeuge')), $('button', { class: 'roomgrid-dock-action primary', onclick: openWorkstationNew }, t('dockOpen')), $('button', { class: 'roomgrid-dock-action primary', onclick: () => { stopAllPageMedia(); openNoopener(buildDiscoverUrl()); } }, tx('发现', 'Discover', 'Entdecken')), $('button', { class: 'roomgrid-dock-action', onclick: () => RoomGridNativeHub.open('library') }, tx('模型库', 'Model library', 'Modellbibliothek')), $('button', { class: 'roomgrid-dock-action', onclick: () => RoomGridNativeHub.open('stats') }, tx('统计中心', 'Statistics center', 'Statistikzentrum')), $('button', { class: 'roomgrid-dock-action', onclick: () => pageCommandPalette.open() }, tx('搜索与命令', 'Search & commands', 'Suche & Befehle')), $('button', { class: 'roomgrid-dock-action', onclick: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'appearance') }, tx('页面设计', 'Page design', 'Seitendesign')), suiteModuleEnabled('optionalTools') ? $('button', { class: 'roomgrid-dock-action', onclick: () => { OptionalTools.capture(document); openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'extras'); } }, tx('可选工具', 'Optional tools', 'Nebenmodule')) : null, $('button', { class: 'roomgrid-dock-action', onclick: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'diagnostics') }, tx('诊断', 'Diagnostics', 'Diagnose')), addBtn, $('button', { class: 'roomgrid-dock-action', onclick: () => RoomGridNativeHub.open('user') }, tx('我的用户', 'My user', 'Mein Nutzer')), $('button', { class: 'roomgrid-dock-action', onclick: () => openControlCenter(suiteStore, suiteAnalytics, suiteNotifications, suiteWindows, 'homepage') }, tx('自定义首页', 'Customize homepage', 'Startseite anpassen')), $('button', { class: 'roomgrid-dock-action', onclick: () => openNoopener('https://chaturbate.com/my_collection/') }, tx('官方收藏', 'Official collection', 'Offizielle Sammlung')), $('button', { class: 'roomgrid-dock-action', onclick: toggleCurrentPagePiP }, t('dockPip')), $('button', { class: 'roomgrid-dock-action', onclick: toggleCurrentPagePlayback }, t('dockPause')), $('button', { class: 'roomgrid-dock-action', onclick: toggleCurrentPageMute }, t('dockMute')), $('button', { class: 'roomgrid-dock-action', onclick: () => { if (confirm(t('openWorkstationHereConfirm'))) openWorkstationHere(); } }, t('dockOpenHere')), ]), $('div', { class: 'roomgrid-dock-foot' }, [ $('button', { class: 'roomgrid-dock-link', onclick: () => { const s = Storage.load(); toast(t('memoryStat', s.rooms.length), 2500); } }, t('memoryView')), $('button', { class: 'roomgrid-dock-link', onclick: () => { collapsed = true; syncDock(); localStorage.setItem('ryujo_fab_collapsed', '1'); } }, t('collapseFAB')), ]), ]); function toggleCurrentRoomSaved() { if (!currentRoom) return; if (Storage.has(currentRoom)) { if (Storage.remove(currentRoom)) toast(t('removedNamed', currentRoom)); } else { const r = Storage.add(currentRoom); toast(r === 'added' ? t('addedNamed', currentRoom) : r === 'exists' ? t('exists') : t('addFailed')); } refreshInjectorState(); updateDockRoom(); } function updateDockRoom() { roomLine.textContent = currentRoom ? t('dockCurrentRoom', currentRoom) : t('dockNoRoom'); addBtn.textContent = currentRoom && Storage.has(currentRoom) ? t('dockRemove') : t('dockAdd'); addBtn.classList.toggle('success', !(currentRoom && Storage.has(currentRoom))); addBtn.classList.toggle('warn', !!(currentRoom && Storage.has(currentRoom))); addBtn.disabled = !currentRoom; addBtn.style.opacity = currentRoom ? '1' : '.55'; } currentRoomSubs.add(updateDockRoom); storageSubs.add(updateDockRoom); function syncDock() { root.classList.toggle('is-collapsed', !!collapsed); root.querySelector('.roomgrid-dock-chevron').textContent = collapsed ? '▴' : '▾'; } const toggleDock = () => { collapsed = !collapsed; localStorage.setItem('ryujo_fab_collapsed', collapsed ? '1' : '0'); syncDock(); }; root.appendChild($('div', { class: 'roomgrid-dock-card' }, [head, body])); document.body.appendChild(root); updateDockRoom(); syncDock(); head.addEventListener('mousedown', (e) => { sx = e.clientX; sy = e.clientY; const rect = root.getBoundingClientRect(); ox = rect.left; oy = rect.top; dragged = false; const move = (ev) => { if (Math.abs(ev.clientX - sx) + Math.abs(ev.clientY - sy) > 6) dragged = true; if (dragged) { root.style.left = (ox + ev.clientX - sx) + 'px'; root.style.top = (oy + ev.clientY - sy) + 'px'; root.style.right = 'auto'; root.style.bottom = 'auto'; } }; const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); // —— 快捷键 —— document.addEventListener('keydown', (e) => { if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; if (e.key.toLowerCase() === 'm') { e.preventDefault(); openWorkstationNew(); } if (e.key.toLowerCase() === 'a' && currentRoom) { e.preventDefault(); if (Storage.has(currentRoom)) toast(t('alreadyAdded', currentRoom)); else { const r = Storage.add(currentRoom); toast(r === 'added' ? t('addedNamed', currentRoom) : t('addFailed')); } refreshInjectorState(); updateDockRoom(); } }); // —— Toast —— function toast(text, ms = 1800) { const toastEl = $('div', { style: { position: 'fixed', left: '50%', top: '20%', transform: 'translateX(-50%)', background: 'rgba(20,20,24,.95)', color: '#fff', padding: '12px 20px', borderRadius: '10px', zIndex: 999999, fontSize: '14px', fontFamily: 'system-ui', boxShadow: '0 8px 24px rgba(0,0,0,.5)', backdropFilter: 'blur(10px)', }, }, String(text || '')); document.body.appendChild(toastEl); setTimeout(() => toastEl.remove(), ms); } // =========================================================== // QuickAdd —— 在 chaturbate 主页/分类页的房间卡片上注入「+」按钮 // 用 MutationObserver 监听动态加载,[data-username] 是稳定锚点 // =========================================================== const quickAddCleanup = initQuickAdd(); const normalPreviewCleanup = initNormalPageLivePreviews(); // 15.9.18: bereits von einer älteren Scriptinstanz eingefügte Zusatz-Tags entfernen. try { document.querySelectorAll('.multicam-room-tags[data-roomgrid-tags="1"]').forEach(el => el.remove()); } catch (_) {} function initQuickAdd() { // 注入 QuickAdd 按钮的样式 const style = $('style', { html: trustedHtml(` .multicam-quick-add { position: absolute; top: 6px; right: 6px; z-index: 99; min-width: 58px; height: 27px; border-radius: 999px; border: 1px solid rgba(255,255,255,.18); cursor: pointer; background: rgba(15,23,42,.62); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(6px); color: #fff; font-size: 11px; font-weight: 800; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity .15s, background .15s, transform .15s; padding: 0 9px; line-height: 1; box-shadow: 0 8px 22px rgba(0,0,0,.28); } .multicam-quick-add:hover { background: #2563eb; transform: translateY(-1px); } .multicam-qa-host:hover .multicam-quick-add, .multicam-quick-add:focus, .multicam-quick-add.added { opacity: 1; } .multicam-quick-add.added { background: #16a34a; opacity: 1; } .multicam-quick-add.added:hover { background: #c62828; } .multicam-quick-strip{position:absolute;top:6px;right:6px;z-index:99;display:flex;gap:5px;opacity:0;transition:opacity .15s}.multicam-qa-host:hover .multicam-quick-strip,.multicam-quick-strip:focus-within{opacity:1}.multicam-quick-strip .multicam-quick-add{position:static;opacity:1}.multicam-quick-mini{width:28px;height:27px;border:1px solid rgba(255,255,255,.18);border-radius:999px;background:rgba(15,23,42,.68);color:#fff;cursor:pointer;font-weight:900;backdrop-filter:blur(8px)}.multicam-quick-mini.active{background:#d97706;color:#fff}.multicam-quick-mini.watch{background:#2563eb}.rg-hide-quick-actions .multicam-quick-strip{display:none!important} `)}); document.head.appendChild(style); const observed = new WeakSet(); // 已注入按钮的元素 const elToUsername = new WeakMap(); // 元素 → 用户名 function updateBtnState(btn, username) { const inList = Storage.has(username); btn.classList.toggle('added', inList); btn.textContent = inList ? tx('已保存', 'Saved', 'Gespeichert') : 'Grid +'; btn.title = inList ? t('quickRemoveTitle') : t('quickAddTitle'); } function observeCard(host, username) { const image = host.querySelector?.('img[src]'); const subjectNode = host.querySelector?.('[data-testid*="subject" i],[class*="subject" i],[data-room-subject]'); const tags = [...(host.querySelectorAll?.('a[href*="/tag/"]') || [])].map(link => String(link.textContent || '').replace(/^#/u, '').trim()).filter(Boolean).slice(0, 20); suiteAnalytics.observeModel?.({ id: username, lastStatus: String(host.dataset?.roomStatus || host.dataset?.status || '').toLocaleLowerCase(), viewerCount: Number(host.dataset?.viewerCount || host.dataset?.numUsers), subject: String(host.dataset?.roomSubject || subjectNode?.textContent || '').trim(), imageUrl: image?.currentSrc || image?.src || '', tags, source: `native${location.pathname.replace(/\/+$/u, '') || '/'}`, observedAt: Date.now(), }); } function injectButton(host, username) { if (observed.has(host)) return; observed.add(host); elToUsername.set(host, username); observeCard(host, username); host.classList.add('multicam-qa-host'); // 给 host 提供定位上下文 const cs = getComputedStyle(host); if (cs.position === 'static') host.style.position = 'relative'; const btn = $('button', { class: 'multicam-quick-add', dataset: { multicamUsername: username, roomgridQuickAdd: '1' }, onclick: (e) => { e.preventDefault(); e.stopPropagation(); if (Storage.has(username)) { if (Storage.remove(username)) toast(t('removedNamed', username)); } else { const r = Storage.add(username); if (r === 'added') toast(t('addedNamed', username)); } refreshInjectorState(); updateBtnState(btn, username); }, }, 'Grid +'); const favoriteBtn = $('button', { class: 'multicam-quick-mini', type: 'button', title: tx('固定预览', 'Pin preview', 'Vorschau anheften'), onclick: e => { e.preventDefault(); e.stopPropagation(); RoomGridUltimate.openPreview(username); } }, '▶'); const watchBtn = $('button', { class: 'multicam-quick-mini', type: 'button', title: tx('快速信息', 'Quick info', 'Schnellinfo'), onclick: e => { e.preventDefault(); e.stopPropagation(); RoomGridUltimate.openInspector(username); } }, 'ⓘ'); const syncMini = () => { favoriteBtn.classList.toggle('active', false); watchBtn.classList.toggle('watch', false); }; host.appendChild($('div', { class: 'multicam-quick-strip' }, [btn, favoriteBtn, watchBtn])); updateBtnState(btn, username); syncMini(); } // ---- 检测候选 host 元素 ---- function scan(maxCards = 900) { const quickActions = suiteStore.state.settings.normalQuickActions !== false; document.body.classList.toggle('rg-hide-quick-actions', !quickActions); if (document.hidden) return; // 策略 1: 元素本身有 data-username(最强信号) let checked = 0; for (const el of document.querySelectorAll('[data-testid="room-card"][data-username],li[data-username],article[data-username]')) { if (++checked > maxCards) break; const u = normalizeUsername(el.getAttribute('data-username')); if (!u || !isLikelyUsername(u)) continue; // 跳过过小的元素(如聊天里的 avatar) if (el.offsetWidth < 100 || el.offsetHeight < 80) continue; if (quickActions) injectButton(el, u); else observeCard(el, u); } // 策略 2: 房间卡片 li,包含一个指向 // 的 // 兼容 chaturbate 列表页结构 checked = 0; for (const li of document.querySelectorAll('[data-testid="room-card"],li.RoomCard,li.roomCard,li.room_list_room')) { if (++checked > maxCards) break; if (observed.has(li)) continue; if (li.offsetWidth < 100 || li.offsetHeight < 80) continue; // 找第一个匹配主播路径的 a const a = li.querySelector('a[href]'); if (!a) continue; const href = a.getAttribute('href') || ''; let path = href; if (/^https?:\/\//i.test(href)) { try { path = new URL(href).pathname; } catch (_) { continue; } } const m = path.match(ROOM_PATH); if (!m) continue; const u = normalizeUsername(m[1]); if (!isLikelyUsername(u)) continue; if (quickActions) injectButton(li, u); else observeCard(li, u); } suiteAnalytics.flush?.().catch?.(() => {}); } // 节流 scan let scanScheduled = false; let allowFullScan = false; function scheduleScan() { if (!allowFullScan || document.hidden || scanScheduled) return; scanScheduled = true; const run = () => { scanScheduled = false; scan(); }; try { if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 900 }); else setTimeout(run, 260); } catch (_) { setTimeout(run, 260); } } const onVisibility = () => { if (!document.hidden) scheduleScan(); }; document.addEventListener('visibilitychange', onVisibility); const fullScanTimer = setTimeout(() => { allowFullScan = true; scheduleScan(); }, 360); requestAnimationFrame(() => { if (!document.hidden) scan(48); }); const mo = new MutationObserver(scheduleScan); mo.observe(document.body, { childList: true, subtree: true }); // 跨标签页 storage 同步:刷新所有按钮状态 storageSubs.add(() => { document.querySelectorAll('.multicam-quick-add').forEach(btn => { const u = btn.dataset.multicamUsername || btn.dataset.username; if (u) updateBtnState(btn, u); }); }); return () => { clearTimeout(fullScanTimer); mo.disconnect(); document.removeEventListener('visibilitychange', onVisibility); style.remove(); }; } function initNormalPageLivePreviews() { const entries = new Map(); let activeLoads = 0; let stopped = false; let scanScheduled = false; let scrollingUntil = 0; let nextWaveAt = 0; const isSupportedPage = () => { const path = String(location.pathname || '/').replace(/\/+$/u, '') || '/'; return path === '/' || /^\/(?:tag|tags|female-cams|male-cams|couple-cams|trans-cams|followed-cams|new-cams|gaming-cams|north-american-cams|south-american-cams|euro-russian-cams|asian-cams|other-region-cams)(?:\/|$)/iu.test(path); }; const options = () => { const settings = suiteStore.state.settings; const fps = Number(settings.normalLivePreviewFps); const rawConcurrency = Number(settings.normalLivePreviewConcurrency); return { enabled: isSupportedPage() && settings.normalLivePreviews !== false && fps > 0, fps: PREVIEW_FPS_VALUES.includes(fps) ? fps : .5, mode: settings.normalLivePreviewMode === 'bounded' ? 'bounded' : 'simultaneous', concurrency: rawConcurrency === 0 ? 0 : clampInt(rawConcurrency, 1, 24, 4), hoverEnabled: settings.normalHoverLivePreview !== false && matchMedia('(hover:hover) and (pointer:fine)').matches, hoverDelay: clampInt(settings.normalHoverLivePreviewDelay, 0, 1000, 250), hoverScale: clampInt(settings.normalHoverLivePreviewScale, 100, 600, 112), }; }; const style = $('style', { id: 'roomgrid-normal-live-preview-style', html: trustedHtml(` .roomgrid-hover-live-active{position:relative!important;z-index:150!important;transform:scale(var(--roomgrid-hover-live-scale,1.12))!important;transform-origin:var(--roomgrid-hover-live-origin,center)!important;box-shadow:0 24px 65px rgba(2,6,23,.38)!important;transition:transform .16s ease,box-shadow .16s ease!important} `) }); document.head.appendChild(style); const usernameFor = card => { const raw = card.getAttribute('data-username') || card.querySelector('[data-room]')?.getAttribute('data-room') || card.querySelector('[data-username]')?.getAttribute('data-username') || ''; let id = normalizeUsername(raw); if (!id) { try { id = normalizeUsername(new URL(card.querySelector('a[href]')?.href || '', location.href).pathname.split('/').filter(Boolean)[0] || ''); } catch (_) {} } return isLikelyUsername(id) ? id : ''; }; const setStatus = (entry, state, text) => { if (!entry.status) return; entry.status.dataset.state = state; entry.status.textContent = text; }; const finish = (entry, ok, token) => { if (!entry || entry.token !== token || !entry.loading) return; entry.loading = false; activeLoads = Math.max(0, activeLoads - 1); clearTimeout(entry.timeout); entry.timeout = 0; if (entry.probe) { entry.probe.onload = null; entry.probe.onerror = null; entry.probe = null; } const config = options(); entry.nextAt = ok && config.fps ? (config.mode === 'simultaneous' ? nextWaveAt : Date.now() + Math.round(1000 / config.fps)) : Date.now() + 15000; }; const deactivate = (entry, hide = false) => { entry.token += 1; if (entry.loading) { entry.loading = false; activeLoads = Math.max(0, activeLoads - 1); } clearTimeout(entry.timeout); entry.timeout = 0; if (entry.probe) { entry.probe.onload = null; entry.probe.onerror = null; entry.probe = null; } if (entry.baseSrc && entry.img.src !== entry.baseSrc) entry.img.src = entry.baseSrc; }; const launch = entry => { const config = options(); if (!config.enabled || !entry.visible || entry.loading || document.hidden) return; const url = discoverSnapshotUrl(entry.id); if (!url) return; entry.loading = true; entry.token += 1; const token = entry.token; activeLoads += 1; const probe = new Image(); entry.probe = probe; probe.onload = () => { if (entry.token !== token || !entry.loading) return; entry.img.src = url; finish(entry, true, token); }; probe.onerror = () => finish(entry, false, token); entry.timeout = setTimeout(() => finish(entry, false, token), 10000); probe.src = url; }; const pump = () => { if (stopped || document.hidden || Date.now() < scrollingUntil) return; const config = options(); if (!config.enabled) return; const now = Date.now(); if (config.mode === 'simultaneous') { // Available cards start together; an individual slow request cannot // hold all other previews. Each entry guards its own in-flight load. if (now < nextWaveAt) return; const due = [...entries.values()].filter(entry => entry.visible && !entry.hovering && !entry.loading && entry.nextAt <= now); if (!due.length) return; nextWaveAt = now + Math.round(1000 / config.fps); due.forEach(launch); return; } const due = [...entries.values()].filter(entry => entry.visible && !entry.hovering && !entry.loading && entry.nextAt <= now).sort((a, b) => a.nextAt - b.nextAt); const concurrency = config.concurrency === 0 ? Math.max(1, due.length + activeLoads) : config.concurrency; while (due.length && activeLoads < concurrency) launch(due.shift()); }; const observer = 'IntersectionObserver' in window ? new IntersectionObserver(items => { items.forEach(item => { const entry = entries.get(item.target); if (!entry) return; entry.visible = !!item.isIntersecting && item.intersectionRatio > 0; if (entry.visible) { entry.nextAt = 0; nextWaveAt = 0; } else { stopHover(entry); deactivate(entry, true); } }); pump(); scheduleTick(); }, { root: null, rootMargin: '0px', threshold: .01 }) : null; const stopHover = entry => { clearTimeout(entry.hoverTimer); entry.hoverTimer = 0; entry.hovering = false; RoomGridUltimate.stopInlineHoverPreview(entry.host); entry.card.classList.remove('roomgrid-hover-live-active'); entry.card.style.removeProperty('--roomgrid-hover-live-scale'); entry.card.style.removeProperty('--roomgrid-hover-live-origin'); }; const startHover = entry => { const config = options(); if (!config.hoverEnabled || !entry.visible || document.hidden) return; deactivate(entry, false); entry.hovering = true; const scale = discoverHoverScale(entry.card, config.hoverScale / 100); entry.card.style.setProperty('--roomgrid-hover-live-origin', discoverHoverTransformOrigin(entry.card, scale)); entry.card.style.setProperty('--roomgrid-hover-live-scale', String(scale)); entry.card.classList.add('roomgrid-hover-live-active'); entry.hoverTimer = setTimeout(() => { entry.hoverTimer = 0; if (entry.hovering && entry.visible && !document.hidden) RoomGridUltimate.startInlineHoverPreview(entry.id, entry.host); }, config.hoverDelay); }; const register = card => { if (entries.has(card)) return; const id = usernameFor(card); const img = card.querySelector('img[data-testid="room-card-image"],a.RoomCardThumbnail img,.RoomCardThumbnail img'); const host = img?.closest?.('a,[data-testid="room-card-image-anchor"],.RoomCardThumbnail') || null; if (!id || !img || !host) return; if (getComputedStyle(host).position === 'static') host.style.position = 'relative'; const rect = card.getBoundingClientRect(); const initiallyVisible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < innerHeight && rect.right > 0 && rect.left < innerWidth; const entry = { card, id, img, host, status: null, baseSrc: img.src, visible: initiallyVisible, loading: false, nextAt: 0, token: 0, timeout: 0, probe: null, hovering: false, hoverTimer: 0 }; entry.onPointerEnter = () => startHover(entry); entry.onPointerLeave = () => { stopHover(entry); entry.nextAt = 0; nextWaveAt = 0; pump(); scheduleTick(); }; card.addEventListener('pointerenter', entry.onPointerEnter); card.addEventListener('pointerleave', entry.onPointerLeave); card.dataset.roomgridLivePreviewRegistered = '1'; entries.set(card, entry); observer?.observe(card); }; const scan = (maxCards = 300) => { scanScheduled = false; if (stopped) return; const removeEntry = (card, entry) => { observer?.unobserve(card); stopHover(entry); deactivate(entry, true); card.removeEventListener('pointerenter', entry.onPointerEnter); card.removeEventListener('pointerleave', entry.onPointerLeave); delete card.dataset.roomgridLivePreviewRegistered; entries.delete(card); }; if (!options().enabled) { entries.forEach(entry => deactivate(entry, true)); } const candidates = [...document.querySelectorAll('[data-testid="room-card"],li.RoomCard,li.roomCard,li.room_list_room')].map((card, index) => { const rect = card.getBoundingClientRect(); const hiddenByFilter = card.classList.contains('rg-home-filter-hidden') || card.classList.contains('rgh-home-group-hidden'); const inViewport = !hiddenByFilter && rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < innerHeight && rect.right > 0 && rect.left < innerWidth; const priority = inViewport ? 0 : (!hiddenByFilter && card.dataset.roomgridCatalogCard === '1' ? 1 : !hiddenByFilter ? 2 : 3); return { card, priority, distance: inViewport ? 0 : Math.abs(rect.top - innerHeight / 2), index }; }).sort((left, right) => left.priority - right.priority || left.distance - right.distance || left.index - right.index).slice(0, Math.max(12, Math.min(300, maxCards))); const wantedCards = new Set(candidates.map(item => item.card)); for (const [card, entry] of entries) if (!card.isConnected || !wantedCards.has(card)) removeEntry(card, entry); candidates.forEach(item => register(item.card)); pump(); }; let allowFullScan = false; const scheduleScan = () => { if (!allowFullScan || scanScheduled || stopped) return; scanScheduled = true; const run = () => scan(); try { if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 900 }); else setTimeout(run, 250); } catch (_) { setTimeout(run, 250); } }; const mutationObserver = new MutationObserver(scheduleScan); mutationObserver.observe(document.body, { childList: true, subtree: true }); const onCatalogCardsChanged = () => scheduleScan(); const onVisibility = () => { if (document.hidden) entries.forEach(entry => { stopHover(entry); deactivate(entry, false); }); else { nextWaveAt = 0; entries.forEach(entry => { if (entry.visible) entry.nextAt = 0; }); pump(); } scheduleTick(); }; const onScroll = () => { scrollingUntil = Date.now() + 250; }; document.addEventListener('visibilitychange', onVisibility); window.addEventListener('scroll', onScroll, true); window.addEventListener('roomgrid_catalog_cards_changed', onCatalogCardsChanged); let tick = 0; const scheduleTick = () => { clearTimeout(tick); if (stopped) return; const config = options(); const visible = !document.hidden && [...entries.values()].some(entry => entry.visible && !entry.hovering); const delay = config.enabled && config.fps && visible ? Math.max(4, Math.min(1000, Math.round(1000 / config.fps))) : 1000; tick = setTimeout(() => { tick = 0; pump(); scheduleTick(); }, delay); }; const unsubscribe = suiteStore.subscribe(() => { scheduleScan(); const config = options(); if (!config.hoverEnabled) entries.forEach(stopHover); if (!config.enabled) entries.forEach(entry => deactivate(entry, true)); else { nextWaveAt = 0; entries.forEach(entry => { if (entry.visible) entry.nextAt = 0; }); pump(); } scheduleTick(); }); const fullScanTimer = setTimeout(() => { allowFullScan = true; scheduleScan(); }, 420); requestAnimationFrame(() => { scan(48); }); scheduleTick(); return () => { stopped = true; clearTimeout(fullScanTimer); clearTimeout(tick); mutationObserver.disconnect(); observer?.disconnect(); document.removeEventListener('visibilitychange', onVisibility); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('roomgrid_catalog_cards_changed', onCatalogCardsChanged); unsubscribe(); entries.forEach(entry => { stopHover(entry); deactivate(entry, true); entry.card.removeEventListener('pointerenter', entry.onPointerEnter); entry.card.removeEventListener('pointerleave', entry.onPointerLeave); delete entry.card.dataset.roomgridLivePreviewRegistered; entry.status?.remove(); }); entries.clear(); style.remove(); }; } // =========================================================== // RoomTags —— normale Start-/Tagseiten: öffentliche Kartentags ergänzen // =========================================================== function initRoomCardTags() { const style = $('style', { html: trustedHtml(` .multicam-tags-host { position:relative !important; } .multicam-room-tags { position:absolute; left:7px; right:7px; bottom:7px; z-index:96; display:flex; flex-wrap:wrap; align-items:center; gap:4px; pointer-events:none; font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; } .multicam-room-tag { display:inline-flex; align-items:center; max-width:150px; padding:3px 6px; border:1px solid rgba(255,255,255,.26); border-radius:999px; background:rgba(15,23,42,.70); color:#f8fafc; font-size:10px; font-weight:700; line-height:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; box-shadow:0 4px 12px rgba(15,23,42,.16); backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); } .multicam-room-tags .multicam-room-tag.is-extra { display:none; } .multicam-room-tags.is-expanded .multicam-room-tag.is-extra { display:inline-flex; } .multicam-room-tags-more { pointer-events:auto; border:1px solid rgba(147,197,253,.55); border-radius:999px; padding:3px 6px; background:rgba(37,99,235,.76); color:#fff; cursor:pointer; font-size:10px; font-weight:800; line-height:1; box-shadow:0 4px 12px rgba(15,23,42,.16); } .multicam-room-tags-more:hover { background:rgba(29,78,216,.92); } `)}); document.head.appendChild(style); const isTargetPage = () => { const path = String(location.pathname || '/').replace(/\/+$/, '') || '/'; return path === '/' || /^\/tags?(?:\/|$)/i.test(path) || /^\/(?:female-cams|male-cams|couple-cams|trans-cams)(?:\/|$)/i.test(path); }; const cleanTag = (value) => { const text = String(value ?? '').trim().replace(/^#+/, '').replace(/^[/\\]+|[/\\]+$/g, ''); if (!text || text.length > 80 || /^(?:tags?|rooms?|live|online|show|more)$/i.test(text)) return ''; return text.replace(/\s+/g, ' '); }; const collectValue = (value, output) => { if (value == null) return; if (Array.isArray(value)) { value.forEach(item => collectValue(item, output)); return; } if (typeof value === 'object') { Object.entries(value).forEach(([key, item]) => { if (/tag/i.test(key)) collectValue(item, output); }); return; } const raw = String(value).trim(); if (!raw) return; if ((raw.startsWith('[') && raw.endsWith(']')) || (raw.startsWith('{') && raw.endsWith('}'))) { try { collectValue(JSON.parse(raw), output); return; } catch (_) {} } raw.split(/[,;|\n]+/).map(cleanTag).filter(Boolean).forEach(tag => output.push(tag)); }; const roomFromCard = (card) => { const direct = normalizeUsername(card?.getAttribute?.('data-username') || card?.dataset?.username || ''); if (isLikelyUsername(direct)) return direct; const named = card?.querySelector?.('[data-username], [data-testid="room-card-username"]'); const candidate = normalizeUsername(named?.getAttribute?.('data-username') || named?.textContent || ''); if (isLikelyUsername(candidate)) return candidate; for (const a of card?.querySelectorAll?.('a[href]') || []) { try { const path = new URL(a.href, location.href).pathname; const match = path.match(ROOM_PATH); const id = normalizeUsername(match?.[1] || ''); if (isLikelyUsername(id)) return id; } catch (_) {} } return ''; }; const cardFor = (el) => el?.closest?.('[data-testid="room-card"],li.RoomCard,li.roomCard,li.room_list_room,article[data-username],li[data-username]') || null; const tagsFromCard = (card) => { const values = []; const nodes = [card, ...card.querySelectorAll('[data-tags], [data-room-tags], [data-broadcast-tags], [data-tag-list], [data-tag]')]; nodes.forEach(node => { ['data-tags', 'data-room-tags', 'data-broadcast-tags', 'data-tag-list', 'data-tag'].forEach(name => { if (node.hasAttribute?.(name)) collectValue(node.getAttribute(name), values); }); }); card.querySelectorAll('a[href]').forEach(a => { try { const path = new URL(a.href, location.href).pathname; const match = path.match(/^\/tags?\/([^/?#]+)/i); if (match) collectValue(decodeURIComponent(match[1]).replace(/[-_]+/g, ' '), values); } catch (_) {} }); card.querySelectorAll('.tag, [class~="tag"], [class*="-tag"]').forEach(el => { if (el.closest('.multicam-room-tags')) return; if (el.children.length === 0) collectValue(el.textContent, values); }); return [...new Set(values.map(cleanTag).filter(Boolean))].slice(0, 30); }; const render = (card, tags) => { if (!card || !card.querySelector) return; let block = card.querySelector('.multicam-room-tags[data-roomgrid-tags="1"]'); if (!tags.length) { block?.remove(); return; } const signature = tags.join('\u001f'); if (block?.dataset.roomgridTagsSignature === signature) return; if (!block) { if (getComputedStyle(card).position === 'static') card.classList.add('multicam-tags-host'); block = $('div', { class: 'multicam-room-tags', dataset: { roomgridTags: '1' }, 'aria-label': t('roomTagsLabel') }); card.appendChild(block); } block.dataset.roomgridTagsSignature = signature; const visible = tags.slice(0, 5); const extra = Math.max(0, tags.length - visible.length); block.replaceChildren(...tags.map((tag, index) => $('span', { class: 'multicam-room-tag' + (index >= visible.length ? ' is-extra' : ''), title: tag, }, '#' + tag))); if (extra) { const more = $('button', { class: 'multicam-room-tags-more', type: 'button', title: t('roomTagsMore', extra), onclick: (e) => { e.preventDefault(); e.stopPropagation(); const expanded = block.classList.toggle('is-expanded'); more.textContent = expanded ? t('roomTagsLess') : t('roomTagsMore', extra); more.title = expanded ? t('roomTagsLess') : t('roomTagsMore', extra); }, }, t('roomTagsMore', extra)); block.appendChild(more); } }; let scheduled = false; const scan = () => { scheduled = false; if (document.hidden || !isTargetPage()) return; const seeds = document.querySelectorAll('[data-testid="room-card"],[data-testid="room-card-username"],li.RoomCard,li.roomCard,li.room_list_room,article[data-username],li[data-username]'); const cards = [...new Set([...seeds].map(cardFor).filter(Boolean))].slice(0, 1200); cards.forEach(card => { const id = roomFromCard(card); if (!id) return; render(card, tagsFromCard(card)); }); }; const schedule = () => { if (scheduled || document.hidden) return; scheduled = true; const run = () => scan(); try { if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 1000 }); else setTimeout(run, 320); } catch (_) { setTimeout(run, 320); } }; scan(); document.addEventListener('visibilitychange', schedule); window.addEventListener('pageshow', schedule); window.addEventListener('popstate', schedule); window.addEventListener('hashchange', schedule); try { const mo = new MutationObserver(schedule); mo.observe(document.body, { childList: true, subtree: true }); } catch (_) {} } return () => { injectorDisposed = true; idleJobs.splice(0).forEach(cancel => { try { cancel(); } catch (_) {} }); clearTimeout(routePollTimer); try { suiteNotifications.destroy(); } catch (_) {} try { suiteWindows.destroy(); } catch (_) {} try { suiteAnalytics.stop(); suiteAnalytics.flush(); } catch (_) {} try { pageCommandPalette.destroy(); normalPageCleanup(); } catch (_) {} try { ultimateCleanup?.(); } catch (_) {} try { nativeHubCleanup?.(); } catch (_) {} try { audioSilencerCleanup?.(); } catch (_) {} try { normalPreviewCleanup?.(); quickAddCleanup?.(); } catch (_) {} try { document.body.classList.remove('rg-hide-quick-actions'); } catch (_) {} try { root.remove(); dockStyle.remove(); } catch (_) {} try { document.querySelector('[data-roomgrid-control-center]')?.remove(); } catch (_) {} }; } /* ============================================================= * 7.5. Entdecken + persönliche Startseite * Öffentlicher Same-Origin-Katalog, lokale Filter/Feeds und ein * zentral begrenzter JPEG-Vorschauplaner ohne Video-/Audiodecoder. * ============================================================= */ const DISCOVER_STORE_KEY = 'ryujo_multicam_discover_v1'; const DISCOVER_BATCH_SIZE = 100; const DISCOVER_MAX_RESULTS = ROOMGRID_DATA_LIMITS.publicCatalogResults; const DISCOVER_PAGE_SIZE = 24; const DISCOVER_CATALOG_CACHE_KEY = 'camclient_live_catalog_cache_v2'; const DISCOVER_CATALOG_CACHE_DB = 'camclient_live_catalog_cache_v2'; const DISCOVER_CATALOG_CACHE_FRESH_MS = 2 * 60 * 1000; let discoverCurrentCatalog = null; // Keep the last complete catalog across updates/outages. Freshness controls // background refresh, not retention; old rows are no proof of live status. const DISCOVER_SNAPSHOT_ORIGIN = 'https://jpeg.live.mmcdn.com'; const DISCOVER_THUMB_ORIGIN = 'https://thumb.live.mmcdn.com'; const CAMCLIENT_TAG_ART_PRESETS = Object.freeze([ { id: 'music', label: 'Music', x: '0%', y: '0%' }, { id: 'gaming', label: 'Gaming', x: '50%', y: '0%' }, { id: 'new', label: 'New', x: '100%', y: '0%' }, { id: 'world', label: 'World', x: '0%', y: '100%' }, { id: 'dance', label: 'Dance', x: '50%', y: '100%' }, { id: 'favorite', label: 'Favorite', x: '100%', y: '100%' }, ]); const CAMCLIENT_TAG_ART_PRESET_IDS = new Set(CAMCLIENT_TAG_ART_PRESETS.map(item => item.id)); function singleDefaultPreferences() { return { layout: 'wide', sidebar: 'right', aspect: '16/9', background: 'black', fit: 'contain', activeTab: 'overview', stageSize: 'large', streamWidth: 100, streamHeight: 60, cockpitSize: 'normal', controlSize: 'normal', playerCorners: 'soft', sidebarCollapsed: false, toolsCollapsed: false, autoplay: true, startMuted: true, rememberVolume: true, maxVolume: 1, pauseHidden: true, lowLatency: true, bufferSeconds: 10, autoReconnect: true, pollSeconds: 45, showLocal: true, showProfile: true, showAlternatives: true, showChat: true, pauseHiddenChat: false, showPlayerStatus: true, compactControls: false, mediaImportQuality: 'high', screenshotFormat: 'png', screenshotHeight: 0, screenshotSaveGallery: true, screenshotMakeCover: false, }; } function singleCleanPreferences(input) { const base = singleDefaultPreferences(); const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; return { layout: ['wide', 'focus', 'compact'].includes(source.layout) ? source.layout : base.layout, sidebar: ['right', 'left', 'below'].includes(source.sidebar) ? source.sidebar : base.sidebar, aspect: ['16/9', '4/3', '21/9'].includes(source.aspect) ? source.aspect : base.aspect, background: ['black', 'soft', 'ambient'].includes(source.background) ? source.background : base.background, fit: ['contain', 'cover'].includes(source.fit) ? source.fit : base.fit, activeTab: ['overview', 'chat', 'notes', 'statistics', 'media', 'profile', 'settings'].includes(source.activeTab) ? source.activeTab : base.activeTab, stageSize: ['standard', 'large', 'maximum', 'custom'].includes(source.stageSize) ? source.stageSize : base.stageSize, streamWidth: clampInt(source.streamWidth, 30, 100, base.streamWidth), streamHeight: clampInt(source.streamHeight, 20, 100, base.streamHeight), sidebarCollapsed: !!source.sidebarCollapsed, toolsCollapsed: !!source.toolsCollapsed, cockpitSize: ['compact', 'normal', 'wide'].includes(source.cockpitSize) ? source.cockpitSize : base.cockpitSize, controlSize: ['compact', 'normal', 'large'].includes(source.controlSize) ? source.controlSize : base.controlSize, playerCorners: ['square', 'soft', 'round'].includes(source.playerCorners) ? source.playerCorners : base.playerCorners, autoplay: source.autoplay === undefined ? base.autoplay : !!source.autoplay, startMuted: source.startMuted === undefined ? base.startMuted : !!source.startMuted, rememberVolume: source.rememberVolume === undefined ? base.rememberVolume : !!source.rememberVolume, maxVolume: [.25, .5, .75, 1].includes(Number(source.maxVolume)) ? Number(source.maxVolume) : base.maxVolume, pauseHidden: source.pauseHidden === undefined ? base.pauseHidden : !!source.pauseHidden, lowLatency: source.lowLatency === undefined ? base.lowLatency : !!source.lowLatency, bufferSeconds: [5, 10, 20, 30, 45].includes(Number(source.bufferSeconds)) ? Number(source.bufferSeconds) : base.bufferSeconds, autoReconnect: source.autoReconnect === undefined ? base.autoReconnect : !!source.autoReconnect, pollSeconds: [15, 30, 45, 60, 90].includes(Number(source.pollSeconds)) ? Number(source.pollSeconds) : base.pollSeconds, showLocal: source.showLocal === undefined ? base.showLocal : !!source.showLocal, showProfile: source.showProfile === undefined ? base.showProfile : !!source.showProfile, showAlternatives: source.showAlternatives === undefined ? base.showAlternatives : !!source.showAlternatives, showChat: source.showChat === undefined ? base.showChat : !!source.showChat, pauseHiddenChat: !!source.pauseHiddenChat, showPlayerStatus: source.showPlayerStatus === undefined ? base.showPlayerStatus : !!source.showPlayerStatus, compactControls: !!source.compactControls, mediaImportQuality: ['original', 'high', 'balanced', 'compact'].includes(source.mediaImportQuality) ? source.mediaImportQuality : base.mediaImportQuality, screenshotFormat: ['png', 'jpeg', 'webp'].includes(source.screenshotFormat) ? source.screenshotFormat : base.screenshotFormat, screenshotHeight: [0, 720, 1080, 1440, 2160].includes(Number(source.screenshotHeight)) ? Number(source.screenshotHeight) : base.screenshotHeight, screenshotSaveGallery: true, screenshotMakeCover: !!source.screenshotMakeCover && (source.screenshotSaveGallery === undefined ? base.screenshotSaveGallery : !!source.screenshotSaveGallery), }; } function discoverDefaultState() { let reduceMotion = false; try { reduceMotion = !!window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches; } catch (_) {} return { schema: 1, view: 'discover', query: '', quick: 'all', genders: [], tagsAll: '', tagsAny: '', tagsNot: '', place: '', advancedFiltersOpen: false, minAge: 0, maxAge: 0, minViewers: 0, maxViewers: 0, minOnlineMinutes: 0, maxOnlineMinutes: 0, localMode: 'all', groupId: '', hideVisitedToday: false, minWatchMinutes: 0, minSessions: 0, sortPrimary: 'viewers_desc', sortSecondary: 'name_asc', randomSeed: 1, page: 1, homePage: 1, homePageSize: 24, homeColumns: 0, homeCardScale: 100, homeImageRatio: '4/3', homeDensity: 'comfortable', homeShowStats: true, homeShowWidgets: true, homeToolsPanel: 'none', homeWidgetDefaultsVersion: 1, previewSettingsSynced: false, previewEnabled: !reduceMotion, previewFps: 1, previewMode: 'simultaneous', previewConcurrency: 0, hoverLivePreview: true, hoverPreviewDelay: 250, hoverPreviewScale: 112, profiles: [], feeds: [], activeProfileId: '', activeFeedId: '', singleId: '', singleContext: [], singleReturnView: 'discover', singlePreferences: singleDefaultPreferences(), tagSearch: '', tagBrowseScope: 'all', tagBrowseSort: 'count', tagBrowsePage: 1, tagResultPage: 1, tagExpanded: [], tagFavorites: [], tagBlocked: [], tagSelection: [], tagSelectionMode: 'and', tagGroups: [], tagHistory: [], tagObservations: {}, tagArtwork: {}, seenModels: {}, temporaryWatchlist: [], themeFavorites: [], discoveryHiddenModels: {}, discoveryLessTags: [], lastCatalogVisitAt: 0, selectedIds: [], searchHistory: [], scrollPositions: {}, dashboardOrder: ['favorites', 'newOnline', 'feed', 'suggestions', 'recent', 'frequent', 'never'], dashboardHidden: ['newOnline', 'recent', 'frequent', 'never'], dashboardLimits: {}, dashboardSizes: {}, }; } function discoverCleanText(value, max = 160) { return String(value ?? '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, max); } function discoverCleanDefinition(input, includeQuery = true) { const base = discoverDefaultState(); const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const genders = [...new Set((Array.isArray(source.genders) ? source.genders : []).filter(value => ['f', 'm', 'c', 't'].includes(value)))]; const allowedQuick = ['all', 'hd', 'new', 'new_since', 'new_to_me', 'returning', 'stable', 'gaming', 'watchlist', 'following', 'saved']; const allowedLocal = ['all', 'saved', 'favorites', 'following', 'known', 'never']; const sortValues = new Set([ 'viewers_desc', 'viewers_asc', 'online_desc', 'online_asc', 'followers_desc', 'followers_asc', 'name_asc', 'name_desc', 'new_desc', 'gaming_desc', 'watch_desc', 'sessions_desc', 'last_desc', 'random', ]); return { query: includeQuery ? discoverCleanText(source.query, 120) : '', quick: allowedQuick.includes(source.quick) ? source.quick : base.quick, genders, tagsAll: discoverCleanText(source.tagsAll), tagsAny: discoverCleanText(source.tagsAny), tagsNot: discoverCleanText(source.tagsNot), place: discoverCleanText(source.place, 100), minAge: clampInt(source.minAge, 0, 99, 0), maxAge: clampInt(source.maxAge, 0, 99, 0), minViewers: Math.max(0, Math.round(Number(source.minViewers) || 0)), maxViewers: Math.max(0, Math.round(Number(source.maxViewers) || 0)), minOnlineMinutes: Math.max(0, Math.round(Number(source.minOnlineMinutes) || 0)), maxOnlineMinutes: Math.max(0, Math.round(Number(source.maxOnlineMinutes) || 0)), localMode: allowedLocal.includes(source.localMode) ? source.localMode : base.localMode, groupId: discoverCleanText(source.groupId, 80), hideVisitedToday: !!source.hideVisitedToday, minWatchMinutes: Math.max(0, Math.round(Number(source.minWatchMinutes) || 0)), minSessions: Math.max(0, Math.round(Number(source.minSessions) || 0)), sortPrimary: sortValues.has(source.sortPrimary) ? source.sortPrimary : base.sortPrimary, sortSecondary: sortValues.has(source.sortSecondary) ? source.sortSecondary : base.sortSecondary, }; } function discoverCleanSavedList(input, max, includeQuery) { if (!Array.isArray(input)) return []; const names = new Set(); const output = []; for (const item of input.slice(0, max)) { if (!item || typeof item !== 'object') continue; const name = discoverCleanText(item.name, 60); const key = name.toLocaleLowerCase(); if (!name || names.has(key)) continue; names.add(key); output.push({ id: discoverCleanText(item.id, 80) || `d_${Date.now()}_${output.length}`, name, createdAt: Math.max(0, Number(item.createdAt) || Date.now()), definition: discoverCleanDefinition(item.definition, includeQuery), }); } return output; } function discoverCleanTagList(input, max = 100) { return [...new Set((Array.isArray(input) ? input : []) .map(value => discoverCleanText(value, 50).toLocaleLowerCase()).filter(Boolean))].slice(0, max); } function discoverCleanTagGroups(input) { if (!Array.isArray(input)) return []; return input.map((item, index) => { if (!item || typeof item !== 'object') return null; const tags = discoverCleanTagList(item.tags, 20); const name = discoverCleanText(item.name, 60); if (!name || !tags.length) return null; return { id: discoverCleanText(item.id, 80) || `tags_${Date.now()}_${index}`, name, mode: ['and', 'or', 'not'].includes(item.mode) ? item.mode : 'and', tags, createdAt: Math.max(0, Number(item.createdAt) || Date.now()), }; }).filter(Boolean); } function discoverCleanTagHistory(input) { if (!Array.isArray(input)) return []; return input.slice(-120).map(item => { if (!item || typeof item !== 'object') return null; const tag = discoverCleanText(item.tag, 50).toLocaleLowerCase(); return tag ? { tag, at: Math.max(0, Number(item.at) || Date.now()) } : null; }).filter(Boolean); } function discoverCleanTagArtwork(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) return {}; const output = {}; for (const [rawTag, rawArtwork] of Object.entries(input).slice(-120)) { const tag = discoverCleanText(rawTag, 50).toLocaleLowerCase(); if (!tag || !rawArtwork || typeof rawArtwork !== 'object' || Array.isArray(rawArtwork)) continue; const preset = String(rawArtwork.preset || ''); if (preset === 'none') { output[tag] = { preset: 'none' }; continue; } if (CAMCLIENT_TAG_ART_PRESET_IDS.has(preset)) { output[tag] = { preset }; continue; } const rawDataUrl = String(rawArtwork.dataUrl || ''); if (!rawDataUrl || rawDataUrl.length > 180000) continue; const dataUrl = sanitizeBrandLogoDataUrl(rawDataUrl); if (dataUrl) output[tag] = { dataUrl }; } return output; } function discoverCleanObservationMap(input, maximum, kind) { if (!input || typeof input !== 'object' || Array.isArray(input)) return {}; const output = {}; for (const [rawKey, rawValue] of Object.entries(input).slice(-maximum)) { const key = kind === 'model' ? normalizeUsername(rawKey) : discoverCleanText(rawKey, 50).toLocaleLowerCase(); if (!(kind === 'model' ? isLikelyUsername(key) : !!key) || !rawValue || typeof rawValue !== 'object') continue; if (kind === 'model') { output[key] = { firstSeenAt: Math.max(0, Number(rawValue.firstSeenAt) || 0), lastSeenAt: Math.max(0, Number(rawValue.lastSeenAt) || 0), }; } else { output[key] = { count: Math.max(0, Math.round(Number(rawValue.count) || 0)), previousCount: Math.max(0, Math.round(Number(rawValue.previousCount) || 0)), observedAt: Math.max(0, Number(rawValue.observedAt) || 0), }; } } return output; } function discoverSanitizeState(input) { const base = discoverDefaultState(); const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const definition = discoverCleanDefinition(source, true); return { ...base, ...definition, schema: 1, advancedFiltersOpen: !!source.advancedFiltersOpen, view: ['dashboard', 'discover', 'tags', 'stats', 'library', 'user', 'single'].includes(source.view) ? source.view : 'discover', page: Math.max(1, Math.round(Number(source.page) || 1)), homePage: Math.max(1, Math.round(Number(source.homePage) || 1)), homePageSize: [12, 24, 36, 48, 60, 90, 120, 180].includes(Number(source.homePageSize)) ? Number(source.homePageSize) : base.homePageSize, homeColumns: clampInt(source.homeColumns, 0, 8, base.homeColumns), homeCardScale: clampInt(source.homeCardScale, 70, 150, base.homeCardScale), homeImageRatio: ['16/9', '4/3', '1/1', '3/4'].includes(source.homeImageRatio) ? source.homeImageRatio : base.homeImageRatio, homeDensity: ['compact', 'comfortable', 'spacious'].includes(source.homeDensity) ? source.homeDensity : base.homeDensity, homeShowStats: source.homeShowStats === undefined ? base.homeShowStats : !!source.homeShowStats, homeShowWidgets: source.homeShowWidgets === undefined ? base.homeShowWidgets : !!source.homeShowWidgets, homeToolsPanel: ['filters', 'display', 'overview'].includes(source.homeToolsPanel) ? source.homeToolsPanel : 'none', homeWidgetDefaultsVersion: 1, previewSettingsSynced: source.previewSettingsSynced === true, previewEnabled: source.previewEnabled === undefined ? base.previewEnabled : !!source.previewEnabled, previewFps: PREVIEW_FPS_VALUES.includes(Number(source.previewFps)) ? Number(source.previewFps) : base.previewFps, previewMode: ['simultaneous', 'bounded'].includes(source.previewMode) ? source.previewMode : base.previewMode, previewConcurrency: Number(source.previewConcurrency) === 0 ? 0 : clampInt(source.previewConcurrency, 1, 24, 4), hoverLivePreview: source.hoverLivePreview === undefined ? base.hoverLivePreview : !!source.hoverLivePreview, hoverPreviewDelay: [0, 150, 250, 400, 600, 1000].includes(Number(source.hoverPreviewDelay)) ? Number(source.hoverPreviewDelay) : base.hoverPreviewDelay, hoverPreviewScale: clampInt(source.hoverPreviewScale, 100, 600, base.hoverPreviewScale), profiles: discoverCleanSavedList(source.profiles, 10, false), feeds: discoverCleanSavedList(source.feeds, 50, true), activeProfileId: discoverCleanText(source.activeProfileId, 80), activeFeedId: discoverCleanText(source.activeFeedId, 80), singleId: isLikelyUsername(normalizeUsername(source.singleId)) ? normalizeUsername(source.singleId) : '', singleContext: [...new Set((Array.isArray(source.singleContext) ? source.singleContext : []) .map(normalizeUsername).filter(isLikelyUsername))].slice(0, DISCOVER_MAX_RESULTS), singleReturnView: ['dashboard', 'discover', 'tags', 'stats', 'library', 'user'].includes(source.singleReturnView) ? source.singleReturnView : 'discover', singlePreferences: singleCleanPreferences(source.singlePreferences), tagSearch: discoverCleanText(source.tagSearch, 80).toLocaleLowerCase(), tagBrowseScope: ['all', 'favorites', 'recent', 'blocked'].includes(source.tagBrowseScope) ? source.tagBrowseScope : 'all', tagBrowseSort: ['count', 'name', 'name_desc', 'trend'].includes(source.tagBrowseSort) ? source.tagBrowseSort : 'count', tagBrowsePage: clampInt(source.tagBrowsePage, 1, 100000, 1), tagResultPage: clampInt(source.tagResultPage, 1, 100000, 1), tagExpanded: [...new Set((Array.isArray(source.tagExpanded) ? source.tagExpanded : []).filter(key => ['groups', 'artwork', 'history', 'categories', 'gaming', 'help', 'browse-options', 'combination', 'selection-tools'].includes(key)))], tagFavorites: discoverCleanTagList(source.tagFavorites, 100), tagBlocked: discoverCleanTagList(source.tagBlocked, 100), tagSelection: discoverCleanTagList(source.tagSelection, 20), tagSelectionMode: ['and', 'or', 'not'].includes(source.tagSelectionMode) ? source.tagSelectionMode : 'and', tagGroups: discoverCleanTagGroups(source.tagGroups), tagHistory: discoverCleanTagHistory(source.tagHistory), tagObservations: discoverCleanObservationMap(source.tagObservations, 400, 'tag'), tagArtwork: discoverCleanTagArtwork(source.tagArtwork), seenModels: discoverCleanObservationMap(source.seenModels, 1200, 'model'), temporaryWatchlist: [...new Set((Array.isArray(source.temporaryWatchlist) ? source.temporaryWatchlist : []) .map(normalizeUsername).filter(isLikelyUsername))].slice(0, 100), themeFavorites: discoverCleanTagList(source.themeFavorites, 60), discoveryHiddenModels: Object.fromEntries(Object.entries(source.discoveryHiddenModels && typeof source.discoveryHiddenModels === 'object' ? source.discoveryHiddenModels : {}).filter(([id, until]) => isLikelyUsername(normalizeUsername(id)) && Number.isFinite(Number(until)) && (Number(until) === 0 || Number(until) > Date.now())).map(([id, until]) => [normalizeUsername(id), Number(until)])), discoveryLessTags: discoverCleanTagList(source.discoveryLessTags, 100), lastCatalogVisitAt: Math.max(0, Number(source.lastCatalogVisitAt) || 0), randomSeed: Math.max(1, Math.round(Number(source.randomSeed) || 1)), selectedIds: [...new Set((Array.isArray(source.selectedIds) ? source.selectedIds : []).map(normalizeUsername).filter(isLikelyUsername))].slice(0, 250), searchHistory: [...new Set((Array.isArray(source.searchHistory) ? source.searchHistory : []).map(value => discoverCleanText(value, 120)).filter(Boolean))].slice(-30), scrollPositions: Object.fromEntries(Object.entries(source.scrollPositions && typeof source.scrollPositions === 'object' ? source.scrollPositions : {}).filter(([key]) => ['dashboard','discover','tags','stats','library','user'].includes(key)).map(([key, value]) => [key, Math.max(0, Math.round(Number(value) || 0))])), dashboardOrder: (() => { const allowed = ['favorites','newOnline','feed','suggestions','recent','frequent','never']; const order = [...new Set((Array.isArray(source.dashboardOrder) ? source.dashboardOrder : []).filter(id => allowed.includes(id)))]; allowed.forEach(id => { if (!order.includes(id)) order.push(id); }); return order; })(), dashboardHidden: [...new Set([ ...(Array.isArray(source.dashboardHidden) ? source.dashboardHidden : []).filter(id => ['favorites','newOnline','feed','suggestions','recent','frequent','never'].includes(id)), ...(Number(source.homeWidgetDefaultsVersion) >= 1 ? [] : ['newOnline','recent','frequent','never']), ])], dashboardLimits: Object.fromEntries(Object.entries(source.dashboardLimits && typeof source.dashboardLimits === 'object' ? source.dashboardLimits : {}).filter(([key]) => ['favorites','newOnline','feed','suggestions','recent','frequent','never'].includes(key)).map(([key, value]) => [key, clampInt(value, 4, 40, 8)])), dashboardSizes: Object.fromEntries(Object.entries(source.dashboardSizes && typeof source.dashboardSizes === 'object' ? source.dashboardSizes : {}).filter(([key, value]) => ['favorites','newOnline','feed','suggestions','recent','frequent','never'].includes(key) && ['small','wide','full'].includes(value))), }; } function discoverLoadState() { try { return discoverSanitizeState(JSON.parse(localStorage.getItem(DISCOVER_STORE_KEY) || '{}')); } catch (error) { StorageHealth.report('discover-load', error); return discoverDefaultState(); } } function discoverSaveState(state) { try { localStorage.setItem(DISCOVER_STORE_KEY, JSON.stringify(discoverSanitizeState(state))); return true; } catch (error) { StorageHealth.report('discover-save', error); return false; } } function discoverSnapshotUrl(username) { const id = normalizeUsername(username); if (!isLikelyUsername(id)) return ''; const url = new URL('/stream', DISCOVER_SNAPSHOT_ORIGIN); url.searchParams.set('room', id); url.searchParams.set('f', `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`); return url.toString(); } function discoverFallbackImage(username) { const id = normalizeUsername(username); if (!isLikelyUsername(id)) return ''; return new URL(`/riw/${encodeURIComponent(id)}.jpg`, DISCOVER_THUMB_ORIGIN).toString(); } function discoverSafeImageUrl(value, username) { try { const url = new URL(String(value || '')); if (url.origin === DISCOVER_THUMB_ORIGIN && (/^\/riw?\//u.test(url.pathname))) return url.toString(); } catch (_) {} return discoverFallbackImage(username); } function discoverNormalizeRoom(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) return null; const username = normalizeUsername(input.username); if (!isLikelyUsername(username)) return null; const currentShow = ['public', 'private', 'group', 'away', 'hidden', 'offline'].includes(input.current_show) ? input.current_show : 'unknown'; const tags = [...new Set((Array.isArray(input.tags) ? input.tags : []) .map(value => discoverCleanText(value, 50).toLocaleLowerCase()).filter(Boolean))].slice(0, 20); const started = Number(input.start_timestamp) || Math.floor(Date.parse(input.start_dt_utc || '') / 1000) || 0; return { id: username, username, currentShow, subject: discoverCleanText(input.room_subject || input.subject, 300), tags, gender: ['f', 'm', 'c', 't'].includes(input.gender) ? input.gender : 'unknown', age: Math.max(0, Math.round(Number(input.display_age) || 0)), location: discoverCleanText(input.location, 100), country: discoverCleanText(input.country, 8).toUpperCase(), viewers: Math.max(0, Math.round(Number(input.num_users) || 0)), followers: Math.max(0, Math.round(Number(input.num_followers) || 0)), secondsOnline: started > 0 ? Math.max(0, Math.round(Date.now() / 1000 - started)) : 0, isNew: !!input.is_new, isGaming: !!input.is_gaming, isFollowing: !!input.is_following, isHd: input.is_hd === true, imageUrl: discoverSafeImageUrl(input.img || input.image_url_360x270 || input.image_url, username), source: 'catalog', }; } function discoverPackCachedRoom(room) { return [ room.id, room.currentShow, room.subject, room.tags, room.gender, room.age, room.location, room.country, room.viewers, room.followers, room.secondsOnline, room.isNew ? 1 : 0, room.isGaming ? 1 : 0, room.isFollowing ? 1 : 0, room.isHd ? 1 : 0, room.imageUrl, ]; } function discoverUnpackCachedRoom(input, elapsedSeconds = 0) { if (!Array.isArray(input) || input.length < 11) return null; const id = normalizeUsername(input[0]); if (!isLikelyUsername(id)) return null; return { id, username: id, currentShow: ['public', 'private', 'group', 'away', 'hidden', 'offline'].includes(input[1]) ? input[1] : 'unknown', subject: discoverCleanText(input[2], 300), tags: [...new Set((Array.isArray(input[3]) ? input[3] : []).map(value => discoverCleanText(value, 50).toLocaleLowerCase()).filter(Boolean))].slice(0, 20), gender: ['f', 'm', 'c', 't'].includes(input[4]) ? input[4] : 'unknown', age: Math.max(0, Math.round(Number(input[5]) || 0)), location: discoverCleanText(input[6], 100), country: discoverCleanText(input[7], 8).toUpperCase(), viewers: Math.max(0, Math.round(Number(input[8]) || 0)), followers: Math.max(0, Math.round(Number(input[9]) || 0)), secondsOnline: Math.max(0, Math.round(Number(input[10]) || 0) + Math.max(0, elapsedSeconds)), isNew: !!input[11], isGaming: !!input[12], isFollowing: !!input[13], isHd: !!input[14], imageUrl: discoverSafeImageUrl(input[15], id), source: 'catalog-cache', }; } const RoomGridLiveCatalogCache = (() => { let databasePromise = null; const validSnapshot = value => !!value && value.schema === 2 && Array.isArray(value.rooms) && Number.isFinite(Number(value.savedAt)) && Number(value.savedAt) > 0 && Number(value.savedAt) <= Date.now() + 60000; const openDatabase = () => { if (!('indexedDB' in window)) return Promise.resolve(null); if (databasePromise) return databasePromise; databasePromise = new Promise(resolve => { try { const request = indexedDB.open(DISCOVER_CATALOG_CACHE_DB, 1); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains('snapshots')) request.result.createObjectStore('snapshots'); }; request.onsuccess = () => resolve(request.result); request.onerror = () => resolve(null); request.onblocked = () => resolve(null); } catch (_) { resolve(null); } }); return databasePromise; }; const saveHot = snapshot => { try { sessionStorage.setItem(DISCOVER_CATALOG_CACHE_KEY, JSON.stringify(snapshot)); return; } catch (_) {} try { const compact = { ...snapshot, rooms: snapshot.rooms.slice(0, 900), offset: Math.min(Number(snapshot.offset) || 0, 900), reachedEnd: false }; sessionStorage.setItem(DISCOVER_CATALOG_CACHE_KEY, JSON.stringify(compact)); } catch (_) {} }; return Object.freeze({ loadHot() { try { const value = JSON.parse(sessionStorage.getItem(DISCOVER_CATALOG_CACHE_KEY) || 'null'); return validSnapshot(value) ? value : null; } catch (_) { return null; } }, async loadFull() { const database = await openDatabase(); if (!database) return null; return new Promise(resolve => { try { const request = database.transaction('snapshots', 'readonly').objectStore('snapshots').get('live'); request.onsuccess = () => resolve(validSnapshot(request.result) ? request.result : null); request.onerror = () => resolve(null); } catch (_) { resolve(null); } }); }, save(snapshot) { if (!validSnapshot(snapshot)) return Promise.resolve(false); saveHot(snapshot); return openDatabase().then(database => { if (!database) return false; return new Promise(resolve => { try { const request = database.transaction('snapshots', 'readwrite').objectStore('snapshots').put(snapshot, 'live'); request.onsuccess = () => resolve(true); request.onerror = () => resolve(false); } catch (_) { resolve(false); } }); }); }, saveHot, // Saving on pagehide must not turn old live information into fresh data. isFresh(snapshot) { return validSnapshot(snapshot) && Date.now() - Number(snapshot.fetchedAt || snapshot.savedAt) <= DISCOVER_CATALOG_CACHE_FRESH_MS; }, }); })(); // The native statistics/library hub is loaded before this module, but it // renders after all modules are initialized. Expose a tiny, read-only bridge // so it can recover the same persisted public catalog instead of showing a // misleadingly small local-only count after a cache expiry or update. globalThis.RoomGridCatalogBridge = Object.freeze({ getCurrentModels() { return discoverCurrentCatalog?.() || null; }, async loadFullModels() { const snapshot = await RoomGridLiveCatalogCache.loadFull(); if (!snapshot?.rooms?.length) return null; const elapsedSeconds = RoomGridLiveCatalogCache.isFresh(snapshot) ? Math.max(0, Math.round((Date.now() - Number(snapshot.savedAt || Date.now())) / 1000)) : 0; const rooms = snapshot.rooms.map(item => discoverUnpackCachedRoom(item, elapsedSeconds)).filter(Boolean); return rooms.length ? { ...snapshot, rooms } : null; }, loadHotModels() { const snapshot = RoomGridLiveCatalogCache.loadHot(); if (!snapshot?.rooms?.length) return null; const elapsedSeconds = RoomGridLiveCatalogCache.isFresh(snapshot) ? Math.max(0, Math.round((Date.now() - Number(snapshot.savedAt || Date.now())) / 1000)) : 0; const rooms = snapshot.rooms.map(item => discoverUnpackCachedRoom(item, elapsedSeconds)).filter(Boolean); return rooms.length ? { ...snapshot, rooms } : null; }, }); function discoverCatalogPayload(input) { let value = input; for (let depth = 0; depth < 4; depth += 1) { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const rooms = Array.isArray(value.rooms) ? value.rooms : Array.isArray(value.results) ? value.results : Array.isArray(value.room_list) ? value.room_list : null; if (rooms) { return { rooms, total_count: Number(value.total_count) || Number(value.count) || Number(value.total) || 0, all_rooms_count: Number(value.all_rooms_count) || 0, }; } value = value.data || value.payload || value.response || null; } return null; } function discoverHoverScale(card, requested) { const rect = card.getBoundingClientRect(); return Math.max(1, Math.min(requested, (innerWidth - 16) / Math.max(1, rect.width), (innerHeight - 24) / Math.max(1, rect.height))); } function discoverHoverTransformOrigin(card, scale) { const rect = card.getBoundingClientRect(); const axis = (start, size, viewport) => { if (scale <= 1 || size <= 0) return '50%'; const delta = scale - 1; const minimum = Math.max(0, size - (viewport - 8 - start - size) / delta); const maximum = Math.min(size, Math.max(0, (start - 8) / delta)); const origin = Math.min(size, Math.max(minimum, Math.min(size / 2, maximum))); return `${100 * origin / size}%`; }; return `${axis(rect.left, rect.width, innerWidth)} ${axis(rect.top, rect.height, innerHeight)}`; } function createDiscoverPreviewScheduler(getOptions) { const entries = new Map(); let activeLoads = 0; let scrollingUntil = 0; let destroyed = false; let nextWaveAt = 0; const setStatus = (entry, value, label) => { if (!entry?.status) return; entry.status.dataset.state = value; entry.status.textContent = label; }; const settle = (entry, ok, token = entry?.token) => { if (!entry || token !== entry.token || !entry.loading) return; entry.loading = false; activeLoads = Math.max(0, activeLoads - 1); clearTimeout(entry.timeout); entry.timeout = 0; if (entry.probe) { entry.probe.onload = null; entry.probe.onerror = null; if (!ok) entry.probe.src = ''; entry.probe = null; } entry.onLoad = null; entry.onError = null; const options = getOptions(); const fps = Number(options.fps) || 0; entry.nextAt = ok && fps ? (options.mode === 'simultaneous' ? nextWaveAt : Date.now() + Math.round(1000 / fps)) : Date.now() + 15000; if (ok) setStatus(entry, 'live', `LIVE · ${String(fps).replace('.', ',')} FPS`); else if (entry.visible) setStatus(entry, 'unavailable', tx('预览不可用', 'Preview unavailable', 'Vorschau nicht verfügbar')); }; const deactivate = (entry, offscreen = false) => { if (!entry) return; entry.token += 1; if (entry.loading) { entry.loading = false; activeLoads = Math.max(0, activeLoads - 1); } clearTimeout(entry.timeout); entry.timeout = 0; if (entry.probe) { entry.probe.onload = null; entry.probe.onerror = null; entry.probe.src = ''; entry.probe = null; } entry.onLoad = null; entry.onError = null; if ((offscreen || document.hidden) && entry.baseSrc && entry.img.src !== entry.baseSrc) entry.img.src = entry.baseSrc; setStatus(entry, offscreen ? 'offscreen' : 'paused', offscreen ? '' : tx('已暂停', 'Paused', 'Pausiert')); }; const launch = (entry) => { const options = getOptions(); if (!entry.visible || entry.hovering || entry.loading || !options.enabled || !options.fps || document.hidden) return; const url = discoverSnapshotUrl(entry.model.id); if (!url) return; entry.loading = true; entry.token += 1; const token = entry.token; activeLoads += 1; setStatus(entry, 'loading', tx('实时画面加载中', 'Loading live frame', 'Livebild wird geladen')); // Keep the last decoded image until its replacement is ready. A failed // frame must not erase a working cover or flash the browser's error icon. const probe = new Image(); entry.probe = probe; probe.onload = () => { if (entry.token !== token || !entry.loading) return; entry.img.src = url; settle(entry, true, token); }; probe.onerror = () => settle(entry, false, token); entry.timeout = setTimeout(() => settle(entry, false, token), 12000); probe.src = url; }; const stopHover = entry => { if (!entry) return; clearTimeout(entry.hoverTimer); entry.hoverTimer = 0; entry.hovering = false; RoomGridUltimate.stopInlineHoverPreview(entry.host); entry.card.classList.remove('discover-hover-live-active'); entry.card.style.removeProperty('--discover-hover-live-scale'); entry.card.style.removeProperty('--discover-hover-live-origin'); }; const startHover = entry => { const options = getOptions(); if (!options.hoverEnabled || !entry.visible || entry.model.currentShow !== 'public' || document.hidden) return; if (entry.hovering) return; deactivate(entry, false); entry.hovering = true; const scale = discoverHoverScale(entry.card, options.hoverScale / 100); entry.card.style.setProperty('--discover-hover-live-origin', discoverHoverTransformOrigin(entry.card, scale)); entry.card.style.setProperty('--discover-hover-live-scale', String(scale)); entry.card.classList.add('discover-hover-live-active'); entry.hoverTimer = setTimeout(() => { entry.hoverTimer = 0; if (entry.hovering && entry.visible && !document.hidden) RoomGridUltimate.startInlineHoverPreview(entry.model.id, entry.host); }, options.hoverDelay); }; const pump = () => { if (destroyed || document.hidden || Date.now() < scrollingUntil) return; const options = getOptions(); if (!options.enabled || !options.fps) return; const now = Date.now(); if (options.mode === 'simultaneous') { // Each card already has its own in-flight guard. A slow frame on one // card must not hold every healthy card until its 12-second timeout. if (now < nextWaveAt) return; const due = [...entries.values()] .filter(entry => entry.visible && !entry.hovering && !entry.loading && entry.model.currentShow === 'public' && entry.nextAt <= now); if (!due.length) return; nextWaveAt = now + Math.round(1000 / Number(options.fps)); due.forEach(launch); return; } const due = [...entries.values()] .filter(entry => entry.visible && !entry.hovering && !entry.loading && entry.model.currentShow === 'public' && entry.nextAt <= now) .sort((left, right) => left.nextAt - right.nextAt); const rawConcurrency = Number(options.concurrency); const concurrency = rawConcurrency === 0 ? Math.max(1, due.length + activeLoads) : clampInt(rawConcurrency, 1, 24, 4); if (activeLoads >= concurrency) return; while (due.length && activeLoads < concurrency) launch(due.shift()); }; const observer = 'IntersectionObserver' in window ? new IntersectionObserver(items => { for (const item of items) { const entry = entries.get(item.target); if (!entry) continue; entry.visible = !!item.isIntersecting && item.intersectionRatio > 0; if (entry.visible) { entry.nextAt = 0; nextWaveAt = 0; if (entry.model.currentShow === 'public') setStatus(entry, 'still', tx('实时静帧', 'Live still', 'Live-Standbild')); // Pointer entry can precede the observer's first asynchronous result. if (entry.pointerInside) startHover(entry); } else { stopHover(entry); deactivate(entry, true); } } pump(); scheduleTick(); }, { root: null, rootMargin: '0px', threshold: 0.01 }) : null; let tick = 0; const scheduleTick = () => { clearTimeout(tick); tick = 0; if (destroyed || document.hidden) return; const options = getOptions(); const fps = Number(options.fps) || 0; if (!options.enabled || !fps || ![...entries.values()].some(entry => entry.visible && !entry.hovering && entry.model.currentShow === 'public')) return; const delay = Math.max(4, Math.min(1000, Math.round(1000 / fps))); tick = setTimeout(() => { tick = 0; pump(); scheduleTick(); }, delay); }; const onScroll = () => { scrollingUntil = Date.now() + 180; }; const onVisibility = () => { if (document.hidden) entries.forEach(entry => { stopHover(entry); deactivate(entry, false); }); else { nextWaveAt = 0; entries.forEach(entry => { if (entry.visible) entry.nextAt = 0; }); pump(); } scheduleTick(); }; window.addEventListener('scroll', onScroll, true); document.addEventListener('visibilitychange', onVisibility); return { register(card, model) { const img = card.querySelector('.discover-preview-image'); const status = card.querySelector('.discover-preview-status'); const host = card.querySelector('.discover-preview'); if (!img || !host) return; const entry = { card, model, img, host, status, baseSrc: img.src, visible: !observer, loading: false, nextAt: 0, token: 0, timeout: 0, onLoad: null, onError: null, hovering: false, hoverTimer: 0 }; entry.onPointerEnter = () => { entry.pointerInside = true; startHover(entry); }; entry.onPointerLeave = () => { entry.pointerInside = false; stopHover(entry); entry.nextAt = 0; nextWaveAt = 0; pump(); scheduleTick(); }; card.addEventListener('pointerenter', entry.onPointerEnter); card.addEventListener('pointerleave', entry.onPointerLeave); entries.set(card, entry); if (model.currentShow !== 'public') setStatus(entry, 'unavailable', tx('未直播', 'Not live', 'Nicht live')); else setStatus(entry, 'still', tx('实时静帧', 'Live still', 'Live-Standbild')); if (observer) observer.observe(card); else { pump(); scheduleTick(); } }, clear() { for (const entry of entries.values()) { try { observer?.unobserve(entry.card); } catch (_) {} stopHover(entry); deactivate(entry, true); entry.card.removeEventListener('pointerenter', entry.onPointerEnter); entry.card.removeEventListener('pointerleave', entry.onPointerLeave); } entries.clear(); clearTimeout(tick); tick = 0; }, refresh() { nextWaveAt = 0; for (const entry of entries.values()) { stopHover(entry); deactivate(entry, false); entry.nextAt = 0; } pump(); scheduleTick(); }, destroy() { destroyed = true; clearTimeout(tick); this.clear(); try { observer?.disconnect(); } catch (_) {} window.removeEventListener('scroll', onScroll, true); document.removeEventListener('visibilitychange', onVisibility); }, status() { return { cards: entries.size, visible: [...entries.values()].filter(entry => entry.visible).length, activeLoads }; }, isHovering() { return [...entries.values()].some(entry => entry.hovering && entry.visible && entry.card.isConnected); }, }; } const discoverPreviewFields = [ ['previewEnabled', 'normalLivePreviews', true], ['previewFps', 'normalLivePreviewFps', .5], ['previewMode', 'normalLivePreviewMode', 'simultaneous'], ['previewConcurrency', 'normalLivePreviewConcurrency', 0], ['hoverLivePreview', 'normalHoverLivePreview', true], ['hoverPreviewDelay', 'normalHoverLivePreviewDelay', 250], ['hoverPreviewScale', 'normalHoverLivePreviewScale', 112], ]; function reconcileDiscoverPreviewSettings(state, settings) { // Repair old split preferences: an explicitly customized settings group // wins; untouched global defaults do not erase a saved Suite preset. for (const group of [discoverPreviewFields.slice(0, 4), discoverPreviewFields.slice(4)]) { if (state.previewSettingsSynced || group.some(([, key, fallback]) => settings[key] !== undefined && settings[key] !== fallback)) { for (const [key, setting, fallback] of group) state[key] = settings[setting] ?? fallback; } } } function initDiscover() { const { Math, Number, String, Array, Object, Date, Set, Map, Promise, JSON, URL } = globalThis; RoomGridNativeHub.captureSessionAccount(document); document.title = tx('RoomGrid 发现', 'RoomGrid Discover', 'RoomGrid Entdecken'); stopAllPageMedia(); document.body.replaceChildren(); const store = createStore(); installSuiteExperienceStyles(); applySuiteAppearance(store); const analytics = createAnalytics(); const audioSilencerCleanup = RoomGridAudioSilencer.init(); const notificationCenter = createNotificationCenter(store); const windowCoordinator = createWindowCoordinator(store); const ultimateCleanup = RoomGridUltimate.init(store, analytics); PrivacyShield.init(store); notificationCenter.start(); windowCoordinator.start(); const state = discoverLoadState(); reconcileDiscoverPreviewSettings(state, store.state.settings); let lastPreviewSettings = {}; function publishPreviewSettings() { const patch = {}; for (const [key, setting] of discoverPreviewFields) { lastPreviewSettings[setting] = state[key]; if (store.state.settings[setting] !== state[key]) patch[setting] = state[key]; } if (Object.keys(patch).length) store.patchSettings(patch); } publishPreviewSettings(); if (!state.previewSettingsSynced) { state.previewSettingsSynced = true; discoverSaveState(state); } const catalog = []; const catalogById = new Map(); let statsById = new Map(); let latestWatchedId = ''; let catalogOffset = 0; let catalogTotal = 0; let catalogTotalKnown = false; let catalogPreferredHost = ''; let catalogRateLimitUntil = 0; let catalogAdaptiveDelayMs = 0; let catalogNextRequestAt = 0; const catalogHostCooldownUntil = new Map(); let catalogReachedEnd = false; let catalogLoading = false; let catalogPrefetching = false; let catalogPrefetchToken = 0; let catalogPrefetchRetryTimer = 0; let catalogPrefetchRetryAttempt = 0; let catalogError = ''; let catalogFetchedAt = 0; let catalogAbort = null; let catalogIsFallback = false; let catalogUsingCache = false; let catalogCacheSavedAt = 0; let catalogCacheSaveTimer = 0; let catalogRefreshSeenIds = null; discoverCurrentCatalog = () => ({ rooms: catalogRefreshSeenIds ? catalog.filter(room => catalogRefreshSeenIds.has(room.id)) : catalog, fetchedAt: catalogFetchedAt, complete: catalogReachedEnd && !catalogUsingCache && !catalogIsFallback && !catalogRefreshSeenIds, fresh: !catalogIsFallback && (!catalogUsingCache || !!catalogRefreshSeenIds?.size), }); let destroyed = false; let storeUnsubscribe = null; let previousCatalogVisitAt = state.lastCatalogVisitAt || 0; let singleContextIds = state.singleContext.slice(); let singleReturnView = state.singleReturnView; let singleSession = null; let singleAnalyticsRefresh = null; let commandPalette = null; let pendingScrollRestore = true; let pendingFilterFocus = null; let filterRenderTimer = 0; let backgroundRenderTimer = 0; let tagSuggestionCache = { signature: '', values: [] }; let discoverInterestProfileCache = { signature: '', value: null }; let activeCardMoreClose = null; const profileChanges = new Map(); const publicProfileRequests = new Map(); const publicProfileRetryAt = new Map(); const PUBLIC_PROFILE_FRESH_MS = 6 * 60 * 60 * 1000; const viewDomCache = new Map(); let activeVisualSignature = ''; let catalogDataRevision = 0; let localMetadataRevision = 0; let observationsRevision = 0; let statsRevision = -1; let roomsIndexRevision = -1; let roomsById = new Map(); let nextRoomExpiry = Infinity; let modelDataCache = new WeakMap(); let tagIndexCache = { signature: '', value: null }; let tagCatalogBreakdownCache = { signature: '', value: null }; const selectionCache = new Map(); const recommendationCache = new WeakMap(); const nameComparer = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }); let catalogRefreshTimer = 0; let catalogRefreshRetryAt = 0; document.head.appendChild($('style', { html: trustedHtml(` :root { color-scheme:light; --dc-bg:var(--rg-page-bg,#f3f6fb); --dc-panel:var(--rg-panel,#fff); --dc-panel-soft:var(--rg-surface,#f8fafc); --dc-text:var(--rg-text,#0f172a); --dc-sub:var(--rg-muted,#475569); --dc-muted:var(--rg-muted,#64748b); --dc-line:var(--rg-border,#dfe6ef); --dc-accent:var(--rg-accent,#2563eb); --dc-accent-soft:var(--accent-soft,#dbeafe); --dc-good:#16a34a; --dc-warn:#d97706; --dc-danger:#dc2626; --dc-shadow:0 12px 34px rgba(15,23,42,.10); } * { box-sizing:border-box; } body { margin:0 !important; min-width:320px; background:radial-gradient(circle at 12% 0,rgba(37,99,235,.10),transparent 28%),var(--dc-bg) !important; color:var(--dc-text) !important; font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif !important; } body.discover-dark { color-scheme:dark; --dc-bg:var(--rg-page-bg,#090d14); --dc-panel:var(--rg-panel,#111827); --dc-panel-soft:var(--rg-surface,#172033); --dc-text:var(--rg-text,#f1f5f9); --dc-sub:var(--rg-muted,#cbd5e1); --dc-muted:var(--rg-muted,#94a3b8); --dc-line:var(--rg-border,#2a374a); --dc-accent:var(--rg-accent,#60a5fa); --dc-accent-soft:var(--accent-soft,#172e52); --dc-good:#4ade80; --dc-warn:#fbbf24; --dc-danger:#f87171; --dc-shadow:0 16px 44px rgba(0,0,0,.32); } button,input,select,textarea { font:inherit; } button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible { outline:3px solid color-mix(in srgb,var(--dc-accent) 34%,transparent); outline-offset:2px; } .discover-shell { min-height:100vh; display:flex; flex-direction:column; } .discover-header { position:sticky; top:0; z-index:100; display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; min-height:66px; padding:8px 18px; border-bottom:1px solid var(--dc-line); background:color-mix(in srgb,var(--dc-panel) 92%,transparent); backdrop-filter:blur(18px); box-shadow:0 3px 14px rgba(15,23,42,.05); } .discover-brand { display:flex; align-items:center; gap:10px; min-width:max-content; } .discover-logo { display:grid; place-items:center; min-width:38px; min-height:38px; border-radius:12px; color:#fff; background:linear-gradient(135deg,#2563eb,#14b8a6); font-weight:900; } .discover-logo:has(img[data-roomgrid-brand-logo]) { width:auto; height:auto; background:transparent!important; } .discover-brand strong { display:block; font-size:14px; } .discover-brand span { display:block; color:var(--dc-muted); font-size:10px; } .discover-nav { order:3; flex:1 0 100%; min-width:0; display:flex; align-items:center; gap:5px; overflow-x:auto; overscroll-behavior-inline:contain; scrollbar-width:thin; padding:4px; border:1px solid var(--dc-line); border-radius:12px; background:var(--dc-panel-soft); } .discover-nav button,.discover-btn { min-height:36px; border:1px solid var(--dc-line); border-radius:10px; padding:7px 11px; color:var(--dc-text); background:var(--dc-panel); cursor:pointer; font-weight:700; font-size:12px; } .discover-nav button { flex:0 0 auto; border-color:transparent; background:transparent; white-space:nowrap; } .discover-nav button.active { color:#fff; background:var(--dc-accent); } .discover-btn:hover,.discover-nav button:hover { border-color:var(--dc-accent); } .discover-btn.primary,.discover-btn.is-active,.discover-btn[aria-pressed="true"] { color:#fff; border-color:var(--dc-accent); background:var(--dc-accent); box-shadow:0 0 0 2px color-mix(in srgb,var(--dc-accent) 22%,transparent),0 7px 18px color-mix(in srgb,var(--dc-accent) 20%,transparent); } .discover-btn.is-active::before,.discover-btn[aria-pressed="true"]::before { content:'✓'; margin-right:5px; font-weight:950; } .discover-btn.danger { color:var(--dc-danger); } .discover-header-actions { margin-left:auto; display:flex; align-items:center; gap:7px; } .discover-main { width:100%; max-width:var(--rg-content-max,none); min-width:0; margin:0 auto; padding:clamp(14px,1.35vw,28px); } body.roomgrid-dashboard-active .discover-main,body.roomgrid-singlecam-active .discover-main { width:100%; max-width:none; padding-inline:clamp(12px,1vw,24px); } .discover-hero { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; margin-bottom:18px; } .discover-kicker { margin:0 0 5px; color:var(--dc-accent); font-size:11px; font-weight:900; letter-spacing:.12em; text-transform:uppercase; } .discover-hero h1 { margin:0; font-size:clamp(24px,3vw,38px); line-height:1.08; } .discover-hero p { margin:8px 0 0; max-width:720px; color:var(--dc-sub); font-size:13px; line-height:1.5; } .discover-summary { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:7px; } .discover-chip { display:inline-flex; align-items:center; gap:6px; min-height:30px; padding:5px 9px; border:1px solid var(--dc-line); border-radius:999px; background:var(--dc-panel); color:var(--dc-sub); font-size:11px; font-weight:750; } .discover-chip.live { color:var(--dc-good); } .discover-panel { margin-bottom:14px; padding:14px; border:1px solid var(--dc-line); border-radius:16px; background:var(--dc-panel); box-shadow:0 4px 16px rgba(15,23,42,.04); } .discover-controls { display:grid; grid-template-columns:minmax(220px,2fr) repeat(4,minmax(130px,1fr)); gap:9px; align-items:end; } .discover-field { display:grid; gap:5px; min-width:0; color:var(--dc-muted); font-size:10px; font-weight:800; letter-spacing:.03em; } .discover-input { width:100%; min-height:38px; padding:8px 10px; border:1px solid var(--dc-line); border-radius:10px; outline:none; background:var(--dc-panel-soft); color:var(--dc-text); font-size:12px; } .discover-input:focus { border-color:var(--dc-accent); box-shadow:0 0 0 3px color-mix(in srgb,var(--dc-accent) 13%,transparent); } .discover-autocomplete { position:relative; min-width:0; } .discover-autocomplete-menu { position:absolute; z-index:80; top:calc(100% + 5px); left:0; right:0; display:none; max-height:260px; overflow:auto; padding:5px; border:1px solid var(--dc-line); border-radius:10px; background:var(--dc-panel); box-shadow:0 16px 38px rgba(2,6,23,.28); } .discover-autocomplete-menu.is-open { display:grid; gap:3px; } .discover-autocomplete-option { width:100%; min-height:30px; padding:6px 8px; border:0; border-radius:7px; color:var(--dc-text); background:transparent; text-align:left; font-size:11px; font-weight:750; cursor:pointer; } .discover-autocomplete-option:hover,.discover-autocomplete-option.is-active { color:#fff; background:var(--dc-accent); } .discover-details { margin-top:10px; border-top:1px solid var(--dc-line); padding-top:10px; } .discover-details > summary { cursor:pointer; color:var(--dc-sub); font-size:12px; font-weight:800; } .discover-active-filters { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin-top:11px; padding:10px 11px; border:1px solid var(--dc-line); border-left:4px solid var(--dc-accent); border-radius:12px; background:var(--dc-panel-soft); } .discover-active-filters.is-active { background:color-mix(in srgb,var(--dc-accent) 8%,var(--dc-panel-soft)); } .discover-active-filter-title { display:inline-flex; align-items:center; gap:6px; margin-right:2px; color:var(--dc-text); font-size:11px; font-weight:900; } .discover-active-filter-count { display:grid; place-items:center; min-width:21px; height:21px; padding:0 6px; border-radius:999px; color:#fff; background:var(--dc-accent); font-size:10px; } .discover-active-filter { display:inline-flex; align-items:center; gap:7px; min-height:30px; max-width:100%; padding:5px 7px 5px 9px; border:1px solid color-mix(in srgb,var(--dc-accent) 42%,var(--dc-line)); border-radius:999px; color:var(--dc-text); background:var(--dc-panel); font-size:11px; font-weight:800; cursor:pointer; } .discover-active-filter:hover { border-color:var(--dc-danger); background:color-mix(in srgb,var(--dc-danger) 8%,var(--dc-panel)); } .discover-active-filter.is-negative { border-color:color-mix(in srgb,var(--dc-danger) 55%,var(--dc-line)); } .discover-active-filter-label { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .discover-active-filter-remove { display:grid; place-items:center; width:18px; height:18px; border-radius:999px; color:var(--dc-muted); background:var(--dc-panel-soft); font-size:14px; line-height:1; } .discover-active-filter:hover .discover-active-filter-remove { color:#fff; background:var(--dc-danger); } .discover-active-filter-empty { color:var(--dc-muted); font-size:11px; font-weight:700; } .discover-active-filter-clear { margin-left:auto; min-height:30px; padding:5px 9px; border:1px solid var(--dc-line); border-radius:9px; color:var(--dc-sub); background:var(--dc-panel); font-size:11px; font-weight:800; cursor:pointer; } .discover-active-filter-clear:hover { color:var(--dc-danger); border-color:var(--dc-danger); } .discover-filter-grid { display:grid; grid-template-columns:repeat(5,minmax(130px,1fr)); gap:9px; margin-top:11px; } .discover-checks { display:flex; align-items:center; flex-wrap:wrap; gap:9px; margin-top:10px; } .discover-check { display:inline-flex; align-items:center; gap:5px; color:var(--dc-sub); font-size:11px; font-weight:700; } .discover-check input { accent-color:var(--dc-accent); } .discover-saved { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:11px; } .discover-saved-row { display:flex; flex-wrap:wrap; align-items:end; gap:7px; padding:10px; border:1px solid var(--dc-line); border-radius:12px; background:var(--dc-panel-soft); } .discover-saved-row .discover-field { flex:1 1 170px; } .discover-status { min-height:38px; display:flex; align-items:center; justify-content:space-between; gap:10px; margin:2px 0 12px; color:var(--dc-sub); font-size:12px; } .discover-status.is-error { color:var(--dc-danger); } .discover-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(clamp(235px,15vw,310px),1fr)); gap:clamp(10px,1vw,18px); } .discover-card { position:relative; min-width:0; overflow:hidden; content-visibility:auto; contain-intrinsic-size:auto 470px; border:1px solid var(--dc-line); border-radius:16px; background:var(--dc-panel); box-shadow:0 5px 18px rgba(15,23,42,.07); transition:transform .16s,border-color .16s,box-shadow .16s; } .discover-card.selected { border-color:var(--rg-accent,var(--dc-accent)); box-shadow:0 0 0 3px rgba(var(--rg-accent-rgb,37,99,235),.18),0 8px 26px rgba(15,23,42,.12); }.discover-select{position:absolute;right:8px;top:8px;z-index:9;width:29px;height:29px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.55);border-radius:9px;background:rgba(15,23,42,.68);color:#fff;cursor:pointer;backdrop-filter:blur(8px)}.discover-select input{accent-color:var(--rg-accent,var(--dc-accent))}.discover-card.compact .discover-select{display:none}.discover-bulk{position:sticky;top:64px;z-index:12;display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 12px;padding:9px 11px;border:1px solid var(--dc-line);border-radius:13px;background:var(--dc-panel);box-shadow:0 10px 28px rgba(15,23,42,.12)} .discover-card:hover { transform:translateY(-2px); border-color:color-mix(in srgb,var(--dc-accent) 50%,var(--dc-line)); box-shadow:var(--dc-shadow); } .discover-card.discover-hover-live-active { z-index:40; transform:translateY(-4px) scale(var(--discover-hover-live-scale,1.12)); transform-origin:var(--discover-hover-live-origin,center); border-color:var(--dc-accent); box-shadow:0 24px 65px rgba(2,6,23,.38); } .discover-preview { position:relative; aspect-ratio:4/3; overflow:hidden; background:#0b1019; } .discover-preview-image { width:100%; height:100%; display:block; object-fit:cover; color:transparent; } .discover-preview-shade { position:absolute; inset:0; pointer-events:none; background:linear-gradient(180deg,rgba(0,0,0,.36),transparent 30%,transparent 62%,rgba(0,0,0,.55)); } .discover-card-badges { position:absolute; top:8px; left:8px; right:8px; display:flex; flex-wrap:wrap; gap:5px; pointer-events:none; } .discover-badge { display:inline-flex; align-items:center; min-height:22px; padding:3px 7px; border:1px solid rgba(255,255,255,.25); border-radius:999px; color:#fff; background:rgba(15,23,42,.72); font-size:9px; font-weight:900; backdrop-filter:blur(8px); } .discover-badge.live { background:rgba(22,163,74,.82); } .discover-badge.new { background:rgba(37,99,235,.86); } .discover-card-body { padding:11px; } .discover-card-title { display:flex; align-items:center; gap:7px; min-width:0; } .discover-card-title strong { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; } .discover-card-title span { margin-left:auto; color:var(--dc-muted); font-size:10px; } .discover-subject { height:36px; margin:7px 0; overflow:hidden; color:var(--dc-sub); font-size:11px; line-height:1.45; } .discover-tags { min-height:25px; display:flex; align-items:flex-start; flex-wrap:wrap; gap:4px; overflow:hidden; } .discover-tag { max-width:110px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:3px 6px; border:0; border-radius:999px; background:var(--dc-accent-soft); color:var(--dc-accent); font-size:9px; font-weight:800; cursor:pointer; } .discover-meta { display:flex; align-items:center; justify-content:space-between; gap:7px; margin-top:8px; color:var(--dc-muted); font-size:10px; } .discover-card-actions { position:relative; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; margin-top:10px; } .discover-card-actions .discover-btn { min-height:33px; padding:5px 7px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .discover-card-group { grid-column:1/-1; } .discover-card-more{position:relative}.discover-card-more>summary{list-style:none}.discover-card-more>summary::-webkit-details-marker{display:none}.discover-card-menu{position:fixed;z-index:310;width:min(286px,calc(100vw - 16px));display:grid;grid-template-columns:1fr 1fr;gap:7px;padding:10px;border:1px solid color-mix(in srgb,var(--dc-accent) 40%,var(--dc-line));border-radius:14px;background:color-mix(in srgb,var(--dc-panel) 96%,transparent);box-shadow:0 22px 65px rgba(0,0,0,.42);backdrop-filter:blur(16px)}.discover-card-menu-head{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 2px 6px;border-bottom:1px solid var(--dc-line);color:var(--dc-muted);font-size:10px;font-weight:850}.discover-card-menu-head strong{min-width:0;color:var(--dc-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.discover-card-menu-close{width:26px;height:26px;padding:0;border:1px solid var(--dc-line);border-radius:8px;background:var(--dc-panel-soft);color:var(--dc-text);cursor:pointer}.discover-card-menu .discover-card-group{grid-column:1/-1}.discover-card-more:not([open]) .discover-card-menu{display:none}.discover-card:has(.discover-card-more[open]){overflow:visible;z-index:300}.discover-card-more[open]>summary{color:#fff;border-color:var(--dc-accent);background:var(--dc-accent)} .discover-card-actions a.discover-btn{display:inline-flex;align-items:center;justify-content:center;text-decoration:none} .discover-empty { grid-column:1/-1; padding:50px 20px; border:1px dashed var(--dc-line); border-radius:16px; color:var(--dc-muted); text-align:center; background:var(--dc-panel); } .discover-pagination { display:flex; justify-content:center; align-items:center; gap:9px; margin:18px 0 4px; color:var(--dc-sub); font-size:12px; } .discover-inspiration { margin:0 0 22px; padding:clamp(13px,1.2vw,20px); border:1px solid color-mix(in srgb,var(--dc-accent) 38%,var(--dc-line)); border-radius:20px; background:linear-gradient(145deg,color-mix(in srgb,var(--dc-accent) 10%,var(--dc-panel)),var(--dc-panel) 48%,color-mix(in srgb,var(--dc-accent-2,#14b8a6) 7%,var(--dc-panel))); box-shadow:var(--dc-shadow); } .discover-inspiration-head { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:14px; } .discover-inspiration-head h2 { margin:0; font-size:clamp(20px,2vw,29px); } .discover-inspiration-head p { max-width:780px; margin:5px 0 0; color:var(--dc-sub); font-size:12px; line-height:1.5; } .discover-inspiration-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:7px; } .discover-inspiration-scope { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin:0 0 13px; padding:9px 10px; border:1px solid var(--dc-line); border-radius:12px; background:color-mix(in srgb,var(--dc-panel-soft) 86%,transparent); color:var(--dc-muted); font-size:11px; } .discover-lanes { display:grid; gap:14px; } .discover-lane { min-width:0; padding:12px; border:1px solid var(--dc-line); border-radius:16px; background:color-mix(in srgb,var(--dc-panel) 94%,transparent); } .discover-lane-head { display:flex; align-items:flex-end; justify-content:space-between; gap:12px; margin-bottom:10px; } .discover-lane-head h3 { margin:0; font-size:17px; } .discover-lane-head p { margin:3px 0 0; color:var(--dc-muted); font-size:11px; line-height:1.4; } .discover-lane-grid { display:grid; grid-auto-flow:column; grid-auto-columns:minmax(230px,290px); grid-template-columns:none; gap:11px; overflow-x:auto; overscroll-behavior-inline:contain; scroll-snap-type:x proximity; padding:2px 2px 9px; scrollbar-width:thin; } .discover-lane-grid .discover-card { scroll-snap-align:start; } .discover-all-results-head { display:flex; align-items:flex-end; justify-content:space-between; gap:12px; margin:24px 0 10px; } .discover-all-results-head h2 { margin:0; font-size:21px; } .discover-all-results-head p { margin:4px 0 0; color:var(--dc-muted); font-size:11px; } .dashboard-stats { display:grid; grid-template-columns:repeat(4,minmax(150px,1fr)); gap:12px; margin-bottom:16px; } .dashboard-stat { padding:15px; border:1px solid var(--dc-line); border-radius:15px; background:var(--dc-panel); box-shadow:0 4px 16px rgba(15,23,42,.04); } .dashboard-stat span { display:block; color:var(--dc-muted); font-size:10px; font-weight:800; text-transform:uppercase; letter-spacing:.08em; } .dashboard-stat strong { display:block; margin-top:6px; font-size:23px; } .dashboard-live-center{margin:18px 0 22px;padding:clamp(12px,1.1vw,18px);border:1px solid var(--dc-line);border-top:4px solid var(--dc-accent);border-radius:18px;background:linear-gradient(180deg,color-mix(in srgb,var(--dc-accent) 5%,var(--dc-panel)),var(--dc-panel));box-shadow:var(--dc-shadow)} .dashboard-live-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:12px}.dashboard-live-head h2{margin:0;font-size:clamp(19px,2vw,27px)}.dashboard-live-head p{margin:4px 0 0;color:var(--dc-muted);font-size:11px;line-height:1.45}.dashboard-live-head-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:7px} .dashboard-live-center{margin-top:0}.dashboard-live-head{align-items:center}.dashboard-tool-count{padding:2px 6px;border-radius:999px;background:var(--dc-accent);color:#fff;font-size:10px}.dashboard-tools-panel{margin-bottom:12px;padding:12px;border:1px solid var(--dc-line);border-radius:13px;background:var(--dc-panel-soft);max-height:52vh;overflow:auto;overscroll-behavior:contain}.dashboard-tools-panel[hidden]{display:none!important}.dashboard-tools-panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.dashboard-tools-panel-head h3{margin:0;font-size:15px}.dashboard-tools-panel .discover-panel{margin-bottom:0;box-shadow:none}.dashboard-tools-panel .dashboard-display-panel{margin-bottom:0}.dashboard-tools-panel .discover-hero{margin-top:0}.dashboard-tools-panel .discover-hero h1{font-size:22px}.dashboard-live-center>.discover-active-filters{margin-top:0}.dashboard-tools-panel:focus-visible{outline:2px solid var(--dc-accent);outline-offset:2px} .dashboard-display-panel{margin:0 0 12px;border:1px solid var(--dc-line);border-radius:13px;background:var(--dc-panel-soft)}.dashboard-display-panel>summary{padding:10px 12px;cursor:pointer;color:var(--dc-text);font-size:12px;font-weight:900}.dashboard-display-grid{display:grid;grid-template-columns:repeat(5,minmax(130px,1fr));gap:9px;padding:0 11px 11px}.dashboard-preset-row,.dashboard-widget-toggle-row{grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:center;gap:7px}.dashboard-widget-toggle-row{padding-top:9px;border-top:1px solid var(--dc-line)}.dashboard-widget-toggle-row>span{color:var(--dc-muted);font-size:11px;font-weight:850}.dashboard-widget-toggle-row .discover-btn{min-height:32px;padding:5px 9px} .dashboard-live-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--dashboard-card-min,260px),1fr));gap:clamp(9px,1vw,17px)}.dashboard-live-grid .discover-preview{aspect-ratio:var(--dashboard-image-ratio,4/3)}.dashboard-live-grid.density-compact .discover-card-body{padding:8px}.dashboard-live-grid.density-compact .discover-subject,.dashboard-live-grid.density-compact .discover-tags{display:none}.dashboard-live-grid.density-compact .discover-card-actions{margin-top:7px}.dashboard-live-grid.density-spacious .discover-card-body{padding:15px}.dashboard-live-grid.density-spacious .discover-subject{height:52px;font-size:12px}.dashboard-live-grid.density-spacious .discover-tags{min-height:44px}.dashboard-live-grid.density-spacious .discover-card-actions .discover-btn{min-height:38px} .dashboard-live-status{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin:8px 0 12px;color:var(--dc-sub);font-size:11px}.dashboard-live-status strong{color:var(--dc-text)} .dashboard-tools-panel-head{position:sticky;top:0;z-index:81;background:var(--dc-panel-soft)} .dashboard-widget-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px;margin:18px 0 24px}.dashboard-section{grid-column:span 12;margin:0;padding:12px;border:1px solid var(--dc-line);border-radius:16px;background:var(--dc-panel)}.dashboard-section.size-small{grid-column:span 4}.dashboard-section.size-wide{grid-column:span 8}.dashboard-section.size-full{grid-column:span 12}.dashboard-section[draggable="true"]{cursor:grab}.dashboard-section.is-dragging{opacity:.45}.dashboard-section.is-drop-target{outline:2px solid var(--rg-accent,#2563eb);outline-offset:3px} .dashboard-section-head { display:flex; align-items:flex-end; justify-content:space-between; gap:10px; margin-bottom:10px; } .dashboard-section h2 { margin:0; font-size:17px; } .dashboard-section p { margin:3px 0 0; color:var(--dc-muted); font-size:11px; } .dashboard-section .discover-grid { grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); } .tag-layout { display:grid; grid-template-columns:minmax(260px,.9fr) minmax(0,2fr); gap:16px; align-items:start; } .tag-browser-column,.tag-results-column { min-width:0; } .tag-browser-column [data-tag-browser] > .tag-cloud { max-height:460px; overflow:auto; padding:2px; } .tag-selection-panel { border-color:var(--dc-accent); border-top:3px solid var(--dc-accent); } .tag-selected-chips { margin:12px 0; }.tag-selected-chips:empty{display:none} .tag-section .discover-field { min-width:0; }.tag-section .discover-field select{width:100%} .tag-rule-explanation,.tag-selection-panel > p { color:var(--dc-muted); font-size:12px; line-height:1.6; } .tag-scope-bar,.tag-pagination { display:flex; align-items:center; flex-wrap:wrap; gap:7px; margin:10px 0; } .tag-pagination { justify-content:center; margin-top:16px; }.tag-pagination span{font-size:12px;color:var(--dc-muted)} .tag-list-status,.tag-suggestions p { color:var(--dc-muted); font-size:11px; line-height:1.5; } .tag-suggestions { display:flex; gap:7px; flex-wrap:wrap; padding:9px; border:1px solid var(--dc-line); border-radius:10px; margin:10px 0; }.tag-suggestions p{width:100%;margin:0} .tag-extra > summary { cursor:pointer; font-weight:800; font-size:13px; }.tag-extra[open] > summary{margin-bottom:10px}.tag-extra > p{color:var(--dc-muted);font-size:12px;line-height:1.5} .tag-main span { overflow-wrap:anywhere; }.tag-pill { max-width:100%; }.tag-section button:focus-visible{outline:2px solid var(--dc-accent);outline-offset:3px} @media(max-width:1100px){.tag-layout{display:flex;flex-direction:column;gap:0}.tag-browser-column,.tag-results-column{display:contents}.tag-layout .tag-section{width:100%;box-sizing:border-box}.tag-layout [data-tag-browser]{order:0}.tag-layout .tag-selection-panel{order:1}.tag-results-column>.tag-section:not(.tag-selection-panel){order:2}.tag-layout .tag-extra{order:3}.tag-browser-column [data-tag-browser]>.tag-cloud{max-height:280px}} .tag-cloud { display:flex; flex-wrap:wrap; gap:8px; } .tag-pill { display:inline-flex; align-items:center; gap:7px; min-height:38px; padding:6px 9px; border:1px solid var(--dc-line); border-radius:12px; background:var(--dc-panel-soft); color:var(--dc-text); font-size:11px; font-weight:800; cursor:pointer; } .tag-pill:hover,.tag-pill.selected { border-color:var(--dc-accent); background:var(--dc-accent-soft); } .tag-pill.blocked { opacity:.48; text-decoration:line-through; } .tag-pill small { color:var(--dc-muted); font-size:9px; } .tag-pill .trend-up { color:var(--dc-good); } .tag-pill .trend-down { color:var(--dc-danger); } .tag-icon { min-width:26px; min-height:26px; padding:2px 5px; border:0; border-radius:8px; background:transparent; color:var(--dc-muted); cursor:pointer; } .tag-icon.active { color:#eab308; } .tag-main { display:inline-flex; align-items:center; gap:7px; min-width:0; color:var(--dc-text); font-weight:850; } .tag-artwork { display:inline-block; width:27px; height:27px; flex:0 0 27px; overflow:hidden; border:1px solid color-mix(in srgb,var(--dc-line) 75%,transparent); border-radius:8px; background-color:color-mix(in srgb,var(--dc-panel) 72%,transparent); background-repeat:no-repeat; box-shadow:0 2px 8px rgba(2,6,23,.18); vertical-align:middle; } .tag-artwork.sprite { background-image:var(--camclient-tag-art-sprite); } .tag-artwork.large { width:64px; height:64px; flex-basis:64px; border-radius:15px; } .tag-artwork.custom { object-fit:contain; background:transparent; } .tag-artwork-edit { color:var(--dc-accent); } .tag-artwork-gallery { display:grid; grid-template-columns:repeat(3,minmax(90px,1fr)); gap:8px; } .tag-artwork-choice { display:flex; align-items:center; gap:9px; min-height:52px; padding:7px; border:1px solid var(--dc-line); border-radius:11px; background:var(--dc-panel-soft); color:var(--dc-text); font-weight:800; cursor:pointer; } .tag-artwork-choice:hover,.tag-artwork-choice.active { border-color:var(--dc-accent); background:var(--dc-accent-soft); } .tag-artwork-modal { position:fixed; z-index:1300; inset:0; display:grid; place-items:center; padding:18px; background:rgba(2,6,23,.72); backdrop-filter:blur(7px); } .tag-artwork-dialog { width:min(620px,100%); max-height:min(760px,92vh); overflow:auto; padding:18px; border:1px solid var(--dc-line); border-radius:18px; background:var(--dc-panel); box-shadow:0 30px 90px rgba(0,0,0,.46); } .tag-artwork-dialog-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; } .tag-artwork-dialog h2 { margin:0; font-size:20px; }.tag-artwork-dialog p{color:var(--dc-muted);font-size:12px;line-height:1.5}.tag-artwork-dialog-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px} .tag-section { margin-bottom:14px; padding:14px; border:1px solid var(--dc-line); border-radius:16px; background:var(--dc-panel); } .tag-section h2 { margin:0 0 4px; font-size:17px; } .tag-section > p { margin:0 0 12px; color:var(--dc-muted); font-size:11px; line-height:1.5; } .tag-toolbar { display:flex; flex-wrap:wrap; gap:8px; align-items:end; margin-bottom:12px; } .tag-toolbar .discover-field { flex:1 1 220px; } .tag-groups { display:grid; gap:8px; } .tag-group { display:flex; align-items:center; flex-wrap:wrap; gap:7px; padding:9px; border:1px solid var(--dc-line); border-radius:11px; background:var(--dc-panel-soft); } .tag-group strong { margin-right:auto; } .tag-list { display:grid; gap:7px; } .tag-list button { text-align:left; } .single-page{width:100%;margin:auto}.single-topbar{position:sticky;top:calc(var(--discover-header-height,108px) + 8px);z-index:55;display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:14px;padding:9px 11px;border:1px solid var(--dc-line);border-left:4px solid #f47321;border-radius:8px;background:color-mix(in srgb,var(--dc-panel) 94%,transparent);box-shadow:0 8px 24px rgba(0,0,0,.14);backdrop-filter:blur(16px)} .single-topbar strong{margin-right:auto;font-size:18px}.single-topbar .single-sound-state{border-color:#23744d;background:#184f38;color:#d9ffea}.single-topbar .single-sound-state.is-on{border-color:#7c5a29;background:#573b17;color:#ffe3ad} .single-layout{display:grid;grid-template-columns:minmax(0,2.25fr) minmax(330px,.75fr);gap:16px;align-items:start}.single-page[data-sidebar="left"] .single-layout{grid-template-columns:minmax(330px,.75fr) minmax(0,2.25fr)}.single-page[data-sidebar="left"] .single-details{order:-1}.single-page[data-sidebar="below"] .single-layout,.single-page[data-layout="focus"] .single-layout{grid-template-columns:1fr}.single-page[data-layout="focus"] .single-details{order:initial}.single-page[data-layout="compact"]{max-width:1320px}.single-page[data-layout="compact"] .single-layout{grid-template-columns:minmax(0,1.65fr) minmax(310px,.75fr)} .single-player-panel{overflow:hidden;border:1px solid #405061;border-top:4px solid var(--dc-accent);border-radius:9px;background:#070a10;box-shadow:0 18px 50px rgba(0,0,0,.25)}.single-stage{position:relative;display:grid;place-items:center;min-height:390px;aspect-ratio:16/9;overflow:hidden;background:#05070b}.single-page[data-aspect="4/3"] .single-stage{aspect-ratio:4/3}.single-page[data-aspect="21/9"] .single-stage{aspect-ratio:21/9}.single-page[data-background="soft"] .single-stage{background:#172033}.single-page[data-background="ambient"] .single-stage{background:radial-gradient(circle at 50% 45%,#31445b,#05070b 72%)} .single-page[data-stage-size="standard"] .single-stage{min-height:320px}.single-page[data-stage-size="large"] .single-stage{min-height:min(56vh,680px)}.single-page[data-stage-size="maximum"] .single-stage{min-height:min(78vh,980px);aspect-ratio:auto}.single-page[data-player-corners="square"] .single-player-panel{border-radius:0}.single-page[data-player-corners="round"] .single-player-panel{border-radius:20px}.single-page[data-cockpit-size="compact"] .single-layout{grid-template-columns:minmax(0,2.7fr) minmax(310px,.65fr)}.single-page[data-cockpit-size="wide"] .single-layout{grid-template-columns:minmax(0,2fr) minmax(500px,1fr)} .single-video{position:relative;z-index:1;width:100%;height:100%;object-fit:contain;background:transparent;will-change:transform}.single-player-state{position:absolute;inset:auto 16px 16px;z-index:2;padding:9px 11px;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;background:rgba(15,23,42,.86);font-size:12px;font-weight:800;pointer-events:none}.single-controls{display:grid;gap:9px;padding:10px;color:#fff;background:#101a27}.single-page[data-control-size="compact"] .single-controls{gap:6px;padding:7px}.single-page[data-control-size="large"] .single-controls{gap:12px;padding:14px}.single-page[data-control-size="large"] .single-controls .discover-btn,.single-page[data-control-size="large"] .single-controls .discover-input{min-height:42px;font-size:12px}.single-controls .discover-btn,.single-controls .discover-input{color:#fff;border-color:#405061;background:#1c2c3a}.single-primary-controls{display:flex;flex-wrap:wrap;align-items:center;gap:7px}.single-primary-controls .single-volume{flex:1 1 130px;max-width:210px}.single-volume{width:100%;accent-color:#f47321}.single-audio-activity{display:grid;grid-template-columns:auto minmax(150px,1fr) auto;align-items:center;gap:8px;min-width:min(100%,420px);padding:7px 9px;border:1px solid #405061;border-radius:7px;background:#0b1722;color:#d8e8f2}.single-audio-title{display:grid;gap:1px;min-width:94px}.single-audio-title strong{font-size:10px}.single-audio-title small{color:#8ea7b7;font-size:8px;line-height:1.25}.single-audio-track{position:relative;height:13px;overflow:hidden;border:1px solid #31485c;border-radius:999px;background:linear-gradient(90deg,#132534 0 58%,#3c2c13 58% 82%,#44202a 82%)}.single-audio-fill{position:absolute;inset:0 auto 0 0;width:0;border-radius:inherit;background:linear-gradient(90deg,#2dd4bf 0 58%,#fbbf24 76%,#fb7185 100%);box-shadow:0 0 10px rgba(45,212,191,.28);transition:width 80ms linear}.single-audio-peak{position:absolute;top:1px;bottom:1px;left:0;width:2px;border-radius:2px;background:#fff;opacity:.9;transition:left 80ms linear}.single-audio-readout{display:grid;justify-items:end;min-width:75px;line-height:1.15}.single-audio-readout strong{font-size:10px}.single-audio-readout span{color:#8ea7b7;font-size:8px}.single-audio-activity[data-state="loud"]{border-color:#fb7185}.single-audio-activity[data-state="medium"]{border-color:#fbbf24}.single-audio-activity[data-state="waiting"] .single-audio-fill,.single-audio-activity[data-state="paused"] .single-audio-fill,.single-audio-activity[data-state="unavailable"] .single-audio-fill{opacity:.32}.single-audio-activate{min-height:27px!important;padding:3px 7px!important;font-size:8px!important}.single-zoom-quick{display:inline-flex;align-items:center;gap:4px;padding:3px;border:1px solid #405061;border-radius:7px;background:#0f1a26}.single-zoom-quick .single-zoom-value{min-width:58px;text-align:center;font-size:10px}.single-zoom-control{grid-column:1/-1;display:grid;gap:7px;padding:10px;border:1px solid #405061;border-radius:7px;background:#0c1824}.single-zoom-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.single-zoom-control small{color:#93a8b8;font-size:9px;line-height:1.4}.single-zoom-presets{display:flex;flex-wrap:wrap;gap:5px}.single-zoom-presets .discover-btn{min-height:29px;padding:4px 7px;font-size:9px}.single-image-tools{border:1px solid #354657;border-radius:6px;background:#121f2b}.single-image-tools>summary{padding:9px 11px;color:#b9ddef;font-size:11px;font-weight:850;cursor:pointer}.single-image-grid{display:grid;grid-template-columns:repeat(4,minmax(125px,1fr));gap:8px;padding:0 10px 10px}.single-image-actions{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:7px}.single-controls .discover-field{color:#aeb9c3} .single-capture-tools{display:grid;grid-template-columns:auto auto repeat(4,minmax(125px,1fr));align-items:end;gap:7px;padding:9px;border:1px solid #405061;border-radius:7px;background:#0b1722}.single-capture-tools .discover-field{display:grid;gap:4px;font-size:9px}.single-capture-tools .discover-field span{color:#9fb3c3;font-weight:800}.single-capture-status{grid-column:1/-1;display:flex;align-items:center;gap:8px;min-height:27px;padding:5px 8px;border-radius:5px;color:#a8c3d2;background:#101f2c;font-size:9px}.single-capture-status.is-recording{color:#fff;border:1px solid #fb7185;background:#451927}.single-capture-status strong{color:inherit}.single-record-button.is-recording{border-color:#fb7185!important;background:#7f1d32!important;color:#fff!important}@media(max-width:1050px){.single-capture-tools{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.single-capture-tools{grid-template-columns:1fr}.single-capture-status{grid-column:1}} .single-details{display:grid;gap:12px}.single-panel{padding:15px;border:1px solid var(--dc-line);border-radius:8px;background:var(--dc-panel)}.single-panel h2,.single-panel h3{margin:0 0 9px}.single-settings-panel{border-top:4px solid #f47321}.single-settings-head{display:flex;align-items:start;justify-content:space-between;gap:10px;margin-bottom:12px}.single-settings-head p{margin:3px 0 0;color:var(--dc-muted);font-size:10px}.single-settings-status{padding:5px 7px;border:1px solid #23744d;border-radius:4px;background:#184f38;color:#d9ffea;font-size:9px;font-weight:850;white-space:nowrap}.single-settings-grid{display:grid;gap:9px}.single-setting-section{border:1px solid var(--dc-line);border-radius:6px;background:var(--dc-panel-soft)}.single-setting-section>summary{padding:10px 11px;color:var(--dc-text);font-size:11px;font-weight:900;cursor:pointer}.single-setting-list{display:grid;gap:7px;padding:0 9px 10px}.single-setting-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:10px;min-height:42px;padding:7px 8px;border:1px solid var(--dc-line);border-radius:5px;background:var(--dc-panel)}.single-setting-row strong{display:block;font-size:11px}.single-setting-row small{display:block;margin-top:2px;color:var(--dc-muted);font-size:9px;line-height:1.35}.single-setting-row input[type="checkbox"]{width:18px;height:18px;accent-color:#f47321}.single-setting-row select{width:145px;min-height:34px}.single-settings-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}.single-facts{display:grid;grid-template-columns:1fr 1fr;gap:7px}.single-fact{padding:8px;border-radius:6px;background:var(--dc-panel-soft)}.single-fact span{display:block;color:var(--dc-muted);font-size:9px;font-weight:850;text-transform:uppercase}.single-fact strong{display:block;margin-top:3px;font-size:12px;word-break:break-word}.single-textarea{width:100%;min-height:90px;resize:vertical;padding:9px;border:1px solid var(--dc-line);border-radius:6px;color:var(--dc-text);background:var(--dc-panel-soft)}.single-links{display:grid;gap:6px}.single-links a{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dc-accent);font-size:11px}.single-similar{margin-top:20px} body.roomgrid-singlecam-active{background:radial-gradient(circle at 70% -20%,color-mix(in srgb,var(--dc-accent) 20%,transparent),transparent 38%),#080d14!important}body.roomgrid-singlecam-active .discover-header{min-height:58px;padding-block:7px;border-bottom-color:#334155;background:rgba(10,18,29,.96);color:#f8fafc}body.roomgrid-singlecam-active .discover-nav{display:flex}body.roomgrid-singlecam-active .discover-brand span span{color:#94a3b8}body.roomgrid-singlecam-active .discover-main{width:100%;max-width:none;padding:12px clamp(10px,1.2vw,24px) 28px}body.roomgrid-singlecam-active .discover-header-actions{overflow:auto}body.roomgrid-singlecam-active .discover-header-actions .discover-btn{border-color:#405061;background:#172533;color:#f8fafc} .single-workspace-badge{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 9px;border:1px solid #405061;border-radius:7px;background:#122231;color:#9bdcff;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.single-model-identity{display:flex;align-items:center;gap:10px;min-width:0;margin-right:auto}.single-model-avatar{width:42px;height:42px;border-radius:8px;object-fit:cover;background:#0b1019}.single-model-title{min-width:0}.single-model-title strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:18px}.single-model-title span{display:block;color:var(--dc-muted);font-size:10px}.single-live-dot{width:8px;height:8px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)} .single-layout{grid-template-columns:minmax(0,2.35fr) minmax(380px,.85fr)}.single-details{position:sticky;top:calc(var(--discover-header-height,108px) + 8px);max-height:calc(100vh - var(--discover-header-height,108px) - 22px);overflow:hidden;border:1px solid #405061;border-radius:10px;background:var(--dc-panel);box-shadow:0 18px 50px rgba(0,0,0,.2)}.single-cockpit-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px;padding:8px;border-bottom:1px solid var(--dc-line);background:var(--dc-panel-soft)}.single-cockpit-tab{min-height:34px;padding:6px 8px;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dc-sub);font-size:10px;font-weight:850;cursor:pointer}.single-cockpit-tab:hover{border-color:var(--dc-accent)}.single-cockpit-tab.active{border-color:var(--dc-accent);background:var(--dc-accent);color:#fff}.single-cockpit-body{max-height:calc(100vh - var(--discover-header-height,108px) - 82px);overflow:auto;padding:10px;scrollbar-gutter:stable}.single-tab-panel{display:grid;gap:10px}.single-tab-panel[hidden]{display:none!important}.single-tab-panel>.single-panel{border-radius:7px;box-shadow:none}.single-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:10px}.single-panel-head p{margin:3px 0 0;color:var(--dc-muted);font-size:10px;line-height:1.4} .single-note-status{min-height:24px;color:var(--dc-muted);font-size:10px;font-weight:750}.single-note-status.saved{color:var(--dc-good)}.single-textarea.single-notebook{min-height:190px;line-height:1.55}.single-tag-editor{display:grid;gap:8px}.single-tag-entry{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}.single-own-tags{display:flex;flex-wrap:wrap;gap:6px;min-height:30px}.single-own-tag{display:inline-flex;align-items:center;gap:5px;padding:4px 7px;border:1px solid color-mix(in srgb,var(--dc-accent) 44%,var(--dc-line));border-radius:999px;background:var(--dc-accent-soft);color:var(--dc-accent);font-size:10px;font-weight:800}.single-own-tag button{border:0;background:transparent;color:inherit;cursor:pointer;font-weight:900}.single-group-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}.single-group-option{display:flex;align-items:center;gap:7px;padding:8px;border:1px solid var(--dc-line);border-radius:6px;background:var(--dc-panel-soft);font-size:10px;font-weight:750}.single-group-option input{accent-color:var(--dc-accent)}.single-personal-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.single-rating{display:flex;gap:2px}.single-rating button{border:0;background:transparent;color:#64748b;font-size:21px;cursor:pointer}.single-rating button.on{color:#f59e0b} .single-stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.single-stat-card{min-width:0;padding:9px;border:1px solid var(--dc-line);border-radius:7px;background:var(--dc-panel-soft)}.single-stat-card span{display:block;color:var(--dc-muted);font-size:9px;font-weight:850;text-transform:uppercase}.single-stat-card strong{display:block;margin-top:4px;overflow:hidden;text-overflow:ellipsis;font-size:14px}.single-week-bars{display:grid;gap:6px}.single-week-bar{display:grid;grid-template-columns:28px minmax(0,1fr) 34px;align-items:center;gap:7px;font-size:9px}.single-week-bar i{display:block;height:8px;border-radius:999px;background:linear-gradient(90deg,var(--dc-accent) var(--bar),var(--dc-panel-soft) var(--bar));box-shadow:inset 0 0 0 1px var(--dc-line)}.single-week-bar strong{text-align:right}.single-best-times{display:flex;flex-wrap:wrap;gap:6px}.single-best-time{padding:6px 8px;border:1px solid var(--dc-line);border-radius:999px;background:var(--dc-accent-soft);color:var(--dc-accent);font-size:9px;font-weight:850}.single-heatmap{display:grid;grid-template-columns:repeat(24,minmax(5px,1fr));gap:2px}.single-heat-cell{aspect-ratio:1;border-radius:2px;background:color-mix(in srgb,var(--dc-accent) var(--heat),var(--dc-panel-soft));min-width:4px}.single-heat-caption{display:flex;justify-content:space-between;margin-top:5px;color:var(--dc-muted);font-size:9px} .single-media-controls{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin:0 0 10px}.single-media-controls select{min-width:155px}.single-media-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.single-media-card{position:relative;overflow:hidden;min-width:0;aspect-ratio:16/9;border:1px solid var(--dc-line);border-radius:7px;background:#080d14}.single-media-card.is-cover{border-color:#f59e0b;box-shadow:0 0 0 1px #f59e0b inset}.single-media-card.is-background{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf8 inset}.single-media-card img,.single-media-card video{width:100%;height:100%;display:block;object-fit:cover;background:#05080d}.single-media-card video{object-fit:contain}.single-media-kind{position:absolute;top:5px;left:5px;z-index:2;padding:3px 6px;border:1px solid rgba(255,255,255,.24);border-radius:999px;background:rgba(2,6,23,.82);color:#fff;font-size:8px;font-weight:900;letter-spacing:.05em}.single-media-caption{position:absolute;left:5px;right:5px;bottom:5px;display:grid;gap:4px;padding:5px 6px;border-radius:4px;background:rgba(2,6,23,.86);color:#fff;font-size:9px;font-weight:800}.single-media-card.is-video .single-media-caption{top:28px;bottom:auto;pointer-events:none}.single-media-meta{color:#b9c9dc;font-size:8px;font-weight:650}.single-media-actions{display:flex;flex-wrap:wrap;gap:4px}.single-media-actions button{min-height:25px;padding:3px 6px;border:1px solid rgba(255,255,255,.25);border-radius:4px;background:#183044;color:#fff;font:800 8px/1.2 inherit;cursor:pointer}.single-media-actions button.is-cover{border-color:#f59e0b;background:#70450f}.single-media-actions button.is-background{border-color:#38bdf8;background:#075985}.single-media-actions button.danger{border-color:#8b3d48;background:#57262c}.single-media-empty{grid-column:1/-1;padding:22px 12px;border:1px dashed var(--dc-line);border-radius:7px;color:var(--dc-muted);text-align:center;font-size:11px}.single-directory-box{display:grid;gap:8px;padding:10px;border:1px solid var(--dc-line);border-radius:7px;background:rgba(0,0,0,.12)}.single-directory-box p{margin:0;color:var(--dc-text);font-size:10px;line-height:1.45}.single-directory-actions{display:flex;flex-wrap:wrap;gap:7px}.single-directory-status{display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:var(--dc-muted);font-size:10px}.single-directory-status strong{color:var(--dc-text)}.single-background-controls{display:grid;gap:8px;padding:9px;border:1px solid var(--dc-line);border-radius:6px;background:var(--dc-panel)}.single-background-controls .single-background-grid{display:grid;grid-template-columns:repeat(3,minmax(120px,1fr));gap:7px}.single-background-field{display:grid;gap:4px;color:var(--dc-muted);font-size:9px;font-weight:850}.single-background-field select,.single-background-field input[type="color"]{width:100%;min-height:34px}.single-background-field input[type="color"]{padding:3px}.single-background-range{display:grid;grid-template-columns:minmax(90px,1fr) 44px;align-items:center;gap:6px}.single-background-range output{color:var(--dc-text);font-size:9px;text-align:right}.single-background-preview{min-height:86px;border:1px solid var(--dc-line);border-radius:6px;background:linear-gradient(135deg,var(--dc-panel-soft),var(--dc-panel));background-size:cover;background-position:center}.single-background-status{color:var(--dc-muted);font-size:9px;line-height:1.4}.single-settings-panel{border-top-width:1px}.single-settings-grid{gap:7px} .single-media-card{cursor:zoom-in}.single-media-card:focus-visible{outline:3px solid var(--dc-accent);outline-offset:2px}.single-media-open{position:absolute;top:5px;right:5px;z-index:3;min-height:25px;padding:3px 7px;border:1px solid rgba(255,255,255,.3);border-radius:5px;background:rgba(2,6,23,.82);color:#fff;font:850 8px/1 inherit;cursor:pointer}.single-gallery-viewer[hidden]{display:none!important}.single-gallery-viewer{position:fixed;z-index:2300;inset:0;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:10px;padding:12px;background:rgba(2,6,23,.94);backdrop-filter:blur(14px);color:#f8fafc}.single-gallery-head{display:flex;align-items:center;gap:10px;min-width:0;padding:4px 6px}.single-gallery-title{min-width:0;margin-right:auto}.single-gallery-title strong,.single-gallery-title span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.single-gallery-title strong{font-size:15px}.single-gallery-title span{margin-top:2px;color:#a8bacb;font-size:9px}.single-gallery-counter{min-width:72px;text-align:center;color:#d8e8f2;font-size:11px;font-weight:900}.single-gallery-canvas{position:relative;display:grid;place-items:center;min-height:0;overflow:hidden;border:1px solid #405061;border-radius:10px;background:radial-gradient(circle at 50% 45%,#182638,#05070b 72%);touch-action:none;cursor:default}.single-gallery-canvas.is-zoomed{cursor:grab}.single-gallery-canvas.is-dragging{cursor:grabbing}.single-gallery-media{display:block;max-width:calc(100% - 24px);max-height:calc(100% - 24px);object-fit:contain;transform-origin:center;will-change:transform;user-select:none;-webkit-user-drag:none}.single-gallery-canvas video.single-gallery-media{width:min(100%,1500px);height:100%;transform:none!important}.single-gallery-arrow{position:absolute;z-index:3;top:50%;translate:0 -50%;width:48px;height:64px;border:1px solid rgba(255,255,255,.24);border-radius:9px;background:rgba(2,6,23,.68);color:#fff;font-size:26px;font-weight:900;cursor:pointer}.single-gallery-arrow:hover{background:rgba(37,99,235,.82)}.single-gallery-arrow.previous{left:12px}.single-gallery-arrow.next{right:12px}.single-gallery-footer{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px}.single-gallery-tools{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.single-gallery-tools button{min-height:34px;padding:6px 10px;border:1px solid #405061;border-radius:6px;background:#162638;color:#fff;font-size:10px;font-weight:850;cursor:pointer}.single-gallery-tools button.primary{border-color:#f59e0b;background:#70450f}.single-gallery-tools button:disabled{opacity:.48;cursor:default}.single-gallery-zoom{min-width:54px;text-align:center;font-size:10px;font-weight:900}.single-gallery-strip{display:flex;gap:6px;min-width:0;overflow:auto;padding:3px;scrollbar-gutter:stable}.single-gallery-thumb{position:relative;flex:0 0 76px;height:48px;overflow:hidden;padding:0;border:2px solid transparent;border-radius:6px;background:#0b1420;color:#fff;cursor:pointer}.single-gallery-thumb.active{border-color:#f59e0b}.single-gallery-thumb img{width:100%;height:100%;display:block;object-fit:cover}.single-gallery-thumb span{display:grid;place-items:center;width:100%;height:100%;font-size:9px;font-weight:900}.single-gallery-cover-badge{position:absolute;top:3px;right:3px;padding:2px 4px;border-radius:999px;background:#f59e0b;color:#111827;font-size:7px;font-weight:950}.single-gallery-hint{color:#8ea7b7;font-size:8px;white-space:nowrap}.single-gallery-open-lock{overflow:hidden!important}@media(max-width:720px){.single-gallery-viewer{padding:7px}.single-gallery-footer{grid-template-columns:1fr}.single-gallery-tools{justify-content:center}.single-gallery-strip{order:3}.single-gallery-hint{display:none}.single-gallery-arrow{width:40px;height:54px}.single-gallery-thumb{flex-basis:62px;height:42px}} .single-gallery-media-host{display:grid;place-items:center;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden} .single-settings-panel .single-background-controls .single-background-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,140px),1fr))}.single-settings-panel .single-background-field{min-width:0}.single-settings-panel .single-background-range{grid-template-columns:minmax(0,1fr) 44px}.single-settings-panel .single-background-range input{min-width:0;width:100%} .single-settings-panel,.single-settings-head>div,.single-setting-section,.single-setting-list,.single-setting-row>*{min-width:0;max-width:100%;box-sizing:border-box}.single-settings-panel{container-type:inline-size;overflow-wrap:anywhere}.single-settings-head{flex-wrap:wrap}.single-settings-panel input,.single-settings-panel select,.single-settings-panel button{max-width:100%;box-sizing:border-box}.single-settings-panel .single-setting-row{grid-template-columns:minmax(0,1fr) minmax(0,145px)}.single-settings-panel .single-setting-row select{width:100%}@container(max-width:430px){.single-settings-panel .single-setting-row{grid-template-columns:minmax(0,1fr)}.single-settings-panel .single-setting-row select{width:100%}} .single-tag-block{display:grid;gap:7px;margin:10px 0;padding:9px;border:1px solid var(--dc-line);border-radius:7px;background:var(--dc-panel-soft)}.single-tag-block>strong{font-size:10px;text-transform:uppercase;color:var(--dc-muted)}.single-chat-panel{padding:10px}.single-chat-actions{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:8px}.single-chat-status{color:var(--dc-muted);font-size:9px;font-weight:750}.single-chat-frame{display:block;width:100%;height:min(68vh,760px);min-height:480px;border:1px solid var(--dc-line);border-radius:7px;background:#0b1420} body.discover-cinema .discover-header { display:flex; } body.discover-cinema .discover-main { width:100%; padding:8px; } body.discover-cinema .single-layout { grid-template-columns:1fr; } body.discover-cinema .single-details,.discover-cinema .single-similar { display:none; } body.discover-cinema .single-stage { min-height:calc(100vh - var(--discover-header-height,108px) - 28px); } .discover-toast-host { position:fixed; z-index:1000; top:78px; right:16px; display:grid; gap:8px; pointer-events:none; } .discover-toast { max-width:360px; padding:10px 12px; border-radius:10px; color:#fff; background:rgba(15,23,42,.94); box-shadow:var(--dc-shadow); font-size:12px; } @media(max-width:1200px){ .discover-header{flex-wrap:wrap} } @media(max-width:1100px){.discover-controls{grid-template-columns:repeat(3,1fr)}.discover-controls .search{grid-column:1/-1}.discover-filter-grid,.dashboard-display-grid{grid-template-columns:repeat(3,1fr)}.dashboard-stats{grid-template-columns:repeat(2,1fr)}.dashboard-section.size-small,.dashboard-section.size-wide{grid-column:span 12}.tag-layout,.single-layout,.single-page[data-layout="compact"] .single-layout,.single-page[data-sidebar="left"] .single-layout,.single-page[data-cockpit-size="compact"] .single-layout,.single-page[data-cockpit-size="wide"] .single-layout{grid-template-columns:1fr}.single-page[data-sidebar="left"] .single-details{order:initial}.single-details{position:relative;top:auto;max-height:none}.single-cockpit-body{max-height:none}.single-image-grid{grid-template-columns:repeat(2,minmax(125px,1fr))}} @media(max-width:720px){.discover-header{align-items:flex-start;padding:9px 11px}.discover-nav{display:flex;flex:1 0 100%;width:100%;max-width:100%;overflow-x:auto}.discover-nav button{min-width:max-content;padding-inline:9px}.discover-header-actions{margin-left:0;width:100%;min-width:0;overflow:auto}.discover-main{padding:15px 10px}.discover-hero{display:block}.discover-summary{justify-content:flex-start;margin-top:12px}.discover-controls,.discover-filter-grid,.discover-saved,.dashboard-display-grid{grid-template-columns:1fr}.discover-grid,.dashboard-live-grid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr))!important}.dashboard-live-head{display:block}.dashboard-live-head-actions{justify-content:flex-start;margin-top:10px}.dashboard-stats{grid-template-columns:1fr 1fr}.tag-artwork-gallery{grid-template-columns:repeat(2,minmax(90px,1fr))}.single-topbar{top:calc(var(--discover-header-height,132px) + 6px)}.single-stage,.single-page[data-stage-size="large"] .single-stage,.single-page[data-stage-size="maximum"] .single-stage{min-height:230px;aspect-ratio:16/9}.single-facts,.single-image-grid{grid-template-columns:1fr}.single-setting-row{grid-template-columns:1fr}.single-setting-row select{width:100%}.single-chat-frame{min-height:560px}.single-cockpit-tabs{grid-template-columns:repeat(2,minmax(0,1fr))}} @media(max-width:720px){ .dashboard-live-center{padding:10px} .dashboard-live-head{display:grid;grid-template-columns:1.65fr .8fr 1fr;gap:6px;align-items:center;margin-bottom:8px} .dashboard-live-head h2{grid-column:1/3;font-size:20px} .dashboard-live-head-actions{display:contents} .dashboard-live-head-actions>.discover-btn{min-width:0;min-height:36px;padding:6px;font-size:10px} .dashboard-live-head-actions>.discover-btn.primary{grid-column:3;grid-row:1;line-height:1.2} .dashboard-live-center>.discover-active-filters{gap:5px;padding:7px} .dashboard-live-center>.discover-active-filters .discover-active-filter-title{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap} .dashboard-live-center>.discover-active-filters .discover-active-filter{gap:4px;padding:4px 6px;font-size:10px} .dashboard-live-center>.discover-active-filters .discover-active-filter-clear{padding:4px 6px;font-size:10px} .dashboard-live-status{gap:5px;margin-bottom:8px;font-size:10px} } @media(prefers-reduced-motion:reduce){ .discover-card,.discover-btn{transition:none!important}.discover-card:hover{transform:none} } .tag-layout{display:flex;flex-direction:column;gap:12px}.tag-browser-column,.tag-results-column{display:contents}.tag-layout [data-tag-browser]{order:0;width:100%;box-sizing:border-box}.tag-layout [data-tag-browser]>.tag-cloud{max-height:180px;overflow:auto;padding:4px}.tag-layout .tag-selection-panel{order:1;width:100%;box-sizing:border-box}.tag-results-column>.tag-section:not(.tag-selection-panel){order:2;width:100%;box-sizing:border-box}.tag-layout .tag-extra{order:3;width:100%;box-sizing:border-box}.tag-pill .tag-icon{min-height:30px;min-width:30px}.tag-selection-panel .tag-toolbar{gap:6px}.tag-help{padding:12px;border:1px solid var(--dc-line);border-radius:8px;background:var(--dc-panel);margin-bottom:10px}.tag-help p{margin:5px 0;line-height:1.5}.tag-result-summary{position:sticky;top:var(--dc-header-height,90px);z-index:5;display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:var(--dc-panel);border:1px solid var(--dc-line);border-radius:8px;padding:10px;margin-bottom:10px}.tag-result-summary button{min-height:34px}.tag-results-column .tag-section{scroll-margin-top:110px} .discover-feedback{display:flex;gap:5px;flex-wrap:wrap;padding:6px 8px;border-top:1px solid var(--dc-line)}.discover-feedback .discover-btn{min-height:30px;font-size:10px;padding:4px 7px}.discover-feedback-history{margin:10px 0}.discover-feedback-row{display:flex;justify-content:space-between;gap:10px;align-items:center;padding:6px 0} .single-page[data-sidebar-collapsed="true"] .single-layout{grid-template-columns:minmax(0,1fr)!important}.single-page[data-sidebar-collapsed="true"] .single-details{display:none!important}.single-page[data-tools-collapsed="true"] .single-controls{display:none!important}.single-page[data-sidebar-collapsed="true"][data-tools-collapsed="true"][data-stage-size="maximum"] .single-stage{min-height:0;aspect-ratio:auto;height:max(120px,calc(100dvh - var(--discover-header-height,108px) - var(--single-toolbar-height,64px) - 72px))} .single-page[data-stage-size="custom"] .single-player-panel{width:min(100%,max(280px,var(--single-stream-width,100%)));margin-inline:auto;align-self:start;min-width:0}.single-page[data-stage-size="custom"] .single-stage{height:var(--single-stream-height,60vh);min-height:0;aspect-ratio:auto;width:100%}.single-page .single-stage:fullscreen{width:100vw!important;height:100vh!important;min-height:0!important;aspect-ratio:auto!important}.single-page [data-single-stream-size] summary{cursor:pointer;font-weight:850;min-height:30px}.single-page [data-single-stream-size] output{font-size:11px} .single-media-grid{grid-template-columns:1fr;gap:12px}.single-media-card{aspect-ratio:auto;display:grid;grid-template-rows:auto auto;padding-top:0}.single-media-card>img,.single-media-card>video{width:100%;height:auto;aspect-ratio:16/9;max-height:300px;object-fit:contain}.single-media-caption,.single-media-card.is-video .single-media-caption{position:static;pointer-events:auto;padding:9px;font-size:11px;border-radius:0;overflow-wrap:anywhere}.single-media-meta{font-size:10px}.single-media-actions button{min-height:32px;font-size:10px}.single-media-actions{margin-top:4px}.single-media-open{position:static!important;min-height:32px!important;margin:5px!important;justify-self:end}.single-media-kind{top:7px;left:7px}.single-media-card details summary{cursor:pointer;min-height:30px;display:flex;align-items:center}.single-media-card details[open]{padding-top:4px} .tag-result-summary{top:calc(var(--discover-header-height,108px) + 4px)}.tag-results-column .tag-section{scroll-margin-top:calc(var(--discover-header-height,108px) + 72px)} .tag-disclosure{margin:8px 0;padding:8px 10px;border:1px solid var(--dc-line);border-radius:9px;background:var(--dc-panel-soft)}.tag-disclosure>summary{cursor:pointer;min-height:30px;line-height:30px;font-weight:600;font-size:12px}.tag-disclosure>summary:focus-visible{outline:2px solid var(--dc-accent);outline-offset:3px}.tag-disclosure[open]>summary{margin-bottom:10px}.tag-disclosure>p{font-size:12px;color:var(--dc-muted);line-height:1.5} @media(min-width:1800px){.single-media-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:1100px){.tag-layout [data-tag-browser]>.tag-cloud{max-height:180px}.tag-result-summary{top:0;position:relative}} `) })); document.documentElement.style.setProperty('--camclient-tag-art-sprite', `url("${CAMCLIENT_TAG_EMOJI_SPRITE_DATA_URL}")`); const toastHost = $('div', { class: 'discover-toast-host' }); const activeToasts = new Map(); const toast = (message, duration = 2600) => { const text = String(message || '').trim(); if (!text) return; const previous = activeToasts.get(text); if (previous?.element?.isConnected) { clearTimeout(previous.timer); previous.timer = setTimeout(() => { try { previous.element.remove(); } catch (_) {} activeToasts.delete(text); }, duration); return; } const element = $('div', { class: 'discover-toast' }, text); toastHost.appendChild(element); const entry = { element, timer: 0 }; entry.timer = setTimeout(() => { try { element.remove(); } catch (_) {} activeToasts.delete(text); }, duration); activeToasts.set(text, entry); }; const discoverIdentity = roomGridBrandIdentity(store.state.settings); const brand = $('div', { class: 'discover-brand' }, [ $('span', { class: 'discover-logo' }, roomGridBrandMark(store.state.settings)), $('span', {}, [$('strong', { dataset: { roomgridBrandName: '1' } }, discoverIdentity.name), $('span', { dataset: { roomgridBrandVersion: '1' } }, discoverIdentity.showVersion ? `v${META.version}` : '')]), ]); const dashboardNav = $('button', { type: 'button' }, tx('主页', 'Home', 'Start')); const multiCamNav = $('button', { type: 'button', dataset: { roomgridNavTarget: 'multicam' }, onclick: () => multiCamBtn.click() }, 'MultiCam'); const discoverNav = $('button', { type: 'button' }, tx('发现', 'Discover', 'Entdecken')); const tagsNav = $('button', { type: 'button' }, tx('标签', 'Tags', 'Tag-Explorer')); const statsNav = $('button', { type: 'button', dataset: { roomgridNavTarget: 'stats' } }, tx('统计中心', 'Statistics center', 'Statistikzentrum')); const libraryNav = $('button', { type: 'button', dataset: { roomgridNavTarget: 'library' } }, tx('模型库', 'Model library', 'Modellbibliothek')); const followingNav = $('button', { type: 'button', dataset: { roomgridNavTarget: 'following' } }, (I18N[LANG]?.followingUi || I18N.en.followingUi).nav); const userNav = $('button', { type: 'button', dataset: { roomgridNavTarget: 'user' } }, tx('账户与使用', 'Account & usage', 'Konto & Nutzung')); const settingsNav = $('button', { type: 'button', class: 'discover-settings-nav', onclick: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'homepage') }, tx('设置', 'Settings', 'Einstellungen')); const nav = $('nav', { class: 'discover-nav', ariaLabel: tx('页面', 'Pages', 'Seiten') }); const helpNav = $('button', { type: 'button', dataset: { camclientHelpButton: '1' }, onclick: () => openCamclientHelp(navMore.querySelector('summary'), view => view === 'multicam' ? multiCamBtn.click() : setView(view)) }, tx('帮助与版本', 'Help & version', 'Hilfe & Version')); const navMore = createSuiteMore([tagsNav, statsNav, userNav, settingsNav, helpNav]); const syncNavigation = () => nav.replaceChildren(dashboardNav, multiCamNav, discoverNav, followingNav, libraryNav, navMore); syncNavigation(); const multiCamBtn = $('button', { class: 'discover-btn primary', type: 'button', onclick: () => { previewScheduler.destroy(); const url = new URL(location.href); url.searchParams.delete('multicam_discover'); url.searchParams.delete('suite_view'); url.searchParams.set('multicam_mode', '1'); url.hash = ''; location.href = url.toString(); } }, 'MultiCam'); const refreshBtn = $('button', { class: 'discover-btn', type: 'button' }, tx('刷新', 'Refresh', 'Aktualisieren')); const commandBtn = $('button', { class: 'discover-btn', type: 'button', title: 'Ctrl+K', onclick: () => commandPalette?.open() }, tx('搜索…', 'Search…', 'Suchen…')); const headerActions = $('div', { class: 'discover-header-actions' }, [commandBtn, refreshBtn, createWatchOnlyButton('discover-btn')]); const header = $('header', { class: 'discover-header' }, [brand, nav, headerActions]); const syncHeaderHeight = () => document.documentElement.style.setProperty('--discover-header-height', `${Math.max(58, Math.ceil(header.getBoundingClientRect().height || 0))}px`); const headerResizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(syncHeaderHeight) : null; headerResizeObserver?.observe(header); const main = $('main', { class: 'discover-main' }); const shell = $('div', { class: 'discover-shell' }, [header, main]); document.body.append(shell, toastHost); requestAnimationFrame(syncHeaderHeight); notificationCenter.mount(header, headerActions); const currentTheme = () => store.state.settings.theme === 'dark'; const applyTheme = () => { const dark = currentTheme(); document.body.classList.toggle('discover-dark', dark); const settings = applySuiteAppearance(store); const accent = cleanColor(settings.accentColor, dark ? '#60a5fa' : '#2563eb'); const [r, g, b] = hexRgb(accent); document.documentElement.style.setProperty('--dc-accent', accent); document.documentElement.style.setProperty('--dc-accent-soft', `rgba(${r},${g},${b},${dark ? '.22' : '.13'})`); syncNavigation(); }; applyTheme(); const previewScheduler = createDiscoverPreviewScheduler(() => ({ enabled: !watchOnlyMode && !!state.previewEnabled, fps: Number(state.previewFps) || 0, mode: state.previewMode === 'bounded' ? 'bounded' : 'simultaneous', concurrency: Number(state.previewConcurrency), hoverEnabled: !watchOnlyMode && state.hoverLivePreview !== false && matchMedia('(hover:hover) and (pointer:fine)').matches, hoverDelay: clampInt(state.hoverPreviewDelay, 0, 1000, 250), hoverScale: clampInt(state.hoverPreviewScale, 100, 600, 112), })); const watchOnlyChanged = () => { previewScheduler.refresh(); if (watchOnlyMode) { clearTimeout(catalogRefreshTimer); clearTimeout(catalogPrefetchRetryTimer); catalogRefreshTimer = 0; catalogPrefetchRetryTimer = 0; } else if (state.view !== 'single') { if (!catalog.length) void refreshCompleteCatalog({ automatic: true }); else { continueCatalogPrefetch(); scheduleCatalogRefresh(); } } }; window.addEventListener('camclient:watch-only', watchOnlyChanged); commandPalette = createCommandPalette(store, () => [ { icon: '⌂', title: tx('个人主页', 'Personal dashboard', 'Persönliche Startseite'), run: () => setView('dashboard') }, { icon: '⌁', title: tx('发现', 'Discover', 'Entdecken'), run: () => setView('discover') }, { icon: '#', title: tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer'), run: () => setView('tags') }, { icon: '▥', title: tx('统计中心', 'Statistics center', 'Statistikzentrum'), run: () => setView('stats') }, { icon: '★', title: tx('模型库', 'Model library', 'Modellbibliothek'), run: () => setView('library') }, { icon: '◎', title: tx('我的用户', 'My user', 'Mein Nutzer'), run: () => setView('user') }, { icon: '⚙', title: tx('设置', 'Settings', 'Einstellungen'), run: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'homepage') }, { icon: '▦', title: 'MultiCam', run: () => multiCamBtn.click() }, { icon: '↻', title: tx('刷新公开目录', 'Refresh public catalog', 'Öffentlichen Katalog aktualisieren'), run: refreshCompleteCatalog }, ], { onModel: id => openSingle(id, selectedModels().map(model => model.id)) }); function groupLabel(group) { if (!group) return ''; if (group.id === FAVORITE_GROUP_ID) return tx('收藏', 'Favorites', 'Favoriten'); if (group.id === DEFAULT_GROUP_ID) return tx('全部', 'All', 'Alle'); if (group.id === ONLINE_GROUP_ID) return tx('在线', 'Online', 'Online'); if (group.id === LIBRARY_GROUP_ID) return tx('资料库', 'Library', 'Bibliothek'); return group.name || group.id; } function ensureLocalIndexes() { const now = Date.now(); if (now >= nextRoomExpiry) localMetadataRevision += 1; if (roomsIndexRevision !== localMetadataRevision) { roomsById = new Map(store.state.rooms.map(room => [room.id, room])); roomsIndexRevision = localMetadataRevision; nextRoomExpiry = Infinity; for (const room of roomsById.values()) for (const field of ['hiddenUntil', 'pausedUntil']) { const expires = Number(room[field]) || 0; if (expires > now) nextRoomExpiry = Math.min(nextRoomExpiry, expires); } } const revision = analytics.getDataRevision?.() ?? 0; if (revision !== statsRevision) { let latestWatchedAt = 0; latestWatchedId = ''; statsById = new Map(); for (const item of analytics.getModelsSync()) { statsById.set(item.id, item); if (item.lastWatchedAt > latestWatchedAt) { latestWatchedAt = item.lastWatchedAt; latestWatchedId = item.id; } } statsRevision = revision; } } function dataCacheSignature() { ensureLocalIndexes(); return `${catalogDataRevision}:${localMetadataRevision}:${statsRevision}:${observationsRevision}:${startOfToday()}`; } function roomFor(id) { // Do not rebuild the analytics map for every card/model lookup. if (roomsIndexRevision !== localMetadataRevision || Date.now() >= nextRoomExpiry) ensureLocalIndexes(); return roomsById.get(id) || null; } function isSaved(id) { return !!roomFor(id); } function isFavorite(id) { const room = roomFor(id); return !!room && roomInGroup(room, FAVORITE_GROUP_ID); } function isFollowing(model) { if (model?.isFollowing) return true; const room = roomFor(model?.id); const followedId = store.state.settings.followedGroupId; return !!room && !!followedId && roomInGroup(room, followedId); } function localModel(id) { const room = roomFor(id); const stat = statsById.get(id); const known = catalogById.get(id); if (known) return known; return { id, username: id, currentShow: room?.lastStatus === 'online' ? 'public' : (room?.lastStatus || stat?.lastStatus || 'unknown'), subject: room?.notes || '', tags: [...new Set([...(stat?.tags || []), ...(room?.localTags || [])])], gender: 'unknown', age: 0, location: '', country: '', viewers: Number(room?.viewerCount || stat?.viewerCount) || 0, followers: 0, secondsOnline: 0, isNew: false, isGaming: false, isFollowing: isFollowing({ id }), isHd: false, imageUrl: discoverFallbackImage(id), source: 'local', }; } function parseTerms(value) { return [...new Set(String(value || '').split(/[,;|\n]+/u).map(term => term.trim().toLocaleLowerCase()).filter(Boolean))].slice(0, 20); } function startOfToday() { const date = new Date(); return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); } function compileFilter(definition) { return { query: String(definition.query || '').trim().toLocaleLowerCase(), all: parseTerms(definition.tagsAll), any: parseTerms(definition.tagsAny), not: parseTerms(definition.tagsNot), places: parseTerms(definition.place), today: startOfToday() }; } function cachedModelData(model) { const signature = `${catalogDataRevision}:${localMetadataRevision}:${statsRevision}`; const cached = modelDataCache.get(model); if (cached?.signature === signature) return cached; const room = roomFor(model.id); const stat = statsById.get(model.id); const tags = [...new Set([...(model.tags || []), ...(stat?.tags || []), ...(room?.localTags || [])].map(tag => String(tag).toLocaleLowerCase()))]; const value = { signature, tags, searchable: [model.id, model.subject, model.location, model.country, room?.notes, ...tags].join(' ').toLocaleLowerCase(), placeText: [model.location, model.country, model.subject, ...tags].join(' ').toLocaleLowerCase() }; modelDataCache.set(model, value); return value; } function modelMatches(model, definition = state, compiled = compileFilter(definition)) { const room = roomFor(model.id); if (room && (room.archived || room.suggestionHidden || Number(room.hiddenUntil || 0) > Date.now() || Number(room.pausedUntil || 0) > Date.now())) return false; const stat = statsById.get(model.id); const { tags, searchable, placeText } = cachedModelData(model); const { query, all, any, not, places, today } = compiled; if (query && !searchable.includes(query)) return false; if (definition.quick === 'new' && !model.isNew) return false; if (definition.quick === 'new_since' && !(Number(state.seenModels?.[model.id]?.firstSeenAt) > previousCatalogVisitAt)) return false; if (definition.quick === 'new_to_me' && Number(stat?.totalMs || 0) > 0) return false; if (definition.quick === 'returning' && Number(stat?.sessions || 0) < 2) return false; if (definition.quick === 'stable') { const observed = state.seenModels?.[model.id]; if (!(model.currentShow === 'public' && Number(observed?.lastSeenAt || 0) - Number(observed?.firstSeenAt || 0) >= 300000 && room?.lastStatus !== 'error' && !room?.errorMsg)) return false; } if (definition.quick === 'hd' && !model.isHd) return false; if (definition.quick === 'gaming' && !model.isGaming) return false; if (definition.quick === 'watchlist' && !state.temporaryWatchlist.includes(model.id)) return false; if (definition.quick === 'following' && !isFollowing(model)) return false; if (definition.quick === 'saved' && !room) return false; if (definition.genders?.length && !definition.genders.includes(model.gender)) return false; const hasTag = needle => tags.some(tag => tag === needle || tag.includes(needle)); if (all.some(term => !hasTag(term))) return false; if (any.length && !any.some(hasTag)) return false; if (not.some(hasTag)) return false; if (places.length && !places.some(place => placeText.includes(place))) return false; if (Number(definition.minAge) > 0 && (!model.age || model.age < Number(definition.minAge))) return false; if (Number(definition.maxAge) > 0 && (!model.age || model.age > Number(definition.maxAge))) return false; if (Number(definition.minViewers) > 0 && model.viewers < Number(definition.minViewers)) return false; if (Number(definition.maxViewers) > 0 && model.viewers > Number(definition.maxViewers)) return false; if (Number(definition.minOnlineMinutes) > 0 && model.secondsOnline < Number(definition.minOnlineMinutes) * 60) return false; if (Number(definition.maxOnlineMinutes) > 0 && model.secondsOnline > Number(definition.maxOnlineMinutes) * 60) return false; if (definition.localMode === 'saved' && !room) return false; if (definition.localMode === 'favorites' && !isFavorite(model.id)) return false; if (definition.localMode === 'following' && !isFollowing(model)) return false; if (definition.localMode === 'known' && !(Number(stat?.totalMs) > 0)) return false; if (definition.localMode === 'never' && Number(stat?.totalMs || 0) > 0) return false; if (definition.groupId && (!room || !roomInGroup(room, definition.groupId))) return false; if (definition.hideVisitedToday && Number(stat?.lastWatchedAt || 0) >= today) return false; if (Number(definition.minWatchMinutes) > 0 && Number(stat?.totalMs || 0) < Number(definition.minWatchMinutes) * 60000) return false; if (Number(definition.minSessions) > 0 && Number(stat?.sessions || 0) < Number(definition.minSessions)) return false; return true; } function compareValue(model, field) { const stat = statsById.get(model.id); if (field === 'viewers') return Number(model.viewers) || 0; if (field === 'online') return Number(model.secondsOnline) || 0; if (field === 'followers') return Number(model.followers) || 0; if (field === 'new') return model.isNew ? 1 : 0; if (field === 'gaming') return model.isGaming ? 1 : 0; if (field === 'watch') return Number(stat?.totalMs) || 0; if (field === 'sessions') return Number(stat?.sessions) || 0; if (field === 'last') return Number(stat?.lastWatchedAt) || 0; return 0; } function compareCriterion(left, right, criterion) { if (criterion === 'random') { const score = id => [...`${id}:${state.randomSeed}`].reduce((sum, char) => ((sum * 33) ^ char.charCodeAt(0)) >>> 0, 5381); return score(left.id) - score(right.id); } const [field, direction = 'asc'] = String(criterion || 'name_asc').split('_'); const multiplier = direction === 'desc' ? -1 : 1; if (field === 'name') return nameComparer.compare(left.id, right.id) * multiplier; return (compareValue(left, field) - compareValue(right, field)) * multiplier; } function selectedModels(definition = state, source = catalog) { const signature = `${dataCacheSignature()}:${JSON.stringify(discoverCleanDefinition(definition))}:${state.randomSeed}:${state.temporaryWatchlist.join(',')}:${previousCatalogVisitAt}`; const cached = source === catalog ? selectionCache.get(signature) : null; if (cached) return cached; const unique = [...new Map(source.map(model => [model.id, model])).values()]; const compiled = compileFilter(definition); const result = unique.filter(model => modelMatches(model, definition, compiled)).sort((left, right) => compareCriterion(left, right, definition.sortPrimary) || compareCriterion(left, right, definition.sortSecondary) || left.id.localeCompare(right.id)); if (source === catalog) { selectionCache.set(signature, result); while (selectionCache.size > 8) selectionCache.delete(selectionCache.keys().next().value); } return result; } function formatCount(value) { try { return new Intl.NumberFormat().format(Number(value) || 0); } catch (_) { return String(value || 0); } } function formatDuration(seconds) { const minutes = Math.max(0, Math.floor((Number(seconds) || 0) / 60)); return minutes >= 60 ? `${Math.floor(minutes / 60)} h ${minutes % 60} min` : `${minutes} min`; } function formatWatch(ms) { const minutes = Math.round((Number(ms) || 0) / 60000); return minutes >= 60 ? `${Math.floor(minutes / 60)} h ${minutes % 60} min` : `${minutes} min`; } function ensureSaved(id) { if (!isSaved(id)) store.addRoom(id); return roomFor(id); } function createModelCard(model, compact = false, contextIds = null) { const live = model.currentShow === 'public'; const cardSettings = store.state.settings || {}; const showCountry = cardSettings.normalHomeShowCountry !== false; const showLocation = cardSettings.normalHomeShowLocation !== false; const showViewers = cardSettings.normalHomeShowViewers !== false; const showAge = cardSettings.normalHomeShowAge !== false; const showStatus = cardSettings.normalHomeShowStatus !== false; const showTags = cardSettings.normalHomeShowTags !== false; const showSubject = cardSettings.normalHomeShowSubject !== false; const showOnlineTime = cardSettings.normalHomeShowOnlineTime !== false; const showLocalStats = cardSettings.normalHomeShowLocalStats !== false; const showAttributeBadges = cardSettings.normalHomeShowBadges !== false; const preview = $('div', { class: 'discover-preview' }); const img = $('img', { class: 'discover-preview-image', src: model.imageUrl || discoverFallbackImage(model.id), alt: tx(`${model.id} 实时预览`, `Live preview of ${model.id}`, `Live-Vorschau von ${model.id}`, `Vista previa en directo de ${model.id}`, `Prévia ao vivo de ${model.id}`), loading: 'lazy', referrerPolicy: 'no-referrer', }); img.addEventListener('error', () => { if (!img.dataset.fallbackApplied) { img.dataset.fallbackApplied = '1'; img.src = discoverFallbackImage(model.id); } }); const badges = $('div', { class: 'discover-card-badges' }, [ showStatus ? $('span', { class: 'discover-badge' + (live ? ' live' : '') }, live ? 'LIVE' : String(model.currentShow || '—').toUpperCase()) : null, showAttributeBadges && model.isNew ? $('span', { class: 'discover-badge new' }, tx('新', 'NEW', 'NEU')) : null, showAttributeBadges && model.isHd ? $('span', { class: 'discover-badge' }, 'HD') : null, showAttributeBadges && model.isGaming ? $('span', { class: 'discover-badge' }, 'GAMING') : null, showAttributeBadges && isFavorite(model.id) ? $('span', { class: 'discover-badge' }, '★') : null, showAge && model.age ? $('span', { class: 'discover-badge' }, String(model.age)) : null, ]); preview.append(img, $('div', { class: 'discover-preview-shade' }), badges); const savedBtn = $('button', { class: 'discover-btn', type: 'button' }); const favoriteBtn = $('button', { class: 'discover-btn', type: 'button', title: tx('收藏切换', 'Toggle favorite', 'Favorit umschalten') }); const syncButtons = () => { savedBtn.textContent = isSaved(model.id) ? tx('已保存', 'Saved', 'Gespeichert') : tx('保存', 'Save', 'Speichern'); favoriteBtn.textContent = isFavorite(model.id) ? '★' : '☆'; favoriteBtn.setAttribute('aria-label', isFavorite(model.id) ? tx('取消收藏', 'Remove favorite', 'Favorit entfernen') : tx('收藏', 'Favorite', 'Favorit')); }; savedBtn.addEventListener('click', () => { if (isSaved(model.id)) { store.removeRoom(model.id); toast(tx('已从资料库移除', 'Removed from library', 'Aus der Bibliothek entfernt')); } else { store.addRoom(model.id); toast(tx('已保存到资料库', 'Saved to library', 'In der Bibliothek gespeichert')); } syncButtons(); }); favoriteBtn.addEventListener('click', () => { ensureSaved(model.id); const active = store.toggleRoomInGroup(model.id, FAVORITE_GROUP_ID); toast(active ? tx('已收藏', 'Added to favorites', 'Als Favorit gespeichert') : tx('已取消收藏', 'Removed from favorites', 'Favorit entfernt')); syncButtons(); }); syncButtons(); const multiBtn = $('button', { class: 'discover-btn primary', type: 'button', onclick: () => { ensureSaved(model.id); toast(tx(`${model.id} 已加入 MultiCam`, `${model.id} added to MultiCam`, `${model.id} wurde zu MultiCam hinzugefügt`, `${model.id} se añadió a MultiCam`, `${model.id} foi adicionado ao MultiCam`)); syncButtons(); } }, 'MultiCam +'); const watchBtn = $('button', { class: 'discover-btn', type: 'button' }); const syncWatch = () => { const active = state.temporaryWatchlist.includes(model.id); watchBtn.textContent = active ? tx('观察中', 'Watching', 'Beobachtet') : tx('观察', 'Watch', 'Merken'); watchBtn.title = tx('临时观察列表', 'Temporary watchlist', 'Temporäre Beobachtungsliste'); }; watchBtn.addEventListener('click', () => { const active = state.temporaryWatchlist.includes(model.id); state.temporaryWatchlist = active ? state.temporaryWatchlist.filter(id => id !== model.id) : [...state.temporaryWatchlist, model.id].slice(-100); persistDiscoverState(); syncWatch(); toast(active ? tx('已从观察列表移除', 'Removed from watchlist', 'Von der Beobachtungsliste entfernt') : tx('已加入临时观察列表', 'Added to watchlist', 'Zur temporären Beobachtungsliste hinzugefügt')); }); syncWatch(); const groupSelect = $('select', { class: 'discover-input discover-card-group', ariaLabel: tx('选择分组', 'Choose group', 'Gruppe auswählen') }, [ $('option', { value: '' }, tx('添加到分组…', 'Add to group…', 'Zu Gruppe hinzufügen…')), ...store.state.groups.filter(group => ![LIBRARY_GROUP_ID, DEFAULT_GROUP_ID, ONLINE_GROUP_ID].includes(group.id)) .map(group => $('option', { value: group.id }, groupLabel(group))), ]); groupSelect.addEventListener('change', () => { if (!groupSelect.value) return; ensureSaved(model.id); if (!roomInGroup(roomFor(model.id), groupSelect.value)) store.toggleRoomInGroup(model.id, groupSelect.value); toast(tx('分组已更新', 'Group updated', 'Gruppe wurde aktualisiert')); groupSelect.value = ''; syncButtons(); moreDetails.open = false; }); let card = null; let moreCloseTimer = 0; const moreMenu = $('div', { class: 'discover-card-menu' }, [ $('div', { class: 'discover-card-menu-head' }, [ $('strong', {}, rawUiText(model.id)), $('button', { class: 'discover-card-menu-close', type: 'button', title: tx('关闭', 'Close', 'Schließen'), ariaLabel: tx('关闭更多操作', 'Close more actions', 'Weitere Aktionen schließen') }, '×'), ]), $('button', { class: 'discover-btn', type: 'button', onclick: () => RoomGridUltimate.openInspector(model.id, { ...model, contextIds: Array.isArray(contextIds) ? contextIds : selectedModels().map(item => item.id) }) }, tx('快速信息', 'Quick info', 'Schnellinfo')), $('button', { class: 'discover-btn', type: 'button', onclick: () => RoomGridUltimate.toggleCompare(model.id) }, RoomGridUltimate.comparisonStatus().ids.includes(model.id) ? tx('从比较中移除', 'Remove from comparison', 'Aus Vergleich entfernen') : tx('加入比较', 'Add to comparison', 'Zum Vergleich')), multiBtn, favoriteBtn, savedBtn, watchBtn, $('button', { class: 'discover-btn', type: 'button', onclick: () => openNoopener(new URL(`/${encodeURIComponent(model.id)}/`, location.origin).toString()) }, tx('网站房间', 'Site room', 'Website-Raum')), groupSelect, ]); const moreDetails = $('details', { class: 'discover-card-more' }, [ $('summary', { class: 'discover-btn', role: 'button' }, tx('更多', 'More', 'Mehr')), ]); const clearMoreCloseTimer = () => { clearTimeout(moreCloseTimer); moreCloseTimer = 0; }; const closeMore = () => { clearMoreCloseTimer(); if (moreDetails.open) moreDetails.open = false; else detachMoreViewport(); }; const closeMoreSoon = () => { clearMoreCloseTimer(); moreCloseTimer = setTimeout(closeMore, 140); }; const positionMoreMenu = () => { if (!moreDetails.open || !moreMenu.isConnected) return; const anchor = moreDetails.querySelector('summary')?.getBoundingClientRect(); const bounds = moreMenu.getBoundingClientRect(); if (!anchor) return; const gap = 8; const viewportWidth = Math.max(320, document.documentElement.clientWidth || window.innerWidth || 0); const viewportHeight = Math.max(240, document.documentElement.clientHeight || window.innerHeight || 0); const left = Math.max(8, Math.min(viewportWidth - bounds.width - 8, anchor.right - bounds.width)); let top = anchor.top - bounds.height - gap; if (top < 8) top = anchor.bottom + gap; if (top + bounds.height > viewportHeight - 8) top = Math.max(8, viewportHeight - bounds.height - 8); moreMenu.style.left = `${Math.round(left)}px`; moreMenu.style.top = `${Math.round(top)}px`; }; const outsideMore = event => { if (moreDetails.open && !moreDetails.contains(event.target) && !moreMenu.contains(event.target)) closeMore(); }; const moreKeydown = event => { if (event.key === 'Escape') closeMore(); }; const detachMoreViewport = () => { window.removeEventListener('scroll', closeMore, true); window.removeEventListener('resize', closeMore); document.removeEventListener('pointerdown', outsideMore, true); document.removeEventListener('keydown', moreKeydown, true); moreMenu.remove(); if (activeCardMoreClose === closeMore) activeCardMoreClose = null; }; moreDetails.addEventListener('toggle', () => { detachMoreViewport(); if (!moreDetails.open) return; if (activeCardMoreClose && activeCardMoreClose !== closeMore) activeCardMoreClose(); activeCardMoreClose = closeMore; document.querySelectorAll('.discover-card-more[open]').forEach(other => { if (other !== moreDetails) other.open = false; }); document.body.appendChild(moreMenu); window.addEventListener('scroll', closeMore, true); window.addEventListener('resize', closeMore); document.addEventListener('pointerdown', outsideMore, true); document.addEventListener('keydown', moreKeydown, true); requestAnimationFrame(positionMoreMenu); }); moreMenu.addEventListener('pointerenter', clearMoreCloseTimer); moreMenu.addEventListener('pointerleave', closeMoreSoon); moreMenu.querySelector('.discover-card-menu-close')?.addEventListener('click', closeMore); moreMenu.addEventListener('click', event => { if (event.target.closest('button') && !event.target.closest('.discover-card-menu-close')) setTimeout(closeMore, 0); }); const stat = statsById.get(model.id); const place = [showCountry ? model.country : '', showLocation ? model.location : ''].filter(Boolean).join(' · '); const metaItems = [ place ? $('span', { dataset: { cardFact: 'place' } }, place) : null, showOnlineTime ? $('span', { dataset: { cardFact: 'online' } }, `${tx('在线', 'Online', 'Online')} ${formatDuration(model.secondsOnline)}`) : null, showLocalStats ? $('span', { dataset: { cardFact: 'local' } }, `${tx('观看', 'Watched', 'Gesehen')} ${formatWatch(stat?.totalMs || 0)} · ${formatCount(stat?.sessions || 0)} ${tx('会话', 'sessions', 'Sitzungen')}`) : null, ].filter(Boolean); const selected = state.selectedIds.includes(model.id); const selectBox = $('input', { type: 'checkbox', checked: selected, ariaLabel: tx('选择模型', 'Select model', 'Model auswählen') }); const cardContextIds = Array.isArray(contextIds) ? contextIds : selectedModels().map(item => item.id); const singleCamButton = $('a', { class: 'discover-btn primary', href: roomGridSingleCamUrl(model.id, { source: 'catalog-card' }), rel: 'noopener noreferrer', title: tx('中键或 Ctrl+点击可在新标签页打开', 'Middle-click or Ctrl+click opens a new tab', 'Mittelklick oder Strg+Klick öffnet einen neuen Tab'), }, 'SingleCam'); singleCamButton.addEventListener('click', event => { if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return; event.preventDefault(); openSingle(model.id, cardContextIds); }); card = $('article', { class: 'discover-card' + (compact ? ' compact' : '') + (selected ? ' selected' : ''), dataset: { discoverUsername: model.id } }, [ preview, $('label', { class: 'discover-select', title: tx('用于批量操作选择', 'Select for bulk actions', 'Für Massenaktionen auswählen') }, [selectBox]), $('div', { class: 'discover-card-body' }, [ $('div', { class: 'discover-card-title' }, [ $('strong', {}, rawUiText(model.id)), showViewers ? $('span', { dataset: { cardFact: 'viewers' } }, `${formatCount(model.viewers)} ${tx('观众', 'viewers', 'Zuschauer')}`) : null, ]), showSubject ? $('p', { class: 'discover-subject', dataset: { cardFact: 'subject' } }, model.subject || tx('无公开描述', 'No public description', 'Keine öffentliche Beschreibung')) : null, model.suggestionReason ? $('div', { class: 'discover-chip', style: { marginBottom: '7px' } }, model.suggestionReason) : null, showTags ? $('div', { class: 'discover-tags', dataset: { cardFact: 'tags' } }, (model.tags || []).slice(0, 5).map(tag => $('button', { class: 'discover-tag', type: 'button', title: tx('在标签浏览器中打开', 'Open in Tag Explorer', 'Im Tag-Explorer öffnen'), onclick: () => openTag(tag) }, `#${tag}`))) : null, metaItems.length ? $('div', { class: 'discover-meta' }, metaItems) : null, $('div', { class: 'discover-card-actions' }, [ singleCamButton, $('button', { class: 'discover-btn', type: 'button', onclick: () => RoomGridUltimate.openPreview(model.id) }, tx('固定预览', 'Pin preview', 'Vorschau')), moreDetails, ]), ]), ]); card.addEventListener('pointerenter', clearMoreCloseTimer); card.addEventListener('pointerleave', closeMoreSoon); selectBox.addEventListener('change', () => { const next = new Set(state.selectedIds); if (selectBox.checked) next.add(model.id); else next.delete(model.id); state.selectedIds = [...next].slice(0, 250); card.classList.toggle('selected', selectBox.checked); persistDiscoverState(); renderCurrentView(); }); previewScheduler.register(card, model); return card; } function currentDefinition(includeQuery = true) { return discoverCleanDefinition(state, includeQuery); } function newSavedId(prefix) { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; } function saveNamed(kind) { const isFeed = kind === 'feed'; const name = discoverCleanText(prompt(isFeed ? tx('动态名称', 'Feed name', 'Name des Feeds') : tx('视图名称', 'View name', 'Name der Ansicht')) || '', isFeed ? 60 : 40); if (!name) return; const list = isFeed ? state.feeds : state.profiles; const maximum = isFeed ? 50 : 10; if (list.some(item => item.name.toLocaleLowerCase() === name.toLocaleLowerCase())) { toast(tx('名称已存在', 'Name already exists', 'Der Name ist bereits vergeben')); return; } if (list.length >= maximum) { toast(tx('已达到保存上限', 'Saved-item limit reached', 'Speicherlimit erreicht')); return; } const item = { id: newSavedId(isFeed ? 'feed' : 'view'), name, createdAt: Date.now(), definition: currentDefinition(isFeed) }; list.push(item); if (isFeed) state.activeFeedId = item.id; else state.activeProfileId = item.id; persistDiscoverState(); renderCurrentView(); toast(isFeed ? tx('动态已保存', 'Feed saved', 'Feed gespeichert') : tx('视图已保存', 'View saved', 'Ansicht gespeichert')); } function applyDefinition(item, isFeed) { if (!item) return; Object.assign(state, discoverCleanDefinition(item.definition, isFeed)); state.page = 1; if (isFeed) state.activeFeedId = item.id; else state.activeProfileId = item.id; persistDiscoverState(); renderCurrentView(); toast(tx('已应用', 'Applied', 'Angewendet')); } function deleteNamed(kind, id) { const isFeed = kind === 'feed'; const list = isFeed ? state.feeds : state.profiles; const item = list.find(entry => entry.id === id); if (!item || !confirm(tx(`删除 ${item.name}?`, `Delete ${item.name}?`, `${item.name} löschen?`, `¿Eliminar ${item.name}?`, `Excluir ${item.name}?`))) return; const next = list.filter(entry => entry.id !== id); if (isFeed) { state.feeds = next; if (state.activeFeedId === id) state.activeFeedId = ''; } else { state.profiles = next; if (state.activeProfileId === id) state.activeProfileId = ''; } persistDiscoverState(); renderCurrentView(); } function exportFeeds() { const payload = { schema: 1, exportedAt: Date.now(), profiles: state.profiles, feeds: state.feeds }; downloadBlob(new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }), `roomgrid-discover-feeds-${stampForFile()}.json`); toast(tx('备份已导出', 'Backup exported', 'Sicherung exportiert')); } async function importFeeds(file) { try { if (!file || file.size > 1024 * 1024) throw new Error('file too large'); const parsed = JSON.parse(await file.text()); if (!parsed || parsed.schema !== 1 || !Array.isArray(parsed.profiles) || !Array.isArray(parsed.feeds)) throw new Error('invalid backup'); const profiles = discoverCleanSavedList(parsed.profiles, 10, false); const feeds = discoverCleanSavedList(parsed.feeds, 50, true); const message = tx(`导入 ${profiles.length} 个视图和 ${feeds.length} 个动态?`, `Import ${profiles.length} views and ${feeds.length} feeds?`, `${profiles.length} Ansichten und ${feeds.length} Feeds importieren?`, `¿Importar ${profiles.length} vistas y ${feeds.length} feeds?`, `Importar ${profiles.length} visualizações e ${feeds.length} feeds?`); if (!confirm(message)) return; state.profiles = profiles; state.feeds = feeds; state.activeProfileId = ''; state.activeFeedId = ''; persistDiscoverState(); renderCurrentView(); toast(tx('备份已导入', 'Backup imported', 'Sicherung importiert')); } catch (_) { toast(tx('备份无效', 'Invalid backup', 'Ungültige Sicherung')); } } function invalidateViewCache() { catalogDataRevision += 1; modelDataCache = new WeakMap(); selectionCache.clear(); tagIndexCache = { signature: '', value: null }; tagCatalogBreakdownCache = { signature: '', value: null }; } function persistDiscoverState(keepViewCache = false) { publishPreviewSettings(); discoverSaveState(state); // Detached views validate their own inputs on return. A SingleCam // preference or tag artwork edit must not discard every other menu. } function visualViewSignature(view) { const shared = [dataCacheSignature(), LANG, store.revision, state.previewEnabled, state.previewFps, state.previewMode, state.previewConcurrency, state.hoverLivePreview, state.hoverPreviewDelay, state.hoverPreviewScale, state.selectedIds, state.temporaryWatchlist, catalogLoading, catalogPrefetching, catalogError, catalogIsFallback, catalogUsingCache, catalogFetchedAt, catalogOffset, catalogTotal, catalogReachedEnd]; const fields = view === 'tags' ? ['tagSearch', 'tagBrowseScope', 'tagBrowseSort', 'tagBrowsePage', 'tagResultPage', 'tagExpanded', 'tagSelection', 'tagSelectionMode', 'tagFavorites', 'tagBlocked', 'tagGroups', 'tagArtwork', 'tagHistory', 'tagObservations', 'themeFavorites', 'sortPrimary', 'sortSecondary'] : view === 'dashboard' ? ['homePage', 'homePageSize', 'homeColumns', 'homeCardScale', 'homeImageRatio', 'homeDensity', 'homeShowStats', 'homeShowWidgets', 'homeToolsPanel', 'dashboardOrder', 'dashboardHidden', 'dashboardLimits', 'dashboardSizes', 'feeds', 'activeFeedId', 'profiles', 'activeProfileId', 'tagArtwork', 'tagFavorites', 'themeFavorites', 'randomSeed'] : ['page', 'profiles', 'feeds', 'activeProfileId', 'activeFeedId', 'advancedFiltersOpen', 'tagArtwork', 'tagFavorites', 'themeFavorites', 'tagHistory', 'searchHistory', 'randomSeed', 'discoveryHiddenModels', 'discoveryLessTags']; return JSON.stringify([shared, view === 'tags' ? null : discoverCleanDefinition(state), fields.map(key => state[key])]); } function cacheCurrentVisualView() { if (!['dashboard', 'discover', 'tags'].includes(state.view) || !main.childNodes.length) return; activeCardMoreClose?.(); previewScheduler.clear(); const fragment = document.createDocumentFragment(); while (main.firstChild) fragment.appendChild(main.firstChild); viewDomCache.set(state.view, { fragment, signature: activeVisualSignature }); } function restoreCachedVisualView(view) { const cached = viewDomCache.get(view); if (!cached) return false; viewDomCache.delete(view); if (cached.signature !== visualViewSignature(view)) return false; main.replaceChildren(cached.fragment); main.querySelectorAll('[data-discover-username]').forEach(card => { const id = normalizeUsername(card.dataset.discoverUsername); const model = catalogById.get(id) || (isLikelyUsername(id) ? localModel(id) : null); if (model) previewScheduler.register(card, model); }); return true; } function setView(view) { const next = ['dashboard', 'discover', 'tags', 'stats', 'library', 'following', 'user', 'single'].includes(view) ? view : 'discover'; if (state.view === next) { if (next !== 'single' && !catalog.length && !catalogLoading) setTimeout(refreshCompleteCatalog, 0); return; } if (state.view !== 'single') state.scrollPositions[state.view] = Math.max(0, Math.round(window.scrollY || 0)); cacheCurrentVisualView(); if (state.view === 'single' && next !== 'single') cleanupSingleSession(); state.view = next; pendingScrollRestore = next !== 'single'; document.body.classList.remove('discover-cinema'); const url = new URL(location.href); url.searchParams.set('suite_view', state.view); if (state.view === 'single' && state.singleId) url.searchParams.set('model', state.singleId); else url.searchParams.delete('model'); history.replaceState(null, '', url.toString()); persistDiscoverState(true); renderCurrentView(true); if (next !== 'single' && !catalog.length && !catalogLoading) setTimeout(refreshCompleteCatalog, 0); else if (next !== 'single') { continueCatalogPrefetch(); scheduleCatalogRefresh(); } } function recordTagVisit(tag) { tag = discoverCleanText(tag, 50).toLocaleLowerCase(); if (!tag) return; state.tagHistory = [...state.tagHistory, { tag, at: Date.now() }].slice(-120); analytics.recordEvent({ type: 'tag', tags: [tag], detail: tag }); persistDiscoverState(); } function openTag(tag) { tag = discoverCleanText(tag, 50).toLocaleLowerCase(); if (!tag) return; recordTagVisit(tag); state.tagSearch = tag; state.tagSelection = [tag]; state.tagSelectionMode = 'and'; state.tagBrowseScope = 'all'; state.tagBrowsePage = 1; state.tagResultPage = 1; persistDiscoverState(); if (state.view === 'tags') renderCurrentView(); else setView('tags'); } function openSingle(id, contextIds = []) { id = normalizeUsername(id); if (!isLikelyUsername(id)) return; if (state.view !== 'single') singleReturnView = ['dashboard', 'discover', 'tags', 'stats', 'library', 'following', 'user'].includes(state.view) ? state.view : 'discover'; cleanupSingleSession(); singleContextIds = [...new Set((Array.isArray(contextIds) ? contextIds : []).map(normalizeUsername).filter(isLikelyUsername))]; if (!singleContextIds.includes(id)) singleContextIds.unshift(id); state.singleId = id; state.singleContext = singleContextIds.slice(0, DISCOVER_MAX_RESULTS); state.singleReturnView = singleReturnView; if (state.view === 'single') { const url = new URL(location.href); url.searchParams.set('suite_view', 'single'); url.searchParams.set('model', id); history.replaceState(null, '', url.toString()); persistDiscoverState(true); renderCurrentView(true); window.scrollTo({ top: 0, behavior: 'auto' }); return; } setView('single'); // A route change must settle immediately; animated scrolling moves the // newly opened cockpit and postpones reliable pointer interaction. window.scrollTo({ top: 0, behavior: 'auto' }); } dashboardNav.addEventListener('click', () => setView('dashboard')); discoverNav.addEventListener('click', () => setView('discover')); tagsNav.addEventListener('click', () => setView('tags')); statsNav.addEventListener('click', () => setView('stats')); libraryNav.addEventListener('click', () => setView('library')); followingNav.addEventListener('click', () => setView('following')); userNav.addEventListener('click', () => setView('user')); refreshBtn.addEventListener('click', refreshCompleteCatalog); function filterField(label, control, className = '') { return $('label', { class: `discover-field ${className}`.trim() }, [$('span', {}, label), control]); } function selectControl(value, options, onChange) { const select = $('select', { class: 'discover-input' }, options.map(option => $('option', { value: option[0] }, option[1]))); select.value = value; select.addEventListener('change', () => onChange(select.value)); return select; } function textControl(value, placeholder, onChange, type = 'text') { const input = $('input', { class: 'discover-input', type, value, placeholder }); const event = type === 'text' ? 'input' : 'change'; input.addEventListener(event, debounce(() => onChange(type === 'number' ? Math.max(0, Number(input.value) || 0) : input.value), 130)); return input; } function knownFilterTags() { const localTagCount = store.state.rooms.reduce((sum, room) => sum + (Array.isArray(room.localTags) ? room.localTags.length : 0), 0); const signature = `${catalog.length}:${catalogFetchedAt}:${Object.keys(state.tagObservations).length}:${localTagCount}:${state.tagHistory.length}`; if (tagSuggestionCache.signature === signature) return tagSuggestionCache.values; const values = new Set(); const add = value => { const tag = discoverCleanText(value, 50).replace(/^#+/u, '').toLocaleLowerCase(); if (tag) values.add(tag); }; Object.keys(state.tagObservations).forEach(add); state.tagFavorites.forEach(add); state.tagBlocked.forEach(add); state.themeFavorites.forEach(add); state.tagHistory.forEach(item => add(item?.tag)); for (const room of store.state.rooms) (room.localTags || []).forEach(add); // The public catalog is the authoritative source for website tags. This // scan is cached and therefore does not run again while the user types. for (const model of catalog) (model.tags || []).forEach(add); const ordered = [...values].sort((left, right) => left.localeCompare(right)); tagSuggestionCache = { signature, values: ordered }; return ordered; } function tagTermsControl(value, placeholder, key) { const input = $('input', { class: 'discover-input', type: 'text', value, placeholder, ariaAutocomplete: 'list', autocomplete: 'off', dataset: { discoverTagFilter: key }, }); const menu = $('div', { class: 'discover-autocomplete-menu', role: 'listbox' }); const shell = $('div', { class: 'discover-autocomplete' }, [input, menu]); let matches = []; let activeIndex = -1; const closeMenu = () => { matches = []; activeIndex = -1; menu.classList.remove('is-open'); menu.replaceChildren(); input.setAttribute('aria-expanded', 'false'); }; const writeState = () => { state[key] = discoverCleanText(input.value, 160); state.page = 1; state.homePage = 1; state.advancedFiltersOpen = true; persistDiscoverState(); }; const applySuggestion = tag => { clearTimeout(filterRenderTimer); filterRenderTimer = 0; const parts = input.value.split(','); parts[parts.length - 1] = ` ${tag}`; input.value = parts.map(part => part.trim()).filter(Boolean).join(', '); writeState(); closeMenu(); analytics.recordEvent({ type: 'filter', detail: key }); renderCurrentView(); }; const renderMenu = () => { const rawParts = input.value.split(','); const fragment = discoverCleanText(rawParts[rawParts.length - 1], 50).replace(/^#+/u, '').toLocaleLowerCase(); const selected = new Set(rawParts.slice(0, -1).map(item => discoverCleanText(item, 50).replace(/^#+/u, '').toLocaleLowerCase()).filter(Boolean)); if (!fragment) { closeMenu(); return; } const pool = knownFilterTags().filter(tag => !selected.has(tag) && tag.includes(fragment)); matches = [ ...pool.filter(tag => tag.startsWith(fragment)), ...pool.filter(tag => !tag.startsWith(fragment)), ].slice(0, 12); activeIndex = -1; if (!matches.length) { closeMenu(); return; } menu.replaceChildren(...matches.map((tag, index) => { const option = $('button', { class: 'discover-autocomplete-option', type: 'button', role: 'option' }, `#${tag}`); option.addEventListener('mousedown', event => event.preventDefault()); option.addEventListener('click', () => applySuggestion(tag)); option.addEventListener('mouseenter', () => { activeIndex = index; [...menu.children].forEach((node, itemIndex) => node.classList.toggle('is-active', itemIndex === activeIndex)); }); return option; })); menu.classList.add('is-open'); input.setAttribute('aria-expanded', 'true'); }; const scheduleResultRefresh = () => { clearTimeout(filterRenderTimer); const start = input.selectionStart ?? input.value.length; const end = input.selectionEnd ?? start; filterRenderTimer = window.setTimeout(() => { pendingFilterFocus = document.activeElement === input ? { key, start, end } : null; analytics.recordEvent({ type: 'filter', detail: key }); renderCurrentView(); }, 360); }; input.addEventListener('focus', () => { state.advancedFiltersOpen = true; renderMenu(); }); input.addEventListener('input', () => { writeState(); renderMenu(); scheduleResultRefresh(); }); input.addEventListener('keydown', event => { if (event.key === 'ArrowDown' && matches.length) { event.preventDefault(); activeIndex = (activeIndex + 1) % matches.length; } else if (event.key === 'ArrowUp' && matches.length) { event.preventDefault(); activeIndex = (activeIndex - 1 + matches.length) % matches.length; } else if (event.key === 'Enter') { event.preventDefault(); clearTimeout(filterRenderTimer); if (activeIndex >= 0 && matches[activeIndex]) { applySuggestion(matches[activeIndex]); return; } writeState(); closeMenu(); analytics.recordEvent({ type: 'filter', detail: key }); renderCurrentView(); return; } else if (event.key === 'Escape') { if (menu.classList.contains('is-open')) event.stopPropagation(); closeMenu(); return; } else return; [...menu.children].forEach((node, index) => node.classList.toggle('is-active', index === activeIndex)); menu.children[activeIndex]?.scrollIntoView?.({ block: 'nearest' }); }); input.addEventListener('blur', () => window.setTimeout(closeMenu, 100)); return shell; } function stateChanged(patch, rerender = true) { Object.assign(state, patch, { page: 1, homePage: 1 }); analytics.recordEvent({ type: 'filter', detail: Object.keys(patch || {}).join(',') }); persistDiscoverState(); if (rerender) renderCurrentView(); } function resetDiscoverFilters() { const base = discoverDefaultState(); Object.assign(state, discoverCleanDefinition(base, true), { page: 1, homePage: 1, activeProfileId: '', activeFeedId: '' }); persistDiscoverState(); renderCurrentView(); } function createActiveFilterSummary() { const chips = []; const add = (label, remove, negative = false) => { const text = discoverCleanText(label, 120); if (!text) return; chips.push($('button', { class: `discover-active-filter${negative ? ' is-negative' : ''}`, type: 'button', title: tx(`移除筛选:${text}`, `Remove filter: ${text}`, `Filter entfernen: ${text}`), ariaLabel: tx(`移除筛选:${text}`, `Remove filter: ${text}`, `Filter entfernen: ${text}`), onclick: remove, }, [ $('span', { class: 'discover-active-filter-label' }, text), $('span', { class: 'discover-active-filter-remove', ariaHidden: 'true' }, '×'), ])); }; const removeTerm = (field, term) => () => stateChanged({ [field]: parseTerms(state[field]).filter(value => value !== term).join(', '), }); const quickLabels = { hd: tx('高清', 'HD', 'HD'), new: tx('新主播', 'New models', 'Neue Models'), new_since: tx('本次新发现', 'New since last visit', 'Seit letztem Besuch neu'), new_to_me: tx('对我来说是新的', 'New to me', 'Neu für mich'), returning: tx('再次访问', 'Returning', 'Wiederkehr'), stable: tx('稳定在线', 'Stable online', 'Stabil online'), gaming: 'Gaming', watchlist: tx('观察列表', 'Watchlist', 'Beobachtungsliste'), following: tx('仅关注', 'Following only', 'Nur Following'), saved: tx('仅已保存', 'Saved only', 'Nur gespeichert'), }; const localLabels = { saved: tx('已保存', 'Saved', 'Gespeichert'), favorites: tx('收藏', 'Favorites', 'Favoriten'), following: 'Following', known: tx('曾观看', 'Previously watched', 'Schon besucht'), never: tx('从未观看', 'Never watched', 'Noch nie besucht'), }; const genderLabels = { f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans', }; if (state.query) add(`${tx('搜索', 'Search', 'Suche')}: ${state.query}`, () => stateChanged({ query: '' })); if (state.quick !== 'all') add(`${tx('快速筛选', 'Quick filter', 'Schnellfilter')}: ${quickLabels[state.quick] || state.quick}`, () => stateChanged({ quick: 'all' })); if (state.localMode !== 'all') add(`${tx('本地集合', 'Local collection', 'Lokale Auswahl')}: ${localLabels[state.localMode] || state.localMode}`, () => stateChanged({ localMode: 'all' })); for (const gender of state.genders) add(`${tx('类型', 'Type', 'Typ')}: ${genderLabels[gender] || gender}`, () => stateChanged({ genders: state.genders.filter(value => value !== gender) })); for (const tag of parseTerms(state.tagsAll)) add(`${tx('全部', 'AND', 'UND')} #${tag}`, removeTerm('tagsAll', tag)); for (const tag of parseTerms(state.tagsAny)) add(`${tx('任一', 'OR', 'ODER')} #${tag}`, removeTerm('tagsAny', tag)); for (const tag of parseTerms(state.tagsNot)) add(`${tx('排除', 'NOT', 'NICHT')} #${tag}`, removeTerm('tagsNot', tag), true); for (const place of parseTerms(state.place)) add(`${tx('地点', 'Place', 'Ort')}: ${place}`, removeTerm('place', place)); if (state.groupId) { const group = store.state.groups.find(item => item.id === state.groupId); add(`${tx('分组', 'Group', 'Gruppe')}: ${groupLabel(group) || state.groupId}`, () => stateChanged({ groupId: '' })); } if (Number(state.minAge) > 0) add(`${tx('年龄', 'Age', 'Alter')} ≥ ${state.minAge}`, () => stateChanged({ minAge: 0 })); if (Number(state.maxAge) > 0) add(`${tx('年龄', 'Age', 'Alter')} ≤ ${state.maxAge}`, () => stateChanged({ maxAge: 0 })); if (Number(state.minViewers) > 0) add(`${tx('观众', 'Viewers', 'Zuschauer')} ≥ ${formatCount(state.minViewers)}`, () => stateChanged({ minViewers: 0 })); if (Number(state.maxViewers) > 0) add(`${tx('观众', 'Viewers', 'Zuschauer')} ≤ ${formatCount(state.maxViewers)}`, () => stateChanged({ maxViewers: 0 })); if (Number(state.minOnlineMinutes) > 0) add(`${tx('在线', 'Online', 'Online')} ≥ ${state.minOnlineMinutes} min`, () => stateChanged({ minOnlineMinutes: 0 })); if (Number(state.maxOnlineMinutes) > 0) add(`${tx('在线', 'Online', 'Online')} ≤ ${state.maxOnlineMinutes} min`, () => stateChanged({ maxOnlineMinutes: 0 })); if (Number(state.minWatchMinutes) > 0) add(`${tx('本地观看', 'Local watch', 'Lokale Sehzeit')} ≥ ${state.minWatchMinutes} min`, () => stateChanged({ minWatchMinutes: 0 })); if (Number(state.minSessions) > 0) add(`${tx('会话', 'Sessions', 'Sitzungen')} ≥ ${state.minSessions}`, () => stateChanged({ minSessions: 0 })); if (state.hideVisitedToday) add(tx('隐藏今天访问过的', 'Hide visited today', 'Heute besuchte ausblenden'), () => stateChanged({ hideVisitedToday: false })); return $('div', { class: `discover-active-filters${chips.length ? ' is-active' : ''}`, role: 'status', ariaLive: 'polite', dataset: { roomgridActiveFilters: '1' }, }, [ $('span', { class: 'discover-active-filter-title' }, [ tx('当前筛选', 'Active filters', 'Aktive Filter'), chips.length ? $('span', { class: 'discover-active-filter-count' }, String(chips.length)) : null, ]), ...(chips.length ? chips : [$('span', { class: 'discover-active-filter-empty' }, tx('未启用筛选 · 显示全部匹配的直播模型', 'No filters active · all matching live models are shown', 'Keine Filter aktiv · alle passenden Live-Models werden angezeigt'))]), chips.length ? $('button', { class: 'discover-active-filter-clear', type: 'button', onclick: resetDiscoverFilters }, tx('全部重置', 'Reset all', 'Alle zurücksetzen')) : null, ].filter(Boolean)); } function createSavedRow(kind) { const isFeed = kind === 'feed'; const list = isFeed ? state.feeds : state.profiles; const activeId = isFeed ? state.activeFeedId : state.activeProfileId; const select = $('select', { class: 'discover-input' }, [ $('option', { value: '' }, isFeed ? tx('未选择动态', 'No feed selected', 'Kein Feed ausgewählt') : tx('未选择视图', 'No view selected', 'Keine Ansicht ausgewählt')), ...list.map(item => $('option', { value: item.id }, rawUiText(item.name))), ]); select.value = list.some(item => item.id === activeId) ? activeId : ''; const applyButton = $('button', { class: 'discover-btn', type: 'button', disabled: !select.value, onclick: () => applyDefinition(list.find(item => item.id === select.value), isFeed) }, tx('应用', 'Apply', 'Anwenden')); const deleteButton = $('button', { class: 'discover-btn danger', type: 'button', disabled: !select.value, onclick: () => deleteNamed(kind, select.value) }, tx('删除', 'Delete', 'Löschen')); select.addEventListener('change', () => { applyButton.disabled = deleteButton.disabled = !select.value; }); return $('div', { class: 'discover-saved-row' }, [ filterField(isFeed ? tx('保存的动态', 'Saved feeds', 'Gespeicherte Feeds') : tx('保存的视图', 'Saved views', 'Gespeicherte Ansichten'), select), $('button', { class: 'discover-btn primary', type: 'button', onclick: () => saveNamed(kind) }, tx('新保存', 'Save new', 'Neu speichern')), applyButton, deleteButton, ]); } function createPreviewControls(homepage = false) { const previewEnabled = $('input', { type: 'checkbox', checked: state.previewEnabled }); previewEnabled.addEventListener('change', () => { state.previewEnabled = previewEnabled.checked; persistDiscoverState(); previewScheduler.refresh(); }); const previewFps = selectControl(String(state.previewFps), [['0', tx('关闭', 'Off', 'Aus')], ['0.1', '0,1 FPS · 10 s'], ['0.2', '0,2 FPS · 5 s'], ['0.25', '0,25 FPS · 4 s'], ['0.33', '0,33 FPS · 3 s'], ['0.5', '0,5 FPS · 2 s'], ['0.75', '0,75 FPS'], ['1', '1 FPS'], ['1.5', '1,5 FPS'], ['2', '2 FPS'], ['3', '3 FPS'], ['4', '4 FPS'], ['5', '5 FPS'], ...PREVIEW_FPS_VALUES.filter(fps => fps > 5).map(fps => [String(fps), `${fps} FPS${fps === 240 ? ' · MAX' : ''}`])], value => { state.previewFps = Number(value); state.previewEnabled = Number(value) > 0; persistDiscoverState(); previewScheduler.refresh(); renderCurrentView(); }); const previewMode = selectControl(state.previewMode, [['simultaneous', tx('全部可见内容同时更新', 'All visible cards simultaneously', 'Alle sichtbaren gleichzeitig')], ['bounded', tx('性能受限的分批更新', 'Performance-bounded batches', 'Leistungsgestaffelt')]], value => { state.previewMode = value; persistDiscoverState(); previewScheduler.refresh(); renderCurrentView(); }); const concurrency = selectControl(String(state.previewConcurrency), [['0', tx('所有可见内容', 'All visible', 'Alle sichtbaren')], ['1', '1'], ['2', '2'], ['4', '4'], ['6', '6'], ['8', '8'], ['12', '12'], ['16', '16'], ['24', '24']], value => { state.previewConcurrency = Number(value); persistDiscoverState(); previewScheduler.refresh(); }); const hoverEnabled = $('input', { type: 'checkbox', checked: state.hoverLivePreview }); hoverEnabled.addEventListener('change', () => { state.hoverLivePreview = hoverEnabled.checked; persistDiscoverState(); previewScheduler.refresh(); }); const hoverDelay = selectControl(String(state.hoverPreviewDelay), [['0', tx('立即', 'Immediately', 'Sofort')], ['150', '0,15 s'], ['250', '0,25 s'], ['400', '0,4 s'], ['600', '0,6 s'], ['1000', '1 s']], value => { state.hoverPreviewDelay = Number(value); persistDiscoverState(); previewScheduler.refresh(); }); const hoverScale = selectControl(String(state.hoverPreviewScale), [...new Set([100, 105, 110, 112, 115, 120, 125, 130, 135, 140, 150, 160, 170, 180, 200, 225, 250, 275, 300, 350, 400, 450, 500, 550, 600, state.hoverPreviewScale])].sort((a, b) => a - b).map(value => [String(value), `${value} %`]), value => { state.hoverPreviewScale = Number(value); persistDiscoverState(); previewScheduler.refresh(); }); const performance = $('details', { class: 'discover-details', open: homepage, dataset: { previewOptions: homepage ? 'homepage' : 'discover' } }, [ $('summary', {}, tx('实时预览与性能', 'Live previews and performance', 'Live-Vorschau und Leistung')), $('div', { class: 'discover-filter-grid' }, [ $('label', { class: 'discover-check', style: { alignSelf: 'end', minHeight: '38px' } }, [previewEnabled, tx('所有可见卡片的静音实时预览', 'Muted live preview for every visible card', 'Stumme Live-Vorschau für alle sichtbaren Karten')]), filterField(tx('更新率', 'Refresh rate', 'Aktualisierungsrate'), previewFps), filterField(tx('更新方式', 'Update mode', 'Aktualisierungsart'), previewMode), filterField(tx('分批并行限制', 'Batch concurrency limit', 'Parallelitätsgrenze bei Staffelung'), concurrency), $('label', { class: 'discover-check', style: { alignSelf: 'end', minHeight: '38px' } }, [hoverEnabled, tx('悬停时播放真正的实时视频', 'Real-time video while hovering', 'Echtes Livevideo beim Darüberfahren')]), filterField(tx('悬停延迟', 'Hover delay', 'Hover-Verzögerung'), hoverDelay), filterField(tx('悬停放大', 'Hover enlargement', 'Hover-Vergrößerung'), hoverScale), ]), $('p', { style: { margin: '9px 0 0', color: 'var(--dc-muted)', fontSize: '11px', lineHeight: '1.45' } }, tx( '可见卡片使用 JPEG 帧。鼠标悬停时,只有一张卡片切换到静音实时视频;移开鼠标后立即完全停止。高帧率会显著增加带宽和 CPU 使用。', 'Visible cards use JPEG frames. On hover, only one card switches to muted real-time video and stops completely when the pointer leaves. High frame rates significantly increase bandwidth and CPU use.', 'Sichtbare Karten verwenden JPEG-Einzelbilder. Beim Darüberfahren wechselt nur diese eine Karte auf ein stummes Echtzeitvideo und stoppt beim Verlassen vollständig. Hohe Bildraten – besonders ab 10 FPS – erhöhen Bandbreite und CPU-Last stark.' )), ]); performance.appendChild($('p', { class: 'discover-preview-size-note' }, tx('放大最大为 600%;实际显示会限制在窗口内。', 'Enlargement up to 600%; the displayed size is limited to the available viewport.', 'Vergrößerung bis 600 %; die tatsächliche Größe wird auf den verfügbaren Bildschirmplatz begrenzt.'))); return performance; } function createDiscoverControls(includeActiveFilters = true) { const quickOptions = [ ['all', tx('全部公开房间', 'All public rooms', 'Alle öffentlichen Räume')], ['hd', tx('已知高清', 'Known HD', 'Bekanntes HD')], ['new', tx('新主播', 'New models', 'Neue Models')], ['new_since', tx('本次新发现', 'New since last visit', 'Seit letztem Besuch neu')], ['new_to_me', tx('对我来说是新的', 'New to me', 'Neu für mich')], ['returning', tx('再次访问', 'Returning', 'Wiederkehr')], ['stable', tx('稳定在线', 'Stable online', 'Stabil online')], ['gaming', 'Gaming'], ['watchlist', tx('观察列表', 'Watchlist', 'Beobachtungsliste')], ['following', tx('仅关注', 'Following only', 'Nur Following')], ['saved', tx('仅已保存', 'Saved only', 'Nur gespeichert')], ]; const localOptions = [ ['all', tx('全部', 'All', 'Alle')], ['saved', tx('已保存', 'Saved', 'Gespeichert')], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['following', 'Following'], ['known', tx('曾观看', 'Previously watched', 'Schon besucht')], ['never', tx('从未观看', 'Never watched', 'Noch nie besucht')], ]; const sortOptions = [ ['random', tx('随机结果', 'Random result', 'Zufällige Reihenfolge')], ['viewers_desc', tx('观众 ↓', 'Viewers ↓', 'Zuschauer ↓')], ['viewers_asc', tx('观众 ↑', 'Viewers ↑', 'Zuschauer ↑')], ['online_desc', tx('在线时长 ↓', 'Online duration ↓', 'Online-Dauer ↓')], ['followers_desc', tx('粉丝 ↓', 'Followers ↓', 'Follower ↓')], ['name_asc', tx('名称 A–Z', 'Name A–Z', 'Name A–Z')], ['name_desc', tx('名称 Z–A', 'Name Z–A', 'Name Z–A')], ['new_desc', tx('新主播优先', 'New first', 'Neue zuerst')], ['gaming_desc', 'Gaming zuerst'], ['watch_desc', tx('观看时长 ↓', 'Watch time ↓', 'Sehzeit ↓')], ['sessions_desc', tx('会话数 ↓', 'Sessions ↓', 'Sitzungen ↓')], ['last_desc', tx('最近观看', 'Last watched', 'Zuletzt besucht')], ]; const panel = $('section', { class: 'discover-panel' }); const queryControl = textControl(state.query, tx('名称、描述、标签、地点', 'Name, description, tag, place', 'Name, Beschreibung, Tag, Ort'), value => stateChanged({ query: discoverCleanText(value, 120) })); queryControl.dataset.discoverSearchField = '1'; queryControl.addEventListener('keydown', event => { if (event.key !== 'Enter') return; const query = discoverCleanText(queryControl.value, 120); if (query) state.searchHistory = [...state.searchHistory.filter(item => item !== query), query].slice(-30); persistDiscoverState(); }); const basic = $('div', { class: 'discover-controls' }, [ filterField(tx('搜索', 'Search', 'Suche'), queryControl, 'search'), filterField(tx('快速筛选', 'Quick filter', 'Schnellfilter'), selectControl(state.quick, quickOptions, value => stateChanged({ quick: value }))), filterField(tx('本地集合', 'Local collection', 'Lokale Auswahl'), selectControl(state.localMode, localOptions, value => stateChanged({ localMode: value }))), filterField(tx('第一排序', 'Primary sort', 'Erste Sortierung'), selectControl(state.sortPrimary, sortOptions, value => stateChanged({ sortPrimary: value }))), filterField(tx('第二排序', 'Secondary sort', 'Zweite Sortierung'), selectControl(state.sortSecondary, sortOptions, value => stateChanged({ sortSecondary: value }))), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.randomSeed = Date.now(); state.sortPrimary = 'random'; persistDiscoverState(); renderCurrentView(); } }, tx('重新随机', 'Shuffle again', 'Neu mischen')), ]); const genderChecks = ['f', 'm', 'c', 't'].map(code => { const names = { f: tx('女', 'Women', 'Frauen'), m: tx('男', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans' }; const input = $('input', { type: 'checkbox', checked: state.genders.includes(code) }); input.addEventListener('change', () => { const next = new Set(state.genders); if (input.checked) next.add(code); else next.delete(code); stateChanged({ genders: [...next] }); }); return $('label', { class: 'discover-check' }, [input, names[code]]); }); const groupOptions = [['', tx('所有分组', 'All groups', 'Alle Gruppen')], ...store.state.groups .filter(group => ![LIBRARY_GROUP_ID, ONLINE_GROUP_ID].includes(group.id)).map(group => [group.id, groupLabel(group)])]; const advancedGrid = $('div', { class: 'discover-filter-grid' }, [ filterField(tx('全部标签(UND)', 'All tags (AND)', 'Alle Tags (UND)'), tagTermsControl(state.tagsAll, 'dance, german', 'tagsAll')), filterField(tx('任一标签(ODER)', 'Any tag (OR)', 'Ein Tag (ODER)'), tagTermsControl(state.tagsAny, 'music, gaming', 'tagsAny')), filterField(tx('排除标签(NICHT)', 'Excluded tags (NOT)', 'Tags ausschließen (NICHT)'), tagTermsControl(state.tagsNot, 'private', 'tagsNot')), filterField(tx('语言、地区或国家', 'Language, region, or country', 'Sprache, Region oder Land'), textControl(state.place, 'DE, Berlin, deutsch', value => stateChanged({ place: value }))), filterField(tx('分组', 'Group', 'Gruppe'), selectControl(state.groupId, groupOptions, value => stateChanged({ groupId: value }))), filterField(tx('最低年龄', 'Minimum age', 'Alter mindestens'), textControl(state.minAge, '0', value => stateChanged({ minAge: value }), 'number')), filterField(tx('最高年龄', 'Maximum age', 'Alter höchstens'), textControl(state.maxAge, '0', value => stateChanged({ maxAge: value }), 'number')), filterField(tx('观众至少', 'Minimum viewers', 'Zuschauer mindestens'), textControl(state.minViewers, '0', value => stateChanged({ minViewers: value }), 'number')), filterField(tx('观众最多', 'Maximum viewers', 'Zuschauer höchstens'), textControl(state.maxViewers, '0', value => stateChanged({ maxViewers: value }), 'number')), filterField(tx('在线分钟至少', 'Minimum online minutes', 'Online-Minuten mindestens'), textControl(state.minOnlineMinutes, '0', value => stateChanged({ minOnlineMinutes: value }), 'number')), filterField(tx('在线分钟最多', 'Maximum online minutes', 'Online-Minuten höchstens'), textControl(state.maxOnlineMinutes, '0', value => stateChanged({ maxOnlineMinutes: value }), 'number')), filterField(tx('本地观看分钟至少', 'Minimum local watch minutes', 'Lokale Sehzeit mindestens'), textControl(state.minWatchMinutes, '0', value => stateChanged({ minWatchMinutes: value }), 'number')), filterField(tx('会话数至少', 'Minimum sessions', 'Sitzungen mindestens'), textControl(state.minSessions, '0', value => stateChanged({ minSessions: value }), 'number')), ]); const todayCheck = $('input', { type: 'checkbox', checked: state.hideVisitedToday }); todayCheck.addEventListener('change', () => stateChanged({ hideVisitedToday: todayCheck.checked })); const reset = $('button', { class: 'discover-btn', type: 'button', onclick: resetDiscoverFilters }, tx('重置筛选', 'Reset filters', 'Filter zurücksetzen')); const advanced = $('details', { class: 'discover-details', open: state.advancedFiltersOpen }, [ $('summary', {}, tx('高级筛选与布尔标签', 'Advanced filters and Boolean tags', 'Erweiterte Filter und boolesche Tags')), advancedGrid, $('div', { class: 'discover-checks' }, [...genderChecks, $('label', { class: 'discover-check' }, [todayCheck, tx('隐藏今天访问过的', 'Hide visited today', 'Heute besuchte ausblenden')]), reset]), ]); advanced.addEventListener('toggle', () => { if (!advanced.isConnected) return; state.advancedFiltersOpen = advanced.open; persistDiscoverState(); }); const saved = $('details', { class: 'discover-details' }, [ $('summary', {}, tx('保存的视图与动态', 'Saved views and feeds', 'Gespeicherte Ansichten und Feeds')), $('div', { class: 'discover-saved' }, [createSavedRow('profile'), createSavedRow('feed')]), $('div', { class: 'discover-checks' }, [ $('button', { class: 'discover-btn', type: 'button', onclick: exportFeeds }, tx('导出', 'Export', 'Exportieren')), (() => { const input = $('input', { type: 'file', accept: 'application/json,.json', style: { display: 'none' } }); input.addEventListener('change', () => { const file = input.files?.[0]; input.value = ''; if (file) importFeeds(file); }); return $('label', { class: 'discover-btn', style: { display: 'inline-flex', alignItems: 'center' } }, [tx('导入', 'Import', 'Importieren'), input]); })(), ]), ]); const history = state.searchHistory.length ? $('details', { class: 'discover-details' }, [ $('summary', {}, tx('最近搜索', 'Recent searches', 'Letzte Suchanfragen')), $('div', { class: 'discover-checks' }, [ ...[...state.searchHistory].reverse().map(query => $('button', { class: 'discover-btn', type: 'button', onclick: () => stateChanged({ query }) }, query)), $('button', { class: 'discover-btn danger', type: 'button', onclick: () => { state.searchHistory = []; persistDiscoverState(); renderCurrentView(); } }, tx('清除', 'Clear', 'Leeren')), ]), ]) : null; const performance = createPreviewControls(); panel.append(basic); if (includeActiveFilters) panel.append(createActiveFilterSummary()); panel.append(advanced, saved); if (history) panel.append(history); panel.append(performance); return panel; } function createHero(title, description) { const online = catalog.filter(model => model.currentShow === 'public').length; return $('section', { class: 'discover-hero' }, [ $('div', {}, [$('p', { class: 'discover-kicker' }, 'RoomGrid Suite'), $('h1', {}, title), $('p', {}, description)]), $('div', { class: 'discover-summary' }, [ $('span', { class: 'discover-chip live' }, `● ${formatCount(online)} LIVE`), $('span', { class: 'discover-chip' }, `${formatCount(store.state.rooms.length)} ${tx('已保存', 'saved', 'gespeichert')}`), $('span', { class: 'discover-chip' }, state.previewEnabled ? `${String(state.previewFps).replace('.', ',')} FPS · ${tx('预览', 'previews', 'Vorschau')}` : tx('预览关闭', 'Previews off', 'Vorschau aus')), catalogFetchedAt ? $('span', { class: 'discover-chip', dataset: { catalogCacheState: catalogUsingCache ? 'local' : 'live' }, title: new Date(catalogFetchedAt).toLocaleString() }, `${tx('目录时间', 'Catalog time', 'Katalogstand')}: ${fmtTime(catalogFetchedAt)}${catalogUsingCache ? ` · ${tx('本地缓存', 'local cache', 'lokal gespeichert')}` : ''}`) : null, ]), ]); } function modelTags(model) { const data = cachedModelData(model); if (!data.cleanTags) data.cleanTags = [...new Set(data.tags.map(tag => discoverCleanText(tag, 50)).filter(Boolean))]; return data.cleanTags; } function currentTagIndex(source = catalog) { const signature = dataCacheSignature(); if (source === catalog && tagIndexCache.signature === signature) return tagIndexCache.value; const index = new Map(); for (const model of source) { for (const tag of modelTags(model)) { if (!index.has(tag)) index.set(tag, { tag, count: 0, models: [] }); const item = index.get(tag); item.count += 1; item.models.push(model); } } if (source === catalog) tagIndexCache = { signature, value: index }; return index; } function tagCatalogBreakdown() { const signature = dataCacheSignature(); if (tagCatalogBreakdownCache.signature === signature) return tagCatalogBreakdownCache.value; const gamingIndex = currentTagIndex(catalog.filter(model => model.isGaming)); const gamingThemes = [...gamingIndex.values()].sort((a, b) => b.count - a.count).slice(0, 24); const countries = new Map(); const genders = new Map(); let unknownCountry = 0; let unknownAge = 0; for (const model of catalog) { if (model.country) countries.set(model.country, (countries.get(model.country) || 0) + 1); else unknownCountry += 1; if (model.gender && model.gender !== 'unknown') genders.set(model.gender, (genders.get(model.gender) || 0) + 1); if (!model.age) unknownAge += 1; } const value = { gamingThemes, countries, genders, unknownCountry, unknownAge }; tagCatalogBreakdownCache = { signature, value }; return value; } async function updateCatalogObservations(reset) { const now = Date.now(); const index = new Map(currentTagIndex()); const next = { ...state.tagObservations }; for (const [tag, item] of index) { const old = next[tag] || {}; next[tag] = { count: item.count, previousCount: reset ? Math.max(0, Number(old.count) || 0) : Math.max(0, Number(old.previousCount) || 0), observedAt: now, }; } const retained = Object.entries(next).sort((left, right) => Number(right[1].observedAt || 0) - Number(left[1].observedAt || 0)).slice(0, 400); state.tagObservations = Object.fromEntries(retained); const observedModels = []; const discoveredEvents = []; for (const model of catalog) { const old = state.seenModels[model.id] || {}; observedModels.push({ id: model.id, currentShow: model.currentShow, viewerCount: model.viewers, tags: model.tags || [], subject: model.subject || '', imageUrl: model.imageUrl || '', country: model.country || '', location: model.location || '', gender: model.gender || 'unknown', age: model.age || 0, followers: model.followers || 0, secondsOnline: model.secondsOnline || 0, isHd: !!model.isHd, isNew: !!model.isNew, isGaming: !!model.isGaming, source: 'roomgrid-discover', observedAt: now, }); if (!old.firstSeenAt) discoveredEvents.push({ type: 'discovered', id: model.id, tags: model.tags || [], status: model.currentShow || '' }); state.seenModels[model.id] = { firstSeenAt: Math.max(0, Number(old.firstSeenAt) || now), lastSeenAt: now, }; } const seenRetained = Object.entries(state.seenModels).sort((left, right) => Number(right[1].lastSeenAt || 0) - Number(left[1].lastSeenAt || 0)).slice(0, 1200); state.seenModels = Object.fromEntries(seenRetained); state.lastCatalogVisitAt = now; observationsRevision += 1; // Catalog ingestion is one logical change, not thousands of full-page // invalidations and individual IndexedDB transactions. await analytics.observeModelsBatch(observedModels); await analytics.recordEventsBatch(discoveredEvents); analytics.flush?.().catch?.(() => {}); persistDiscoverState(); } function definitionForTags(tags, mode) { const definition = discoverCleanDefinition(discoverDefaultState(), true); definition.sortPrimary = state.sortPrimary; definition.sortSecondary = state.sortSecondary; const text = discoverCleanTagList(tags, 20).join(', '); if (mode === 'or') definition.tagsAny = text; else if (mode === 'not') definition.tagsNot = text; else definition.tagsAll = text; return definition; } function applyTagSelection(tags = state.tagSelection, mode = state.tagSelectionMode) { tags = discoverCleanTagList(tags, 20); if (!tags.length) return; Object.assign(state, definitionForTags(tags, mode), { page: 1, activeProfileId: '', activeFeedId: '' }); tags.forEach(recordTagVisit); setView('discover'); } function saveTagFeed(tags, mode, preferredName = '') { tags = discoverCleanTagList(tags, 20); if (!tags.length || state.feeds.length >= 50) { toast(tx('无法保存更多动态', 'No more feeds can be saved', 'Es können keine weiteren Feeds gespeichert werden')); return; } const fallback = `${mode === 'or' ? 'ODER' : mode === 'not' ? 'NICHT' : 'UND'} · ${tags.join(' + ')}`.slice(0, 60); const name = discoverCleanText(preferredName || fallback, 60); const item = { id: newSavedId('feed'), name, createdAt: Date.now(), definition: definitionForTags(tags, mode) }; state.feeds.push(item); state.activeFeedId = item.id; persistDiscoverState(); toast(tx('标签动态已保存', 'Tag feed saved', 'Tag-Feed gespeichert')); } function toggleTagList(key, tag, maximum) { tag = discoverCleanText(tag, 50).toLocaleLowerCase(); const list = new Set(state[key]); if (list.has(tag)) list.delete(tag); else list.add(tag); state[key] = [...list].slice(-maximum); persistDiscoverState(); renderCurrentView(); } function automaticTagArtworkPreset(tag) { const value = String(tag || '').toLocaleLowerCase(); if (/(?:music|musica|musik|sing|song|singer|dj)/u.test(value)) return 'music'; if (/(?:gaming|gamer|game|videogame|playstation|xbox)/u.test(value)) return 'gaming'; if (/(?:new|newbie|fresh|firstday)/u.test(value)) return 'new'; if (/(?:world|international|country|german|spanish|latina|brazil|europe|asian)/u.test(value)) return 'world'; if (/(?:dance|dancing|fitness|sport|yoga)/u.test(value)) return 'dance'; if (/(?:love|favorite|romantic|heart|couple)/u.test(value)) return 'favorite'; return ''; } function resolvedTagArtwork(tag) { const saved = state.tagArtwork?.[tag]; if (saved?.preset === 'none') return null; if (saved?.dataUrl) return saved; const preset = saved?.preset || automaticTagArtworkPreset(tag); return preset ? { preset } : null; } function tagArtworkNode(tag, large = false, forcedPreset = '') { const artwork = forcedPreset ? { preset: forcedPreset } : resolvedTagArtwork(tag); if (!artwork) return null; if (artwork.dataUrl) return $('img', { class: `tag-artwork custom${large ? ' large' : ''}`, src: artwork.dataUrl, alt: '', ariaHidden: 'true' }); const preset = CAMCLIENT_TAG_ART_PRESETS.find(item => item.id === artwork.preset); if (!preset) return null; return $('span', { class: `tag-artwork sprite${large ? ' large' : ''}`, ariaHidden: 'true', style: { backgroundSize: '300% 200%', backgroundPosition: `${preset.x} ${preset.y}`, } }); } function tagArtworkDataFromFile(file) { return new Promise((resolve, reject) => { if (!file || !/^image\/(?:png|jpe?g|webp|svg\+xml)$/iu.test(file.type) || file.size > 1150000) { reject(new Error(tx('请选择小于 1.1 MB 的 PNG、JPEG、WebP 或安全 SVG。', 'Choose a PNG, JPEG, WebP or safe SVG smaller than 1.1 MB.', 'Wähle PNG, JPEG, WebP oder ein sicheres SVG unter 1,1 MB.'))); return; } const reader = new FileReader(); reader.onerror = () => reject(new Error(tx('无法读取图像。', 'The image could not be read.', 'Das Bild konnte nicht gelesen werden.'))); reader.onload = () => { const safeSource = sanitizeBrandLogoDataUrl(String(reader.result || '')); if (!safeSource) { reject(new Error(tx('图像格式不安全或不受支持。', 'The image format is unsafe or unsupported.', 'Das Bildformat ist unsicher oder wird nicht unterstützt.'))); return; } const image = new Image(); image.onerror = () => reject(new Error(tx('无法解码图像。', 'The image could not be decoded.', 'Das Bild konnte nicht dekodiert werden.'))); image.onload = () => { const canvas = document.createElement('canvas'); canvas.width = 144; canvas.height = 144; const context = canvas.getContext('2d', { alpha: true }); context.clearRect(0, 0, 144, 144); const scale = Math.min(144 / Math.max(1, image.naturalWidth), 144 / Math.max(1, image.naturalHeight)); const width = Math.max(1, Math.round(image.naturalWidth * scale)); const height = Math.max(1, Math.round(image.naturalHeight * scale)); context.drawImage(image, Math.round((144 - width) / 2), Math.round((144 - height) / 2), width, height); let dataUrl = canvas.toDataURL('image/png'); if (dataUrl.length > 180000) dataUrl = canvas.toDataURL('image/webp', .9); dataUrl = dataUrl.length <= 180000 ? sanitizeBrandLogoDataUrl(dataUrl) : ''; if (!dataUrl) reject(new Error(tx('压缩后的图像仍然太大。', 'The compressed image is still too large.', 'Das komprimierte Bild ist noch zu groß.'))); else resolve(dataUrl); }; image.src = safeSource; }; reader.readAsDataURL(file); }); } function openTagArtworkEditor(tag) { tag = discoverCleanText(tag, 50).toLocaleLowerCase(); if (!tag) return; const close = () => { modal.remove(); document.removeEventListener('keydown', onKey); }; const applyArtwork = artwork => { state.tagArtwork = { ...(state.tagArtwork || {}), [tag]: artwork }; persistDiscoverState(); close(); renderCurrentView(); }; const removeOverride = () => { const next = { ...(state.tagArtwork || {}) }; delete next[tag]; state.tagArtwork = next; persistDiscoverState(); close(); renderCurrentView(); }; const picker = $('input', { type: 'file', accept: 'image/png,image/jpeg,image/webp,image/svg+xml', style: { display: 'none' } }); picker.addEventListener('change', async () => { try { applyArtwork({ dataUrl: await tagArtworkDataFromFile(picker.files?.[0]) }); } catch (error) { alert(error?.message || String(error)); } }); const choices = $('div', { class: 'tag-artwork-gallery' }, CAMCLIENT_TAG_ART_PRESETS.map(preset => $('button', { class: `tag-artwork-choice${state.tagArtwork?.[tag]?.preset === preset.id ? ' active' : ''}`, type: 'button', onclick: () => applyArtwork({ preset: preset.id }), }, [tagArtworkNode('', false, preset.id), $('span', {}, preset.label)]))); const dialog = $('section', { class: 'tag-artwork-dialog', role: 'dialog', ariaModal: 'true', ariaLabel: tx('标签图像编辑器', 'Tag artwork editor', 'Tag-Bild bearbeiten') }, [ $('div', { class: 'tag-artwork-dialog-head' }, [$('div', {}, [$('h2', {}, `#${tag}`), $('p', {}, tx('选择 Camclient 示例图或上传自己的透明图像。', 'Choose a Camclient example or upload your own transparent image.', 'Wähle ein Camclient-Beispiel oder lade ein eigenes transparentes Bild hoch.'))]), $('button', { class: 'discover-btn', type: 'button', onclick: close }, '×')]), choices, $('p', {}, tx('PNG、WebP 和安全 SVG 支持透明背景。图像在本地缩放到 144 × 144 像素并与设置一起保存。', 'PNG, WebP and safe SVG support transparency. Images are resized locally to 144 × 144 pixels and saved with your settings.', 'PNG, WebP und sichere SVGs unterstützen Transparenz. Bilder werden lokal auf 144 × 144 Pixel verkleinert und zusammen mit deinen Einstellungen gespeichert.')), $('div', { class: 'tag-artwork-dialog-actions' }, [ $('button', { class: 'discover-btn primary', type: 'button', onclick: () => picker.click() }, tx('上传自己的图像', 'Upload own image', 'Eigenes Bild hochladen')), $('button', { class: 'discover-btn', type: 'button', onclick: removeOverride }, tx('自动示例', 'Automatic example', 'Automatisches Beispiel')), $('button', { class: 'discover-btn danger', type: 'button', onclick: () => applyArtwork({ preset: 'none' }) }, tx('不显示图像', 'Show no image', 'Kein Bild anzeigen')), ]), picker, ]); const modal = $('div', { class: 'tag-artwork-modal', onclick: event => { if (event.target === modal) close(); } }, dialog); const onKey = event => { if (event.key === 'Escape') close(); }; document.addEventListener('keydown', onKey); document.body.appendChild(modal); } function createTagPill(item, options = {}) { const tag = item.tag; const observation = state.tagObservations[tag] || {}; const delta = Number(observation.previousCount) > 0 ? Number(observation.count ?? item.count) - Number(observation.previousCount) : 0; const selected = state.tagSelection.includes(tag); const blocked = state.tagBlocked.includes(tag); const main = $('button', { class: 'tag-icon tag-main', type: 'button', ariaPressed: String(selected), title: tx('选择标签', 'Select tag', 'Tag auswählen'), onclick: () => { const set = new Set(state.tagSelection); if (!set.has(tag) && set.size >= 20) { toast(tx('最多组合20个标签。请先移除一个标签。', 'Combine up to 20 tags. Remove one before adding another.', 'Du kannst bis zu 20 Tags kombinieren. Entferne zuerst einen Tag, bevor du einen weiteren hinzufügst.')); return; } if (set.has(tag)) set.delete(tag); else set.add(tag); state.tagSelection = [...set].slice(-20); state.tagResultPage = 1; recordTagVisit(tag); renderCurrentView(); if (state.tagSelection.includes(tag)) requestAnimationFrame(() => main.querySelector('[data-tag-results]')?.closest('.tag-section')?.scrollIntoView({ block: 'start', behavior: 'auto' })); } }, [tagArtworkNode(tag), $('span', {}, `#${tag}`)]); main.style.flex = '1 1 auto'; const favoriteKey = options.theme ? 'themeFavorites' : 'tagFavorites'; const favorite = state[favoriteKey].includes(tag); return $('div', { class: `tag-pill${selected ? ' selected' : ''}${blocked ? ' blocked' : ''}` }, [ main, $('small', { class: delta > 0 ? 'trend-up' : delta < 0 ? 'trend-down' : '', title: tx('已加载目录中的模型数;变化仅在本机观察。', 'Models in the loaded catalog; changes are observed locally only.', 'Models im geladenen Katalog; Änderungen sind nur lokal beobachtet.') }, `${item.count}${delta ? ` · ${delta > 0 ? '+' : ''}${delta}` : ''}`), $('button', { class: 'tag-icon tag-artwork-edit', type: 'button', title: tx('为标签选择图像', 'Choose artwork for tag', 'Bild für Tag wählen'), onclick: () => openTagArtworkEditor(tag) }, '🖼'), $('button', { class: `tag-icon${favorite ? ' active' : ''}`, type: 'button', ariaPressed: String(favorite), title: tx('标签收藏', 'Tag favorite', 'Tag-Favorit'), onclick: () => toggleTagList(favoriteKey, tag, options.theme ? 60 : 100) }, favorite ? '★' : '☆'), options.noBlock ? null : $('button', { class: 'tag-icon', type: 'button', title: blocked ? tx('恢复标签建议', 'Restore tag suggestion', 'Tag-Vorschlag wieder einblenden') : tx('隐藏标签建议;不是模型过滤器', 'Hide tag suggestion; not a model filter', 'Tag-Vorschlag ausblenden; kein Model-Filter'), onclick: () => toggleTagList('tagBlocked', tag, 100) }, blocked ? '↩' : '⊘'), ]); } function tagSection(title, description, content) { return $('section', { class: 'tag-section' }, [$('h2', {}, title), $('p', {}, description), content]); } function createTagGroup() { const tags = discoverCleanTagList(state.tagSelection, 20); if (!tags.length) { toast(tx('先选择标签', 'Select tags first', 'Wähle zuerst Tags aus')); return; } if (state.tagGroups.length >= 30) { toast(tx('最多保存30个标签组。请先明确删除一个组;现有组不会被覆盖。', 'Up to 30 tag groups can be saved. Delete a group first; existing groups are never overwritten.', 'Es können 30 Tag-Gruppen gespeichert werden. Lösche zuerst gezielt eine Gruppe; bestehende Gruppen werden nicht überschrieben.')); return; } const name = discoverCleanText(prompt(tx('标签组名称', 'Tag-group name', 'Name der Tag-Gruppe')) || '', 60); if (!name) return; state.tagGroups.push({ id: newSavedId('tags'), name, mode: state.tagSelectionMode, tags, createdAt: Date.now() }); persistDiscoverState(); renderCurrentView(); toast(tx('标签组已保存', 'Tag group saved', 'Tag-Gruppe gespeichert')); } function createGamingWorkspace() { const models = catalog.filter(model => model.isGaming); if (!models.length) { toast(tx('当前目录中没有游戏直播', 'No gaming rooms in the current catalog', 'Im aktuellen Katalog gibt es keine Gaming-Räume')); return; } const name = discoverCleanText(prompt(tx('工作区名称', 'Workspace name', 'Name des Arbeitsbereichs'), `Gaming ${new Date().toLocaleDateString()}`) || '', 60); if (!name) return; const groupId = store.addGroup(name); store.syncRoomsToGroup(models.map(model => model.id), groupId); toast(tx(`${models.length} 个房间已加入工作区`, `${models.length} rooms added to workspace`, `${models.length} Räume wurden dem Arbeitsbereich hinzugefügt`, `${models.length} salas añadidas al espacio de trabajo`, `${models.length} salas adicionadas ao espaço de trabalho`)); } function renderTagExplorer() { previewScheduler.clear(); // Native toggle events are queued. An immediate option change must not // rebuild the view with a stale closed state before that event arrives. const expandedNow = new Set(state.tagExpanded); main.querySelectorAll('[data-tag-disclosure]').forEach(section => { if (section.open) expandedNow.add(section.dataset.tagDisclosure); else expandedNow.delete(section.dataset.tagDisclosure); }); state.tagExpanded = [...expandedNow]; const disclosure = (key, title, contents, className = '') => { const section = $('details', { class: `tag-disclosure ${className}`, open: state.tagExpanded.includes(key), dataset: { tagDisclosure: key } }, [$('summary', {}, title), ...contents]); section.addEventListener('toggle', () => { if (!section.isConnected || state.view !== 'tags') return; const expanded = new Set(state.tagExpanded); if (section.open) expanded.add(key); else expanded.delete(key); state.tagExpanded = [...expanded]; persistDiscoverState(true); }); return section; }; const index = new Map(currentTagIndex()); const search = state.tagSearch.replace(/^#+/u, '').trim().toLocaleLowerCase(); const remembered = [...state.tagFavorites, ...state.tagBlocked, ...state.tagSelection, ...state.tagHistory.map(item => item.tag)]; remembered.forEach(tag => { if (!index.has(tag)) index.set(tag, { tag, count: 0 }); }); const blocked = new Set(state.tagBlocked); const favorite = new Set(state.tagFavorites); const recent = new Set(state.tagHistory.map(item => item.tag)); const ranked = [...index.values()].filter(item => (!search || item.tag.includes(search)) && ( state.tagBrowseScope === 'blocked' ? blocked.has(item.tag) : state.tagBrowseScope === 'favorites' ? favorite.has(item.tag) : state.tagBrowseScope === 'recent' ? recent.has(item.tag) : !blocked.has(item.tag) )).sort((left, right) => { if (state.tagBrowseSort === 'name') return left.tag.localeCompare(right.tag); if (state.tagBrowseSort === 'name_desc') return right.tag.localeCompare(left.tag); if (state.tagBrowseSort === 'trend') { const delta = item => { const observation = state.tagObservations[item.tag]; return Number(observation?.previousCount) > 0 ? Number(observation.count) - Number(observation.previousCount) : 0; }; const difference = delta(right) - delta(left); if (difference) return difference; } return right.count - left.count || left.tag.localeCompare(right.tag); }); const tagPages = Math.max(1, Math.ceil(ranked.length / 60)); state.tagBrowsePage = Math.min(state.tagBrowsePage, tagPages); const visibleTags = ranked.slice((state.tagBrowsePage - 1) * 60, state.tagBrowsePage * 60); const selectedDefinition = definitionForTags(state.tagSelection, state.tagSelectionMode); const tagModels = state.tagSelection.length ? selectedModels(selectedDefinition) : []; const resultPages = Math.max(1, Math.ceil(tagModels.length / 12)); state.tagResultPage = Math.min(state.tagResultPage, resultPages); const relatedCounts = new Map(); if (state.tagSelection.length) { for (const model of tagModels) for (const tag of modelTags(model)) { if (!state.tagSelection.includes(tag)) relatedCounts.set(tag, (relatedCounts.get(tag) || 0) + 1); } } const related = [...relatedCounts].filter(([tag]) => !blocked.has(tag)).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 20); const historyCounts = new Map(); state.tagHistory.forEach(item => historyCounts.set(item.tag, (historyCounts.get(item.tag) || 0) + 1)); const frequent = [...historyCounts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count).slice(0, 16); const { gamingThemes, countries, genders, unknownCountry, unknownAge } = tagCatalogBreakdown(); const toolbarMode = selectControl(state.tagSelectionMode, [ ['and', tx('必须包含所有标签', 'Must contain every tag', 'Alle ausgewählten Tags enthalten (UND)')], ['or', tx('包含至少一个标签', 'Contains at least one tag', 'Mindestens einen Tag enthalten (ODER)')], ['not', tx('不含任何选中标签', 'Contains none of these tags', 'Keinen ausgewählten Tag enthalten (Ausschließen)')], ], value => { state.tagSelectionMode = value; state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); }); toolbarMode.dataset.tagCombination = '1'; const tagSort = selectControl(state.sortPrimary, [ ['viewers_desc', tx('最多观众', 'Most viewers', 'Meiste Zuschauer')], ['viewers_asc', tx('最少观众', 'Fewest viewers', 'Wenigste Zuschauer')], ['online_desc', tx('最长在线时长', 'Longest online duration', 'Längste Online-Dauer')], ['followers_desc', tx('最多粉丝', 'Most followers', 'Meiste Follower')], ['name_asc', tx('名称 A–Z', 'Name A–Z', 'Name A–Z')], ['new_desc', tx('新主播优先', 'New first', 'Neue zuerst')], ['watch_desc', tx('最多本机观看时长', 'Most local watch time', 'Meiste lokale Sehzeit')], ['last_desc', tx('最近观看', 'Last watched', 'Zuletzt besucht')], ], value => { state.sortPrimary = value; state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); }); tagSort.dataset.tagModelSort = '1'; const tagSearch = textControl(state.tagSearch, tx('例如 music 或 gaming', 'For example music or gaming', 'Zum Beispiel music oder gaming'), value => { if (!tagSearch.isConnected || state.view !== 'tags') return; state.tagSearch = discoverCleanText(value, 80).toLocaleLowerCase(); state.tagBrowsePage = 1; persistDiscoverState(); renderCurrentView(); }); tagSearch.dataset.tagExplorerSearch = '1'; tagSearch.autocomplete = 'off'; const browseSort = selectControl(state.tagBrowseSort, [ ['count', tx('最多模型', 'Most models', 'Meiste Models')], ['name', 'A–Z'], ['name_desc', 'Z–A'], ['trend', tx('本机观察变化', 'Locally observed change', 'Lokal beobachtete Veränderung')], ], value => { state.tagBrowseSort = value; state.tagBrowsePage = 1; persistDiscoverState(); renderCurrentView(); }); browseSort.dataset.tagBrowseSort = '1'; const scopes = [['all', tx('所有建议', 'All suggestions', 'Alle Vorschläge')], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['recent', tx('最近使用', 'Recently used', 'Zuletzt verwendet')], ['blocked', tx('隐藏', 'Hidden', 'Ausgeblendet')]]; const browsing = $('section', { class: 'tag-section', dataset: { tagBrowser: '1' } }, [ $('h2', {}, tx('1. 查找标签', '1. Find tags', '1. Tags finden')), $('p', {}, tx('搜索只缩小标签列表;点击标签才选择模型。星标是快捷收藏。', 'Search narrows the tag list only; click a tag to select models. Stars save shortcuts.', 'Die Suche durchsucht nur die Tag-Liste. Klicke einen Tag an, um passende Models zu sehen. Mit dem Stern merkst du dir einen Tag.')), $('div', { class: 'tag-toolbar' }, [filterField(tx('标签搜索', 'Tag search', 'Tag-Suche'), tagSearch)]), disclosure('browse-options', tx('标签列表选项', 'Tag list options', 'Tag-Liste anpassen') + ` · ${scopes.find(([scope]) => scope === state.tagBrowseScope)?.[1] || ''}`, [ filterField(tx('标签排序', 'Tag sorting', 'Tags sortieren'), browseSort), $('div', { class: 'tag-scope-bar', role: 'group', ariaLabel: tx('标签列表', 'Tag list', 'Tag-Liste') }, scopes.map(([scope, label]) => $('button', { class: `discover-btn${state.tagBrowseScope === scope ? ' primary' : ''}`, type: 'button', ariaPressed: String(state.tagBrowseScope === scope), dataset: { tagScope: scope }, onclick: () => { state.tagBrowseScope = scope; state.tagBrowsePage = 1; persistDiscoverState(); renderCurrentView(); } }, label))), ]), search ? $('div', { class: 'tag-suggestions', dataset: { tagSuggestions: '1' } }, [ $('p', {}, tx('点击建议即可选择:', 'Select a suggestion:', 'Vorschlag anklicken zum Auswählen:')), ...ranked.slice().sort((a, b) => Number(b.tag.startsWith(search)) - Number(a.tag.startsWith(search)) || b.count - a.count).slice(0, 6).map(item => createTagPill(item)), ]) : null, $('p', { class: 'tag-list-status', role: 'status' }, `${formatCount(ranked.length)} ${tx('标签', 'tags', 'Tags')} · ${tx('数字是已加载目录中的模型数', 'Counts refer to models in the loaded catalog', 'Zahlen = Models im geladenen Katalog')}`), ]); const toolbar = $('section', { class: 'tag-section tag-selection-panel', dataset: { tagSelectionPanel: '1' } }, [ $('h2', {}, tx('2. 组合标签', '2. Combine tags', '2. Auswahl kombinieren')), $('div', { class: 'tag-cloud tag-selected-chips' }, state.tagSelection.length ? state.tagSelection.map(tag => $('button', { type: 'button', class: 'discover-btn primary', ariaLabel: `${tx('移除', 'Remove', 'Entfernen')}: #${tag}`, dataset: { tagSelected: tag }, onclick: () => { state.tagSelection = state.tagSelection.filter(value => value !== tag); state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); } }, `#${tag} ×`)) : [$('p', {}, tx('还未选择标签。点击左侧标签开始。', 'No tags selected. Choose a tag to begin.', 'Noch kein Tag ausgewählt. Klicke zum Start einen Tag an.'))]), disclosure('combination', tx('组合选择', 'Combine selection', 'Auswahl kombinieren') + ` · ${state.tagSelectionMode === 'or' ? 'ODER' : state.tagSelectionMode === 'not' ? tx('排除', 'Exclude', 'Ausschließen') : 'UND'}`, [ filterField(tx('匹配规则', 'Matching rule', 'Welche Models möchtest du sehen?'), toolbarMode), $('p', { class: 'tag-rule-explanation' }, state.tagSelectionMode === 'or' ? tx('至少一个选中标签匹配。适合更广泛地发现模型。', 'At least one selected tag must match. Useful for broader discovery.', 'Mindestens einer der ausgewählten Tags muss passen. So entdeckst du eine größere Auswahl.') : state.tagSelectionMode === 'not' ? tx('模型不能含任何选中标签。收藏或隐藏标签不会过滤模型。', 'Models must have none of the selected tags. Favoriting or hiding suggestions does not filter models.', 'Models mit einem der ausgewählten Tags werden ausgeschlossen. Favorisieren oder Ausblenden von Vorschlägen filtert keine Models.') : tx('所有选中标签必须同时匹配。没有结果时尝试“至少一个”。', 'Every selected tag must match together. If there are no results, try “at least one”.', 'Alle ausgewählten Tags müssen gleichzeitig passen. Bei zu wenigen Treffern probiere „Mindestens einen Tag“.')), ]), disclosure('selection-tools', tx('保存或继续使用', 'Save or reuse selection', 'Auswahl speichern oder weiterverwenden'), [ $('div', { class: 'tag-toolbar' }, [ $('button', { class: 'discover-btn primary', type: 'button', disabled: !state.tagSelection.length, dataset: { tagApply: '1' }, onclick: () => applyTagSelection() }, tx('在发现中打开所有结果', 'Open all matches in Discover', 'Alle Treffer in Entdecken öffnen')), $('button', { class: 'discover-btn', type: 'button', disabled: !state.tagSelection.length, onclick: createTagGroup }, tx('保存标签组', 'Save tag group', 'Tag-Gruppe speichern')), $('button', { class: 'discover-btn', type: 'button', disabled: !state.tagSelection.length, onclick: () => saveTagFeed(state.tagSelection, state.tagSelectionMode) }, tx('保存标签动态', 'Save tag feed', 'Tag-Feed speichern')), ]), $('p', {}, tx('标签组保存组合;动态可在主页再次使用。图片按钮仅改变外观。', 'Groups save combinations; feeds can be reused on the homepage. Image buttons only change appearance.', 'Tag-Gruppen speichern deine Kombination. Ein Tag-Feed ist ein gespeicherter Filter für die Startseite. Der Bild-Knopf ändert nur das Tag-Bild.')), ]), $('button', { class: 'discover-btn', type: 'button', disabled: !state.tagSelection.length, onclick: () => { state.tagSelection = []; state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); } }, tx('清除选择', 'Clear selection', 'Auswahl leeren')), ]); const tagCloud = $('div', { class: 'tag-cloud' }, visibleTags.map(item => createTagPill(item))); if (!visibleTags.length) tagCloud.appendChild($('div', { class: 'discover-empty' }, tx('没有匹配标签。请换个搜索词或列表。', 'No matching tags. Try another search or list.', 'Keine passenden Tags. Probiere einen anderen Suchbegriff oder eine andere Liste.'))); const pagination = (page, pages, key, datasetKey) => $('div', { class: 'tag-pagination', dataset: { [datasetKey]: '1' } }, [ $('button', { class: 'discover-btn', type: 'button', disabled: page <= 1, onclick: () => { state[key] = page - 1; persistDiscoverState(); renderCurrentView(); } }, tx('上一页', 'Previous', 'Zurück')), $('span', { role: 'status' }, `${tx('页', 'Page', 'Seite')} ${page} / ${pages}`), $('button', { class: 'discover-btn', type: 'button', disabled: page >= pages, onclick: () => { state[key] = page + 1; persistDiscoverState(); renderCurrentView(); } }, tx('下一页', 'Next', 'Weiter')), ]); browsing.append(tagCloud, pagination(state.tagBrowsePage, tagPages, 'tagBrowsePage', 'tagBrowsePagination')); const groupList = $('div', { class: 'tag-groups' }, state.tagGroups.length ? state.tagGroups.map(group => $('div', { class: 'tag-group' }, [ $('strong', {}, rawUiText(group.name)), $('span', { class: 'discover-chip' }, group.mode === 'or' ? tx('任一', 'OR', 'ODER') : group.mode === 'not' ? tx('排除', 'EXCLUDE', 'Ausschließen') : tx('全部', 'AND', 'UND')), ...group.tags.slice(0, 5).map(tag => $('span', { class: 'discover-tag' }, `#${tag}`)), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.tagSelection = group.tags.slice(); state.tagSelectionMode = group.mode; state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); } }, tx('在此加载', 'Load here', 'Hier bearbeiten')), $('button', { class: 'discover-btn primary', type: 'button', onclick: () => applyTagSelection(group.tags, group.mode) }, tx('应用', 'Apply', 'Anwenden')), $('button', { class: 'discover-btn danger', type: 'button', onclick: () => { state.tagGroups = state.tagGroups.filter(item => item.id !== group.id); persistDiscoverState(); renderCurrentView(); } }, tx('删除', 'Delete', 'Löschen')), ])) : [$('div', { class: 'discover-empty' }, tx('尚未保存标签组。', 'No tag groups saved yet.', 'Noch keine Tag-Gruppen gespeichert.'))]); const favoriteItems = state.tagFavorites.map(tag => index.get(tag) || { tag, count: 0 }); const recentItems = [...state.tagHistory].reverse().filter((item, position, all) => all.findIndex(entry => entry.tag === item.tag) === position).slice(0, 16).map(item => index.get(item.tag) || { tag: item.tag, count: 0 }); const categoryButtons = $('div', { class: 'tag-cloud' }, [ ...[...genders].map(([gender, count]) => ({ tag: gender, count })).map(item => $('button', { class: 'tag-pill', type: 'button', onclick: () => { state.genders = [item.tag]; setView('discover'); } }, `${({ f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: 'Trans' })[item.tag] || item.tag} · ${item.count}`)), ...[...countries].sort((a, b) => b[1] - a[1]).slice(0, 24).map(([country, count]) => $('button', { class: 'tag-pill', type: 'button', onclick: () => { const values = new Set(parseTerms(state.place)); values.add(country.toLocaleLowerCase()); state.place = [...values].join(', '); setView('discover'); } }, `${country} · ${count}`)), $('button', { class: 'tag-pill', type: 'button', onclick: () => { state.minAge = 18; state.maxAge = 24; setView('discover'); } }, '18–24'), $('button', { class: 'tag-pill', type: 'button', onclick: () => { state.minAge = 25; state.maxAge = 34; setView('discover'); } }, '25–34'), $('span', { class: 'discover-chip' }, `${tx('未知地区', 'Unknown region', 'Region unbekannt')}: ${unknownCountry}`), $('span', { class: 'discover-chip' }, `${tx('未知年龄', 'Unknown age', 'Alter unbekannt')}: ${unknownAge}`), ]); const specialActions = $('div', { class: 'tag-toolbar' }, [ $('button', { class: 'discover-btn primary', type: 'button', onclick: () => { state.quick = 'new_since'; setView('discover'); } }, `${tx('本次新发现', 'New since last visit', 'Seit letztem Besuch neu')} · ${catalog.filter(model => Number(state.seenModels[model.id]?.firstSeenAt) > previousCatalogVisitAt).length}`), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.quick = 'new'; setView('discover'); } }, `${tx('公开标记“新”', 'Public “New”', 'Öffentlich „Neu“')} · ${catalog.filter(model => model.isNew).length}`), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.quick = 'watchlist'; setView('discover'); } }, `${tx('观察列表', 'Watchlist', 'Beobachtungsliste')} · ${state.temporaryWatchlist.length}`), $('button', { class: 'discover-btn', type: 'button', onclick: () => { const item = { id: newSavedId('feed'), name: tx('我的新直播', 'My new cams', 'Meine neuen Cams'), createdAt: Date.now(), definition: { ...discoverCleanDefinition(discoverDefaultState(), true), quick: 'new_since' } }; if (state.feeds.length < 50) { state.feeds.push(item); state.activeFeedId = item.id; persistDiscoverState(); toast(tx('新直播动态已保存', 'New-cams feed saved', 'Neue-Cams-Feed gespeichert')); } } }, tx('保存“我的新直播”动态', 'Save “My new cams” feed', 'Feed „Meine neuen Cams“ speichern')), $('button', { class: 'discover-btn', type: 'button', onclick: createGamingWorkspace }, tx('创建游戏工作区', 'Create gaming workspace', 'Gaming-Arbeitsbereich erstellen')), ]); const artworkExamples = $('div', { class: 'tag-artwork-gallery' }, CAMCLIENT_TAG_ART_PRESETS.map(preset => $('div', { class: 'tag-artwork-choice' }, [tagArtworkNode('', false, preset.id), $('span', {}, preset.label)]))); const resultGrid = $('div', { class: 'discover-grid', dataset: { tagResults: '1', tagMatchCount: String(tagModels.length) } }); const contextIds = tagModels.map(item => item.id); if (tagModels.length) tagModels.slice((state.tagResultPage - 1) * 12, state.tagResultPage * 12).forEach(model => resultGrid.appendChild(createModelCard(model, true, contextIds))); else resultGrid.appendChild($('div', { class: 'discover-empty' }, state.tagSelection.length ? tx('当前组合没有匹配结果。', 'No matches for this combination.', 'Für diese Kombination gibt es keine Treffer.') : tx('选择标签即可在此预览匹配房间。', 'Select tags to preview matching rooms here.', 'Wähle Tags, um hier passende Räume zu sehen.'))); const fold = (key, title, description, contents) => { const section = $('details', { class: 'tag-section tag-extra', open: state.tagExpanded.includes(key), dataset: { tagExtra: key } }, [ $('summary', {}, title), $('p', {}, description), contents, ]); section.addEventListener('toggle', () => { if (!section.isConnected || state.view !== 'tags') return; const expanded = new Set(state.tagExpanded); if (section.open) expanded.add(key); else expanded.delete(key); state.tagExpanded = [...expanded]; persistDiscoverState(true); }); return section; }; const catalogStatus = $('div', { class: `discover-status${catalogError ? ' is-error' : ''}`, role: 'status', dataset: { tagCatalogStatus: '1' } }, [ $('span', {}, catalogError || (catalogIsFallback ? tx('公共目录不可用;显示本地已知模型。', 'Public catalog unavailable; showing locally known models.', 'Öffentlicher Katalog nicht verfügbar; gezeigt werden lokal bekannte Models.') : catalogUsingCache ? tx('显示上次目录;正在后台刷新。', 'Showing the previous catalog; refreshing in the background.', 'Vorheriger Katalog wird angezeigt und im Hintergrund aktualisiert.') : catalogLoading || catalogPrefetching || !catalogReachedEnd ? tx('目录仍在加载,计数可能增加。', 'Catalog is still loading; counts may increase.', 'Der Katalog lädt noch. Tag- und Trefferzahlen können zunehmen.') : tx('已加载目录已完整。', 'The loaded catalog is complete.', 'Der aktuelle Katalog ist vollständig geladen.'))), $('span', {}, `${formatCount(catalog.length)} ${tx('已加载模型', 'loaded models', 'geladene Models')}`), $('button', { class: 'discover-btn', type: 'button', disabled: catalogLoading || catalogPrefetching, onclick: refreshCompleteCatalog }, tx('刷新目录', 'Refresh catalog', 'Katalog aktualisieren')), ]); main.replaceChildren( createHero(tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer'), tx( '通过主题发现模型:查找标签,选择组合,直接查看匹配房间。', 'Discover models by topic: find tags, combine your selection, and open matching rooms directly.', 'Entdecke Models nach Themen: Suche Tags, kombiniere deine Auswahl und öffne direkt passende Räume.' )), disclosure('help', tx('它能做什么?', 'What can I do here?', 'Was kann ich hier machen?'), [ $('p', {}, tx('标签描述直播的主题。点击一个标签,下面立即显示带有该标签的房间。不需要再到发现页面。', 'Tags describe stream topics. Click a tag and matching rooms appear below immediately, without opening Discover.', 'Tags beschreiben Themen eines Streams. Klicke einen Tag an: Darunter erscheinen sofort die passenden Räume. Du musst dafür nicht erst Entdecken öffnen.')), $('p', {}, tx('选择多个标签可缩小或扩大结果。星标保存快捷方式;图片只改变外观。', 'Combine tags to narrow or broaden results. Stars save shortcuts; artwork only changes appearance.', 'Mehrere Tags kannst du kombinieren: „Alle“ grenzt ein, „Mindestens einen“ erweitert die Auswahl. Der Stern merkt einen Tag, ein Bild verändert nur dessen Aussehen.')), ], 'tag-help'), catalogStatus, $('div', { class: 'tag-result-summary', dataset: { tagResultSummary: '1' }, role: 'region', ariaLabel: tx('选择和结果', 'Selection and results', 'Auswahl und Ergebnisse') }, [ $('strong', {}, state.tagSelection.length ? state.tagSelection.map(tag => `#${tag}`).join(' · ') : tx('选择一个标签开始', 'Choose a tag to begin', 'Wähle einen Tag zum Starten')), state.tagSelection.length ? $('span', { class: 'discover-chip', dataset: { tagActiveRule: '1' } }, state.tagSelectionMode === 'or' ? tx('任一标签', 'Any tag', 'Mindestens ein Tag') : state.tagSelectionMode === 'not' ? tx('排除标签', 'Exclude tags', 'Tags ausschließen') : tx('所有标签', 'All tags', 'Alle Tags')) : null, $('span', { role: 'status' }, `${formatCount(tagModels.length)} ${tx('匹配房间', 'matching rooms', 'passende Räume')}`), $('button', { class: 'discover-btn primary', type: 'button', disabled: !state.tagSelection.length, dataset: { tagShowStreams: '1' }, onclick: () => resultGrid.closest('.tag-section')?.scrollIntoView({ block: 'start', behavior: 'auto' }) }, tx('查看直播', 'See streams', 'Streams ansehen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => browsing.scrollIntoView({ block: 'start', behavior: 'auto' }) }, tx('选择标签', 'Choose tags', 'Tags auswählen')), ]), $('div', { class: 'tag-layout' }, [ $('div', { class: 'tag-browser-column' }, [ browsing, fold('groups', tx('保存的标签组', 'Saved tag groups', 'Gespeicherte Tag-Gruppen'), tx('加载到这里继续编辑,或直接在发现中打开。', 'Load here to edit, or open directly in Discover.', 'Hier bearbeiten lädt eine Kombination in den Explorer. Anwenden öffnet sie direkt in Entdecken.'), groupList), fold('artwork', tx('标签图片与表情', 'Tag artwork and emoji', 'Tag-Bilder und Emojis'), tx('图片只是标记,不改变搜索。点击标签旁的图片按钮选择示例或上传透明图片。', 'Images are visual labels, not filters. Use the image button beside a tag to choose an example or upload a transparent image.', 'Bilder sind Erkennungshilfen, keine Filter. Über den Bild-Knopf neben einem Tag kannst du ein Beispiel oder dein eigenes transparentes Bild auswählen.'), artworkExamples), fold('history', tx('个人标签历史', 'Personal tag history', 'Persönliche Tag-Historie'), tx('仅此设备最近和经常使用的标签。', 'Tags used recently and frequently on this device only.', 'Zuletzt und häufig auf diesem Gerät verwendete Tags.'), $('div', {}, [$('div', { class: 'tag-cloud' }, recentItems.map(item => createTagPill(item))), $('div', { class: 'tag-cloud', style: { marginTop: '8px' } }, frequent.map(item => createTagPill(item)))])), fold('categories', tx('类别、年龄与地区', 'Categories, age, and regions', 'Kategorien, Alter und Regionen'), tx('这些快捷方式打开额外的模型过滤器,不是标签。', 'These shortcuts open additional model filters; they are not tags.', 'Diese zusätzlichen Schnellfilter öffnen Entdecken. Kategorien, Alter und Regionen sind keine Tags.'), categoryButtons), fold('gaming', tx('新内容与游戏', 'New and gaming', 'Neu und Gaming'), tx('更多发现方式和游戏主题。', 'More discovery shortcuts and gaming topics.', 'Weitere Entdecken-Schnellfilter und Gaming-Themen.'), $('div', {}, [specialActions, $('div', { class: 'tag-cloud' }, gamingThemes.map(item => createTagPill(item, { theme: true, noBlock: true })))])), ]), $('div', { class: 'tag-results-column' }, [ toolbar, tagSection(tx('选中标签的结果', 'Results for selected tags', 'Ergebnisse der gewählten Tags'), tx('3. 打开房间,或翻页查看所有匹配模型。', '3. Open a room, or browse every matching model using the pages.', '3. Öffne einen Raum oder blättere durch alle passenden Models.'), $('div', {}, [ $('div', { class: 'tag-toolbar' }, [$('strong', { role: 'status', dataset: { tagResultCount: '1' } }, `${formatCount(tagModels.length)} ${tx('匹配模型', 'matching models', 'passende Models')}`), filterField(tx('模型排序', 'Model sorting', 'Models sortieren'), tagSort)]), tagModels.length > 12 ? pagination(state.tagResultPage, resultPages, 'tagResultPage', 'tagResultPaginationTop') : null, resultGrid, pagination(state.tagResultPage, resultPages, 'tagResultPage', 'tagResultPagination'), state.tagSelection.length && !tagModels.length && state.tagSelectionMode === 'and' ? $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.tagSelectionMode = 'or'; state.tagResultPage = 1; persistDiscoverState(); renderCurrentView(); } }, tx('尝试至少一个标签', 'Try at least one tag', 'Mindestens einen Tag ausprobieren')) : null, ])), tagSection(tx('相关标签', 'Related tags', 'Verwandte Tags'), tx('这些标签也出现在匹配模型上;点击可扩大或缩小选择。', 'These tags also appear on matching models; click to refine your selection.', 'Diese Tags kommen bei passenden Models ebenfalls vor. Anklicken ergänzt deine Auswahl.'), $('div', { class: 'tag-cloud' }, related.length ? related.map(item => createTagPill(item)) : [$('span', { class: 'discover-chip' }, tx('先选择标签', 'Select a tag first', 'Wähle zuerst einen Tag'))])), favoriteItems.length ? tagSection(tx('收藏的标签', 'Favorite tags', 'Favorisierte Tags'), tx('个人快捷列表。', 'Your personal shortcut list.', 'Deine persönliche Schnellliste.'), $('div', { class: 'tag-cloud' }, favoriteItems.map(item => createTagPill(item)))) : null, ]), ]), ); } function createBulkSelectionBar(result) { const resultIds = new Set(result.map(model => model.id)); const selectedResultIds = state.selectedIds.filter(id => resultIds.has(id)); if (!selectedResultIds.length) return null; const saveSelected = () => { selectedResultIds.forEach(id => ensureSaved(id)); toast(tx(`${selectedResultIds.length} 个模型已保存`, `${selectedResultIds.length} models saved`, `${selectedResultIds.length} Models gespeichert`, `${selectedResultIds.length} modelos guardados`, `${selectedResultIds.length} modelos salvos`)); renderCurrentView(); }; const selectedGroup = $('select', { class: 'discover-input', ariaLabel: tx('批量目标组', 'Bulk target group', 'Zielgruppe für Auswahl') }, [ $('option', { value: '' }, tx('添加到组…', 'Add to group…', 'Zu Gruppe hinzufügen…')), ...store.state.groups.filter(group => !group.system).map(group => $('option', { value: group.id }, groupLabel(group))), ]); selectedGroup.addEventListener('change', () => { if (!selectedGroup.value) return; selectedResultIds.forEach(id => { ensureSaved(id); store.moveToGroup(id, selectedGroup.value); }); toast(tx('所选模型已添加到组', 'Selected models added to group', 'Ausgewählte Models wurden der Gruppe hinzugefügt')); selectedGroup.value = ''; }); const createSelectionWorkspace = () => { const name = discoverCleanText(prompt(tx('新工作区名称', 'New workspace name', 'Name des neuen Arbeitsbereichs')) || '', 60); if (!name) return; const groupId = store.addGroup(name); selectedResultIds.forEach(id => { ensureSaved(id); store.moveToGroup(id, groupId); }); const workspaceId = store.addWorkspace(name, tx('从“发现”多选创建', 'Created from a Discover selection', 'Aus einer Entdecken-Auswahl erstellt')); store.activateWorkspace(workspaceId); const count = Math.min(MAX_LAYOUT_SLOTS, selectedResultIds.length); const cols = Math.min(5, Math.ceil(Math.sqrt(count))); const rows = Math.min(5, Math.ceil(count / cols)); store.patchSettings({ activeGroup: groupId, viewMode: 'focus', focusedRoomId: selectedResultIds[0], focusRoomSlots: selectedResultIds.slice(0, MAX_LAYOUT_SLOTS), useCustomLayout: true, customLayoutRows: rows, customLayoutCols: cols, pageIndex: 0 }); store.saveActiveWorkspace(); toast(tx('工作区已创建', 'Workspace created', 'Arbeitsbereich erstellt')); }; return $('div', { class: 'discover-bulk' }, [ $('strong', {}, `${selectedResultIds.length} ${tx('已选择', 'selected', 'ausgewählt')}`), $('button', { class: 'discover-btn', type: 'button', onclick: saveSelected }, tx('保存全部', 'Save all', 'Alle speichern')), selectedGroup, $('button', { class: 'discover-btn primary', type: 'button', onclick: createSelectionWorkspace }, tx('创建工作区', 'Create workspace', 'Arbeitsbereich erstellen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.selectedIds = []; persistDiscoverState(); renderCurrentView(); } }, tx('取消选择', 'Clear selection', 'Auswahl leeren')), ]); } function buildDiscoverInterestProfile() { ensureLocalIndexes(); const signature = [ // Seen timestamps/day changes affect recommendation lanes, not the // weighted interests. Reuse the 60k-history aggregation until one of // its actual inputs changes; recommendations keep their own signature. catalogDataRevision, localMetadataRevision, statsRevision, state.themeFavorites.join('|'), state.tagFavorites.join('|'), ].join('::'); if (discoverInterestProfileCache.signature === signature && discoverInterestProfileCache.value) return discoverInterestProfileCache.value; const tagWeights = new Map(); const countryWeights = new Map(); const genderWeights = new Map(); let signals = 0; const normalizedWeights = new Map(); const normalizedTags = new Map(); const addWeight = (map, key, amount) => { let clean = normalizedWeights.get(key); if (clean === undefined) { clean = discoverCleanText(key, 60).toLocaleLowerCase(); normalizedWeights.set(key, clean); } if (!clean || !Number.isFinite(amount) || amount <= 0) return; map.set(clean, (map.get(clean) || 0) + amount); }; const knownIds = new Set([...catalog.map(model => model.id), ...store.state.rooms.map(room => room.id), ...statsById.keys()]); for (const id of knownIds) { const stat = statsById.get(id); const room = roomsById.get(id); const saved = !!room; const favorite = !!room && roomInGroup(room, FAVORITE_GROUP_ID); const watchedMinutes = Math.max(0, Number(stat?.totalMs || 0) / 60000); const sessions = Math.max(0, Number(stat?.sessions || 0)); const weight = (favorite ? 12 : 0) + (saved ? 1.5 : 0) + Math.min(10, Math.log2(1 + watchedMinutes) * 1.8) + Math.min(5, sessions * .55); if (weight <= 0) continue; // Interest aggregation needs metadata, not thumbnails or room-card // objects. Avoid constructing 60k localModel() objects and repeatedly // normalizing the same tag vocabulary on the first Discover visit. const model = catalogById.get(id); signals += weight; const cleanedTags = new Set(); for (const tags of [model?.tags, stat?.tags, room?.localTags]) { if (!tags) continue; for (const tag of tags) { let clean = normalizedTags.get(tag); if (clean === undefined) { clean = discoverCleanText(tag, 50).toLocaleLowerCase(); normalizedTags.set(tag, clean); } if (clean) cleanedTags.add(clean); } } // Deduplicate normalized tags directly, without allocating an extra // raw-tag Set for every historical model. Preserve accumulation order. cleanedTags.forEach(tag => addWeight(tagWeights, tag, weight)); addWeight(countryWeights, model?.country || model?.location, weight * .32); if (model?.gender && model.gender !== 'unknown') addWeight(genderWeights, model.gender, weight * .25); } state.themeFavorites.forEach(tag => { addWeight(tagWeights, tag, 9); signals += 9; }); state.tagFavorites.forEach(tag => { addWeight(tagWeights, tag, 5); signals += 5; }); const value = { tagWeights, countryWeights, genderWeights, hasSignals: signals >= 3 }; discoverInterestProfileCache = { signature, value }; return value; } function discoverRecommendationData(source) { const now = Date.now(); const hidden = state.discoveryHiddenModels || {}; const lessTags = new Set(state.discoveryLessTags || []); const activeHidden = Object.keys(hidden).filter(id => hidden[id] === 0 || hidden[id] > now); const signature = `${dataCacheSignature()}:${state.randomSeed}:${state.themeFavorites.join(',')}:${state.tagFavorites.join(',')}:${activeHidden.join(',')}:${[...lessTags].join(',')}`; const cached = recommendationCache.get(source); if (cached?.signature === signature) return cached.value; const hiddenIds = new Set(activeHidden); const publicModels = source.filter(model => model.currentShow === 'public' && !hiddenIds.has(model.id)); const feedbackPenalty = model => modelTags(model).filter(tag => lessTags.has(tag)).length * 20; const profile = buildDiscoverInterestProfile(); const stableNoise = id => [...`${id}:${state.randomSeed}`].reduce((sum, char) => ((sum * 33) ^ char.charCodeAt(0)) >>> 0, 5381) % 997; const personalized = publicModels.map(model => { const matches = modelTags(model).map(tag => ({ tag, weight: profile.tagWeights.get(tag) || 0 })).filter(item => item.weight > 0).sort((a, b) => b.weight - a.weight); const country = discoverCleanText(model.country || model.location, 60).toLocaleLowerCase(); const score = matches.reduce((sum, item) => sum + item.weight, 0) + (profile.countryWeights.get(country) || 0) + (profile.genderWeights.get(String(model.gender || '').toLocaleLowerCase()) || 0) + Math.log2(1 + Math.max(0, Number(model.viewers) || 0)) + stableNoise(model.id) / 10000 - feedbackPenalty(model); const reason = matches.slice(0, 2).map(item => `#${item.tag}`).join(' · '); return { ...model, suggestionScore: score, suggestionReason: reason ? tx(`符合你的兴趣:${reason}`, `Matches your interests: ${reason}`, `Passt zu deinen Interessen: ${reason}`, `Coincide con tus intereses: ${reason}`, `Combina com seus interesses: ${reason}`) : '' }; }).filter(model => profile.hasSignals ? model.suggestionReason : true).sort((a, b) => b.suggestionScore - a.suggestionScore); const forYou = profile.hasSignals ? personalized : personalized.map(model => ({ ...model, suggestionScore: Math.log2(1 + Math.max(0, Number(model.viewers) || 0)) + stableNoise(model.id) / 10000 - feedbackPenalty(model), suggestionReason: tx('入门推荐 · 当前热门', 'Starter pick · popular now', 'Startempfehlung · aktuell beliebt', 'Recomendación inicial · popular ahora', 'Sugestão inicial · popular agora'), })).sort((a, b) => b.suggestionScore - a.suggestionScore); const firstSeen = model => Number(state.seenModels?.[model.id]?.firstSeenAt || 0); const newModels = [...publicModels].filter(model => model.isNew || firstSeen(model) > previousCatalogVisitAt || (model.secondsOnline > 0 && model.secondsOnline <= 2700)) .sort((a, b) => Number(b.isNew) - Number(a.isNew) || firstSeen(b) - firstSeen(a) || a.secondsOnline - b.secondsOnline); const popular = [...publicModels].sort((a, b) => b.viewers - a.viewers || a.id.localeCompare(b.id)); const fresh = [...publicModels].filter(model => model.secondsOnline > 0 && model.secondsOnline <= 5400) .sort((a, b) => (b.viewers / Math.max(10, b.secondsOnline / 60)) - (a.viewers / Math.max(10, a.secondsOnline / 60)) || b.viewers - a.viewers); const unseen = [...publicModels].filter(model => !Number(statsById.get(model.id)?.totalMs || 0) && !isFavorite(model.id)) .sort((a, b) => b.viewers - a.viewers || stableNoise(a.id) - stableNoise(b.id)); const returning = [...publicModels].filter(model => Number(statsById.get(model.id)?.sessions || 0) > 0) .sort((a, b) => Number(statsById.get(b.id)?.lastWatchedAt || 0) - Number(statsById.get(a.id)?.lastWatchedAt || 0)); const value = { profile, forYou, newModels, popular, fresh, unseen, returning }; recommendationCache.set(source, { signature, value }); return value; } function createDiscoverLane(id, title, description, models, usedIds, maximum = 8) { if (state.discoveryLessTags.length) { const less = new Set(state.discoveryLessTags); const preferred = [], reduced = []; models.forEach(model => (modelTags(model).some(tag => less.has(tag)) ? reduced : preferred).push(model)); models = preferred.concat(reduced); } const unique = []; for (const model of models) { if (!model?.id || usedIds.has(model.id)) continue; usedIds.add(model.id); unique.push(model); if (unique.length >= maximum) break; } // Prefer fresh cards in every lane. If all suitable models were already // used by earlier lanes, keep the category useful with a small fallback // instead of hiding the entire discovery reason from the user. if (!unique.length) { for (const model of models) { if (!model?.id || unique.some(item => item.id === model.id)) continue; unique.push(model); if (unique.length >= Math.min(maximum, 4)) break; } } if (!unique.length) return null; unique.forEach(model => usedIds.add(model.id)); const grid = $('div', { class: 'discover-lane-grid' }); const contextIds = unique.map(model => model.id); unique.forEach(model => { const card = createModelCard(model, true, contextIds); const hide = days => { state.discoveryHiddenModels[model.id] = days ? Date.now() + days * 86400000 : 0; persistDiscoverState(); renderCurrentView(); }; const menu = $('details', { class: 'discover-feedback', dataset: { discoverFeedback: model.id } }, [ $('summary', {}, tx('推荐反馈', 'Recommendation feedback', 'Vorschlag anpassen')), $('button', { class: 'discover-btn', type: 'button', dataset: { discoverNotInterested: model.id }, onclick: () => hide(0) }, tx('不感兴趣', 'Not interested', 'Nicht interessiert')), $('button', { class: 'discover-btn', type: 'button', onclick: () => hide(1) }, tx('隐藏一天', 'Hide for a day', '1 Tag ausblenden')), $('button', { class: 'discover-btn', type: 'button', onclick: () => hide(7) }, tx('隐藏一周', 'Hide for a week', '1 Woche ausblenden')), $('button', { class: 'discover-btn', type: 'button', disabled: !modelTags(model).length, dataset: { discoverLessSimilar: model.id }, onclick: () => { const next = [...new Set([...state.discoveryLessTags, ...modelTags(model).slice(0, 3)])]; if (next.length > 100) { toast(tx('先恢复一些主题。', 'Restore some topics first.', 'Blende zuerst einige Themen wieder ein.')); return; } state.discoveryLessTags = next; persistDiscoverState(); renderCurrentView(); } }, tx('减少相似推荐', 'Fewer similar suggestions', 'Weniger ähnliche Vorschläge')), ]); card.append(menu); grid.appendChild(card); }); return $('section', { class: 'discover-lane', dataset: { discoverLane: id } }, [ $('div', { class: 'discover-lane-head' }, [ $('div', {}, [$('h3', {}, title), $('p', {}, description)]), $('span', { class: 'discover-chip' }, String(unique.length)), ]), grid, ]); } function createDiscoverInspiration(result) { const recommendations = discoverRecommendationData(result); const usedIds = new Set(); const lanes = [ createDiscoverLane('for-you', tx('为你推荐', 'For you', 'Für dich', 'Para ti', 'Para você'), recommendations.profile.hasSignals ? tx('基于你本地保存的收藏、标签和观看活动。每个推荐都会显示原因。', 'Based on your locally saved favorites, tags, and viewing activity. Every pick shows its reason.', 'Basiert auf deinen lokal gespeicherten Favoriten, Tags und Besuchen. Jede Empfehlung zeigt ihren Grund.', 'Basado en tus favoritos, etiquetas y actividad guardados localmente.', 'Com base nos seus favoritos, tags e atividade salvos localmente.') : tx('还没有足够的本地偏好数据,因此这里先显示当前热门的入门推荐。', 'There is not enough local preference data yet, so these are transparent starter picks based on current popularity.', 'Noch fehlen genügend lokale Vorlieben. Deshalb siehst du hier transparente Startempfehlungen anhand der aktuellen Beliebtheit.', 'Aún no hay suficientes preferencias locales; mostramos recomendaciones iniciales populares.', 'Ainda não há preferências locais suficientes; mostramos sugestões iniciais populares.'), recommendations.forYou, usedIds), createDiscoverLane('new', tx('新发现', 'New discoveries', 'Neu entdeckt', 'Nuevos descubrimientos', 'Novas descobertas'), tx('新加入目录、刚被 Camclient 发现或刚刚开始直播。', 'New to the catalog, newly seen by Camclient, or just started streaming.', 'Neu im Katalog, erstmals von Camclient gesehen oder gerade erst live gegangen.', 'Nuevos en el catálogo o recién conectados.', 'Novos no catálogo ou recém-conectados.'), recommendations.newModels, usedIds), createDiscoverLane('popular', tx('当前热门', 'Popular now', 'Gerade beliebt', 'Popular ahora', 'Popular agora'), tx('按公开观众数排序;显示最后读取的目录。', 'Ranked by public viewer count in the last fetched catalog.', 'Nach öffentlicher Zuschauerzahl im zuletzt gelesenen Katalog sortiert.', 'Ordenado por espectadores públicos actuales.', 'Ordenado pelo número atual de espectadores públicos.'), recommendations.popular, usedIds), createDiscoverLane('fresh', tx('快速起步', 'Fast starters', 'Beliebt & frisch gestartet', 'Inicios fuertes', 'Começos fortes'), tx('直播时间不长,但已经获得公开关注。', 'Live for a short time and already drawing public activity.', 'Noch nicht lange live und bereits mit öffentlicher Aktivität.', 'Llevan poco tiempo en directo y ya atraen actividad.', 'Ao vivo há pouco tempo e já atraindo atividade.'), recommendations.fresh, usedIds), createDiscoverLane('unseen', tx('未访问', 'Unvisited', 'Noch nie besucht', 'Nunca visitados', 'Nunca visitados'), tx('Camclient 中还没有本地观看时间的公开直播模型。', 'Public live models with no local watch time in Camclient.', 'Öffentliche Live-Models ohne lokale Sehzeit in Camclient.', 'Modelos en vivo sin tiempo de visualización local.', 'Modelos ao vivo sem tempo de exibição local.'), recommendations.unseen, usedIds), createDiscoverLane('returning', tx('再次上线', 'Back online', 'Wieder da', 'De vuelta', 'De volta'), tx('你在本地访问过、现在重新公开直播的模型。', 'Models you have visited locally and that are public again now.', 'Models, die du lokal bereits besucht hast und die jetzt wieder öffentlich live sind.', 'Modelos que ya visitaste y ahora están en vivo de nuevo.', 'Modelos que você já visitou e estão ao vivo novamente.'), recommendations.returning, usedIds), ].filter(Boolean); const jumpToFilters = () => document.getElementById('camclient-discover-filters')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' }); const quickButton = (label, quick) => $('button', { class: `discover-btn${state.quick === quick ? ' is-active' : ''}`, type: 'button', ariaPressed: state.quick === quick ? 'true' : 'false', onclick: () => stateChanged({ quick }) }, label); return $('section', { class: 'discover-inspiration', dataset: { discoverRecommendations: '1' } }, [ $('div', { class: 'discover-inspiration-head' }, [ $('div', {}, [$('p', { class: 'discover-kicker' }, tx('灵感而非重复', 'Inspiration, not repetition', 'Inspiration statt Wiederholung')), $('h2', {}, tx('你的发现组合', 'Your discovery mix', 'Dein Entdeckungs-Mix')), $('p', {}, tx('独立的推荐区帮助你发现新模型,而不仅仅是再次显示主页列表。', 'Distinct recommendation lanes help you find something new instead of repeating the homepage catalog.', 'Eigene Empfehlungsbereiche helfen dir, wirklich Neues zu finden, statt nur den Startseiten-Katalog zu wiederholen.'))]), $('div', { class: 'discover-inspiration-actions' }, [ quickButton(tx('全部建议', 'All suggestions', 'Alle Vorschläge'), 'all'), quickButton(tx('仅新发现', 'Only new', 'Nur neu'), 'new_since'), quickButton(tx('对我来说是新的', 'New to me', 'Neu für mich'), 'new_to_me'), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.randomSeed = (Number(state.randomSeed) || 0) + 1; persistDiscoverState(); renderCurrentView(); } }, tx('重新混合', 'Remix', 'Neu mischen')), ]), ]), $('div', { class: 'discover-inspiration-scope' }, [ $('strong', {}, `${formatCount(result.length)} ${tx('当前匹配', 'current matches', 'aktuelle Treffer')}`), $('span', {}, tx('这些区域会采用你当前的搜索和筛选设置。', 'The lanes respect your active search and filter settings.', 'Die Bereiche berücksichtigen deine aktiven Such- und Filtereinstellungen.')), $('button', { class: 'discover-btn', type: 'button', onclick: jumpToFilters }, tx('筛选建议', 'Filter suggestions', 'Vorschläge filtern')), ]), Object.keys(state.discoveryHiddenModels).length || state.discoveryLessTags.length ? $('details', { class: 'discover-feedback-history', dataset: { discoverFeedbackHistory: '1' } }, [ $('summary', {}, tx('管理你的反馈', 'Manage your feedback', 'Ausgeblendete Vorschläge und Themen')), $('p', {}, tx('这些设置只影响推荐,不会取消关注或从主页删除模型。', 'Feedback affects recommendations only. It does not unfollow models or remove them from the homepage.', 'Gilt nur für Vorschläge. Models bleiben auf der Startseite und in deiner Sammlung; niemandem wird entfolgt.')), ...Object.entries(state.discoveryHiddenModels).filter(([, until]) => until === 0 || until > Date.now()).map(([id, until]) => $('div', { class: 'discover-feedback-row' }, [ $('span', {}, `${id} · ${until ? new Date(until).toLocaleString() : tx('不感兴趣', 'Not interested', 'Nicht interessiert')}`), $('button', { class: 'discover-btn', type: 'button', dataset: { discoverRestore: id }, onclick: () => { delete state.discoveryHiddenModels[id]; persistDiscoverState(); renderCurrentView(); } }, tx('恢复', 'Restore', 'Rückgängig')), ])), ...state.discoveryLessTags.map(tag => $('div', { class: 'discover-feedback-row' }, [ $('span', {}, `#${tag}`), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.discoveryLessTags = state.discoveryLessTags.filter(value => value !== tag); persistDiscoverState(); renderCurrentView(); } }, tx('恢复主题', 'Restore topic', 'Thema wieder bevorzugen')), ])), ]) : null, lanes.length ? $('div', { class: 'discover-lanes' }, lanes) : $('div', { class: 'discover-empty' }, tx('没有符合当前筛选条件的推荐。', 'No recommendations match the active filters.', 'Keine Empfehlungen passen zu den aktiven Filtern.')), ]); } function renderDiscover() { previewScheduler.clear(); const result = selectedModels(); const pageCount = Math.max(1, Math.ceil(result.length / DISCOVER_PAGE_SIZE)); state.page = Math.min(Math.max(1, Number(state.page) || 1), pageCount); const items = result.slice((state.page - 1) * DISCOVER_PAGE_SIZE, state.page * DISCOVER_PAGE_SIZE); const controls = createDiscoverControls(); controls.id = 'camclient-discover-filters'; const selectionBar = createBulkSelectionBar(result); const status = $('div', { class: 'discover-status' + (catalogError ? ' is-error' : '') }, [ $('span', {}, catalogLoading ? (catalog.length ? tx('缓存的直播房间已可用;正在后台更新…', 'Cached live rooms are ready; refreshing in the background…', 'Zwischengespeicherte Live-Räume sind sofort verfügbar; Aktualisierung läuft im Hintergrund…') : tx('正在加载公开目录…', 'Loading public catalog…', 'Öffentlicher Katalog wird geladen…')) : catalogPrefetching ? tx('完整目录正在后台加载…', 'Loading the complete catalog in the background…', 'Vollständiger Katalog wird im Hintergrund geladen…') : catalogError || tx(`${result.length} 个匹配结果`, `${result.length} matching results`, `${result.length} passende Ergebnisse`, `${result.length} resultados coincidentes`, `${result.length} resultados correspondentes`)), $('span', {}, catalogFetchedAt ? `${tx('更新', 'Updated', 'Aktualisiert')} ${fmtTime(catalogFetchedAt)}` : ''), $('span', {}, catalogTotal ? tx( `已读取目录 ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} · ${catalog.length} 个直播模型`, `Catalog ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} fetched · ${catalog.length} live models`, `Katalog ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} abgerufen · ${catalog.length} Live-Models`, `Catálogo ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} consultado · ${catalog.length} modelos en vivo`, `Catálogo ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} consultado · ${catalog.length} modelos ao vivo`, ) : catalogIsFallback ? tx(`本地模型 ${catalog.length}`, `${catalog.length} local models`, `${catalog.length} lokale Modelle`) : ''), ]); const grid = $('section', { class: 'discover-grid', ariaLabel: tx('公开房间', 'Public rooms', 'Öffentliche Räume'), dataset: { discoverAllResultsGrid: '1' } }); const contextIds = result.map(model => model.id); if (items.length) items.forEach(model => grid.appendChild(createModelCard(model, false, contextIds))); else grid.appendChild($('div', { class: 'discover-empty' }, catalogLoading ? tx('目录正在加载。', 'The catalog is loading.', 'Der Katalog wird geladen.') : tx('没有匹配的房间。请调整搜索或筛选。', 'No matching rooms. Adjust search or filters.', 'Keine passenden Räume. Passe Suche oder Filter an.'))); const pagination = $('div', { class: 'discover-pagination' }, [ $('button', { class: 'discover-btn', type: 'button', disabled: state.page <= 1, onclick: () => { state.page -= 1; persistDiscoverState(); renderCurrentView(); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, tx('上一页', 'Previous', 'Zurück')), $('span', {}, `${tx('页', 'Page', 'Seite')} ${state.page} / ${pageCount}`), $('button', { class: 'discover-btn', type: 'button', disabled: state.page >= pageCount, onclick: () => { state.page += 1; persistDiscoverState(); renderCurrentView(); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, tx('下一页', 'Next', 'Weiter')), $('button', { class: 'discover-btn', type: 'button', disabled: catalogLoading || catalogPrefetching || !catalogHasMore(), onclick: continueCatalogPrefetch }, catalogPrefetching ? tx('正在自动加载完整目录…', 'Automatically loading the complete catalog…', 'Vollständiger Katalog wird automatisch geladen…') : tx('继续加载完整目录', 'Continue loading the complete catalog', 'Vollständigen Katalog weiterladen')), ]); main.replaceChildren( createHero(tx('发现新的直播', 'Discover something new', 'Entdecke etwas Neues'), tx( '个性化建议、新发现、当前热门和未访问的模型——清晰地与主页的完整目录分开。', 'Personal picks, new discoveries, popular live rooms, and models you have never visited—clearly separated from the complete homepage catalog.', 'Persönliche Vorschläge, neue Models, gerade beliebte Live-Räume und noch nie Besuchte – klar getrennt vom vollständigen Startseiten-Katalog.' )), $('nav', { class: 'discover-checks', ariaLabel: tx('发现视图', 'Discover views', 'Entdecken-Ansichten') }, [ $('a', { class: 'discover-btn', href: '#camclient-recommendations', onclick: event => { event.preventDefault(); document.getElementById('camclient-recommendations')?.scrollIntoView(); } }, tx('推荐', 'Recommendations', 'Empfehlungen')), $('a', { class: 'discover-btn', href: '#camclient-live-rooms', onclick: event => { event.preventDefault(); document.getElementById('camclient-live-rooms')?.scrollIntoView(); } }, tx('所有直播', 'All live rooms', 'Alle Live-Räume')), $('button', { class: 'discover-btn', type: 'button', onclick: () => setView('tags') }, tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer')), ]), $('div', { id: 'camclient-recommendations' }, createDiscoverInspiration(result)), $('div', { class: 'discover-all-results-head' }, [ $('div', {}, [$('h2', {}, tx('所有直播模型', 'All live models', 'Alle Live-Models')), $('p', {}, tx('使用完整搜索、筛选和排序查看所有当前结果。', 'Use the complete search, filters, and sorting for every current result.', 'Durchsuche alle aktuellen Ergebnisse mit der vollständigen Suche, Filtern und Sortierung.'))]), $('span', { class: 'discover-chip' }, formatCount(result.length)), ]), $('div', { id: 'camclient-live-rooms' }, controls), ...(selectionBar ? [selectionBar] : []), status, grid, pagination, ); } function moveDashboardSection(id, delta) { const order = [...state.dashboardOrder]; const index = order.indexOf(id); const target = index + delta; if (index < 0 || target < 0 || target >= order.length) return; [order[index], order[target]] = [order[target], order[index]]; state.dashboardOrder = order; persistDiscoverState(); renderCurrentView(); } function placeDashboardSection(sourceId, targetId) { if (!sourceId || !targetId || sourceId === targetId) return; const next = state.dashboardOrder.filter(id => id !== sourceId); const target = next.indexOf(targetId); next.splice(target < 0 ? next.length : target, 0, sourceId); state.dashboardOrder = next; persistDiscoverState(); renderCurrentView(); } function section(title, description, models, maximum = 8, id = '') { const defaultSize = ['recent','frequent','never'].includes(id) ? 'small' : 'wide'; const size = state.dashboardSizes[id] || defaultSize; const wrapper = $('section', { class: `dashboard-section size-${size}`, draggable: !!id, dataset: { dashboardWidget: id } }); if (id) { wrapper.addEventListener('dragstart', event => { event.dataTransfer?.setData('text/roomgrid-dashboard-widget', id); wrapper.classList.add('is-dragging'); }); wrapper.addEventListener('dragend', () => wrapper.classList.remove('is-dragging')); wrapper.addEventListener('dragover', event => { event.preventDefault(); wrapper.classList.add('is-drop-target'); }); wrapper.addEventListener('dragleave', () => wrapper.classList.remove('is-drop-target')); wrapper.addEventListener('drop', event => { event.preventDefault(); wrapper.classList.remove('is-drop-target'); placeDashboardSection(event.dataTransfer?.getData('text/roomgrid-dashboard-widget'), id); }); } wrapper.appendChild($('div', { class: 'dashboard-section-head' }, [ $('div', {}, [$('h2', {}, title), $('p', {}, description)]), $('div', { class: 'discover-checks' }, [ models.length ? $('span', { class: 'discover-chip' }, String(models.length)) : null, id ? $('button', { class: 'discover-btn', type: 'button', title: tx('向上移动', 'Move up', 'Nach oben'), onclick: () => moveDashboardSection(id, -1) }, '↑') : null, id ? $('button', { class: 'discover-btn', type: 'button', title: tx('向下移动', 'Move down', 'Nach unten'), onclick: () => moveDashboardSection(id, 1) }, '↓') : null, id ? $('select', { class: 'discover-input', style: { width: '74px', minHeight: '34px' }, title: tx('显示数量', 'Number shown', 'Anzahl anzeigen'), onchange: event => { state.dashboardLimits[id] = clampInt(event.target.value, 4, 40, 8); persistDiscoverState(); renderCurrentView(); } }, [4,8,12,20,30,40].map(value => $('option', { value, selected: value === maximum }, String(value)))) : null, id ? $('select', { class: 'discover-input', style: { width: '104px', minHeight: '34px' }, title: tx('组件宽度', 'Widget width', 'Widget-Breite'), onchange: event => { state.dashboardSizes[id] = event.target.value; persistDiscoverState(); renderCurrentView(); } }, [['small', tx('小', 'Small', 'Klein')], ['wide', tx('宽', 'Wide', 'Breit')], ['full', tx('全宽', 'Full', 'Volle Breite')]].map(([value, label]) => $('option', { value, selected: value === size }, label))) : null, id ? $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.dashboardHidden = [...new Set([...state.dashboardHidden, id])]; persistDiscoverState(); renderCurrentView(); } }, tx('隐藏', 'Hide', 'Ausblenden')) : null, ]), ])); const grid = $('div', { class: 'discover-grid' }); const contextIds = models.map(item => item.id); if (models.length) models.slice(0, maximum).forEach(model => grid.appendChild(createModelCard(model, true, contextIds))); else grid.appendChild($('div', { class: 'discover-empty' }, tx('此处还没有项目。', 'Nothing here yet.', 'Hier gibt es noch keine Einträge.'))); wrapper.appendChild(grid); return wrapper; } function applyHomepageStudioSettingsToDashboard(settings = store.state.settings, rerender = true) { const tags = discoverCleanTagList(String(settings.normalHomeTagFilter || '').split(/[,;|]+/u), 20).join(', '); const matchAll = settings.normalHomeTagMatchMode !== 'any'; const gender = ['f', 'm', 'c', 't'].includes(settings.normalHomeGender) ? settings.normalHomeGender : ''; const sortMap = { native: state.sortPrimary, viewers_desc: 'viewers_desc', viewers_asc: 'viewers_asc', name_asc: 'name_asc', name_desc: 'name_desc', online_desc: 'online_desc', online_asc: 'online_asc', new_first: 'new_desc', gaming_first: 'gaming_desc', watched_desc: 'watch_desc', recent_desc: 'last_desc', random: 'random', }; let quick = 'all'; if (settings.normalHomeOnlyHd) quick = 'hd'; else if (settings.normalHomeOnlyNew) quick = 'new'; else if (settings.normalHomeOnlyGaming) quick = 'gaming'; else if (settings.normalHomeOnlyFollowing) quick = 'following'; else if (settings.normalHomeOnlySaved) quick = 'saved'; let localMode = 'all'; if (settings.normalHomeOnlyFavorites) localMode = 'favorites'; else if (settings.normalHomeOnlyFollowing) localMode = 'following'; else if (settings.normalHomeOnlySaved) localMode = 'saved'; Object.assign(state, { query: discoverCleanText(settings.normalHomeSearch, 120), quick, localMode, genders: gender ? [gender] : [], tagsAll: matchAll ? tags : '', tagsAny: matchAll ? '' : tags, place: discoverCleanText(settings.normalHomeCountryFilter, 100), minAge: clampInt(settings.normalHomeMinAge, 0, 99, 0), maxAge: clampInt(settings.normalHomeMaxAge, 0, 99, 0), minViewers: Math.max(0, Math.round(Number(settings.normalHomeMinViewers) || 0)), maxViewers: Math.max(0, Math.round(Number(settings.normalHomeMaxViewers) || 0)), hideVisitedToday: !!settings.normalHomeHideVisited, sortPrimary: sortMap[settings.normalHomeSort] || state.sortPrimary, homePage: 1, }); const pageSize = Number(settings.normalHomePageSize); if ([12, 24, 36, 48, 60, 90, 120, 180].includes(pageSize)) state.homePageSize = pageSize; state.homeColumns = clampInt(settings.normalHomeColumns, 0, 8, state.homeColumns); if (['16/9', '4/3', '1/1', '3/4'].includes(settings.normalHomeImageRatio)) state.homeImageRatio = settings.normalHomeImageRatio; state.homeCardScale = clampInt(settings.normalPageCardScale, 70, 150, state.homeCardScale); persistDiscoverState(); if (rerender && !destroyed) renderCurrentView(); } function createDashboardLiveCenter(overviewNodes = [], resumeRoom = '') { const result = selectedModels(); const pageSize = Number(state.homePageSize) || 24; const pageCount = Math.max(1, Math.ceil(result.length / pageSize)); state.homePage = Math.min(Math.max(1, Number(state.homePage) || 1), pageCount); const start = (state.homePage - 1) * pageSize; const items = result.slice(start, start + pageSize); const contextIds = result.map(model => model.id); const homeChanged = patch => { Object.assign(state, patch); persistDiscoverState(); renderCurrentView(); continueCatalogPrefetch(); }; const homePresets = { compact: { homePage: 1, homePageSize: 48, homeColumns: 6, homeCardScale: 78, homeImageRatio: '16/9', homeDensity: 'compact' }, balanced: { homePage: 1, homePageSize: 24, homeColumns: 0, homeCardScale: 100, homeImageRatio: '4/3', homeDensity: 'comfortable' }, rich: { homePage: 1, homePageSize: 24, homeColumns: 4, homeCardScale: 115, homeImageRatio: '4/3', homeDensity: 'spacious' }, cinema: { homePage: 1, homePageSize: 12, homeColumns: 3, homeCardScale: 145, homeImageRatio: '16/9', homeDensity: 'spacious' }, }; const performancePresets = { off: { previewEnabled: false, previewFps: 0, previewMode: 'bounded', previewConcurrency: 1 }, eco: { previewEnabled: true, previewFps: .5, previewMode: 'bounded', previewConcurrency: 4 }, balanced: { previewEnabled: true, previewFps: 1, previewMode: 'simultaneous', previewConcurrency: 0 }, fluid: { previewEnabled: true, previewFps: 5, previewMode: 'simultaneous', previewConcurrency: 0 }, }; const matchesPreset = preset => Object.entries(preset).every(([key, value]) => key === 'homePage' || state[key] === value); const choiceButton = (label, active, run) => $('button', { class: `discover-btn${active ? ' is-active' : ''}`, type: 'button', ariaPressed: String(active), onclick: run, }, label); const applyHomePreset = (kind) => { homeChanged(homePresets[kind] || homePresets.balanced); }; const applyPerformancePreset = (kind) => { Object.assign(state, performancePresets[kind] || performancePresets.balanced); persistDiscoverState(); previewScheduler.refresh(); renderCurrentView(); }; const widgetDefinitions = [ ['newOnline', tx('新上线', 'Newly online', 'Neu online')], ['recent', tx('最近访问', 'Recently visited', 'Zuletzt besucht')], ['frequent', tx('经常访问', 'Frequently visited', 'Häufig besucht')], ['never', tx('从未访问', 'Never visited', 'Noch nie besucht')], ]; const widgetVisible = id => state.homeShowWidgets && !state.dashboardHidden.includes(id); const toggleWidget = id => { const show = !widgetVisible(id); state.homeShowWidgets = show || state.homeShowWidgets; state.dashboardHidden = show ? state.dashboardHidden.filter(value => value !== id) : [...new Set([...state.dashboardHidden, id])]; persistDiscoverState(); renderCurrentView(); }; const showStats = $('input', { type: 'checkbox', checked: state.homeShowStats }); showStats.addEventListener('change', () => homeChanged({ homeShowStats: showStats.checked })); const showWidgets = $('input', { type: 'checkbox', checked: state.homeShowWidgets }); showWidgets.addEventListener('change', () => homeChanged({ homeShowWidgets: showWidgets.checked })); const displayPanel = $('details', { class: 'dashboard-display-panel', open: true, dataset: { roomgridHomeDisplay: '1' } }, [ $('summary', {}, tx('主页外观、分页与性能', 'Homepage appearance, pagination, and performance', 'Startseiten-Ansicht, Seiteneinteilung und Leistung')), $('div', { class: 'dashboard-display-grid' }, [ filterField(tx('每页模型数', 'Models per page', 'Models pro Seite'), selectControl(String(state.homePageSize), [['12','12'],['24','24'],['36','36'],['48','48'],['60','60'],['90','90'],['120','120'],['180','180']], value => homeChanged({ homePage: 1, homePageSize: Number(value) }))), filterField(tx('每行卡片数', 'Cards per row', 'Karten pro Reihe'), selectControl(String(state.homeColumns), [['0', tx('自动', 'Automatic', 'Automatisch')],['2','2'],['3','3'],['4','4'],['5','5'],['6','6'],['7','7'],['8','8']], value => homeChanged({ homeColumns: Number(value) }))), filterField(tx('卡片大小', 'Card size', 'Kartengröße'), selectControl(String(state.homeCardScale), [['70','70 %'],['78','78 %'],['85','85 %'],['100','100 %'],['115','115 %'],['130','130 %'],['145','145 %'],['150','150 %']], value => homeChanged({ homeCardScale: Number(value) }))), filterField(tx('图像比例', 'Image ratio', 'Bildformat'), selectControl(state.homeImageRatio, [['16/9','16:9'],['4/3','4:3'],['1/1','1:1'],['3/4','3:4']], value => homeChanged({ homeImageRatio: value }))), filterField(tx('信息密度', 'Information density', 'Informationsdichte'), selectControl(state.homeDensity, [['compact', tx('紧凑', 'Compact', 'Kompakt')],['comfortable', tx('舒适', 'Comfortable', 'Komfortabel')],['spacious', tx('详细', 'Detailed', 'Ausführlich')]], value => homeChanged({ homeDensity: value }))), $('div', { class: 'dashboard-preset-row' }, [ $('strong', {}, tx('布局', 'Layout', 'Layout')), choiceButton(tx('紧凑', 'Compact', 'Kompakt'), matchesPreset(homePresets.compact), () => applyHomePreset('compact')), choiceButton(tx('平衡', 'Balanced', 'Ausgewogen'), matchesPreset(homePresets.balanced), () => applyHomePreset('balanced')), choiceButton(tx('数据丰富', 'Data rich', 'Datenreich'), matchesPreset(homePresets.rich), () => applyHomePreset('rich')), choiceButton(tx('影院', 'Cinema', 'Kino'), matchesPreset(homePresets.cinema), () => applyHomePreset('cinema')), $('strong', { style: { marginLeft: '10px' } }, tx('性能', 'Performance', 'Leistung')), choiceButton(tx('静态', 'Static', 'Statisch'), matchesPreset(performancePresets.off), () => applyPerformancePreset('off')), choiceButton('Eco · 0,5 FPS', matchesPreset(performancePresets.eco), () => applyPerformancePreset('eco')), choiceButton('Balance · 1 FPS', matchesPreset(performancePresets.balanced), () => applyPerformancePreset('balanced')), choiceButton('Flüssig · 5 FPS', matchesPreset(performancePresets.fluid), () => applyPerformancePreset('fluid')), $('label', { class: 'discover-check' }, [showStats, tx('显示统计摘要', 'Show statistics summary', 'Statistikübersicht anzeigen')]), $('label', { class: 'discover-check' }, [showWidgets, tx('显示个人组件', 'Show personal widgets', 'Persönliche Widgets anzeigen')]), ]), $('div', { class: 'dashboard-widget-toggle-row' }, [ $('span', {}, tx('直播列表下方的区域', 'Sections below the live catalog', 'Bereiche unter den Live-Räumen')), ...widgetDefinitions.map(([id, label]) => choiceButton(label, widgetVisible(id), () => toggleWidget(id))), ]), ]), ]); const grid = $('section', { class: `dashboard-live-grid density-${state.homeDensity}`, ariaLabel: tx('主页直播模型', 'Homepage live models', 'Live-Models der Startseite'), dataset: { roomgridHomeGrid: '1' } }); if (state.homeColumns > 0) grid.style.gridTemplateColumns = `repeat(${state.homeColumns},minmax(0,1fr))`; grid.style.setProperty('--dashboard-card-min', `${Math.round(260 * state.homeCardScale / 100)}px`); grid.style.setProperty('--dashboard-image-ratio', String(state.homeImageRatio).replace('/', ' / ')); if (items.length) items.forEach(model => grid.appendChild(createModelCard(model, false, contextIds))); else grid.appendChild($('div', { class: 'discover-empty' }, catalogLoading ? tx('目录正在加载。', 'The catalog is loading.', 'Der Katalog wird geladen.') : tx('没有匹配的房间。请调整搜索或筛选。', 'No matching rooms. Adjust search or filters.', 'Keine passenden Räume. Passe Suche oder Filter an.'))); const status = $('div', { class: 'dashboard-live-status' + (catalogError ? ' is-error' : '') }, [ $('span', {}, catalogLoading ? (catalog.length ? tx('缓存的直播房间已可用;正在后台更新…', 'Cached live rooms are ready; refreshing in the background…', 'Zwischengespeicherte Live-Räume sind sofort verfügbar; Aktualisierung läuft im Hintergrund…') : tx('正在加载公开目录…', 'Loading public catalog…', 'Öffentlicher Katalog wird geladen…')) : catalogPrefetching ? tx('完整目录正在后台加载…', 'Loading the complete catalog in the background…', 'Vollständiger Katalog wird im Hintergrund geladen…') : catalogError || $('strong', {}, tx(`${result.length} 个匹配结果`, `${result.length} matching live models`, `${result.length} passende Live-Models`))), $('span', {}, `${items.length ? start + 1 : 0}–${Math.min(start + items.length, result.length)} / ${result.length}`), $('span', { dataset: { roomgridCatalogProgress: '1' } }, catalog.length ? (catalogTotal ? tx( `目录 ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} · ${catalog.length} 个直播模型`, `Catalog ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} · ${catalog.length} live models`, `Katalog ${Math.min(catalogOffset, catalogTotal)} / ${catalogTotal} · ${catalog.length} Live-Models`, ) : catalogIsFallback ? tx(`本地模型 ${catalog.length}`, `${catalog.length} local models`, `${catalog.length} lokale Modelle`) : tx(`目录已加载 ${catalog.length}`, `${catalog.length} catalog rooms loaded`, `${catalog.length} Katalogräume geladen`)) : ''), $('span', {}, catalogFetchedAt ? `${tx('更新', 'Updated', 'Aktualisiert')} ${fmtTime(catalogFetchedAt)}` : ''), resumeRoom ? $('button', { class: 'discover-btn', type: 'button', dataset: { continueWatching: resumeRoom }, title: catalogById.get(resumeRoom)?.currentShow === 'public' ? tx('目录显示直播', 'Live according to catalog', 'Laut Katalog live') : tx('打开后检查状态', 'Check status when opened', 'Status wird beim Öffnen geprüft'), onclick: () => openSingle(resumeRoom, [resumeRoom]), }, `${tx('继续观看', 'Continue watching', 'Weiter ansehen')} · @${resumeRoom}`) : null, ]); const pagination = $('div', { class: 'discover-pagination' }, [ $('button', { class: 'discover-btn', type: 'button', disabled: state.homePage <= 1, onclick: () => { state.homePage -= 1; persistDiscoverState(); renderCurrentView(); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, tx('上一页', 'Previous', 'Zurück')), $('span', {}, `${tx('页', 'Page', 'Seite')} ${state.homePage} / ${pageCount}`), $('button', { class: 'discover-btn', type: 'button', disabled: state.homePage >= pageCount, onclick: () => { state.homePage += 1; persistDiscoverState(); renderCurrentView(); window.scrollTo({ top: 0, behavior: 'smooth' }); } }, tx('下一页', 'Next', 'Weiter')), $('button', { class: 'discover-btn', type: 'button', disabled: catalogLoading || catalogPrefetching || !catalogHasMore(), onclick: continueCatalogPrefetch }, catalogPrefetching ? tx('正在自动加载完整目录…', 'Automatically loading the complete catalog…', 'Vollständiger Katalog wird automatisch geladen…') : tx('继续加载完整目录', 'Continue loading the complete catalog', 'Vollständigen Katalog weiterladen')), ]); const selectionBar = createBulkSelectionBar(result); const activeFilters = createActiveFilterSummary(); const labels = (I18N[LANG]?.homeToolsUi || I18N.en.homeToolsUi); const panels = new Map(), toggles = new Map(); const showPanel = (kind, returnFocus = false) => { state.homeToolsPanel = state.homeToolsPanel === kind ? 'none' : kind; panels.forEach((panel, id) => { panel.hidden = state.homeToolsPanel !== id; }); toggles.forEach((button, id) => { button.setAttribute('aria-expanded', String(state.homeToolsPanel === id)); button.classList.toggle('is-active', state.homeToolsPanel === id); }); persistDiscoverState(true); activeVisualSignature = visualViewSignature('dashboard'); if (returnFocus) toggles.get(kind)?.focus({ preventScroll: true }); }; const toolPanel = (id, label, nodes) => { const panel = $('section', { class: 'dashboard-tools-panel', id: `camclient-home-${id}`, hidden: state.homeToolsPanel !== id, ariaLabel: label, dataset: { homeToolsPanel: id } }, [ $('div', { class: 'dashboard-tools-panel-head' }, [ $('h3', {}, label), $('button', { class: 'discover-btn', type: 'button', onclick: () => showPanel(id, true) }, labels.close), ]), ...nodes, ]); panels.set(id, panel); panel.addEventListener('keydown', event => { if (event.key === 'Escape' && state.homeToolsPanel === id) { event.preventDefault(); event.stopPropagation(); showPanel(id, true); } }); return panel; }; const toolButton = (id, label) => { const count = id === 'filters' ? activeFilters.querySelector('.discover-active-filter-count')?.textContent : ''; const button = $('button', { class: `discover-btn${state.homeToolsPanel === id ? ' is-active' : ''}`, type: 'button', ariaExpanded: String(state.homeToolsPanel === id), ariaControls: `camclient-home-${id}`, dataset: { homeToolsToggle: id }, onclick: () => showPanel(id), }, [$('span', {}, label), count && count !== '0' ? $('span', { class: 'dashboard-tool-count' }, count) : null]); toggles.set(id, button); return button; }; return $('section', { class: 'dashboard-live-center', dataset: { roomgridCustomHome: '1' } }, [ $('div', { class: 'dashboard-live-head' }, [ $('h2', {}, labels.live), $('div', { class: 'dashboard-live-head-actions' }, [ toolButton('filters', labels.filters), toolButton('display', labels.display), toolButton('overview', labels.overview), $('button', { class: 'discover-btn primary', type: 'button', onclick: refreshCompleteCatalog }, tx('刷新直播列表', 'Refresh live catalog', 'Live-Liste aktualisieren')), ]), ]), activeFilters, toolPanel('filters', labels.filters, [createDiscoverControls(false)]), toolPanel('display', labels.display, [displayPanel, createPreviewControls(true), $('div', { class: 'discover-checks' }, [ $('button', { class: 'discover-btn', type: 'button', onclick: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'homepage', { onHomepageSettings: settings => applyHomepageStudioSettingsToDashboard(settings) }) }, tx('主页设置', 'Homepage settings', 'Startseiten-Einstellungen')), ])]), toolPanel('overview', labels.overview, overviewNodes), ...(selectionBar ? [selectionBar] : []), status, grid, pagination, ]); } function dashboardWidgetModels(id, build) { if (!state.homeShowWidgets || state.dashboardHidden.includes(id)) return []; return build(); } function renderDashboard() { previewScheduler.clear(); const stats = [...statsById.values()]; const statisticsReady = analytics.isReady(); // The headline needs the count even with hidden widgets; only the // visible Favorites widget needs the complete sorted list. const liveFavorites = catalog.filter(model => model.currentShow === 'public' && isFavorite(model.id)); if (state.homeShowWidgets && !state.dashboardHidden.includes('favorites')) liveFavorites.sort((a, b) => b.viewers - a.viewers); const newOnline = dashboardWidgetModels('newOnline', () => catalog.filter(model => model.currentShow === 'public' && (model.isNew || model.secondsOnline <= 3600)).sort((a, b) => a.secondsOnline - b.secondsOnline)); const recent = analytics.isEnabled() ? dashboardWidgetModels('recent', () => stats.filter(item => item.lastWatchedAt).sort((a, b) => b.lastWatchedAt - a.lastWatchedAt).slice(0, 40).map(item => localModel(item.id))) : []; const frequent = analytics.isEnabled() ? dashboardWidgetModels('frequent', () => stats.filter(item => item.totalMs).sort((a, b) => b.totalMs - a.totalMs).slice(0, 40).map(item => localModel(item.id))) : []; const never = dashboardWidgetModels('never', () => store.state.rooms.filter(room => !Number(statsById.get(room.id)?.totalMs)).slice(0, 40).map(room => localModel(room.id))); const activeFeed = state.feeds.find(item => item.id === state.activeFeedId) || state.feeds[0] || null; const feedModels = activeFeed ? dashboardWidgetModels('feed', () => selectedModels(activeFeed.definition)) : []; const suggestions = dashboardWidgetModels('suggestions', () => { const favoriteTags = new Set(liveFavorites.flatMap(model => model.tags || [])); return catalog.filter(model => model.currentShow === 'public' && !isFavorite(model.id) && !roomFor(model.id)?.suggestionHidden).map(model => { const shared = (model.tags || []).filter(tag => favoriteTags.has(tag)); const localReason = shared[0] ? tx(`与收藏共享标签 #${shared[0]}`, `Shares #${shared[0]} with a favorite`, `Teilt #${shared[0]} mit einem Favoriten`, `Comparte #${shared[0]} con un favorito`, `Compartilha #${shared[0]} com um favorito`) : model.isGaming && state.themeFavorites.some(tag => (model.tags || []).includes(tag)) ? tx('匹配收藏的游戏主题', 'Matches a favorite gaming theme', 'Passt zu einem favorisierten Gaming-Thema') : ''; return localReason ? { ...model, suggestionReason: localReason, suggestionScore: shared.length * 100 + model.viewers } : null; }).filter(Boolean).sort((left, right) => right.suggestionScore - left.suggestionScore); }); const totalMs = stats.reduce((sum, item) => sum + Number(item.totalMs || 0), 0); const sessions = stats.reduce((sum, item) => sum + Number(item.sessions || 0), 0); const statRow = $('section', { class: 'dashboard-stats' }, [ $('div', { class: 'dashboard-stat' }, [$('span', {}, tx('已保存模型', 'Saved models', 'Gespeicherte Models')), $('strong', {}, formatCount(store.state.rooms.length))]), $('div', { class: 'dashboard-stat' }, [$('span', {}, tx('在线收藏', 'Online favorites', 'Online-Favoriten')), $('strong', {}, formatCount(liveFavorites.length))]), $('div', { class: 'dashboard-stat', ariaBusy: String(!statisticsReady), dataset: { dashboardStat: 'watch-time' } }, [$('span', {}, tx('本地观看时间', 'Local watch time', 'Lokale Sehzeit')), $('strong', {}, statisticsReady ? formatWatch(totalMs) : '…')]), $('div', { class: 'dashboard-stat', ariaBusy: String(!statisticsReady), dataset: { dashboardStat: 'sessions' } }, [$('span', {}, tx('会话', 'Sessions', 'Sitzungen')), $('strong', {}, statisticsReady ? formatCount(sessions) : '…')]), ]); const actions = $('section', { class: 'discover-panel', style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px' } }, [ $('button', { class: 'discover-btn primary', type: 'button', onclick: () => multiCamBtn.click() }, tx('继续上次 MultiCam 工作区', 'Continue last MultiCam workspace', 'Letzten MultiCam-Arbeitsbereich fortsetzen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => setView('discover') }, tx('打开完整发现页面', 'Open full Discover page', 'Vollständige Entdecken-Seite öffnen')), $('span', { class: 'discover-chip' }, `${tx('在线通知', 'Online notifications', 'Online-Meldungen')}: ${store.state.settings.notifyOnline ? tx('开启', 'on', 'an') : tx('关闭', 'off', 'aus')}`), $('span', { class: 'discover-chip' }, `${tx('离线通知', 'Offline notifications', 'Offline-Meldungen')}: ${store.state.settings.notifyOffline ? tx('开启', 'on', 'an') : tx('关闭', 'off', 'aus')}`), ...state.dashboardHidden.map(id => $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.dashboardHidden = state.dashboardHidden.filter(value => value !== id); persistDiscoverState(); renderCurrentView(); } }, `${tx('显示', 'Show', 'Einblenden')}: ${{ favorites: tx('收藏', 'Favorites', 'Favoriten'), newOnline: tx('新上线', 'Newly online', 'Neu online'), feed: tx('动态', 'Feed', 'Feed'), suggestions: tx('建议', 'Suggestions', 'Vorschläge'), recent: tx('最近访问', 'Recent', 'Zuletzt besucht'), frequent: tx('常看', 'Frequent', 'Häufig besucht'), never: tx('未访问', 'Never visited', 'Nie besucht') }[id] || id}`)), ]); const savedViewActions = state.profiles.length ? $('section', { class: 'discover-panel' }, [ $('div', { style: { marginBottom: '8px', fontWeight: '850', fontSize: '13px' } }, tx('保存的筛选视图', 'Saved filter views', 'Gespeicherte Filteransichten')), $('div', { class: 'discover-checks' }, state.profiles.map(item => $('button', { class: 'discover-btn' + (item.id === state.activeProfileId ? ' primary' : ''), type: 'button', onclick: () => { state.view = 'discover'; applyDefinition(item, false); } }, rawUiText(item.name)))), ]) : null; const feedActions = state.feeds.length ? $('section', { class: 'discover-panel' }, [ $('div', { style: { marginBottom: '8px', fontWeight: '850', fontSize: '13px' } }, tx('保存的动态', 'Saved feeds', 'Gespeicherte Feeds')), $('div', { class: 'discover-checks' }, state.feeds.map(item => $('button', { class: 'discover-btn' + (item.id === activeFeed?.id ? ' primary' : ''), type: 'button', onclick: () => { state.activeFeedId = item.id; persistDiscoverState(); renderCurrentView(); } }, rawUiText(item.name)))), ]) : null; const dashboardSections = { favorites: () => liveFavorites.length ? section(tx('在线收藏', 'Online favorites', 'Online-Favoriten'), tx('当前公开在线的收藏。', 'Favorites currently public and online.', 'Favoriten, die gerade öffentlich online sind.'), liveFavorites, state.dashboardLimits.favorites || 8, 'favorites') : null, newOnline: () => newOnline.length ? section(tx('新上线', 'Newly online', 'Neu online'), tx('新的或刚刚开始的公开直播。', 'New models or public streams that started recently.', 'Neue Models oder erst kürzlich gestartete öffentliche Streams.'), newOnline, state.dashboardLimits.newOnline || 8, 'newOnline') : null, feed: () => activeFeed && feedModels.length ? section(activeFeed.name, tx('当前保存动态的新结果。', 'Current results from the selected feed.', 'Aktuelle Ergebnisse des gewählten Feeds.'), feedModels, state.dashboardLimits.feed || 8, 'feed') : null, suggestions: () => suggestions.length ? section(tx('有理由的建议', 'Suggestions with reasons', 'Vorschläge mit Begründung'), tx('仅根据共同公开标签和你本地收藏的主题;不会推断身份。', 'Based only on shared public tags and locally favored themes; no identity inference.', 'Nur anhand gemeinsamer öffentlicher Tags und lokal favorisierter Themen; keine Identitätsableitung.'), suggestions, state.dashboardLimits.suggestions || 8, 'suggestions') : null, recent: () => analytics.isEnabled() && recent.length ? section(tx('继续观看', 'Continue watching', 'Weiter ansehen'), tx('按本地最后访问时间排序。', 'Sorted by local last-visit time.', 'Nach lokalem letzten Besuch sortiert.'), recent, state.dashboardLimits.recent || 8, 'recent') : null, frequent: () => analytics.isEnabled() && frequent.length ? section(tx('经常访问', 'Frequently visited', 'Häufig besucht'), tx('按本地观看时间排序。', 'Sorted by local watch time.', 'Nach lokaler Sehzeit sortiert.'), frequent, state.dashboardLimits.frequent || 8, 'frequent') : null, never: () => never.length ? section(tx('从未访问的已保存模型', 'Saved models never visited', 'Noch nie besuchte gespeicherte Models'), tx('保存在资料库中,但没有本地观看时间。', 'Saved in the library without local watch time.', 'In der Bibliothek gespeichert, aber ohne lokale Sehzeit.'), never, state.dashboardLimits.never || 8, 'never') : null, }; const overviewNodes = [ createHero(tx('你的 RoomGrid 主页', 'Your RoomGrid homepage', 'Deine RoomGrid-Startseite'), tx( '快速、可自定义的直播目录,包含 SingleCam、MultiCam、完整筛选、保存视图和你的个人组件。', 'A fast, customizable live catalog with SingleCam, MultiCam, complete filters, saved views, and your personal widgets.', 'Ein schneller, frei anpassbarer Live-Katalog mit SingleCam, MultiCam, vollständigen Filtern, gespeicherten Ansichten und deinen persönlichen Widgets.' )), ...(state.homeShowStats ? [statRow] : []), actions, ...(savedViewActions ? [savedViewActions] : []), ...(feedActions ? [feedActions] : []), $('button', { class: 'discover-btn', type: 'button', onclick: () => setView('tags') }, tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer')), ]; const resumeRoom = state.homeShowWidgets && analytics.isEnabled() && !analytics.getUsageSummary().privateSession ? latestWatchedId : ''; const liveCenter = createDashboardLiveCenter(overviewNodes, resumeRoom); const widgetGrid = state.homeShowWidgets ? $('div', { class: 'dashboard-widget-grid', ariaLabel: tx('可拖动的主页组件', 'Draggable dashboard widgets', 'Verschiebbare Dashboard-Widgets') }, state.dashboardOrder.filter(id => !state.dashboardHidden.includes(id)).map(id => dashboardSections[id]?.()).filter(Boolean)) : null; main.replaceChildren(liveCenter, ...(widgetGrid ? [widgetGrid] : [])); } function cleanupSingleSession() { const session = singleSession; singleSession = null; singleAnalyticsRefresh = null; if (!session) return; try { session.abort?.abort(); } catch (_) {} try { session.finishLoadProgress?.(); } catch (_) {} clearTimeout(session.pollTimer); session.resumePendingConnection = null; try { session.audioMeter?.destroy?.(); } catch (_) {} try { session.layoutObserver?.disconnect?.(); } catch (_) {} try { session.capture?.stop?.(true); } catch (_) {} try { window.removeEventListener('beforeunload', session.capture?.beforeUnload); } catch (_) {} try { document.removeEventListener('visibilitychange', session.visibilityHandler); } catch (_) {} try { window.removeEventListener('roomgrid_audio_silencer_changed', session.soundHandler); } catch (_) {} try { window.removeEventListener('roomgrid:capture-directory-changed', session.captureDirectoryHandler); } catch (_) {} try { window.removeEventListener('roomgrid:capture-file-saved', session.captureFileHandler); } catch (_) {} try { document.removeEventListener('keydown', session.galleryKeyHandler); } catch (_) {} try { session.stopGalleryMedia?.(); } catch (_) {} try { session.galleryViewer?.remove?.(); document.documentElement.classList.remove('single-gallery-open-lock'); } catch (_) {} try { (session.galleryUrls || []).forEach(url => URL.revokeObjectURL(url)); } catch (_) {} try { if (session.backgroundPreviewUrl) URL.revokeObjectURL(session.backgroundPreviewUrl); } catch (_) {} try { session.hls?.destroy(); } catch (_) {} try { stopMediaElement(session.video, false); } catch (_) {} try { session.chatObserver?.disconnect?.(); } catch (_) {} try { if (session.chatFrame) session.chatFrame.src = 'about:blank'; } catch (_) {} try { analytics.stop(); analytics.flush(); } catch (_) {} } function createSingleAudioMeter(video) { const fill = $('span', { class: 'single-audio-fill' }); const peak = $('span', { class: 'single-audio-peak' }); const track = $('div', { class: 'single-audio-track' }, [fill, peak]); const value = $('strong', {}, '0 %'); const stateLabel = $('span', {}, tx('等待音频', 'Waiting for audio', 'Wartet auf Audio')); const activate = $('button', { class: 'discover-btn single-audio-activate', type: 'button', hidden: true }, tx('启用电平', 'Enable meter', 'Pegel aktivieren')); const root = $('div', { class: 'single-audio-activity', role: 'meter', ariaLabel: tx('直播音频活动', 'Live stream audio activity', 'Audioaktivität des Livestreams'), ariaValueMin: '0', ariaValueMax: '100', ariaValueNow: '0', dataset: { singleAudioMeter: '1', state: 'waiting', source: 'none' }, }, [ $('div', { class: 'single-audio-title' }, [ $('strong', {}, tx('音频活动', 'Audio activity', 'Audioaktivität')), $('small', {}, tx('即使输出静音也会测量', 'Measured even while output is muted', 'Messung auch bei stummem Ton')), ]), track, $('div', { class: 'single-audio-readout' }, [value, stateLabel, activate]), ]); const item = { context: null, stream: null, source: null, analyser: null, samples: null, timer: 0, retryTimer: 0, retries: 0, level: 0, peak: 0, destroyed: false, listeners: [], gestureHandler: null, streamTrackHandler: null, }; const paint = (level = 0, status = 'waiting', note = '') => { const bounded = Math.max(0, Math.min(1, Number(level) || 0)); item.level = bounded; item.peak = ['paused', 'waiting', 'unavailable'].includes(status) ? item.peak * .72 : Math.max(bounded, item.peak * .965); const percent = Math.round(bounded * 100); fill.style.width = `${percent}%`; peak.style.left = `${Math.max(0, Math.min(99.5, item.peak * 100))}%`; value.textContent = status === 'unavailable' ? '—' : `${percent} %`; const labels = { silence: tx('几乎无声', 'Almost silent', 'Fast still'), low: tx('安静', 'Quiet', 'Leise'), medium: tx('中等', 'Medium', 'Mittel'), loud: tx('响亮', 'Loud', 'Laut'), paused: tx('暂停', 'Paused', 'Pausiert'), waiting: tx('等待音频', 'Waiting for audio', 'Wartet auf Audio'), unavailable: tx('无音轨', 'No audio track', 'Kein Audiotrack'), }; stateLabel.textContent = note || labels[status] || labels.waiting; root.dataset.state = status; root.setAttribute('aria-valuenow', String(percent)); root.title = tx( '这里显示直播音轨的活动,与播放器是否静音无关。', 'Shows activity in the stream audio track independently of whether player output is muted.', 'Zeigt die Aktivität des Stream-Audiotracks unabhängig davon, ob die Wiedergabe stumm ist.' ); }; const scheduleAttachRetry = () => { clearTimeout(item.retryTimer); if (item.destroyed || item.analyser || item.retries >= 20) { if (!item.destroyed && !item.analyser && item.retries >= 20) paint(0, 'unavailable', tx('浏览器未提供音轨', 'Browser exposed no audio track', 'Brave stellt keinen Audiotrack bereit')); return; } item.retryTimer = setTimeout(() => { item.retries += 1; attach(); }, 750); }; const loop = () => { clearTimeout(item.timer); if (item.destroyed) return; if (!item.analyser || !item.samples) { scheduleAttachRetry(); return; } if (video.paused || video.ended || video.readyState < 2) { item.level *= .72; paint(item.level, 'paused'); item.timer = setTimeout(loop, document.hidden ? 700 : 140); return; } if (item.context?.state === 'suspended') { activate.hidden = false; paint(item.level * .72, 'waiting', tx('单击激活电平', 'Click to enable meter', 'Zum Aktivieren klicken')); item.timer = setTimeout(loop, 260); return; } activate.hidden = true; try { item.analyser.getByteTimeDomainData(item.samples); let energy = 0; for (const sample of item.samples) { const normalized = (sample - 128) / 128; energy += normalized * normalized; } const rms = Math.sqrt(energy / Math.max(1, item.samples.length)); const normalizedLevel = Math.max(0, Math.min(1, (20 * Math.log10(Math.max(rms, .0001)) + 60) / 60)); const smoothed = item.level * .58 + normalizedLevel * .42; const status = rms < .006 ? 'silence' : rms < .035 ? 'low' : rms < .12 ? 'medium' : 'loud'; paint(smoothed, status); } catch (_) { paint(0, 'unavailable'); } item.timer = setTimeout(loop, document.hidden ? 650 : 80); }; const resume = async () => { if (item.destroyed) return; try { if (item.context?.state === 'suspended') await item.context.resume(); } catch (_) {} activate.hidden = item.context?.state !== 'suspended'; if (!item.analyser) attach(); else loop(); }; const attach = () => { if (item.destroyed || item.analyser) return; try { const Context = window.AudioContext || window.webkitAudioContext; if (!Context) throw new Error('AudioContext unavailable'); if (!item.context || item.context.state === 'closed') item.context = new Context(); const capture = video.captureStream || video.mozCaptureStream || video.webkitCaptureStream; if (typeof capture !== 'function') throw new Error('captureStream unavailable'); if (!item.stream || (!item.stream.getAudioTracks?.().length && item.retries > 0 && item.retries % 3 === 0)) { try { item.stream?.removeEventListener?.('addtrack', item.streamTrackHandler); } catch (_) {} item.stream = capture.call(video); item.streamTrackHandler = () => attach(); item.stream?.addEventListener?.('addtrack', item.streamTrackHandler); } if (!item.stream?.getAudioTracks?.().length) { paint(0, 'waiting'); scheduleAttachRetry(); return; } item.source = item.context.createMediaStreamSource(item.stream); item.analyser = item.context.createAnalyser(); item.analyser.fftSize = 1024; item.analyser.smoothingTimeConstant = .62; item.source.connect(item.analyser); item.samples = new Uint8Array(item.analyser.fftSize); root.dataset.source = 'capture-stream'; item.retries = 0; activate.hidden = item.context.state !== 'suspended'; resume(); } catch (_) { paint(0, 'waiting'); scheduleAttachRetry(); } }; for (const eventName of ['loadedmetadata', 'canplay', 'playing', 'play', 'emptied']) { const listener = () => { if (eventName === 'emptied') paint(0, 'waiting'); attach(); }; video.addEventListener(eventName, listener); item.listeners.push([eventName, listener]); } item.gestureHandler = () => resume(); document.addEventListener('pointerdown', item.gestureHandler, { passive: true }); document.addEventListener('keydown', item.gestureHandler); activate.addEventListener('click', resume); const destroy = () => { item.destroyed = true; clearTimeout(item.timer); clearTimeout(item.retryTimer); item.listeners.forEach(([name, listener]) => video.removeEventListener(name, listener)); document.removeEventListener('pointerdown', item.gestureHandler); document.removeEventListener('keydown', item.gestureHandler); try { item.source?.disconnect?.(); } catch (_) {} try { item.analyser?.disconnect?.(); } catch (_) {} try { item.stream?.removeEventListener?.('addtrack', item.streamTrackHandler); } catch (_) {} try { item.context?.close?.(); } catch (_) {} item.source = null; item.analyser = null; item.stream = null; item.context = null; }; return { root, start: attach, resume, destroy }; } function singleStatusFromContext(data) { const value = String(data?.room_status || data?.current_show || data?.status || '').toLocaleLowerCase(); if (['public', 'online'].includes(value) && isSafeStreamUrl(data?.hls_source)) return 'online'; if (['private', 'group', 'hidden'].includes(value)) return 'private'; if (['offline', 'away'].includes(value) || !data?.hls_source) return 'offline'; return isSafeStreamUrl(data?.hls_source) ? 'online' : 'unknown'; } async function fetchSingleContext(id, signal) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); const parentAbort = () => controller.abort(); try { if (signal) { if (signal.aborted) controller.abort(); else signal.addEventListener('abort', parentAbort, { once: true }); } const response = await fetch(`/api/chatvideocontext/${encodeURIComponent(id)}/`, { credentials: 'include', signal: controller.signal, cache: 'no-store', referrer: new URL(`/${encodeURIComponent(id)}/`, location.origin).toString(), referrerPolicy: 'strict-origin-when-cross-origin', }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('Ungültige Raumantwort'); return data; } finally { clearTimeout(timeout); try { signal?.removeEventListener?.('abort', parentAbort); } catch (_) {} } } function applySingleQuality(session, value) { if (!session?.hls) return; const wanted = Number(value) || 0; if (!wanted) { session.hls.currentLevel = -1; return; } let chosen = -1; let chosenHeight = -1; (session.hls.levels || []).forEach((level, index) => { const height = Number(level?.height) || 0; if (height <= wanted && height >= chosenHeight) { chosen = index; chosenHeight = height; } }); session.hls.currentLevel = chosen; } function attachSingleStream(session, source, qualitySelect, statusNode) { if (singleSession !== session || !isSafeStreamUrl(source)) return false; const video = session.video; if (window.Hls && Hls.isSupported()) { const bufferSeconds = Number(session.preferences?.bufferSeconds) || 10; const hls = new Hls({ liveDurationInfinity: true, lowLatencyMode: session.preferences?.lowLatency !== false, maxBufferLength: bufferSeconds, backBufferLength: Math.max(5, Math.min(30, bufferSeconds)) }); session.hls = hls; hls.loadSource(source); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (singleSession !== session) return; const heights = [...new Set((hls.levels || []).map(level => Number(level.height) || 0).filter(Boolean))].sort((a, b) => a - b); qualitySelect.replaceChildren($('option', { value: '0' }, tx('自动画质', 'Auto quality', 'Qualität automatisch')), ...heights.map(height => $('option', { value: String(height) }, `${height}p`))); qualitySelect.value = session.quality || '0'; applySingleQuality(session, qualitySelect.value); if (session.preferences?.autoplay !== false) video.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_event, data) => { if (!data?.fatal || singleSession !== session) return; session.failures = (session.failures || 0) + 1; if (session.preferences?.autoReconnect !== false && data.type === Hls.ErrorTypes.NETWORK_ERROR && session.failures <= 3) { try { hls.startLoad(); } catch (_) {} } else if (session.preferences?.autoReconnect !== false && data.type === Hls.ErrorTypes.MEDIA_ERROR && session.failures <= 3) { try { hls.recoverMediaError(); } catch (_) {} } else statusNode.textContent = tx('视频流中断;状态检查仍在继续。', 'Stream interrupted; status checks continue.', 'Stream unterbrochen; die Statusprüfung läuft weiter.'); }); return true; } if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = source; video.addEventListener('loadedmetadata', () => { if (session.preferences?.autoplay !== false) video.play().catch(() => {}); }, { once: true }); return true; } statusNode.textContent = tx('此浏览器不支持 HLS 播放。', 'This browser cannot play HLS.', 'Dieser Browser kann HLS nicht wiedergeben.'); return false; } function startSinglePlayer(session, qualitySelect, statusNode) { clearTimeout(session.pollTimer); session.resumePendingConnection = null; session.finishLoadProgress?.(); const connectionRevision = (Number(session.connectionRevision) || 0) + 1; session.connectionRevision = connectionRevision; const isCurrent = () => singleSession === session && session.connectionRevision === connectionRevision; const progressTask = notificationCenter.beginProgress?.(tx('正在连接单人直播', 'Connecting to the SingleCam stream', 'SingleCam-Stream wird verbunden'), { delayMs: 320 }); let progressFinished = false; const finishProgress = () => { if (progressFinished) return; progressFinished = true; session.video?.removeEventListener('loadeddata', onVideoReady); session.video?.removeEventListener('playing', onVideoReady); session.finishLoadProgress = null; progressTask?.finish(); }; const onVideoReady = () => finishProgress(); session.finishLoadProgress = finishProgress; session.video.addEventListener('loadeddata', onVideoReady, { once: true }); session.video.addEventListener('playing', onVideoReady, { once: true }); const connect = async (poll = false) => { if (!isCurrent()) return; if (document.hidden) { // Resume the pending initial connection/poll on activation, not on // the next 45-second polling tick. Clear before connecting so repeated // visibility events cannot start the same request twice. session.resumePendingConnection = () => { session.resumePendingConnection = null; if (isCurrent()) connect(poll); }; return; } try { if (!poll) { statusNode.textContent = tx('正在连接直播…', 'Connecting to live stream…', 'Live-Stream wird verbunden…'); progressTask?.update({ label: tx('正在读取直播状态', 'Reading live room status', 'Live-Status wird gelesen') }); } const data = await fetchSingleContext(session.id, session.abort.signal); if (!isCurrent()) return; const status = singleStatusFromContext(data); session.status = status; session.context = data; session.viewers = Math.max(0, Math.round(Number(data.num_viewers || data.viewer_count || data.viewers) || session.model.viewers || 0)); analytics.observeModel?.({ id: session.id, source: 'singlecam/context', room: { lastStatus: status, lastSeenOnline: status === 'online' ? Date.now() : 0, viewerCount: session.viewers, tags: Array.isArray(data.tags) ? data.tags : modelTags(session.model), subject: data.room_subject || data.subject || session.model.subject, country: data.country || session.model.country, location: data.location || session.model.location, gender: data.gender || session.model.gender, displayAge: data.display_age || data.age || session.model.age, followers: data.num_followers || data.followers || session.model.followers, secondsOnline: data.seconds_online || session.model.secondsOnline, isHd: data.is_hd ?? session.model.isHd, isNew: data.is_new ?? session.model.isNew, isGaming: data.is_gaming ?? session.model.isGaming, }, }); try { session.onContext?.(data, status); } catch (_) {} if (status === 'online') { progressTask?.update({ label: tx('直播已找到,正在连接视频', 'Live room found; connecting video', 'Live-Raum gefunden; Video wird verbunden') }); statusNode.textContent = `${tx('在线', 'Online', 'Online')} · ${formatCount(session.viewers)} ${tx('观众', 'viewers', 'Zuschauer')}`; if (session.source !== data.hls_source || !session.hls) { try { session.hls?.destroy(); } catch (_) {} session.hls = null; session.source = data.hls_source; if (!attachSingleStream(session, data.hls_source, qualitySelect, statusNode)) finishProgress(); } if (!session.analyticsStarted) { session.analyticsStarted = true; analytics.start(() => singleSession === session ? [{ id: session.id, video: session.video, room: { lastStatus: 'online', lastSeenOnline: Date.now(), viewerCount: session.viewers, tags: modelTags(session.model) } }] : []); } } else { finishProgress(); try { session.hls?.destroy(); } catch (_) {} session.hls = null; stopMediaElement(session.video, false); analytics.stop(); session.analyticsStarted = false; statusNode.textContent = status === 'private' ? tx('当前不是公开直播。', 'The room is not public right now.', 'Der Raum ist gerade nicht öffentlich.') : tx('当前离线。下面显示相同筛选中的替代项。', 'Currently offline. Alternatives from the same filter are shown below.', 'Derzeit offline. Unten stehen Alternativen aus demselben Filter.'); } } catch (_) { if (isCurrent() && !session.abort.signal.aborted) { statusNode.textContent = tx('状态暂时无法读取;将自动重试。', 'Status temporarily unavailable; retrying automatically.', 'Status vorübergehend nicht verfügbar; automatischer neuer Versuch folgt.'); finishProgress(); } } finally { if (isCurrent()) session.pollTimer = setTimeout(() => connect(true), Math.max(15000, (Number(session.preferences?.pollSeconds) || 45) * 1000)); } }; connect(false); } function publicProfileNode(id, model) { const host = $('div', { class: 'single-panel' }, [$('h3', {}, tx('公开资料', 'Public profile', 'Öffentliches Profil')), $('p', { style: { color: 'var(--dc-muted)', fontSize: '11px' } }, tx('正在读取公开资料…', 'Loading public profile…', 'Öffentliches Profil wird geladen…'))]); const render = (profile, failed = false) => { if (!profile) { host.replaceChildren($('h3', {}, tx('公开资料', 'Public profile', 'Öffentliches Profil')), $('p', { style: { color: 'var(--dc-muted)', fontSize: '11px' } }, failed ? tx('资料暂时不可用。', 'Profile temporarily unavailable.', 'Profil vorübergehend nicht verfügbar.') : tx('没有缓存资料。', 'No cached profile.', 'Kein zwischengespeichertes Profil.'))); return; } const groupedLinks = new Map(); for (const raw of profile.links || []) { try { const url = new URL(raw); if (!['http:', 'https:'].includes(url.protocol) || url.hostname === location.hostname) continue; if (!groupedLinks.has(url.hostname)) groupedLinks.set(url.hostname, []); groupedLinks.get(url.hostname).push(url.toString()); } catch (_) {} } const serviceLabel = hostname => { const hostName = String(hostname || '').replace(/^www\./iu, '').toLocaleLowerCase(); if (hostName === 'x.com' || hostName.endsWith('.x.com') || hostName.includes('twitter.com')) return 'X / Twitter'; if (hostName.includes('instagram.com')) return 'Instagram'; if (hostName.includes('tiktok.com')) return 'TikTok'; if (hostName.includes('youtube.com') || hostName === 'youtu.be') return 'YouTube'; if (hostName.includes('onlyfans.com')) return 'OnlyFans'; if (hostName.includes('snapchat.com')) return 'Snapchat'; if (hostName.includes('linktr.ee')) return 'Linktree'; return hostname; }; const links = $('div', { class: 'single-links' }, [...groupedLinks].flatMap(([hostname, urls]) => [ $('strong', { style: { fontSize: '10px' } }, serviceLabel(hostname)), ...urls.map(url => $('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, url)), ])); const sections = (profile.sections || []).map(section => $('section', { class: 'single-tag-block' }, [ $('strong', {}, section.label), $('p', { style: { margin: 0, color: 'var(--dc-sub)', fontSize: '11px', whiteSpace: 'pre-wrap', lineHeight: '1.5' } }, section.text), ])); const profileNodes = [ $('h3', {}, profile.displayName || id), profileChanges.get(id) ? $('span', { class: 'discover-chip', style: { color: 'var(--dc-warn)', marginBottom: '8px' } }, tx('公开资料已更改', 'Public profile changed', 'Öffentliches Profil seit dem letzten Abruf geändert')) : null, $('p', { style: { color: 'var(--dc-sub)', fontSize: '12px', whiteSpace: 'pre-wrap', lineHeight: '1.5' } }, profile.bio || tx('没有公开简介。', 'No public bio.', 'Keine öffentliche Bio.')), ...sections, $('div', { class: 'discover-tags' }, (profile.tags || []).map(tag => $('button', { class: 'discover-tag', type: 'button', onclick: () => openTag(tag) }, `#${tag}`))), links, $('p', { style: { margin: '10px 0 0', color: 'var(--dc-muted)', fontSize: '10px', lineHeight: '1.45' } }, `${tx('来源', 'Source', 'Quelle')}: ${(() => { try { return new URL(profile.sourceUrl || `/${id}/`, location.origin).toString(); } catch (_) { return new URL(`/${id}/`, location.origin).toString(); } })()} · ${tx('读取时间', 'Observed', 'Abgerufen')}: ${profile.observedAt ? new Date(profile.observedAt).toLocaleString() : '—'}`), groupedLinks.size ? $('p', { style: { margin: '7px 0 0', color: 'var(--dc-muted)', fontSize: '10px', lineHeight: '1.45' } }, tx('只显示在公开资料页上自行发布的链接;相同用户名不能证明身份。', 'Only links self-published on the public profile are shown. A matching username alone is not proof of identity.', 'Es werden nur auf der öffentlichen Profilseite selbst veröffentlichte Links gezeigt. Ein gleicher Benutzername allein ist kein Identitätsnachweis.')) : null, ].filter(Boolean); host.replaceChildren(...profileNodes); }; const refreshButton = $('button', { type: 'button', class: 'suite-action' }, t('publicProfileRefresh')); const cachedProfile = analytics.isPrivateSession() ? null : analytics.getProfileSync(id); let cached = Number(cachedProfile?.parserVersion) >= 2 ? cachedProfile : null; if (cached) render(cached); const refreshProfile = async (force = false) => { if (!force && cached && cached.parserVersion >= 3 && Date.now() - Number(cached.observedAt || 0) < PUBLIC_PROFILE_FRESH_MS && Number(cached.observedAt) <= Date.now()) return cached; if (!force && Date.now() < Number(publicProfileRetryAt.get(id) || 0)) return cached; if (publicProfileRequests.has(id)) return publicProfileRequests.get(id); refreshButton.disabled = true; const pending = (async () => { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { const response = await fetch(`/${encodeURIComponent(id)}/`, { credentials: 'include', cache: 'no-store', referrerPolicy: 'strict-origin-when-cross-origin', signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const html = await response.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const cleanProfileText = (node, max = 3000) => { if (!node) return ''; const copy = node.cloneNode(true); copy.querySelectorAll('a, script, style, noscript, svg').forEach(child => child.remove()); return discoverCleanText(copy.textContent || '', max); }; const profileRoot = doc.querySelector('#roomTabs, [data-testid="room-bio"], [data-testid*="profile-tabs" i], [class*="roomBio" i]'); const sectionNodes = [...new Set([ ...doc.querySelectorAll('[data-testid^="bio-tab-"][data-testid$="-value"]'), ...(profileRoot?.querySelectorAll('[data-testid*="bio-tab" i][data-testid*="value" i], [data-testid*="about" i][data-testid*="value" i], [data-testid*="wish" i][data-testid*="value" i], [data-testid*="interest" i][data-testid*="value" i], [data-testid*="rule" i][data-testid*="value" i]') || []), ])]; // A login/error page with HTTP 200 must not replace a saved profile. if (!profileRoot && !sectionNodes.length) throw new Error('Public profile not recognized'); const sectionLabel = node => { const token = String(node.getAttribute('data-testid') || node.id || '') .replace(/^bio-tab-/iu, '').replace(/-value$/iu, '').replace(/[-_]+/gu, ' ').trim(); const nearby = node.previousElementSibling?.textContent || node.closest('section,article,div')?.querySelector('h1,h2,h3,h4,label,strong')?.textContent || ''; return discoverCleanText(token || nearby || tx('简介', 'Profile information', 'Profilinformation'), 100); }; const allSections = []; const sectionKeys = new Set(); for (const node of sectionNodes) { const testId = String(node.getAttribute('data-testid') || '').toLocaleLowerCase(); if (testId.includes('real-name')) continue; const text = cleanProfileText(node); const label = sectionLabel(node); const key = `${label.toLocaleLowerCase()}\n${text.toLocaleLowerCase()}`; if (!label || !text || sectionKeys.has(key)) continue; sectionKeys.add(key); allSections.push({ label, text }); } const aboutSection = allSections.find(section => /about|bio|über mich|sobre m[ií]|acerca|简介/iu.test(section.label)); const bio = (aboutSection?.text || cleanProfileText(doc.querySelector('[data-testid="bio-tab-about-me-value"], [data-testid*="about" i][data-testid*="value" i]'), 4000)).slice(0, 4000); const sections = allSections.filter(section => section !== aboutSection && section.text !== bio).slice(0, 16); const displayName = discoverCleanText(doc.querySelector('[data-testid="bio-tab-real-name-value"], [data-testid*="real-name" i][data-testid*="value" i]')?.textContent || id, 160) || id; const trustedLinkRoots = [...new Set([profileRoot, ...sectionNodes].filter(Boolean))]; const links = [...new Set(trustedLinkRoots.flatMap(root => [...root.querySelectorAll('a[href]')]).map(anchor => { try { const candidateUrl = new URL(anchor.getAttribute('href'), location.origin); if (candidateUrl.origin === location.origin && candidateUrl.pathname === '/external_link/') return new URL(candidateUrl.searchParams.get('url') || '').toString(); return candidateUrl.toString(); } catch (_) { return ''; } }).filter(url => { try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol) && parsed.hostname !== location.hostname; } catch (_) { return false; } }))].slice(0, 40); const tags = [...doc.querySelectorAll('a[href*="/tags/"]')].map(anchor => discoverCleanText(anchor.textContent, 50).toLocaleLowerCase()).filter(Boolean).slice(0, 80); const candidate = { id, parserVersion: 3, displayName, bio, sections, links, tags, sourceUrl: `/${encodeURIComponent(id)}/`, observedAt: Date.now(), viewerCount: model.viewers, lastStatus: model.currentShow === 'public' ? 'online' : model.currentShow }; if (cached) { const comparable = value => JSON.stringify({ displayName: value.displayName, bio: value.bio, sections: value.sections || [], links: value.links || [], tags: value.tags || [] }); if (comparable(cached) !== comparable(candidate)) { profileChanges.set(id, true); notificationCenter.observeProfileChange(id); } } if (!analytics.isPrivateSession()) await analytics.upsertProfile(candidate); publicProfileRetryAt.delete(id); return candidate; } catch (_) { publicProfileRetryAt.set(id, Date.now() + 60000); return cached; } finally { clearTimeout(timeout); } })(); publicProfileRequests.set(id, pending); try { return await pending; } finally { if (publicProfileRequests.get(id) === pending) publicProfileRequests.delete(id); refreshButton.disabled = false; } }; const showProfile = profile => { if (!host.isConnected || destroyed) return; if (profile) cached = profile; render(profile, !profile); host.append(refreshButton); }; refreshButton.addEventListener('click', () => { refreshButton.disabled = true; refreshProfile(true).then(showProfile).finally(() => { refreshButton.disabled = false; }); }); void (async () => { if (!cached) { void analytics.init(store.state.settings.analyticsEnabled !== false); const localProfile = await analytics.getProfile(id); if (Number(localProfile?.parserVersion) >= 2) cached = localProfile; } if (cached) showProfile(cached); showProfile(await refreshProfile()); })(); return host; } function renderSingleCam() { cleanupSingleSession(); previewScheduler.clear(); const id = normalizeUsername(state.singleId); if (!isLikelyUsername(id)) { setView(singleReturnView); return; } const preferences = singleCleanPreferences(state.singlePreferences); state.singlePreferences = preferences; const model = localModel(id); const stat = statsById.get(id) || {}; const observed = state.seenModels[id] || {}; const contextIndex = Math.max(0, singleContextIds.indexOf(id)); const previousId = contextIndex > 0 ? singleContextIds[contextIndex - 1] : ''; const nextId = contextIndex < singleContextIds.length - 1 ? singleContextIds[contextIndex + 1] : ''; const rememberedVolume = Number.isFinite(Number(roomFor(id)?.volume)) ? Number(roomFor(id).volume) : 0.5; const video = $('video', { class: 'single-video', playsInline: true, autoplay: preferences.autoplay, muted: preferences.startMuted, crossOrigin: 'anonymous' }); video.volume = Math.min(preferences.maxVolume, Math.max(0, rememberedVolume)); video.style.objectFit = preferences.fit; const statusNode = $('div', { class: 'single-player-state' }, tx('正在准备…', 'Preparing…', 'Wird vorbereitet…')); const stage = $('div', { class: 'single-stage' }, [video, statusNode]); const playBtn = $('button', { class: 'discover-btn', type: 'button' }, preferences.autoplay ? tx('暂停', 'Pause', 'Pause') : tx('播放', 'Play', 'Abspielen')); const muteBtn = $('button', { class: 'discover-btn', type: 'button' }, video.muted ? tx('开启声音', 'Unmute', 'Ton an') : tx('静音', 'Mute', 'Stumm')); const volume = $('input', { class: 'single-volume', type: 'range', min: '0', max: String(preferences.maxVolume), step: '0.01', value: String(video.volume), title: tx('音量', 'Volume', 'Lautstärke') }); const quality = $('select', { class: 'discover-input', title: tx('画质', 'Quality', 'Qualität'), style: { width: '145px' } }, [$('option', { value: '0' }, tx('自动画质', 'Auto quality', 'Qualität automatisch'))]); const fit = $('select', { class: 'discover-input', title: tx('画面适配', 'Image fit', 'Bildanpassung'), style: { width: '120px' } }, [$('option', { value: 'contain', selected: preferences.fit === 'contain' }, tx('完整画面', 'Contain', 'Einpassen')), $('option', { value: 'cover', selected: preferences.fit === 'cover' }, tx('填满', 'Cover', 'Ausfüllen'))]); const savedTransform = sanitizeVideoTransform(store.state.settings.videoTransforms?.[id]); let mirrored = savedTransform.mirror; let flipped = savedTransform.flip; let rotation = savedTransform.rotation; let zoom = savedTransform.zoom; let panX = savedTransform.x; let panY = savedTransform.y; let brightness = savedTransform.brightness; let contrast = savedTransform.contrast; let saturation = savedTransform.saturation; let hue = savedTransform.hue; let sepia = savedTransform.sepia; let grayscale = savedTransform.grayscale; let invert = savedTransform.invert; let blur = savedTransform.blur; let showOriginal = false; const preferenceTargets = { page: null, local: null, profile: null, alternatives: null, imageTools: null, notesTab: null, profileTab: null, chatTab: null, chatPanel: null, chatFrame: null }; let activateSingleTab = () => {}; let prepareSingleAlternatives = () => {}; const applyPagePreferences = () => { const page = preferenceTargets.page; if (!page) return; page.dataset.layout = preferences.layout; page.dataset.sidebar = preferences.sidebar; page.dataset.aspect = preferences.aspect; page.dataset.background = preferences.background; page.dataset.stageSize = preferences.stageSize; page.dataset.sidebarCollapsed = String(preferences.sidebarCollapsed); page.dataset.toolsCollapsed = String(preferences.toolsCollapsed); page.querySelectorAll('[data-single-module-toggle]').forEach(button => { const sidebar = button.dataset.singleModuleToggle === 'sidebar'; const collapsed = sidebar ? preferences.sidebarCollapsed : preferences.toolsCollapsed; button.textContent = sidebar ? (collapsed ? tx('显示侧栏', 'Show sidebar', 'Seitenleiste einblenden') : tx('隐藏侧栏', 'Hide sidebar', 'Seitenleiste ausblenden')) : (collapsed ? tx('显示工具', 'Show tools', 'Werkzeuge einblenden') : tx('隐藏工具', 'Hide tools', 'Werkzeuge ausblenden')); button.setAttribute('aria-expanded', String(!collapsed)); }); const sizeSelect = page.querySelector('[data-single-preference="stageSize"]'); if (sizeSelect) sizeSelect.value = preferences.stageSize; page.style.setProperty('--single-stream-width', `${preferences.streamWidth}%`); page.style.setProperty('--single-stream-height', `${preferences.streamHeight}vh`); page.dataset.cockpitSize = preferences.cockpitSize; page.dataset.controlSize = preferences.controlSize; page.dataset.playerCorners = preferences.playerCorners; page.dataset.compactControls = String(preferences.compactControls); if (preferenceTargets.notesTab) preferenceTargets.notesTab.hidden = !preferences.showLocal; if (preferenceTargets.profileTab) preferenceTargets.profileTab.hidden = !preferences.showProfile; if (preferenceTargets.local) preferenceTargets.local.hidden = !preferences.showLocal; if (preferenceTargets.profile) preferenceTargets.profile.hidden = !preferences.showProfile; if (preferenceTargets.alternatives) { preferenceTargets.alternatives.hidden = !preferences.showAlternatives; if (preferences.showAlternatives) prepareSingleAlternatives(); } if (preferenceTargets.chatTab) preferenceTargets.chatTab.hidden = !preferences.showChat; if (preferenceTargets.chatPanel) preferenceTargets.chatPanel.hidden = !preferences.showChat; if (preferenceTargets.imageTools) preferenceTargets.imageTools.open = !preferences.compactControls; statusNode.hidden = !preferences.showPlayerStatus; video.style.objectFit = preferences.fit; volume.max = String(preferences.maxVolume); if (video.volume > preferences.maxVolume) { video.volume = preferences.maxVolume; volume.value = String(preferences.maxVolume); } }; const persistSinglePreferences = patch => { Object.assign(preferences, patch || {}); Object.assign(preferences, singleCleanPreferences(preferences)); state.singlePreferences = { ...preferences }; if (singleSession?.id === id) singleSession.preferences = { ...preferences }; persistDiscoverState(); applyPagePreferences(); }; const streamSizeControls = $('details', { class: 'single-zoom-control', dataset: { singleStreamSize: '1' } }, [ $('summary', {}, tx('直播尺寸', 'Stream size', 'Streamgröße')), $('small', {}, tx('独立调整宽度和高度;画面适应窗口。', 'Adjust width and height independently; the player adapts to the window.', 'Breite und Höhe frei einstellen. Prozentwerte passen sich dem Fenster an; „Einpassen“ zeigt das vollständige Bild. Nicht mit Bildzoom verwechseln.')), ]); for (const [key, label, min, max, unit] of [ ['streamWidth', tx('宽度', 'Width', 'Breite'), 30, 100, tx('% 播放器区域', '% of player area', '% des Playerbereichs')], ['streamHeight', tx('高度', 'Height', 'Höhe'), 20, 100, tx('% 窗口高度', '% of window height', '% der Fensterhöhe')], ]) { const output = $('output', {}, `${preferences[key]} ${unit}`); const input = $('input', { type: 'range', min: String(min), max: String(max), step: '1', value: String(preferences[key]), class: 'single-volume', dataset: { singleStreamDimension: key } }); input.addEventListener('input', () => { output.textContent = `${input.value} ${unit}`; persistSinglePreferences({ stageSize: 'custom', [key]: Number(input.value) }); }); streamSizeControls.appendChild($('label', { class: 'discover-field' }, [$('span', {}, label), input, output])); } streamSizeControls.appendChild($('button', { class: 'discover-btn', type: 'button', onclick: () => persistSinglePreferences({ stageSize: 'large' }) }, tx('自动尺寸', 'Automatic size', 'Automatische Größe'))); const brightnessInput = $('input', { class: 'single-volume', type: 'range', min: '.4', max: '1.8', step: '.05', value: String(brightness), title: tx('亮度', 'Brightness', 'Helligkeit') }); const contrastInput = $('input', { class: 'single-volume', type: 'range', min: '.4', max: '2', step: '.05', value: String(contrast), title: tx('对比度', 'Contrast', 'Kontrast') }); const saturationInput = $('input', { class: 'single-volume', type: 'range', min: '0', max: '2.5', step: '.05', value: String(saturation), title: tx('饱和度', 'Saturation', 'Sättigung') }); const hueInput = $('input', { class: 'single-volume', type: 'range', min: '-180', max: '180', step: '5', value: String(hue), title: tx('色相', 'Hue', 'Farbton') }); const sepiaInput = $('input', { class: 'single-volume', type: 'range', min: '0', max: '1', step: '.05', value: String(sepia), title: tx('棕褐色', 'Sepia', 'Sepia') }); const grayscaleInput = $('input', { class: 'single-volume', type: 'range', min: '0', max: '1', step: '.05', value: String(grayscale), title: tx('灰度', 'Grayscale', 'Graustufen') }); const invertInput = $('input', { class: 'single-volume', type: 'range', min: '0', max: '1', step: '.05', value: String(invert), title: tx('反色', 'Invert', 'Invertieren') }); const blurInput = $('input', { class: 'single-volume', type: 'range', min: '0', max: '12', step: '.25', value: String(blur), title: tx('模糊', 'Blur', 'Weichzeichnen') }); const zoomInput = $('input', { class: 'single-volume', type: 'range', min: '1', max: '12', step: '.05', value: String(zoom), title: tx('画面缩放', 'Image zoom', 'Bildzoom') }); const panXInput = $('input', { class: 'single-volume', type: 'range', min: '-1500', max: '1500', step: '5', value: String(panX), title: tx('水平位置', 'Horizontal position', 'Horizontale Position') }); const panYInput = $('input', { class: 'single-volume', type: 'range', min: '-1500', max: '1500', step: '5', value: String(panY), title: tx('垂直位置', 'Vertical position', 'Vertikale Position') }); const zoomValue = $('strong', { class: 'single-zoom-value', dataset: { singleZoomValue: '1' } }, `${Math.round(zoom * 100)} %`); const quickZoomValue = $('strong', { class: 'single-zoom-value', dataset: { singleZoomQuickValue: '1' } }, `${Math.round(zoom * 100)} %`); const persistTransform = () => store.patchSettings({ videoTransforms: { ...(store.state.settings.videoTransforms || {}), [id]: sanitizeVideoTransform({ ...savedTransform, mirror: mirrored, flip: flipped, rotation, zoom, x: panX, y: panY, brightness, contrast, saturation, hue, sepia, grayscale, invert, blur }) } }); const applyTransform = () => { const transforms = []; if (panX || panY) transforms.push(`translate(${panX}px, ${panY}px)`); if (zoom !== 1) transforms.push(`scale(${zoom})`); if (mirrored || flipped) transforms.push(`scale(${mirrored ? -1 : 1}, ${flipped ? -1 : 1})`); if (rotation) transforms.push(`rotate(${rotation}deg)`); video.style.transform = transforms.join(' '); video.style.transformOrigin = 'center center'; video.style.cursor = zoom > 1 ? 'grab' : ''; video.style.filter = showOriginal ? 'none' : `brightness(${brightness}) contrast(${contrast}) saturate(${saturation}) hue-rotate(${hue}deg) sepia(${sepia}) grayscale(${grayscale}) invert(${invert}) blur(${blur}px)`; zoomInput.value = String(zoom); panXInput.value = String(panX); panYInput.value = String(panY); zoomValue.textContent = `${Math.round(zoom * 100)} %`; quickZoomValue.textContent = zoomValue.textContent; }; applyTransform(); const syncPlaybackButtons = () => { playBtn.textContent = video.paused ? tx('播放', 'Play', 'Abspielen') : tx('暂停', 'Pause', 'Pause'); muteBtn.textContent = video.muted ? tx('开启声音', 'Unmute', 'Ton an') : tx('静音', 'Mute', 'Stumm'); }; playBtn.addEventListener('click', () => { if (video.paused) video.play().catch(() => {}); else video.pause(); syncPlaybackButtons(); }); muteBtn.addEventListener('click', () => { video.muted = !video.muted; syncPlaybackButtons(); }); volume.addEventListener('input', () => { video.volume = Math.max(0, Math.min(1, Number(volume.value) || 0)); if (!video.volume) video.muted = true; }); volume.addEventListener('change', () => { if (preferences.rememberVolume) { ensureSaved(id); store.patchRoom(id, { volume: video.volume }); } syncPlaybackButtons(); }); fit.addEventListener('change', () => persistSinglePreferences({ fit: fit.value })); video.addEventListener('play', syncPlaybackButtons); video.addEventListener('pause', syncPlaybackButtons); brightnessInput.addEventListener('input', () => { brightness = Number(brightnessInput.value) || 1; applyTransform(); }); contrastInput.addEventListener('input', () => { contrast = Number(contrastInput.value) || 1; applyTransform(); }); saturationInput.addEventListener('input', () => { saturation = Number(saturationInput.value); applyTransform(); }); hueInput.addEventListener('input', () => { hue = Number(hueInput.value) || 0; applyTransform(); }); sepiaInput.addEventListener('input', () => { sepia = Number(sepiaInput.value) || 0; applyTransform(); }); grayscaleInput.addEventListener('input', () => { grayscale = Number(grayscaleInput.value) || 0; applyTransform(); }); invertInput.addEventListener('input', () => { invert = Number(invertInput.value) || 0; applyTransform(); }); blurInput.addEventListener('input', () => { blur = Number(blurInput.value) || 0; applyTransform(); }); zoomInput.addEventListener('input', () => { zoom = Math.max(1, Math.min(12, Number(zoomInput.value) || 1)); if (zoom === 1) { panX = 0; panY = 0; } applyTransform(); }); panXInput.addEventListener('input', () => { panX = Number(panXInput.value) || 0; applyTransform(); }); panYInput.addEventListener('input', () => { panY = Number(panYInput.value) || 0; applyTransform(); }); [brightnessInput, contrastInput, saturationInput, hueInput, sepiaInput, grayscaleInput, invertInput, blurInput, zoomInput, panXInput, panYInput].forEach(control => control.addEventListener('change', persistTransform)); let panning = false; let pointerX = 0; let pointerY = 0; stage.addEventListener('wheel', event => { if (!event.ctrlKey && !event.metaKey) return; event.preventDefault(); zoom = Math.max(1, Math.min(12, Math.round(zoom * (event.deltaY < 0 ? 1.14 : .88) * 100) / 100)); if (zoom === 1) { panX = 0; panY = 0; } applyTransform(); persistTransform(); }, { passive: false }); stage.addEventListener('pointerdown', event => { if (event.button !== 0 || zoom <= 1 || event.target.closest?.('button,input,select,a')) return; panning = true; pointerX = event.clientX; pointerY = event.clientY; video.style.cursor = 'grabbing'; try { stage.setPointerCapture(event.pointerId); } catch (_) {} event.preventDefault(); }); stage.addEventListener('pointermove', event => { if (!panning) return; panX = Math.max(-5000, Math.min(5000, panX + event.clientX - pointerX)); panY = Math.max(-5000, Math.min(5000, panY + event.clientY - pointerY)); pointerX = event.clientX; pointerY = event.clientY; applyTransform(); }); const finishPan = () => { if (!panning) return; panning = false; applyTransform(); persistTransform(); }; stage.addEventListener('pointerup', finishPan); stage.addEventListener('pointercancel', finishPan); stage.addEventListener('dblclick', event => { if (event.target === video || event.target === stage) { zoom = 1; panX = 0; panY = 0; applyTransform(); persistTransform(); } }); const setZoom = value => { zoom = Math.max(1, Math.min(12, Number(value) || 1)); if (zoom === 1) { panX = 0; panY = 0; } applyTransform(); persistTransform(); }; const screenshotFormat = $('select', { class: 'discover-input', dataset: { singleScreenshotFormat: '1' } }, [ ['png', 'PNG · verlustfrei'], ['jpeg', 'JPEG · kleiner'], ['webp', 'WebP · effizient'], ].map(([value, label]) => $('option', { value, selected: preferences.screenshotFormat === value }, label))); const screenshotHeight = $('select', { class: 'discover-input', dataset: { singleScreenshotHeight: '1' } }, [ ['0', tx('源分辨率', 'Source resolution', 'Originalauflösung')], ['2160', '4K · 2160p'], ['1440', 'QHD · 1440p'], ['1080', 'Full HD · 1080p'], ['720', 'HD · 720p'], ].map(([value, label]) => $('option', { value, selected: Number(value) === preferences.screenshotHeight }, label))); const recordingBitrate = $('select', { class: 'discover-input', dataset: { singleRecordingBitrate: '1' } }, [2, 4, 6, 8, 12, 16, 20].map(value => $('option', { value: String(value * 1000000), selected: Number(store.state.settings.recordingVideoBitrate) === value * 1000000 }, `${value} Mbit/s`))); const recordingSegment = $('select', { class: 'discover-input', dataset: { singleRecordingSegment: '1' } }, [1, 5, 10, 20, 30, 60].map(value => $('option', { value: String(value), selected: Number(store.state.settings.recordingSegmentMinutes) === value }, `${value} min`))); const screenshotGallery = $('input', { type: 'checkbox', checked: true, disabled: true, dataset: { singleScreenshotGallery: '1' } }); const screenshotCover = $('input', { type: 'checkbox', checked: preferences.screenshotMakeCover, dataset: { singleScreenshotCover: '1' } }); const captureStatus = $('div', { class: 'single-capture-status', dataset: { singleCaptureStatus: '1' }, role: 'status', ariaLive: 'polite' }, [ $('strong', {}, tx('准备就绪', 'Ready', 'Bereit')), $('span', {}, tx('截图和录制片段会直接保存到此模型的本地图库;电脑下载会独立进行。', 'Screenshots and recording segments are saved directly to this model’s local gallery; computer downloads run independently.', 'Screenshots und Aufnahme-Segmente werden direkt in der lokalen Galerie dieses Models gespeichert; der PC-Download läuft unabhängig davon.')), ]); const captureStatusTitle = captureStatus.querySelector('strong'); const captureStatusDetail = captureStatus.querySelector('span'); const screenshotButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleScreenshot: '1' } }, `▣ ${tx('截图', 'Screenshot', 'Screenshot')}`); const recordButton = $('button', { class: 'discover-btn single-record-button', type: 'button', dataset: { singleRecord: '1' } }, `● ${tx('开始录制', 'Start recording', 'Aufnahme starten')}`); const captureDirectoryBadge = $('span', { class: 'discover-chip', dataset: { singleCaptureDirectoryBadge: '1' } }, tx('媒体文件夹:正在检查…', 'Media folder: checking…', 'Medienordner: wird geprüft …')); const captureStorageStatus = $('span', { class: 'single-note-status', dataset: { singleCaptureStorage: '1' }, role: 'status', ariaLive: 'polite' }); const captureState = { active: false, recorder: null, stream: null, chunks: [], segmentTimer: 0, ticker: 0, overallStartedAt: 0, segmentStartedAt: 0, part: 0, stopReason: '', silent: false }; const setCaptureStatus = (title, detail = '', recording = false) => { captureStatusTitle.textContent = title; captureStatusDetail.textContent = detail; captureStatus.classList.toggle('is-recording', recording); recordButton.classList.toggle('is-recording', recording); recordButton.textContent = recording ? `■ ${tx('停止并保存', 'Stop and save', 'Stoppen und speichern')}` : `● ${tx('开始录制', 'Start recording', 'Aufnahme starten')}`; }; const durationText = ms => { const seconds = Math.max(0, Math.floor((Number(ms) || 0) / 1000)); const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const rest = seconds % 60; return hours ? `${hours}:${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}` : `${minutes}:${String(rest).padStart(2, '0')}`; }; const availableRecordingMime = () => { if (typeof MediaRecorder === 'undefined') return ''; return ['video/mp4;codecs=h264,aac', 'video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm'] .find(type => { try { return MediaRecorder.isTypeSupported(type); } catch (_) { return false; } }) || ''; }; const getSingleCaptureStream = () => { const capture = video.captureStream || video.mozCaptureStream || video.webkitCaptureStream; if (typeof capture !== 'function') return null; try { const source = capture.call(video); const tracks = [...(source?.getVideoTracks?.() || []), ...(source?.getAudioTracks?.() || [])].filter(track => track.readyState === 'live'); return tracks.some(track => track.kind === 'video') ? new MediaStream(tracks) : null; } catch (_) { return null; } }; const cleanupCaptureStream = () => { try { captureState.stream?.getTracks?.().forEach(track => track.stop()); } catch (_) {} captureState.stream = null; }; const saveCaptureBlob = async (blob, fileName, kind) => { let gallerySaved = false; try { if (kind === 'video') await RoomGridModelMedia.importVideo(id, blob, { fileName }); else await RoomGridModelMedia.importFile(id, blob, { quality: 'original', fileName, makeCover: screenshotCover.checked, keepDuplicate: true }); gallerySaved = true; } catch (error) { toast(tx('媒体未能保存到本地图库;电脑下载仍会执行。', 'Media could not be saved to the local gallery; the computer download will still proceed.', 'Medium konnte nicht dauerhaft in der lokalen Galerie gespeichert werden; der Download auf den PC wird trotzdem ausgeführt.')); try { Diagnostics.report('single-gallery-save', error, id, 'warning'); } catch (_) {} } if (singleSession === session) { session.lastCaptureSave = { fileName, gallerySaved, pc: 'pending' }; captureStorageStatus.textContent = `${fileName} · ${gallerySaved ? tx('已保存在模型图库', 'Saved in model gallery', 'In Modelgalerie gespeichert') : tx('仅当前会话,图库保存失败', 'Session only; gallery save failed', 'Nur Sitzung; Galeriespeicherung fehlgeschlagen')} · ${tx('电脑保存待完成', 'PC save pending', 'PC-Speicherung wartet')}`; session.recentCaptures = [ { name: fileName, blob, kind, type: blob.type, size: blob.size, lastModified: Date.now(), source: gallerySaved ? 'gallery' : 'session-only' }, ...(session.recentCaptures || []).filter(item => item.name !== fileName), ].slice(0, 24); await session.refreshMedia?.(true); if (singleSession === session) activateSingleTab('media'); } // A save picker, slow disk or unresolved GM.download must not delay // the model gallery, the capture button or the next recording segment. (async () => { let directoryResult = { saved: false, reason: 'unavailable' }; try { directoryResult = await RoomGridCaptureDirectory.save(id, fileName, blob); } catch (_) {} const downloadResult = directoryResult.saved ? null : await downloadBlobToSubfolder(blob, fileName, `Camclient/${id}`); if (singleSession === session && session.lastCaptureSave?.fileName === fileName) { session.lastCaptureSave.pc = directoryResult.saved || downloadResult?.browserApi ? 'saved' : 'submitted'; captureStorageStatus.textContent = `${fileName} · ${gallerySaved ? tx('已保存在模型图库', 'Saved in model gallery', 'In Modelgalerie gespeichert') : tx('仅当前会话', 'Session only', 'Nur Sitzung')} · ${session.lastCaptureSave.pc === 'saved' ? tx('电脑保存完成', 'PC save completed', 'PC-Speicherung abgeschlossen') : tx('下载已交给浏览器;请检查下载', 'Download submitted to browser; check Downloads', 'Download an Browser übergeben; Downloads prüfen')}`; } })().catch(error => { if (singleSession === session && session.lastCaptureSave?.fileName === fileName) captureStorageStatus.textContent = `${fileName} · ${gallerySaved ? tx('模型图库已保存', 'Model gallery saved', 'Modelgalerie gespeichert') : tx('仅当前会话', 'Session only', 'Nur Sitzung')} · ${tx('电脑保存失败', 'PC save failed', 'PC-Speicherung fehlgeschlagen')}`; try { Diagnostics.report('single-capture-download', error, id, 'warning'); } catch (_) {} }); return { gallerySaved }; }; const saveCaptureSegment = async (chunks, mimeType, reason) => { const usable = (chunks || []).filter(chunk => chunk?.size); if (!usable.length) return false; const type = mimeType || 'video/webm'; const blob = new Blob(usable, { type }); const ext = type.includes('mp4') ? 'mp4' : 'webm'; const part = String(captureState.part).padStart(3, '0'); const fileName = `camclient-${safeFilePart(id)}-${stampForFile()}-part${part}.${ext}`; const result = await saveCaptureBlob(blob, fileName, 'video'); captureState.gallerySaveFailed = !result.gallerySaved; analytics.recordEvent?.({ type: 'recording', id, at: Date.now(), detail: `${reason}:${blob.size}` }); setCaptureStatus( result.gallerySaved ? tx('录制已保存', 'Recording saved', 'Aufnahme gespeichert') : tx('图库保存失败', 'Gallery save failed', 'Galerie-Speicherung fehlgeschlagen'), result.gallerySaved ? `${tx('图库', 'Gallery', 'Galerie')} · ${id} · ${fileName}` : tx('仅当前会话可用;电脑下载仍会执行。', 'Available only in this session; the computer download will still proceed.', 'Nur in dieser Sitzung verfügbar; der PC-Download wird weiterhin versucht.'), ); return result.gallerySaved; }; const stopRecordingSegment = reason => { clearTimeout(captureState.segmentTimer); captureState.segmentTimer = 0; captureState.stopReason = reason; const recorder = captureState.recorder; if (recorder && recorder.state !== 'inactive') { recordButton.disabled = true; try { recorder.requestData?.(); } catch (_) {} try { recorder.stop(); } catch (_) {} } }; const startRecordingSegment = () => { if (!captureState.active || singleSession?.id !== id) return false; const stream = getSingleCaptureStream(); const mimeType = availableRecordingMime(); if (!stream || typeof MediaRecorder === 'undefined') { cleanupCaptureStream(); return false; } const options = { videoBitsPerSecond: Math.max(500000, Math.min(20000000, Number(recordingBitrate.value) || 6000000)) }; if (mimeType) options.mimeType = mimeType; let recorder; try { recorder = new MediaRecorder(stream, options); } catch (_) { try { stream.getTracks().forEach(track => track.stop()); } catch (_) {} return false; } captureState.stream = stream; captureState.recorder = recorder; captureState.chunks = []; captureState.part += 1; captureState.segmentStartedAt = Date.now(); captureState.stopReason = ''; recorder.ondataavailable = event => { if (event.data?.size) captureState.chunks.push(event.data); }; recorder.onstop = async () => { const reason = captureState.stopReason || 'source-ended'; const saved = await saveCaptureSegment(captureState.chunks, recorder.mimeType || mimeType, reason); captureState.recorder = null; captureState.chunks = []; cleanupCaptureStream(); recordButton.disabled = false; if (captureState.active && reason === 'segment' && singleSession?.id === id) { setCaptureStatus(tx('正在继续录制', 'Continuing recording', 'Aufnahme wird fortgesetzt'), `${tx('已保存片段', 'Saved segment', 'Segment gespeichert')} ${captureState.part}`, true); setTimeout(() => { if (!startRecordingSegment()) captureState.stop(false, true); }, 120); } else if (!captureState.active) { clearInterval(captureState.ticker); captureState.ticker = 0; setCaptureStatus(saved ? tx('录制已保存', 'Recording saved', 'Aufnahme gespeichert') : captureState.gallerySaveFailed ? tx('图库保存失败', 'Gallery save failed', 'Galerie-Speicherung fehlgeschlagen') : tx('没有录制数据', 'No recording data', 'Keine Aufnahmedaten'), `${captureState.part} ${tx('片段', 'segment(s)', 'Segment(e)')}`); } else { captureState.active = false; clearInterval(captureState.ticker); captureState.ticker = 0; setCaptureStatus(tx('源已结束', 'Source ended', 'Quelle beendet'), saved ? tx('已保存可用的录制数据。', 'Available recording data was saved.', 'Verfügbare Aufnahmedaten wurden gespeichert.') : tx('图库保存失败', 'Gallery save failed', 'Galerie-Speicherung fehlgeschlagen')); } }; try { recorder.start(1000); } catch (_) { cleanupCaptureStream(); captureState.recorder = null; return false; } captureState.segmentTimer = setTimeout(() => stopRecordingSegment('segment'), Math.max(1, Number(recordingSegment.value) || 10) * 60000); return true; }; captureState.stop = (silent = false, failedStart = false) => { captureState.silent = !!silent; const hadRecorder = !!captureState.recorder; captureState.active = false; clearTimeout(captureState.segmentTimer); captureState.segmentTimer = 0; clearInterval(captureState.ticker); captureState.ticker = 0; if (hadRecorder) stopRecordingSegment('manual'); else { cleanupCaptureStream(); if (!silent) setCaptureStatus(failedStart ? tx('录制不可用', 'Recording unavailable', 'Aufnahme nicht verfügbar') : tx('录制已停止', 'Recording stopped', 'Aufnahme beendet'), tx('Brave 没有提供可录制的视频轨道。', 'Brave exposed no recordable video track.', 'Brave stellt keine aufnehmbare Videospur bereit.')); } }; captureState.beforeUnload = event => { if (!captureState.active || !store.state.settings.recordingExitWarn) return; event.preventDefault(); event.returnValue = tx('录制仍在进行。', 'Recording is still active.', 'Eine Aufnahme läuft noch.'); return event.returnValue; }; const captureScreenshot = async () => { if (!video.videoWidth || !video.videoHeight || video.readyState < 2) { setCaptureStatus(tx('截图不可用', 'Screenshot unavailable', 'Screenshot nicht verfügbar'), tx('视频还没有可用画面。', 'The video has no usable frame yet.', 'Das Video besitzt noch kein verfügbares Bild.')); return; } screenshotButton.disabled = true; try { const wantedHeight = Number(screenshotHeight.value) || video.videoHeight; const height = Math.max(1, Math.min(video.videoHeight, wantedHeight)); const width = Math.max(1, Math.round(video.videoWidth * height / video.videoHeight)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d', { alpha: false }); if (!context) throw new Error('Canvas unavailable'); context.drawImage(video, 0, 0, width, height); const format = screenshotFormat.value; const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png'; const blob = await new Promise(resolve => canvas.toBlob(resolve, mimeType, format === 'png' ? undefined : .94)); if (!(blob instanceof Blob) || !blob.size) throw new Error('Screenshot encoding failed'); const ext = format === 'jpeg' ? 'jpg' : format; const fileName = `camclient-${safeFilePart(id)}-${stampForFile()}-${crypto.randomUUID()}.${ext}`; const result = await saveCaptureBlob(blob, fileName, 'image'); analytics.recordEvent?.({ type: 'screenshot', id, at: Date.now(), detail: `${width}x${height}:${format}` }); setCaptureStatus(result.gallerySaved ? tx('截图已保存', 'Screenshot saved', 'Screenshot gespeichert') : tx('图库保存失败', 'Gallery save failed', 'Galerie-Speicherung fehlgeschlagen'), `${width} × ${height} · ${format.toUpperCase()} · ${id} · ${result.gallerySaved ? tx('图库', 'Gallery', 'Galerie') : tx('当前会话', 'Current session', 'Aktuelle Sitzung')}`); } catch (error) { setCaptureStatus(tx('截图失败', 'Screenshot failed', 'Screenshot fehlgeschlagen'), tx('Brave 或视频源阻止了画面捕获。', 'Brave or the video source blocked frame capture.', 'Brave oder die Videoquelle hat die Bilderfassung blockiert.')); try { Diagnostics.report('singlecam-screenshot', error, id, 'warning'); } catch (_) {} } finally { screenshotButton.disabled = false; } }; screenshotButton.addEventListener('click', captureScreenshot); recordButton.addEventListener('click', () => { if (captureState.active) { captureState.stop(false); return; } captureState.active = true; captureState.part = 0; captureState.overallStartedAt = Date.now(); if (!startRecordingSegment()) { captureState.stop(false, true); return; } setCaptureStatus(tx('录制中', 'Recording', 'Aufnahme läuft'), '0:00', true); captureState.ticker = setInterval(() => setCaptureStatus(tx('录制中', 'Recording', 'Aufnahme läuft'), `${durationText(Date.now() - captureState.overallStartedAt)} · ${Math.round((Number(recordingBitrate.value) || 6000000) / 1000000)} Mbit/s · ${tx('片段', 'part', 'Teil')} ${captureState.part}`, true), 500); }); screenshotFormat.addEventListener('change', () => persistSinglePreferences({ screenshotFormat: screenshotFormat.value })); screenshotHeight.addEventListener('change', () => persistSinglePreferences({ screenshotHeight: Number(screenshotHeight.value) || 0 })); screenshotCover.addEventListener('change', () => { if (screenshotCover.checked) screenshotGallery.checked = true; persistSinglePreferences({ screenshotMakeCover: screenshotCover.checked, screenshotSaveGallery: screenshotGallery.checked }); }); recordingBitrate.addEventListener('change', () => store.patchSettings({ recordingVideoBitrate: Number(recordingBitrate.value) || 6000000 })); recordingSegment.addEventListener('change', () => store.patchSettings({ recordingSegmentMinutes: Number(recordingSegment.value) || 10 })); window.addEventListener('beforeunload', captureState.beforeUnload); const captureTools = $('div', { class: 'single-capture-tools', dataset: { singleCaptureTools: '1' } }, [ screenshotButton, recordButton, $('label', { class: 'discover-field' }, [$('span', {}, tx('截图格式', 'Screenshot format', 'Screenshot-Format')), screenshotFormat]), $('label', { class: 'discover-field' }, [$('span', {}, tx('截图分辨率', 'Screenshot resolution', 'Screenshot-Auflösung')), screenshotHeight]), $('label', { class: 'discover-field' }, [$('span', {}, tx('录制质量', 'Recording quality', 'Aufnahmequalität')), recordingBitrate]), $('label', { class: 'discover-field' }, [$('span', {}, tx('自动分段', 'Automatic segments', 'Automatische Segmente')), recordingSegment]), $('label', { class: 'discover-chip' }, [screenshotGallery, ` ${tx('保存到模型图库', 'Save to model gallery', 'In Modellgalerie speichern')}`]), $('label', { class: 'discover-chip' }, [screenshotCover, ` ${tx('设为模型封面', 'Use as model cover', 'Als Modell-Cover setzen')}`]), captureDirectoryBadge, captureStatus, ]); const zoomPresets = $('div', { class: 'single-zoom-presets', dataset: { singleZoomPresets: '1' } }, [1, 1.25, 1.5, 2, 3, 4, 6, 8, 10, 12].map(value => $('button', { class: 'discover-btn', type: 'button', onclick: () => setZoom(value) }, `${Math.round(value * 100)} %`))); const imageActions = $('div', { class: 'single-image-actions' }, [ $('button', { class: 'discover-btn', type: 'button', onclick: () => { mirrored = !mirrored; applyTransform(); persistTransform(); } }, tx('镜像', 'Mirror', 'Spiegeln')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { flipped = !flipped; applyTransform(); persistTransform(); } }, tx('垂直翻转', 'Flip vertically', 'Vertikal spiegeln')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { rotation = (rotation + 90) % 360; applyTransform(); persistTransform(); } }, tx('旋转', 'Rotate', 'Drehen')), $('button', { class: 'discover-btn', type: 'button', onclick: event => { showOriginal = !showOriginal; event.currentTarget.textContent = showOriginal ? tx('显示编辑', 'Show edited', 'Bearbeitet zeigen') : tx('显示原图', 'Show original', 'Original zeigen'); applyTransform(); } }, tx('显示原图', 'Show original', 'Original zeigen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { brightness = 1; contrast = 1; saturation = 1; hue = 0; sepia = 0; grayscale = 0; invert = 0; blur = 0; mirrored = false; flipped = false; rotation = 0; zoom = 1; panX = 0; panY = 0; [[brightnessInput,brightness],[contrastInput,contrast],[saturationInput,saturation],[hueInput,hue],[sepiaInput,sepia],[grayscaleInput,grayscale],[invertInput,invert],[blurInput,blur],[zoomInput,zoom],[panXInput,panX],[panYInput,panY]].forEach(([node,value]) => { node.value = String(value); }); applyTransform(); persistTransform(); } }, tx('重置图像', 'Reset image', 'Bild zurücksetzen')), ]); const imageTools = $('details', { class: 'single-image-tools', open: !preferences.compactControls }, [ $('summary', {}, 'Bild optimieren · großer Zoom, Position, Filter und Drehung'), $('div', { class: 'single-image-grid' }, [ $('div', { class: 'single-zoom-control' }, [$('div', { class: 'single-zoom-head' }, [$('strong', {}, tx('画面缩放', 'Image zoom', 'Bildzoom')), zoomValue]), zoomInput, $('small', {}, tx('按住 Ctrl 并滚动可缩放;放大后拖动图像。双击可重置。', 'Hold Ctrl and scroll to zoom; drag the image when zoomed. Double-click to reset.', 'Mit Strg + Mausrad zoomen; das vergrößerte Bild ziehen. Doppelklick setzt zurück.')), zoomPresets]), $('label', { class: 'discover-field' }, [tx('水平位置', 'Horizontal position', 'Horizontale Position'), panXInput]), $('label', { class: 'discover-field' }, [tx('垂直位置', 'Vertical position', 'Vertikale Position'), panYInput]), $('label', { class: 'discover-field' }, [tx('亮度', 'Brightness', 'Helligkeit'), brightnessInput]), $('label', { class: 'discover-field' }, [tx('对比度', 'Contrast', 'Kontrast'), contrastInput]), $('label', { class: 'discover-field' }, [tx('饱和度', 'Saturation', 'Sättigung'), saturationInput]), $('label', { class: 'discover-field' }, [tx('色相', 'Hue', 'Farbton'), hueInput]), $('label', { class: 'discover-field' }, [tx('棕褐色', 'Sepia', 'Sepia'), sepiaInput]), $('label', { class: 'discover-field' }, [tx('灰度', 'Grayscale', 'Graustufen'), grayscaleInput]), $('label', { class: 'discover-field' }, [tx('反色', 'Invert', 'Invertieren'), invertInput]), $('label', { class: 'discover-field' }, [tx('模糊', 'Blur', 'Weichzeichnen'), blurInput]), imageActions, ]), ]); preferenceTargets.imageTools = imageTools; const primaryControls = $('div', { class: 'single-primary-controls' }, [ playBtn, muteBtn, volume, quality, fit, $('div', { class: 'single-zoom-quick' }, [ $('button', { class: 'discover-btn', type: 'button', title: tx('缩小', 'Zoom out', 'Verkleinern'), onclick: () => setZoom(Math.max(1, zoom - .25)) }, '−'), quickZoomValue, $('button', { class: 'discover-btn', type: 'button', title: tx('放大', 'Zoom in', 'Vergrößern'), onclick: () => setZoom(Math.min(12, zoom + .25)) }, '+'), $('button', { class: 'discover-btn', type: 'button', title: tx('重置缩放和位置', 'Reset zoom and position', 'Zoom und Position zurücksetzen'), onclick: () => setZoom(1) }, '1:1'), ]), $('button', { class: 'discover-btn', type: 'button', onclick: async () => { try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await video.requestPictureInPicture?.(); } catch (_) {} } }, 'PiP'), $('button', { class: 'discover-btn', type: 'button', onclick: () => openNoopener(roomGridSingleCamUrl(id, { source: 'player-window' })) }, tx('播放器窗口', 'Player window', 'Playerfenster')), $('button', { class: 'discover-btn', type: 'button', onclick: () => document.body.classList.toggle('discover-cinema') }, tx('影院模式', 'Cinema', 'Kino')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { try { stage.requestFullscreen?.(); } catch (_) {} } }, tx('全屏', 'Fullscreen', 'Vollbild')), $('button', { class: 'discover-btn', type: 'button', onclick: () => activateSingleTab('settings') }, 'Einstellungen'), ]); const singleAudioMeter = createSingleAudioMeter(video); const controls = $('div', { class: 'single-controls' }, [primaryControls, streamSizeControls, singleAudioMeter.root, captureTools, imageTools]); const session = { id, model, video, hls: null, source: '', status: 'loading', viewers: model.viewers, abort: new AbortController(), pollTimer: 0, failures: 0, quality: String(roomFor(id)?.qualityHeight || 0), analyticsStarted: false, preferences: { ...preferences }, visibilityHandler: null, soundHandler: null, captureDirectoryHandler: null, captureFileHandler: null, galleryUrls: [], backgroundPreviewUrl: '', recentCaptures: [], pausedForVisibility: false, connectionRevision: 0 }; session.audioMeter = singleAudioMeter; session.capture = captureState; singleSession = session; singleAudioMeter.start(); quality.addEventListener('change', () => { session.quality = quality.value; applySingleQuality(session, quality.value); ensureSaved(id); store.patchRoom(id, { qualityHeight: Number(quality.value) || 0 }); }); session.visibilityHandler = () => { if (document.hidden && session.preferences.pauseHidden && !video.paused) { session.pausedForVisibility = true; video.pause(); } else if (!document.hidden && session.pausedForVisibility) { session.pausedForVisibility = false; if (session.preferences.autoplay) video.play().catch(() => {}); } if (!document.hidden) session.resumePendingConnection?.(); }; document.addEventListener('visibilitychange', session.visibilityHandler); // Start the live request before building profile, gallery and tag panels. // Their completion is not a prerequisite for the stream connection. startSinglePlayer(session, quality, statusNode); const savedBtn = $('button', { class: 'discover-btn', type: 'button' }); const favoriteBtn = $('button', { class: 'discover-btn', type: 'button' }); const syncActions = () => { savedBtn.textContent = isSaved(id) ? tx('已保存', 'Saved', 'Gespeichert', 'Guardado', 'Salvo') : tx('保存', 'Save', 'Speichern', 'Guardar', 'Salvar'); favoriteBtn.textContent = `${isFavorite(id) ? '★' : '☆'} ${tx('收藏', 'Favorite', 'Favorit', 'Favorito', 'Favorito')}`; }; savedBtn.addEventListener('click', () => { if (isSaved(id)) store.removeRoom(id); else store.addRoom(id); syncActions(); }); favoriteBtn.addEventListener('click', () => { ensureSaved(id); store.toggleRoomInGroup(id, FAVORITE_GROUP_ID); syncActions(); }); syncActions(); const customGroups = store.state.groups.filter(group => ![LIBRARY_GROUP_ID, DEFAULT_GROUP_ID, ONLINE_GROUP_ID, FAVORITE_GROUP_ID].includes(group.id)); const notes = $('textarea', { class: 'single-textarea single-notebook', maxlength: 4000, placeholder: tx('私人备注、记忆或下次要查看的内容(仅保存在本机)', 'Private notes, memories, or things to check next time (stored locally)', 'Private Notizen, Erinnerungen oder Dinge für das nächste Mal (nur lokal gespeichert)'), value: roomFor(id)?.notes || '' }); const noteStatus = $('div', { class: 'single-note-status', dataset: { singleAutosaveStatus: '1' } }, tx('自动保存已启用', 'Autosave is on', 'Automatisches Speichern ist aktiv')); let tagValues = [...new Set(roomFor(id)?.localTags || [])]; const tagListId = `single-tag-suggestions-${id}`; const tagDatalist = $('datalist', { id: tagListId }); let tagSuggestionsSignature = ''; const prepareTagSuggestions = () => { const signature = `${dataCacheSignature()}:${store.revision}`; if (tagSuggestionsSignature === signature) return; const tags = new Set(currentTagIndex().keys()); store.state.rooms.forEach(room => (room.localTags || []).forEach(tag => tags.add(String(tag).trim().toLocaleLowerCase()))); modelTags(model).forEach(tag => tags.add(tag)); tagDatalist.replaceChildren(...[...tags].filter(Boolean).sort((a, b) => a.localeCompare(b)).slice(0, 3000).map(tag => $('option', { value: tag }))); tagSuggestionsSignature = signature; }; const tagInput = $('input', { class: 'discover-input', ariaLabel: `${tx('自己的标签', 'Own tags', 'Eigene Tags')}: ${id}`, placeholder: tx('输入自己的标签…', 'Enter your own tag…', 'Eigenen Tag eingeben …') }); tagInput.setAttribute('list', tagListId); tagInput.addEventListener('focus', prepareTagSuggestions); const tagChips = $('div', { class: 'single-own-tags', dataset: { singleOwnTags: '1' } }); let overviewOwnTags = null; const persistPersonal = (announce = false) => { ensureSaved(id); store.patchRoom(id, { notes: notes.value, localTags: tagValues }); noteStatus.textContent = `${tx('保存于', 'Saved at', 'Gespeichert um')} ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; noteStatus.classList.add('saved'); if (announce) toast(tx('本地详情已保存', 'Local details saved', 'Lokale Details gespeichert')); }; const autosavePersonal = debounce(() => persistPersonal(false), 550); const renderTagChips = () => { tagChips.replaceChildren(...tagValues.map(tag => $('span', { class: 'single-own-tag' }, [rawUiText(`#${tag}`), $('button', { type: 'button', title: tx('删除标签', 'Remove tag', 'Tag entfernen'), onclick: () => { tagValues = tagValues.filter(value => value !== tag); renderTagChips(); persistPersonal(false); } }, '×')]))); if (!tagValues.length) tagChips.appendChild($('span', { class: 'single-note-status' }, tx('还没有自己的标签。', 'No personal tags yet.', 'Noch keine eigenen Tags.'))); if (overviewOwnTags) { overviewOwnTags.replaceChildren(...tagValues.map(tag => $('span', { class: 'single-own-tag' }, `#${tag}`))); if (!tagValues.length) overviewOwnTags.appendChild($('span', { class: 'single-note-status' }, tx('还没有自己的标签。', 'No personal tags yet.', 'Noch keine eigenen Tags.'))); } }; const addTag = (sourceInput = tagInput) => { const next = discoverCleanText(sourceInput.value, 50).replace(/^#+/u, '').toLocaleLowerCase(); if (!next || tagValues.includes(next)) { sourceInput.value = ''; return; } if (tagValues.length >= 40) { const message = tx('最多40个自己的标签。先删除一个;输入会保留。', 'Up to 40 personal tags. Remove one first; your input is kept.', 'Maximal 40 eigene Tags. Entferne zuerst einen Tag; deine Eingabe bleibt erhalten.'); noteStatus.textContent = message; toast(message); return; } sourceInput.value = ''; tagValues = [...tagValues, next]; renderTagChips(); persistPersonal(false); }; notes.addEventListener('input', () => { noteStatus.textContent = tx('保存中…', 'Saving…', 'Wird gespeichert …'); noteStatus.classList.remove('saved'); autosavePersonal(); }); tagInput.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ',') { event.preventDefault(); addTag(); } }); renderTagChips(); const groupGrid = $('div', { class: 'single-group-grid', dataset: { singleGroups: '1' } }, customGroups.length ? customGroups.map(group => { const input = $('input', { type: 'checkbox', checked: roomInGroup(roomFor(id), group.id) }); input.addEventListener('change', () => { ensureSaved(id); const active = roomInGroup(roomFor(id), group.id); if (active !== input.checked) store.toggleRoomInGroup(id, group.id); toast(tx('分组已更新', 'Group updated', 'Gruppe wurde aktualisiert')); }); return $('label', { class: 'single-group-option' }, [input, $('span', {}, groupLabel(group))]); }) : [$('div', { class: 'single-note-status' }, tx('先在“组”中创建自己的组。', 'Create your first group under Groups.', 'Erstelle zuerst unter „Gruppen“ eine eigene Gruppe.'))]); const ratingHost = $('div', { class: 'single-rating', ariaLabel: tx('本地评分', 'Local rating', 'Lokale Bewertung') }); const renderRating = () => { const current = Number(roomFor(id)?.rating || 0); ratingHost.replaceChildren(...[1, 2, 3, 4, 5].map(value => $('button', { type: 'button', class: current >= value ? 'on' : '', title: `${value} Sterne`, onclick: () => { ensureSaved(id); store.patchRoom(id, { rating: current === value ? 0 : value }); renderRating(); } }, '★'))); }; renderRating(); const prioritySelect = $('select', { class: 'discover-input' }, [0, 1, 2, 3, 4, 5].map(value => $('option', { value: String(value), selected: Number(roomFor(id)?.priority || 0) === value }, value ? `${tx('优先级', 'Priority', 'Priorität')} ${value}` : tx('无优先级', 'No priority', 'Keine Priorität')))); prioritySelect.addEventListener('change', () => { ensureSaved(id); store.patchRoom(id, { priority: Number(prioritySelect.value) || 0 }); }); const watchlistInput = $('input', { type: 'checkbox', checked: !!roomFor(id)?.watchlist }); watchlistInput.addEventListener('change', () => { ensureSaved(id); store.patchRoom(id, { watchlist: watchlistInput.checked }); }); const detailActions = $('div', { class: 'tag-toolbar' }, [ savedBtn, favoriteBtn, $('button', { class: 'discover-btn primary', type: 'button', onclick: () => { ensureSaved(id); toast(tx('已加入 MultiCam', 'Added to MultiCam', 'Zu MultiCam hinzugefügt')); } }, 'MultiCam +'), $('button', { class: 'discover-btn', type: 'button', onclick: () => activateSingleTab('notes') }, tx('编辑私人资料', 'Edit private details', 'Private Details bearbeiten')), ]); const genderLabels = { f: tx('女性', 'Women', 'Frauen'), m: tx('男性', 'Men', 'Männer'), c: tx('情侣', 'Couples', 'Paare'), t: tx('跨性别', 'Trans', 'Trans'), unknown: tx('未知', 'Unknown', 'Unbekannt'), }; const factRows = [ ['status', 'Status', model.currentShow === 'public' ? 'LIVE' : model.currentShow || '—'], ['viewers', tx('观众', 'Viewers', 'Zuschauer'), formatCount(model.viewers)], ['region', tx('地区', 'Region', 'Region'), [model.country, model.location].filter(Boolean).join(' · ') || tx('未知', 'Unknown', 'Unbekannt')], ['age', tx('年龄', 'Age', 'Alter'), model.age || tx('未知', 'Unknown', 'Unbekannt')], ['gender', tx('类型', 'Type', 'Typ'), genderLabels[model.gender] || genderLabels.unknown], ['followers', tx('关注者', 'Followers', 'Follower'), model.followers ? formatCount(model.followers) : '—'], ['online', tx('目前在线', 'Current online time', 'Aktuell online'), model.secondsOnline ? formatDuration(model.secondsOnline) : '—'], ['quality', tx('直播质量', 'Stream attributes', 'Stream-Merkmale'), [model.isHd ? 'HD' : '', model.isGaming ? tx('游戏', 'Gaming', 'Gaming') : '', model.isNew ? tx('新', 'New', 'Neu') : ''].filter(Boolean).join(' · ') || '—'], ['following', 'Following', isFollowing(model) ? tx('是', 'Yes', 'Ja') : tx('否 / 未知', 'No / unknown', 'Nein / unbekannt')], ['firstObserved', tx('首次发现', 'First observed', 'Erstmals gesehen'), observed.firstSeenAt ? new Date(observed.firstSeenAt).toLocaleString() : '—'], ['lastObserved', tx('最后发现', 'Last observed', 'Zuletzt gesehen'), observed.lastSeenAt ? new Date(observed.lastSeenAt).toLocaleString() : '—'], ['sessions', tx('本地访问', 'Local visits', 'Lokale Besuche'), formatCount(stat.sessions)], ['watchTime', tx('本地观看时间', 'Local watch time', 'Lokale Sehzeit'), formatWatch(stat.totalMs)], ['firstWatched', tx('首次观看', 'First watched', 'Erstmals angesehen'), stat.firstWatchedAt ? new Date(stat.firstWatchedAt).toLocaleString() : '—'], ['lastWatched', tx('最后观看', 'Last watched', 'Zuletzt angesehen'), stat.lastWatchedAt ? new Date(stat.lastWatchedAt).toLocaleString() : '—'], ]; const factNodes = new Map(); const facts = $('div', { class: 'single-facts' }, factRows.map(([key, label, value]) => { const strong = $('strong', { dataset: { singleFact: key } }, String(value)); factNodes.set(key, strong); return $('div', { class: 'single-fact' }, [$('span', {}, label), strong]); })); const infoSubject = $('p', { style: { color: 'var(--dc-sub)', lineHeight: '1.5', fontSize: '12px' }, dataset: { singleSubject: '1' } }, model.subject || tx('没有公开描述。', 'No public description.', 'Keine öffentliche Beschreibung.')); const publicTagsHost = $('div', { class: 'discover-tags', dataset: { singlePublicTags: '1' } }); const renderPublicTags = values => { const clean = [...new Set((values || []).map(tag => discoverCleanText(tag, 50).replace(/^#+/u, '').toLocaleLowerCase()).filter(Boolean))].slice(0, 80); publicTagsHost.replaceChildren(...clean.map(tag => $('button', { class: 'discover-tag', type: 'button', onclick: () => openTag(tag) }, `#${tag}`))); if (!clean.length) publicTagsHost.appendChild($('span', { class: 'single-note-status' }, tx('没有公开标签。', 'No public tags available.', 'Keine öffentlichen Tags verfügbar.'))); }; renderPublicTags(modelTags(model)); const overviewTagInput = $('input', { class: 'discover-input', placeholder: tx('快速添加自己的标签…', 'Quick-add your own tag…', 'Eigenen Tag direkt hinzufügen …') }); overviewTagInput.setAttribute('list', tagListId); overviewTagInput.setAttribute('aria-label', `${tx('自己的标签', 'Own tags', 'Eigene Tags')}: ${id}`); overviewTagInput.addEventListener('focus', prepareTagSuggestions); overviewTagInput.addEventListener('keydown', event => { if (event.key === 'Enter' || event.key === ',') { event.preventDefault(); addTag(overviewTagInput); } }); overviewOwnTags = $('div', { class: 'single-own-tags', dataset: { singleOverviewTags: '1' } }); renderTagChips(); const infoPanel = $('section', { class: 'single-panel' }, [ $('h2', {}, id), infoSubject, $('div', { class: 'single-tag-block' }, [$('strong', {}, tx('公开标签', 'Public tags', 'Öffentliche Tags')), publicTagsHost]), $('div', { class: 'single-tag-block' }, [$('strong', {}, tx('自己的标签', 'Your own tags', 'Eigene Tags')), overviewOwnTags, $('div', { class: 'single-tag-entry' }, [overviewTagInput, $('button', { class: 'discover-btn', type: 'button', onclick: () => addTag(overviewTagInput) }, tx('添加', 'Add', 'Hinzufügen'))])]), detailActions, facts, ]); const setFact = (key, value) => { const node = factNodes.get(key); if (node && value !== undefined && value !== null && String(value).trim()) node.textContent = String(value); }; session.onContext = (data, status) => { const contextTags = Array.isArray(data.tags) && data.tags.length ? data.tags : modelTags(model); renderPublicTags(contextTags); const subject = discoverCleanText(data.room_subject || data.subject || model.subject, 300); if (subject) infoSubject.textContent = subject; const country = discoverCleanText(data.country || model.country, 40).toUpperCase(); const locationText = discoverCleanText(data.location || model.location, 100); const age = Math.max(0, Math.round(Number(data.display_age || data.age || model.age) || 0)); const gender = ['f', 'm', 'c', 't'].includes(data.gender) ? data.gender : model.gender; const followers = Math.max(0, Math.round(Number(data.num_followers || data.followers || model.followers) || 0)); const secondsOnline = Math.max(0, Math.round(Number(data.seconds_online || model.secondsOnline) || 0)); setFact('status', status === 'online' ? 'LIVE' : status === 'private' ? tx('私人', 'Private', 'Privat') : status === 'offline' ? tx('离线', 'Offline', 'Offline') : status); setFact('viewers', formatCount(session.viewers)); setFact('region', [country, locationText].filter(Boolean).join(' · ') || tx('未知', 'Unknown', 'Unbekannt')); setFact('age', age || tx('未知', 'Unknown', 'Unbekannt')); setFact('gender', genderLabels[gender] || genderLabels.unknown); setFact('followers', followers ? formatCount(followers) : '—'); setFact('online', secondsOnline ? formatDuration(secondsOnline) : '—'); setFact('quality', [data.is_hd || model.isHd ? 'HD' : '', data.is_gaming || model.isGaming ? tx('游戏', 'Gaming', 'Gaming') : '', data.is_new || model.isNew ? tx('新', 'New', 'Neu') : ''].filter(Boolean).join(' · ') || '—'); setFact('following', data.is_following === true || isFollowing(model) ? tx('是', 'Yes', 'Ja') : tx('否 / 未知', 'No / unknown', 'Nein / unbekannt')); }; const localPanel = $('section', { class: 'single-panel' }, [ $('div', { class: 'single-panel-head' }, [$('div', {}, [$('h3', {}, tx('私人详情', 'Private details', 'Private Details')), $('p', {}, tx('所有内容只保存在这个浏览器中 und bleibt updatefest.', 'Everything stays in this browser and survives updates.', 'Alles bleibt in diesem Browser und updatefest gespeichert.'))]), noteStatus]), $('label', { class: 'discover-field' }, [tx('私人笔记', 'Private notebook', 'Privates Notizbuch'), notes]), $('div', { class: 'single-tag-editor' }, [$('strong', {}, tx('自己的标签', 'Your own tags', 'Eigene Tags')), tagDatalist, $('div', { class: 'single-tag-entry' }, [tagInput, $('button', { class: 'discover-btn', type: 'button', onclick: () => addTag() }, tx('添加', 'Add', 'Hinzufügen'))]), tagChips]), $('div', { class: 'single-personal-grid' }, [ $('div', {}, [$('strong', {}, tx('本地评分', 'Local rating', 'Lokale Bewertung')), ratingHost]), $('label', { class: 'discover-field' }, [tx('优先级', 'Priority', 'Priorität'), prioritySelect]), ]), $('label', { class: 'single-group-option' }, [watchlistInput, $('span', {}, tx('加入观察列表', 'Keep on watchlist', 'Auf Merkliste behalten'))]), $('strong', {}, tx('组分配(可多选)', 'Group assignment (multiple)', 'Gruppenzuordnung (mehrfach)')), groupGrid, $('button', { class: 'discover-btn primary', type: 'button', onclick: () => persistPersonal(true) }, tx('立即保存', 'Save now', 'Jetzt speichern')), ]); localPanel.dataset.singleLocalPanel = '1'; const profilePanel = publicProfileNode(id, model); profilePanel.dataset.singleProfilePanel = '1'; const chatStatus = $('span', { class: 'single-chat-status' }, tx('聊天仅在打开此区域时加载。', 'Chat loads only when this section is opened.', 'Der Chat wird erst beim Öffnen dieses Bereichs geladen.')); const chatFrame = $('iframe', { class: 'single-chat-frame', title: `${id} · ${tx('官方聊天', 'Official chat', 'Offizieller Chat')}`, src: 'about:blank', loading: 'lazy', referrerPolicy: 'strict-origin-when-cross-origin', allow: 'clipboard-write; fullscreen', dataset: { singleChatFrame: '1' }, }); session.chatFrame = chatFrame; let chatLoaded = false; const isolateChatFrame = () => { if (!chatLoaded || chatFrame.src === 'about:blank') return; try { const doc = chatFrame.contentDocument; const win = chatFrame.contentWindow; if (!doc?.body || !win) return; const silenceMedia = root => root?.querySelectorAll?.('video,audio').forEach(media => { try { media.pause(); media.muted = true; media.volume = 0; } catch (_) {} }); silenceMedia(doc); if (RoomGridAudioSilencer.isEnabled()) { try { const proto = win.AudioBufferSourceNode?.prototype; if (proto?.start && !proto.start.__roomgridSingleChatGuard) { const original = proto.start; const guarded = function (...args) { if (RoomGridAudioSilencer.isEnabled()) return; return original.apply(this, args); }; guarded.__roomgridSingleChatGuard = true; proto.start = guarded; } } catch (_) {} } const selectors = [ '[data-testid*="chat" i]', '#chat', '#chat_box', '[id*="chat" i]', '[class*="ChatRoom" i]', '[class*="chat-room" i]', '[class*="chat_container" i]', '[class*="chat-container" i]', '[class*="chatBox" i]', ]; const candidates = [...new Set(selectors.flatMap(selector => { try { return [...doc.querySelectorAll(selector)]; } catch (_) { return []; } }))] .filter(node => { const rect = node.getBoundingClientRect(); return rect.width >= 220 && rect.height >= 220; }); const scored = candidates.map(node => { const rect = node.getBoundingClientRect(); const hasComposer = !!node.querySelector('textarea,input[type="text"],[contenteditable="true"]'); const messages = node.querySelectorAll('[data-testid*="message" i],[class*="message" i],li').length; return { node, score: (hasComposer ? 5000 : 0) + Math.min(500, messages) - (rect.width * rect.height / 1000000) }; }).sort((a, b) => b.score - a.score); const target = scored[0]?.node; if (target) { doc.querySelectorAll('.roomgrid-single-chat-path,.roomgrid-single-chat-target').forEach(node => node.classList.remove('roomgrid-single-chat-path', 'roomgrid-single-chat-target')); target.classList.add('roomgrid-single-chat-target'); for (let node = target.parentElement; node && node !== doc.body; node = node.parentElement) node.classList.add('roomgrid-single-chat-path'); doc.body.classList.add('roomgrid-single-chat-isolated'); const style = doc.createElement('style'); style.dataset.roomgridSingleChatStyle = '1'; style.textContent = `html,body{margin:0!important;min-height:100%!important;background:#0b1420!important}body.roomgrid-single-chat-isolated> *:not(.roomgrid-single-chat-path):not(.roomgrid-single-chat-target){display:none!important}.roomgrid-single-chat-path:not(.roomgrid-single-chat-target)> *:not(.roomgrid-single-chat-path):not(.roomgrid-single-chat-target){display:none!important}.roomgrid-single-chat-target{display:flex!important;position:relative!important;inset:auto!important;width:100%!important;max-width:none!important;height:100vh!important;max-height:none!important;margin:0!important;border:0!important}`; doc.head?.appendChild(style); chatStatus.textContent = tx('官方聊天已连接。', 'Official chat connected.', 'Offizieller Chat verbunden.'); } else chatStatus.textContent = tx('聊天已加载;当前页面结构无法完全分离。', 'Chat loaded; the current page structure could not be fully isolated.', 'Chat geladen; die aktuelle Seitenstruktur ließ sich nicht vollständig trennen.'); const observer = new win.MutationObserver(records => records.forEach(record => record.addedNodes.forEach(node => { if (node.nodeType === 1) silenceMedia(node.matches?.('video,audio') ? node.parentElement : node); }))); observer.observe(doc.documentElement, { childList: true, subtree: true }); session.chatObserver?.disconnect?.(); session.chatObserver = observer; } catch (_) { chatStatus.textContent = tx('聊天已加载。', 'Chat loaded.', 'Chat geladen.'); } }; const unloadChat = () => { session.chatObserver?.disconnect(); session.chatObserver = null; chatFrame.src = 'about:blank'; chatLoaded = false; }; const loadChat = (reload = false) => { if (!preferences.showChat || singleSession !== session) return; if (reload) unloadChat(); if (chatLoaded) return; chatLoaded = true; chatStatus.textContent = tx('正在加载官方聊天…', 'Loading official chat…', 'Offizieller Chat wird geladen …'); chatFrame.src = new URL(`/${encodeURIComponent(id)}/?roomgrid_official=1&roomgrid_chat_embed=1`, location.origin).toString(); }; chatFrame.addEventListener('load', () => { if (chatFrame.src !== 'about:blank') setTimeout(isolateChatFrame, 900); }); const chatPanel = $('section', { class: 'single-panel single-chat-panel', dataset: { singleChat: '1' } }, [ $('div', { class: 'single-panel-head' }, [ $('div', {}, [$('h3', {}, tx('官方聊天室', 'Official chat room', 'Offizieller Chatraum')), $('p', {}, tx('仅在需要时加载;RoomGrid 不保存也不会自动发送聊天消息。', 'Loaded only when needed; RoomGrid never stores or automatically sends chat messages.', 'Wird nur bei Bedarf geladen; RoomGrid speichert und versendet keine Chatnachrichten automatisch.'))]), chatStatus, ]), $('div', { class: 'single-chat-actions' }, [ $('button', { class: 'discover-btn', type: 'button', onclick: () => loadChat(true) }, tx('重新加载聊天', 'Reload chat', 'Chat neu laden')), $('button', { class: 'discover-btn', type: 'button', onclick: () => openNoopener(new URL(`/${encodeURIComponent(id)}/?roomgrid_official=1`, location.origin).toString()) }, tx('在新窗口打开官方房间', 'Open official room in a new window', 'Offiziellen Raum in neuem Fenster öffnen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => { persistSinglePreferences({ showChat: false, activeTab: 'overview' }); activateSingleTab('overview'); unloadChat(); } }, tx('隐藏聊天', 'Hide chat', 'Chat ausblenden')), ]), chatFrame, ]); preferenceTargets.chatFrame = chatFrame; preferenceTargets.chatPanel = chatPanel; const statisticsPanel = $('section', { class: 'single-panel', dataset: { singleStatistics: '1' } }); const renderSingleStatistics = () => { const latest = statsById.get(id) || analytics.getModelSync?.(id) || stat || {}; const activeDays = Object.values(latest.days || {}).filter(value => Number(value) > 0).length; const average = Number(latest.sessions) ? Number(latest.totalMs || 0) / Number(latest.sessions) : 0; const heat = Array.from({ length: 168 }, (_, index) => Math.max(0, Number(latest.onlineHours?.[index] || latest.hours?.[index]) || 0)); const maxHeat = Math.max(1, ...heat); const observedTotal = heat.reduce((sum, value) => sum + value, 0); const coveredSlots = heat.filter(Boolean).length; let peakIndex = 0; heat.forEach((value, index) => { if (value > heat[peakIndex]) peakIndex = index; }); const weekdays = [tx('周日', 'Sun', 'So'), tx('周一', 'Mon', 'Mo'), tx('周二', 'Tue', 'Di'), tx('周三', 'Wed', 'Mi'), tx('周四', 'Thu', 'Do'), tx('周五', 'Fri', 'Fr'), tx('周六', 'Sat', 'Sa')]; const peakLabel = heat[peakIndex] ? `${weekdays[Math.floor(peakIndex / 24)]} ${String(peakIndex % 24).padStart(2, '0')}:00` : '—'; const byWeekday = Array.from({ length: 7 }, (_, day) => heat.slice(day * 24, day * 24 + 24).reduce((sum, value) => sum + value, 0)); const maxWeekday = Math.max(1, ...byWeekday); const bestSlots = heat.map((value, index) => ({ value, index })).filter(item => item.value > 0).sort((a, b) => b.value - a.value || a.index - b.index).slice(0, 6); const confidence = observedTotal >= 36 ? tx('良好趋势', 'Good trend', 'Gute Tendenz') : observedTotal >= 12 ? tx('中等趋势', 'Moderate trend', 'Mittlere Tendenz') : observedTotal >= 3 ? tx('初步趋势', 'Initial trend', 'Erste Tendenz') : observedTotal ? tx('首次观察', 'First observation', 'Erste Beobachtung') : tx('尚无经验数据', 'No experience data yet', 'Noch keine Erfahrungswerte'); const now = Date.now(); const watchInDays = days => Object.entries(latest.days || {}).reduce((sum, [key, value]) => { const at = Date.parse(`${key}T00:00:00Z`); return Number.isFinite(at) && now - at <= days * 86400000 ? sum + Math.max(0, Number(value) || 0) : sum; }, 0); let streak = 0; for (let offset = 0; offset < 450; offset += 1) { const day = new Date(now - offset * 86400000).toISOString().slice(0, 10); if (Number(latest.days?.[day] || 0) > 0) streak += 1; else if (offset > 0 || streak) break; } const rows = [ [tx('总观看时间', 'Total watch time', 'Gesamte Sehzeit'), formatWatch(latest.totalMs)], [tx('会话', 'Sessions', 'Sitzungen'), formatCount(latest.sessions)], [tx('平均会话', 'Average session', 'Ø Sitzung'), formatWatch(average)], [tx('最长会话', 'Longest session', 'Längste Sitzung'), formatWatch(latest.longestSessionMs)], [tx('活跃天数', 'Active days', 'Aktive Tage'), formatCount(activeDays)], [tx('出现次数', 'Appearances', 'Aufgetaucht'), formatCount(latest.appearances)], [tx('在线样本', 'Live observations', 'Live-Beobachtungen'), formatCount(latest.onlineSamples)], [tx('趋势可信度', 'Trend confidence', 'Trend-Verlässlichkeit'), confidence], [tx('已知时间段', 'Known time slots', 'Bekannte Zeitfenster'), `${coveredSlots} / 168`], [tx('关注者', 'Followers', 'Follower'), formatCount(latest.followers)], [tx('目前在线', 'Current online time', 'Aktuell online'), formatDuration(latest.secondsOnline)], [tx('典型峰值', 'Typical peak', 'Typischer Höhepunkt'), peakLabel], [tx('最近7天观看', 'Watched in 7 days', 'In 7 Tagen gesehen'), formatWatch(watchInDays(7))], [tx('最近30天观看', 'Watched in 30 days', 'In 30 Tagen gesehen'), formatWatch(watchInDays(30))], [tx('当前连续天数', 'Current streak', 'Aktuelle Serie'), `${streak} ${tx('天', 'days', 'Tage')}`], [tx('首次发现', 'First observed', 'Erstmals erfasst'), latest.firstObservedAt ? new Date(latest.firstObservedAt).toLocaleString() : '—'], [tx('最后在线', 'Last online', 'Zuletzt online'), latest.lastOnlineAt ? new Date(latest.lastOnlineAt).toLocaleString() : '—'], ]; const heatmap = $('div', { class: 'single-heatmap', ariaLabel: tx('典型每周在线时间', 'Typical weekly online times', 'Typische wöchentliche Onlinezeiten') }, heat.map((value, index) => $('span', { class: 'single-heat-cell', style: { '--heat': `${Math.round(8 + value / maxHeat * 84)}%` }, title: `${weekdays[Math.floor(index / 24)] || ''} ${String(index % 24).padStart(2, '0')}:00 · ${value}` }))); const statGrid = $('div', { class: 'single-stat-grid' }, rows.map(([label, value]) => $('div', { class: 'single-stat-card' }, [ $('span', {}, label), $('strong', {}, String(value ?? '—')), ]))); const weekdayBars = $('div', { class: 'single-week-bars', dataset: { singleWeekdayBars: '1' } }, byWeekday.map((value, index) => $('div', { class: 'single-week-bar', title: `${weekdays[index]} · ${value}` }, [ $('span', {}, weekdays[index]), $('i', { style: { '--bar': `${Math.max(4, Math.round(value / maxWeekday * 100))}%` } }), $('strong', {}, String(value)), ]))); const bestTimes = $('div', { class: 'single-best-times', dataset: { singleBestTimes: '1' } }, bestSlots.length ? bestSlots.map((item, rank) => $('span', { class: 'single-best-time' }, `${rank + 1}. ${weekdays[Math.floor(item.index / 24)]} ${String(item.index % 24).padStart(2, '0')}:00 · ${item.value}×`)) : [$('span', { class: 'single-note-status' }, tx('在线时间会从现在起在此设备上学习。', 'Online times will be learned on this device from now on.', 'Onlinezeiten werden ab jetzt auf diesem Gerät gelernt.'))]); statisticsPanel.replaceChildren( $('div', { class: 'single-panel-head' }, [$('div', {}, [$('h3', {}, tx('本地模型统计', 'Local model statistics', 'Lokale Model-Statistik')), $('p', {}, tx('基于此设备上观察到的数据;不会猜测缺失的信息。', 'Based on observations on this device; missing information is never guessed.', 'Basiert auf Beobachtungen auf diesem Gerät; fehlende Angaben werden nicht geraten.'))])]), statGrid, $('h3', { style: { marginTop: '12px' } }, tx('每周活动分布', 'Activity by weekday', 'Aktivität nach Wochentag')), weekdayBars, $('h3', { style: { marginTop: '12px' } }, tx('最可能在线的时间', 'Most likely online times', 'Wahrscheinlichste Onlinezeiten')), bestTimes, $('h3', { style: { marginTop: '12px' } }, tx('在线时间图', 'Online-time map', 'Onlinezeit-Karte')), heatmap, $('div', { class: 'single-heat-caption' }, [$('span', {}, `${weekdays[0]} 00:00`), $('span', {}, `${weekdays[6]} 23:00`)]), ); }; singleAnalyticsRefresh = () => { if (preferences.activeTab === 'statistics' && !preferences.sidebarCollapsed) renderSingleStatistics(); }; // The player/overview is usable before constructing the 168-slot analysis. const mediaGrid = $('div', { class: 'single-media-grid', dataset: { singleMediaGrid: '1' } }); session.stopGalleryMedia = (preserveViewer = false) => { for (const root of [mediaGrid, session.galleryViewer]) root?.querySelectorAll('img,video').forEach(media => { if (preserveViewer && media.matches('.single-gallery-media')) return; if (media.tagName === 'VIDEO') stopMediaElement(media, false); else media.removeAttribute('src'); }); }; const currentModelBackground = () => sanitizeRoomGridBackground(store.state.settings.backgroundModels?.[id], 'inherit'); let syncModelBackgroundControls = () => {}; const mediaStatus = $('div', { class: 'single-note-status' }, tx('截图、录制和本地示例媒体。', 'Screenshots, recordings, and local example media.', 'Screenshots, Aufnahmen und lokale Beispielmedien.')); const mediaFile = $('input', { type: 'file', accept: 'image/jpeg,image/png,image/webp', multiple: true, hidden: true, dataset: { singleMediaFile: '1' } }); const mediaQuality = $('select', { class: 'discover-input', dataset: { singleMediaQuality: '1' }, title: tx('导入质量', 'Import quality', 'Importqualität') }, [ ['original', tx('原始文件', 'Original file', 'Originaldatei')], ['high', tx('非常高(最高 4K)', 'Very high (up to 4K)', 'Sehr hoch (bis 4K)')], ['balanced', tx('高(最高 1440p)', 'High (up to 1440p)', 'Hoch (bis 1440p)')], ['compact', tx('节省空间(最高 900p)', 'Space-saving (up to 900p)', 'Platzsparend (bis 900p)')], ].map(([value, label]) => $('option', { value, selected: preferences.mediaImportQuality === value }, label))); const makeImportedCover = $('input', { type: 'checkbox', checked: false, dataset: { singleMediaMakeCover: '1' } }); const chooseMediaButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleMediaChoose: '1' } }, tx('从电脑添加图片', 'Add images from computer', 'Bilder vom PC hinzufügen')); const clearCoverButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleMediaClearCover: '1' }, hidden: true }, tx('使用网站封面', 'Use website cover', 'Website-Cover verwenden')); const clearModelBackgroundButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleBackgroundClear: '1' }, hidden: currentModelBackground().mode === 'inherit' }, tx('使用页面背景', 'Use page background', 'Seitenhintergrund verwenden')); const chooseDirectoryButton = $('button', { class: 'discover-btn primary', type: 'button', dataset: { singleDirectoryChoose: '1' } }, tx('选择媒体文件夹', 'Choose media folder', 'Medienordner auswählen')); const directoryImportInput = $('input', { type: 'file', accept: 'image/jpeg,image/png,image/webp', multiple: true, hidden: true, dataset: { singleDirectoryImport: '1' } }); directoryImportInput.setAttribute('webkitdirectory', ''); directoryImportInput.setAttribute('directory', ''); const grantDirectoryButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleDirectoryGrant: '1' }, hidden: true }, tx('允许文件夹访问', 'Allow folder access', 'Ordnerzugriff freigeben')); const clearDirectoryButton = $('button', { class: 'discover-btn', type: 'button', dataset: { singleDirectoryClear: '1' }, hidden: true }, tx('移除链接', 'Remove link', 'Verknüpfung entfernen')); const directoryStatus = $('div', { class: 'single-directory-status', dataset: { singleDirectoryStatus: '1' }, role: 'status', ariaLive: 'polite' }, [$('strong', {}, tx('媒体文件夹', 'Media folder', 'Medienordner')), $('span', {}, tx('正在检查…', 'Checking…', 'Wird geprüft …'))]); const directoryStatusText = directoryStatus.querySelector('span'); const directoryHelpText = $('p', { dataset: { singleDirectoryHelp: '1' } }); const directoryFootnote = $('small', { dataset: { singleDirectoryFootnote: '1' } }); const syncCaptureDirectoryStatus = async () => { const current = await RoomGridCaptureDirectory.status(); if (singleSession?.id !== id) return current; const configured = !!current.configured; const granted = current.permission === 'granted'; const braveMode = !current.supported && current.reason === 'brave-disabled'; chooseDirectoryButton.textContent = braveMode ? tx('将图片文件夹导入图库', 'Import image folder into gallery', 'Bilderordner in Galerie einlesen') : configured ? tx('更换媒体文件夹', 'Change media folder', 'Medienordner ändern') : tx('选择媒体文件夹', 'Choose media folder', 'Medienordner auswählen'); chooseDirectoryButton.title = current.supported ? tx('选择自动保存截图和录制的主文件夹。', 'Choose the main folder where screenshots and recordings are saved automatically.', 'Hauptordner auswählen, in dem Screenshots und Aufnahmen automatisch gespeichert werden.') : braveMode ? tx('选择一个现有文件夹,并将其中的图片添加到此模型的图库。', 'Choose an existing folder and add its images to this model gallery.', 'Vorhandenen Ordner auswählen und dessen Bilder in die Galerie dieses Models übernehmen.') : tx('此浏览器没有可写文件夹接口。', 'This browser has no writable-folder interface.', 'Dieser Browser besitzt keine beschreibbare Ordnerschnittstelle.'); grantDirectoryButton.hidden = !configured || granted; clearDirectoryButton.hidden = !configured; directoryStatusText.textContent = !current.supported ? braveMode ? tx('Brave 模式:自动保存到本地图库和常规下载。', 'Brave mode: automatic saving to the local gallery and normal downloads.', 'Brave-Modus: automatische Speicherung in der lokalen Galerie und als normaler Download.') : tx('此浏览器不支持直接文件夹保存。', 'This browser does not support direct folder saving.', 'Dieser Browser unterstützt das direkte Speichern in Ordnern nicht.') : !configured ? tx('未选择;捕获使用浏览器下载。', 'Not selected; captures use browser downloads.', 'Nicht ausgewählt; Aufnahmen verwenden Browser-Downloads.') : granted ? `${current.name} / ${id} · ${tx('自动保存已启用', 'automatic saving enabled', 'automatisches Speichern aktiv')}` : `${current.name} · ${tx('需要再次授权', 'access must be allowed again', 'Zugriff muss erneut freigegeben werden')}`; captureDirectoryBadge.textContent = granted ? `${tx('媒体文件夹', 'Media folder', 'Medienordner')}: ${current.name} / ${id}` : configured ? `${tx('媒体文件夹', 'Media folder', 'Medienordner')}: ${tx('需要授权', 'permission needed', 'Freigabe nötig')}` : braveMode ? tx('Brave:图库 + 下载/Camclient', 'Brave: gallery + Downloads/Camclient', `Brave: Galerie + Downloads/Camclient/${id}`) : `${tx('媒体文件夹', 'Media folder', 'Medienordner')}: ${tx('浏览器下载', 'browser download', 'Browser-Download')}`; captureDirectoryBadge.classList.toggle('is-on', granted); directoryHelpText.textContent = braveMode ? tx( 'Brave 有意禁用网站的直接文件夹写入。因此,Camclient 会将截图和录制保存在本地图库和 Downloads/Camclient/模型 中。下面的按钮用于将现有图片文件夹导入图库。', 'Brave intentionally disables direct folder writing for websites. Camclient therefore keeps captures in the local gallery and under Downloads/Camclient/model. The button below imports an existing image folder into the gallery.', `Brave deaktiviert das direkte Schreiben in frei gewählte Ordner absichtlich. Camclient speichert deshalb in der lokalen Galerie und per Browser-Download. Für Speichern ohne Abfrage: brave://settings/downloads öffnen, den gewünschten Download-Ordner wählen und „Vor jedem Download nach dem Speicherort fragen“ ausschalten. In den Website-Einstellungen für chaturbate.com „Automatische Downloads“ erlauben. In Tampermonkey den Download-Modus „Browser API“ verwenden; dann ist Downloads/Camclient/${id} möglich. Ohne Browser-API landen Dateien direkt im Download-Ordner. Mit dem Knopf unten kannst du einen vorhandenen Bilderordner in die Galerie einlesen.`, ) : tx( '选择一次主文件夹。Camclient 会自动为每个模型创建子文件夹,并将截图和录制直接保存到其中,而无需每次询问保存位置。', 'Choose one main folder. Camclient automatically creates a subfolder for each model and saves screenshots and recordings there without asking for a location every time.', 'Wähle einmal einen Hauptordner. Camclient legt darin automatisch pro Model einen Unterordner an und speichert Screenshots und Aufnahmen ohne erneute Nachfrage nach dem Speicherort.', ); directoryFootnote.textContent = braveMode ? tx( 'Hinweis: Einen frei wählbaren dauerhaft beschreibbaren Ordner stellen derzeit Chrome und Edge bereit. Das ist keine einschaltbare Brave-Website-Berechtigung.', 'Note: Chrome and Edge currently provide a permanently writable user-selected folder. This is not a Brave site permission that can be enabled.', 'Hinweis: Einen frei wählbaren dauerhaft beschreibbaren Ordner stellen derzeit Chrome und Edge bereit. Das ist keine einschaltbare Brave-Website-Berechtigung.', ) : tx( '链接保存在浏览器本地。浏览器重启后可能需要再次允许访问。“移除链接”不会删除任何文件。', 'The link stays stored locally in the browser. After a browser restart, access may need to be allowed again. “Remove link” never deletes files.', 'Die Verknüpfung bleibt lokal im Browser gespeichert. Nach einem Browser-Neustart kann einmalig eine erneute Freigabe nötig sein. „Verknüpfung entfernen“ löscht keine Dateien.', ); return current; }; const mediaControls = $('div', { class: 'single-media-controls' }, [ mediaQuality, chooseMediaButton, $('button', { class: 'discover-btn', type: 'button', onclick: () => activateSingleTab('settings') }, tx('保存文件夹设置', 'Storage folder settings', 'Speicherordner einstellen')), $('label', { class: 'discover-chip' }, [makeImportedCover, ` ${tx('首张图片设为封面', 'Use first image as cover', 'Erstes Bild als Cover')}`]), clearCoverButton, clearModelBackgroundButton, mediaFile, ]); let modelAvatar = null; let gallerySources = []; const galleryState = { index: 0, zoom: 1, x: 0, y: 0, dragging: false, startX: 0, startY: 0, originX: 0, originY: 0 }; const galleryTitle = $('div', { class: 'single-gallery-title' }, [$('strong'), $('span')]); const galleryCounter = $('div', { class: 'single-gallery-counter', dataset: { singleGalleryCounter: '1' } }); const galleryMediaHost = $('div', { class: 'single-gallery-media-host' }); const galleryPrevious = $('button', { class: 'single-gallery-arrow previous', type: 'button', ariaLabel: tx('上一个媒体', 'Previous media', 'Vorheriges Medium'), dataset: { singleGalleryPrevious: '1' } }, '‹'); const galleryNext = $('button', { class: 'single-gallery-arrow next', type: 'button', ariaLabel: tx('下一个媒体', 'Next media', 'Nächstes Medium'), dataset: { singleGalleryNext: '1' } }, '›'); const galleryCanvas = $('div', { class: 'single-gallery-canvas', dataset: { singleGalleryCanvas: '1' } }, [galleryMediaHost, galleryPrevious, galleryNext]); const galleryStrip = $('div', { class: 'single-gallery-strip', dataset: { singleGalleryStrip: '1' } }); const galleryZoomValue = $('span', { class: 'single-gallery-zoom', dataset: { singleGalleryZoomValue: '1' } }, '100 %'); const galleryZoomOut = $('button', { type: 'button', dataset: { singleGalleryZoomOut: '1' }, title: tx('缩小', 'Zoom out', 'Verkleinern') }, '−'); const galleryZoomIn = $('button', { type: 'button', dataset: { singleGalleryZoomIn: '1' }, title: tx('放大', 'Zoom in', 'Vergrößern') }, '+'); const galleryZoomReset = $('button', { type: 'button', dataset: { singleGalleryZoomReset: '1' } }, tx('适合', 'Fit', 'Einpassen')); const galleryCover = $('button', { class: 'primary', type: 'button', dataset: { singleGalleryCover: '1' } }, tx('设为模型封面', 'Use as model cover', 'Als Model-Cover verwenden')); const galleryClose = $('button', { type: 'button', dataset: { singleGalleryClose: '1' } }, tx('关闭', 'Close', 'Schließen')); const galleryViewer = $('div', { class: 'single-gallery-viewer', hidden: true, role: 'dialog', ariaModal: 'true', ariaLabel: tx('媒体图库', 'Media gallery', 'Mediengalerie'), dataset: { singleGalleryViewer: '1' } }, [ $('div', { class: 'single-gallery-head' }, [galleryTitle, galleryCounter, galleryClose]), galleryCanvas, $('div', { class: 'single-gallery-footer' }, [ $('div', { class: 'single-gallery-tools' }, [galleryZoomOut, galleryZoomValue, galleryZoomIn, galleryZoomReset, galleryCover]), galleryStrip, $('span', { class: 'single-gallery-hint' }, tx('滚轮缩放 · 拖动移动 · 方向键切换', 'Wheel to zoom · drag to pan · arrow keys to navigate', 'Mausrad: Zoom · Ziehen: Verschieben · Pfeiltasten: Wechseln')), ]), ]); document.body.appendChild(galleryViewer); session.galleryViewer = galleryViewer; const galleryFormatBytes = value => { const bytes = Math.max(0, Number(value) || 0); if (!bytes) return ''; if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`; return `${(bytes / 1024 / 1024).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`; }; const gallerySourceMeta = source => [source?.origin, galleryFormatBytes(source?.size), source?.lastModified ? new Date(source.lastModified).toLocaleString() : ''].filter(Boolean).join(' · '); const applyGalleryTransform = () => { const media = galleryMediaHost.querySelector('img.single-gallery-media'); if (media) media.style.transform = `translate3d(${galleryState.x}px,${galleryState.y}px,0) scale(${galleryState.zoom})`; galleryZoomValue.textContent = `${Math.round(galleryState.zoom * 100)} %`; galleryCanvas.classList.toggle('is-zoomed', galleryState.zoom > 1); }; const setGalleryZoom = value => { galleryState.zoom = Math.min(6, Math.max(.5, Number(value) || 1)); if (galleryState.zoom <= 1) { galleryState.x = 0; galleryState.y = 0; } applyGalleryTransform(); }; const closeGalleryViewer = () => { try { galleryMediaHost.querySelector('video')?.pause(); } catch (_) {} galleryViewer.hidden = true; document.documentElement.classList.remove('single-gallery-open-lock'); }; const updateGalleryViewer = (preserve = false) => { if (!gallerySources.length) { closeGalleryViewer(); return; } galleryState.index = (galleryState.index + gallerySources.length) % gallerySources.length; if (!preserve) { galleryState.zoom = 1; galleryState.x = 0; galleryState.y = 0; } const source = gallerySources[galleryState.index]; const previousMedia = galleryMediaHost.querySelector('.single-gallery-media'); if (!preserve || previousMedia?.getAttribute('src') !== source.url) { if (previousMedia?.tagName === 'VIDEO') stopMediaElement(previousMedia, false); const media = source.kind === 'video' ? $('video', { class: 'single-gallery-media', src: source.url, controls: true, preload: 'metadata', playsInline: true }) : $('img', { class: 'single-gallery-media', src: source.url, alt: `${id} · ${source.label}`, draggable: false, referrerPolicy: 'no-referrer' }); galleryMediaHost.replaceChildren(media); } galleryTitle.querySelector('strong').textContent = source.label || id; galleryTitle.querySelector('span').textContent = gallerySourceMeta(source) || id; galleryCounter.textContent = `${galleryState.index + 1} / ${gallerySources.length}`; galleryPrevious.disabled = galleryNext.disabled = gallerySources.length < 2; galleryCover.hidden = source.kind === 'video'; galleryCover.disabled = !!source.record?.isCustomCover; galleryCover.textContent = source.record?.isCustomCover ? tx('当前模型封面', 'Current model cover', 'Aktuelles Model-Cover') : tx('设为模型封面', 'Use as model cover', 'Als Model-Cover verwenden'); galleryStrip.replaceChildren(...gallerySources.map((entry, index) => $('button', { class: `single-gallery-thumb${index === galleryState.index ? ' active' : ''}`, type: 'button', title: entry.label, dataset: { singleGalleryIndex: String(index) }, onclick: () => { galleryState.index = index; updateGalleryViewer(); }, }, [ entry.kind === 'video' ? $('span', {}, '▶ VIDEO') : $('img', { src: entry.url, alt: '', loading: 'lazy', referrerPolicy: 'no-referrer' }), entry.record?.isCustomCover ? $('b', { class: 'single-gallery-cover-badge' }, tx('封面', 'COVER', 'COVER')) : null, ]))); requestAnimationFrame(() => galleryStrip.querySelector('.single-gallery-thumb.active')?.scrollIntoView({ block: 'nearest', inline: 'center' })); applyGalleryTransform(); }; const openGalleryViewer = index => { if (!gallerySources.length) return; galleryState.index = Math.min(gallerySources.length - 1, Math.max(0, Number(index) || 0)); galleryViewer.hidden = false; document.documentElement.classList.add('single-gallery-open-lock'); updateGalleryViewer(); galleryClose.focus({ preventScroll: true }); }; const moveGallery = direction => { if (!gallerySources.length) return; galleryState.index = (galleryState.index + direction + gallerySources.length) % gallerySources.length; updateGalleryViewer(); }; const ensureGalleryRecord = async (source, makeCover = false) => { if (source?.record) { if (makeCover && !source.record.isCustomCover) source.record = await RoomGridModelMedia.setCustomCover(id, source.record.mediaId); return source.record; } if (source?.blob instanceof Blob) { source.record = await RoomGridModelMedia.importFile(id, source.blob, { quality: 'original', makeCover, fileName: source.name || `camclient-${id}-${Date.now()}.webp` }); return source.record; } if (source?.remoteUrl) { source.record = await RoomGridModelMedia.importUrl(id, source.remoteUrl, { quality: 'high', makeCover, fileName: source.name || `camclient-${id}-${Date.now()}.webp` }); return source.record; } throw new Error('Gallery image unavailable'); }; galleryPrevious.addEventListener('click', () => moveGallery(-1)); galleryNext.addEventListener('click', () => moveGallery(1)); galleryClose.addEventListener('click', closeGalleryViewer); galleryZoomOut.addEventListener('click', () => setGalleryZoom(galleryState.zoom - .25)); galleryZoomIn.addEventListener('click', () => setGalleryZoom(galleryState.zoom + .25)); galleryZoomReset.addEventListener('click', () => setGalleryZoom(1)); galleryCover.addEventListener('click', async () => { const source = gallerySources[galleryState.index]; if (!source || source.kind === 'video') return; galleryCover.disabled = true; galleryCover.textContent = tx('正在保存封面…', 'Saving cover…', 'Cover wird gespeichert …'); try { await ensureGalleryRecord(source, true); await renderMedia(false); toast(tx('模型封面已保存。', 'Model cover saved.', 'Model-Cover wurde gespeichert.')); } catch (error) { galleryCover.disabled = false; galleryCover.textContent = tx('重试设为封面', 'Retry setting cover', 'Cover-Auswahl erneut versuchen'); toast(tx('无法保存此封面。', 'This cover could not be saved.', 'Dieses Cover konnte nicht gespeichert werden.')); try { Diagnostics.report('single-gallery-cover', error, id, 'warning'); } catch (_) {} } }); galleryViewer.addEventListener('pointerdown', event => { if (event.target === galleryViewer) closeGalleryViewer(); }); galleryCanvas.addEventListener('wheel', event => { if (gallerySources[galleryState.index]?.kind === 'video') return; event.preventDefault(); setGalleryZoom(galleryState.zoom + (event.deltaY < 0 ? .25 : -.25)); }, { passive: false }); galleryCanvas.addEventListener('dblclick', () => setGalleryZoom(galleryState.zoom > 1 ? 1 : 2)); galleryCanvas.addEventListener('pointerdown', event => { if (galleryState.zoom <= 1 || !event.target.closest('img.single-gallery-media')) return; galleryState.dragging = true; galleryState.startX = event.clientX; galleryState.startY = event.clientY; galleryState.originX = galleryState.x; galleryState.originY = galleryState.y; galleryCanvas.classList.add('is-dragging'); galleryCanvas.setPointerCapture?.(event.pointerId); event.preventDefault(); }); galleryCanvas.addEventListener('pointermove', event => { if (!galleryState.dragging) return; galleryState.x = galleryState.originX + event.clientX - galleryState.startX; galleryState.y = galleryState.originY + event.clientY - galleryState.startY; applyGalleryTransform(); }); const stopGalleryDrag = event => { galleryState.dragging = false; galleryCanvas.classList.remove('is-dragging'); try { galleryCanvas.releasePointerCapture?.(event.pointerId); } catch (_) {} }; galleryCanvas.addEventListener('pointerup', stopGalleryDrag); galleryCanvas.addEventListener('pointercancel', stopGalleryDrag); session.galleryKeyHandler = event => { if (galleryViewer.hidden) return; if (event.key === 'Escape') closeGalleryViewer(); else if (event.key === 'ArrowLeft') moveGallery(-1); else if (event.key === 'ArrowRight') moveGallery(1); else if (event.key === '+' || event.key === '=') setGalleryZoom(galleryState.zoom + .25); else if (event.key === '-') setGalleryZoom(galleryState.zoom - .25); else if (event.key === '0') setGalleryZoom(1); else return; event.preventDefault(); }; document.addEventListener('keydown', session.galleryKeyHandler); const mediaPanel = $('section', { class: 'single-panel', dataset: { singleMedia: '1' } }, [ $('div', { class: 'single-panel-head' }, [$('div', {}, [$('h3', {}, tx('本地媒体库', 'Local media gallery', 'Lokale Mediengalerie')), $('p', {}, tx('截图和录制会立即显示在这里。视频可以直接播放;图片也可以设为模型封面。', 'Screenshots and recordings appear here immediately. Videos play directly; images can also become the model cover.', 'Screenshots und Aufnahmen erscheinen sofort hier. Videos lassen sich direkt abspielen; Bilder können zusätzlich als Modell-Cover dienen.'))]), mediaStatus]), mediaControls, mediaGrid, ]); const applyCoverToAvatar = record => { clearCoverButton.hidden = !record; if (!modelAvatar) return; const objectUrl = record ? RoomGridModelMedia.objectUrl(record) : ''; if (!objectUrl) { modelAvatar.src = model.imageUrl || discoverFallbackImage(id); return; } modelAvatar.addEventListener('load', () => setTimeout(() => URL.revokeObjectURL(objectUrl), 0), { once: true }); modelAvatar.src = objectUrl; }; let mediaRenderRevision = 0; const renderMedia = async (force = false, localOnly = false) => { const revision = ++mediaRenderRevision; mediaStatus.textContent = force ? tx('正在更新媒体…', 'Updating media…', 'Medien werden aktualisiert …') : tx('正在加载媒体…', 'Loading media…', 'Medien werden geladen …'); let records = []; const directoryResult = session.directoryMediaResult || { files: [], status: {} }; try { records = await (force ? RoomGridModelMedia.capturePack(id, { force: true, live: model.currentShow === 'public', urls: [model.imageUrl, ...(stat.previewUrls || [])].filter(Boolean) }) : RoomGridModelMedia.list(id)); } catch (_) { records = []; } if (singleSession !== session || revision !== mediaRenderRevision) return; const selectedSource = !galleryViewer.hidden ? gallerySources[galleryState.index] : null; const sourceKey = source => source?.record?.mediaId || source?.name || `${source?.origin}:${source?.label}`; const selectedKey = selectedSource ? sourceKey(selectedSource) : ''; // Cancel lazy image loads and video decoders before releasing their // URLs. The asynchronous folder refresh can arrive mid-load. session.stopGalleryMedia(!!selectedSource); const keptUrl = selectedSource?.url?.startsWith('blob:') ? selectedSource.url : ''; (session.galleryUrls || []).filter(url => url !== keptUrl).forEach(url => { try { URL.revokeObjectURL(url); } catch (_) {} }); session.galleryUrls = keptUrl ? [keptUrl] : []; const customCover = records.find(record => record.isCustomCover) || null; applyCoverToAvatar(customCover); const sources = []; const add = item => { if (!item?.url || sources.some(entry => entry.url === item.url || (item.name && entry.name === item.name))) return; if (selectedKey && sourceKey(item) === selectedKey && item.size === selectedSource.size && item.lastModified === selectedSource.lastModified) { if (item.url !== selectedSource.url && item.url.startsWith('blob:')) { URL.revokeObjectURL(item.url); session.galleryUrls = session.galleryUrls.filter(url => url !== item.url); } item.url = selectedSource.url; } sources.push(item); }; const makeObjectUrl = blob => { if (!(blob instanceof Blob)) return ''; const url = URL.createObjectURL(blob); session.galleryUrls.push(url); return url; }; const recordNames = new Set(records.map(record => String(record.fileName || '')).filter(Boolean)); const directoryNames = new Set((directoryResult.files || []).map(file => file.name)); records.forEach((record, index) => add({ url: makeObjectUrl(record.blob), name: record.fileName || '', kind: record.blob.type.startsWith('video/') ? 'video' : 'image', record, blob: record.blob, label: record.blob.type.startsWith('video/') ? record.fileName || tx('录制', 'Recording', 'Aufnahme') : record.userOwned ? `${tx('自己的图片', 'Own image', 'Eigenes Bild')} ${records.filter(entry => entry.userOwned).length - records.filter(entry => entry.userOwned).indexOf(record)}` : `${record.kind === 'cover' ? 'Cover' : tx('示例图片', 'Example image', 'Beispielbild')} ${records.length - index}`, size: record.bytes, lastModified: record.capturedAt, origin: record.userOwned ? tx('图库', 'Gallery', 'Galerie') : tx('公共预览', 'Public preview', 'Öffentliche Vorschau'), })); (directoryResult.files || []).forEach(file => { if (recordNames.has(file.name)) return; add({ url: makeObjectUrl(file.file), name: file.name, kind: file.kind, blob: file.file, label: file.name, size: file.size, lastModified: file.lastModified, origin: `${directoryResult.status?.name || tx('媒体文件夹', 'Media folder', 'Medienordner')} / ${id}` }); }); (session.recentCaptures || []).forEach(file => { if (directoryNames.has(file.name) || recordNames.has(file.name)) return; add({ url: makeObjectUrl(file.blob), name: file.name, kind: file.kind, blob: file.blob, label: file.name, size: file.size, lastModified: file.lastModified, origin: tx('当前会话', 'Current session', 'Aktuelle Sitzung') }); }); const websiteCover = model.imageUrl || discoverFallbackImage(id); add({ url: websiteCover, remoteUrl: websiteCover, kind: 'image', label: tx('当前网站封面', 'Current website cover', 'Aktuelles Website-Cover'), origin: tx('网站', 'Website', 'Website') }); const alternativePreview = `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(id)}.jpg`; add({ url: alternativePreview, remoteUrl: alternativePreview, kind: 'image', label: tx('备用预览', 'Alternative preview', 'Alternative Vorschau'), origin: tx('网站', 'Website', 'Website') }); if (model.currentShow === 'public') { const publicImage = `https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(id)}&f=${Date.now()}`; add({ url: publicImage, remoteUrl: publicImage, kind: 'image', label: tx('当前公开画面', 'Current public image', 'Aktuelles öffentliches Bild'), origin: tx('网站', 'Website', 'Website') }); } gallerySources = sources; if (selectedSource) { const nextIndex = sources.findIndex(source => sourceKey(source) === selectedKey); if (nextIndex >= 0) { galleryState.index = nextIndex; updateGalleryViewer(true); } else { closeGalleryViewer(); const media = galleryMediaHost.querySelector('.single-gallery-media'); if (media?.tagName === 'VIDEO') stopMediaElement(media, false); else media?.removeAttribute('src'); } if (keptUrl && !sources.some(source => source.url === keptUrl)) { URL.revokeObjectURL(keptUrl); session.galleryUrls = session.galleryUrls.filter(url => url !== keptUrl); } } mediaGrid.dataset.renderRevision = String(revision); mediaGrid.dataset.directoryFiles = String(directoryResult.files?.length || 0); mediaGrid.dataset.recentCaptures = String(session.recentCaptures?.length || 0); mediaGrid.replaceChildren(...sources.map((source, sourceIndex) => { const activeBackground = currentModelBackground(); const isBackground = !!source.record && activeBackground.mode === 'image' && activeBackground.mediaOwner === id && activeBackground.mediaId === source.record.mediaId; const media = source.kind === 'video' ? $('video', { src: source.url, controls: true, preload: 'metadata', playsInline: true }) : $('img', { src: source.url, alt: `${id} · ${source.label}`, loading: 'lazy', referrerPolicy: 'no-referrer' }); if (source.kind !== 'video') media.addEventListener('error', () => media.closest('.single-media-card')?.remove()); const buttons = source.kind !== 'video' ? $('div', { class: 'single-media-actions' }, [ $('button', { type: 'button', class: source.record?.isCustomCover ? 'is-cover' : '', disabled: !!source.record?.isCustomCover, onclick: async event => { event.preventDefault(); event.stopPropagation(); mediaStatus.textContent = tx('正在保存封面…', 'Saving cover…', 'Cover wird gespeichert …'); try { await ensureGalleryRecord(source, true); await renderMedia(false); toast(tx('模型封面已保存。', 'Model cover saved.', 'Model-Cover wurde gespeichert.')); } catch (error) { toast(tx('无法保存此封面。', 'This cover could not be saved.', 'Dieses Cover konnte nicht gespeichert werden.')); try { Diagnostics.report('single-gallery-cover', error, id, 'warning'); } catch (_) {} } } }, source.record?.isCustomCover ? tx('当前封面', 'Current cover', 'Aktuelles Cover') : tx('设为封面', 'Set as cover', 'Als Cover festlegen')), $('button', { type: 'button', class: isBackground ? 'is-background' : '', disabled: isBackground, onclick: async event => { event.preventDefault(); event.stopPropagation(); const record = await ensureGalleryRecord(source, false); await RoomGridBackgrounds.useModelMedia(store, id, record.mediaId); clearModelBackgroundButton.hidden = false; syncModelBackgroundControls(); await renderMedia(false); } }, isBackground ? tx('当前背景', 'Current background', 'Aktueller Hintergrund') : tx('设为背景', 'Set as background', 'Als Hintergrund')), source.record?.userOwned ? $('button', { type: 'button', class: 'danger', onclick: async event => { event.preventDefault(); event.stopPropagation(); if (!confirm(tx('从本地图库中删除此图片?', 'Delete this image from the local gallery?', 'Dieses Bild aus der lokalen Galerie löschen?'))) return; if (currentModelBackground().mediaId === source.record.mediaId) { RoomGridBackgrounds.patch(store, 'model', defaultRoomGridBackground('inherit'), id); clearModelBackgroundButton.hidden = true; syncModelBackgroundControls(); } await RoomGridModelMedia.removeMedia(id, source.record.mediaId); await renderMedia(false); } }, tx('删除', 'Delete', 'Löschen')) : null, ]) : null; const meta = gallerySourceMeta(source); const card = $('div', { class: `single-media-card${source.kind === 'video' ? ' is-video' : ''}${source.record?.isCustomCover ? ' is-cover' : ''}${isBackground ? ' is-background' : ''}`, tabindex: '0', role: 'button', ariaLabel: `${source.label} · ${tx('在图库中打开', 'Open in gallery', 'In Galerie öffnen')}`, dataset: { singleMediaCard: String(sourceIndex) } }, [ media, $('span', { class: 'single-media-kind' }, source.kind === 'video' ? tx('视频', 'VIDEO', 'VIDEO') : tx('图片', 'IMAGE', 'BILD')), $('button', { class: 'single-media-open', type: 'button', onclick: event => { event.preventDefault(); event.stopPropagation(); openGalleryViewer(sourceIndex); } }, tx('打开', 'Open', 'Öffnen')), $('div', { class: 'single-media-caption' }, [$('span', {}, source.label), meta ? $('span', { class: 'single-media-meta' }, meta) : null, buttons ? $('details', {}, [$('summary', {}, tx('图片操作', 'Image actions', 'Bildaktionen')), buttons]) : null]), ]); card.addEventListener('click', event => { if (!event.target.closest('button,video,details')) openGalleryViewer(sourceIndex); }); card.addEventListener('keydown', event => { if (event.target === card && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); openGalleryViewer(sourceIndex); } }); return card; })); if (!mediaGrid.children.length) mediaGrid.appendChild($('div', { class: 'single-media-empty' }, tx('还没有媒体。截图或开始录制。', 'No media yet. Take a screenshot or start recording.', 'Noch keine Medien. Erstelle einen Screenshot oder starte eine Aufnahme.'))); const ownCount = records.filter(record => record.userOwned && record.blob.type.startsWith('image/')).length; const videoCount = sources.filter(source => source.kind === 'video').length; mediaStatus.replaceChildren($('span', {}, `${sources.filter(source => source.kind !== 'video').length} ${tx('图片', 'images', 'Bilder')} · ${videoCount} ${tx('视频', 'videos', 'Videos')} · ${ownCount} ${tx('图库图片', 'gallery images', 'Galeriebilder')}`), $('button', { class: 'discover-btn', type: 'button', style: { marginLeft: '8px' }, onclick: () => renderMedia(true) }, tx('全部刷新', 'Refresh all', 'Alles aktualisieren'))); if (captureStorageStatus.textContent) mediaStatus.appendChild(captureStorageStatus); if (!localOnly) { syncCaptureDirectoryStatus().catch(() => {}); RoomGridCaptureDirectory.list(id).then(result => { if (singleSession !== session || revision !== mediaRenderRevision) return; session.directoryMediaResult = result; const before = directoryResult.files || [], after = result.files || []; if (before.length !== after.length || after.some((file, index) => file.name !== before[index]?.name || file.size !== before[index]?.size || file.lastModified !== before[index]?.lastModified)) renderMedia(false, true).catch(() => {}); }).catch(() => {}); } }; session.refreshMedia = (localOnly = false) => renderMedia(false, localOnly); mediaQuality.addEventListener('change', () => persistSinglePreferences({ mediaImportQuality: mediaQuality.value })); chooseMediaButton.addEventListener('click', () => mediaFile.click()); clearCoverButton.addEventListener('click', async () => { await RoomGridModelMedia.clearCustomCover(id); await renderMedia(false); }); clearModelBackgroundButton.addEventListener('click', async () => { RoomGridBackgrounds.patch(store, 'model', defaultRoomGridBackground('inherit'), id); clearModelBackgroundButton.hidden = true; syncModelBackgroundControls(); await renderMedia(false); }); chooseDirectoryButton.addEventListener('click', async () => { chooseDirectoryButton.disabled = true; try { const current = await RoomGridCaptureDirectory.status(); if (!current.supported && current.reason === 'brave-disabled') { directoryImportInput.click(); return; } const result = await RoomGridCaptureDirectory.choose(); if (!result.supported) { const message = tx('此浏览器不提供直接文件夹写入。捕获内容仍保存在图库和下载中。', 'This browser does not provide direct folder writing. Captures are still saved in the gallery and downloads.', 'Dieser Browser stellt kein direktes Schreiben in Ordner bereit. Aufnahmen werden weiterhin in Galerie und Downloads gespeichert.'); directoryStatusText.textContent = message; mediaStatus.textContent = message; toast(message, 5200); return; } await syncCaptureDirectoryStatus(); await renderMedia(false); } catch (error) { if (error?.name !== 'AbortError') { const message = tx('无法打开媒体文件夹。', 'The media folder could not be opened.', 'Der Medienordner konnte nicht geöffnet werden.'); directoryStatusText.textContent = message; toast(message, 5200); try { Diagnostics.report('capture-directory-choose', error, id, 'warning'); } catch (_) {} } } finally { chooseDirectoryButton.disabled = false; } }); grantDirectoryButton.addEventListener('click', async () => { await RoomGridCaptureDirectory.grant(); await syncCaptureDirectoryStatus(); await renderMedia(false); }); clearDirectoryButton.addEventListener('click', async () => { await RoomGridCaptureDirectory.clear(); await syncCaptureDirectoryStatus(); await renderMedia(false); }); directoryImportInput.addEventListener('change', async () => { const files = [...(directoryImportInput.files || [])].filter(file => /^image\/(?:jpeg|png|webp)$/iu.test(String(file.type || ''))); if (!files.length) { directoryImportInput.value = ''; const message = tx('所选文件夹中没有支持的图片。', 'The selected folder contains no supported images.', 'Im gewählten Ordner wurden keine unterstützten Bilder gefunden.'); mediaStatus.textContent = message; toast(message); return; } chooseDirectoryButton.disabled = true; let saved = 0; let failed = 0; for (let index = 0; index < files.length; index += 1) { mediaStatus.textContent = tx(`正在导入图片 ${index + 1}/${files.length}…`, `Importing image ${index + 1}/${files.length}…`, `Bild ${index + 1}/${files.length} wird importiert …`); try { const relativeName = String(files[index].webkitRelativePath || files[index].name || '').replace(/^.*?\//u, '').replace(/\//gu, ' – '); await RoomGridModelMedia.importFile(id, files[index], { quality: mediaQuality.value, makeCover: makeImportedCover.checked && saved === 0, fileName: relativeName || files[index].name }); saved += 1; } catch (_) { failed += 1; } if (index && index % 4 === 0) await new Promise(resolve => setTimeout(resolve, 0)); } directoryImportInput.value = ''; chooseDirectoryButton.disabled = false; await renderMedia(false); const message = failed ? tx(`${saved} 张图片已导入,${failed} 张无法处理。`, `${saved} images imported; ${failed} could not be processed.`, `${saved} Bilder importiert; ${failed} konnten nicht verarbeitet werden.`) : tx(`${saved} 张图片已导入图库。`, `${saved} images imported into the gallery.`, `${saved} Bilder wurden in die Galerie importiert.`); mediaStatus.textContent = message; toast(message); }); mediaFile.addEventListener('change', async () => { const files = [...(mediaFile.files || [])].slice(0, RoomGridModelMedia.limits.userPerModel); if (!files.length) return; chooseMediaButton.disabled = true; mediaStatus.textContent = tx('正在处理自己的图片…', 'Processing your images…', 'Eigene Bilder werden verarbeitet …'); let saved = 0; let failed = 0; for (let index = 0; index < files.length; index += 1) { try { await RoomGridModelMedia.importFile(id, files[index], { quality: mediaQuality.value, makeCover: makeImportedCover.checked && index === 0, fileName: files[index].name }); saved += 1; } catch (_) { failed += 1; } } mediaFile.value = ''; chooseMediaButton.disabled = false; await renderMedia(false); mediaStatus.textContent = failed ? tx(`${saved} 张图片已添加,${failed} 张无法处理。`, `${saved} images added; ${failed} could not be processed.`, `${saved} Bilder hinzugefügt; ${failed} konnten nicht verarbeitet werden.`) : tx(`${saved} 张图片已添加。`, `${saved} images added.`, `${saved} Bilder hinzugefügt.`); }); renderMedia(false); RoomGridModelMedia.list(id).then(records => { if (!records.length && singleSession?.id === id) renderMedia(true); }).catch(() => {}); session.captureDirectoryHandler = () => { syncCaptureDirectoryStatus().catch(() => {}); }; session.captureFileHandler = event => { if (event?.detail?.id !== id) return; setTimeout(() => { if (singleSession === session) session.refreshMedia?.(); }, 0); }; window.addEventListener('roomgrid:capture-directory-changed', session.captureDirectoryHandler); window.addEventListener('roomgrid:capture-file-saved', session.captureFileHandler); const settingRow = (title, note, control) => $('label', { class: 'single-setting-row' }, [ $('span', {}, [$('strong', {}, title), $('small', {}, note)]), control, ]); const togglePreference = (key, title, note, onApply) => { const input = $('input', { type: 'checkbox', checked: !!preferences[key], dataset: { singlePreference: key } }); input.addEventListener('change', () => { persistSinglePreferences({ [key]: input.checked }); onApply?.(input.checked); }); return settingRow(title, note, input); }; const selectPreference = (key, title, note, options, onApply) => { const select = $('select', { class: 'discover-input', dataset: { singlePreference: key } }, options.map(([value, label]) => $('option', { value: String(value), selected: String(preferences[key]) === String(value) }, label))); select.addEventListener('change', () => { const raw = select.value; const value = typeof preferences[key] === 'number' ? Number(raw) : raw; persistSinglePreferences({ [key]: value }); onApply?.(value); }); return settingRow(title, note, select); }; const modelBackgroundFile = $('input', { type: 'file', accept: 'image/jpeg,image/png,image/webp', hidden: true, dataset: { singleBackgroundFile: '1' } }); const modelBackgroundPreview = $('div', { class: 'single-background-preview', dataset: { singleBackgroundPreview: '1' } }); const modelBackgroundStatus = $('div', { class: 'single-background-status', role: 'status', ariaLive: 'polite' }); const backgroundSelect = (key, options) => { const select = $('select', { class: 'discover-input', dataset: { singleBackgroundControl: key } }, options.map(([value, label]) => $('option', { value }, label))); select.addEventListener('change', () => { RoomGridBackgrounds.patch(store, 'model', { [key]: select.value }, id); syncModelBackgroundControls(); }); return select; }; const backgroundColor = key => { const input = $('input', { type: 'color', dataset: { singleBackgroundControl: key } }); input.addEventListener('input', () => { RoomGridBackgrounds.patch(store, 'model', { [key]: input.value }, id); syncModelBackgroundControls(false); }); return input; }; const backgroundRange = (key, min, max, step, suffix = '%') => { const input = $('input', { type: 'range', min, max, step, dataset: { singleBackgroundControl: key } }); const output = $('output'); input.addEventListener('input', () => { output.textContent = `${input.value}${suffix}`; RoomGridBackgrounds.patch(store, 'model', { [key]: Number(input.value) }, id); syncModelBackgroundControls(false); }); return $('div', { class: 'single-background-range' }, [input, output]); }; const backgroundField = (label, control) => $('label', { class: 'single-background-field' }, [$('span', {}, label), control]); const backgroundMode = backgroundSelect('mode', [ ['inherit', tx('使用页面背景', 'Use page background', 'Seitenhintergrund verwenden')], ['color', tx('纯色', 'Solid color', 'Farbe')], ['gradient', tx('渐变', 'Gradient', 'Verlauf')], ['image', tx('自己的图片', 'Own image', 'Eigenes Bild')], ]); const backgroundFit = backgroundSelect('fit', [ ['cover', tx('填充并裁剪', 'Fill and crop', 'Füllen & zuschneiden')], ['contain', tx('完整显示', 'Show completely', 'Vollständig zeigen')], ['stretch', tx('拉伸', 'Stretch', 'Strecken')], ['original', tx('原始尺寸', 'Original size', 'Originalgröße')], ['tile', tx('平铺', 'Tile', 'Kacheln')], ]); const backgroundControls = { mode: backgroundMode, fit: backgroundFit, color: backgroundColor('color'), gradientStart: backgroundColor('gradientStart'), gradientEnd: backgroundColor('gradientEnd'), gradientAngle: backgroundRange('gradientAngle', 0, 360, 1, '°'), zoom: backgroundRange('zoom', 50, 240, 1), positionX: backgroundRange('positionX', 0, 100, 1), positionY: backgroundRange('positionY', 0, 100, 1), opacity: backgroundRange('opacity', 10, 100, 1), dim: backgroundRange('dim', 0, 90, 1), blur: backgroundRange('blur', 0, 32, 1, ' px'), panelOpacity: backgroundRange('panelOpacity', 55, 100, 1), }; const modelBackgroundFixed = $('input', { type: 'checkbox', dataset: { singleBackgroundControl: 'fixed' } }); modelBackgroundFixed.addEventListener('change', () => { RoomGridBackgrounds.patch(store, 'model', { fixed: modelBackgroundFixed.checked }, id); syncModelBackgroundControls(false); }); const refreshModelBackgroundPreview = async config => { if (session.backgroundPreviewUrl) { try { URL.revokeObjectURL(session.backgroundPreviewUrl); } catch (_) {} session.backgroundPreviewUrl = ''; } modelBackgroundPreview.replaceChildren(); modelBackgroundPreview.style.background = ''; modelBackgroundPreview.style.backgroundSize = ''; modelBackgroundPreview.style.backgroundPosition = `${config.positionX}% ${config.positionY}%`; if (config.mode === 'inherit') { modelBackgroundPreview.style.background = 'linear-gradient(135deg,var(--dc-panel-soft),var(--dc-panel))'; return; } if (config.mode === 'color') { modelBackgroundPreview.style.background = config.color; return; } if (config.mode === 'gradient') { modelBackgroundPreview.style.background = `linear-gradient(${config.gradientAngle}deg,${config.gradientStart},${config.gradientEnd})`; return; } const url = await RoomGridBackgrounds.previewUrl(config); if (singleSession !== session || !url) return; session.backgroundPreviewUrl = url; modelBackgroundPreview.style.backgroundImage = `linear-gradient(rgba(0,0,0,${config.dim / 100}),rgba(0,0,0,${config.dim / 100})),url("${url}")`; modelBackgroundPreview.style.backgroundRepeat = config.fit === 'tile' ? 'repeat' : 'no-repeat'; modelBackgroundPreview.style.backgroundSize = config.fit === 'tile' ? 'auto' : config.fit === 'stretch' ? '100% 100%' : config.fit; }; syncModelBackgroundControls = (refreshPreview = true) => { const config = currentModelBackground(); Object.entries(backgroundControls).forEach(([key, control]) => { if (control.matches?.('select,input')) control.value = String(config[key]); else { const input = control.querySelector('input'); const output = control.querySelector('output'); if (input) input.value = String(config[key]); if (output) output.textContent = `${config[key]}${key === 'gradientAngle' ? '°' : key === 'blur' ? ' px' : '%'}`; } }); modelBackgroundFixed.checked = config.fixed !== false; clearModelBackgroundButton.hidden = config.mode === 'inherit'; modelBackgroundStatus.textContent = config.mode === 'inherit' ? tx('此模型使用 SingleCam 页面或全局背景。', 'This model uses the SingleCam page or global background.', 'Dieses Model verwendet den SingleCam-Seiten- oder globalen Hintergrund.') : tx('此背景仅用于这个模型,并会在更新后保留。', 'This background is only for this model and survives updates.', 'Dieser Hintergrund gilt nur für dieses Model und bleibt bei Updates erhalten.'); if (refreshPreview) void refreshModelBackgroundPreview(config); }; modelBackgroundFile.addEventListener('change', async () => { const file = modelBackgroundFile.files?.[0]; if (!file) return; modelBackgroundStatus.textContent = tx('正在处理背景…', 'Processing background…', 'Hintergrund wird verarbeitet …'); try { await RoomGridBackgrounds.importImage(store, 'model', file, id); await renderMedia(false); } catch (_) { modelBackgroundStatus.textContent = tx('无法处理此图片。', 'This image could not be processed.', 'Dieses Bild konnte nicht verarbeitet werden.'); } modelBackgroundFile.value = ''; syncModelBackgroundControls(); }); const modelBackgroundSection = $('details', { class: 'single-setting-section', open: true, dataset: { singleBackgroundSettings: '1' } }, [ $('summary', {}, tx('此模型的页面背景', 'Page background for this model', 'Seitenhintergrund für dieses Model')), $('div', { class: 'single-background-controls' }, [ modelBackgroundPreview, modelBackgroundStatus, $('div', { class: 'single-background-grid' }, [ backgroundField(tx('模式', 'Mode', 'Modus'), backgroundControls.mode), backgroundField(tx('填充', 'Fit', 'Darstellung'), backgroundControls.fit), backgroundField(tx('颜色', 'Color', 'Farbe'), backgroundControls.color), backgroundField(tx('渐变开始', 'Gradient start', 'Verlauf Start'), backgroundControls.gradientStart), backgroundField(tx('渐变结束', 'Gradient end', 'Verlauf Ende'), backgroundControls.gradientEnd), backgroundField(tx('渐变角度', 'Gradient angle', 'Verlaufswinkel'), backgroundControls.gradientAngle), backgroundField(tx('缩放', 'Zoom', 'Zoom'), backgroundControls.zoom), backgroundField(tx('水平位置', 'Horizontal position', 'Position horizontal'), backgroundControls.positionX), backgroundField(tx('垂直位置', 'Vertical position', 'Position vertikal'), backgroundControls.positionY), backgroundField(tx('图片透明度', 'Image opacity', 'Bilddeckkraft'), backgroundControls.opacity), backgroundField(tx('变暗', 'Darkening', 'Abdunklung'), backgroundControls.dim), backgroundField(tx('模糊', 'Blur', 'Unschärfe'), backgroundControls.blur), backgroundField(tx('面板透明度', 'Panel opacity', 'Panel-Deckkraft'), backgroundControls.panelOpacity), ]), $('label', { class: 'single-setting-row' }, [$('span', {}, [$('strong', {}, tx('固定背景', 'Fixed background', 'Hintergrund fixieren')), $('small', {}, tx('滚动时背景保持不动。', 'The background stays still while scrolling.', 'Der Hintergrund bleibt beim Scrollen stehen.'))]), modelBackgroundFixed]), $('div', { class: 'single-directory-actions' }, [ $('button', { class: 'discover-btn primary', type: 'button', onclick: () => modelBackgroundFile.click() }, tx('从电脑选择图片', 'Choose image from computer', 'Bild vom PC wählen')), $('button', { class: 'discover-btn', type: 'button', onclick: () => activateSingleTab('media') }, tx('从图库选择', 'Choose from gallery', 'Aus Galerie wählen')), clearModelBackgroundButton, ]), modelBackgroundFile, ]), ]); syncModelBackgroundControls(); const soundToggle = $('input', { type: 'checkbox', checked: RoomGridAudioSilencer.isEnabled(), dataset: { singleSoundMute: '1' } }); const soundStatus = $('span', { class: 'single-settings-status' }); const topSoundStatus = $('span', { class: 'discover-chip single-sound-state' }); const syncSoundStatus = () => { const muted = RoomGridAudioSilencer.isEnabled(); soundToggle.checked = muted; soundStatus.textContent = muted ? tx('所有页面声音已关闭', 'ALL PAGE SOUNDS OFF', 'ALLE SEITENTÖNE AUS', 'TODOS LOS SONIDOS DE LA PÁGINA DESACTIVADOS', 'TODOS OS SONS DA PÁGINA DESATIVADOS') : tx('允许页面声音', 'PAGE SOUNDS ALLOWED', 'SEITENTÖNE ERLAUBT', 'SONIDOS DE LA PÁGINA ACTIVADOS', 'SONS DA PÁGINA ATIVADOS'); topSoundStatus.textContent = muted ? tx('聊天/代币声音关闭', 'Chat/token sounds OFF', 'Chat/Token-Töne AUS', 'Sonidos de chat/tokens DESACTIVADOS', 'Sons de chat/tokens DESATIVADOS') : tx('聊天/代币声音开启', 'Chat/token sounds ON', 'Chat/Token-Töne AN', 'Sonidos de chat/tokens ACTIVADOS', 'Sons de chat/tokens ATIVADOS'); topSoundStatus.classList.toggle('is-on', !muted); }; session.soundHandler = syncSoundStatus; window.addEventListener('roomgrid_audio_silencer_changed', session.soundHandler); soundToggle.addEventListener('change', () => { RoomGridAudioSilencer.setEnabled(soundToggle.checked); syncSoundStatus(); }); const reconnectPlayer = () => { clearTimeout(session.pollTimer); session.pollTimer = 0; session.connectionRevision += 1; try { session.hls?.destroy(); } catch (_) {} session.hls = null; session.source = ''; session.failures = 0; stopMediaElement(video, false); statusNode.textContent = tx('正在使用新设置重新连接播放器…', 'Reconnecting the player with the new settings…', 'Player wird mit den neuen Einstellungen verbunden …', 'Reconectando el reproductor con los nuevos ajustes…', 'Reconectando o player com as novas configurações…'); startSinglePlayer(session, quality, statusNode); }; const settingsPanel = $('section', { class: 'single-panel single-settings-panel', dataset: { singleSettings: '1' } }, [ $('div', { class: 'single-settings-head' }, [ $('div', {}, [$('h3', {}, 'SingleCam einstellen'), $('p', {}, 'Alle Änderungen werden lokal gespeichert und gelten auch beim nächsten Model.')]), soundStatus, ]), $('div', { class: 'single-settings-grid' }, [ $('details', { class: 'single-setting-section', open: true, dataset: { singleDirectorySettings: '1' } }, [ $('summary', {}, tx('媒体文件夹和自动保存', 'Media folder & automatic saving', 'Medienordner & automatisches Speichern')), $('div', { class: 'single-directory-box' }, [ directoryStatus, directoryHelpText, $('div', { class: 'single-directory-actions' }, [chooseDirectoryButton, grantDirectoryButton, clearDirectoryButton]), directoryFootnote, directoryImportInput, ]), ]), modelBackgroundSection, $('details', { class: 'single-setting-section', open: true }, [ $('summary', {}, 'Ton & Ruhe'), $('div', { class: 'single-setting-list' }, [ settingRow( tx('完全关闭聊天、代币和小费提示音', 'Turn off chat, token and tip sounds completely', 'Chat-, Token- und Tip-Töne vollständig aus', 'Desactivar completamente los sonidos de chat, tokens y propinas', 'Desativar completamente os sons de chat, tokens e gorjetas'), tx('阻止音频元素和 Web Audio 的页面提示音。直播声音仍可通过“打开声音”单独控制。', 'Blocks page sounds from audio elements and Web Audio. Stream audio remains separately controllable through “Sound on”.', 'Blockiert Seitentöne aus Audioelementen und Web-Audio. Der Streamton bleibt über „Ton an“ separat steuerbar.', 'Bloquea los sonidos de la página procedentes de elementos de audio y Web Audio. El sonido del stream se controla por separado mediante «Activar sonido».', 'Bloqueia os sons da página provenientes de elementos de áudio e Web Audio. O som da transmissão continua sendo controlado separadamente por “Ativar som”.'), soundToggle, ), togglePreference('startMuted', 'Stream immer stumm starten', 'Jedes neu geöffnete Model beginnt ohne Streamton.', checked => { if (checked) { video.muted = true; syncPlaybackButtons(); } }), togglePreference('rememberVolume', 'Lautstärke pro Model merken', 'Speichert die gewählte Streamlautstärke lokal im Modelprofil.'), selectPreference('maxVolume', 'Maximale Stream-Lautstärke', 'Begrenzt den Lautstärkeregler unabhängig vom gespeicherten Wert.', [[.25, '25 %'], [.5, '50 %'], [.75, '75 %'], [1, '100 %']]), togglePreference('pauseHidden', 'Bei verstecktem Tab pausieren', 'Stoppt Wiedergabe im Hintergrund und setzt sie bei Rückkehr fort.'), ]), ]), $('details', { class: 'single-setting-section', open: true }, [ $('summary', {}, 'Player & Verbindung'), $('div', { class: 'single-setting-list' }, [ togglePreference('autoplay', 'Automatisch abspielen', 'Startet einen verfügbaren Live-Stream ohne zusätzlichen Klick.'), togglePreference('lowLatency', 'Niedrige Live-Latenz', 'Bevorzugt Echtzeitnähe; Änderungen greifen beim Neuverbinden.'), togglePreference('autoReconnect', 'Automatisch neu verbinden', 'Behebt begrenzte Netzwerk- und Medienfehler selbstständig.'), selectPreference('bufferSeconds', 'Puffergröße', 'Kleiner reagiert schneller, größer ist bei schwankender Verbindung stabiler.', [[5, '5 Sekunden'], [10, '10 Sekunden'], [20, '20 Sekunden'], [30, '30 Sekunden'], [45, '45 Sekunden']]), selectPreference('pollSeconds', 'Status-Aktualisierung', 'Prüft Offline-/Privat-/Live-Status in diesem Abstand.', [[15, '15 Sekunden'], [30, '30 Sekunden'], [45, '45 Sekunden'], [60, '60 Sekunden'], [90, '90 Sekunden']]), selectPreference('aspect', 'Player-Seitenverhältnis', 'Ändert sofort die Größe der Videofläche.', [['16/9', '16:9 Breitbild'], ['4/3', '4:3 Klassisch'], ['21/9', '21:9 Kino']]), selectPreference('stageSize', 'Player-Höhe', 'Eigene Größe über „Streamgröße“ direkt unter dem Stream frei einstellen.', [['standard', 'Standard'], ['large', 'Groß'], ['maximum', 'Maximal'], ['custom', 'Eigene Größe']]), selectPreference('fit', 'Bildanpassung', 'Einpassen zeigt alles; Ausfüllen nutzt die ganze Fläche.', [['contain', 'Vollständig einpassen'], ['cover', 'Fläche ausfüllen']], value => { fit.value = value; }), selectPreference('background', 'Player-Hintergrund', 'Schwarz, weich oder mit ruhiger Umgebungsfläche.', [['black', 'Tiefschwarz'], ['soft', 'Weich dunkel'], ['ambient', 'Ambient']]), selectPreference('playerCorners', 'Player-Ecken', 'Eckig, dezent oder deutlich abgerundet.', [['square', 'Eckig'], ['soft', 'Dezent'], ['round', 'Rund']]), togglePreference('showPlayerStatus', 'Status im Player anzeigen', 'Zeigt Verbindung, Live-Status und Zuschauerzahl über dem Bild.'), ]), ]), $('details', { class: 'single-setting-section' }, [ $('summary', {}, 'Layout & sichtbare Bereiche'), $('div', { class: 'single-setting-list' }, [ selectPreference('layout', 'Seitengröße', 'Breit nutzt den Platz, Fokus stellt den Player über die Informationen.', [['wide', 'Breit'], ['focus', 'Player-Fokus'], ['compact', 'Kompakt']]), selectPreference('sidebar', 'Position der Informationen', 'Rechts, links oder vollständig unter dem Player.', [['right', 'Rechts'], ['left', 'Links'], ['below', 'Unter dem Player']]), selectPreference('cockpitSize', 'Breite des Informationsbereichs', 'Kompakt, normal oder extra breit für lange Angaben.', [['compact', 'Kompakt'], ['normal', 'Normal'], ['wide', 'Extra breit']]), selectPreference('controlSize', 'Größe der Bedienelemente', 'Steuert Abstände und Schaltflächen unter dem Stream.', [['compact', 'Kompakt'], ['normal', 'Normal'], ['large', 'Groß']]), togglePreference('showLocal', 'Private Notizen anzeigen', 'Blendet lokale Notizen und eigene Tags ein oder aus.'), togglePreference('showProfile', 'Öffentliches Profil anzeigen', 'Blendet Bio, öffentliche Tags und Profilverweise ein oder aus.'), togglePreference('showChat', 'Chat anzeigen', 'Der offizielle Chat wird erst beim Öffnen des Chat-Tabs geladen.', checked => { if (!checked) { unloadChat(); if (preferences.activeTab === 'chat') activateSingleTab('overview'); } }), togglePreference('pauseHiddenChat', tx('离开后暂停聊天', 'Pause hidden chat', 'Chat beim Verlassen pausieren'), tx('离开聊天标签时断开连接。', 'Disconnect the chat when leaving its tab.', 'Der Chat wird beim Verlassen seines Reiters getrennt.'), checked => { if (checked && chatLoaded) unloadChat(); }), togglePreference('showAlternatives', 'Ähnliche Models anzeigen', 'Steuert den Bereich unterhalb der Einzelansicht.'), togglePreference('compactControls', 'Bildwerkzeuge einklappen', 'Startet den großen Filterbereich platzsparend geschlossen.'), ]), ]), ]), $('div', { class: 'single-settings-actions' }, [ $('button', { class: 'discover-btn primary', type: 'button', onclick: reconnectPlayer }, 'Player neu verbinden'), $('button', { class: 'discover-btn', type: 'button', onclick: () => { state.singlePreferences = singleDefaultPreferences(); RoomGridAudioSilencer.setEnabled(true); persistDiscoverState(); renderSingleCam(); } }, 'SingleCam-Standard wiederherstellen'), ]), ]); const similarGrid = $('div', { class: 'discover-grid' }); let similarSignature = ''; const prepareSimilarModels = () => { const signature = dataCacheSignature(); if (similarSignature === signature) return; const sharedTags = new Set(modelTags(model)); const similar = catalog.filter(item => item.id !== id).map(item => ({ item, score: modelTags(item).filter(tag => sharedTags.has(tag)).length })).filter(row => row.score > 0).sort((a, b) => b.score - a.score || b.item.viewers - a.item.viewers).map(row => row.item).slice(0, 8); const alternatives = similar.length ? similar : singleContextIds.filter(other => other !== id).map(localModel).slice(0, 8); similarGrid.replaceChildren(...alternatives.map(item => createModelCard(item, true, alternatives.map(other => other.id)))); if (!alternatives.length) similarGrid.appendChild($('div', { class: 'discover-empty' }, tx('没有可用的替代项。', 'No alternatives available.', 'Keine Alternativen verfügbar.'))); similarSignature = signature; }; prepareSingleAlternatives = prepareSimilarModels; if (preferences.showAlternatives) prepareSimilarModels(); const similarPanel = $('section', { class: 'dashboard-section single-similar', dataset: { singleAlternatives: '1' } }, [$('div', { class: 'dashboard-section-head' }, [$('div', {}, [$('h2', {}, tx('相似模型与离线替代项', 'Similar models and offline alternatives', 'Ähnliche Models und Offline-Alternativen')), $('p', {}, tx('按共同标签优先;不推断身份或外部账户。', 'Prioritized by shared tags; no identity or external-account inference.', 'Nach gemeinsamen Tags priorisiert; keine Ableitung von Identität oder externen Konten.'))])]), similarGrid]); const tabDefinitions = [ ['overview', tx('概览', 'Overview', 'Übersicht'), infoPanel], ['chat', tx('聊天', 'Chat', 'Chat'), chatPanel], ['notes', tx('笔记和组织', 'Notes & organization', 'Notizen & Ordnung'), localPanel], ['statistics', tx('统计', 'Statistics', 'Statistik'), statisticsPanel], ['media', tx('图库', 'Gallery', 'Galerie'), mediaPanel], ['profile', tx('公开资料', 'Public profile', 'Öffentliches Profil'), profilePanel], ['settings', tx('设置', 'Settings', 'Einstellungen'), settingsPanel], ]; const tabButtons = new Map(); const tabPanels = new Map(); const tabNav = $('nav', { class: 'single-cockpit-tabs', ariaLabel: tx('单摄像头区域', 'SingleCam sections', 'SingleCam-Bereiche') }); const tabBody = $('div', { class: 'single-cockpit-body' }); tabDefinitions.forEach(([key, label, panel]) => { const button = $('button', { class: 'single-cockpit-tab', type: 'button', dataset: { singleTab: key } }, label); const host = $('div', { class: 'single-tab-panel', dataset: { singlePanel: key } }, [panel]); button.addEventListener('click', () => activateSingleTab(key)); tabButtons.set(key, button); tabPanels.set(key, host); tabNav.appendChild(button); tabBody.appendChild(host); }); activateSingleTab = (requested, persist = true) => { let key = tabPanels.has(requested) ? requested : 'overview'; if (key === 'notes' && !preferences.showLocal) key = 'overview'; if (key === 'profile' && !preferences.showProfile) key = 'overview'; if (key === 'chat' && !preferences.showChat) key = 'overview'; tabButtons.forEach((button, name) => { button.classList.toggle('active', name === key); button.setAttribute('aria-selected', String(name === key)); }); tabPanels.forEach((panel, name) => { panel.hidden = name !== key; }); if (key === 'chat' && (!preferences.sidebarCollapsed || persist || !preferences.pauseHiddenChat)) loadChat(false); else if (preferences.pauseHiddenChat && chatLoaded) unloadChat(); if (key === 'statistics') renderSingleStatistics(); if (persist && (preferences.activeTab !== key || preferences.sidebarCollapsed)) persistSinglePreferences({ activeTab: key, sidebarCollapsed: false }); }; const cockpit = $('aside', { class: 'single-details', dataset: { singleCockpit: '1' } }, [tabNav, tabBody]); const topbar = $('div', { class: 'single-topbar' }, [ $('button', { class: 'discover-btn', type: 'button', dataset: { singleModuleToggle: 'sidebar' }, onclick: () => { persistSinglePreferences({ sidebarCollapsed: !preferences.sidebarCollapsed }); if (!preferences.sidebarCollapsed) activateSingleTab(preferences.activeTab, false); else if (preferences.pauseHiddenChat && chatLoaded) unloadChat(); } }), $('button', { class: 'discover-btn', type: 'button', dataset: { singleModuleToggle: 'tools' }, onclick: () => persistSinglePreferences({ toolsCollapsed: !preferences.toolsCollapsed }) }), $('button', { class: 'discover-btn', type: 'button', dataset: { singleMaximize: '1' }, onclick: () => persistSinglePreferences({ layout: 'wide', sidebarCollapsed: true, toolsCollapsed: true, stageSize: 'maximum', streamWidth: 100 }) }, tx('最大直播区域', 'Maximize stream', 'Stream maximieren')), $('button', { class: 'discover-btn', type: 'button', title: tx('返回原始结果', 'Back to original results', 'Zurück zur ursprünglichen Suche'), ariaLabel: tx('返回原始结果', 'Back to original results', 'Zurück zur ursprünglichen Suche'), onclick: () => setView(singleReturnView) }, `← ${tx('返回列表', 'Back to list', 'Zur Liste')}`), $('span', { class: 'single-workspace-badge' }, 'SingleCam Pro'), $('div', { class: 'single-model-identity' }, [ (modelAvatar = $('img', { class: 'single-model-avatar', src: model.imageUrl || discoverFallbackImage(id), alt: id, referrerPolicy: 'no-referrer' })), $('span', { class: 'single-live-dot', title: model.currentShow === 'public' ? 'LIVE' : String(model.currentShow || '—') }), $('span', { class: 'single-model-title' }, [$('strong', {}, id), $('span', {}, [model.country, model.location, model.age ? `${model.age} ${tx('岁', 'years', 'Jahre')}` : ''].filter(Boolean).join(' · ') || tx('私人工作区', 'Private workspace', 'Privater Arbeitsbereich'))]), ]), topSoundStatus, $('span', { class: 'discover-chip' }, singleContextIds.length ? `${contextIndex + 1} / ${singleContextIds.length}` : '1 / 1'), $('button', { class: 'discover-btn', type: 'button', disabled: !previousId, onclick: () => openSingle(previousId, singleContextIds) }, `← ${tx('上一项', 'Previous', 'Zurück')}`), $('button', { class: 'discover-btn', type: 'button', disabled: !nextId, onclick: () => openSingle(nextId, singleContextIds) }, `${tx('下一项', 'Next', 'Weiter')} →`), $('button', { class: 'discover-btn', type: 'button', dataset: { singleChatToggle: '1' }, onclick: () => { if (!preferences.showChat) persistSinglePreferences({ showChat: true }); activateSingleTab(preferences.activeTab === 'chat' ? 'overview' : 'chat'); } }, tx('聊天开/关', 'Chat on/off', 'Chat ein/aus')), $('button', { class: 'discover-btn', type: 'button', dataset: { singleSimilarToggle: '1' }, onclick: event => { persistSinglePreferences({ showAlternatives: !preferences.showAlternatives }); if (preferences.showAlternatives) prepareSimilarModels(); event.currentTarget.textContent = preferences.showAlternatives ? tx('隐藏相似模型', 'Hide similar models', 'Ähnliche Models ausblenden') : tx('显示相似模型', 'Show similar models', 'Ähnliche Models anzeigen'); } }, preferences.showAlternatives ? tx('隐藏相似模型', 'Hide similar models', 'Ähnliche Models ausblenden') : tx('显示相似模型', 'Show similar models', 'Ähnliche Models anzeigen')), ]); const secondaryTools = [...topbar.querySelectorAll('[data-single-module-toggle], [data-single-maximize], [data-single-similar-toggle]')]; topbar.append($('details', { class: 'single-secondary-tools', open: !matchMedia('(max-width: 700px)').matches }, [ $('summary', { class: 'discover-btn' }, tx('更多', 'More', 'Mehr')), $('div', { class: 'single-secondary-actions' }, secondaryTools), ])); const singlePage = $('div', { class: 'single-page', dataset: { singleId: id } }, [ topbar, $('div', { class: 'single-layout' }, [$('section', { class: 'single-player-panel' }, [stage, controls]), cockpit]), similarPanel, ]); preferenceTargets.page = singlePage; preferenceTargets.local = localPanel; preferenceTargets.profile = profilePanel; preferenceTargets.notesTab = tabButtons.get('notes'); preferenceTargets.profileTab = tabButtons.get('profile'); preferenceTargets.chatTab = tabButtons.get('chat'); preferenceTargets.chatPanel = chatPanel; preferenceTargets.alternatives = similarPanel; main.replaceChildren(singlePage); const syncToolbarHeight = () => singlePage.style.setProperty('--single-toolbar-height', `${Math.ceil(topbar.getBoundingClientRect().height)}px`); syncToolbarHeight(); if (typeof ResizeObserver === 'function') { session.layoutObserver = new ResizeObserver(syncToolbarHeight); session.layoutObserver.observe(topbar); } RoomGridModelMedia.getCustomCover(id).then(record => { if (singleSession?.id === id) applyCoverToAvatar(record); }).catch(() => {}); applyPagePreferences(); activateSingleTab(preferences.activeTab, false); syncSoundStatus(); } function renderIntegratedHub(view) { if (!main.querySelector('[data-roomgrid-embedded-hub="1"]')) main.replaceChildren(); RoomGridNativeHub.mount(main, view, store, analytics, notificationCenter, windowCoordinator, next => setView(next)); } function renderBackgroundView() { if (destroyed) return; // Catalog/analytics enrichment can wait until the pointer leaves. User // navigation, filters and explicit refresh still render immediately. if (previewScheduler.isHovering()) { if (!backgroundRenderTimer) backgroundRenderTimer = setTimeout(() => { backgroundRenderTimer = 0; renderBackgroundView(); }, 120); return; } renderCurrentView(true); } function renderCurrentView(allowCachedView = false) { if (destroyed) return; clearTimeout(backgroundRenderTimer); backgroundRenderTimer = 0; const activeFilter = document.activeElement?.dataset?.discoverTagFilter ? document.activeElement : null; const filterFocus = activeFilter ? { key: activeFilter.dataset.discoverTagFilter, start: activeFilter.selectionStart, end: activeFilter.selectionEnd } : pendingFilterFocus; const activeSearch = document.activeElement?.dataset?.discoverSearchField ? document.activeElement : null; const searchFocus = activeSearch ? { start: activeSearch.selectionStart, end: activeSearch.selectionEnd } : null; const homeTools = state.view === 'dashboard' ? main.querySelector('[data-home-tools-panel]:not([hidden])') : null; const homeToolsScroll = homeTools ? { id: homeTools.dataset.homeToolsPanel, top: homeTools.scrollTop } : null; pendingFilterFocus = null; ensureLocalIndexes(); const activeTagSearch = state.view === 'tags' && document.activeElement?.dataset?.tagExplorerSearch ? document.activeElement : null; const tagSearchFocus = activeTagSearch ? { start: activeTagSearch.selectionStart, end: activeTagSearch.selectionEnd } : null; if (activeTagSearch && activeTagSearch.value !== state.tagSearch) { state.tagSearch = discoverCleanText(activeTagSearch.value, 80).toLocaleLowerCase(); state.tagBrowsePage = 1; persistDiscoverState(true); } activeCardMoreClose?.(); const hubView = ['stats', 'library', 'following', 'user'].includes(state.view); if (!hubView) RoomGridNativeHub.unmountEmbedded(); document.body.classList.toggle('roomgrid-singlecam-active', state.view === 'single'); document.body.classList.toggle('roomgrid-dashboard-active', state.view === 'dashboard'); header.dataset.singleCam = state.view === 'single' ? '1' : '0'; dashboardNav.classList.toggle('active', state.view === 'dashboard'); discoverNav.classList.toggle('active', state.view === 'discover'); tagsNav.classList.toggle('active', state.view === 'tags'); statsNav.classList.toggle('active', state.view === 'stats'); libraryNav.classList.toggle('active', state.view === 'library'); followingNav.classList.toggle('active', state.view === 'following'); userNav.classList.toggle('active', state.view === 'user'); refreshBtn.disabled = catalogLoading; document.title = state.view === 'dashboard' ? tx('RoomGrid 主页', 'RoomGrid Home', 'RoomGrid Start') : state.view === 'tags' ? tx('RoomGrid 标签', 'RoomGrid Tags', 'RoomGrid Tag-Explorer') : state.view === 'stats' ? tx('RoomGrid 统计', 'RoomGrid Statistics', 'RoomGrid Statistikzentrum') : state.view === 'library' ? tx('RoomGrid 模型库', 'RoomGrid Model library', 'RoomGrid Modellbibliothek') : state.view === 'following' ? (I18N[LANG]?.followingUi || I18N.en.followingUi).title : state.view === 'user' ? tx('RoomGrid 我的用户', 'RoomGrid My user', 'RoomGrid Mein Nutzer') : state.view === 'single' ? `SingleCam · ${state.singleId}` : tx('RoomGrid 发现', 'RoomGrid Discover', 'RoomGrid Entdecken'); const restored = allowCachedView && restoreCachedVisualView(state.view); if (!restored) { if (state.view === 'dashboard') renderDashboard(); else if (state.view === 'tags') renderTagExplorer(); else if (hubView) renderIntegratedHub(state.view); else if (state.view === 'single') renderSingleCam(); else renderDiscover(); } activeVisualSignature = ['dashboard', 'discover', 'tags'].includes(state.view) ? visualViewSignature(state.view) : ''; if (filterFocus && ['discover', 'dashboard'].includes(state.view)) { const input = main.querySelector(`[data-discover-tag-filter="${filterFocus.key}"]`); input?.focus({ preventScroll: true }); try { input?.setSelectionRange(filterFocus.start, filterFocus.end); } catch (_) {} } if (searchFocus && ['discover', 'dashboard'].includes(state.view)) { const input = main.querySelector('[data-discover-search-field]'); input?.focus({ preventScroll: true }); try { input?.setSelectionRange(searchFocus.start, searchFocus.end); } catch (_) {} } if (homeToolsScroll && state.view === 'dashboard') { const panel = main.querySelector(`[data-home-tools-panel="${homeToolsScroll.id}"]:not([hidden])`); if (panel) panel.scrollTop = homeToolsScroll.top; } if (tagSearchFocus && state.view === 'tags') { const input = main.querySelector('[data-tag-explorer-search]'); input?.focus({ preventScroll: true }); try { input?.setSelectionRange(tagSearchFocus.start, tagSearchFocus.end); } catch (_) {} } if (pendingScrollRestore && state.view !== 'single') { pendingScrollRestore = false; const y = Math.max(0, Number(state.scrollPositions[state.view]) || 0); requestAnimationFrame(() => window.scrollTo({ top: y, behavior: 'auto' })); } void RoomGridBackgrounds.apply(store); } function createCatalogCacheSnapshot() { const rooms = catalog .filter(model => model?.source === 'catalog' || model?.source === 'catalog-cache') .map(discoverPackCachedRoom); if (!rooms.length) return null; return { schema: 2, savedAt: Date.now(), fetchedAt: Number(catalogFetchedAt) || Date.now(), total: Math.max(rooms.length, Number(catalogTotal) || 0), totalKnown: !!catalogTotalKnown, offset: Math.max(rooms.length, Number(catalogOffset) || 0), reachedEnd: !!catalogReachedEnd, rooms, }; } function saveCatalogCacheNow(hotOnly = false) { clearTimeout(catalogCacheSaveTimer); catalogCacheSaveTimer = 0; const snapshot = createCatalogCacheSnapshot(); if (!snapshot) return; catalogCacheSavedAt = snapshot.savedAt; if (hotOnly) RoomGridLiveCatalogCache.saveHot(snapshot); else RoomGridLiveCatalogCache.save(snapshot).catch(() => {}); } function scheduleCatalogCacheSave(immediate = false) { clearTimeout(catalogCacheSaveTimer); catalogCacheSaveTimer = setTimeout(() => saveCatalogCacheNow(false), immediate ? 0 : 650); } function applyCatalogCacheSnapshot(snapshot) { if (!snapshot || !Array.isArray(snapshot.rooms) || !snapshot.rooms.length) return false; const elapsedSeconds = RoomGridLiveCatalogCache.isFresh(snapshot) ? Math.max(0, Math.round((Date.now() - Number(snapshot.savedAt || Date.now())) / 1000)) : 0; const models = snapshot.rooms.map(item => discoverUnpackCachedRoom(item, elapsedSeconds)).filter(Boolean); if (!models.length) return false; // A late full-cache restore enriches the same catalog in the background. // Keep the hovered card alive until renderBackgroundView can replace it. if (!previewScheduler.isHovering()) previewScheduler.clear(); invalidateViewCache(); catalog.length = 0; catalogById.clear(); for (const model of models) { catalog.push(model); catalogById.set(model.id, model); } catalogOffset = Math.max(models.length, Number(snapshot.offset) || 0); catalogTotal = Math.max(models.length, Number(snapshot.total) || 0); catalogTotalKnown = snapshot.totalKnown === true || !!snapshot.reachedEnd; catalogReachedEnd = !!snapshot.reachedEnd && catalogOffset >= catalogTotal; catalogFetchedAt = Number(snapshot.fetchedAt) || Number(snapshot.savedAt) || Date.now(); catalogCacheSavedAt = Number(snapshot.savedAt) || 0; catalogIsFallback = false; catalogUsingCache = true; catalogError = ''; return true; } async function restoreFullCatalogAndContinue(hotSnapshot) { let activeSnapshot = hotSnapshot; try { const fullSnapshot = await RoomGridLiveCatalogCache.loadFull(); const fullIsUseful = fullSnapshot && (!activeSnapshot || Number(fullSnapshot.savedAt) > Number(activeSnapshot.savedAt) || fullSnapshot.rooms.length > catalog.length); if (!destroyed && !catalogLoading && fullIsUseful && applyCatalogCacheSnapshot(fullSnapshot)) { activeSnapshot = fullSnapshot; if (state.view !== 'single') renderBackgroundView(); } } catch (_) {} if (destroyed || state.view === 'single') return; if (!activeSnapshot || !RoomGridLiveCatalogCache.isFresh(activeSnapshot)) refreshCompleteCatalog({ background: !!catalog.length, automatic: true }); else { continueCatalogPrefetch(); scheduleCatalogRefresh(); } } function fallbackToLibrary() { catalogIsFallback = true; catalogUsingCache = false; invalidateViewCache(); catalog.length = 0; catalogById.clear(); for (const room of store.state.rooms) { const model = localModel(room.id); catalog.push(model); catalogById.set(model.id, model); } } function catalogHasMore() { if (catalogReachedEnd) return false; return !catalogTotalKnown || !catalogTotal || catalogOffset < catalogTotal; } function continueCatalogPrefetch(event) { if (destroyed || state.view === 'single' || catalogLoading || catalogPrefetching || !catalogHasMore()) return; const explicit = event?.type === 'click'; if (watchOnlyMode && !explicit) return; clearTimeout(catalogPrefetchRetryTimer); catalogPrefetchRetryTimer = 0; const token = catalogPrefetchToken; setTimeout(() => prefetchCompleteCatalog(token, null, explicit), 0); } function scheduleCatalogPrefetchRetry(token) { if (destroyed || watchOnlyMode || token !== catalogPrefetchToken || !catalogHasMore() || catalogPrefetchRetryTimer) return; catalogPrefetchRetryAttempt = Math.min(6, catalogPrefetchRetryAttempt + 1); const delay = Math.min(30000, 1800 * (2 ** (catalogPrefetchRetryAttempt - 1))); catalogPrefetchRetryTimer = setTimeout(() => { catalogPrefetchRetryTimer = 0; if (!destroyed && token === catalogPrefetchToken && state.view !== 'single' && catalogHasMore()) prefetchCompleteCatalog(token); }, delay); } async function waitForCatalogRate(signal) { // A 429 applies to this catalog session, not just one translated host. // Never cycle through alternate hosts to bypass the server's cooldown. while (Date.now() < Math.max(catalogRateLimitUntil, catalogNextRequestAt)) { if (signal?.aborted) throw new DOMException('Katalogabruf abgebrochen', 'AbortError'); await new Promise(resolve => setTimeout(resolve, Math.min(250, Math.max(catalogRateLimitUntil, catalogNextRequestAt) - Date.now()))); } if (signal?.aborted) throw new DOMException('Katalogabruf abgebrochen', 'AbortError'); } async function requestCatalogJson(url, signal) { await waitForCatalogRate(signal); const result = await roomGridRequestJson(url, { signal, timeoutMs: 12000 }); catalogNextRequestAt = Date.now() + catalogAdaptiveDelayMs; return result; } function scheduleCatalogRefresh() { clearTimeout(catalogRefreshTimer); if (destroyed || watchOnlyMode || !catalog.length || state.view === 'single' || catalogLoading || catalogPrefetching) return; const delay = Math.max(1000, catalogFetchedAt + DISCOVER_CATALOG_CACHE_FRESH_MS - Date.now(), catalogRefreshRetryAt - Date.now(), catalogRateLimitUntil - Date.now()); catalogRefreshTimer = setTimeout(() => { catalogRefreshTimer = 0; if (destroyed || state.view === 'single') return; if (document.hidden) { catalogRefreshRetryAt = Date.now() + 30000; scheduleCatalogRefresh(); return; } refreshCompleteCatalog({ background: true, automatic: true }); }, delay); } async function refreshCompleteCatalog(options = {}) { // One shared background pass, not a new download owned by each menu. if (destroyed || catalogLoading || catalogPrefetching) return; if (watchOnlyMode && options.automatic && catalog.length) return; clearTimeout(catalogRefreshTimer); const background = options.background === true && catalog.length > 0; const progressTask = notificationCenter.beginProgress?.(tx('正在加载完整直播目录', 'Loading the complete live catalog', 'Vollständiger Live-Katalog wird geladen'), { delayMs: 320, background }); try { const loaded = await loadCatalog(true, { progressTask, quiet: background }); if (loaded && catalogHasMore()) await prefetchCompleteCatalog(catalogPrefetchToken, progressTask, !options.automatic); if (!destroyed && state.view !== 'single') renderBackgroundView(); } finally { catalogRefreshRetryAt = catalogError ? Date.now() + 30000 : 0; scheduleCatalogRefresh(); progressTask?.finish(); } } async function prefetchCompleteCatalog(token, sharedProgressTask = null, explicit = false) { if (catalogPrefetching || token !== catalogPrefetchToken || !catalogHasMore()) return; const canContinue = () => explicit || (!watchOnlyMode && state.view !== 'single'); if (!canContinue()) return; const progressTask = sharedProgressTask || notificationCenter.beginProgress?.(tx('直播目录正在补全', 'Completing the live catalog', 'Live-Katalog wird vervollständigt'), { delayMs: 420, background: true }); catalogPrefetching = true; if (!destroyed && state.view !== 'single') renderBackgroundView(); let batchesSinceRender = 0; let consecutiveFailures = 0; try { while (!destroyed && canContinue() && token === catalogPrefetchToken && catalogHasMore()) { const before = catalogOffset; const loaded = await loadCatalog(false, { quiet: true, progressTask }); if (!loaded || catalogOffset <= before) { consecutiveFailures += 1; if (consecutiveFailures >= 4) break; await new Promise(resolve => setTimeout(resolve, 1200 * consecutiveFailures)); continue; } consecutiveFailures = 0; catalogPrefetchRetryAttempt = 0; batchesSinceRender += 1; progressTask?.update({ label: tx('直播目录正在补全', 'Completing the live catalog', 'Live-Katalog wird vervollständigt'), done: catalogOffset, total: catalogTotalKnown ? catalogTotal : null, background: !sharedProgressTask }); if (batchesSinceRender >= 10) { batchesSinceRender = 0; if (state.view !== 'single') renderBackgroundView(); } // Sequential requests already wait for the server. Yield for input // without adding 31+ seconds of fixed foreground delay to 12k rooms. // Hidden tabs stay conservative; error/429 backoff is unchanged. await new Promise(resolve => setTimeout(resolve, document.hidden ? 700 : 0)); } } finally { if (token === catalogPrefetchToken) { catalogPrefetching = false; if (!destroyed && catalogFetchedAt) await updateCatalogObservations(false); if (!destroyed && state.view !== 'single') renderBackgroundView(); if (!destroyed && state.view !== 'single' && consecutiveFailures >= 4 && catalogHasMore()) scheduleCatalogPrefetchRetry(token); } if (!sharedProgressTask) progressTask?.finish(); } } async function loadCatalog(reset = true, options = {}) { if (catalogLoading) return false; const quiet = !!options.quiet; if (reset) { const preserveExisting = options.preserveExisting !== false && catalog.length > 0 && !catalogIsFallback; catalogPrefetchToken += 1; catalogPrefetching = false; clearTimeout(catalogPrefetchRetryTimer); catalogPrefetchRetryTimer = 0; catalogPrefetchRetryAttempt = 0; previousCatalogVisitAt = state.lastCatalogVisitAt || 0; try { catalogAbort?.abort(); } catch (_) {} catalogOffset = 0; catalogTotal = 0; catalogTotalKnown = false; catalogReachedEnd = false; catalogIsFallback = false; catalogRefreshSeenIds = preserveExisting ? new Set() : null; if (!preserveExisting) { previewScheduler.clear(); invalidateViewCache(); catalogUsingCache = false; catalog.length = 0; catalogById.clear(); } } catalogLoading = true; refreshBtn.disabled = true; catalogError = ''; const controller = new AbortController(); catalogAbort = controller; let timedOut = false; const timeout = setTimeout(() => { timedOut = true; try { controller.abort(); } catch (_) {} }, 65000); if (!quiet && state.view !== 'single') renderCurrentView(); try { // Pick the healthy/preferred host after cooldown, not before waiting; // otherwise a transiently blocked host is unnecessarily skipped. await waitForCatalogRate(controller.signal); let data = null; let lastError = null; const availableHosts = [...new Set([ location.hostname, 'chaturbate.com', 'de.chaturbate.com', 'en.chaturbate.com', ])]; const orderedHosts = catalogPreferredHost && availableHosts.includes(catalogPreferredHost) ? [catalogPreferredHost, ...availableHosts.filter(hostname => hostname !== catalogPreferredHost)] : availableHosts; const now = Date.now(); const readyHosts = orderedHosts.filter(hostname => Number(catalogHostCooldownUntil.get(hostname) || 0) <= now); const hosts = (readyHosts.length ? readyHosts : orderedHosts.slice().sort((left, right) => Number(catalogHostCooldownUntil.get(left) || 0) - Number(catalogHostCooldownUntil.get(right) || 0)) ).slice(0, 2); for (const hostname of hosts) { if (controller.signal.aborted) break; try { const url = new URL('/api/ts/roomlist/room-list/', `https://${hostname}`); url.searchParams.set('offset', String(catalogOffset)); url.searchParams.set('limit', String(DISCOVER_BATCH_SIZE)); const candidate = discoverCatalogPayload(await requestCatalogJson(url.toString(), controller.signal)); if (!candidate) throw new Error(`Ungültige Katalogantwort (${hostname})`); data = candidate; catalogPreferredHost = hostname; catalogHostCooldownUntil.delete(hostname); } catch (error) { lastError = error; if (controller.signal.aborted) throw error; // Fail over immediately. A second request to the same throttled or // unhealthy host only delays the usable catalogue; the preferred // host is updated as soon as another endpoint succeeds. const throttled = /HTTP\s+429/u.test(String(error?.message || '')); catalogHostCooldownUntil.set(hostname, Date.now() + (throttled ? 12000 : 2500)); if (throttled) { catalogRateLimitUntil = Date.now() + Math.max(12000, Number(error.retryAfterMs) || 0); catalogAdaptiveDelayMs = Math.min(3000, Math.max(400, catalogAdaptiveDelayMs * 2)); options.progressTask?.update({ label: tx('服务器要求暂停;已加载的房间仍可使用', 'Server requests a pause; loaded rooms remain usable', 'Server fordert eine Pause; geladene Räume bleiben nutzbar'), done: catalogOffset, total: catalogTotalKnown ? catalogTotal : null, background: quiet }); break; } } if (controller.signal.aborted || data) break; } if (!data) throw lastError || new Error('Leere Katalogantwort'); catalogIsFallback = false; const normalized = data.rooms.slice(0, DISCOVER_BATCH_SIZE).map(discoverNormalizeRoom).filter(Boolean); invalidateViewCache(); for (const model of normalized) { catalogRefreshSeenIds?.add(model.id); const previous = catalogById.get(model.id); if (previous) Object.assign(previous, model); else { catalog.push(model); catalogById.set(model.id, model); } } const receivedCount = data.rooms.length; const reportedTotal = Math.round(Number(data.total_count) || Number(data.all_rooms_count) || 0); if (reportedTotal > 0) { // A public live catalogue changes while it is being read. Keep one bounded // target per pass so newly appearing rooms cannot move the finish line // forever; the next refresh starts a fresh snapshot immediately. catalogTotal = catalogTotalKnown && catalogTotal > 0 ? Math.min(catalogTotal, reportedTotal) : reportedTotal; catalogTotalKnown = true; } else { catalogTotal = Math.max(catalog.length, catalogTotal); } catalogOffset += receivedCount; catalogReachedEnd = !receivedCount || receivedCount < DISCOVER_BATCH_SIZE || (catalogTotalKnown && catalogTotal > 0 && catalogOffset >= catalogTotal); if (catalogReachedEnd) { catalogTotal = Math.max(catalog.length, reportedTotal || 0); catalogTotalKnown = true; } options.progressTask?.update({ label: quiet ? tx('直播目录 wird im Hintergrund ergänzt', 'Completing the live catalog in the background', 'Live-Katalog wird im Hintergrund ergänzt') : tx('正在加载直播目录', 'Loading the live catalog', 'Live-Katalog wird geladen'), done: catalogOffset, total: catalogTotalKnown ? catalogTotal : null, background: quiet }); catalogFetchedAt = Date.now(); if (catalogReachedEnd && catalogRefreshSeenIds) { for (let index = catalog.length - 1; index >= 0; index -= 1) { if (catalogRefreshSeenIds.has(catalog[index].id)) continue; catalogById.delete(catalog[index].id); catalog.splice(index, 1); } catalogRefreshSeenIds = null; catalogTotal = Math.max(catalog.length, reportedTotal || 0); catalogUsingCache = false; } if (catalogOffset <= DISCOVER_BATCH_SIZE || catalogReachedEnd || catalogOffset % 1000 === 0) scheduleCatalogCacheSave(catalogReachedEnd); if (!quiet || (catalogReachedEnd && !catalogPrefetching)) await updateCatalogObservations(reset); window.dispatchEvent(new CustomEvent('roomgrid:live-catalog-updated')); return true; } catch (error) { if (controller.signal.aborted && !timedOut) return false; catalogError = catalog.length ? tx('目录更新暂时不可用;继续显示缓存的直播房间。', 'Catalog refresh is temporarily unavailable; cached live rooms remain visible.', 'Die Katalog-Aktualisierung ist vorübergehend nicht verfügbar; die zwischengespeicherten Live-Räume bleiben sichtbar.') : tx('公开目录暂时不可用,显示本地资料库。', 'Public catalog unavailable; showing the local library.', 'Der öffentliche Katalog ist vorübergehend nicht verfügbar; die lokale Bibliothek wird angezeigt.'); if (!catalog.length) fallbackToLibrary(); try { console.warn('[RoomGrid] discover catalog failed', error); Diagnostics.report('discover-catalog', error, 'public catalog unavailable', 'warning'); } catch (_) {} return false; } finally { clearTimeout(timeout); if (catalogAbort === controller) { catalogLoading = false; catalogAbort = null; } // An in-flight catalogue refresh may finish after SingleCam opens. // Rebuilding that view aborts its room request and destroys its player. if (!destroyed) refreshBtn.disabled = catalogLoading; if (!destroyed && !quiet && state.view !== 'single') renderCurrentView(); } } function prepare() { const requestedView = new URLSearchParams(location.search).get('suite_view'); const requestedModel = normalizeUsername(new URLSearchParams(location.search).get('model')); if (['dashboard', 'discover', 'tags', 'stats', 'library', 'following', 'user', 'single'].includes(requestedView)) state.view = requestedView; if (state.view === 'single' && isLikelyUsername(requestedModel)) state.singleId = requestedModel; singleContextIds = state.singleContext.slice(); singleReturnView = state.singleReturnView; const hotCatalogSnapshot = RoomGridLiveCatalogCache.loadHot(); if (hotCatalogSnapshot) applyCatalogCacheSnapshot(hotCatalogSnapshot); renderCurrentView(); // The public catalog and all controls must be usable immediately. Large // local histories (30k+ models / 100k events) hydrate progressively and // enrich the already visible cards when finished instead of blocking the // complete Discover page before its first useful interaction. restoreFullCatalogAndContinue(hotCatalogSnapshot); const hydrateAnalytics = async () => { try { await analytics.init(store.state.settings.analyticsEnabled !== false); ensureLocalIndexes(); } catch (_) { statsById = new Map(); latestWatchedId = ''; } invalidateViewCache(); if (!destroyed && state.view === 'single') singleAnalyticsRefresh?.(); else if (!destroyed) renderBackgroundView(); }; try { if ('requestIdleCallback' in window) requestIdleCallback(() => hydrateAnalytics(), { timeout: 1200 }); else setTimeout(hydrateAnalytics, 0); } catch (_) { setTimeout(hydrateAnalytics, 0); } } const rerenderFromStore = debounce(() => { if (destroyed) return; applyTheme(); if (state.view !== 'single') renderBackgroundView(); }, 80); storeUnsubscribe = store.subscribe((_next, path) => { if (path === 'rooms' || path === 'groups' || path === 'all' || String(path).startsWith('room:')) { localMetadataRevision += 1; rerenderFromStore(); } if (path === 'all' || String(path).startsWith('settings')) { let changed = false; for (const [key, setting] of discoverPreviewFields) { if (store.state.settings[setting] !== lastPreviewSettings[setting]) { state[key] = store.state.settings[setting]; changed = true; } } if (changed) { persistDiscoverState(); previewScheduler.refresh(); } rerenderFromStore(); } }); const rememberScroll = debounce(() => { if (state.view !== 'single') state.scrollPositions[state.view] = Math.max(0, Math.round(window.scrollY || 0)); }, 160); const rememberCatalogBeforePageHide = () => saveCatalogCacheNow(true); window.addEventListener('scroll', rememberScroll, { passive: true }); window.addEventListener('pagehide', rememberCatalogBeforePageHide, { capture: true }); if (state.view === 'dashboard' && !state.tagsAll && !state.tagsAny && String(store.state.settings.normalHomeTagFilter || '').trim()) applyHomepageStudioSettingsToDashboard(store.state.settings, false); prepare(); return () => { destroyed = true; window.removeEventListener('camclient:watch-only', watchOnlyChanged); discoverCurrentCatalog = null; activeCardMoreClose?.(); clearTimeout(filterRenderTimer); clearTimeout(backgroundRenderTimer); clearTimeout(catalogCacheSaveTimer); clearTimeout(catalogRefreshTimer); clearTimeout(catalogPrefetchRetryTimer); saveCatalogCacheNow(true); catalogPrefetchToken += 1; catalogPrefetching = false; try { catalogAbort?.abort(); } catch (_) {} try { cleanupSingleSession(); } catch (_) {} try { RoomGridNativeHub.unmountEmbedded(); } catch (_) {} try { previewScheduler.destroy(); } catch (_) {} try { analytics.stop(); analytics.flush(); } catch (_) {} try { notificationCenter.destroy(); } catch (_) {} try { windowCoordinator.destroy(); } catch (_) {} try { ultimateCleanup?.(); } catch (_) {} try { audioSilencerCleanup?.(); } catch (_) {} try { headerResizeObserver?.disconnect(); document.documentElement.style.removeProperty('--discover-header-height'); } catch (_) {} try { storeUnsubscribe?.(); } catch (_) {} try { window.removeEventListener('scroll', rememberScroll); window.removeEventListener('pagehide', rememberCatalogBeforePageHide, true); commandPalette?.destroy(); persistDiscoverState(); } catch (_) {} try { document.body.classList.remove('roomgrid-singlecam-active', 'roomgrid-dashboard-active', 'discover-cinema'); } catch (_) {} stopAllPageMedia(); }; } /* ============================================================= * 8. 工作台 / Workstation * ============================================================= */ function initWorkstation() { document.title = t('title'); // 在当前页打开工作台时,先停止原页面自带的 video/audio,避免页面清空后仍有声音。 stopAllPageMedia(); document.body.replaceChildren(); const store = createStore(); const startupParams = new URLSearchParams(location.search); const requestedSuiteWorkspace = String(startupParams.get('suite_workspace') || '').slice(0, 64); if (requestedSuiteWorkspace) store.activateWorkspace(requestedSuiteWorkspace); const requestedWorkspace = String(startupParams.get('workspace') || ''); if (requestedWorkspace && store.state.groups.some(group => group.id === requestedWorkspace)) store.setActiveGroup(requestedWorkspace); const requestedWindowName = String(startupParams.get('window_name') || '').trim().slice(0, 60); if (requestedWindowName) store.patchSettings({ windowName: requestedWindowName }); const service = createRoomService(store); const analytics = createAnalytics(); const notificationCenter = createNotificationCenter(store); const windowCoordinator = createWindowCoordinator(store); PrivacyShield.init(store); notificationCenter.start(); windowCoordinator.start(); Notify.init(); installSuiteExperienceStyles(); applySuiteAppearance(store); const ultimateCleanup = RoomGridUltimate.init(store, analytics); let commandPalette = null; // 离开工作台页时,统一停流,避免浏览器残留音轨。 window.addEventListener('pagehide', () => { try { grid.querySelectorAll('.resizer').forEach(el => el.roomgridResizeCleanup?.()); } catch (_) {} if (ROOMGRID_CAPTURE_TOOLS_ENABLED) { try { stopAllRecordings({ final: true, silent: true }); } catch (_) {} } try { service.stopAll(); } catch (_) { stopAllPageMedia(); } try { disposeAudioMeters(); } catch (_) {} try { analytics.stop(); analytics.flush(); } catch (_) {} try { notificationCenter.destroy(); } catch (_) {} try { windowCoordinator.destroy(); } catch (_) {} try { commandPalette?.destroy(); } catch (_) {} try { ultimateCleanup?.(); } catch (_) {} try { replacementTimers.forEach(timer => clearTimeout(timer)); replacementTimers.clear(); } catch (_) {} }); window.addEventListener('beforeunload', (e) => { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED || !store.state.settings.recordingExitWarn || !recordings.size) return; const msg = t('recordingExitWarnMessage'); e.preventDefault(); e.returnValue = msg; return msg; }); // 全局样式 document.head.appendChild($('style', { html: trustedHtml(` :root { color-scheme: light; /* —— 更像标准 SaaS 产品的浅色设计系统:更清晰层级、更统一圆角、更轻的高光与阴影 —— */ --bg: #f4f7fb; --bg-elevated: rgba(255,255,255,.88); --bg-card: #ffffff; --bg-input: #ffffff; --bg-hover: rgba(15,23,42,.05); --bg-overlay: rgba(15,23,42,.18); --border: #e6ebf2; --border-strong: #d3dce8; --text: #0f172a; --text-secondary: #334155; --text-muted: #64748b; --accent: #2563eb; --accent-hover: #1d4ed8; --accent-soft: rgba(37,99,235,.10); --info: #2563eb; --info-soft: rgba(37,99,235,.10); --success: #16a34a; --danger: #dc2626; --warning: #d97706; --radius-sm: 10px; --radius-md: 14px; --radius-lg: 18px; --shadow-sm: 0 1px 2px rgba(15,23,42,.04), 0 4px 14px rgba(15,23,42,.04); --shadow-md: 0 8px 24px rgba(15,23,42,.08); --shadow-lg: 0 18px 48px rgba(15,23,42,.12); } * { box-sizing: border-box; } body { margin:0; height:100vh; display:flex; flex-direction:column; background:radial-gradient(circle at top left, rgba(37,99,235,.06), transparent 24%), linear-gradient(180deg, #f8fbff 0%, var(--bg) 100%); color:var(--text); font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; overflow:hidden; } button { font-family:inherit; } ::-webkit-scrollbar { width:10px; height:10px; } ::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:999px; border:2px solid transparent; background-clip:padding-box; } ::-webkit-scrollbar-thumb:hover { background:#c1cada; background-clip:padding-box; } ::-webkit-scrollbar-track { background:transparent; } video:focus { outline:none; } .cam-card { position:relative; background:#f8fafc; border-radius:var(--radius-md); overflow:hidden; border:1px solid rgba(15,23,42,.08); box-shadow:var(--shadow-sm); transition:border-color .2s, box-shadow .2s, transform .15s; display:block; min-height:0; min-width:0; width:100%; height:100%; isolation:isolate; contain:layout paint; } .grid.view-grid { align-content:start; align-items:stretch; } .grid.view-grid .cam-card { aspect-ratio:auto; } .cam-card.dragging { opacity:.4; transform:scale(.96); } .cam-card .cam-remove-view { position:absolute; top:8px; right:8px; z-index:30; width:26px; height:26px; padding:0; border:1px solid rgba(255,255,255,.35); border-radius:50%; background:rgba(0,0,0,.65); color:#fff; font:20px/1 sans-serif; cursor:pointer; } .cam-card .cam-remove-view:hover, .cam-card .cam-remove-view:focus-visible { background:#b91c1c; outline:2px solid #fff; outline-offset:1px; } .cam-card .pill { margin-right:32px; } .cam-card.drop-before { box-shadow: inset 3px 0 0 0 var(--accent), inset 0 3px 0 0 var(--accent); } .cam-card.drop-after { box-shadow: inset -3px 0 0 0 var(--accent), inset 0 -3px 0 0 var(--accent); } .focus-slot-target { position:relative; } .focus-drop-available { outline:2px dashed rgba(37,99,235,.68) !important; outline-offset:-2px; } .focus-drop-target { outline:3px solid var(--accent) !important; outline-offset:-3px; box-shadow:inset 0 0 0 3px rgba(37,99,235,.55) !important; } .grid.view-focus .focus-slot-badge { position:absolute; top:8px; left:8px; z-index:8; display:inline-flex; align-items:center; gap:6px; max-width:calc(100% - 16px); padding:4px 8px; border:1px solid rgba(255,255,255,.72); border-radius:999px; background:rgba(15,23,42,.78); color:#fff; box-shadow:0 6px 16px rgba(15,23,42,.20); font-size:10px; line-height:1.2; font-weight:750; pointer-events:none; backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); } .focus-slot-badge-label, .focus-slot-badge-drop { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .focus-slot-badge-drop { display:none; color:#bfdbfe; } .focus-drop-available .focus-slot-badge-drop { display:inline; } .focus-drop-target .focus-slot-badge { background:var(--accent); border-color:rgba(255,255,255,.92); } .focus-drop-target .focus-slot-badge-drop { color:#fff; } .focus-empty-slot { gap:5px; text-align:center; } .focus-empty-slot-label { font-weight:750; color:var(--text-secondary); } .focus-empty-slot-drop { display:none; color:var(--accent); font-size:11px; font-weight:750; } .focus-drop-available .focus-empty-slot-drop { display:block; } .focus-drop-target .focus-empty-slot-label { color:var(--accent); } .group-user-item { user-select:none; -webkit-user-drag:element; } .group-user-item:hover { background:var(--bg-hover) !important; } .group-user-item.is-drag-source { background:var(--accent-soft) !important; outline:1px solid var(--accent); opacity:.65; } .group-user-drag-grip { flex:0 0 12px; opacity:.45; display:inline-flex; pointer-events:none; } .drag-handle svg, .drag-handle svg * { pointer-events:none; } .group-user-item:hover .group-user-drag-grip { opacity:1; } .room-drag-ghost { position:fixed; top:-200px; left:-1000px; width:240px; padding:12px 16px; border:1px solid var(--accent); border-radius:12px; background:var(--bg-card); color:var(--text); font-size:13px; font-weight:750; box-shadow:var(--shadow-md); pointer-events:none; } .room-drop-hint { position:absolute; z-index:40; top:50%; left:50%; transform:translate(-50%,-50%); max-width:calc(100% - 24px); padding:9px 13px; border:1px solid var(--accent); border-radius:10px; background:var(--bg-card); color:var(--text); font-size:12px; font-weight:700; line-height:1.5; white-space:pre-line; text-align:center; pointer-events:none; opacity:0; } .focus-drop-available .room-drop-hint { opacity:.9; } .focus-drop-target .room-drop-hint { opacity:1; background:var(--accent); color:#fff; } .grid-empty-slot { position:relative; } .grid .cam-card.focus-drop-available, .grid .focus-empty-slot.focus-drop-available, .grid .grid-empty-slot.focus-drop-available { outline:2px dashed var(--accent) !important; outline-offset:-2px; } .grid .cam-card.focus-drop-target, .grid .focus-empty-slot.focus-drop-target, .grid .grid-empty-slot.focus-drop-target { outline:3px solid var(--accent) !important; outline-offset:-3px; box-shadow:inset 0 0 0 3px var(--accent) !important; } body.focus-drag-active .focus-slot-badge, body.focus-drag-active .focus-empty-slot-drop { display:none !important; } .room-drag-ghost { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .room-drop-hint { overflow-wrap:anywhere; max-height:calc(100% - 12px); overflow:hidden; } .room-drop-hint strong { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } /* 避免使用 .overlay 通用类名:CB 站点样式会把它拉伸成整卡黑色遮罩。 */ .cam-card .mc-hover-ui { opacity:0; transition:opacity .14s ease, transform .14s ease; pointer-events:none; transform:translateY(-2px); background:transparent !important; width:auto !important; height:auto !important; filter:none !important; mix-blend-mode:normal !important; } .cam-card:hover .mc-hover-ui, .cam-card:not(.compact) .mc-hover-ui { opacity:1; transform:translateY(0); } .cam-card .mc-hover-ui.drag-handle { position:absolute !important; top:6px !important; left:50% !important; right:auto !important; bottom:auto !important; transform:translateX(-50%) !important; } .cam-card .mc-hover-ui.ops-row { position:absolute !important; top:8px !important; right:8px !important; left:auto !important; bottom:auto !important; display:flex !important; } .cam-card .mc-hover-ui button, .cam-card .mc-hover-ui[draggable="true"] { pointer-events:auto; } .cam-card:hover .cam-video, .cam-card .cam-video:hover { filter:none !important; opacity:1 !important; } .cam-card::before, .cam-card::after { pointer-events:none; background:transparent !important; } .cam-video { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; background:transparent; z-index:1; pointer-events:none; } .cam-audio-meter { position:absolute; right:6px; bottom:6px; z-index:14; width:86px; min-height:22px; display:grid; grid-template-columns:18px minmax(24px,1fr) 25px; align-items:center; gap:3px; padding:3px 5px; box-sizing:border-box; border:1px solid rgba(255,255,255,.42); border-radius:999px; background:rgba(15,23,42,.68); color:#86efac; box-shadow:0 5px 14px rgba(15,23,42,.18); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); pointer-events:none; opacity:.78; transition:opacity .18s, color .18s, background .18s; } .cam-audio-mode, .cam-audio-value { font-size:7px; line-height:1; font-weight:850; letter-spacing:.01em; color:currentColor; white-space:nowrap; } .cam-audio-value { text-align:right; font-variant-numeric:tabular-nums; } .cam-audio-track { position:relative; display:block; height:5px; overflow:hidden; border-radius:999px; background:rgba(255,255,255,.20); } .cam-audio-fill { position:absolute; inset:0 auto 0 0; width:0%; border-radius:inherit; background:currentColor; transition:width .10s linear; } .cam-audio-meter.is-active { opacity:1; color:#4ade80; background:rgba(15,23,42,.82); } .cam-audio-meter.is-still { color:#facc15; opacity:.82; } .cam-audio-meter.is-output { color:#67e8f9; opacity:.9; } .cam-audio-meter.is-paused { color:#93c5fd; opacity:.68; } .cam-audio-meter.is-waiting { color:#facc15; opacity:.78; } .cam-audio-meter.is-muted, .cam-audio-meter.is-unavailable { color:#94a3b8; opacity:.62; } .cam-card.audio-loudest { border-color:rgba(74,222,128,.92) !important; box-shadow:0 0 0 2px rgba(74,222,128,.2), var(--shadow-md) !important; } .cam-card.audio-meter-hidden .cam-audio-meter { display:none; } .cam-card.video-zoomed .cam-video { cursor:grab; } .cam-card.video-panning .cam-video { cursor:grabbing !important; } .cam-video::-webkit-media-controls, .cam-video::-webkit-media-controls-enclosure { display:none !important; opacity:0 !important; } .cam-card.not-online { background:linear-gradient(135deg, rgba(255,255,255,.92), rgba(241,245,249,.96)); } .status-layer { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; flex-direction:column; gap:6px; color:var(--text-muted); font-size:12px; text-align:center; padding:16px; pointer-events:none; background:transparent; z-index:4; } .status-layer .status-icon { font-size:24px; line-height:1; opacity:.9; } .status-layer .status-chip { font-weight:650; padding:3px 9px; border-radius:999px; background:rgba(17,24,39,.045); border:1px solid rgba(17,24,39,.06); } .cam-card.flash { animation: flash 1.5s ease 3; } @keyframes flash { 0%,100% { box-shadow:0 0 0 0 rgba(35,165,89,0); border-color:var(--border); } 50% { box-shadow:0 0 0 4px rgba(35,165,89,.55); border-color:var(--success); } } /* —— 卡片浮层控件:透明毛玻璃,避免遮挡画面 —— */ .cam-card video { -webkit-user-drag:none; user-drag:none; } .pill { display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:700; backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; background:rgba(255,255,255,.86); color:var(--text); border:1px solid rgba(255,255,255,.55); box-shadow:0 6px 16px rgba(15,23,42,.08); } .dot { width:7px; height:7px; border-radius:50%; display:inline-block; } .svg-icon { width:1em; height:1em; display:inline-block; vertical-align:-.16em; flex:0 0 auto; } .btn-label { white-space:nowrap; } .ctrl-btn, .seg button { display:inline-flex; align-items:center; justify-content:center; gap:7px; } .icon-btn .svg-icon { width:15px; height:15px; } .status-dot-large { width:12px; height:12px; border-radius:999px; display:inline-block; background:currentColor; box-shadow:0 0 0 6px currentColor; opacity:.18; } .icon-btn { background:rgba(255,255,255,.88); backdrop-filter:blur(10px) !important; -webkit-backdrop-filter:blur(10px) !important; border:1px solid rgba(255,255,255,.7); color:var(--text-secondary); width:29px; height:29px; border-radius:9px; cursor:pointer; box-shadow:0 4px 12px rgba(15,23,42,.08); display:flex; align-items:center; justify-content:center; font-size:12px; transition:background .15s, transform .15s, color .15s, border-color .15s, box-shadow .15s; } .icon-btn:hover { background:#fff; color:var(--accent); border-color:rgba(37,99,235,.2); transform:translateY(-1px); box-shadow:0 8px 18px rgba(15,23,42,.12); } .icon-btn.danger:hover { background:var(--danger); } .icon-btn.recording { background:var(--danger); color:#fff; animation: recordPulse 1s ease infinite; } .icon-btn.recording.waiting { background:var(--warning); color:#fff; animation:none; } .cam-card.recording { outline:2px solid var(--danger); outline-offset:2px; } .cam-card.recording-waiting { outline-color:var(--warning); } @keyframes recordPulse { 0%,100% { opacity:1; } 50% { opacity:.58; } } /* 拖拽手柄 */ .drag-handle { backdrop-filter:blur(8px) !important; -webkit-backdrop-filter:blur(8px) !important; border:1px solid rgba(255,255,255,.55); box-shadow:0 6px 16px rgba(15,23,42,.10); transition:background .15s, transform .15s, border-color .15s; } .drag-handle:hover { background:var(--accent) !important; color:#fff; border-color:transparent; transform:translateY(-1px); } /* —— 卡片响应式:根据宽度收起部分按钮 —— */ .cam-card.compact .ops-extra { display:none; } .cam-card.compact .pill .pill-text { display:none; } .cam-card.compact .name-label { font-size:11px; padding:3px 7px; max-width:55%; } .cam-card.tiny .ops-row { gap:3px; } .cam-card.tiny .icon-btn { width:22px; height:22px; font-size:10px; } .cam-card.tiny .drag-handle { padding:2px 10px; font-size:11px; } .cam-card.tiny .name-label { display:none; } .cam-card.tiny .pill { padding:2px 6px; font-size:10px; } .cam-card.tiny .cam-audio-meter { width:62px; grid-template-columns:minmax(22px,1fr) 25px; } .cam-card.tiny .cam-audio-mode { display:none; } /* —— 主屏(focus 模式)—— */ .grid.view-focus { display:grid; grid-template-areas:'main' 'bar' 'thumbs'; grid-template-rows:minmax(0,var(--focus-main-pct,68fr)) 10px minmax(108px,var(--focus-thumb-pct,32fr)); grid-template-columns:minmax(0,1fr); gap:12px; padding:18px; height:100%; overflow:hidden; } .grid.view-focus .focused-row { grid-area:main; min-height:0; min-width:0; display:grid; place-items:center; overflow:hidden; position:relative; z-index:1; } .grid.view-focus .focused-row .cam-card { max-width:100%; max-height:100%; flex:0 0 auto; min-height:0; } .grid.view-focus .focused-row .cam-card.is-focus-main { border-color:var(--accent); box-shadow:0 0 0 1px rgba(16,163,127,.25); } .grid.view-focus .resizer { grid-area:bar; height:8px; min-height:8px; cursor:ns-resize; background:transparent; position:relative; z-index:2; } .grid.view-focus .resizer::before { content:''; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:50px; height:3px; border-radius:2px; background:var(--border-strong); transition:background .15s, width .15s; } .grid.view-focus .resizer:hover::before, .grid.view-focus .resizer.dragging::before { background:var(--accent); width:80px; } .grid.view-focus .thumbs-row { grid-area:thumbs; min-height:0; min-width:0; display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--focus-thumb-min,180px),1fr)); grid-auto-flow:row; gap:12px; overflow:auto; padding:2px 2px 8px; align-content:start; align-items:start; position:relative; z-index:1; } .grid.view-focus .thumbs-row .cam-card { cursor:pointer; width:100%; height:auto; min-height:0; aspect-ratio:var(--focus-thumb-aspect,16/9); } .grid.view-focus .thumbs-row .cam-card:hover { border-color:var(--accent); } /* —— 控件 —— */ .group-tab { padding:10px 12px; border-radius:12px; cursor:pointer; font-size:13px; background:transparent; border:none; color:var(--text-secondary); text-align:left; transition:background .12s, color .12s, border-color .12s, box-shadow .12s; border:1px solid transparent; display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%; } .group-tab:hover { background:#fff; color:var(--text); border-color:var(--border); box-shadow:var(--shadow-sm); } .group-tab.active { background:linear-gradient(180deg, rgba(37,99,235,.10), rgba(37,99,235,.06)); color:var(--accent); border-color:rgba(37,99,235,.18); box-shadow:var(--shadow-sm); } .group-tab.drop-target { background:var(--accent-soft); outline:1.5px dashed var(--accent); } .ctrl-input { background:var(--bg-input); border:1px solid var(--border); color:var(--text); padding:8px 12px; min-height:38px; border-radius:12px; font-size:13px; outline:none; box-shadow:0 1px 0 rgba(255,255,255,.7) inset; transition:border-color .15s, background .15s, box-shadow .15s; } .ctrl-input:focus { border-color:rgba(37,99,235,.38); box-shadow:0 0 0 4px rgba(37,99,235,.10); } .ctrl-input:hover:not(:focus) { border-color:var(--border-strong); } .ctrl-btn { background:var(--bg-input); border:1px solid var(--border); color:var(--text); padding:8px 12px; min-height:38px; border-radius:12px; font-size:13px; cursor:pointer; box-shadow:0 1px 0 rgba(255,255,255,.7) inset; transition:background .15s, border-color .15s, transform .15s, box-shadow .15s; } .ctrl-btn:hover { background:#fff; border-color:var(--border-strong); transform:translateY(-1px); box-shadow:var(--shadow-sm); } .ctrl-btn.primary { background:linear-gradient(180deg, rgba(37,99,235,.12), rgba(37,99,235,.08)); border-color:rgba(37,99,235,.28); color:var(--accent); } .ctrl-btn.primary:hover { background:var(--accent); color:#fff; border-color:var(--accent); } .ctrl-btn.disabled, .ctrl-btn:disabled { opacity:.5; cursor:not-allowed; } .toggle { display:inline-flex; align-items:center; gap:6px; cursor:pointer; font-size:12px; color:var(--text-secondary); padding:7px 10px; border-radius:12px; user-select:none; border:1px solid transparent; transition:background .12s, border-color .12s; } .toggle:hover { background:#fff; color:var(--text); border-color:var(--border); } .toggle input { accent-color:var(--accent); } .empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text-muted); gap:16px; padding:40px; text-align:center; } .menu-pop { position:absolute; background:var(--bg-elevated); backdrop-filter:none; border:1px solid var(--border); border-radius:8px; padding:5px; min-width:180px; box-shadow:var(--shadow-lg); z-index:1000; display:flex; flex-direction:column; gap:1px; } .more-menu-pop { z-index:100030; } .menu-pop button { background:transparent; border:none; color:var(--text); padding:8px 12px; text-align:left; cursor:pointer; border-radius:5px; font-size:13px; } .menu-pop button:hover { background:var(--bg-hover); } .menu-pop button.danger { color:var(--danger); } .menu-pop button.danger:hover { background:rgba(242,63,67,.15); } .card-ops-menu-pop { z-index:100030;box-sizing:border-box; overscroll-behavior:contain; scrollbar-gutter:stable; } .card-ops-menu-header { position:sticky; top:-5px; z-index:6; display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:8px; margin:-5px -5px 5px; padding:9px 10px; min-height:48px; box-sizing:border-box; color:var(--text); background:var(--bg-elevated); border-bottom:1px solid var(--border); box-shadow:0 5px 12px rgba(15,23,42,.08); } .card-ops-menu-title { min-width:0; font-size:12px; font-weight:850; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .card-ops-menu-subtitle { margin-top:2px; color:var(--text-muted); font-size:9px; font-weight:650; } .menu-pop .card-ops-menu-close { width:28px; height:28px; padding:0; display:grid; place-items:center; text-align:center; border:1px solid var(--border); border-radius:8px; background:var(--bg-input); font-size:16px; line-height:1; } .card-quality-panel { display:grid; gap:6px; padding:9px 10px; border-top:1px solid var(--border); color:var(--text); font-size:11px; } .card-quality-status { color:var(--text-muted); font-size:9px; line-height:1.35; } .card-menu-category { margin:2px 0; border:1px solid var(--border); border-radius:9px; overflow:hidden; background:color-mix(in srgb,var(--bg) 74%,transparent); } .card-menu-category > summary { list-style:none; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; color:var(--text-secondary); font-size:11px; font-weight:850; user-select:none; } .card-menu-category > summary::-webkit-details-marker { display:none; } .card-menu-category > summary::after { content:'›'; color:var(--text-muted); font-size:15px; transition:transform .14s ease; } .card-menu-category[open] > summary::after { transform:rotate(90deg); } .card-menu-category-body { display:flex; flex-direction:column; gap:1px; padding:0 4px 4px; border-top:1px solid var(--border); } .multicam-center-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; } .multicam-center-card { min-width:0; padding:11px; border:1px solid var(--border); border-radius:12px; background:var(--bg); } .multicam-center-card > h3 { margin:0 0 5px; color:var(--text); font-size:13px; } .multicam-health-row { display:grid; grid-template-columns:minmax(0,1fr) 70px 70px; gap:7px; align-items:center; padding:6px 0; border-bottom:1px solid var(--border); font-size:11px; } .multicam-health-row:last-child { border-bottom:0; } [data-multicam-health-list] { max-height:240px; overflow:auto; overscroll-behavior:contain; } .multicam-health-score { justify-self:end; font-weight:850; }.multicam-health-score.good{color:var(--success)}.multicam-health-score.warning{color:var(--warning)}.multicam-health-score.critical{color:var(--danger)} .multicam-scene-row { display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:6px; align-items:center; padding:6px 0; border-bottom:1px solid var(--border); } @media (max-width:760px) { .multicam-center-grid { grid-template-columns:1fr; } .multicam-health-row { grid-template-columns:minmax(0,1fr) 62px; }.multicam-health-row > :last-child { display:none; } } .card-focus-picker { margin:4px 0 5px; padding:8px; border:1px solid var(--border); border-radius:10px; background:var(--bg); max-height:min(440px, 58vh); overflow:auto; scrollbar-width:thin; } .card-focus-picker-title { color:var(--text); font-size:12px; font-weight:800; margin-bottom:3px; } .card-focus-picker-hint { color:var(--text-muted); font-size:10px; line-height:1.35; margin-bottom:7px; } .card-focus-picker-group { border-top:1px solid var(--border); } .card-focus-picker-group:first-of-type { border-top:0; } .card-focus-picker-group-summary { list-style:none; cursor:pointer; padding:6px 2px; color:var(--text-secondary); font-size:11px; font-weight:750; user-select:none; } .card-focus-picker-group-summary::-webkit-details-marker { display:none; } .card-focus-picker-group-summary::before { content:'›'; display:inline-block; width:13px; color:var(--text-muted); transition:transform .14s ease; } .card-focus-picker-group[open] .card-focus-picker-group-summary::before { transform:rotate(90deg); } .card-focus-picker-list { display:flex; flex-direction:column; gap:2px; padding:0 0 5px 13px; } .menu-pop .card-focus-picker-item { display:flex; align-items:center; gap:7px; width:100%; padding:6px 7px; color:var(--text-secondary); border:1px solid transparent; border-radius:7px; font-size:11px; } .menu-pop .card-focus-picker-item:hover, .menu-pop .card-focus-picker-item:focus-visible { background:var(--bg-hover); border-color:var(--border); outline:none; } .menu-pop .card-focus-picker-item.is-current { background:rgba(37,99,235,.10); border-color:rgba(37,99,235,.28); color:var(--accent); } .menu-pop .card-focus-picker-item.is-in-view:not(.is-current) { background:rgba(16,185,129,.08); border-color:rgba(16,185,129,.25); } .card-focus-picker-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .card-focus-picker-meta { flex:0 0 auto; margin-left:auto; color:var(--text-muted); font-size:9px; white-space:nowrap; } .card-focus-picker-item.is-in-view .card-focus-picker-meta { display:grid; gap:3px; max-width:58%; white-space:normal; text-align:right; } .card-focus-picker-assignment { color:var(--success); font-weight:750; line-height:1.3; overflow-wrap:anywhere; } .is-current .card-focus-picker-assignment { color:var(--accent); } .card-focus-picker-empty { color:var(--text-muted); font-size:11px; padding:6px 2px; } /* —— 视图切换段 segmented control —— */ .seg { display:inline-flex; background:var(--bg-input); border:1px solid var(--border); border-radius:12px; padding:3px; box-shadow:0 1px 0 rgba(255,255,255,.7) inset; } .seg button { background:transparent; border:none; color:var(--text-secondary); padding:6px 12px; border-radius:9px; cursor:pointer; font-size:12px; font-weight:600; transition:background .15s, color .15s; } .seg button.active { background:var(--accent); color:#fff; box-shadow:0 6px 16px rgba(37,99,235,.20); } .seg button:hover:not(.active) { color:var(--text); } .toolbar-group { display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 10px; background:rgba(255,255,255,.76); border:1px solid var(--border); border-radius:14px; box-shadow:var(--shadow-sm); } .toolbar-group.compact { padding:6px 8px; } .toolbar-spacer { margin-left:auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; } .sidebar-brand { padding:8px 10px; border-radius:12px; background:var(--bg-card); border:1px solid var(--border); box-shadow:var(--shadow-sm); margin-bottom:8px; } .sidebar-brand .title { font-size:15px; font-weight:800; letter-spacing:.01em; color:var(--text); } .sidebar-brand .sub { display:none; } .sidebar-brand [data-sidebar-hover-preview] { margin-top:6px; } .sidebar-brand [data-sidebar-hover-preview][aria-pressed="true"] { background:var(--accent); border-color:var(--accent); color:var(--rg-accent-text,#fff); } .sidebar-section-title { font-size:11px; color:var(--text-muted); padding:6px 8px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; } .sidebar-browser { display:grid; gap:8px; padding:10px; margin-bottom:10px; border:1px solid var(--border); border-radius:12px; background:var(--bg-card); min-width:0; } .sidebar-filter-field { display:grid; gap:4px; min-width:0; font-size:11px; color:var(--text-secondary); } .sidebar-browser input,.sidebar-browser select { width:100%; min-width:0; box-sizing:border-box; padding:7px 6px; border:1px solid var(--border-strong); border-radius:7px; background:var(--bg-input); color:var(--text); font-size:11px; } .sidebar-browser small,.sidebar-following small { display:block; color:var(--text-muted); font-size:10px; line-height:1.4; overflow-wrap:anywhere; } .sidebar-filter-details { display:grid; } .sidebar-filter-details[open] { max-height:clamp(150px,25vh,260px); overflow-y:auto; padding-right:4px; overscroll-behavior:contain; } .sidebar-filter-details summary { cursor:pointer; color:var(--accent); font-weight:700; font-size:11px; padding:5px 0; } .sidebar-filter-details[open] summary { position:sticky; top:0; background:var(--bg-card); z-index:1; } .sidebar-filter-details .sidebar-filter-field { margin-top:7px; } .sidebar-filter-pair { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:6px; } .sidebar-lists { display:flex; flex-direction:column; min-width:0; } .sidebar-following { border:1px solid var(--border); border-radius:12px; padding:8px; margin-bottom:10px; background:var(--bg-card); } .sidebar-following summary { cursor:pointer; font-size:12px; font-weight:800; padding:4px 0 8px; } .sidebar-following summary .group-count { float:right; } .sidebar-following .btn { margin:6px 0; } .sidebar-browser button,.sidebar-following button.btn { border:1px solid var(--border-strong); border-radius:7px; padding:6px 8px; color:var(--text); background:var(--bg-input); cursor:pointer; font-size:11px; } .sidebar-browser button:hover,.sidebar-following button.btn:hover { border-color:var(--accent); color:var(--accent); background:var(--bg-hover); } .sidebar-following button:disabled { opacity:.5; cursor:default; } .group-user-status-dot { font-size:8px; flex:0 0 auto; opacity:.8; } .sidebar-list-pagination { display:flex; align-items:center; justify-content:space-between; gap:4px; font-size:10px; padding:4px; } .sidebar-list-pagination button { padding:5px 9px; border:1px solid var(--border); border-radius:6px; background:var(--bg-input); color:var(--text); cursor:pointer; } .sidebar-list-pagination button:disabled { opacity:.4; cursor:default; } .sidebar-empty { color:var(--text-muted); padding:4px; font-size:10px; } .group-user-item { transition:background .14s ease, color .14s ease, transform .14s ease; } .group-user-item:hover, .group-user-item:focus-visible { background:var(--bg-hover) !important; color:var(--text) !important; transform:translateX(2px); outline:none; } .group-user-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .group-user-live-indicator { flex:0 0 auto; color:#16a34a; font-size:12px; line-height:1; text-shadow:0 0 0 rgba(22,163,74,0); animation:rg-live-pulse 1.35s ease-in-out infinite; } @keyframes rg-live-pulse { 0%, 100% { opacity:.72; transform:scale(.9); text-shadow:0 0 0 rgba(22,163,74,0); } 50% { opacity:1; transform:scale(1.12); text-shadow:0 0 7px rgba(22,163,74,.56); } } .group-user-live-slots { flex:0 1 auto; min-width:0; max-width:122px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:2px 5px; border-radius:999px; color:var(--success); background:rgba(22,163,74,.10); border:1px solid rgba(22,163,74,.20); font-size:9px; line-height:1.2; font-weight:800; } .sidebar-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-top:auto; padding:10px 4px 2px; } .sidebar-stat { background:rgba(255,255,255,.84); border:1px solid var(--border); border-radius:14px; padding:10px 8px; box-shadow:var(--shadow-sm); } .sidebar-stat .k { font-size:10px; color:var(--text-muted); margin-bottom:4px; } .sidebar-stat .v { font-size:14px; font-weight:800; color:var(--text); } .mc-tooltip { position:fixed; z-index:1000002; max-width:min(360px, calc(100vw - 24px)); padding:6px 8px; border-radius:6px; background:rgba(17,24,39,.92); color:#fff; font-size:11px; line-height:1.35; pointer-events:none; box-shadow:var(--shadow-md); white-space:normal; transform:translateY(2px); opacity:0; transition:opacity .08s, transform .08s; } .mc-tooltip.show { opacity:1; transform:translateY(0); } .sidebar-preview { position:fixed; z-index:1000005; width:min(278px, calc(100vw - 16px)); padding:8px; border:1px solid var(--border); border-radius:14px; background:var(--bg-elevated); color:var(--text); box-shadow:0 18px 48px rgba(15,23,42,.24); pointer-events:none; opacity:0; transform:translateY(5px) scale(.97); transform-origin:top left; transition:opacity .16s ease, transform .16s ease; overflow:hidden; } .sidebar-preview.is-visible { opacity:1; transform:translateY(0) scale(1); } .sidebar-preview-head { display:flex; align-items:center; justify-content:space-between; gap:8px; min-width:0; padding:2px 3px 7px; font-size:12px; font-weight:750; } .sidebar-preview-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .sidebar-preview-live { flex:0 0 auto; color:var(--success); font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; } .sidebar-preview-media { position:relative; aspect-ratio:16 / 9; min-height:120px; overflow:hidden; border-radius:10px; background:#05070b; } .sidebar-preview-image, .sidebar-preview-video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; } .sidebar-preview-image { z-index:0; background:#05070b; opacity:.84; transition:opacity .16s ease; } .sidebar-preview-video { z-index:1; background:transparent; } .sidebar-preview.is-playing .sidebar-preview-image { opacity:0; } .sidebar-preview.is-playing .sidebar-preview-video { background:#000; } .sidebar-preview-status { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; gap:7px; padding:12px; z-index:2; color:#e5e7eb; background:rgba(5,7,11,.42); font-size:11px; line-height:1.3; text-align:center; transition:opacity .16s ease; } .sidebar-preview-status.is-hidden { opacity:0; } .sidebar-preview-status:not(.is-loading) .sidebar-preview-spinner { display:none; } .sidebar-preview-spinner { width:13px; height:13px; flex:0 0 auto; border:2px solid rgba(255,255,255,.3); border-top-color:#fff; border-radius:50%; animation:sidebarPreviewSpin .75s linear infinite; } @keyframes sidebarPreviewSpin { to { transform:rotate(360deg); } } body.rg-dark-mode .sidebar-preview { background:#111827; border-color:#3b4a61; color:#f1f5f9; box-shadow:0 18px 48px rgba(0,0,0,.48); } body.rg-dark-mode .sidebar-preview-media { background:#05070b; } @media (prefers-reduced-motion: reduce) { .sidebar-preview, .sidebar-preview-status { transition:none !important; } .sidebar-preview-spinner { animation:none !important; } .group-user-live-indicator { animation:none !important; } } /* —— Window-first:把空间留给窗口,所有操作只在需要时出现 —— */ .app-shell { display:flex; height:100vh; gap:10px; padding:10px; } .suite-global-header{position:relative;z-index:100020;display:grid;flex:0 0 auto;gap:5px;padding:7px 10px 6px;border-bottom:1px solid var(--rg-border,var(--border));background:color-mix(in srgb,var(--rg-surface-strong,var(--bg-card)) 94%,transparent);color:var(--rg-text,var(--text));box-shadow:0 3px 14px rgba(15,23,42,.08);backdrop-filter:blur(18px)} .suite-global-top{display:flex;align-items:center;gap:10px;min-width:0}.suite-global-brand{display:flex;align-items:center;gap:9px;min-width:max-content}.suite-global-mark{display:grid;place-items:center;min-width:35px;min-height:35px;border-radius:11px;color:#fff;background:linear-gradient(135deg,var(--rg-accent,var(--accent)),var(--rg-secondary,#14b8a6));font-size:11px;font-weight:900;overflow:hidden}.suite-global-mark:has(img[data-roomgrid-brand-logo]){width:auto;height:auto;background:transparent!important}.suite-global-brand-copy strong,.suite-global-brand-copy span{display:block}.suite-global-brand-copy strong{font-size:13px}.suite-global-brand-copy span{color:var(--rg-muted,var(--text-muted));font-size:9px}.suite-global-actions{display:flex;align-items:center;gap:6px;margin-left:auto;min-width:max-content}.suite-global-actions .shell-controls{position:static!important;inset:auto!important;display:flex!important;align-items:center;gap:6px;z-index:auto;pointer-events:auto}.suite-global-actions .shell-controls button{min-height:31px;border-radius:8px;padding:5px 9px;box-shadow:none;white-space:nowrap}.suite-global-button,.suite-global-nav button{min-height:31px;padding:5px 9px;border:1px solid var(--rg-border,var(--border));border-radius:8px;background:var(--rg-input,var(--bg-input));color:var(--rg-text,var(--text));font:750 10px/1.2 inherit;cursor:pointer;white-space:nowrap}.suite-global-button:hover,.suite-global-nav button:hover{border-color:var(--rg-accent,var(--accent))}.suite-global-button.active,.suite-global-nav button.active{border-color:var(--rg-accent,var(--accent));background:var(--rg-accent,var(--accent));color:var(--rg-accent-text,#fff)}.suite-global-nav{display:flex;align-items:center;gap:4px;min-width:0;overflow-x:auto;overscroll-behavior-inline:contain;padding:3px;border:1px solid var(--rg-border,var(--border));border-radius:9px;background:var(--rg-surface,var(--bg));scrollbar-width:thin}.suite-global-nav button{flex:0 0 auto;border-color:transparent;background:transparent} .suite-global-header~.app-shell{height:auto!important;flex:1 1 auto;min-height:0!important}.suite-global-header~.app-shell main{height:auto!important;min-height:0!important}.suite-global-header .roomgrid-notification-surface[data-position="header"]{min-height:31px;max-width:720px}.suite-global-header .roomgrid-notification-empty{min-height:29px} .suite-meter-control{display:grid;gap:2px;flex:0 0 auto;color:var(--rg-muted,var(--text-muted));font-size:9px;font-weight:750}.suite-meter-control select{max-width:160px;min-width:110px}.suite-meter-control select:focus-visible{outline:2px solid var(--rg-accent,var(--accent));outline-offset:2px} @media(max-width:900px){.suite-global-top{flex-wrap:wrap}.suite-global-top .roomgrid-notification-surface[data-position="header"]{order:3;flex:1 0 100%;max-width:none}.suite-global-actions{overflow-x:auto;max-width:55vw}.suite-global-nav{width:100%}} @media(max-width:900px){.suite-global-actions{min-width:0;max-width:100%;width:100%;flex:1 0 100%}} .grid { background:linear-gradient(180deg, rgba(255,255,255,.34), rgba(255,255,255,.14)); } body.rg-video-cover .cam-video { object-fit:cover !important; } body.rg-video-contain .cam-video { object-fit:contain !important; } body.rg-viewer-mode { background:#050607; } body.rg-viewer-mode .sidebar, body.rg-viewer-mode header, body.rg-viewer-mode .top-accent { display:none !important; } body.rg-viewer-mode .app-shell { padding:0 !important; gap:0 !important; } body.rg-viewer-mode main { width:100vw !important; height:100vh !important; border:0 !important; border-radius:0 !important; box-shadow:none !important; background:#050607 !important; } body.rg-viewer-mode .grid { padding:8px !important; background:#050607 !important; } body.rg-viewer-mode .grid.view-grid { gap:8px !important; } .cam-card:not(.compact) .mc-hover-ui { opacity:0 !important; pointer-events:none !important; } .cam-card:hover .mc-hover-ui, .cam-card:focus-within .mc-hover-ui { opacity:1 !important; pointer-events:auto !important; } .cam-card .pill, .cam-card .name-label { opacity:0; pointer-events:none; transition:opacity .16s ease, transform .16s ease; } .cam-card:hover .pill, .cam-card:focus-within .pill, .cam-card:hover .name-label, .cam-card:focus-within .name-label { opacity:1; pointer-events:auto; } .cam-card.not-online .pill, .cam-card.recording .pill { opacity:1; } .cam-card.recording .name-label { opacity:1; } .cam-card .ops-row { top:auto !important; right:auto !important; bottom:10px !important; left:50% !important; transform:translateX(-50%) translateY(8px) !important; display:flex !important; align-items:center !important; gap:3px !important; padding:4px !important; border-radius:999px !important; background:rgba(15,23,42,.42) !important; border:1px solid rgba(255,255,255,.18) !important; backdrop-filter:blur(14px) !important; -webkit-backdrop-filter:blur(14px) !important; box-shadow:0 10px 30px rgba(0,0,0,.18) !important; } .cam-card:hover .ops-row, .cam-card:focus-within .ops-row { transform:translateX(-50%) translateY(0) !important; } .cam-card .ops-row .icon-btn { width:28px; height:28px; border-radius:999px; color:rgba(255,255,255,.92); background:transparent; border:0; box-shadow:none; } .cam-card .ops-row .icon-btn:hover { background:rgba(255,255,255,.16); color:#fff; transform:none; box-shadow:none; } .cam-card .ops-row .icon-btn.danger:hover { background:rgba(220,38,38,.86); color:#fff; } .cam-card.compact .ops-extra { display:flex; } .cam-card.tiny .ops-extra { display:none; } .cam-card.tiny .ops-row .icon-btn { width:24px; height:24px; } .cam-card .drag-handle { top:8px !important; left:8px !important; right:auto !important; bottom:auto !important; transform:translateY(-4px) !important; padding:5px 7px !important; border-radius:999px !important; background:rgba(15,23,42,.40) !important; color:rgba(255,255,255,.88) !important; border:1px solid rgba(255,255,255,.16) !important; box-shadow:none !important; } .cam-card:hover .drag-handle, .cam-card:focus-within .drag-handle { transform:translateY(0) !important; } .cam-card .name-label { top:8px !important; bottom:auto !important; left:42px !important; transform:translateY(-4px); } .cam-card:hover .name-label, .cam-card:focus-within .name-label { transform:translateY(0); } .cam-card .pill { top:8px !important; right:8px !important; left:auto !important; } .grid.view-focus { grid-template-areas:'main thumbs' !important; grid-template-columns:minmax(0,1fr) minmax(190px,var(--focus-rail-width,260px)) !important; grid-template-rows:minmax(0,1fr) !important; gap:10px !important; padding:10px !important; background:#050607 !important; } .grid.view-focus .focused-row { background:#050607; border-radius:12px; } .grid.view-focus .focused-row .cam-card { border-radius:12px; border-color:rgba(255,255,255,.10); box-shadow:none; background:#000; } .grid.view-focus .focused-row .cam-video { background:#000; } .grid.view-focus .resizer { display:none !important; } .grid.view-focus .thumbs-row { grid-template-columns:1fr !important; gap:8px !important; padding:0 2px 0 0 !important; background:transparent; } .grid.view-focus .thumbs-row .cam-card { border-radius:10px; border-color:rgba(255,255,255,.12); opacity:.82; transition:opacity .14s,border-color .14s,transform .14s; } .grid.view-focus .thumbs-row .cam-card:hover { opacity:1; transform:translateY(-1px); } body.rg-focus-thumbs-collapsed .grid.view-focus { grid-template-areas:'main' !important; grid-template-columns:minmax(0,1fr) !important; } body.rg-focus-thumbs-collapsed .grid.view-focus .thumbs-row { display:none !important; } @media (max-width: 980px) { .grid.view-focus { grid-template-areas:'main' 'thumbs' !important; grid-template-columns:minmax(0,1fr) !important; grid-template-rows:minmax(0,1fr) minmax(92px,24vh) !important; } .grid.view-focus .thumbs-row { grid-template-columns:repeat(auto-fill,minmax(var(--focus-thumb-min,150px),1fr)) !important; padding:0 0 2px !important; } } /* —— v13.2:窗口优先最终覆盖。卡片默认只看画面,主屏默认只看主画面。 —— */ .cam-card .ops-row .ops-extra, .cam-card .ops-row .icon-btn.danger { display:none !important; } .cam-card .ops-row .icon-btn.recording { display:flex !important; } .cam-card .ops-row { opacity:0; pointer-events:none; } .cam-card:hover .ops-row, .cam-card:focus-within .ops-row { opacity:1; pointer-events:auto; } .cam-card.is-online .pill, .cam-card.is-online .name-label { opacity:0 !important; pointer-events:none !important; } .cam-card.is-online:hover .pill, .cam-card.is-online:focus-within .pill, .cam-card.is-online:hover .name-label, .cam-card.is-online:focus-within .name-label { opacity:1 !important; } .grid.view-focus { display:block !important; position:relative !important; grid-template-areas:none !important; grid-template-columns:none !important; grid-template-rows:none !important; padding:0 !important; gap:0 !important; background:#000 !important; overflow:hidden !important; } .grid.view-focus .focused-row { position:absolute !important; inset:0 !important; grid-area:auto !important; min-width:0 !important; min-height:0 !important; display:grid !important; place-items:center !important; background:#000 !important; border-radius:0 !important; overflow:hidden !important; } .grid.view-focus .focused-row .cam-card { border:0 !important; border-radius:0 !important; box-shadow:none !important; background:#000 !important; } .grid.view-focus .focused-row .drag-handle { display:none !important; } .grid.view-focus .focused-row .ops-row { top:14px !important; right:14px !important; bottom:auto !important; left:auto !important; transform:translateY(-6px) !important; } .grid.view-focus .focused-row .cam-card:hover .ops-row, .grid.view-focus .focused-row .cam-card:focus-within .ops-row { transform:translateY(0) !important; } .grid.view-focus .focused-row .name-label { top:auto !important; bottom:14px !important; left:14px !important; } .grid.view-focus .resizer { display:none !important; } .grid.view-focus .thumbs-row { position:absolute !important; grid-area:auto !important; left:14px !important; right:14px !important; bottom:10px !important; height:var(--focus-filmstrip-height,118px) !important; display:flex !important; gap:10px !important; overflow-x:auto !important; overflow-y:hidden !important; padding:10px !important; z-index:12 !important; background:rgba(15,23,42,.56) !important; border:1px solid rgba(255,255,255,.16) !important; border-radius:16px !important; box-shadow:0 14px 40px rgba(0,0,0,.32) !important; backdrop-filter:blur(14px) !important; -webkit-backdrop-filter:blur(14px) !important; transform:translateY(calc(100% - 20px)) !important; opacity:.20 !important; transition:transform .18s ease, opacity .18s ease, background .18s ease !important; } .grid.view-focus .thumbs-row:hover, .grid.view-focus .thumbs-row:focus-within { transform:translateY(0) !important; opacity:1 !important; background:rgba(15,23,42,.70) !important; } .grid.view-focus .thumbs-row .cam-card { flex:0 0 var(--focus-thumb-min,150px) !important; width:auto !important; height:100% !important; border-radius:10px !important; border-color:rgba(255,255,255,.18) !important; box-shadow:none !important; opacity:.86; } .grid.view-focus .thumbs-row .cam-card:hover { opacity:1; transform:translateY(-1px); } .grid.view-focus .thumbs-row .pill, .grid.view-focus .thumbs-row .name-label, .grid.view-focus .thumbs-row .ops-row, .grid.view-focus .thumbs-row .drag-handle { display:none !important; } body.rg-focus-thumbs-collapsed .grid.view-focus .thumbs-row { display:none !important; } body.rg-focus-mode { background:#000; } body.rg-focus-mode .app-shell { padding:0 !important; gap:0 !important; } body.rg-focus-mode .sidebar { display:none !important; } body.rg-focus-mode main { width:100vw !important; height:100vh !important; border:0 !important; border-radius:0 !important; box-shadow:none !important; background:#000 !important; position:relative !important; } body.rg-focus-mode .top-accent { display:none !important; } body.rg-focus-mode header { position:absolute !important; left:12px !important; right:12px !important; top:10px !important; z-index:70 !important; transform:translateY(calc(-100% + 8px)) !important; opacity:.14 !important; transition:transform .18s ease, opacity .18s ease !important; } body.rg-focus-mode header:hover, body.rg-focus-mode header:focus-within { transform:translateY(0) !important; opacity:1 !important; } /* —— v14.0:重新按“多窗口视频优先”收敛。窗口是产品主体,控件只做临时 HUD。 —— */ body { background:#0b0e13 !important; } .app-shell { gap:8px !important; padding:8px !important; background:#0b0e13 !important; } main { background:#07090d !important; border-color:rgba(255,255,255,.08) !important; border-radius:12px !important; box-shadow:none !important; } .top-accent { display:none !important; } header { padding:8px 10px !important; gap:8px !important; background:rgba(255,255,255,.92) !important; border-bottom:1px solid rgba(15,23,42,.08) !important; } .toolbar-group { padding:5px 7px !important; gap:6px !important; border-radius:10px !important; box-shadow:none !important; background:#fff !important; } .ctrl-btn, .ctrl-input { min-height:32px !important; border-radius:9px !important; padding:5px 9px !important; font-size:12px !important; } .seg { border-radius:10px !important; padding:2px !important; } .seg button { padding:5px 9px !important; border-radius:8px !important; } .toggle { padding:5px 7px !important; border-radius:9px !important; } .toolbar-spacer { gap:6px !important; } .sidebar { background:#111827 !important; color:#e5e7eb !important; border-color:rgba(255,255,255,.08) !important; border-radius:12px !important; box-shadow:none !important; padding:10px 8px !important; width:var(--rg-sidebar-width,220px) !important; } .sidebar-brand { background:transparent !important; border:0 !important; box-shadow:none !important; margin:0 0 4px !important; padding:8px 10px !important; } .sidebar-brand .title { color:#f8fafc !important; font-size:14px !important; } .sidebar-section-title { color:#94a3b8 !important; } .group-tab { color:#cbd5e1 !important; padding:8px 10px !important; border-radius:10px !important; } .group-tab:hover { background:rgba(255,255,255,.06) !important; border-color:rgba(255,255,255,.08) !important; box-shadow:none !important; color:#fff !important; } .group-tab.active { background:rgba(37,99,235,.18) !important; border-color:rgba(96,165,250,.30) !important; color:#bfdbfe !important; box-shadow:none !important; } .sidebar-stat { background:rgba(255,255,255,.04) !important; border-color:rgba(255,255,255,.08) !important; box-shadow:none !important; } .sidebar-stat .k { color:#94a3b8 !important; } .sidebar-stat .v { color:#f8fafc !important; } .grid.view-grid { background:#07090d !important; padding:8px !important; gap:6px !important; } .grid.view-grid .cam-card { border-radius:6px !important; } .cam-card { background:#000 !important; border:1px solid rgba(255,255,255,.075) !important; box-shadow:none !important; } .cam-card:hover, .cam-card:focus-within { border-color:rgba(96,165,250,.55) !important; box-shadow:0 0 0 1px rgba(96,165,250,.10) !important; } .cam-card.is-online .status-layer { display:none !important; } .cam-card.is-online .status-layer[data-stream-startup]:not([data-stream-startup="ready"]), body.rg-pure-mode .cam-card .status-layer[data-stream-startup]:not([data-stream-startup="ready"]) { display:flex !important; } .cam-card .cam-video { background:#000 !important; } .cam-card .pill, .cam-card .name-label, .cam-card .drag-handle { opacity:0 !important; pointer-events:none !important; } .cam-card.not-online .pill, .cam-card.not-online .name-label, .cam-card.recording .name-label { opacity:1 !important; } .cam-card:hover .name-label, .cam-card:focus-within .name-label { opacity:1 !important; } .cam-card:hover .drag-handle, .cam-card:focus-within .drag-handle { opacity:.65 !important; pointer-events:auto !important; } .cam-card .name-label { left:8px !important; top:8px !important; bottom:auto !important; max-width:calc(100% - 52px) !important; background:rgba(0,0,0,.42) !important; color:#f8fafc !important; border-color:rgba(255,255,255,.10) !important; border-radius:6px !important; font-size:11px !important; padding:4px 7px !important; } .cam-card .pill { display:none !important; } .cam-card.not-online .pill { display:inline-flex !important; } .cam-card .drag-handle { display:none !important; } .cam-card:hover .drag-handle, .cam-card:focus-within .drag-handle { display:flex !important; } .cam-card .ops-row { top:8px !important; right:42px !important; left:auto !important; bottom:auto !important; transform:none !important; padding:0 !important; background:transparent !important; border:0 !important; box-shadow:none !important; backdrop-filter:none !important; -webkit-backdrop-filter:none !important; opacity:0 !important; } .cam-card .mc-hover-ui.ops-row { right:42px !important; } .cam-card:hover .ops-row, .cam-card:focus-within .ops-row { opacity:1 !important; transform:none !important; } .cam-card .ops-row .icon-btn { width:30px !important; height:30px !important; border-radius:8px !important; background:rgba(0,0,0,.48) !important; color:#f8fafc !important; border:1px solid rgba(255,255,255,.14) !important; box-shadow:none !important; } .cam-card .ops-row .icon-btn:hover { background:rgba(37,99,235,.86) !important; border-color:rgba(96,165,250,.50) !important; } .cam-card .ops-row .icon-btn:not(:last-child) { display:none !important; } .cam-card.recording::after { content:'REC'; position:absolute; left:8px; bottom:8px; z-index:28; padding:3px 6px; border-radius:5px; background:rgba(220,38,38,.92); color:#fff; font-size:10px; font-weight:800; letter-spacing:.04em; pointer-events:none; } .cam-card.recording-waiting::after { content:'REC PAUSED'; background:rgba(217,119,6,.94); } .status-layer { color:#cbd5e1 !important; background:linear-gradient(180deg,rgba(15,23,42,.10),rgba(15,23,42,.24)) !important; } .status-layer .status-chip { background:rgba(255,255,255,.06) !important; color:#e5e7eb !important; border-color:rgba(255,255,255,.10) !important; } body.rg-viewer-mode .app-shell { padding:0 !important; gap:0 !important; } body.rg-viewer-mode .sidebar, body.rg-viewer-mode header, body.rg-viewer-mode .top-accent { display:none !important; } body.rg-viewer-mode main { width:100vw !important; height:100vh !important; border:0 !important; border-radius:0 !important; } body.rg-viewer-mode .grid.view-grid { padding:4px !important; gap:4px !important; } body.rg-focus-mode { background:#000 !important; } body.rg-focus-mode .app-shell { padding:0 !important; gap:0 !important; } body.rg-focus-mode .sidebar, body.rg-focus-mode .top-accent { display:none !important; } body.rg-focus-mode main { width:100vw !important; height:100vh !important; border:0 !important; border-radius:0 !important; box-shadow:none !important; background:#000 !important; } body.rg-focus-mode header { transform:translateY(calc(-100% + 4px)) !important; opacity:.02 !important; pointer-events:auto !important; } body.rg-focus-mode header:hover, body.rg-focus-mode header:focus-within { transform:translateY(0) !important; opacity:1 !important; pointer-events:auto !important; } body.rg-focus-mode::before { content:''; position:fixed; left:0; right:0; top:0; height:14px; z-index:69; pointer-events:none; } body.rg-focus-mode:hover header { pointer-events:auto !important; } .grid.view-focus { padding:0 !important; background:#000 !important; } .grid.view-focus .focused-row .cam-card { border:0 !important; border-radius:0 !important; } .grid.view-focus .focused-row .name-label { top:auto !important; bottom:14px !important; left:14px !important; background:rgba(0,0,0,.34) !important; opacity:0 !important; } .grid.view-focus .focused-row .cam-card:hover .name-label, .grid.view-focus .focused-row .cam-card:focus-within .name-label { opacity:1 !important; } .grid.view-focus .focused-row .ops-row { top:14px !important; right:48px !important; opacity:0 !important; } .grid.view-focus .focused-row .cam-card:hover .ops-row, .grid.view-focus .focused-row .cam-card:focus-within .ops-row { opacity:1 !important; } .grid.view-focus .thumbs-row { transform:translateY(calc(100% + 6px)) !important; opacity:0 !important; bottom:8px !important; left:8px !important; right:8px !important; height:104px !important; } .grid.view-focus .thumbs-row:hover, .grid.view-focus .thumbs-row:focus-within { transform:translateY(0) !important; opacity:1 !important; } body:not(.rg-focus-thumbs-collapsed) .grid.view-focus::after { content:''; position:absolute; left:50%; bottom:7px; width:64px; height:4px; transform:translateX(-50%); border-radius:999px; background:rgba(255,255,255,.22); z-index:11; pointer-events:none; } body.rg-focus-thumbs-collapsed .grid.view-focus::after { display:none !important; } .grid.view-focus .thumbs-row .cam-card { border-radius:6px !important; } .grid.view-focus .thumbs-row .cam-card.is-focus-main { outline:2px solid rgba(96,165,250,.92) !important; outline-offset:-2px; } /* —— 纯净模式:隐藏工具栏、按钮和所有浮层,只保留画面 —— */ body.rg-pure-mode { background:#000; } body.rg-pure-mode .sidebar, body.rg-pure-mode header, body.rg-pure-mode .top-accent, body.rg-pure-mode .mc-hover-ui, body.rg-pure-mode .pill, body.rg-pure-mode .name-label, body.rg-pure-mode .status-layer, body.rg-pure-mode .resizer, body.rg-pure-mode .menu-pop, body.rg-pure-mode .mc-tooltip { display:none !important; } body.rg-pure-mode main { width:100vw; height:100vh; background:#000; } body.rg-pure-mode .grid { padding:0 !important; gap:0 !important; background:#000; } body.rg-pure-mode .grid.view-focus { padding:0 !important; gap:0 !important; grid-template-areas:'main'; grid-template-rows:minmax(0,1fr) !important; } body.rg-pure-mode .grid.view-focus .focused-row { grid-area:main; } body.rg-pure-mode .grid.view-focus .thumbs-row { display:none !important; } body.rg-pure-mode .cam-card { border:none !important; border-radius:0 !important; box-shadow:none !important; background:#000 !important; outline:none !important; } body.rg-pure-mode .cam-video { background:#000 !important; } body.rg-pure-mode.pure-cursor-hidden, body.rg-pure-mode.pure-cursor-hidden * { cursor:none !important; } .pure-exit-chip { display:none; position:fixed; right:10px; bottom:10px; z-index:1000001; border:1px solid rgba(255,255,255,.22); background:rgba(0,0,0,.22); color:rgba(255,255,255,.72); border-radius:999px; padding:5px 9px; font-size:11px; cursor:pointer; opacity:.10; transition:opacity .15s, background .15s; } body.rg-pure-mode .pure-exit-chip { display:block; } body.rg-pure-mode .pure-exit-chip:hover, body.rg-pure-mode .pure-exit-chip:focus { opacity:1; background:rgba(0,0,0,.55); } /* 顶部一抹橙色细线,作为品牌呼应 */ .top-accent { height:3px; background:linear-gradient(90deg, #60a5fa 0%, var(--accent) 35%, #22c55e 100%); flex-shrink:0; } /* v15 —— normal multiview product surface: light tone, collapsible shell, paged layouts */ body { background:#f3f1ea !important; } .app-shell { background:#f3f1ea !important; gap:8px !important; padding:8px !important; } main { background:#fbfaf6 !important; border-color:#dedbd2 !important; border-radius:14px !important; } .sidebar { background:#fbfaf6 !important; border-color:#dedbd2 !important; border-radius:14px !important; box-shadow:none !important; } header { background:#fbfaf6 !important; border-bottom-color:#dedbd2 !important; padding:10px 12px !important; gap:8px !important; } .top-accent { display:none !important; } .toolbar-group { background:#fffefb !important; border-color:#e3e0d7 !important; border-radius:10px !important; box-shadow:none !important; padding:6px 8px !important; } .toolbar-group-title { font-size:11px; line-height:1; font-weight:700; color:#7a7468; margin-right:2px; white-space:nowrap; } .ctrl-btn, .ctrl-input { border-radius:9px !important; min-height:34px !important; box-shadow:none !important; } .seg { border-radius:9px !important; box-shadow:none !important; } .seg button { border-radius:7px !important; } .layout-seg button { min-width:38px; } .page-indicator { min-width:74px; text-align:center; font-size:12px; color:var(--text-secondary); font-weight:700; } .shell-controls { position:fixed; left:10px; top:10px; z-index:1000001; display:flex; gap:6px; pointer-events:auto; } .shell-controls button { border:1px solid #dedbd2; background:rgba(255,254,251,.92); color:#334155; border-radius:9px; padding:6px 9px; min-height:30px; font-size:12px; cursor:pointer; box-shadow:0 6px 18px rgba(15,23,42,.08); } .shell-controls .escape-menu-btn { display:inline-flex !important; align-items:center; justify-content:center; font-weight:800; background:rgba(255,254,251,.96); } body.rg-dark-mode .shell-controls button, body.rg-pure-mode .shell-controls button, body.rg-viewer-mode .shell-controls button { background:rgba(15,23,42,.92); color:#f8fafc; border-color:rgba(255,255,255,.24); box-shadow:0 8px 24px rgba(0,0,0,.32); } body:not(.rg-toolbar-collapsed) .shell-controls .show-toolbar-btn { display:none; } body:not(.rg-sidebar-collapsed) .shell-controls .show-sidebar-btn { display:none; } body.rg-toolbar-collapsed header, body.rg-toolbar-collapsed .top-accent { display:none !important; } body.rg-sidebar-collapsed .sidebar { display:none !important; } body.rg-sidebar-collapsed .app-shell { grid-template-columns:1fr !important; } .grid { background:#ece9df !important; padding:10px !important; } .grid.view-grid { height:100%; overflow:hidden !important; align-content:stretch !important; align-items:stretch !important; } .grid.view-grid .cam-card { border-radius:8px !important; border-color:#d4d0c6 !important; background:#f8f6ee !important; box-shadow:none !important; } .cam-card { background:#f8f6ee !important; } .cam-video { background:#f8f6ee !important; } .status-layer { background:#f8f6ee !important; } .cam-card .name-label, .cam-card .pill { background:rgba(255,254,251,.88) !important; color:#334155 !important; border-color:#e3e0d7 !important; box-shadow:none !important; } .cam-card .ops-row { bottom:8px !important; } .icon-btn { background:rgba(255,254,251,.88) !important; border-color:#e3e0d7 !important; box-shadow:none !important; color:#334155 !important; } .icon-btn:hover { background:#fff !important; color:var(--accent) !important; transform:none !important; } /* Focus multiview: main top-left, secondary right and bottom; resizers adapt secondaries */ .grid.view-focus { display:grid !important; height:100% !important; overflow:hidden !important; grid-template-columns:minmax(260px,var(--focus-main-w,62fr)) 8px minmax(190px,var(--focus-side-w,38fr)) !important; grid-template-rows:minmax(220px,var(--focus-main-h,64fr)) 8px minmax(120px,var(--focus-bottom-h,36fr)) !important; grid-template-areas:'main vbar side' 'hbar hbar side' 'bottom bottom side' !important; gap:8px !important; padding:10px !important; background:#ece9df !important; } .grid.view-focus.no-bottom { grid-template-rows:minmax(0,1fr) !important; grid-template-areas:'main vbar side' !important; } .grid.view-focus.no-bottom .focus-bottom-row, .grid.view-focus.no-bottom .focus-h-resizer { display:none !important; } .grid.view-focus .focused-row { grid-area:main !important; display:grid !important; place-items:stretch !important; min-width:0; min-height:0; overflow:hidden; } .grid.view-focus .focus-side-row { grid-area:side; display:grid; grid-auto-flow:row; gap:8px; min-width:0; min-height:0; overflow:hidden; } .grid.view-focus .focus-bottom-row { grid-area:bottom; display:grid; grid-auto-flow:column; gap:8px; min-width:0; min-height:0; overflow:hidden; } .grid.view-focus .focus-side-row .cam-card, .grid.view-focus .focus-bottom-row .cam-card { width:100% !important; height:100% !important; min-height:0 !important; aspect-ratio:auto !important; border-radius:8px !important; } .grid.view-focus .focused-row .cam-card { width:100% !important; height:100% !important; max-width:100% !important; max-height:100% !important; border-radius:8px !important; } .grid.view-focus .resizer { display:block !important; background:transparent !important; position:relative; z-index:3; } .grid.view-focus .focus-v-resizer { grid-area:vbar; height:auto; align-self:stretch; cursor:ew-resize; } .grid.view-focus .focus-h-resizer { grid-area:hbar; cursor:ns-resize; } .grid.view-focus .resizer::before { content:''; position:absolute; inset:0; margin:auto; border-radius:999px; background:#d4d0c6; opacity:.72; transition:opacity .12s, background .12s; } .grid.view-focus .focus-v-resizer::before { width:3px; height:52px; } .grid.view-focus .focus-h-resizer::before { width:52px; height:3px; } .grid.view-focus .resizer:hover::before, .grid.view-focus .resizer.dragging::before { background:var(--accent); opacity:1; } .grid.view-focus .thumbs-row { display:none !important; } body.rg-viewer-mode { background:#f3f1ea !important; } body.rg-viewer-mode .app-shell { padding:0 !important; gap:0 !important; background:#f3f1ea !important; } body.rg-viewer-mode main { background:#f3f1ea !important; } body.rg-viewer-mode .grid { background:#ece9df !important; padding:8px !important; } body.rg-viewer-mode .sidebar, body.rg-viewer-mode header, body.rg-viewer-mode .top-accent { display:none !important; } /* v15.1 repairs: reliable collapsed sidebar, wheel-safe controls and readable group contrast */ .app-shell { display:flex !important; min-width:0 !important; min-height:0 !important; } .sidebar { width:var(--rg-sidebar-width,220px) !important; min-width:var(--rg-sidebar-width,220px) !important; max-width:var(--rg-sidebar-width,220px) !important; flex:0 0 var(--rg-sidebar-width,220px) !important; color:#1f2937 !important; } .sidebar-resizer { width:8px; flex:0 0 8px; cursor:ew-resize; position:relative; touch-action:none; } .sidebar-resizer::after { content:''; position:absolute; top:20%; bottom:20%; left:3px; width:2px; border-radius:999px; background:var(--border-strong); opacity:.35; transition:opacity .15s,background .15s; } .sidebar-resizer:hover::after,.sidebar-resizer.dragging::after { opacity:1; background:var(--accent); } body.rg-sidebar-collapsed .sidebar-resizer,body.rg-viewer-mode .sidebar-resizer,body.rg-pure-mode .sidebar-resizer{display:none!important} .sidebar.is-collapsed, body.rg-sidebar-collapsed .sidebar { display:none !important; width:0 !important; min-width:0 !important; max-width:0 !important; flex:0 0 0 !important; flex-basis:0 !important; padding:0 !important; margin:0 !important; border:0 !important; overflow:hidden !important; } body.rg-sidebar-collapsed main { flex:1 1 100% !important; min-width:0 !important; } body.rg-toolbar-collapsed header, body.rg-toolbar-collapsed .top-accent { display:none !important; } .shell-controls { display:flex !important; align-items:center; } body:not(.rg-toolbar-collapsed):not(.rg-sidebar-collapsed) .shell-controls { display:flex !important; } body.rg-toolbar-collapsed .shell-controls .show-toolbar-btn { display:inline-flex !important; } body.rg-sidebar-collapsed .shell-controls .show-sidebar-btn { display:inline-flex !important; } .sidebar-brand .title { color:#111827 !important; } .sidebar-section-title { color:#5f574a !important; font-weight:800 !important; letter-spacing:.08em !important; } .sidebar .group-tab { color:#293241 !important; font-weight:650 !important; background:transparent !important; border-color:transparent !important; } .sidebar .group-tab:hover { background:#f1efe7 !important; color:#111827 !important; border-color:#d8d3c5 !important; box-shadow:none !important; } .sidebar .group-tab.active { background:#e8efff !important; color:#0f172a !important; border-color:#b8c9ff !important; box-shadow:inset 3px 0 0 #2563eb !important; } .sidebar .group-tab > span:first-child { color:inherit !important; } .sidebar .group-tab > span:last-child { color:#475569 !important; background:#eee9dc !important; border-color:#d8d3c5 !important; } .sidebar .group-tab.active > span:last-child { color:#1d4ed8 !important; background:#dbeafe !important; border-color:#bfdbfe !important; } .sidebar-collapse-btn { border:1px solid #d8d3c5; background:#f1efe7; color:#334155; border-radius:8px; padding:4px 8px; font-size:12px; line-height:1.2; cursor:pointer; } .sidebar-collapse-btn:hover { background:#e7e2d5; color:#0f172a; border-color:#c9c1b1; } .sidebar .group-tab { display:flex !important; align-items:center !important; justify-content:space-between !important; gap:8px !important; } .sidebar .group-name { color:inherit !important; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .sidebar .group-count { color:#334155 !important; background:#eee9dc !important; border:1px solid #d8d3c5 !important; border-radius:999px; padding:2px 7px; min-width:26px; text-align:center; font-size:11px; line-height:1.25; font-weight:750; } .sidebar .group-tab.active .group-count { color:#1d4ed8 !important; background:#dbeafe !important; border-color:#bfdbfe !important; } .sidebar .group-tab[style] { color:#293241 !important; } .sidebar-stat { background:#fffefb !important; border-color:#e3e0d7 !important; } .sidebar-stat .k { color:#6b6256 !important; } .sidebar-stat .v { color:#111827 !important; } input[type=range], input[type=number], select { overscroll-behavior:contain; } /* v15.3: top controls stay on one row; compact mode hides labels on narrower screens. */ header { flex-wrap:nowrap !important; overflow-x:auto !important; overflow-y:hidden !important; scrollbar-width:thin; white-space:nowrap; } header .toolbar-group { flex-wrap:nowrap !important; flex:0 0 auto !important; align-items:center !important; } header .toolbar-spacer { flex-wrap:nowrap !important; flex:0 0 auto !important; margin-left:auto !important; } header .ctrl-input { height:34px !important; } header select.ctrl-input { max-width:150px; } header .roomgrid-compact-select { width:auto !important; min-width:92px; padding-right:24px !important; } header .toolbar-group-title { flex:0 0 auto; } @media (max-width: 1360px) { header { gap:8px !important; padding:10px 12px !important; } header .toolbar-group { gap:6px !important; padding:4px 6px !important; } header .toolbar-group-title { display:none !important; } header .ctrl-input { height:32px !important; padding-top:5px !important; padding-bottom:5px !important; } header .ctrl-btn { min-height:32px !important; padding:5px 8px !important; } header .ctrl-btn .btn-label { display:none !important; } header input[placeholder] { max-width:150px !important; } header select.ctrl-input { max-width:118px !important; } header input[type=range] { width:72px !important; } } /* v15.4: interaction polish. Keep core layout intact; make common actions one click and reduce visual jank. */ .cam-card { backface-visibility:hidden; transform:translateZ(0); } .cam-card.rg-card-enter { animation:rg-card-enter .16s ease-out both; } @keyframes rg-card-enter { from { opacity:.55; transform:scale(.985); } to { opacity:1; transform:scale(1); } } .cam-card .ops-row { transition:opacity .12s ease, transform .12s ease !important; } .cam-card .ops-row .icon-btn.quick-op { display:flex !important; } .cam-card.tiny .ops-row .icon-btn.quick-optional { display:none !important; } .cam-card .ops-row .icon-btn.quick-more { display:flex !important; } .cam-card .ops-row .icon-btn:active, .ctrl-btn:active, .group-tab:active { transform:scale(.96) !important; } .grid.view-focus .focus-side-row .cam-card, .grid.view-focus .focus-bottom-row .cam-card { cursor:pointer; transition:border-color .12s ease, transform .12s ease, opacity .12s ease; } .grid.view-focus .focus-side-row .cam-card:hover, .grid.view-focus .focus-bottom-row .cam-card:hover { transform:translateY(-1px); border-color:rgba(37,99,235,.62) !important; } .roomgrid-modal-backdrop { position:fixed; inset:0; z-index:1000003; display:flex; align-items:center; justify-content:center; padding:18px; background:rgba(15,23,42,.40); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); } .roomgrid-modal { width:min(560px, calc(100vw - 36px)); background:#fffefb; border:1px solid #e3e0d7; border-radius:14px; box-shadow:0 22px 80px rgba(15,23,42,.24); padding:14px; color:#111827; } .roomgrid-modal-title { font-size:15px; font-weight:800; margin-bottom:6px; } .roomgrid-modal-hint { font-size:12px; line-height:1.45; color:#64748b; margin-bottom:10px; } .roomgrid-modal-textarea { width:100%; min-height:220px; resize:vertical; border:1px solid #dedbd2; border-radius:10px; padding:10px; outline:none; font:13px/1.45 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background:#fff; color:#111827; } .roomgrid-modal-textarea:focus { border-color:rgba(37,99,235,.45); box-shadow:0 0 0 3px rgba(37,99,235,.10); } .roomgrid-modal-count { margin-top:8px; font-size:12px; color:#64748b; } .roomgrid-modal-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; } /* Custom dark mode: override the later v15 light-surface defaults as well. */ body.rg-dark-mode { color-scheme: dark !important; --bg: #0b0f16; --bg-elevated: rgba(17,24,39,.96); --bg-card: #111827; --bg-input: #172033; --bg-hover: rgba(148,163,184,.12); --bg-overlay: rgba(0,0,0,.46); --border: #293548; --border-strong: #3b4a61; --text: #f1f5f9; --text-secondary: #cbd5e1; --text-muted: #94a3b8; --accent: #60a5fa; --accent-hover: #93c5fd; --accent-soft: rgba(96,165,250,.16); --info: #60a5fa; --info-soft: rgba(96,165,250,.16); --success: #4ade80; --danger: #f87171; --warning: #fbbf24; background:#090b10 !important; color:var(--text) !important; } body.rg-dark-mode .app-shell { background:#090b10 !important; } body.rg-dark-mode main, body.rg-dark-mode .sidebar { background:#111827 !important; border-color:#293548 !important; color:var(--text) !important; } body.rg-dark-mode header { background:#111827 !important; border-bottom-color:#293548 !important; } body.rg-dark-mode .toolbar-group { background:#0f172a !important; border-color:#293548 !important; color:var(--text) !important; } body.rg-dark-mode .toolbar-group-title { color:#94a3b8 !important; } body.rg-dark-mode .grid, body.rg-dark-mode .grid.view-grid, body.rg-dark-mode .grid.view-focus { background:#0b0e13 !important; } body.rg-dark-mode .grid.view-grid .cam-card, body.rg-dark-mode .cam-card { background:#05070b !important; border-color:#293548 !important; } body.rg-dark-mode .cam-video { background:#000 !important; } body.rg-dark-mode .status-layer { background:#111827 !important; color:#cbd5e1 !important; } body.rg-dark-mode .status-layer .status-chip { background:rgba(148,163,184,.12) !important; color:#e5e7eb !important; border-color:#3b4a61 !important; } body.rg-dark-mode .cam-card .name-label, body.rg-dark-mode .cam-card .pill { background:rgba(15,23,42,.92) !important; color:#e5e7eb !important; border-color:#344155 !important; } body.rg-dark-mode .icon-btn, body.rg-dark-mode .shell-controls button { background:rgba(23,32,51,.94) !important; color:#cbd5e1 !important; border-color:#344155 !important; } body.rg-dark-mode .icon-btn:hover, body.rg-dark-mode .shell-controls button:hover { background:#1e3a5f !important; color:#bfdbfe !important; border-color:#60a5fa !important; } body.rg-dark-mode .cam-card .ops-row .icon-btn { background:rgba(15,23,42,.72) !important; color:#e2e8f0 !important; border-color:#3b4a61 !important; } body.rg-dark-mode .cam-card .ops-row .icon-btn:hover { background:rgba(37,99,235,.86) !important; color:#fff !important; border-color:#60a5fa !important; } body.rg-dark-mode .cam-card .drag-handle { background:rgba(15,23,42,.78) !important; border-color:#3b4a61 !important; color:#e2e8f0 !important; } body.rg-dark-mode .sidebar-brand { background:transparent !important; border-color:transparent !important; } body.rg-dark-mode .sidebar-brand .title { color:#f8fafc !important; } body.rg-dark-mode .sidebar-section-title, body.rg-dark-mode .sidebar-stat .k { color:#94a3b8 !important; } body.rg-dark-mode .sidebar .group-tab { color:#cbd5e1 !important; background:transparent !important; border-color:transparent !important; } body.rg-dark-mode .sidebar .group-tab:hover { background:rgba(148,163,184,.12) !important; color:#f8fafc !important; border-color:#344155 !important; } body.rg-dark-mode .sidebar .group-tab.active { background:rgba(37,99,235,.22) !important; color:#bfdbfe !important; border-color:#4b6b9d !important; box-shadow:inset 3px 0 0 #60a5fa !important; } body.rg-dark-mode .sidebar .group-tab > span:last-child, body.rg-dark-mode .sidebar .group-count { color:#cbd5e1 !important; background:#1e293b !important; border-color:#3b4a61 !important; } body.rg-dark-mode .sidebar .group-tab.active > span:last-child, body.rg-dark-mode .sidebar .group-tab.active .group-count { color:#bfdbfe !important; background:#1e3a5f !important; border-color:#4b6b9d !important; } body.rg-dark-mode .sidebar-collapse-btn { background:#172033 !important; color:#cbd5e1 !important; border-color:#3b4a61 !important; } body.rg-dark-mode .sidebar-collapse-btn:hover { background:#24324a !important; color:#f8fafc !important; border-color:#60a5fa !important; } body.rg-dark-mode .sidebar-stat { background:#0f172a !important; border-color:#293548 !important; } body.rg-dark-mode .sidebar-stat .v { color:#f8fafc !important; } body.rg-dark-mode .ctrl-input, body.rg-dark-mode .ctrl-btn, body.rg-dark-mode .seg { background:#172033 !important; color:var(--text) !important; border-color:#3b4a61 !important; box-shadow:none !important; } body.rg-dark-mode .ctrl-btn:hover { background:#24324a !important; border-color:#60a5fa !important; color:#f8fafc !important; } body.rg-dark-mode .ctrl-btn.primary { background:rgba(37,99,235,.22) !important; color:#bfdbfe !important; border-color:#4b6b9d !important; } body.rg-dark-mode .ctrl-btn.primary:hover { background:#2563eb !important; color:#fff !important; border-color:#60a5fa !important; } body.rg-dark-mode .seg button { color:#cbd5e1 !important; } body.rg-dark-mode .seg button.active { background:#2563eb !important; color:#fff !important; } body.rg-dark-mode .toggle { color:#cbd5e1 !important; } body.rg-dark-mode .toggle:hover { background:rgba(148,163,184,.12) !important; color:#f8fafc !important; border-color:#344155 !important; } body.rg-dark-mode .menu-pop { background:rgba(17,24,39,.98) !important; border-color:#3b4a61 !important; } body.rg-dark-mode .menu-pop button { color:#e5e7eb !important; } body.rg-dark-mode .menu-pop button:hover { background:rgba(148,163,184,.12) !important; } body.rg-dark-mode .roomgrid-modal-backdrop { background:rgba(0,0,0,.62) !important; } body.rg-dark-mode .roomgrid-modal { background:#111827 !important; border-color:#3b4a61 !important; color:#f1f5f9 !important; } body.rg-dark-mode .roomgrid-modal-hint, body.rg-dark-mode .roomgrid-modal-count { color:#94a3b8 !important; } body.rg-dark-mode .roomgrid-modal-textarea { background:#0f172a !important; color:#e5e7eb !important; border-color:#3b4a61 !important; } body.rg-dark-mode.rg-viewer-mode, body.rg-dark-mode.rg-viewer-mode .app-shell, body.rg-dark-mode.rg-viewer-mode main { background:#050607 !important; } body.rg-dark-mode.rg-viewer-mode .grid { background:#050607 !important; } body:not(.rg-viewer-mode):not(.rg-focus-mode),body:not(.rg-viewer-mode):not(.rg-focus-mode) .app-shell,body:not(.rg-viewer-mode):not(.rg-focus-mode) .grid{background:var(--rg-page-bg,var(--bg-app))!important;color:var(--rg-text,var(--text))!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) main,body:not(.rg-viewer-mode):not(.rg-focus-mode) .sidebar{background:var(--rg-panel,var(--bg-panel))!important;border-color:var(--rg-border,var(--border))!important;color:var(--rg-text,var(--text))!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) header,body:not(.rg-viewer-mode):not(.rg-focus-mode) .toolbar-group{background:var(--rg-surface-strong,var(--bg-raised))!important;border-color:var(--rg-border,var(--border))!important;color:var(--rg-text,var(--text))!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) .ctrl-input,body:not(.rg-viewer-mode):not(.rg-focus-mode) .ctrl-btn,body:not(.rg-viewer-mode):not(.rg-focus-mode) .seg{background:var(--rg-input,var(--bg-input))!important;border-color:var(--rg-border,var(--border))!important;color:var(--rg-text,var(--text))!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) .ctrl-btn:hover{border-color:var(--rg-secondary)!important;background:var(--rg-secondary-soft)!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) .ctrl-btn.primary,body:not(.rg-viewer-mode):not(.rg-focus-mode) .seg button.active{border-color:var(--rg-accent)!important;background:var(--rg-accent)!important;color:var(--rg-accent-text)!important}body:not(.rg-viewer-mode):not(.rg-focus-mode) .sidebar .group-tab.active{border-color:var(--rg-accent)!important;background:var(--accent-soft)!important;color:var(--rg-accent)!important;box-shadow:inset 3px 0 0 var(--rg-accent)!important} @media (prefers-reduced-motion: reduce) { .cam-card, .ctrl-btn, .icon-btn, .group-tab, .menu-pop, .mc-tooltip { transition:none !important; animation:none !important; } } `), })); // ---- 布局骨架 ---- const sidebar = $('aside', { class: 'sidebar', style: { width: '248px', background: 'linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,.78))', border: '1px solid var(--border)', borderRadius: '18px', padding: '14px 10px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px', flexShrink: '0', transition: 'width .2s, padding .2s', boxShadow: 'var(--shadow-md)', } }); const main = $('main', { style: { flex: '1', display: 'flex', flexDirection: 'column', minWidth: '0', background: 'linear-gradient(180deg, rgba(255,255,255,.82), rgba(255,255,255,.72))', border: '1px solid var(--border)', borderRadius: '18px', overflow: 'hidden', boxShadow: 'var(--shadow-md)' } }); const topAccent = $('div', { class: 'top-accent' }); const toolbar = $('header', { style: { padding: '14px 16px', background: 'transparent', borderBottom: '1px solid var(--border)', display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'nowrap', flexShrink: '0', overflowX: 'auto', overflowY: 'hidden', } }); const grid = $('section', { class: 'grid view-grid', style: { flex: '1', overflowY: 'auto', padding: '16px', } }); main.append(topAccent, toolbar, grid); const sidebarResizer = $('div', { class: 'sidebar-resizer', role: 'separator', ariaOrientation: 'vertical', title: tx('拖动调整侧栏宽度', 'Drag to resize sidebar', 'Ziehen, um die Seitenleiste zu vergrößern') }); const appShell = $('div', { class: 'app-shell', lang: LANG }, [sidebar, sidebarResizer, main]); document.body.append(appShell); sidebarResizer.addEventListener('pointerdown', event => { event.preventDefault(); sidebarResizer.classList.add('dragging'); sidebarResizer.setPointerCapture?.(event.pointerId); const move = moveEvent => { const width = clampInt(moveEvent.clientX - appShell.getBoundingClientRect().left, 180, 460, 220); document.documentElement.style.setProperty('--rg-sidebar-width', `${width}px`); sidebarResizer.dataset.width = String(width); }; const up = upEvent => { sidebarResizer.classList.remove('dragging'); sidebarResizer.releasePointerCapture?.(upEvent.pointerId); sidebarResizer.removeEventListener('pointermove', move); sidebarResizer.removeEventListener('pointerup', up); const width = clampInt(sidebarResizer.dataset.width, 180, 460, store.state.settings.sidebarWidth || 220); store.patchSettings({ sidebarWidth: width }); }; sidebarResizer.addEventListener('pointermove', move); sidebarResizer.addEventListener('pointerup', up); }); const toastHost = $('div', { style: { position: 'fixed', right: '16px', top: '16px', zIndex: '1000000', display: 'flex', flexDirection: 'column', gap: '8px', pointerEvents: 'none' } }); document.body.appendChild(toastHost); function toast(msg, duration = 2600, action = null) { const el = $('div', { style: { maxWidth: '360px', background: 'rgba(17,24,39,.88)', color: '#fff', padding: '9px 12px', borderRadius: '8px', boxShadow: 'var(--shadow-md)', fontSize: '12px', lineHeight: '1.35' } }, String(msg || '')); if (action) { el.style.pointerEvents = 'auto'; el.appendChild(action); } toastHost.appendChild(el); setTimeout(() => { try { el.style.opacity = '0'; el.style.transform = 'translateY(-4px)'; el.style.transition = 'opacity .18s, transform .18s'; } catch (_) {} }, Math.max(400, duration - 400)); setTimeout(() => { try { el.remove(); } catch (_) {} }, duration); return el; } const showStorageProblem = (problem) => toast(tx( '无法安全保存工作区。请立即导出备份。', 'The workspace could not be saved safely. Export a backup now.', 'Der Arbeitsbereich konnte nicht sicher gespeichert werden. Bitte jetzt eine Sicherung exportieren.' ) + (problem?.message ? ` (${problem.message})` : ''), 10000); StorageHealth.subscribe(showStorageProblem); if (StorageHealth.error) setTimeout(() => showStorageProblem(StorageHealth.error), 250); function installHintSystem() { let tooltip = null; let timer = 0; let activeEl = null; const autoText = (el) => { if (!el) return ''; const explicit = el.dataset?.hint || el.getAttribute?.('aria-label') || el.title; if (explicit) return explicit; if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') return el.placeholder || ''; if (el.tagName === 'SELECT') return el.title || t('hintSort'); if (el.tagName === 'BUTTON') return (el.textContent || '').replace(/\s+/g, ' ').trim(); return ''; }; const ensure = (el) => { const text = autoText(el); if (text) setElementHint(el, text); return text; }; const fill = (root = document.body) => { try { root.querySelectorAll('button,input,select,textarea,[title]').forEach(el => ensure(el)); } catch (_) {} }; const hide = () => { clearTimeout(timer); timer = 0; activeEl = null; if (tooltip) { try { tooltip.remove(); } catch (_) {} tooltip = null; } }; const show = (el) => { if (!el || el.disabled || document.body.classList.contains('rg-pure-mode')) return; const text = ensure(el); if (!text) return; if (!tooltip) tooltip = $('div', { class: 'mc-tooltip' }); tooltip.textContent = text; document.body.appendChild(tooltip); const rect = el.getBoundingClientRect(); const tw = Math.min(tooltip.offsetWidth || 240, window.innerWidth - 24); let left = Math.max(8, Math.min(window.innerWidth - tw - 8, rect.left + rect.width / 2 - tw / 2)); let top = rect.bottom + 8; if (top + (tooltip.offsetHeight || 32) > window.innerHeight - 8) top = Math.max(8, rect.top - (tooltip.offsetHeight || 32) - 8); tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; requestAnimationFrame(() => tooltip?.classList.add('show')); }; const schedule = (el) => { clearTimeout(timer); activeEl = el; timer = setTimeout(() => show(el), 420); }; document.addEventListener('pointerover', (e) => { const el = e.target?.closest?.('button,input,select,textarea,[data-hint],[title]'); if (!el || el === activeEl) return; schedule(el); }, true); document.addEventListener('pointerout', (e) => { if (!activeEl) return; if (!activeEl.contains(e.relatedTarget)) hide(); }, true); document.addEventListener('focusin', (e) => { const el = e.target?.closest?.('button,input,select,textarea,[data-hint],[title]'); if (el) schedule(el); }, true); document.addEventListener('focusout', hide, true); document.addEventListener('click', hide, true); window.addEventListener('scroll', hide, true); try { const mo = new MutationObserver(ms => { for (const m of ms) for (const n of m.addedNodes || []) if (n.nodeType === 1) fill(n); }); mo.observe(document.body, { childList: true, subtree: true }); } catch (_) {} fill(document.body); } installHintSystem(); function installWheelInputGuard() { const isGuarded = (el) => { if (!el || !el.closest) return false; const target = el.closest('input, select, textarea'); if (!target) return false; const tag = target.tagName; const type = String(target.getAttribute('type') || '').toLowerCase(); return tag === 'SELECT' || type === 'range' || type === 'number'; }; document.addEventListener('wheel', (e) => { if (!isGuarded(e.target)) return; e.preventDefault(); e.stopPropagation(); const target = e.target.closest('input, select, textarea'); // 浏览器默认会用滚轮改 range / number / select 的值,进而触发整页重排;这里直接阻断。 try { target.blur(); } catch (_) {} }, { capture: true, passive: false }); } installWheelInputGuard(); const recordings = new Map(); const replacementTimers = new Map(); const recordingLog = []; const pureExitChip = $('button', { class: 'pure-exit-chip', title: t('pureExitHint'), onclick: () => setPureMode(false), }, t('pureExitChip')); document.body.appendChild(pureExitChip); const shellControls = $('div', { class: 'shell-controls' }); const escapeMenuBtn = $('button', { class: 'escape-menu-btn', title: tx('打开菜单或恢复界面', 'Open menu or restore controls', 'Menü öffnen oder Oberfläche wiederherstellen'), 'aria-haspopup': 'menu', 'aria-expanded': 'false', onclick: (e) => { const locked = !!store.state.settings.pureMode || !!store.state.settings.viewerMode || !!store.state.settings.toolbarCollapsed || !!store.state.settings.sidebarCollapsed; if (locked) { closeTransientUi(); document.body.classList.remove('rg-pure-mode', 'rg-viewer-mode', 'rg-toolbar-collapsed', 'rg-sidebar-collapsed'); sidebar.classList.remove('is-collapsed'); store.patchSettings({ pureMode: false, viewerMode: false, toolbarCollapsed: false, sidebarCollapsed: false }); toast(tx('界面已恢复', 'Controls restored', 'Oberfläche wieder eingeblendet')); } else { openMoreMenu(e.currentTarget); } }, }, '☰ ' + tx('菜单', 'Menu', 'Menü')); const showToolbarBtn = $('button', { class: 'show-toolbar-btn', title: tx('显示顶部工具栏', 'Show toolbar', 'Werkzeugleiste anzeigen'), onclick: () => { document.body.classList.remove('rg-toolbar-collapsed'); store.patchSettings({ toolbarCollapsed: false }); }, }, tx('工具', 'Tools', 'Werkzeuge')); const showSidebarBtn = $('button', { class: 'show-sidebar-btn', title: tx('显示左侧分组', 'Show sidebar', 'Seitenleiste anzeigen'), onclick: () => { if (compactRooms.matches) { setCompactRoomsOpen(true); return; } document.body.classList.remove('rg-sidebar-collapsed'); sidebar.classList.remove('is-collapsed'); store.patchSettings({ sidebarCollapsed: false }); }, }, tx('分组', 'Groups', 'Gruppen')); shellControls.append(escapeMenuBtn, showToolbarBtn, showSidebarBtn); document.body.appendChild(shellControls); function syncShellControls() { if (!shellControls) return; const locked = !!store.state.settings.pureMode || !!store.state.settings.viewerMode || !!store.state.settings.toolbarCollapsed || !!store.state.settings.sidebarCollapsed; shellControls.style.display = 'flex'; escapeMenuBtn.textContent = '☰ ' + (locked ? tx('恢复界面', 'Restore controls', 'Oberfläche wiederherstellen') : tx('菜单', 'Menu', 'Menü')); setElementHint(escapeMenuBtn, locked ? tx('显示工具栏、分组和菜单', 'Show toolbar, groups, and menus', 'Werkzeugleiste, Gruppen und Menüs anzeigen') : tx('Weitere Werkzeuge öffnen', 'Open more tools', 'Weitere Werkzeuge öffnen')); } let pureCursorTimer = 0; let _moreMenuClose = null; let _moreMenuRelayout = null; let _moreMenuKeydown = null; function clearMoreMenuListeners(anchor = null) { if (_moreMenuClose) { document.removeEventListener('click', _moreMenuClose); _moreMenuClose = null; } if (_moreMenuRelayout) { window.removeEventListener('resize', _moreMenuRelayout); window.visualViewport?.removeEventListener('resize', _moreMenuRelayout); window.visualViewport?.removeEventListener('scroll', _moreMenuRelayout); _moreMenuRelayout = null; } if (_moreMenuKeydown) { document.removeEventListener('keydown', _moreMenuKeydown, true); _moreMenuKeydown = null; } anchor?.setAttribute('aria-expanded', 'false'); } function closeTransientUi() { document.querySelectorAll('.menu-pop,.mc-tooltip').forEach(el => { try { el.remove(); } catch (_) {} }); clearMoreMenuListeners(escapeMenuBtn); } function openAdvancedFilterMenu(anchor) { document.querySelectorAll('.advanced-filter-pop').forEach(el => { try { el.remove(); } catch (_) {} }); const rect = anchor.getBoundingClientRect(); const menu = $('div', { class: 'menu-pop advanced-filter-pop', style: { left: `${Math.max(8, Math.min(window.innerWidth - 318, rect.left))}px`, top: `${Math.min(window.innerHeight - 420, rect.bottom + 6)}px`, width: '310px', maxHeight: 'min(420px, calc(100vh - 24px))', overflowY: 'auto', padding: '10px', }, }); const f = store.state.settings.filter || {}; const label = (text, control) => $('label', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '5px 2px', fontSize: '12px', color: 'var(--text)' } }, [control, $('span', {}, text)]); const field = (text, value, type, onChange, extra = {}) => { const props = { class: 'ctrl-input', value, style: { flex: '1', minWidth: '0', padding: '5px 7px', fontSize: '11px' }, onchange: e => onChange(e.target.value), ...extra }; if (type !== 'select') props.type = type; const input = $(type === 'select' ? 'select' : 'input', props); return $('label', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', padding: '5px 2px', fontSize: '11px', color: 'var(--text-secondary)' } }, [$( 'span', {}, text), input]); }; const watched = $('select', { class: 'ctrl-input', style: { flex: '1', padding: '5px 7px', fontSize: '11px' }, onchange: e => { const v = e.target.value; store.patchFilter({ onlyWatched: v === 'watched', neverWatched: v === 'never' }); } }, [ $('option', { value: 'all' }, tx('alle Sehstände', 'all watch states', 'alle Sehstände')), $('option', { value: 'watched' }, t('filterWatched')), $('option', { value: 'never' }, t('filterNeverWatched')), ]); watched.value = f.onlyWatched ? 'watched' : (f.neverWatched ? 'never' : 'all'); const check = (key, text) => label(text, $('input', { type: 'checkbox', checked: !!f[key], onchange: e => store.patchFilter({ [key]: e.target.checked }) })); const divider = $('div', { style: { height: '1px', background: 'var(--border)', margin: '7px 0' } }); menu.append( $('div', { style: { fontSize: '11px', fontWeight: '800', color: 'var(--text-muted)', marginBottom: '4px' } }, t('advancedFilter')), $('label', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', padding: '5px 2px', fontSize: '11px', color: 'var(--text-secondary)' } }, [$( 'span', {}, tx('Sehstatus', 'Watch status', 'Sehstatus')), watched]), check('favoritesOnly', t('filterFavorites')), check('followedOnly', t('filterFollowed')), check('inActiveView', t('filterInView')), check('hasNotes', t('filterHasNotes')), divider, field(t('filterTagInclude'), f.tagInclude || '', 'text', value => store.patchFilter({ tagInclude: value }), { placeholder: 'z. B. cosplay' }), field(t('filterTagExclude'), f.tagExclude || '', 'text', value => store.patchFilter({ tagExclude: value }), { placeholder: 'z. B. private' }), field(t('filterMinWatch'), Math.round((Number(f.minWatchMs) || 0) / 60000), 'number', value => store.patchFilter({ minWatchMs: Math.max(0, Number(value) || 0) * 60000 }), { min: '0', step: '1' }), field(t('filterMinSessions'), Number(f.minSessions) || 0, 'number', value => store.patchFilter({ minSessions: value }), { min: '0', step: '1' }), field(t('filterLastWatchedDays'), Number(f.lastWatchedDays) || 0, 'number', value => store.patchFilter({ lastWatchedDays: value }), { min: '0', step: '1', placeholder: '0 = egal wann' }), field(t('filterViewerMin'), Number(f.viewerMin) || 0, 'number', value => store.patchFilter({ viewerMin: value }), { min: '0', step: '1' }), field(t('filterViewerMax'), Number(f.viewerMax) || 0, 'number', value => store.patchFilter({ viewerMax: value }), { min: '0', step: '1', placeholder: '0 = unbegrenzt' }), $('div', { style: { color: 'var(--text-muted)', fontSize: '10px', lineHeight: '1.35', marginTop: '6px' } }, tx('0 bei Zuschauergrenzen bedeutet keine Begrenzung.', '0 means no viewer limit.', '0 bedeutet keine Zuschauerbegrenzung.')), ); document.body.appendChild(menu); const close = e => { if (!menu.contains(e.target) && e.target !== anchor) { menu.remove(); document.removeEventListener('click', close); } }; setTimeout(() => document.addEventListener('click', close), 0); } function applyPureModeState() { const on = !!store.state.settings.pureMode; const viewerOn = !!store.state.settings.viewerMode && !on; const darkMode = store.state.settings.theme === 'dark'; const thumbsCollapsed = !!store.state.settings.focusThumbsCollapsed; const videoFit = store.state.settings.videoFit === 'cover' ? 'cover' : 'contain'; document.body.classList.toggle('rg-dark-mode', darkMode); document.body.classList.toggle('rg-pure-mode', on); document.body.classList.toggle('rg-viewer-mode', viewerOn); document.body.classList.toggle('rg-toolbar-collapsed', !!store.state.settings.toolbarCollapsed && !on && !viewerOn); document.body.classList.toggle('rg-sidebar-collapsed', !!store.state.settings.sidebarCollapsed && !on && !viewerOn); document.body.classList.toggle('rg-focus-thumbs-collapsed', thumbsCollapsed); document.body.classList.toggle('rg-video-cover', videoFit === 'cover'); document.body.classList.toggle('rg-video-contain', videoFit !== 'cover'); document.body.classList.toggle('rg-focus-mode', store.state.settings.viewMode === 'focus' && !on && !viewerOn); syncShellControls?.(); document.body.classList.remove('pure-cursor-hidden'); setElementHint(pureExitChip, t('pureExitHint')); pureExitChip.textContent = t('pureExitChip'); if (typeof pureModeBtn !== 'undefined' && pureModeBtn) { setTrustedHtml(pureModeBtn, trustedHtml(iconLabel('clean', on ? t('pureModeOff') : t('pureMode')))); pureModeBtn.classList.toggle('primary', on); setElementHint(pureModeBtn, on ? t('pureModeOff') + ' · Alt+P/C' : t('pureModeHint')); } if (typeof viewerModeBtn !== 'undefined' && viewerModeBtn) { setTrustedHtml(viewerModeBtn, trustedHtml(iconLabel('focus', viewerOn ? t('viewerModeOff') : t('viewerMode')))); viewerModeBtn.classList.toggle('primary', viewerOn); setElementHint(viewerModeBtn, viewerOn ? t('viewerModeOff') + ' · Alt+V' : t('viewerModeHint')); } if (typeof focusThumbToggleBtn !== 'undefined' && focusThumbToggleBtn) { setTrustedHtml(focusThumbToggleBtn, trustedHtml(iconLabel('grid', thumbsCollapsed ? t('focusThumbsShow') : t('focusThumbsHide')))); focusThumbToggleBtn.classList.toggle('primary', !thumbsCollapsed && store.state.settings.viewMode === 'focus'); setElementHint(focusThumbToggleBtn, t('focusThumbsHint')); } if (typeof videoFitBtn !== 'undefined' && videoFitBtn) { setTrustedHtml(videoFitBtn, trustedHtml(iconLabel('expand', videoFit === 'cover' ? t('videoFitCover') : t('videoFitContain')))); videoFitBtn.classList.toggle('primary', videoFit === 'cover'); setElementHint(videoFitBtn, t('videoFitHint')); } if (on) closeTransientUi(); requestAnimationFrame(() => { applyGridSize(); applyFocusMainSizing(); }); } function setPureMode(on) { store.patchSettings({ pureMode: !!on }); if (on) toast(t('pureModeOn') + ' · Alt+P/C / Esc'); } function togglePureMode() { setPureMode(!store.state.settings.pureMode); } function toggleViewerMode() { store.patchSettings({ viewerMode: !store.state.settings.viewerMode }); } function toggleFocusThumbs() { store.patchSettings({ focusThumbsCollapsed: !store.state.settings.focusThumbsCollapsed }); } function toggleVideoFit() { store.patchSettings({ videoFit: store.state.settings.videoFit === 'cover' ? 'contain' : 'cover' }); } function bumpPureCursor() { if (!store.state.settings.pureMode) return; document.body.classList.remove('pure-cursor-hidden'); clearTimeout(pureCursorTimer); pureCursorTimer = setTimeout(() => { if (store.state.settings.pureMode) document.body.classList.add('pure-cursor-hidden'); }, 1800); } document.addEventListener('mousemove', bumpPureCursor, true); document.addEventListener('pointerdown', bumpPureCursor, true); const patchSettingsSoft = (() => { let pending = {}; const flush = debounce(() => { const patch = pending; pending = {}; if (Object.keys(patch).length) store.patchSettings(patch); }, 180); return (patch) => { Object.assign(pending, patch || {}); flush(); }; })(); // ---- Toolbar 渲染 ---- const tbInput = $('input', { class: 'ctrl-input', placeholder: t('addPlaceholder'), title: t('hintAddInput'), style: { width: '200px' }, onpaste: (e) => { setTimeout(() => { const raw = String(e.target.value || ''); const parts = raw.split(/[\s,;,;]+/).map(x => x.trim()).filter(Boolean); if (parts.length <= 1) return; const r = importUsernameList(parts); e.target.value = ''; toast(t('manualImportDone', r.added, r.exists)); }, 0); }, onkeypress: (e) => { if (e.key === 'Enter') { const v = normalizeUsername(e.target.value); if (!v) return; if (!isLikelyUsername(v)) { toast(t('invalidUsername')); return; } if (store.addRoom(v)) service.start(v); e.target.value = ''; } }, }); const searchInput = $('input', { class: 'ctrl-input', placeholder: t('searchPlaceholder'), title: t('hintSearchInput'), value: store.state.settings.searchQuery || '', style: { width: '150px' }, oninput: debounce((e) => store.patchSettings({ searchQuery: normalizeUsername(e.target.value), pageIndex: 0 }), 80), }); const tempUrlBtn = $('button', { class: 'ctrl-btn', title: tx('导入临时 URL,刷新后消失,不保存到配置', 'Import temporary URLs; they disappear after refresh', 'Temporäre URLs importieren; sie verschwinden nach dem Neuladen'), onclick: () => importTemporaryUrls(), }, tx('导入URL', 'URL', 'URLs')); const mkToggle = (label, key, sub = false) => { const cb = $('input', { type: 'checkbox', checked: sub ? store.state.settings.filter[key] : store.state.settings[key], onchange: (e) => sub ? store.update(s => { s.settings.filter[key] = e.target.checked; s.settings.pageIndex = 0; }, 'settings') : store.patchSettings({ [key]: e.target.checked, pageIndex: 0 }) }); return $('label', { class: 'toggle', title: String(label).replace(/^[^\p{L}\p{N}]+/u, '').trim() || String(label) }, [cb, label]); }; function filterModeFromState() { const f = store.state.settings.filter || {}; if (f.onlyOnline) return 'online'; if (f.hideOffline && f.hidePrivate) return 'hideOfflinePrivate'; if (f.hideOffline) return 'hideOffline'; if (f.hidePrivate) return 'hidePrivate'; return 'all'; } function applyFilterMode(mode) { const next = { hideOffline: false, hidePrivate: false, onlyOnline: false }; if (mode === 'online') next.onlyOnline = true; else if (mode === 'hideOffline') next.hideOffline = true; else if (mode === 'hidePrivate') next.hidePrivate = true; else if (mode === 'hideOfflinePrivate') { next.hideOffline = true; next.hidePrivate = true; } store.update(s => { Object.assign(s.settings.filter, next); s.settings.pageIndex = 0; }, 'settings:filter,pageIndex'); } const filterSel = $('select', { class: 'ctrl-input roomgrid-compact-select', title: tx('筛选当前分组', 'Filter current group', 'Aktuelle Gruppe filtern'), onchange: (e) => applyFilterMode(e.target.value), }, [ $('option', { value: 'all' }, tx('全部状态', 'All status', 'Alle Status')), $('option', { value: 'online' }, t('onlyOnline')), $('option', { value: 'hideOffline' }, t('hideOffline')), $('option', { value: 'hidePrivate' }, t('hidePrivate')), $('option', { value: 'hideOfflinePrivate' }, tx('隐藏离线/私密', 'Hide offline/private', 'Offline/Private ausblenden')), ]); filterSel.value = filterModeFromState(); const sortSel = $('select', { class: 'ctrl-input', title: t('hintSort'), style: { padding: '6px 8px' }, onchange: (e) => store.patchSettings({ sortBy: e.target.value, pageIndex: 0 }), }, [ $('option', { value: 'manual' }, t('sortManual')), $('option', { value: 'status' }, t('sortStatus')), $('option', { value: 'name' }, t('sortName')), $('option', { value: 'addedAt' }, t('sortAdded')), $('option', { value: 'watchTime' }, t('sortWatchTime')), $('option', { value: 'sessions' }, t('sortSessions')), $('option', { value: 'lastWatched' }, t('sortLastWatched')), $('option', { value: 'firstWatched' }, t('sortFirstWatched')), $('option', { value: 'viewerCount' }, t('sortViewerCount')), $('option', { value: 'lastOnline' }, t('sortLastOnline')), ]); sortSel.value = store.state.settings.sortBy; const advancedFilterBtn = $('button', { class: 'ctrl-btn', title: t('advancedFilter'), onclick: (e) => openAdvancedFilterMenu(e.currentTarget), }, '⚙'); const refreshAllBtn = $('button', { class: 'ctrl-btn primary', title: t('refreshAll') + ' · R', style: { cursor: 'pointer' }, onclick: () => service.refreshAll(), html: trustedHtml(iconLabel('refresh', t('refreshAll'))), }); const compactRooms = matchMedia('(max-width: 700px)'); let compactRoomsOpen = false; const setCompactRoomsOpen = open => { compactRoomsOpen = open; sidebar.classList.toggle('multicam-room-drawer', open && compactRooms.matches); sidebarToggleBtn.setAttribute('aria-expanded', String(!sidebarIsHidden())); scheduleSidebarRender(); if (open) requestAnimationFrame(() => sidebar.querySelector('.sidebar-collapse-btn')?.focus()); }; const sidebarToggleBtn = $('button', { class: 'ctrl-btn', title: t('hintSidebarToggle'), ariaLabel: t('hintSidebarToggle'), style: { cursor: 'pointer', minWidth: '38px', padding: '6px 10px' }, onclick: () => { if (compactRooms.matches) { setCompactRoomsOpen(!compactRoomsOpen); return; } const v = !store.state.settings.sidebarCollapsed; document.body.classList.toggle('rg-sidebar-collapsed', v); sidebar.classList.toggle('is-collapsed', v); store.patchSettings({ sidebarCollapsed: v }); }, html: trustedHtml(iconSvg('menu', 16)), }); const resetCompactRooms = () => setCompactRoomsOpen(false); compactRooms.addEventListener('change', resetCompactRooms); window.addEventListener('pagehide', () => compactRooms.removeEventListener('change', resetCompactRooms), { once: true }); sidebar.addEventListener('keydown', event => { if (event.key !== 'Escape' || !compactRoomsOpen) return; event.stopPropagation(); setCompactRoomsOpen(false); sidebarToggleBtn.focus(); }); const toolbarCollapseBtn = $('button', { class: 'ctrl-btn', title: tx('收起顶部工具栏', 'Collapse toolbar', 'Werkzeugleiste einklappen'), style: { cursor: 'pointer', minWidth: '38px', padding: '6px 10px' }, onclick: () => store.patchSettings({ toolbarCollapsed: true }), }, tx('收起', 'Hide', 'Ausblenden')); // —— 视图模式:使用 select 减少顶部按钮数量 —— // function setViewMode(mode) { if (mode !== 'focus') { store.patchSettings({ viewMode: 'grid' }); return; } if (!store.state.settings.focusedRoomId) { const vr = visibleRooms(); const first = vr.find(r => r.lastStatus === 'online') || vr[0]; if (first) store.patchSettings({ viewMode: 'focus', focusedRoomId: first.id, focusThumbsCollapsed: false }); else store.patchSettings({ viewMode: 'focus', focusThumbsCollapsed: false }); } else { store.patchSettings({ viewMode: 'focus', focusThumbsCollapsed: false }); } } const viewModeSel = $('select', { class: 'ctrl-input roomgrid-compact-select', title: t('viewModeLabel') + ' · G/F', onchange: (e) => setViewMode(e.target.value), }, [ $('option', { value: 'grid' }, t('viewGrid')), $('option', { value: 'focus' }, t('viewFocus')), ]); function syncViewSeg() { viewModeSel.value = store.state.settings.viewMode === 'focus' ? 'focus' : 'grid'; } syncViewSeg(); const layoutSel = $('select', { class: 'ctrl-input roomgrid-compact-select', title: tx('单屏窗口数量', 'Windows per page', 'Fenster pro Seite'), onchange: (e) => { if (e.target.value === 'custom') store.patchSettings({ useCustomLayout: true, pageIndex: 0 }); else store.patchSettings({ useCustomLayout: false, layoutSize: Number(e.target.value), pageIndex: 0 }); }, }, [ ...SUPPORTED_LAYOUT_SIZES.map(n => $('option', { value: String(n) }, tx(`${n} 窗口`, `${n} rooms`, `${n} Räume`, `${n} salas`, `${n} salas`))), $('option', { value: 'custom' }, tx('自定义', 'Custom grid', 'Freies Raster')), ]); layoutSel.value = store.state.settings.useCustomLayout ? 'custom' : String(store.state.settings.layoutSize || 4); const pagePrevBtn = $('button', { class: 'ctrl-btn', title: tx('上一页', 'Previous page', 'Vorherige Seite'), onclick: () => setPageIndex((store.state.settings.pageIndex || 0) - 1), }, '‹'); const pageNextBtn = $('button', { class: 'ctrl-btn', title: tx('下一页', 'Next page', 'Nächste Seite'), onclick: () => setPageIndex((store.state.settings.pageIndex || 0) + 1), }, '›'); const pageIndicator = $('span', { class: 'page-indicator' }, '1 / 1'); // 全局音量 const volSlider = $('input', { type: 'range', min: '0', max: '100', value: String(store.state.settings.volume * 100), title: t('hintVolume'), style: { width: '90px', cursor: 'pointer', accentColor: 'var(--accent)' }, oninput: (e) => { const v = Number(e.target.value) / 100; store.state.settings.volume = v; patchSettingsSoft({ volume: v }); requestAnimationFrame(() => store.state.rooms.forEach(r => applyMute(r.id))); }, onchange: (e) => store.patchSettings({ volume: Number(e.target.value) / 100 }), }); const volLabel = $('span', { style: { fontSize: '14px', color: 'var(--text-muted)', display:'inline-flex', alignItems:'center' }, html: trustedHtml(iconSvg('volume', 15)) }); const audioModeSel = $('select', { class: 'ctrl-input roomgrid-compact-select', title: tx('选择多摄像头声音路由', 'Choose MultiCam audio routing', 'MultiCam-Audioverwaltung wählen'), 'data-roomgrid-audio-routing': '1', onchange: event => { const audioRoutingMode = ['manual', 'solo', 'focus'].includes(event.currentTarget.value) ? event.currentTarget.value : 'manual'; let audioSoloRoomId = store.state.settings.audioSoloRoomId || ''; if (audioRoutingMode === 'solo' && !findRoomAny(audioSoloRoomId)) audioSoloRoomId = visibleRooms().find(room => room.lastStatus === 'online')?.id || visibleRooms()[0]?.id || ''; store.patchSettings({ audioRoutingMode, audioSoloRoomId }); requestAnimationFrame(applyAllAudioRouting); }, }, [ $('option', { value: 'manual' }, tx('音频:手动', 'Audio: manual', 'Audio: manuell')), $('option', { value: 'solo' }, tx('音频:单独', 'Audio: solo cam', 'Audio: eine Cam')), $('option', { value: 'focus' }, tx('音频:跟随主屏', 'Audio: follow focus', 'Audio: Hauptbild folgen')), ]); audioModeSel.value = store.state.settings.audioRoutingMode || 'manual'; // 网格大小 const sizeSlider = $('input', { type: 'range', min: '220', max: '900', value: String(store.state.settings.gridSize), title: t('hintGridSize'), style: { width: '110px', cursor: 'pointer', accentColor: 'var(--accent)' }, oninput: (e) => { const gridSize = parseInt(e.target.value, 10); store.state.settings.gridSize = gridSize; patchSettingsSoft({ gridSize }); applyGridSize(); }, onchange: (e) => store.patchSettings({ gridSize: parseInt(e.target.value, 10) }), }); const focusScaleSlider = $('input', { type: 'range', min: '45', max: '76', value: String(store.state.settings.focusMainPct || 62), title: t('hintMainRatio'), style: { width: '95px', cursor: 'pointer', accentColor: 'var(--accent)' }, oninput: (e) => { const focusMainPct = parseInt(e.target.value, 10); store.state.settings.focusMainPct = focusMainPct; patchSettingsSoft({ focusMainPct }); applyGridSize(); applyFocusMainSizing(); }, onchange: (e) => store.patchSettings({ focusMainPct: parseInt(e.target.value, 10) }), }); const focusAspectSel = $('select', { class: 'ctrl-input', title: t('hintMainAspect'), style: { padding: '6px 8px', width: '86px' }, onchange: (e) => { store.patchSettings({ focusAspect: e.target.value }); applyFocusMainSizing(); }, }, [ $('option', { value: 'auto' }, tx('自适应', 'Auto', 'Automatisch')), $('option', { value: '16:9' }, '16:9'), $('option', { value: '4:3' }, '4:3'), $('option', { value: '1:1' }, '1:1'), $('option', { value: '9:16' }, '9:16'), ]); focusAspectSel.value = store.state.settings.focusAspect || 'auto'; const focusThumbSlider = $('input', { type: 'range', min: '96', max: '260', value: String(store.state.settings.focusThumbSize || 150), title: t('hintThumbSize'), style: { width: '80px', cursor: 'pointer', accentColor: 'var(--accent)' }, oninput: (e) => { const focusThumbSize = parseInt(e.target.value, 10); store.state.settings.focusThumbSize = focusThumbSize; patchSettingsSoft({ focusThumbSize }); applyGridSize(); }, onchange: (e) => store.patchSettings({ focusThumbSize: parseInt(e.target.value, 10) }), }); const viewerModeBtn = $('button', { class: 'ctrl-btn', title: t('viewerModeHint'), style: { cursor: 'pointer' }, onclick: () => toggleViewerMode(), html: trustedHtml(iconLabel('focus', t('viewerMode'))), }); const focusThumbToggleBtn = $('button', { class: 'ctrl-btn', title: t('focusThumbsHint'), style: { cursor: 'pointer' }, onclick: () => toggleFocusThumbs(), html: trustedHtml(iconLabel('grid', t('focusThumbsHide'))), }); const videoFitBtn = $('button', { class: 'ctrl-btn', title: t('videoFitHint'), style: { cursor: 'pointer' }, onclick: () => toggleVideoFit(), html: trustedHtml(iconLabel('expand', t('videoFitContain'))), }); const pureModeBtn = $('button', { class: 'ctrl-btn', title: t('pureModeHint'), style: { cursor: 'pointer' }, onclick: () => togglePureMode(), html: trustedHtml(iconLabel('clean', t('pureMode'))), }); // 通知开关 const notifyToggle = $('label', { class: 'toggle', title: t('notifyTitle') }, [ $('input', { type: 'checkbox', checked: store.state.settings.notifyOnline, onchange: async (e) => { if (e.target.checked) { const ok = await Notify.request(); if (!ok) alert(t('permDenied')); } store.patchSettings({ notifyOnline: e.target.checked }); }, }), t('notifyOnline'), ]); function importUsernameList(usernames) { const clean = [...new Set((usernames || []) .map(u => normalizeUsername(u)) .filter(isLikelyUsername))]; let added = 0, exists = 0; clean.forEach(u => { const hadRoom = !!store.state.rooms.find(r => r.id === u); if (store.addRoom(u)) { if (!hadRoom) service.start(u); added++; } else { exists++; } }); return { added, exists, total: clean.length }; } function openManualImportPrompt() { closeTransientUi(); const backdrop = $('div', { class: 'roomgrid-modal-backdrop' }); const panel = $('div', { class: 'roomgrid-modal' }); const title = $('div', { class: 'roomgrid-modal-title' }, t('manualImport')); const hint = $('div', { class: 'roomgrid-modal-hint' }, t('manualImportPrompt')); const ta = $('textarea', { class: 'roomgrid-modal-textarea', placeholder: 'alice\nbob\ncarol', }); const countEl = $('div', { class: 'roomgrid-modal-count' }, '0'); let escHandler = null; const close = () => { if (escHandler) { document.removeEventListener('keydown', escHandler); escHandler = null; } try { backdrop.remove(); } catch (_) {} }; const parse = () => String(ta.value || '').split(/[\s,;,;]+/).map(s => s.trim()).filter(Boolean); const update = () => { const count = [...new Set(parse().map(u => normalizeUsername(u)).filter(isLikelyUsername))].length; countEl.textContent = tx(`可添加 ${count} 个用户名`, `${count} valid usernames`, `${count} gültige Benutzernamen`, `${count} nombres de usuario válidos`, `${count} nomes de usuário válidos`); }; const cancel = $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')); const apply = $('button', { class: 'ctrl-btn primary', onclick: () => { const r = importUsernameList(parse()); close(); toast(t('manualImportDone', r.added, r.exists)); }, }, tx('添加', 'Add', 'Hinzufügen')); ta.addEventListener('input', update); panel.append(title, hint, ta, countEl, $('div', { class: 'roomgrid-modal-actions' }, [cancel, apply])); backdrop.appendChild(panel); backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(); }); escHandler = (ev) => { if (ev.key === 'Escape') close(); }; document.addEventListener('keydown', escHandler); document.body.appendChild(backdrop); update(); setTimeout(() => ta.focus(), 0); } // 批量添加按钮:只处理用户粘贴的用户名,不再抓取 followed-cams。 const manualImportBtn = $('button', { class: 'ctrl-btn', style: { cursor: 'pointer' }, title: t('manualImport'), onclick: openManualImportPrompt, }, t('manualImport')); // 更多菜单按钮(含语言切换、关于、捐赠) const moreBtn = $('button', { class: 'ctrl-btn multicam-extra-menu', style: { cursor: 'pointer', minWidth: '40px', padding: '6px 12px' }, title: t('hintMoreMenu'), onclick: (e) => openMoreMenu(e.currentTarget), }, t('moreMenu')); // 横向分隔条:使用 var(--border) 而非硬编码 const toolbarGroup = (children, style = {}, compact = false) => $('div', { class: 'toolbar-group' + (compact ? ' compact' : ''), style }, children); const lbl = (text) => $('span', { style: { fontSize: '12px', color: 'var(--text-muted)', fontWeight: '600' } }, text); const discoverBtn = $('button', { class: 'ctrl-btn primary', title: tx('打开发现与个人主页', 'Open Discover and dashboard', 'Entdecken und persönliche Startseite öffnen'), onclick: () => { const url = new URL(location.href); url.searchParams.delete('multicam_mode'); url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', 'discover'); url.hash = ''; location.href = url.toString(); }, }, tx('发现', 'Discover', 'Entdecken')); const multiCamCenterBtn = $('button', { class: 'ctrl-btn', title: tx('流健康度、质量预算、声音、场景和替换模型', 'Stream health, quality budget, audio, scenes, and replacements', 'Stream-Gesundheit, Qualitätsbudget, Audio, Szenen und Ersatzmodels'), 'data-open-multicam-center': '1', onclick: () => openMultiCamCenter(), }, tx('多摄像头控制', 'MultiCam controls', 'MultiCam-Steuerung')); const languageSelect = createLanguageSwitcher('toolbar-language-select'); const openSuiteView = view => { const url = new URL(location.href); url.searchParams.delete('multicam_mode'); url.searchParams.delete('model'); url.searchParams.delete('source'); url.searchParams.set('multicam_discover', '1'); url.searchParams.set('suite_view', view); url.hash = ''; location.href = url.toString(); }; const openMultiCamView = () => { const url = new URL(location.href); for (const key of ['multicam_discover', 'suite_view', 'model', 'source']) url.searchParams.delete(key); url.searchParams.set('multicam_mode', '1'); url.hash = ''; if (url.href !== location.href) location.href = url.toString(); }; const suiteHeaderIdentity = roomGridBrandIdentity(store.state.settings); const suiteHeaderBrand = $('div', { class: 'suite-global-brand' }, [ $('span', { class: 'suite-global-mark' }, roomGridBrandMark(store.state.settings, { size: 27 })), $('span', { class: 'suite-global-brand-copy' }, [ $('strong', { dataset: { roomgridBrandName: '1' } }, suiteHeaderIdentity.name), $('span', { dataset: { roomgridBrandVersion: '1' } }, suiteHeaderIdentity.showVersion ? `v${META.version}` : ''), ]), ]); const suiteHeaderActions = $('div', { class: 'suite-global-actions', dataset: { roomgridHeaderActions: '1' } }, [ createMeterDisplayControl(true), $('button', { class: 'suite-global-button', type: 'button', title: 'Ctrl+K', onclick: () => commandPalette?.open() }, tx('搜索…', 'Search…', 'Suchen…')), $('button', { class: 'suite-global-button', type: 'button', onclick: () => refreshAllBtn.click() }, tx('刷新', 'Refresh', 'Aktualisieren')), createWatchOnlyButton('suite-global-button'), ]); suiteHeaderActions.appendChild(shellControls); const suiteHeaderTop = $('div', { class: 'suite-global-top' }, [suiteHeaderBrand, suiteHeaderActions]); const suiteRouteButton = ([view, label]) => $('button', { type: 'button', dataset: { suiteNav: view }, class: view === 'multicam' ? 'active' : '', ariaCurrent: view === 'multicam' ? 'page' : undefined, onclick: () => view === 'multicam' ? openMultiCamView() : openSuiteView(view) }, label); const suiteMore = createSuiteMore([ suiteRouteButton(['tags', tx('标签浏览器', 'Tag Explorer', 'Tag-Explorer')]), suiteRouteButton(['stats', tx('统计中心', 'Statistics center', 'Statistikzentrum')]), suiteRouteButton(['user', tx('账户与使用', 'Account & usage', 'Konto & Nutzung')]), $('button', { type: 'button', dataset: { suiteNav: 'settings' }, onclick: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'homepage') }, tx('设置', 'Settings', 'Einstellungen')), $('button', { type: 'button', dataset: { camclientHelpButton: '1' }, onclick: () => openCamclientHelp(suiteMore.querySelector('summary'), view => view === 'multicam' ? openMultiCamView() : openSuiteView(view)) }, tx('帮助与版本', 'Help & version', 'Hilfe & Version')), ]); const suiteHeaderNav = $('nav', { class: 'suite-global-nav', ariaLabel: tx('页面', 'Pages', 'Seiten') }, [ ...[ ['dashboard', tx('主页', 'Home', 'Start')], ['multicam', 'MultiCam'], ['discover', tx('发现', 'Discover', 'Entdecken')], ['following', (I18N[LANG]?.followingUi || I18N.en.followingUi).nav], ['library', tx('模型库', 'Model library', 'Modellbibliothek')], ].map(suiteRouteButton), suiteMore, ]); const suiteGlobalHeader = $('div', { class: 'suite-global-header', dataset: { roomgridUnifiedHeader: '1', activeView: 'multicam' } }, [suiteHeaderTop, suiteHeaderNav]); document.body.prepend(suiteGlobalHeader); notificationCenter.mount(suiteHeaderTop, suiteHeaderActions); toolbar.classList.add('multicam-compact-toolbar'); document.head.appendChild($('style', { html: trustedHtml(` .app-shell main header.multicam-compact-toolbar{flex-wrap:wrap!important;overflow:visible!important;gap:6px!important} body.rg-focus-mode .app-shell main header.multicam-compact-toolbar{position:relative!important;inset:auto!important;transform:none!important;opacity:1!important;transition:none!important} .multicam-compact-toolbar .toolbar-group{flex-wrap:wrap!important;max-width:100%;min-width:0;box-sizing:border-box} .multicam-compact-toolbar .ctrl-input{min-width:0;max-width:100%} .multicam-compact-toolbar .multicam-add-room .ctrl-input{width:min(11rem,46vw)!important} .multicam-toolbar-options{min-width:0;max-width:100%;box-sizing:border-box} .multicam-toolbar-options[open]{flex-basis:100%} .multicam-toolbar-options summary{cursor:pointer;list-style-position:inside} .multicam-toolbar-options .toolbar-group{margin-top:6px} .multicam-pagination{display:inline-flex;align-items:center;gap:5px} @media(max-width:700px){ .app-shell .multicam-room-drawer{position:fixed!important;inset:0 auto 0 0!important;height:100dvh!important;z-index:100100!important;box-shadow:0 0 0 100vmax #0006!important} .multicam-compact-toolbar .toolbar-group-title{display:none} .multicam-compact-toolbar .ctrl-btn{white-space:normal} .multicam-compact-toolbar .multicam-extra-menu,.multicam-pagination:has(button:first-child:disabled):has(button:last-child:disabled){display:none} body:not(.rg-toolbar-collapsed):not(.rg-pure-mode):not(.rg-viewer-mode) .suite-global-actions .show-sidebar-btn{display:none!important} } `) })); const addRoomGroup = toolbarGroup([sidebarToggleBtn, tbInput]); addRoomGroup.classList.add('multicam-add-room'); toolbar.append( addRoomGroup, toolbarGroup([viewModeSel, layoutSel, $('span', { class: 'multicam-pagination' }, [pagePrevBtn, pageIndicator, pageNextBtn])], {}, true), toolbarGroup([audioModeSel, $('button', { type: 'button', class: 'ctrl-btn', onclick: () => openMultiCamCenter('scenes') }, tx('场景', 'Scenes', 'Szenen')), multiCamCenterBtn, moreBtn], {}, true), $('details', { class: 'multicam-toolbar-options' }, [ $('summary', { class: 'ctrl-btn' }, tx('视图选项', 'View options', 'Ansichtsoptionen')), toolbarGroup([searchInput, filterSel, sortSel, advancedFilterBtn, videoFitBtn, pureModeBtn, toolbarCollapseBtn, volLabel, volSlider, tempUrlBtn, refreshAllBtn, languageSelect], {}, true), ]), ); function layoutSize() { if (store.state.settings.useCustomLayout) { const shape = layoutShape(); return Math.max(1, Math.min(MAX_LAYOUT_SLOTS, shape.rows * shape.cols)); } const n = Number(store.state.settings.layoutSize || 4); return SUPPORTED_LAYOUT_SIZES.includes(n) ? n : 4; } function layoutShape() { if (store.state.settings.useCustomLayout) { const rows = clampInt(store.state.settings.customLayoutRows, 1, 5, 2); const cols = clampInt(store.state.settings.customLayoutCols, 1, 5, 2); return rows * cols <= MAX_LAYOUT_SLOTS ? { cols, rows } : { cols: Math.max(1, Math.floor(MAX_LAYOUT_SLOTS / rows)), rows }; } const n = layoutSize(); if (n === 2) return { cols: 2, rows: 1 }; if (n === 6) return { cols: 3, rows: 2 }; if (n === 9) return { cols: 3, rows: 3 }; if (n === 12) return { cols: 4, rows: 3 }; if (n === 15) return { cols: 5, rows: 3 }; return { cols: 2, rows: 2 }; } function fullVisibleRooms() { return visibleRooms(); } function hasCustomFocusLayout() { const slots = store.state.settings.focusRoomSlots; return Array.isArray(slots) && slots.length > 0; } function focusSlotsForView(list = fullVisibleRooms()) { if (!hasCustomFocusLayout()) return null; const byId = new Map(list.map(room => [room.id, room])); const raw = Array.isArray(store.state.settings.focusRoomSlots) ? store.state.settings.focusRoomSlots : []; const used = new Set(); return Array.from({ length: layoutSize() }, (_, idx) => { const id = raw[idx]; if (!id || used.has(id)) return null; const room = byId.get(id) || null; if (room) used.add(id); return room; }); } function focusRoomsForView(list = fullVisibleRooms()) { const slots = focusSlotsForView(list); return slots ? slots.filter(Boolean) : pagedRooms(list); } function gridViewContextKey() { const s = store.state.settings; const f = s.filter || {}; return JSON.stringify({ group: s.activeGroup || DEFAULT_GROUP_ID, query: normalizeUsername(s.searchQuery || ''), filter: { hideOffline: !!f.hideOffline, hidePrivate: !!f.hidePrivate, onlyOnline: !!f.onlyOnline, onlyWatched: !!f.onlyWatched, neverWatched: !!f.neverWatched, favoritesOnly: !!f.favoritesOnly, followedOnly: !!f.followedOnly, inActiveView: !!f.inActiveView, hasNotes: !!f.hasNotes, tagInclude: String(f.tagInclude || ''), tagExclude: String(f.tagExclude || ''), minWatchMs: Number(f.minWatchMs) || 0, minSessions: Number(f.minSessions) || 0, lastWatchedDays: Number(f.lastWatchedDays) || 0, viewerMin: Number(f.viewerMin) || 0, viewerMax: Number(f.viewerMax) || 0, }, recording: !!s.showRecordingOnly, favoriteFirst: s.favoriteFirst !== false, sort: s.sortBy || 'manual', layout: layoutSize(), ...(s.useCustomLayout ? { customRows: layoutShape().rows, customCols: layoutShape().cols } : {}), page: clampInt(s.pageIndex, 0, 100000, 0), }); } function gridRoomsForView(baseList = pagedRooms(fullVisibleRooms())) { const contextKey = gridViewContextKey(); const savedMap = store.state.settings.gridRoomSlots || {}; // Versions through 15.9.20 accidentally shortened context keys to 220 // characters although even the empty default context is longer. Accept // that legacy prefix once so fixed slot assignments remain recoverable. const saved = savedMap[contextKey] || Object.entries(savedMap).find(([key]) => key.length === 220 && contextKey.startsWith(key))?.[1]; if (!Array.isArray(saved)) return baseList; const roomMap = new Map(allRoomsForView().map(room => [room.id, room])); const used = new Set(); return Array.from({ length: layoutSize() }, (_, idx) => { const id = normalizeUsername(saved[idx] || ''); if (!id || used.has(id)) return null; const room = roomMap.get(id) || null; if (room) used.add(id); return room; }); } function focusSecondaryShape(size = layoutSize()) { const secondary = Math.max(0, size - 1); const side = secondary <= 1 ? secondary : Math.min(5, Math.ceil(secondary / 2)); return { side, bottom: secondary - side }; } function focusSlotLabel(slotIndex, size = layoutSize(), sideCount = focusSecondaryShape(size).side) { const idx = Number(slotIndex); if (!Number.isInteger(idx) || idx < 0 || idx >= size) return t('focusRoomEmpty'); if (idx === 0) return t('focusRoomMain'); if (idx <= sideCount) return `${t('focusRoomRight')} ${idx}`; return `${t('focusRoomBottom')} ${idx - sideCount}`; } function focusSlotIdsForCurrentLayout() { if (store.state.settings.viewMode !== 'focus') return []; const size = layoutSize(); const full = fullVisibleRooms(); if (hasCustomFocusLayout()) { const raw = Array.isArray(store.state.settings.focusRoomSlots) ? store.state.settings.focusRoomSlots : []; return Array.from({ length: size }, (_, idx) => raw[idx] || null); } const list = pagedRooms(full); const ids = Array.from({ length: size }, () => null); let focusedId = store.state.settings.focusedRoomId; if (!list.some(room => room.id === focusedId)) focusedId = (list.find(room => room.lastStatus === 'online') || list[0])?.id || null; ids[0] = focusedId; const secondary = list.filter(room => room.id !== focusedId); const sideCount = focusSecondaryShape(Math.max(1, list.length)).side; secondary.forEach((room, idx) => { const slot = idx < sideCount ? idx + 1 : sideCount + idx + 1; if (slot < size) ids[slot] = room.id; }); return ids; } function focusSlotEntriesForSidebar() { const size = layoutSize(); const full = fullVisibleRooms(); if (store.state.settings.viewMode === 'grid') { return gridRoomsForView(pagedRooms(full)) .map((room, idx) => room ? ({ id: room.id, label: t('gridWindowLabel', idx + 1), }) : null) .filter(Boolean); } if (store.state.settings.viewMode !== 'focus') return []; const currentIds = focusSlotIdsForCurrentLayout(); const visibleIds = hasCustomFocusLayout() ? (focusSlotsForView(full) || []).map(room => room?.id || null) : currentIds; const sideCount = hasCustomFocusLayout() ? focusSecondaryShape(size).side : focusSecondaryShape(Math.max(1, pagedRooms(full).length)).side; return visibleIds.map((id, idx) => ({ id, label: focusSlotLabel(idx, size, sideCount) })); } function focusSlotLabelsForRoom(roomId) { const normalized = normalizeUsername(roomId); if (!normalized) return []; return sidebarSlotLabels.get(normalized) || []; } function maxPageFor(total = fullVisibleRooms().length) { return Math.max(0, Math.ceil(total / layoutSize()) - 1); } function clampCurrentPage(total = fullVisibleRooms().length) { const max = maxPageFor(total); const cur = Math.max(0, Math.min(max, Number(store.state.settings.pageIndex || 0))); if (cur !== store.state.settings.pageIndex) store.state.settings.pageIndex = cur; return cur; } function pagedRooms(list = fullVisibleRooms()) { const page = clampCurrentPage(list.length); const size = layoutSize(); return list.slice(page * size, page * size + size); } function setPageIndex(n) { const max = maxPageFor(); store.patchSettings({ pageIndex: Math.max(0, Math.min(max, Number(n) || 0)) }); } function syncLayoutControls() { const size = layoutSize(); layoutSel.value = store.state.settings.useCustomLayout ? 'custom' : String(size); const total = fullVisibleRooms().length; const max = maxPageFor(total); const page = clampCurrentPage(total); if (hasCustomFocusLayout() && store.state.settings.viewMode === 'focus') { const selected = store.state.settings.focusRoomSlots.filter(Boolean).length; pageIndicator.textContent = `${t('focusCustomIndicator')} · ${selected}/${layoutSize()}`; pageIndicator.title = t('focusRoomManagerHint'); pagePrevBtn.disabled = true; pageNextBtn.disabled = true; } else { pageIndicator.textContent = `${page + 1} / ${max + 1}`; pageIndicator.title = tx(`当前页 ${page + 1} / ${max + 1},共 ${total} 个窗口`, `Page ${page + 1} / ${max + 1}, ${total} rooms`, `Seite ${page + 1} / ${max + 1}, ${total} Räume`, `Página ${page + 1} / ${max + 1}, ${total} salas`, `Página ${page + 1} / ${max + 1}, ${total} salas`); pagePrevBtn.disabled = page <= 0; pageNextBtn.disabled = page >= max; } } function applyGridSize() { const mode = store.state.settings.viewMode; if (mode === 'focus') { const w = Math.max(45, Math.min(76, Number(store.state.settings.focusMainPct || 62))); const h = Math.max(44, Math.min(78, Number(store.state.settings.focusMainHPct || 64))); grid.style.display = 'grid'; grid.style.gridTemplateColumns = `minmax(260px, ${w}fr) 8px minmax(190px, ${100 - w}fr)`; grid.style.gridTemplateRows = `minmax(220px, ${h}fr) 8px minmax(120px, ${100 - h}fr)`; grid.style.gridTemplateAreas = `'main vbar side' 'hbar hbar side' 'bottom bottom side'`; grid.style.gap = '8px'; grid.style.alignItems = 'stretch'; grid.style.alignContent = 'stretch'; grid.style.overflow = 'hidden'; grid.style.setProperty('--focus-main-w', w + 'fr'); grid.style.setProperty('--focus-side-w', (100 - w) + 'fr'); grid.style.setProperty('--focus-main-h', h + 'fr'); grid.style.setProperty('--focus-bottom-h', (100 - h) + 'fr'); requestAnimationFrame(applyFocusMainSizing); return; } const shape = layoutShape(); grid.style.display = 'grid'; grid.style.gridTemplateAreas = ''; grid.style.gridTemplateColumns = `repeat(${shape.cols}, minmax(0, 1fr))`; grid.style.gridTemplateRows = `repeat(${shape.rows}, minmax(0, 1fr))`; grid.style.gridAutoRows = ''; grid.style.gridAutoFlow = 'row'; grid.style.gap = '8px'; grid.style.alignItems = 'stretch'; grid.style.alignContent = 'stretch'; grid.style.overflow = 'hidden'; } function gridMetrics() { const base = clampInt(store.state.settings.gridCellSize || 80, 56, 120, 80); const row = Math.max(32, Math.round(base * 9 / 16)); return { base, row }; } function defaultTileSize() { const { base, row } = gridMetrics(); const target = clampInt(store.state.settings.gridSize || 400, 220, 900, 400); const cols = clampInt(Math.round(target / base), 3, 18, 5); const rows = clampInt(Math.round((cols * base * 9 / 16) / row), 3, 18, cols); return { cols, rows }; } function tileSizeForRoom(room) { const groupId = store.state.settings.activeGroup || DEFAULT_GROUP_ID; const map = normalizeCardSizeMap(room?.cardSizeByGroup); return normalizeCardSize(map[groupId]) || defaultTileSize(); } function applyCardGridSizing(card, room) { if (!card) return; card.classList.remove('is-focus-main'); card.style.gridColumn = ''; card.style.gridRow = ''; card.style.width = '100%'; card.style.height = '100%'; card.style.maxWidth = ''; card.style.maxHeight = ''; card.style.aspectRatio = 'auto'; } function parseFocusAspect() { const v = store.state.settings.focusAspect || 'auto'; if (v === 'auto') return null; if (v === '4:3') return 4 / 3; if (v === '1:1') return 1; if (v === '9:16') return 9 / 16; return 16 / 9; } function resetCardSizing(card) { if (!card) return; card.classList.remove('is-focus-main'); card.style.width = ''; card.style.height = ''; card.style.flex = ''; card.style.margin = ''; card.style.maxWidth = ''; card.style.maxHeight = ''; card.style.aspectRatio = '16 / 9'; card.style.gridColumn = ''; card.style.gridRow = ''; } function applyFocusMainSizing() { if (store.state.settings.viewMode !== 'focus') return; const row = grid.querySelector('.focused-row'); const card = row && row.querySelector('.cam-card'); if (!row || !card) return; card.classList.add('is-focus-main'); card.style.width = '100%'; card.style.height = '100%'; card.style.maxWidth = '100%'; card.style.maxHeight = '100%'; card.style.flex = '1 1 auto'; card.style.aspectRatio = 'auto'; } window.addEventListener('resize', debounce(applyFocusMainSizing, 120)); // v15.9.8: kurze, stumme Livevorschau beim Hover über einen Sidebar-Eintrag. // Der Player bleibt bewusst außerhalb von cardMap und RoomService-Sessions, // damit bestehende Cam-Karten, Audio und Workspace-Daten unberührt bleiben. const SIDEBAR_PREVIEW_DELAY_MS = 320; const SIDEBAR_PREVIEW_CLOSE_GRACE_MS = 160; const SIDEBAR_PREVIEW_SOURCE_TTL_MS = 60000; const SIDEBAR_PREVIEW_SOURCE_CACHE_MAX = 80; const SIDEBAR_PREVIEW_FAILURE_BASE_MS = 12000; const SIDEBAR_PREVIEW_FAILURE_MAX_MS = 60000; let sidebarPreviewTimer = null; let sidebarPreviewCloseTimer = null; let sidebarPreviewAnchor = null; let sidebarPreview = null; let sidebarPreviewSerial = 0; let sidebarRenderDeferredForPreview = false; const sidebarPreviewSourceCache = new Map(); const sidebarPreviewFailures = new Map(); function clearSidebarPreviewTimer() { if (sidebarPreviewTimer) clearTimeout(sidebarPreviewTimer); sidebarPreviewTimer = null; } function clearSidebarPreviewCloseTimer() { if (sidebarPreviewCloseTimer) clearTimeout(sidebarPreviewCloseTimer); sidebarPreviewCloseTimer = null; } function cachedSidebarPreviewSource(roomId) { roomId = normalizeUsername(roomId); const cached = sidebarPreviewSourceCache.get(roomId); if (!cached) return ''; if (cached.expiresAt <= Date.now() || !isSafeStreamUrl(cached.source)) { sidebarPreviewSourceCache.delete(roomId); return ''; } // Map insertion order doubles as a tiny LRU list. sidebarPreviewSourceCache.delete(roomId); sidebarPreviewSourceCache.set(roomId, cached); return cached.source; } function rememberSidebarPreviewSource(roomId, source) { roomId = normalizeUsername(roomId); if (!roomId || !isSafeStreamUrl(source)) return; sidebarPreviewSourceCache.delete(roomId); sidebarPreviewSourceCache.set(roomId, { source, expiresAt: Date.now() + SIDEBAR_PREVIEW_SOURCE_TTL_MS }); while (sidebarPreviewSourceCache.size > SIDEBAR_PREVIEW_SOURCE_CACHE_MAX) { sidebarPreviewSourceCache.delete(sidebarPreviewSourceCache.keys().next().value); } } function clearSidebarPreviewFailure(roomId) { sidebarPreviewFailures.delete(normalizeUsername(roomId)); } function rememberSidebarPreviewFailure(roomId) { roomId = normalizeUsername(roomId); if (!roomId) return; const previous = sidebarPreviewFailures.get(roomId) || { attempts: 0, retryAt: 0 }; const attempts = Math.min(4, Number(previous.attempts || 0) + 1); const wait = Math.min(SIDEBAR_PREVIEW_FAILURE_MAX_MS, SIDEBAR_PREVIEW_FAILURE_BASE_MS * Math.pow(1.7, attempts - 1)); sidebarPreviewFailures.set(roomId, { attempts, retryAt: Date.now() + wait }); while (sidebarPreviewFailures.size > SIDEBAR_PREVIEW_SOURCE_CACHE_MAX) sidebarPreviewFailures.delete(sidebarPreviewFailures.keys().next().value); } function sidebarPreviewCoolingDown(roomId) { const state = sidebarPreviewFailures.get(normalizeUsername(roomId)); if (!state) return false; if (Number(state.retryAt) <= Date.now()) return false; return true; } function destroySidebarPreviewHls(preview) { if (!preview?.hls) return; try { preview.hls.stopLoad?.(); } catch (_) {} try { preview.hls.detachMedia?.(); } catch (_) {} try { preview.hls.destroy?.(); } catch (_) {} preview.hls = null; } function clearSidebarPreviewMediaTimers(preview) { if (!preview) return; for (const key of ['loadTimer', 'stallTimer', 'recoveryTimer']) { if (preview[key]) clearTimeout(preview[key]); preview[key] = null; } } function setSidebarPreviewStatus(preview, message, loading = false) { if (!preview?.statusEl || sidebarPreview !== preview) return; preview.statusText.textContent = String(message || ''); preview.statusEl.classList.toggle('is-loading', !!loading); preview.statusEl.classList.remove('is-hidden'); } function hideSidebarPreviewStatus(preview) { if (!preview?.statusEl || sidebarPreview !== preview) return; clearSidebarPreviewMediaTimers(preview); preview.statusEl.classList.remove('is-loading'); preview.statusEl.classList.add('is-hidden'); preview.element?.classList.add('is-playing'); } function positionSidebarPreview(preview) { if (!preview?.element || !preview.anchor?.isConnected) return; const rect = preview.anchor.getBoundingClientRect(); const width = preview.element.offsetWidth || 278; const height = preview.element.offsetHeight || 190; const gap = 10; let left = rect.right + gap; if (left + width > window.innerWidth - 8) left = rect.left - width - gap; if (left < 8) left = Math.max(8, Math.min(window.innerWidth - width - 8, rect.right + gap)); let top = rect.top; if (top + height > window.innerHeight - 8) top = window.innerHeight - height - 8; top = Math.max(8, top); preview.element.style.left = `${Math.round(left)}px`; preview.element.style.top = `${Math.round(top)}px`; } function closeSidebarPreview(options = {}) { const flushSidebar = options.flushSidebar !== false; clearSidebarPreviewTimer(); clearSidebarPreviewCloseTimer(); sidebarPreviewAnchor = null; const preview = sidebarPreview; sidebarPreview = null; sidebarPreviewSerial += 1; if (preview) { clearSidebarPreviewMediaTimers(preview); try { preview.abortController?.abort(); } catch (_) {} destroySidebarPreviewHls(preview); if (preview.video) stopMediaElement(preview.video, true); try { preview.element?.remove(); } catch (_) {} } if (flushSidebar && sidebarRenderDeferredForPreview) { sidebarRenderDeferredForPreview = false; requestAnimationFrame(() => scheduleSidebarRender()); } } function showSidebarPreviewUnavailable(preview, message) { if (!preview || sidebarPreview !== preview) return; rememberSidebarPreviewFailure(preview.id); clearSidebarPreviewMediaTimers(preview); destroySidebarPreviewHls(preview); if (preview.video) { stopMediaElement(preview.video, true); try { preview.video.remove(); } catch (_) {} preview.video = null; } preview.element?.classList.remove('is-playing'); setSidebarPreviewStatus(preview, message || t('previewUnavailable'), false); } function playSidebarPreview(preview) { if (!preview?.video || sidebarPreview !== preview) return; preview.video.play().catch(() => showSidebarPreviewUnavailable(preview, t('previewUnavailable'))); } function startSidebarPreviewSource(preview, source) { if (!preview || sidebarPreview !== preview || !isSafeStreamUrl(source)) { if (preview && sidebarPreview === preview) showSidebarPreviewUnavailable(preview, t('previewUnavailable')); return; } rememberSidebarPreviewSource(preview.id, source); const video = $('video', { class: 'sidebar-preview-video', autoplay: true, muted: true, playsInline: true, preload: 'metadata', controls: false, draggable: false, dataset: { multicamPreviewRoomId: preview.id }, }); preview.video = video; preview.mediaEl.insertBefore(video, preview.statusEl); preview.loadTimer = setTimeout(() => { if (sidebarPreview === preview && !preview.hasPlayed) showSidebarPreviewUnavailable(preview, t('previewUnavailable')); }, 12000); video.addEventListener('playing', () => { preview.hasPlayed = true; preview.recovering = false; clearSidebarPreviewFailure(preview.id); hideSidebarPreviewStatus(preview); }); video.addEventListener('waiting', () => { if (sidebarPreview !== preview || !preview.hasPlayed || preview.stallTimer) return; preview.stallTimer = setTimeout(() => { preview.stallTimer = null; if (sidebarPreview === preview && video.readyState < 3) setSidebarPreviewStatus(preview, tx( '正在稳定实时预览…', 'Stabilizing live preview…', 'Live-Vorschau wird stabilisiert…' ), true); }, 1200); }); video.addEventListener('error', () => { if (!preview.hls) showSidebarPreviewUnavailable(preview, t('previewError')); }); const HlsCtor = window.Hls; if (HlsCtor && HlsCtor.isSupported()) { const hls = new HlsCtor({ liveDurationInfinity: true, lowLatencyMode: true, capLevelToPlayerSize: true, startLevel: 0, maxBufferLength: 4, maxMaxBufferLength: 8, backBufferLength: 0, liveSyncDurationCount: 2, liveMaxLatencyDurationCount: 5, manifestLoadingMaxRetry: 1, levelLoadingMaxRetry: 1, fragLoadingMaxRetry: 1, }); preview.hls = hls; hls.loadSource(source); hls.attachMedia(video); hls.on(HlsCtor.Events.MANIFEST_PARSED, () => { if (sidebarPreview !== preview || preview.hls !== hls) return; const levels = Array.isArray(hls.levels) ? hls.levels : []; let cap = -1; let capHeight = 0; levels.forEach((level, index) => { const height = Number(level?.height) || 0; if (height && height <= 480 && height >= capHeight) { cap = index; capHeight = height; } }); if (cap >= 0) { try { hls.autoLevelCapping = cap; } catch (_) {} } playSidebarPreview(preview); }); hls.on(HlsCtor.Events.ERROR, (_, data) => { if (sidebarPreview !== preview || preview.hls !== hls || !data?.fatal) return; if (preview.recovering || preview.recoveryAttempts >= 1) { sidebarPreviewSourceCache.delete(preview.id); showSidebarPreviewUnavailable(preview, t('previewError')); return; } preview.recoveryAttempts += 1; preview.recovering = true; setSidebarPreviewStatus(preview, tx( '正在恢复实时预览…', 'Recovering live preview…', 'Live-Vorschau wird wiederhergestellt…' ), true); try { if (data.type === HlsCtor.ErrorTypes?.MEDIA_ERROR) hls.recoverMediaError?.(); else hls.startLoad?.(); } catch (_) {} preview.recoveryTimer = setTimeout(() => { preview.recoveryTimer = null; preview.recovering = false; if (sidebarPreview !== preview) return; if (video.readyState >= 3 && !video.paused) hideSidebarPreviewStatus(preview); else { sidebarPreviewSourceCache.delete(preview.id); showSidebarPreviewUnavailable(preview, t('previewError')); } }, 6500); }); return; } if (video.canPlayType('application/vnd.apple.mpegurl') || video.canPlayType('video/mp4')) { video.src = source; playSidebarPreview(preview); return; } showSidebarPreviewUnavailable(preview, t('previewUnavailable')); } function createSidebarPreview(room, anchor) { const name = room.displayName || room.id; const statusText = $('span', {}, t('previewLoading')); const statusEl = $('div', { class: 'sidebar-preview-status is-loading', role: 'status' }, [ $('span', { class: 'sidebar-preview-spinner' }), statusText, ]); const previewImage = $('img', { class: 'sidebar-preview-image', src: `https://thumb.live.mmcdn.com/riw/${encodeURIComponent(room.id)}.jpg`, alt: '', loading: 'eager', decoding: 'async', referrerPolicy: 'strict-origin-when-cross-origin', }); const mediaEl = $('div', { class: 'sidebar-preview-media' }, [previewImage, statusEl]); const element = $('div', { class: 'sidebar-preview', dataset: { roomId: room.id } }, [ $('div', { class: 'sidebar-preview-head' }, [ $('span', { class: 'sidebar-preview-name' }, rawUiText(name)), $('span', { class: 'sidebar-preview-live' }, t('previewLive')), ]), mediaEl, ]); const preview = { id: room.id, anchor, element, mediaEl, statusEl, statusText, video: null, hls: null, previewImage, abortController: new AbortController(), serial: ++sidebarPreviewSerial, hasPlayed: false, recoveryAttempts: 0, recovering: false, loadTimer: null, stallTimer: null, recoveryTimer: null, }; sidebarPreview = preview; sidebarPreviewAnchor = anchor; document.body.appendChild(element); positionSidebarPreview(preview); requestAnimationFrame(() => { if (sidebarPreview === preview) { positionSidebarPreview(preview); element.classList.add('is-visible'); } }); return preview; } function canOpenSidebarPreview(anchor) { if (!anchor?.isConnected || document.hidden || document.body.classList.contains('rg-pure-mode')) return false; return sidebar.contains(anchor) ? store.state.settings.sidebarHoverPreview !== false && !sidebarIsHidden() : !watchOnlyMode; } async function openSidebarPreview(room, anchor) { if (!room?.id || !canOpenSidebarPreview(anchor)) return; if (sidebarPreview?.id === normalizeUsername(room.id) && sidebarPreview.anchor === anchor) { positionSidebarPreview(sidebarPreview); return; } closeSidebarPreview({ flushSidebar: false }); const currentRoom = findRoomAny(room.id) || room; const preview = createSidebarPreview(currentRoom, anchor); if (sidebarPreviewCoolingDown(preview.id)) { setSidebarPreviewStatus(preview, tx( '实时预览短暂暂停,以防止重新加载循环。请稍后再试。', 'Live preview is briefly paused to prevent a reload loop. Try again shortly.', 'Die Live-Vorschau pausiert kurz, um eine Nachladeschleife zu verhindern. Gleich erneut versuchen.' ), false); return; } const sourceUrl = currentRoom.sourceUrl; if (sourceUrl) { startSidebarPreviewSource(preview, sourceUrl); return; } const cachedSource = cachedSidebarPreviewSource(preview.id); if (cachedSource) { startSidebarPreviewSource(preview, cachedSource); return; } let data; try { data = await service.fetchContext(preview.id, preview.abortController.signal); } catch (_) { if (sidebarPreview === preview && !preview.abortController.signal.aborted) showSidebarPreviewUnavailable(preview, t('previewError')); return; } if (sidebarPreview !== preview || preview.abortController.signal.aborted) return; if (data?.room_status === 'offline') { showSidebarPreviewUnavailable(preview, t('previewOffline')); return; } if (data?.room_status === 'private' || data?.room_status === 'hidden' || data?.room_status === 'away') { showSidebarPreviewUnavailable(preview, t('previewPrivate')); return; } if (!isSafeStreamUrl(data?.hls_source)) { showSidebarPreviewUnavailable(preview, t('previewUnavailable')); return; } startSidebarPreviewSource(preview, data.hls_source); } function scheduleSidebarPreview(room, anchor) { if (isRoomDragActive() || Date.now() < suppressSidebarClickUntil) return; if (!room?.id || !canOpenSidebarPreview(anchor)) return; clearSidebarPreviewCloseTimer(); // Auch ein bereits für den nächsten Frame vorgemerkter Listen-Neuaufbau // darf nicht zwischen Hover-Verzögerung und Player-Start hineinlaufen. if (sidebarRenderRaf) { cancelAnimationFrame(sidebarRenderRaf); sidebarRenderRaf = 0; sidebarRenderDeferredForPreview = true; } const normalizedId = normalizeUsername(room.id); if (sidebarPreview?.id === normalizedId) { sidebarPreview.anchor = anchor; sidebarPreviewAnchor = anchor; positionSidebarPreview(sidebarPreview); return; } if (sidebarPreviewTimer && sidebarPreviewAnchor === anchor) return; closeSidebarPreview({ flushSidebar: false }); sidebarPreviewAnchor = anchor; sidebarPreviewTimer = setTimeout(() => { sidebarPreviewTimer = null; if (sidebarPreviewAnchor === anchor) openSidebarPreview(room, anchor); }, SIDEBAR_PREVIEW_DELAY_MS); } function cancelSidebarPreview(anchor) { if (sidebarPreviewAnchor !== anchor && sidebarPreview?.anchor !== anchor) return; clearSidebarPreviewTimer(); clearSidebarPreviewCloseTimer(); sidebarPreviewCloseTimer = setTimeout(() => { sidebarPreviewCloseTimer = null; if (sidebarPreviewAnchor === anchor || sidebarPreview?.anchor === anchor) closeSidebarPreview(); }, SIDEBAR_PREVIEW_CLOSE_GRACE_MS); } // Scrolling into a long list can fire after pointerenter. Closing its // pending preview then leaves the stationary pointer without another // enter event. Re-evaluate the hovered source after scrolling settles. let sidebarScrollPreviewTimer = 0; sidebar.addEventListener('scroll', () => { closeSidebarPreview(); clearTimeout(sidebarScrollPreviewTimer); sidebarScrollPreviewTimer = setTimeout(() => { sidebarScrollPreviewTimer = 0; if (document.hidden || isRoomDragActive()) return; const anchor = sidebar.querySelector('.group-user-item:hover'); const room = anchor && findRoomAny(anchor.dataset.sidebarRoomId); if (room) scheduleSidebarPreview(room, anchor); }, 120); }, { passive: true }); window.addEventListener('scroll', closeSidebarPreview, { passive: true }); window.addEventListener('resize', () => { if (sidebarPreview) positionSidebarPreview(sidebarPreview); }); document.addEventListener('visibilitychange', () => { if (document.hidden) closeSidebarPreview(); }); window.addEventListener('pagehide', () => { clearTimeout(sidebarScrollPreviewTimer); closeSidebarPreview(); }); // Sidebar browsing is independent of camera filtering/layout. Account rows // stay transient until the user explicitly adds or places one camera. let sidebarFollowing = RoomGridNativeHub.followingSnapshot(); let sidebarFollowingRequested = false; let sidebarAccountRooms = new Map(); const sidebarPages = new Map(); let sidebarTagOptions = []; let sidebarCountryNames = null; let sidebarSlotLabels = new Map(); try { sidebarCountryNames = new Intl.DisplayNames([LANG], { type: 'region' }); } catch (_) {} const sidebarCountryName = value => { try { return sidebarCountryNames?.of(String(value || '').toUpperCase()) || value || ''; } catch (_) { return value || ''; } }; const sidebarTagTokens = value => [...new Set(String(value || '').toLocaleLowerCase().split(/[,;\s]+/u).map(tag => tag.replace(/^#/u, '').trim()).filter(Boolean))]; function sidebarModels() { const following = new Map(sidebarFollowing.rows.map(row => [row.id, row])); const rooms = new Map(allRoomsForView().map(room => [room.id, room])); for (const row of following.values()) if (!rooms.has(row.id)) rooms.set(row.id, { id: row.id, lastStatus: 'unknown', muted: true }); const result = new Map(); sidebarAccountRooms = new Map(); for (const room of rooms.values()) { const model = analytics.getModelSync(room.id) || {}; const row = following.get(room.id); const metadata = Object.fromEntries(Object.entries(row?.metadata || {}).filter(([, value]) => value != null && value !== '' && !(Array.isArray(value) && !value.length))); const tags = [...new Set([...(model.tags || []), ...(metadata.tags || []), ...(room.localTags || [])].map(tag => String(tag).toLocaleLowerCase().replace(/^#/u, '')))]; const newerStatus = Number(room.fieldModifiedAt?.lastStatus || 0) > Number(sidebarFollowing.updatedAt || 0); const status = row && !newerStatus ? (row.status === 'live' ? 'online' : 'offline') : room.lastStatus || 'unknown'; const item = { ...model, ...metadata, ...room, tags, country: metadata.country || model.country || '', age: metadata.age || model.age || 0, followers: metadata.followers ?? model.followers ?? 0, secondsOnline: metadata.secondsOnline ?? model.secondsOnline ?? 0, viewerCount: newerStatus ? room.viewerCount ?? metadata.viewerCount ?? model.viewerCount : metadata.viewerCount ?? room.viewerCount ?? model.viewerCount, lastStatus: status, totalMs: model.totalMs || 0, sessions: model.sessions || 0, lastWatchedAt: model.lastWatchedAt || 0 }; result.set(room.id, item); if (row) sidebarAccountRooms.set(room.id, item); } sidebarTagOptions = [...new Set([...result.values()].flatMap(room => room.tags))].sort(); return result; } function filteredSidebarRooms(rooms, groupId = '') { const prefs = store.state.settings.sidebarBrowser; const include = sidebarTagTokens(prefs.tags), exclude = sidebarTagTokens(prefs.excludeTags); const query = prefs.query.trim().toLocaleLowerCase(), country = prefs.country.trim().toLocaleLowerCase(); const filtered = rooms.filter(room => { if (prefs.status !== 'all' && room.lastStatus !== prefs.status) return false; if (prefs.gender !== 'all' && room.gender !== prefs.gender) return false; if (country && !`${room.country} ${sidebarCountryName(room.country)}`.toLocaleLowerCase().includes(country)) return false; if ((prefs.ageMin || prefs.ageMax) && (room.age < 18 || (prefs.ageMin && room.age < prefs.ageMin) || (prefs.ageMax && room.age > prefs.ageMax))) return false; if ((prefs.viewerMin || prefs.viewerMax) && (room.viewerCount == null || (prefs.viewerMin && room.viewerCount < prefs.viewerMin) || (prefs.viewerMax && room.viewerCount > prefs.viewerMax))) return false; const tags = new Set(room.tags); if (include.length && !(prefs.tagMode === 'any' ? include.some(tag => tags.has(tag)) : include.every(tag => tags.has(tag)))) return false; if (exclude.some(tag => tags.has(tag))) return false; if (prefs.local === 'favorites' && !roomInGroup(room, FAVORITE_GROUP_ID)) return false; if (prefs.local === 'watched' && !room.totalMs) return false; if (prefs.local === 'unwatched' && room.totalMs > 0) return false; if (prefs.local === 'notes' && !String(room.notes || '').trim()) return false; return !query || `${room.id} ${room.displayName || ''} ${room.country} ${room.tags.join(' ')} ${room.notes || ''}`.toLocaleLowerCase().includes(query); }); const number = (room, key) => Number(room[key]) || 0; return filtered.sort((a, b) => { let order = 0; if (prefs.sort === 'manual') order = groupId ? roomOrderInGroup(a, groupId) - roomOrderInGroup(b, groupId) : 0; else if (prefs.sort === 'name_desc') order = b.id.localeCompare(a.id); else if (prefs.sort === 'live_first') order = Number(b.lastStatus === 'online') - Number(a.lastStatus === 'online'); else if (prefs.sort === 'country') order = String(a.country).localeCompare(String(b.country)); else { const numericSorts = { viewers: ['viewerCount', -1], viewers_low: ['viewerCount', 1], age_young: ['age', 1], age_old: ['age', -1], rating: ['rating', -1], priority: ['priority', -1], watch: ['totalMs', -1], sessions: ['sessions', -1], recent: ['lastWatchedAt', -1], added: ['addedAt', -1], followers: ['followers', -1], duration: ['secondsOnline', -1] }; const spec = numericSorts[prefs.sort]; if (spec) { const [key, direction] = spec; const knownA = a[key] != null && (key !== 'age' || a.age >= 18), knownB = b[key] != null && (key !== 'age' || b.age >= 18); order = knownA !== knownB ? Number(knownB) - Number(knownA) : direction * (number(a, key) - number(b, key)); } } return order || a.id.localeCompare(b.id); }); } function ensureSidebarRoom(id) { if (store.state.rooms.some(room => room.id === id) || tempRooms.some(room => room.id === id)) return true; if (!sidebarAccountRooms.has(id)) return false; store.addRoom(id); return store.state.rooms.some(room => room.id === id); } function installSidebarBrowser() { if (sidebar.querySelector('.sidebar-browser')) return; const ui = I18N[LANG]?.followingUi || I18N.en.followingUi; const panel = $('div', { class: 'sidebar-browser' }); const apply = debounce(() => { closeSidebarPreview({ flushSidebar: false }); const next = { ...store.state.settings.sidebarBrowser }; panel.querySelectorAll('[data-sidebar-filter]').forEach(input => { next[input.dataset.sidebarFilter] = input.type === 'number' ? Number(input.value) || 0 : input.value; }); sidebarPages.clear(); store.patchSettings({ sidebarBrowser: next }); }, 160); const field = (key, label, options = null, type = 'text') => { const input = options ? $('select', {}, options.map(([value, text]) => $('option', { value }, text))) : $('input', { type, min: type === 'number' ? 0 : undefined, max: key.startsWith('age') ? 99 : undefined }); input.dataset.sidebarFilter = key; input.value = store.state.settings.sidebarBrowser[key]; input.addEventListener(options ? 'change' : 'input', apply); if (key === 'tags' || key === 'excludeTags') { const listId = `sidebar-${key}-suggestions`; const list = $('datalist', { id: listId }); input.setAttribute('list', listId); input.addEventListener('input', () => { const split = input.value.lastIndexOf(','); const prefix = split >= 0 ? input.value.slice(0, split + 1) + ' ' : ''; const term = input.value.slice(split + 1).trim().replace(/^#/u, '').toLocaleLowerCase(); list.replaceChildren(...sidebarTagOptions.filter(tag => tag.startsWith(term)).slice(0, 60).map(tag => $('option', { value: prefix + tag }))); }); panel.appendChild(list); } return $('label', { class: 'sidebar-filter-field' }, [$('span', {}, label), input]); }; panel.append(field('query', tx('搜索左侧列表', 'Search sidebar', 'Linke Liste durchsuchen'))); panel.append(field('status', ui.status, [['all', ui.all], ['online', ui.live], ['offline', ui.offline], ['private', tx('私密', 'Private', 'Privat')], ['unknown', tx('未知', 'Unknown', 'Unbekannt')]])); panel.append($('button', { class: 'btn btn-sm', onclick: () => { panel.querySelector('[data-sidebar-filter="status"]').value = 'online'; apply(); } }, tx('仅在线', 'Online only', 'Nur online'))); const sorts = [['manual', tx('分组顺序', 'Group order', 'Gruppenreihenfolge')], ['name', ui.sortName], ['name_desc', 'Name Z–A'], ['live_first', tx('在线优先', 'Live first', 'Live zuerst')], ['viewers', tx('最多观众', 'Most viewers', 'Meiste Zuschauer')], ['viewers_low', tx('最少观众', 'Fewest viewers', 'Wenigste Zuschauer')], ['age_young', tx('年龄升序', 'Age ascending', 'Alter aufsteigend')], ['age_old', tx('年龄降序', 'Age descending', 'Alter absteigend')], ['country', tx('国家', 'Country', 'Land')], ['rating', tx('评分', 'Rating', 'Sternebewertung')], ['priority', tx('优先级', 'Priority', 'Priorität')], ['watch', tx('观看时间', 'Watch time', 'Sehzeit')], ['sessions', tx('会话', 'Sessions', 'Sitzungen')], ['recent', tx('最近观看', 'Last watched', 'Zuletzt angesehen')], ['added', tx('最近添加', 'Recently added', 'Zuletzt hinzugefügt')], ['followers', tx('关注人数', 'Followers', 'Followerzahl')], ['duration', tx('在线时长', 'Online duration', 'Online-Dauer')]]; panel.append(field('sort', tx('排序', 'Sort', 'Sortierung'), sorts)); const details = $('details', { class: 'sidebar-filter-details' }, $('summary', {}, tx('更多筛选', 'More filters', 'Weitere Filter'))); details.open = store.state.settings.sidebarBrowser.expanded; details.addEventListener('toggle', () => { if (details.open !== store.state.settings.sidebarBrowser.expanded) store.patchSettings({ sidebarBrowser: { ...store.state.settings.sidebarBrowser, expanded: details.open } }); }); details.append(field('tags', tx('包含标签(逗号分隔)', 'Include tags (comma-separated)', 'Tags enthalten (Komma getrennt)')), field('tagMode', tx('标签组合', 'Tag matching', 'Tags kombinieren'), [['all', tx('全部标签', 'All tags', 'Alle Tags (UND)')], ['any', tx('任一标签', 'Any tag', 'Ein Tag (ODER)')]]), field('excludeTags', tx('排除标签', 'Exclude tags', 'Tags ausschließen')), field('country', tx('国家代码或名称', 'Country code or name', 'Land: Kürzel oder Name')), field('gender', tx('类型', 'Type', 'Typ'), [['all', ui.all], ['f', tx('女性', 'Women', 'Frauen')], ['m', tx('男性', 'Men', 'Männer')], ['c', tx('情侣', 'Couples', 'Paare')], ['t', 'Trans']]), $('div', { class: 'sidebar-filter-pair' }, [field('ageMin', tx('最低年龄', 'Minimum age', 'Alter ab'), null, 'number'), field('ageMax', tx('最高年龄', 'Maximum age', 'Alter bis'), null, 'number')]), $('div', { class: 'sidebar-filter-pair' }, [field('viewerMin', tx('最少观众', 'Minimum viewers', 'Zuschauer ab'), null, 'number'), field('viewerMax', tx('最多观众', 'Maximum viewers', 'Zuschauer bis'), null, 'number')]), field('local', tx('本地选择', 'Local selection', 'Lokale Auswahl'), [['all', ui.all], ['favorites', tx('收藏', 'Favorites', 'Favoriten')], ['watched', tx('已观看', 'Watched', 'Angesehen')], ['unwatched', tx('从未观看', 'Never watched', 'Noch nie angesehen')], ['notes', tx('有备注', 'With notes', 'Mit Notizen')]])); panel.append(details, $('small', { class: 'sidebar-filter-summary', role: 'status' }), $('button', { class: 'btn btn-sm', onclick: () => { // Flush an outstanding input callback before reset; it reads the reset // controls later, never restoring the previous filter by accident. const defaults = { ...defaultState().settings.sidebarBrowser, expanded: details.open }; panel.querySelectorAll('[data-sidebar-filter]').forEach(input => { input.value = defaults[input.dataset.sidebarFilter]; }); sidebarPages.clear(); store.patchSettings({ sidebarBrowser: defaults }); } }, tx('重置筛选', 'Reset filters', 'Filter zurücksetzen')), $('small', {}, tx('仅筛选左侧列表;播放窗口不变。具体筛选不包含未知数据。', 'Filters only the sidebar; cameras stay unchanged. Specific filters exclude unknown metadata.', 'Nur die linke Liste wird gefiltert; Kameras bleiben unverändert. Unbekannte Angaben erfüllen konkrete Filter nicht.'))); sidebar.appendChild(panel); } function appendSidebarPage(host, key, members, onSelect) { const pageSize = 60; const pages = Math.max(1, Math.ceil(members.length / pageSize)); const page = Math.min(sidebarPages.get(key) || 0, pages - 1); sidebarPages.set(key, page); members.slice(page * pageSize, (page + 1) * pageSize).forEach(room => host.appendChild(sidebarRoomButton(room, () => onSelect(room)))); if (!members.length) host.appendChild($('small', { class: 'sidebar-empty' }, tx('没有匹配结果', 'No matches', 'Keine Treffer'))); if (pages > 1) host.appendChild($('div', { class: 'sidebar-list-pagination' }, [ $('button', { disabled: page === 0, onclick: () => { sidebarPages.set(key, page - 1); renderSidebar(); } }, '‹'), $('span', {}, `${page + 1}/${pages} · ${members.length}`), $('button', { disabled: page === pages - 1, onclick: () => { sidebarPages.set(key, page + 1); renderSidebar(); } }, '›'), ])); } async function refreshSidebarFollowing(refresh = false) { await RoomGridNativeHub.loadFollowingSnapshot({ refresh }); sidebarFollowing = RoomGridNativeHub.followingSnapshot(); scheduleSidebarRender(); } function followingSidebarPartialText(ui) { return String(ui.partial).replaceAll('{count}', String(sidebarFollowing.rows.length)).replaceAll('{pages}', String(sidebarFollowing.pages)); } function assignGridOrFocusFirst(id) { const mode = store.state.settings.viewMode; const targets = [...grid.querySelectorAll(mode === 'focus' ? '[data-focus-slot]' : '[data-grid-slot]')]; const target = targets.find(node => !node.dataset.roomId) || targets[0]; if (!target) return; if (mode === 'focus') assignFocusRoomToSlot(id, Number(target.dataset.focusSlot)); else assignGridRoomToSlot(id, Number(target.dataset.gridSlot)); } window.addEventListener('roomgrid:following-updated', () => { sidebarFollowing = RoomGridNativeHub.followingSnapshot(); scheduleSidebarRender(); }); window.addEventListener('roomgrid:live-catalog-updated', scheduleSidebarRender); // ---- 侧边栏渲染 ---- function sidebarIsHidden() { const { sidebarCollapsed, pureMode, viewerMode } = store.state.settings; return pureMode || viewerMode || (compactRooms.matches ? !compactRoomsOpen : sidebarCollapsed); } const sidebarHoverButton = $('button', { type: 'button', class: 'btn btn-sm', dataset: { sidebarHoverPreview: '1' }, title: tx('鼠标悬停时显示左侧列表的实时预览', 'Show a live preview when hovering over rooms in the sidebar', 'Beim Überfahren eines Raums in der linken Liste eine Live-Vorschau zeigen'), ariaPressed: String(store.state.settings.sidebarHoverPreview !== false), onclick: () => store.patchSettings({ sidebarHoverPreview: store.state.settings.sidebarHoverPreview === false }), }, tx('实时预览', 'Live preview', 'Live-Vorschau')); function renderSidebar() { if (isRoomDragActive()) { dragSidebarRenderDeferred = true; return; } if (sidebarIsHidden() && sidebar.style.display === 'none') return; closeSidebarPreview({ flushSidebar: false }); sidebarRenderDeferredForPreview = false; const collapsed = compactRooms.matches ? !compactRoomsOpen : !!store.state.settings.sidebarCollapsed; sidebarToggleBtn.setAttribute('aria-expanded', String(!sidebarIsHidden())); sidebar.dataset.collapsed = collapsed ? 'true' : 'false'; sidebar.classList.toggle('is-collapsed', collapsed); document.body.classList.toggle('rg-sidebar-collapsed', collapsed); if (sidebarIsHidden()) { sidebar.style.setProperty('display', 'none', 'important'); sidebar.style.setProperty('width', '0px', 'important'); sidebar.style.setProperty('min-width', '0px', 'important'); sidebar.style.setProperty('max-width', '0px', 'important'); sidebar.style.setProperty('flex-basis', '0px', 'important'); sidebar.style.setProperty('padding', '0px', 'important'); sidebar.style.setProperty('border-width', '0px', 'important'); return; } sidebar.style.setProperty('display', 'flex', 'important'); if (!sidebarFollowingRequested) { sidebarFollowingRequested = true; void refreshSidebarFollowing(); } const sidebarWidth = `${clampInt(store.state.settings.sidebarWidth, 180, compactRooms.matches ? Math.min(460, innerWidth - 24) : 460, 220)}px`; sidebar.style.setProperty('width', sidebarWidth, 'important'); sidebar.style.setProperty('min-width', sidebarWidth, 'important'); sidebar.style.setProperty('max-width', sidebarWidth, 'important'); sidebar.style.setProperty('flex-basis', sidebarWidth, 'important'); sidebar.style.setProperty('padding', '10px 8px', 'important'); sidebar.style.setProperty('border-width', '1px', 'important'); const sidebarRooms = allRoomsForView(); const total = sidebarRooms.length; const online = sidebarRooms.filter(r => r.lastStatus === 'online').length; const muted = sidebarRooms.filter(r => r.muted).length; if (!sidebar.querySelector('.sidebar-brand')) sidebar.append($('div', { class: 'sidebar-brand' }, [ $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } }, [ $('div', { class: 'title', dataset: { roomgridBrandName: '1' } }, roomGridBrandIdentity(store.state.settings).name), $('button', { class: 'sidebar-collapse-btn', title: tx('收起左侧分组', 'Collapse groups', 'Gruppen einklappen'), onclick: () => { document.body.classList.add('rg-sidebar-collapsed'); sidebar.classList.add('is-collapsed'); if (compactRooms.matches) { setCompactRoomsOpen(false); sidebarToggleBtn.focus(); return; } store.patchSettings({ sidebarCollapsed: true }); }, }, tx('收起', 'Hide', 'Ausblenden')), ]), $('div', { class: 'sub' }, t('appTagline')), sidebarHoverButton, ])); installSidebarBrowser(); const modelById = sidebarModels(); // Camera placement is identical for every sidebar row. Calculating the // filtered/sorted camera catalogue per row caused periodic UI stalls. sidebarSlotLabels = new Map(); for (const entry of focusSlotEntriesForSidebar()) { if (!entry?.id) continue; const labels = sidebarSlotLabels.get(entry.id) || []; labels.push(entry.label); sidebarSlotLabels.set(entry.id, labels); } const prefs = store.state.settings.sidebarBrowser; sidebar.querySelectorAll('[data-sidebar-filter]').forEach(input => { if (document.activeElement !== input) input.value = prefs[input.dataset.sidebarFilter]; }); const activeCount = ['query', 'tags', 'excludeTags', 'country', 'ageMin', 'ageMax', 'viewerMin', 'viewerMax'].filter(key => prefs[key]).length + ['status', 'gender', 'local'].filter(key => prefs[key] !== 'all').length; sidebar.querySelector('.sidebar-filter-summary').textContent = activeCount ? tx(`${activeCount} 个筛选已启用`, `${activeCount} active filters`, `${activeCount} aktive Filter`) : tx('无筛选', 'No filters active', 'Keine Filter aktiv'); let listing = sidebar.querySelector('.sidebar-lists'); if (!listing) { listing = $('div', { class: 'sidebar-lists' }); sidebar.appendChild(listing); } listing.replaceChildren(); const ui = I18N[LANG]?.followingUi || I18N.en.followingUi; const followed = filteredSidebarRooms(sidebarFollowing.rows.map(row => modelById.get(row.id)).filter(Boolean)); const followedSection = $('details', { class: 'sidebar-following' }); followedSection.open = loadGroupCollapseState().__account_following__ !== true; followedSection.addEventListener('toggle', () => { if (!followedSection.isConnected) return; const state = loadGroupCollapseState(); if (state.__account_following__ === !followedSection.open) return; state.__account_following__ = !followedSection.open; saveGroupCollapseState(state); }); followedSection.append($('summary', {}, [ui.nav, $('span', { class: 'group-count' }, `${followed.length}/${sidebarFollowing.rows.length}`)])); const followStatus = sidebarFollowing.status; const message = followStatus === 'idle' || followStatus === 'loading' ? ui.loading : followStatus === 'login' ? ui.login : followStatus === 'parser' ? ui.parser : ['error', 'cancelled'].includes(followStatus) ? ui.failed : followStatus === 'partial' ? followingSidebarPartialText(ui) : sidebarFollowing.rows.length ? tx('来自当前 Chaturbate 会话', 'From the current Chaturbate session', 'Aus der aktuellen Chaturbate-Sitzung') : ui.empty; followedSection.append($('small', { role: 'status', dataset: { sidebarFollowingState: followStatus } }, message)); if (sidebarFollowing.rows.length && !['complete', 'partial'].includes(followStatus)) followedSection.append($('small', {}, tx('正在显示上次结果', 'Showing previous results', 'Letzter Stand wird angezeigt'))); followedSection.append($('button', { class: 'btn btn-sm', disabled: followStatus === 'loading', onclick: () => { void refreshSidebarFollowing(true); } }, ui.refresh)); const accountList = $('div', { class: 'group-user-list', dataset: { sidebarAccountList: '1' } }); if (sidebarFollowing.rows.length || followStatus === 'complete' || followStatus === 'partial') appendSidebarPage(accountList, '__account_following__', followed, room => { if (ensureSidebarRoom(room.id)) assignGridOrFocusFirst(room.id); }); followedSection.append(accountList); listing.append(followedSection, $('div', { class: 'sidebar-section-title' }, t('groupsHeading'))); const ordered = [...store.state.groups].filter(group => !group.archived).sort((a, b) => { if (!!a.system !== !!b.system) return a.system ? -1 : 1; if (a.parentId === b.id) return 1; if (b.parentId === a.id) return -1; return a.order - b.order; }); const groupDisplayName = (g) => { if (g.name === '__library__') return t('groupLibrary'); if (g.name === '__all__') return t('groupAll'); if (g.name === '__online__') return t('groupOnline'); if (g.name === '__fav__') return t('groupFav'); return g.name; }; for (const g of ordered) { const isActive = store.state.settings.activeGroup === g.id; const tab = $('button', { class: 'group-tab' + (isActive ? ' active' : ''), dataset: { groupId: g.id }, title: g.id === LIBRARY_GROUP_ID ? t('hintLibraryTab') : t('hintGroupTab', groupDisplayName(g)), onclick: () => store.setActiveGroup(g.id), oncontextmenu: (e) => { if (!g.system) { e.preventDefault(); openGroupMenu(e, g); } }, ondragover: (e) => { if (g.id === LIBRARY_GROUP_ID || g.id === ONLINE_GROUP_ID) return; e.preventDefault(); tab.classList.add('drop-target'); }, ondragleave: () => tab.classList.remove('drop-target'), ondrop: (e) => { if (g.id === LIBRARY_GROUP_ID || g.id === ONLINE_GROUP_ID) return; e.preventDefault(); tab.classList.remove('drop-target'); const id = e.dataTransfer.getData('text/room-id'); if (id && ensureSidebarRoom(id)) store.moveToGroup(id, g.id); }, }, [ $('span', { class: 'group-name' }, `${g.parentId ? '↳ ' : ''}${groupDisplayName(g)}`), $('span', { class: 'group-count' }, '0'), ]); listing.appendChild(tab); // v15.6-minimal: 分组内用户展开列表。只读展示,不改变原来的分组数据。 const collapsedMap = loadGroupCollapseState(); const collapsedUsers = collapsedMap[g.id] === true; const rawMembers = sidebarRooms.filter(r => (g.id === LIBRARY_GROUP_ID || roomInGroup(modelById.get(r.id), g.id)) && (r.temporary || (!r.archived && Number(r.hiddenUntil || 0) <= Date.now() && Number(r.pausedUntil || 0) <= Date.now()))); const members = filteredSidebarRooms(rawMembers.map(room => modelById.get(room.id)), g.id); tab.querySelector('.group-count').textContent = activeCount ? `${members.length}/${rawMembers.length}` : String(rawMembers.length); const memberBox = $('div', { class: 'group-user-list', style: { display: collapsedUsers ? 'none' : 'flex', flexDirection: 'column', gap: '2px', padding: '2px 4px 6px 18px' } }); tab.insertBefore($('span', { class: 'group-fold-toggle', title: tx('展开/折叠组内用户', 'Expand/collapse users', 'Benutzer ein-/ausblenden'), onclick: (ev) => { ev.preventDefault(); ev.stopPropagation(); const m = loadGroupCollapseState(); m[g.id] = !(m[g.id] === true); saveGroupCollapseState(m); renderSidebar(); }, style: { opacity: '.65', minWidth: '12px', display: 'inline-block', textAlign: 'center' }, }, collapsedUsers ? '›' : '⌄'), tab.firstChild); appendSidebarPage(memberBox, g.id, members, r => { closeSidebarPreview(); store.setActiveGroup(g.id === LIBRARY_GROUP_ID ? LIBRARY_GROUP_ID : g.id); const cameraOrder = fullVisibleRooms(); store.patchSettings({ focusedRoomId: r.id, pageIndex: Math.max(0, Math.floor(cameraOrder.findIndex(x => x.id === r.id) / layoutSize())) }); }); listing.appendChild(memberBox); } listing.appendChild($('button', { class: 'group-tab', title: t('hintNewGroup'), style: { color: 'var(--text-secondary)', marginTop: '8px', justifyContent: 'center', borderStyle: 'dashed' }, onclick: () => { const name = prompt(t('newGroupPrompt')); if (name && name.trim()) store.addGroup(name.trim()); }, }, t('newGroup'))); const statColorOnline = 'var(--success)'; listing.appendChild($('div', { class: 'sidebar-stats' }, [ $('div', { class: 'sidebar-stat' }, [ $('div', { class: 'k' }, t('statTotal')), $('div', { class: 'v' }, String(total)) ]), $('div', { class: 'sidebar-stat' }, [ $('div', { class: 'k' }, t('statOnline')), $('div', { class: 'v', style: { color: statColorOnline } }, String(online)) ]), $('div', { class: 'sidebar-stat' }, [ $('div', { class: 'k' }, t('statMuted')), $('div', { class: 'v' }, String(muted)) ]), ])); } function sidebarRoomButton(r, onSelect) { const slotLabels = focusSlotLabelsForRoom(r.id); const displayName = `${r.temporary ? '临时 · ' : ''}${r.displayName || r.id}`; const liveLabel = slotLabels.length ? t('focusLivePlacement', slotLabels.join(' · ')) : ''; const liveIndicator = slotLabels.length ? $('span', { class: 'group-user-live-indicator', role: 'img', title: liveLabel, 'aria-label': liveLabel }, '●') : $('span', { class: 'group-user-status-dot', title: statusMeta(r.lastStatus).label, 'aria-hidden': 'true' }, '●'); const nameEl = $('span', { class: 'group-user-name' }, rawUiText(displayName)); const placementEl = slotLabels.length ? $('span', { class: 'group-user-live-slots', title: slotLabels.join(' · ') }, liveLabel) : null; return $('button', { class: 'group-user-item' + (slotLabels.length ? ' is-focus-live' : ''), title: slotLabels.length ? `${t('focusDragSourceHint')} · ${slotLabels.join(' · ')}` : t('focusDragSourceHint'), draggable: 'true', dataset: { sidebarRoomId: r.id }, style: { border: '0', background: 'transparent', textAlign: 'left', cursor: 'grab', padding: '4px 6px', minHeight: '26px', borderRadius: '6px', fontSize: '11px', color: r.lastStatus === 'online' ? 'var(--success)' : 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: '6px', minWidth: '0', width: '100%' }, onpointerenter: (e) => scheduleSidebarPreview(r, e.currentTarget), onpointerleave: (e) => cancelSidebarPreview(e.currentTarget), onfocus: (e) => scheduleSidebarPreview(r, e.currentTarget), onblur: (e) => cancelSidebarPreview(e.currentTarget), onclick: () => { if (Date.now() < suppressSidebarClickUntil) return; closeSidebarPreview(); onSelect(); }, oncontextmenu: (e) => { if (store.state.settings.viewMode !== 'grid') return; e.preventDefault(); e.stopPropagation(); openGridAssignmentMenu(e, r.id, e.currentTarget); }, ondragstart: (e) => { focusDragRoomId = r.id; roomDragSource = e.currentTarget; // Removing the preview must not replace the native drag source. closeSidebarPreview({ flushSidebar: false }); e.dataTransfer.setData('text/roomgrid-focus-id', r.id); e.dataTransfer.setData('text/room-id', r.id); e.dataTransfer.effectAllowed = 'move'; setFocusDropMode(true); roomDragGhost = $('div', { class: 'room-drag-ghost', 'aria-hidden': 'true' }, `↕ ${r.displayName || r.id}`); document.body.appendChild(roomDragGhost); try { e.dataTransfer.setDragImage(roomDragGhost, 24, 20); } catch (_) {} roomDragSource.classList.add('is-drag-source'); }, ondragend: finishRoomDrag, }, [$('span', { class: 'group-user-drag-grip', 'aria-hidden': 'true', html: trustedHtml(iconSvg('drag', 12)) }), liveIndicator, nameEl, placementEl]); } function countByGroup() { const rooms = allRoomsForView(); const c = { [LIBRARY_GROUP_ID]: rooms.length, [ONLINE_GROUP_ID]: rooms.filter(r => r.lastStatus === 'online').length }; for (const r of rooms) for (const g of getRoomGroups(r)) c[g] = (c[g] || 0) + 1; return c; } function openGroupMenu(e, g) { const menu = $('div', { class: 'menu-pop', style: { left: e.clientX + 'px', top: e.clientY + 'px' } }, [ $('button', { onclick: () => { const n = prompt(t('renameGroupPrompt'), g.name); if (n) store.renameGroup(g.id, n.trim()); menu.remove(); } }, t('renameGroup')), $('button', { class: 'danger', onclick: () => { if (confirm(t('deleteGroupConfirm', g.name))) store.removeGroup(g.id); menu.remove(); } }, t('deleteGroup')), ]); document.body.appendChild(menu); const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close); } }; setTimeout(() => document.addEventListener('click', close), 0); } // ---- 卡片管理 ---- const cardMap = new Map(); // id -> {root, video, statusEl, badgeEl} // Global silence is a window-local output override, never a rewrite of // each camera's saved mute preference. New/reconnected videos inherit it. let temporaryAudioMuted = false; let focusDragRoomId = ''; let cardDragRoomId = ''; let roomDragSource = null; let roomDragGhost = null; let dragSidebarRenderDeferred = false; let dragGridRenderDeferred = false; let suppressSidebarClickUntil = 0; function isRoomDragActive() { return !!(focusDragRoomId || cardDragRoomId); } function finishRoomDrag() { if (!isRoomDragActive() && !roomDragGhost) return; suppressSidebarClickUntil = Date.now() + 250; focusDragRoomId = ''; cardDragRoomId = ''; roomDragSource?.classList.remove('is-drag-source'); roomDragSource = null; roomDragGhost?.remove(); roomDragGhost = null; closeSidebarPreview({ flushSidebar: false }); clearFocusDropIndicators(); sidebar.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target')); if (dragSidebarRenderDeferred || sidebarRenderDeferredForPreview) { dragSidebarRenderDeferred = false; sidebarRenderDeferredForPreview = false; scheduleSidebarRender(); } if (dragGridRenderDeferred) { dragGridRenderDeferred = false; scheduleGridRender(); } } document.addEventListener('dragend', finishRoomDrag); document.addEventListener('drop', finishRoomDrag); // F-009: Statistik wird erst nach erfolgreichem Laden aus der getrennten // IndexedDB gestartet. Die Quelle ist ausschließlich die aktuelle CardMap. analytics.init(store.state.settings.analyticsEnabled !== false).then(() => { analytics.start(() => [...cardMap.entries()].map(([id, card]) => ({ id, video: card.video, room: findRoomAny(id) }))); scheduleSidebarRender(); scheduleGridRender(); }); // v15.6-minimal: 临时 URL 窗口。只存在于当前页面内存,刷新后消失;不写入 localStorage。 const tempRooms = []; const tempGroupCollapsedKey = 'ryujo_multicam_group_user_collapsed_v1'; function loadGroupCollapseState() { try { return JSON.parse(localStorage.getItem(tempGroupCollapsedKey) || '{}') || {}; } catch (_) { return {}; } } function saveGroupCollapseState(v) { try { localStorage.setItem(tempGroupCollapsedKey, JSON.stringify(v || {})); } catch (_) {} } function allRoomsForView() { return [...store.state.rooms, ...tempRooms]; } function findRoomAny(id) { id = String(id || ''); return store.state.rooms.find(r => r.id === id) || tempRooms.find(r => r.id === id) || sidebarAccountRooms.get(id) || null; } function applyAudioMeterVisibility(roomId) { const card = cardMap.get(String(roomId || '')); if (!card?.root) return; const room = findRoomAny(roomId); const mode = store.state.settings.multicamMeterDisplayMode; const visible = mode === 'all-show' ? true : mode === 'all-hide' ? false : typeof room?.audioMeterVisible === 'boolean' ? room.audioMeterVisible : store.state.settings.audioMeterVisible !== false; card.root.classList.toggle('audio-meter-hidden', !visible); } function applyAllAudioMeterVisibility() { for (const roomId of cardMap.keys()) applyAudioMeterVisibility(roomId); } function createMeterDisplayControl(header = false) { const select = $('select', { class: header ? 'suite-global-button' : 'ctrl-input', 'data-multicam-meter-mode': '1', title: t('multicamMeterModeHint') }, [ $('option', { value: 'individual' }, t('multicamMeterModeIndividual')), $('option', { value: 'all-show' }, t('multicamMeterModeAllShow')), $('option', { value: 'all-hide' }, t('multicamMeterModeAllHide')), ]); select.value = store.state.settings.multicamMeterDisplayMode || 'individual'; select.addEventListener('change', () => store.patchSettings({ multicamMeterDisplayMode: select.value })); return $('label', { class: header ? 'suite-meter-control' : 'roomgrid-modal-hint' }, [ $('span', {}, t('multicamMeterModeLabel')), select, ]); } function syncAudioMeterControls() { const mode = store.state.settings.multicamMeterDisplayMode || 'individual'; document.querySelectorAll('[data-multicam-meter-mode]').forEach(select => { select.value = mode; }); document.querySelectorAll('[data-multicam-meter-visible]').forEach(input => { input.checked = store.state.settings.audioMeterVisible !== false; input.disabled = mode !== 'individual'; }); } // F-008: ein gemeinsamer AudioContext mit genau einem Analyzer je aktivem Video. // captureStream() misst den Ton, ohne den Audio-Ausgang des Videos umzuleiten. // Der direkte Stream-Audiotrack bleibt auch bei stummgeschaltetem Video messbar. // Falls Brave den Track erst nach dem HLS-Start bereitstellt, wird begrenzt // erneut verbunden; ohne Track bleibt die Anzeige ehrlich auf „nicht verfügbar“. const audioMeters = new Map(); let audioContext = null; let audioMeterTimer = 0; function getAudioContext() { if (audioContext) return audioContext; const AudioContextCtor = window.AudioContext || window.webkitAudioContext; if (!AudioContextCtor) return null; try { audioContext = new AudioContextCtor(); } catch (_) { audioContext = null; } return audioContext; } function resumeAudioContext() { const ctx = getAudioContext(); if (!ctx) { [...audioMeters.values()].forEach(item => { item.unavailable = true; updateAudioMeterVisual(item, 0, 'unavailable'); }); return; } const afterResume = () => { [...audioMeters.values()].forEach(item => { if (item.video && (!item.analyser || item.unavailable)) attachAudioMeter(item.roomId, item.video, { force: true }); if (item.analyser) item.available = true; }); scheduleAudioMeterLoop(); }; try { if (ctx.state === 'suspended') { const pending = ctx.resume?.(); if (pending?.then) pending.then(afterResume).catch(afterResume); else afterResume(); } else afterResume(); } catch (_) { afterResume(); } } function updateAudioMeterVisual(item, level = 0, status = 'still') { const c = cardMap.get(item?.roomId); const meter = c?.audioMeter; if (!meter) return; const bounded = Math.max(0, Math.min(1, Number(level) || 0)); const outputMuted = !!findRoomAny(item?.roomId)?.muted || !!item?.video?.muted || Number(item?.video?.volume) === 0; const displayStatus = status; for (const name of ['active', 'still', 'output', 'muted', 'paused', 'waiting', 'unavailable']) meter.classList.toggle(`is-${name}`, displayStatus === name); const hint = displayStatus === 'unavailable' ? tx('浏览器未提供音轨', 'Browser exposed no audio track', 'Brave stellt keinen Audiotrack bereit') : displayStatus === 'waiting' ? tx('等待直播音轨或点击页面以启用电平', 'Waiting for the stream audio track or a click to enable the meter', 'Wartet auf den Stream-Audiotrack oder einen Klick zum Aktivieren') : displayStatus === 'paused' ? t('audioPaused') : tx( outputMuted ? '实时音频活动;声音输出已静音' : '实时音频活动', outputMuted ? 'Live audio activity; sound output is muted' : 'Live audio activity', outputMuted ? 'Live-Audioaktivität; die Tonausgabe ist stumm' : 'Live-Audioaktivität' ); meter.title = hint; meter.setAttribute('aria-label', hint); meter.dataset.state = displayStatus; meter.dataset.source = item?.analysisMode || 'none'; meter.dataset.outputMuted = outputMuted ? '1' : '0'; meter.dataset.band = bounded < .15 ? 'quiet' : bounded < .48 ? 'medium' : 'loud'; const visualLevel = ['active', 'still', 'paused'].includes(displayStatus) ? bounded : 0; meter.setAttribute('aria-valuenow', String(Math.round(visualLevel * 100))); if (c.audioMeterFill) c.audioMeterFill.style.width = `${Math.round(visualLevel * 100)}%`; if (c.audioMeterMode) c.audioMeterMode.textContent = displayStatus === 'active' || displayStatus === 'still' ? 'LIVE' : displayStatus === 'paused' ? 'PAU' : displayStatus === 'waiting' ? '…' : 'N/V'; if (c.audioMeterValue) c.audioMeterValue.textContent = displayStatus === 'unavailable' ? '—' : `${Math.round(visualLevel * 100)}%`; } function detachAudioMeter(roomId) { roomId = normalizeUsername(roomId); const item = audioMeters.get(roomId); if (!item) return; item.destroyed = true; for (const [eventName, listener] of item.mediaListeners || []) { try { item.video?.removeEventListener?.(eventName, listener); } catch (_) {} } clearTimeout(item.retryTimer); try { item.stream?.removeEventListener?.('addtrack', item.streamTrackHandler); } catch (_) {} try { item.source?.disconnect?.(); } catch (_) {} try { item.analyser?.disconnect?.(); } catch (_) {} cardMap.get(roomId)?.root?.classList.remove('audio-loudest'); audioMeters.delete(roomId); } function scheduleAudioMeterAttachRetry(item) { clearTimeout(item.retryTimer); if (!item || item.destroyed || item.analyser) return; if (item.retries >= 20) { item.unavailable = true; updateAudioMeterVisual(item, 0, 'unavailable'); return; } item.retryTimer = setTimeout(() => { item.retryTimer = 0; item.retries += 1; attachAudioMeter(item.roomId, item.video, { retry: true }); }, 750); } function attachAudioMeter(roomId, video, options = {}) { roomId = normalizeUsername(roomId); const c = cardMap.get(roomId); if (!c?.audioMeter || !video) return; if (!suiteModuleEnabled('audioMeters')) { updateAudioMeterVisual({ roomId, video, disabled: true }, 0, 'unavailable'); return; } let item = audioMeters.get(roomId); if (item?.video !== video) { if (item) detachAudioMeter(roomId); item = null; } if (!item) { item = { roomId, video, source: null, stream: null, analyser: null, data: null, level: 0, silentFrames: 0, available: false, unavailable: false, analysisMode: '', mediaListeners: [], retryTimer: 0, retries: 0, streamTrackHandler: null, destroyed: false, }; audioMeters.set(roomId, item); const syncVisual = () => updateAudioMeterVisual(item, item.level, item.analyser ? (video.paused ? 'paused' : (item.level > .015 ? 'active' : 'still')) : (item.unavailable ? 'unavailable' : 'waiting')); for (const eventName of ['volumechange', 'play', 'pause', 'playing', 'waiting', 'ended']) { const listener = () => { syncVisual(); if (['play', 'playing', 'waiting'].includes(eventName) && !item.analyser) attachAudioMeter(roomId, video, { retry: true }); }; video.addEventListener(eventName, listener); item.mediaListeners.push([eventName, listener]); } } if (item.analyser) { updateAudioMeterVisual(item, item.level, video.paused ? 'paused' : (item.level > .015 ? 'active' : 'still')); scheduleAudioMeterLoop(); return; } clearTimeout(item.retryTimer); item.retryTimer = 0; item.unavailable = false; try { const ctx = getAudioContext(); if (!ctx) throw new Error('AudioContext unavailable'); const capture = video.captureStream || video.mozCaptureStream || video.webkitCaptureStream; if (typeof capture !== 'function') throw new Error('captureStream unavailable'); const shouldRecapture = !item.stream || (options.retry && item.retries > 0 && item.retries % 3 === 0); if (shouldRecapture) { try { item.stream?.removeEventListener?.('addtrack', item.streamTrackHandler); } catch (_) {} item.stream = capture.call(video); item.streamTrackHandler = () => attachAudioMeter(roomId, video, { retry: true }); item.stream?.addEventListener?.('addtrack', item.streamTrackHandler); } if (!item.stream?.getAudioTracks?.().length) { updateAudioMeterVisual(item, 0, 'waiting'); scheduleAudioMeterAttachRetry(item); return; } item.source = ctx.createMediaStreamSource(item.stream); item.analyser = ctx.createAnalyser(); item.analyser.fftSize = 1024; item.analyser.smoothingTimeConstant = 0.62; item.source.connect(item.analyser); item.data = new Uint8Array(item.analyser.fftSize); item.analysisMode = 'capture-stream'; item.available = ctx.state !== 'suspended'; item.retries = 0; updateAudioMeterVisual(item, item.level, ctx.state === 'suspended' ? 'waiting' : (video.paused ? 'paused' : 'still')); scheduleAudioMeterLoop(); } catch (_) { updateAudioMeterVisual(item, 0, 'waiting'); scheduleAudioMeterAttachRetry(item); } } function scheduleAudioMeterLoop() { if (audioMeterTimer || ![...audioMeters.values()].some(item => item.analyser && !item.unavailable)) return; audioMeterTimer = setTimeout(updateAudioMeters, document.hidden ? 650 : 80); } function updateAudioMeters() { audioMeterTimer = 0; let needsLoop = false; let loudestRoomId = ''; let loudestLevel = 0; for (const [roomId, item] of audioMeters) { const c = cardMap.get(roomId); if (!c || c.video !== item.video || !document.documentElement.contains(item.video)) { detachAudioMeter(roomId); continue; } if (!item.analyser || item.unavailable) { updateAudioMeterVisual(item, 0, 'unavailable'); continue; } needsLoop = true; if (service.isPaused(roomId) || item.video.paused) { item.level *= 0.78; updateAudioMeterVisual(item, item.level, 'paused'); continue; } if (audioContext?.state === 'suspended') { item.level *= 0.82; updateAudioMeterVisual(item, item.level, 'waiting'); continue; } try { item.analyser.getByteTimeDomainData(item.data); let energy = 0; for (const sample of item.data) { const normalized = (sample - 128) / 128; energy += normalized * normalized; } const rms = Math.sqrt(energy / Math.max(1, item.data.length)); const normalizedLevel = Math.max(0, Math.min(1, (20 * Math.log10(Math.max(rms, .0001)) + 60) / 60)); item.level = item.level * 0.58 + normalizedLevel * 0.42; const active = rms >= .006; item.silentFrames = active ? 0 : item.silentFrames + 1; updateAudioMeterVisual(item, item.level, active ? 'active' : 'still'); if (active && item.level > loudestLevel) { loudestLevel = item.level; loudestRoomId = roomId; } } catch (_) { item.unavailable = true; updateAudioMeterVisual(item, 0, 'unavailable'); } } for (const [roomId] of audioMeters) cardMap.get(roomId)?.root?.classList.toggle('audio-loudest', roomId === loudestRoomId && loudestLevel > 0.035); if (needsLoop) scheduleAudioMeterLoop(); } function disposeAudioMeters() { for (const roomId of [...audioMeters.keys()]) detachAudioMeter(roomId); if (audioMeterTimer) { clearTimeout(audioMeterTimer); audioMeterTimer = 0; } try { audioContext?.close?.(); } catch (_) {} audioContext = null; } document.addEventListener('pointerdown', resumeAudioContext, true); document.addEventListener('keydown', resumeAudioContext, true); // F-004: The authenticated Chaturbate Following pages are the source of // truth for one dedicated workspace group. Other groups and layout data // are intentionally never touched by this sync. const FOLLOWED_SYNC_INTERVAL_MS = 10 * 60 * 1000; const FOLLOWED_SYNC_PAGE_LIMIT = 80; let followedSyncBusy = false; let followedSyncTimer = 0; function safeFollowedUrl(href) { try { const url = new URL(String(href || ''), location.origin); if (url.origin !== location.origin) return null; const path = url.pathname.replace(/\/+$/, '') || '/'; if (path !== '/followed-cams' && path !== '/followed-cams/offline') return null; url.hash = ''; return url; } catch (_) { return null; } } function followedRoomIdFromHref(href) { try { const url = new URL(String(href || ''), location.origin); if (url.origin !== location.origin) return ''; const match = url.pathname.match(/^\/([^/]+)\/?$/); return match ? normalizeUsername(decodeURIComponent(match[1])) : ''; } catch (_) { return ''; } } function extractFollowedPage(doc) { const selector = '[data-testid="room-card"], [data-testid="room-card-username"], [class*="RoomCard"], [class*="room-card"]'; const cards = [...new Set([...doc.querySelectorAll(selector)].map(el => el.closest('[data-testid="room-card"]') || el))]; const ids = new Set(); const addCandidate = (value) => { const id = normalizeUsername(String(value || '').trim()); if (isLikelyUsername(id)) ids.add(id); }; cards.forEach(card => { addCandidate(card.getAttribute('data-room')); card.querySelectorAll('[data-room]').forEach(el => addCandidate(el.getAttribute('data-room'))); addCandidate(card.querySelector('[data-testid="room-card-username"]')?.textContent); card.querySelectorAll('a[href]').forEach(a => addCandidate(followedRoomIdFromHref(a.getAttribute('href')))); }); const bodyText = String(doc.body?.textContent || '').replace(/\s+/g, ' '); const emptyState = /(?:not following any|no followed|no models? followed|you are not following|keine(?:n|) folgen|du folgst keinen|暂无关注|还没有关注)/i.test(bodyText); return { ids: [...ids], recognized: cards.length > 0 || emptyState || !!doc.querySelector('[data-testid="room-list"], [data-testid="room-card-list"], .RoomCardGrid') }; } function followedPageLinks(doc) { return [...doc.querySelectorAll('a[href]')] .map(a => safeFollowedUrl(a.getAttribute('href'))) .filter(url => url && url.searchParams.has('page')); } async function fetchFollowedPage(url) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { const response = await fetch(url.href, { credentials: 'include', cache: 'no-store', headers: { Accept: 'text/html' }, signal: controller.signal, }); const finalUrl = new URL(response.url || url.href, location.origin); if (finalUrl.origin !== location.origin) { const err = new Error('Following redirect left the current origin'); err.code = 'parser'; throw err; } if (/\/auth\/login(?:\/|$)/i.test(finalUrl.pathname)) { const err = new Error('login required'); err.code = 'login'; throw err; } if (!response.ok) throw new Error(`HTTP ${response.status}`); const html = await response.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); if (/\/auth\/login(?:\/|$)/i.test(finalUrl.pathname) || (/\blogin\b/i.test(doc.title || '') && !doc.querySelector('[data-testid="room-card"]'))) { const err = new Error('login required'); err.code = 'login'; throw err; } const parsed = extractFollowedPage(doc); return { doc, parsed }; } catch (err) { if (err?.name === 'AbortError') { const timeoutErr = new Error('Following page timeout'); timeoutErr.code = 'timeout'; throw timeoutErr; } throw err; } finally { clearTimeout(timeout); } } async function readFollowedRoomIds() { const snapshot = await RoomGridNativeHub.loadFollowingSnapshot({ refresh: true }); // Never detach memberships from a partial/error/login result. if (snapshot.status !== 'complete' || !snapshot.recognized) throw Object.assign(new Error('Following list is not complete'), { code: snapshot.status }); return snapshot.rows.map(row => row.id); } function ensureFollowedGroup() { const existingId = store.state.settings.followedGroupId; let group = store.state.groups.find(g => g.id === existingId && !g.system); if (!group) { const knownNames = new Set(['follower', 'following', '关注']); group = store.state.groups.find(g => !g.system && knownNames.has(String(g.name || '').trim().toLowerCase())); } if (!group) { const id = store.addGroup(t('followingGroupName')); group = store.state.groups.find(g => g.id === id && !g.system); } if (group && store.state.settings.followedGroupId !== group.id) store.patchSettings({ followedGroupId: group.id }); return group || null; } async function syncFollowedRooms({ manual = false } = {}) { if (!manual && store.state.settings.followedSyncEnabled === false) return; if (followedSyncBusy) { if (manual) toast(t('followedSyncRunning')); return; } followedSyncBusy = true; if (manual) toast(t('followedSyncRunning'), 1200); try { const ids = await readFollowedRoomIds(); const group = ensureFollowedGroup(); if (!group) throw new Error('Following group unavailable'); const result = store.syncRoomsToGroup(ids, group.id); store.patchSettings({ followedGroupId: group.id, followedSyncLastAt: Date.now() }); ids.forEach(id => { if (!service.has(id)) service.start(id); }); if (manual || result.added || result.attached || result.removed) { toast(t('followedSyncDone', result.added, result.attached, result.removed)); } } catch (err) { console.warn('[RoomGrid] Following sync skipped:', err); try { Diagnostics.report('following-sync', err, 'synchronization skipped', 'warning'); } catch (_) {} if (manual) toast(err?.code === 'login' ? t('followedSyncLogin') : t('followedSyncFailed'), 3200); } finally { followedSyncBusy = false; } } function scheduleFollowedSync() { if (followedSyncTimer) clearTimeout(followedSyncTimer); followedSyncTimer = 0; if (store.state.settings.followedSyncEnabled === false) return; const delay = document.hidden ? 30000 : FOLLOWED_SYNC_INTERVAL_MS; followedSyncTimer = setTimeout(async () => { followedSyncTimer = 0; if (!document.hidden) await syncFollowedRooms(); scheduleFollowedSync(); }, delay); } document.addEventListener('visibilitychange', () => { if (!document.hidden && store.state.settings.followedSyncEnabled !== false) { const last = Number(store.state.settings.followedSyncLastAt || 0); if (!last || Date.now() - last >= FOLLOWED_SYNC_INTERVAL_MS) syncFollowedRooms(); } scheduleFollowedSync(); }); function isDirectMediaUrl(url) { return /\.(m3u8|mp4|webm|mov|m4v)(?:[?#].*)?$/i.test(String(url || '')); } function isHlsUrl(url) { return /\.m3u8(?:[?#].*)?$/i.test(String(url || '')) || /m3u8/i.test(String(url || '')); } function tempIdFromUrl(url) { try { const u = new URL(String(url)); const name = (u.hostname + u.pathname).replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 40) || 'url'; return 'tmp_' + name + '_' + Math.random().toString(36).slice(2, 7); } catch (_) { return 'tmp_url_' + Math.random().toString(36).slice(2, 9); } } function extractMediaUrlFromHtml(html, baseUrl) { const srcs = []; const push = (v) => { if (!v) return; let s = String(v).replace(/\\\//g, '/').replace(/&/g, '&').trim(); try { s = new URL(s, baseUrl).href; } catch (_) {} if (isSafeHttpUrl(s) && isDirectMediaUrl(s)) srcs.push(s); }; String(html || '').replace(/https?:\\?\/\\?\/[^\s"'<>]+?(?:m3u8|mp4|webm|mov|m4v)(?:[^\s"'<>]*)?/ig, m => { push(m); return m; }); String(html || '').replace(/(?:src|source|file|url)["']?\s*[:=]\s*["']([^"']+)["']/ig, (_, m) => { push(m); return _; }); return srcs.find(isHlsUrl) || srcs[0] || ''; } async function fetchTextBestEffort(url) { const res = await fetch(url, { credentials: 'include' }); if (!res.ok) throw new Error('http ' + res.status); return res.text(); } async function importTemporaryUrls() { const raw = prompt(tx('粘贴 URL,支持房间页、m3u8、mp4/webm。普通网页会尝试解析真实播放源;失败请手动粘贴真实播放地址。', 'Paste URLs. Room pages, m3u8, mp4/webm are supported. Normal pages will be parsed best-effort.', 'URLs einfügen. Raumseiten, m3u8 sowie mp4/webm werden unterstützt. Bei normalen Webseiten wird die Quelle bestmöglich erkannt.')); if (!raw) return; const parts = String(raw).split(/[\s,,]+/).map(x => x.trim()).filter(Boolean); let added = 0, failed = 0; for (const part of parts) { try { if (!isSafeHttpUrl(part)) { failed++; continue; } const u = new URL(part, location.href); if (safeChaturbateHost(u.hostname)) { const username = normalizeUsername(part); if (username && isLikelyUsername(username)) { const activeGroup = store.state.settings.activeGroup; const groupId = (!activeGroup || activeGroup === LIBRARY_GROUP_ID || activeGroup === ONLINE_GROUP_ID) ? DEFAULT_GROUP_ID : activeGroup; if (!findRoomAny(username)) tempRooms.push({ id: username, group: groupId, groups: [groupId], addedAt: Date.now(), order: 100000 + tempRooms.length, lastStatus: 'unknown', lastSeenOnline: 0, muted: false, temporary: true }); service.start(username); added++; continue; } } let mediaUrl = isDirectMediaUrl(part) ? part : ''; if (!mediaUrl) { try { mediaUrl = extractMediaUrlFromHtml(await fetchTextBestEffort(part), part); } catch (_) {} } if (!mediaUrl) { failed++; continue; } const id = tempIdFromUrl(mediaUrl); const activeGroup = store.state.settings.activeGroup; const groupId = (!activeGroup || activeGroup === LIBRARY_GROUP_ID || activeGroup === ONLINE_GROUP_ID) ? DEFAULT_GROUP_ID : activeGroup; tempRooms.push({ id, displayName: new URL(mediaUrl).hostname, sourceUrl: mediaUrl, group: groupId, groups: [groupId], addedAt: Date.now(), order: 100000 + tempRooms.length, lastStatus: 'online', lastSeenOnline: Date.now(), muted: false, temporary: true }); added++; } catch (_) { failed++; } } store.patchSettings({ pageIndex: 0 }); renderSidebar(); renderGrid(); toast(tx(`临时 URL:新增 ${added},失败 ${failed}`, `Temporary URLs: ${added} added, ${failed} failed`, `Temporäre URLs: ${added} hinzugefügt, ${failed} fehlgeschlagen`, `URL temporales: ${added} añadidas, ${failed} fallidas`, `URLs temporárias: ${added} adicionadas, ${failed} com falha`)); } function statusMeta(s) { switch (s) { case 'online': return { color: '#16a34a', label: t('stOnline') }; case 'offline': return { color: '#64748b', label: t('stOffline') }; case 'private': return { color: '#d97706', label: t('stPrivate') }; case 'loading': return { color: '#2563eb', label: t('stLoading') }; case 'error': return { color: '#dc2626', label: t('stError') }; default: return { color: '#64748b', label: t('stUnknown') }; } } function buildCard(room) { // 卡片本身不做 draggable,避免 video controls 拦截 dragstart const card = $('div', { class: 'cam-card rg-card-enter', dataset: { roomId: room.id } }); setTimeout(() => { try { card.classList.remove('rg-card-enter'); } catch (_) {} }, 220); // 状态徽标(左上) const badge = $('div', { class: 'pill', style: { position: 'absolute', top: '8px', left: '8px', zIndex: '20' } }); // 名字(左下,常驻) const name = $('div', { class: 'name-label', style: { position: 'absolute', bottom: '8px', left: '8px', zIndex: '20', background: 'rgba(15,23,42,.46)', color: '#fff', padding: '5px 9px', borderRadius: '999px', fontSize: '12px', fontWeight: '650', pointerEvents: 'none', maxWidth: '72%', border: '1px solid rgba(255,255,255,.16)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)', boxShadow: 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, }, room.id); const audioMeterMode = $('span', { class: 'cam-audio-mode', 'aria-hidden': 'true' }, tx('关闭', 'OFF', 'AUS')); const audioMeterFill = $('span', { class: 'cam-audio-fill' }); const audioMeterTrack = $('span', { class: 'cam-audio-track', 'aria-hidden': 'true' }, audioMeterFill); const audioMeterValue = $('span', { class: 'cam-audio-value', 'aria-hidden': 'true' }, '0%'); const audioMeter = setElementHint($('div', { class: 'cam-audio-meter is-waiting', role: 'meter', 'aria-label': tx('直播音频活动', 'Live stream audio activity', 'Audioaktivität des Livestreams'), 'aria-valuemin': '0', 'aria-valuemax': '100', 'aria-valuenow': '0', title: tx('即使输出静音也会测量', 'Measured even while output is muted', 'Messung auch bei stummem Ton'), 'data-state': 'waiting', 'data-source': 'none', 'data-multicam-audio-meter': '1', }, [audioMeterMode, audioMeterTrack, audioMeterValue]), tx('即使输出静音也会测量', 'Measured even while output is muted', 'Messung auch bei stummem Ton')); // 拖拽手柄(hover 显示,承担 dragstart) const dragHandle = $('div', { class: 'mc-hover-ui drag-handle', draggable: 'true', title: tx('拖动以排序 / 跨组', 'Drag to reorder / move group', 'Ziehen zum Sortieren / Verschieben in eine andere Gruppe'), style: { position: 'absolute', top: '6px', left: '50%', right: 'auto', bottom: 'auto', transform: 'translateX(-50%)', zIndex: '25', padding: '5px 14px', borderRadius: '10px', background: 'rgba(255,255,255,.86)', color: 'var(--text-secondary)', cursor: 'grab', fontSize: '13px', userSelect: 'none', letterSpacing: '2px', lineHeight: '1', fontFamily: 'sans-serif', }, ondragstart: (e) => { cardDragRoomId = room.id; e.dataTransfer.setData('text/room-id', room.id); e.dataTransfer.effectAllowed = 'move'; // 自定义拖拽缩略图(用未加 dragging 状态的 card 拍快照) try { e.dataTransfer.setDragImage(card, card.offsetWidth / 2, 24); } catch (_) {} // 推迟到下一帧再加 .dragging 类,避免缩略图被半透明状态污染 setTimeout(() => { if (cardDragRoomId === room.id) card.classList.add('dragging'); }, 0); dragHandle.style.cursor = 'grabbing'; }, ondragend: () => { card.classList.remove('dragging'); dragHandle.style.cursor = 'grab'; // 清掉所有 drop indicator document.querySelectorAll('.cam-card').forEach(c => c.classList.remove('drop-before', 'drop-after')); clearFocusDropIndicators(); finishRoomDrag(); }, html: trustedHtml(iconSvg('drag', 15)), }); // 操作条(右上,hover 显示) const opsRow = $('div', { class: 'mc-hover-ui ops-row', style: { position: 'absolute', top: '10px', right: '10px', left: 'auto', bottom: 'auto', zIndex: '25', display: 'flex', gap: '6px', background: 'transparent', width: 'auto', height: 'auto' } }); const mkOp = (iconName, title, onclick, opts = {}) => setElementHint($('button', { class: 'icon-btn' + (opts.danger ? ' danger' : '') + (opts.extra ? ' ops-extra' : ''), title, html: trustedHtml(iconSvg(iconName, 15)), onclick: (e) => { e.stopPropagation(); onclick(e); }, }), title); const playBtn = mkOp('pause', t('opPause'), () => { service.togglePause(room.id); requestAnimationFrame(() => updateCardButtons(room.id)); }); const refreshBtn = mkOp('refresh', t('opRefresh'), () => service.refresh(room.id), { extra: true }); const muteBtn = mkOp(room.muted ? 'volume' : 'volumeOff', t('opMuteToggle'), () => { const target = store.state.rooms.find(r => r.id === room.id); if (!target) return; store.patchRoom(room.id, { muted: !target.muted }); requestAnimationFrame(() => applyMute(room.id)); }); const recordBtn = ROOMGRID_CAPTURE_TOOLS_ENABLED ? mkOp('record', t('opRecordStart'), () => toggleCardRecording(room.id), { extra: true }) : null; const pipBtn = mkOp('pip', t('opPiP'), async () => { const v = cardMap.get(room.id)?.video; if (!v) return; try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await v.requestPictureInPicture(); } catch (_) {} }, { extra: true }); const fullBtn = mkOp('expand', t('opFullscreen'), () => { document.fullscreenElement ? document.exitFullscreen() : card.requestFullscreen().catch(() => {}); }, { extra: true }); const moreOpsBtn = mkOp('more', t('moreOps'), (ev) => openCardOpsMenu(ev, room.id, card)); const removeViewLabel = tx('从当前视图移除直播', 'Remove stream from this view', 'Stream aus dieser Ansicht entfernen'); const removeBtn = setElementHint($('button', { type: 'button', class: 'cam-remove-view', 'aria-label': removeViewLabel, onclick: e => { e.stopPropagation(); if (store.state.settings.viewMode === 'focus') removeFocusRoomFromSlot(room.id, card.dataset.focusSlot); else removeGridRoomFromSlot(room.id, card.dataset.gridSlot); }, }, '×'), removeViewLabel); playBtn.classList.add('quick-op', 'quick-play'); muteBtn.classList.add('quick-op', 'quick-mute'); refreshBtn.classList.add('quick-op', 'quick-refresh', 'quick-optional'); fullBtn.classList.add('quick-op', 'quick-full', 'quick-optional'); moreOpsBtn.classList.add('quick-op', 'quick-more'); opsRow.append(...[playBtn, muteBtn, refreshBtn, recordBtn, fullBtn, moreOpsBtn].filter(Boolean)); // 状态文字(中央覆盖层) const statusEl = $('div', { class: 'status-layer' }); card.append(badge, name, opsRow, dragHandle, audioMeter, statusEl, removeBtn); // —— 双击全屏 —— card.addEventListener('mouseenter', () => { try { card.style.filter = 'none'; card.style.opacity = '1'; const v = card.querySelector('.cam-video'); if (v) { applyVideoTransform(room.id); v.style.opacity = '1'; v.controls = false; v.removeAttribute('controls'); } } catch (_) {} }); card.addEventListener('dblclick', (e) => { if (e.target.closest('.icon-btn') || e.target.closest('.drag-handle')) return; const transform = getVideoTransform(room.id); if (transform.zoom !== 1 || transform.x || transform.y) { e.preventDefault(); e.stopPropagation(); patchVideoTransform(room.id, { zoom: 1, x: 0, y: 0 }); return; } document.fullscreenElement ? document.exitFullscreen() : card.requestFullscreen().catch(() => {}); }); card.addEventListener('contextmenu', (e) => { if (e.target.closest('.icon-btn') || e.target.closest('.drag-handle')) return; e.preventDefault(); openCardOpsMenu(e, room.id, card); }); // —— 卡片作为 drop 目标(dragover/drop 不会被 video 拦截)—— card.addEventListener('dragover', (e) => { if (handleSidebarSlotDrop(e, card)) return; const focusId = focusDragRoomId || e.dataTransfer?.getData('text/roomgrid-focus-id'); if (focusId && store.state.settings.viewMode === 'focus' && card.dataset.focusSlot !== undefined) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; setFocusDropTarget(card); return; } const draggingId = cardDragRoomId || document.querySelector('.cam-card.dragging')?.dataset.roomId; if (!draggingId) return; if (draggingId === room.id) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; // 计算插入位置:鼠标在卡片左/上半 → before,右/下半 → after const rect = card.getBoundingClientRect(); // grid 布局可能横可能竖,取占比更大的轴 const useHoriz = rect.width >= rect.height; const ratio = useHoriz ? (e.clientX - rect.left) / rect.width : (e.clientY - rect.top) / rect.height; const before = ratio < 0.5; // 仅切换两个 class,避免反复 toggle 导致动画抖动 if (before) { if (!card.classList.contains('drop-before')) { card.classList.add('drop-before'); card.classList.remove('drop-after'); } } else { if (!card.classList.contains('drop-after')) { card.classList.add('drop-after'); card.classList.remove('drop-before'); } } }); card.addEventListener('dragleave', (e) => { // 只有真的离开卡片才移除(避免子元素事件冒泡误清) if (!card.contains(e.relatedTarget)) { card.classList.remove('drop-before', 'drop-after'); card.classList.remove('focus-drop-target'); setFocusDropCopy(card, 'focusDropHere'); } }); card.addEventListener('drop', (e) => { if (handleSidebarSlotDrop(e, card, true)) return; const focusId = focusDragRoomId || e.dataTransfer.getData('text/roomgrid-focus-id'); if (focusId && store.state.settings.viewMode === 'focus' && card.dataset.focusSlot !== undefined) { e.preventDefault(); e.stopPropagation(); setFocusDropTarget(card); assignFocusRoomToSlot(focusId, card.dataset.focusSlot); focusDragRoomId = ''; clearFocusDropIndicators(); return; } e.preventDefault(); const fromId = e.dataTransfer.getData('text/room-id'); const rect = card.getBoundingClientRect(); const isBefore = (rect.width >= rect.height ? (e.clientX - rect.left) / rect.width : (e.clientY - rect.top) / rect.height) < .5; card.classList.remove('drop-before', 'drop-after'); if (!fromId || fromId === room.id) return; reorderByDrop(fromId, room.id, isBefore ? 'before' : 'after'); }); installCardZoomHandlers(card, room.id); cardMap.set(room.id, { root: card, video: null, statusEl, badge, playBtn, muteBtn, recordBtn, removeBtn, audioMeter, audioMeterMode, audioMeterFill, audioMeterValue, resizeObserver: null }); applyAudioMeterVisibility(room.id); // —— 响应式:根据卡片宽度自动加 .compact / .tiny class —— // 避免按钮 + pill + 名字标签在小卡片时互相覆盖 try { const ro = new ResizeObserver(entries => { for (const entry of entries) { const w = entry.contentRect.width; card.classList.toggle('compact', w < 320); card.classList.toggle('tiny', w < 230); } }); ro.observe(card); const entry = cardMap.get(room.id); if (entry) entry.resizeObserver = ro; } catch (_) { /* 旧浏览器忽略 */ } // —— focus 模式:单击非主屏卡片(缩略图)→ 设为主屏 —— card.addEventListener('click', (e) => { if (store.state.settings.viewMode !== 'focus') return; if (e.target.tagName === 'VIDEO') return; // 让 video 控件正常响应 if (e.target.closest('.icon-btn') || e.target.closest('.drag-handle')) return; if (store.state.settings.focusedRoomId === room.id) return; // v15.4:新主屏布局的右侧 / 下方副窗口也可以直接点击切主屏。 const p = card.parentElement; const canPromote = p?.classList.contains('thumbs-row') || p?.classList.contains('focus-side-row') || p?.classList.contains('focus-bottom-row'); if (!canPromote) return; focusRoom(room.id); }); return card; } function updateCardButtons(id) { const c = cardMap.get(id); if (!c) return; const room = store.state.rooms.find(r => r.id === id); const paused = service.isPaused(id); if (c.playBtn) { setTrustedHtml(c.playBtn, trustedHtml(iconSvg(paused ? 'play' : 'pause', 15))); setElementHint(c.playBtn, paused ? t('opResume') : t('opPause')); } if (c.muteBtn && room) { setTrustedHtml(c.muteBtn, trustedHtml(iconSvg(room.muted ? 'volume' : 'volumeOff', 15))); setElementHint(c.muteBtn, room.muted ? tx('取消静音', 'Unmute', 'Ton einschalten') : tx('静音', 'Mute', 'Stummschalten')); } if (c.removeBtn) setElementHint(c.removeBtn, tx('从当前视图移除直播', 'Remove stream from this view', 'Stream aus dieser Ansicht entfernen')); const rec = recordings.get(id); if (c.recordBtn) { setTrustedHtml(c.recordBtn, trustedHtml(iconSvg(rec ? 'stop' : 'record', 15))); setElementHint(c.recordBtn, rec?.waitingForSource ? t('recordingWaiting') : (rec ? t('opRecordStop') : t('opRecordStart'))); c.recordBtn.classList.toggle('recording', !!rec); c.recordBtn.classList.toggle('waiting', !!rec?.waitingForSource); } c.root.classList.toggle('recording', !!rec); c.root.classList.toggle('recording-waiting', !!rec?.waitingForSource); } function captureCardScreenshot(roomId) { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; const c = cardMap.get(roomId); const video = c?.video; if (!video || !video.videoWidth || !video.videoHeight) { toast(t('captureFailed')); return; } try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob(async (blob) => { if (!blob) { toast(t('captureFailed')); return; } const fileName = `camclient-${safeFilePart(roomId)}-${stampForFile()}.png`; downloadBlob(blob, fileName); try { await RoomGridModelMedia.importFile(roomId, blob, { quality: 'original', fileName }); } catch (_) {} toast(t('screenshotSaved')); }, 'image/png'); } catch (err) { console.warn('[RoomGrid] screenshot failed', err); try { Diagnostics.report('screenshot', err, 'capture failed', 'warning'); } catch (_) {} toast(t('captureFailed')); } } function getVideoCaptureStream(video) { if (!video) return null; if (typeof video.captureStream === 'function') return video.captureStream(); if (typeof video.mozCaptureStream === 'function') return video.mozCaptureStream(); return null; } function recordingSegmentMs() { return clampInt(store.state.settings.recordingSegmentMinutes, 1, 180, 10) * 60 * 1000; } function recordingVideoBitrate() { return clampInt(store.state.settings.recordingVideoBitrate, 500000, 20000000, 6000000); } function recordingMimeType() { if (typeof MediaRecorder === 'undefined') return ''; const mimes = ['video/mp4;codecs=h264,aac', 'video/mp4;codecs=avc1,mp4a.40.2', 'video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm']; return mimes.find(m => { try { return MediaRecorder.isTypeSupported(m); } catch (_) { return false; } }) || ''; } function recordingFileExt(mimeType) { return String(mimeType || '').includes('mp4') ? 'mp4' : 'webm'; } function recordingReasonSuffix(reason) { if (reason === 'source-loss') return '-source-paused'; if (reason === 'manual' || reason === 'final') return '-final'; return ''; } function getRecordingVideo(roomId) { const c = cardMap.get(roomId); if (c?.video) return c.video; try { return document.querySelector(`video[data-multicam-room-id="${String(roomId).replace(/"/g, '')}"]`); } catch (_) { return null; } } function createRecordingStream(video) { const source = getVideoCaptureStream(video); if (!source) return null; const videoTracks = source.getVideoTracks().filter(track => track.readyState === 'live'); const audioTracks = source.getAudioTracks().filter(track => track.readyState === 'live'); if (!videoTracks.length || typeof MediaRecorder === 'undefined') { try { source.getTracks().forEach(track => track.stop()); } catch (_) {} return null; } return new MediaStream([...videoTracks, ...audioTracks]); } function cleanupRecordingStream(rec) { try { rec.stream?.getTracks?.().forEach(track => track.stop()); } catch (_) {} rec.stream = null; } function saveRecordingSegment(roomId, rec, reason = 'segment') { const chunks = Array.isArray(rec?.chunks) ? rec.chunks.filter(chunk => chunk && chunk.size) : []; if (!chunks.length) return false; const mimeType = rec.mimeType || 'video/webm'; const blob = new Blob(chunks, { type: mimeType }); const part = String(rec.segmentIndex || 1).padStart(3, '0'); const ext = recordingFileExt(mimeType); downloadBlob(blob, `camclient-${safeFilePart(roomId)}-${stampForFile()}-part${part}${recordingReasonSuffix(reason)}.${ext}`); rec.savedSegments = (Number(rec.savedSegments) || 0) + 1; rec.savedBytes = (Number(rec.savedBytes) || 0) + blob.size; rec.lastSavedAt = Date.now(); recordingLog.unshift({ roomId, reason, ts: Date.now(), size: blob.size, part, ext }); recordingLog.splice(80); return true; } function finishCurrentRecordingSegment(roomId, rec, reason) { if (!rec || rec.stopping) return; try { clearTimeout(rec.timer); } catch (_) {} rec.timer = null; rec.stopReason = reason; const recorder = rec.recorder; if (recorder && recorder.state !== 'inactive') { rec.stopping = true; try { recorder.requestData?.(); } catch (_) {} try { recorder.stop(); } catch (_) { handleRecorderStop(roomId, rec); } } else { handleRecorderStop(roomId, rec); } } function handleRecorderStop(roomId, rec) { if (!rec) return; const reason = rec.stopReason || (rec.manualStop ? 'manual' : 'segment'); try { clearTimeout(rec.timer); } catch (_) {} rec.timer = null; rec.stopping = false; const hadData = saveRecordingSegment(roomId, rec, reason); cleanupRecordingStream(rec); rec.recorder = null; rec.chunks = []; if (recordings.get(roomId) !== rec) return; if (rec.manualStop || reason === 'final' || reason === 'manual') { recordings.delete(roomId); setRecordingIntent(roomId, false); updateCardButtons(roomId); if (!rec.silentStop) toast(hadData ? t('recordingFinalSaved') : t('recordingNoData')); return; } if (reason === 'source-loss') { rec.waitingForSource = true; updateCardButtons(roomId); if (!rec.sourceLossToastShown && !rec.silentStop) { rec.sourceLossToastShown = true; toast(t('recordingPausedSource')); } return; } if (reason === 'segment') { if (hadData && !rec.silentStop) toast(t('recordingSegmentSaved')); if (!startRecordingSegment(roomId, rec)) { rec.waitingForSource = true; updateCardButtons(roomId); if (!rec.sourceLossToastShown && !rec.silentStop) { rec.sourceLossToastShown = true; toast(t('recordingPausedSource')); } } } } function startRecordingSegment(roomId, rec) { const video = getRecordingVideo(roomId); const stream = createRecordingStream(video); if (!stream) return false; const mimeType = recordingMimeType(); const options = { videoBitsPerSecond: recordingVideoBitrate() }; if (mimeType) options.mimeType = mimeType; let recorder; try { recorder = new MediaRecorder(stream, options); } catch (_) { try { stream.getTracks().forEach(track => track.stop()); } catch (e) {} return false; } rec.roomId = roomId; rec.stream = stream; rec.recorder = recorder; rec.mimeType = mimeType || 'video/webm'; rec.chunks = []; rec.startedAt = Date.now(); rec.segmentIndex = (Number(rec.segmentIndex) || 0) + 1; rec.waitingForSource = false; rec.sourceLossToastShown = false; rec.manualStop = false; rec.silentStop = false; rec.stopReason = ''; recorder.ondataavailable = ev => { if (ev.data && ev.data.size) rec.chunks.push(ev.data); }; recorder.onstop = () => handleRecorderStop(roomId, rec); try { recorder.start(1000); } catch (_) { cleanupRecordingStream(rec); rec.recorder = null; rec.chunks = []; return false; } rec.timer = setTimeout(() => { const current = recordings.get(roomId); if (current === rec && !rec.manualStop && !rec.waitingForSource) { finishCurrentRecordingSegment(roomId, rec, 'segment'); } }, recordingSegmentMs()); updateCardButtons(roomId); return true; } function startCardRecording(roomId, options = {}) { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; if (recordings.has(roomId)) return; const opts = options && typeof options === 'object' ? options : {}; if (!opts.skipConfirm && !confirm(t('recordingConsent'))) return; const rec = { roomId, segmentIndex: 0, chunks: [] }; recordings.set(roomId, rec); if (!startRecordingSegment(roomId, rec)) { if (opts.waitForSource) { rec.waitingForSource = true; rec.sourceLossToastShown = true; rec.startedAt = Date.now(); setRecordingIntent(roomId, true); updateCardButtons(roomId); return; } recordings.delete(roomId); updateCardButtons(roomId); toast(t('recordingUnsupported')); return; } setRecordingIntent(roomId, true); toast(t('recordingStarted')); } function pauseRecordingForSourceLoss(roomId, opts = {}) { const rec = recordings.get(roomId); if (!rec || rec.manualStop) return false; if (rec.waitingForSource && !rec.recorder) return false; rec.waitingForSource = true; rec.silentStop = !!opts.silent; finishCurrentRecordingSegment(roomId, rec, 'source-loss'); updateCardButtons(roomId); return true; } function resumeWaitingRecording(roomId) { const rec = recordings.get(roomId); if (!rec || !rec.waitingForSource || rec.manualStop || rec.recorder) return false; if (!startRecordingSegment(roomId, rec)) return false; toast(t('recordingResumed')); return true; } function stopCardRecording(roomId, options = false) { const rec = recordings.get(roomId); if (!rec) return; const opts = typeof options === 'boolean' ? { silent: options, final: true } : (options || {}); rec.manualStop = true; rec.waitingForSource = false; rec.silentStop = !!opts.silent; if (rec.recorder) { finishCurrentRecordingSegment(roomId, rec, opts.final === false ? 'manual' : 'final'); } else { cleanupRecordingStream(rec); recordings.delete(roomId); setRecordingIntent(roomId, false); updateCardButtons(roomId); if (!rec.silentStop) toast(t('recordingNoData')); } } function toggleCardRecording(roomId) { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; if (recordings.has(roomId)) stopCardRecording(roomId); else startCardRecording(roomId); } function stopAllRecordings(options = {}) { [...recordings.keys()].forEach(id => stopCardRecording(id, options)); } function openRecordingSettings() { openRecordingSettingsPanel(); } function fmtDuration(ms) { const sec = Math.max(0, Math.floor(Number(ms || 0) / 1000)); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; return h ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; } function fmtBytes(bytes) { const n = Math.max(0, Number(bytes) || 0); if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`; return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`; } let closeActiveToolPanel = null; function openToolPanel(title, build) { closeActiveToolPanel?.(); closeTransientUi(); document.querySelectorAll('.roomgrid-modal-backdrop').forEach(el => { try { el.remove(); } catch (_) {} }); const backdrop = $('div', { class: 'roomgrid-modal-backdrop' }); const panel = $('div', { class: 'roomgrid-modal', style: { width: 'min(760px, calc(100vw - 36px))', maxHeight: 'min(780px, calc(100vh - 36px))', overflowY: 'auto' } }); const head = $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', marginBottom: '10px' } }, [ $('div', { class: 'roomgrid-modal-title', style: { marginBottom: '0' } }, title), $('button', { class: 'ctrl-btn', onclick: () => close() }, '×'), ]); const body = $('div'); let cleanup = null; let closed = false; let removalObserver = null; const close = () => { if (closed) return; closed = true; removalObserver?.disconnect(); removalObserver = null; if (closeActiveToolPanel === close) closeActiveToolPanel = null; const release = cleanup; cleanup = null; try { release?.(); } catch (_) {} try { backdrop.remove(); } catch (_) {} }; closeActiveToolPanel = close; backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); }); panel.append(head, body); backdrop.appendChild(panel); document.body.appendChild(backdrop); // Other dialogs/routes may remove modal backdrops without this close path. if (typeof MutationObserver === 'function') { removalObserver = new MutationObserver(() => { if (!backdrop.isConnected) close(); }); removalObserver.observe(document.body, { childList: true }); } try { const release = build?.(body, close); if (typeof release === 'function') { // A builder may close or replace its panel before returning cleanup. if (closed) { try { release(); } catch (_) {} } else cleanup = release; } } catch (error) { close(); throw error; } return { body, close }; } async function openStatisticsPanel() { const panel = openToolPanel(t('statisticsTitle'), (body) => { body.style.minHeight = '520px'; const tabBar = $('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '10px', borderBottom: '1px solid var(--border)', paddingBottom: '8px' } }); const content = $('div', { style: { minHeight: '390px' } }); const tabs = [ ['overview', t('statisticsOverview')], ['ranking', t('statisticsRanking')], ['profile', t('statisticsProfile')], ['data', t('statisticsData')], ]; let activeTab = 'overview'; let selectedId = ''; let models = []; let rankingDays = 0; const btnStyle = { padding: '7px 10px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-input)', color: 'var(--text)', cursor: 'pointer', fontSize: '11px' }; const actionBtn = (text, onclick, primary = false) => $('button', { class: `ctrl-btn${primary ? ' primary' : ''}`, onclick }, text); const formatWatch = ms => fmtDuration(ms); const formatDate = ts => ts ? new Date(ts).toLocaleString() : '—'; const watchedModels = () => models.filter(m => Number(m.totalMs) > 0).sort((a, b) => (Number(b.totalMs) || 0) - (Number(a.totalMs) || 0)); const aggregateTags = () => { const totals = new Map(); watchedModels().forEach(m => (m.tags || []).forEach(tag => totals.set(tag, (totals.get(tag) || 0) + Number(m.totalMs || 0)))); return [...totals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10); }; const aggregateHours = () => { const hours = Array.from({ length: 24 }, () => 0); watchedModels().forEach(m => (m.hours || []).forEach((value, index) => { hours[index % 24] += Number(value) || 0; })); return hours.map((value, hour) => ({ hour, value })).sort((a, b) => b.value - a.value).slice(0, 8); }; const section = (title, children) => $('div', { style: { marginTop: '12px' } }, [$( 'div', { style: { fontSize: '12px', fontWeight: '800', color: 'var(--text-secondary)', marginBottom: '6px' } }, title), ...children]); const empty = text => $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '14px' } }, text); const statCard = (label, value) => $('div', { style: { background: 'var(--bg-input)', border: '1px solid var(--border)', borderRadius: '10px', padding: '10px', minWidth: '120px', flex: '1' } }, [$( 'div', { style: { fontSize: '10px', color: 'var(--text-muted)', marginBottom: '4px' } }, label), $( 'div', { style: { fontSize: '18px', fontWeight: '800', color: 'var(--text)' } }, value)]); const makeList = (rows, emptyText) => rows.length ? $('div', { style: { display: 'grid', gap: '5px' } }, rows) : empty(emptyText); async function fetchPublicProfile(id) { const room = findRoomAny(id) || {}; try { const res = await fetch(`/${encodeURIComponent(id)}/`, { credentials: 'include', referrerPolicy: 'strict-origin-when-cross-origin' }); if (!res.ok) throw new Error('http ' + res.status); const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const aboutRoot = doc.querySelector('[data-testid="bio-tab-about-me-value"], [data-testid*="about" i][data-testid*="value" i]'); const bio = String((() => { if (!aboutRoot) return ''; const copy = aboutRoot.cloneNode(true); copy.querySelectorAll('a, script, style').forEach(node => node.remove()); return copy.textContent || ''; })()).trim().slice(0, 4000); const displayName = String(doc.querySelector('[data-testid="bio-tab-real-name-value"], [data-testid*="real-name" i][data-testid*="value" i]')?.textContent || id).trim().slice(0, 140) || id; const links = [...(aboutRoot?.querySelectorAll('a[href]') || [])].map(a => { try { const candidateUrl = new URL(a.getAttribute('href'), location.origin); if (candidateUrl.origin === location.origin && candidateUrl.pathname === '/external_link/') return new URL(candidateUrl.searchParams.get('url') || '').toString(); return candidateUrl.toString(); } catch (_) { return ''; } }).filter(url => { try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol) && parsed.hostname !== location.hostname; } catch (_) { return false; } }).slice(0, 40); const tags = [...doc.querySelectorAll('a[href*="/tags/"]')].map(a => a.textContent.trim()).filter(Boolean).slice(0, 80); await analytics.upsertProfile({ id, parserVersion: 2, displayName, bio, links, tags, sourceUrl: `/${encodeURIComponent(id)}/`, observedAt: Date.now(), viewerCount: room.viewerCount, lastStatus: room.lastStatus }); return analytics.getProfileSync(id); } catch (err) { console.warn('[RoomGrid] public profile read failed', err); return null; } } function renderTabs() { tabBar.replaceChildren(...tabs.map(([key, label]) => $('button', { style: { ...btnStyle, background: key === activeTab ? 'var(--accent)' : 'var(--bg-input)', color: key === activeTab ? '#fff' : 'var(--text)' }, onclick: () => { activeTab = key; render(); } }, label))); } function renderOverview() { const watched = watchedModels(); const totalMs = watched.reduce((sum, m) => sum + Number(m.totalMs || 0), 0); const sessionsTotal = watched.reduce((sum, m) => sum + Number(m.sessions || 0), 0); const cards = $('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } }, [ statCard(t('statisticsModels'), String(watched.length)), statCard(t('statisticsSessions'), String(sessionsTotal)), statCard(t('statisticsWatchTime'), formatWatch(totalMs)), ]); const tags = aggregateTags().map(([tag, ms]) => $('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '8px', padding: '5px 8px', background: 'var(--bg-input)', borderRadius: '7px', fontSize: '11px' } }, [$( 'span', {}, rawUiText(tag)), $( 'span', { style: { color: 'var(--text-muted)' } }, formatWatch(ms))])); const hours = aggregateHours().map(row => $('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '8px', padding: '5px 8px', background: 'var(--bg-input)', borderRadius: '7px', fontSize: '11px' } }, [$( 'span', {}, `${String(row.hour).padStart(2, '0')}:00`), $( 'span', { style: { color: 'var(--text-muted)' } }, formatWatch(row.value))])); content.replaceChildren( $('div', { class: 'roomgrid-modal-hint' }, `${t('statisticsSince')}: ${formatDate(analytics.startedAt())}`), cards, section(t('statisticsTopTags'), tags), section(t('statisticsTopHours'), hours), $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '16px' } }, t('statisticsObservedOnly')), ); } function renderRanking() { const cutoff = rankingDays > 0 ? Date.now() - rankingDays * 86400000 : 0; const watched = watchedModels().filter(m => !cutoff || Number(m.lastWatchedAt || 0) >= cutoff); const rows = watched.map((m, index) => $('button', { style: { display: 'grid', gridTemplateColumns: '28px minmax(0,1fr) 100px 62px', alignItems: 'center', gap: '8px', textAlign: 'left', width: '100%', padding: '8px', border: '1px solid var(--border)', background: 'var(--bg-input)', color: 'var(--text)', borderRadius: '8px', cursor: 'pointer' }, onclick: () => { selectedId = m.id; activeTab = 'details'; render(); } }, [$( 'span', { style: { color: 'var(--text-muted)', fontWeight: '800' } }, String(index + 1)), $( 'span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: '700' } }, m.id), $( 'span', { style: { color: 'var(--text-secondary)' } }, formatWatch(m.totalMs)), $( 'span', { style: { color: 'var(--text-muted)' } }, `${m.sessions || 0}×`)])); const rangeSel = $('select', { class: 'ctrl-input', style: { padding: '5px 7px', fontSize: '11px' }, value: String(rankingDays), onchange: e => { rankingDays = Number(e.target.value) || 0; renderRanking(); } }, [ $('option', { value: '0' }, tx('Gesamt', 'All time', 'Gesamt')), $('option', { value: '7' }, tx('7 Tage', '7 days', '7 Tage')), $('option', { value: '30' }, tx('30 Tage', '30 days', '30 Tage')), $('option', { value: '90' }, tx('90 Tage', '90 days', '90 Tage')), ]); content.replaceChildren( $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', marginTop: '12px' } }, [$( 'div', { style: { fontSize: '12px', fontWeight: '800', color: 'var(--text-secondary)' } }, t('statisticsRanking')), rangeSel]), watched.length ? $('div', { style: { display: 'grid', gap: '5px', marginTop: '6px' } }, rows) : empty(t('statisticsNoData')), ...(watched.length ? [empty(tx('Klicke auf ein Model für Details.', 'Click a model for details.', 'Klicke auf ein Model für Details.'))] : []), ); } function renderProfile() { const watched = watchedModels(); const top = watched.slice(0, 5).map(m => $('div', { style: { display: 'flex', justifyContent: 'space-between', padding: '5px 8px', background: 'var(--bg-input)', borderRadius: '7px', fontSize: '11px' } }, [$( 'span', {}, m.id), $( 'span', { style: { color: 'var(--text-muted)' } }, formatWatch(m.totalMs))])); const activeDays = new Set(watched.flatMap(m => Object.keys(m.days || {}))).size; const groupTotals = new Map(); watched.forEach(m => getRoomGroups(findRoomAny(m.id)).forEach(groupId => { const group = store.state.groups.find(g => g.id === groupId); const name = group?.system ? (groupId === DEFAULT_GROUP_ID ? t('groupAll') : groupId === FAVORITE_GROUP_ID ? t('groupFav') : groupId) : (group?.name || groupId); groupTotals.set(name, (groupTotals.get(name) || 0) + Number(m.totalMs || 0)); })); const topGroups = [...groupTotals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, ms]) => $('div', { style: { display: 'flex', justifyContent: 'space-between', padding: '5px 8px', background: 'var(--bg-input)', borderRadius: '7px', fontSize: '11px' } }, [$( 'span', {}, rawUiText(name)), $( 'span', { style: { color: 'var(--text-muted)' } }, formatWatch(ms))])); content.replaceChildren( $('div', { class: 'roomgrid-modal-hint' }, t('statisticsLocalOnly')), $('div', { class: 'roomgrid-modal-hint' }, t('statisticsObservedOnly')), section(t('statisticsProfile'), [ statCard(t('statisticsWatchTime'), formatWatch(watched.reduce((sum, m) => sum + Number(m.totalMs || 0), 0))), statCard(t('statisticsModels'), String(watched.length)), statCard(t('statisticsSessions'), String(watched.reduce((sum, m) => sum + Number(m.sessions || 0), 0))), statCard(tx('Aktive Tage', 'Active days', 'Aktive Tage'), String(activeDays)), ]), section(tx('Am meisten geschaut', 'Most watched', 'Am meisten geschaut'), top), section(tx('Meistgenutzte Gruppen', 'Most watched groups', 'Meistgenutzte Gruppen'), topGroups), $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '14px' } }, tx('Kontodaten wie Beitrittsdatum werden nur angezeigt, wenn Chaturbate sie sichtbar und eindeutig auf derselben Seite bereitstellt. Das Script erfindet keine Werte.', 'Account fields such as join date are shown only when Chaturbate exposes them clearly on the same page. The script does not invent values.', 'Kontodaten wie das Beitrittsdatum werden nur angezeigt, wenn Chaturbate sie auf derselben Seite eindeutig sichtbar macht.')), ); } async function renderDetails() { const model = selectedId ? await analytics.getModel(selectedId) : null; if (!model) { activeTab = 'ranking'; renderRanking(); return; } const profile = analytics.getProfileSync(model.id); const tags = [...new Set([...(model.tags || []), ...(profile?.tags || [])])]; const linkEls = (profile?.links || []).map(url => $('a', { href: url, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--accent)', fontSize: '11px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, url)); const observedHours = (model.hours || []).map((value, index) => ({ value: Number(value) || 0, hour: index % 24 })).sort((a, b) => b.value - a.value).filter(x => x.value > 0).slice(0, 6); const scheduleReady = Number(model.sessions || 0) >= 3 && observedHours.length > 0; content.replaceChildren( $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' } }, [$( 'div', { style: { fontSize: '17px', fontWeight: '800' } }, rawUiText(model.id)), actionBtn(t('statisticsOpenRoom'), () => { location.href = `/${encodeURIComponent(model.id)}/`; }, true)]), $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '5px' } }, `${t('statisticsWatchTime')}: ${formatWatch(model.totalMs)} · ${t('statisticsSessions')}: ${model.sessions || 0}`), $('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '10px' } }, [statCard(t('statisticsFirstWatched'), formatDate(model.firstWatchedAt)), statCard(t('statisticsLastWatched'), formatDate(model.lastWatchedAt)), statCard(tx('Durchschnittliche Sitzung', 'Average session', 'Durchschnittliche Sitzung'), formatWatch(model.sessions ? model.totalMs / model.sessions : 0)), statCard(t('statisticsObservedOnline'), formatDate(model.lastOnlineAt))]), section(t('statisticsTags'), [tags.length ? $('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '5px' } }, tags.map(tag => $('span', { style: { padding: '4px 7px', borderRadius: '999px', background: 'var(--accent-soft)', color: 'var(--accent)', fontSize: '10px' } }, rawUiText(tag)))) : empty(t('statisticsNoProfile'))]), section(t('statisticsTopHours'), [scheduleReady ? $('div', { style: { display: 'grid', gap: '5px' } }, observedHours.map(x => $('div', { style: { display: 'flex', justifyContent: 'space-between', padding: '5px 8px', background: 'var(--bg-input)', borderRadius: '7px', fontSize: '11px' } }, [`${String(x.hour).padStart(2, '0')}:00`, formatWatch(x.value)]))) : empty(`${t('statisticsNoSchedule')} · ${model.sessions || 0} ${t('statisticsSessions').toLowerCase()}`)]), section(t('statisticsBio'), [profile?.bio ? $('div', { style: { whiteSpace: 'pre-wrap', fontSize: '11px', lineHeight: '1.45', color: 'var(--text-secondary)', background: 'var(--bg-input)', border: '1px solid var(--border)', borderRadius: '8px', padding: '8px' } }, profile.bio) : empty(t('statisticsNoProfile'))]), section(t('statisticsLinks'), [linkEls.length ? $('div', { style: { display: 'grid', gap: '4px' } }, linkEls) : empty(t('statisticsNoProfile'))]), $('div', { class: 'roomgrid-modal-actions' }, [ actionBtn(profile ? t('statisticsRefresh') : t('statisticsDetails'), async () => { const fresh = await fetchPublicProfile(model.id); if (!fresh) toast(t('statisticsNoProfile')); render(); }), actionBtn(t('statisticsDeleteModel'), async () => { await analytics.deleteModel(model.id); selectedId = ''; activeTab = 'ranking'; toast(t('statisticsDeleted')); render(); }), actionBtn(tx('Zurück', 'Back', 'Zurück'), () => { activeTab = 'ranking'; render(); }), ]), ); } function renderData() { const enabled = analytics.isEnabled(); const importInput = $('input', { type: 'file', accept: 'application/json', style: { display: 'none' }, onchange: async e => { const file = e.target.files?.[0]; if (!file) return; try { if (file.size > 20 * 1024 * 1024) throw new Error('file too large'); const incoming = JSON.parse(await file.text()); if (!Array.isArray(incoming?.models) || incoming.models.length > ROOMGRID_DATA_LIMITS.modelRecords) throw new Error('invalid statistics file'); const summary = tx( `统计导入预览:${incoming.models.length} 个模型,${Array.isArray(incoming.events) ? incoming.events.length : 0} 个事件。继续将安全合并数据并先创建备份。`, `Statistics import preview: ${incoming.models.length} models and ${Array.isArray(incoming.events) ? incoming.events.length : 0} events. Continuing safely merges the data and creates a backup first.`, `Statistik-Importvorschau: ${incoming.models.length} Models und ${Array.isArray(incoming.events) ? incoming.events.length : 0} Ereignisse. Beim Fortfahren werden die Daten sicher zusammengeführt und vorher gesichert.`, `Vista previa de la importación de estadísticas: ${incoming.models.length} modelos y ${Array.isArray(incoming.events) ? incoming.events.length : 0} eventos. Al continuar, los datos se combinarán de forma segura después de crear una copia de seguridad.`, `Prévia da importação de estatísticas: ${incoming.models.length} modelos e ${Array.isArray(incoming.events) ? incoming.events.length : 0} eventos. Ao continuar, os dados serão mesclados com segurança após a criação de um backup.`, ); if (!confirm(summary)) return; await analytics.importData(incoming); toast(t('statisticsImported')); render(); } catch (err) { alert(tx('Statistikimport fehlgeschlagen: ', 'Statistics import failed: ', 'Statistikimport fehlgeschlagen: ') + err.message); } finally { try { importInput.remove(); } catch (_) {} } } }); content.replaceChildren( $('div', { class: 'roomgrid-modal-hint' }, t('statisticsEnableHint')), $('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', margin: '10px 0' } }, [ statCard(t('statisticsSince'), analytics.startedAt() ? formatDate(analytics.startedAt()) : '—'), statCard(t('statisticsModels'), String(models.length)), statCard(t('statisticsWatchTime'), formatWatch(models.reduce((sum, m) => sum + Number(m.totalMs || 0), 0))), ]), $('div', { class: 'roomgrid-modal-actions', style: { justifyContent: 'flex-start', flexWrap: 'wrap' } }, [ actionBtn(enabled ? t('statisticsDisable') : t('statisticsEnable'), async () => { await analytics.setEnabled(!enabled); store.patchSettings({ analyticsEnabled: !enabled }); toast(!enabled ? t('statisticsEnable') : t('statisticsDisabled')); render(); }, true), actionBtn(t('statisticsExport'), async () => { const data = await analytics.exportData(); downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `roomgrid-statistics-${Date.now()}.json`); toast(t('statisticsExported')); }), actionBtn(t('statisticsImport'), () => { document.body.appendChild(importInput); importInput.click(); }), actionBtn(t('statisticsDeleteAll'), async () => { if (!confirm(t('statisticsClearConfirm'))) return; await analytics.createBackup('before-clear'); await analytics.clearAll(); toast(t('statisticsDeleted')); render(); }), ]), ); } async function render() { renderTabs(); models = analytics.getModelsSync(); if (!analytics.isEnabled() && activeTab !== 'data') content.replaceChildren(empty(t('statisticsDisabled'))); else if (activeTab === 'overview') renderOverview(); else if (activeTab === 'ranking') renderRanking(); else if (activeTab === 'profile') renderProfile(); else if (activeTab === 'details') await renderDetails(); else renderData(); } render(); }); return panel; } function buildShareSnapshot() { const settings = store.state.settings; const slots = (settings.viewMode === 'focus' ? focusSlotIdsForCurrentLayout() : gridRoomsForView().map(room => room?.id || null)) .map(id => findRoomAny(id)?.sourceUrl ? null : id); // A share is deliberately NOT a backup. Only public room IDs and a // small layout whitelist leave this browser; no store/settings spreads. return sanitizeShareSnapshot({ format: 'camclient-share', version: 1, slots, layout: settings }); } function sanitizeShareSnapshot(input) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; // Legacy links are accepted only through the same whitelist. Their // private metadata is never imported or copied into the local store. const rawSlots = Array.isArray(source.slots) ? source.slots : (Array.isArray(source.rooms) ? source.rooms.map(room => room?.id) : []); const seen = new Set(); const slots = rawSlots.slice(0, MAX_LAYOUT_SLOTS).map(raw => { const id = normalizeUsername(raw); if (!isLikelyUsername(id) || seen.has(id)) return null; seen.add(id); return id; }); const rawLayout = source.layout || source.settings || {}; const keys = ['layoutSize', 'useCustomLayout', 'customLayoutRows', 'customLayoutCols', 'viewMode', 'focusMainPct', 'focusMainHPct', 'focusAspect', 'focusThumbSize', 'videoFit']; const picked = {}; for (const key of keys) if (Object.hasOwn(rawLayout, key)) picked[key] = rawLayout[key]; const safe = sanitizeState({ settings: picked }).settings; const layout = Object.fromEntries(keys.map(key => [key, safe[key]])); return { format: 'camclient-share', version: 1, slots, layout }; } function buildShareLink() { const url = new URL('/', location.origin); url.searchParams.set('multicam_mode', '1'); url.hash = 'roomgrid=' + encodeSharePayload(buildShareSnapshot()); return url.toString(); } async function openSharePanel() { openToolPanel(t('sharePanel'), (body) => { const link = buildShareLink(); const input = $('textarea', { class: 'roomgrid-modal-textarea', readonly: true, value: link, style: { minHeight: '120px' }, onclick: (e) => e.currentTarget.select(), }); body.append( $('div', { class: 'roomgrid-modal-hint' }, tx('链接仅包含当前窗口的公开房间名称和布局;不含备注、标签、个人设置或临时媒体链接。', 'The link contains only public room names in the current view and its layout; no notes, tags, personal settings or temporary media links.', 'Der Link enthält nur öffentliche Raumnamen der aktuellen Ansicht und deren Layout; keine Notizen, Tags, persönlichen Einstellungen oder temporären Medienlinks.')), input, $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn primary', onclick: async () => { await copyText(link); toast(t('copied')); } }, t('shareCopyLink')), ]), ); }); } function loadSharedWorkspaceFromHash() { const hash = String(location.hash || '').replace(/^#/, ''); const payload = hash.startsWith('roomgrid=') ? hash.slice('roomgrid='.length) : ''; if (!payload) return false; try { const shared = sanitizeShareSnapshot(decodeSharePayload(payload)); const ids = shared.slots.filter(Boolean); if (!ids.length) throw new Error('empty share'); if (store.state.groups.length >= 80 || store.state.rooms.length + ids.filter(id => !findRoomAny(id)).length > MAX_STORED_ROOMS) throw new Error('local storage capacity reached'); if (!confirm(tx(`将 ${ids.length} 个房间添加为新分组并应用此布局?现有房间、备注和设置会保留,导入前会创建备份。`, `Add ${ids.length} rooms as a new group and apply this layout? Existing rooms, notes and settings are preserved; a backup is created first.`, `${ids.length} Räume als neue Gruppe hinzufügen und dieses Layout anwenden? Bestehende Räume, Notizen und Einstellungen bleiben erhalten; vorher wird eine Sicherung erstellt.`))) return false; backupCurrentConfig(); const groupId = store.addGroup(tx('共享视图', 'Shared view', 'Geteilte Ansicht')); store.syncRoomsToGroup(ids, groupId); store.reorderRooms(ids, groupId); store.patchSettings({ ...shared.layout, activeGroup: groupId, pageIndex: 0, searchQuery: '', filter: { ...defaultState().settings.filter }, showRecordingOnly: false, focusedRoomId: shared.layout.viewMode === 'focus' ? ids[0] : null, focusRoomSlots: shared.layout.viewMode === 'focus' ? shared.slots : [], }); if (shared.layout.viewMode === 'grid') { store.patchSettings({ gridRoomSlots: { ...store.state.settings.gridRoomSlots, [gridViewContextKey()]: shared.slots } }); } store.flushPersistence(); toast(t('shareImported')); try { const url = new URL(location.href); url.hash = ''; history.replaceState(null, '', url.toString()); } catch (_) {} return true; } catch (err) { console.warn('[RoomGrid] shared workspace import failed', err); toast(t('shareInvalid')); return false; } } function currentPageRoomIds() { return pagedRooms(fullVisibleRooms()).map(r => r.id); } function currentGroupRoomIds() { return fullVisibleRooms().map(r => r.id); } function recordRoomIds(ids) { const clean = [...new Set((ids || []).map(normalizeUsername).filter(Boolean))]; if (!clean.length) return; if (!confirm(t('recordingConsent'))) return; clean.forEach(id => startCardRecording(id, { skipConfirm: true, waitForSource: true })); renderGrid(); } function recordCurrentPage() { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; recordRoomIds(currentPageRoomIds()); } function recordCurrentGroup() { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; recordRoomIds(currentGroupRoomIds()); } function recordOnlineRooms() { if (!ROOMGRID_CAPTURE_TOOLS_ENABLED) return false; recordRoomIds(fullVisibleRooms().filter(r => r.lastStatus === 'online').map(r => r.id)); } function pauseCurrentPage() { const ids = currentPageRoomIds(); service.pauseAll(ids); requestAnimationFrame(() => ids.forEach(updateCardButtons)); toast(t('pausedVisible')); } function openCurrentPageRooms() { currentPageRoomIds().forEach(id => openNoopener(location.origin + '/' + id + '/')); } function moveCurrentPageToGroup() { const name = prompt(t('newGroupPrompt')); if (!name || !name.trim()) return; const safeName = safeGroupName(name, ''); let group = store.state.groups.find(g => safeGroupName(g.name, g.id).toLowerCase() === safeName.toLowerCase()); const groupId = group?.id || store.addGroup(safeName); currentPageRoomIds().forEach(id => store.moveToGroup(id, groupId)); } function queueRecordingIntent(roomId) { roomId = normalizeUsername(roomId); if (!roomId || recordings.has(roomId)) return; recordings.set(roomId, { roomId, segmentIndex: 0, chunks: [], waitingForSource: true, sourceLossToastShown: true, startedAt: Date.now(), }); setRecordingIntent(roomId, true); updateCardButtons(roomId); if (findRoomAny(roomId)?.lastStatus === 'online') resumeWaitingRecording(roomId); else if (service.has(roomId)) service.refresh(roomId); else service.start(roomId); } function checkRecordingIntentRecovery() { const ids = loadRecordingIntents().filter(id => findRoomAny(id)); if (!ids.length) return; if (!confirm(t('recordingRecoverPrompt', ids.length))) { saveRecordingIntents([]); return; } ids.forEach(queueRecordingIntent); renderGrid(); } function renderRecordingCenterBody(body) { body.replaceChildren(); const actions = $('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' } }, [ $('button', { class: 'ctrl-btn primary', onclick: recordCurrentPage }, t('recordCurrentPage')), $('button', { class: 'ctrl-btn', onclick: recordCurrentGroup }, t('recordCurrentGroup')), $('button', { class: 'ctrl-btn', onclick: recordOnlineRooms }, t('recordOnlineRooms')), $('button', { class: 'ctrl-btn', onclick: () => { store.patchSettings({ showRecordingOnly: !store.state.settings.showRecordingOnly, pageIndex: 0 }); } }, store.state.settings.showRecordingOnly ? t('hideRecordingOnly') : t('showRecordingOnly')), $('button', { class: 'ctrl-btn danger', onclick: () => stopAllRecordings({ final: true }) }, t('stopAllRecordings')), ]); body.appendChild(actions); const rows = [...recordings.entries()]; if (!rows.length) { body.appendChild($('div', { class: 'roomgrid-modal-hint' }, t('recordingCenterEmpty'))); } else { const table = $('div', { style: { display: 'grid', gap: '8px' } }); rows.forEach(([id, rec]) => { const status = rec.waitingForSource ? t('recordingWaitingShort') : t('recordingActive'); const duration = rec.startedAt ? fmtDuration(Date.now() - rec.startedAt) : '0:00'; table.appendChild($('div', { style: { border: '1px solid var(--border)', borderRadius: '8px', padding: '10px', display: 'grid', gridTemplateColumns: '1fr auto', gap: '8px', alignItems: 'center' } }, [ $('div', {}, [ $('div', { style: { fontWeight: '750' } }, id), $('div', { style: { fontSize: '12px', color: 'var(--text-muted)', marginTop: '4px' } }, `${status} · ${t('recordingDuration')}: ${duration} · ${t('recordingSavedSegments')}: ${Number(rec.savedSegments) || 0} · ${t('recordingBitrate')}: ${(recordingVideoBitrate() / 1000000).toFixed(1)} Mbps`), ]), $('button', { class: 'ctrl-btn danger', onclick: () => stopCardRecording(id) }, t('opRecordStop')), ])); }); body.appendChild(table); } if (recordingLog.length) { body.appendChild($('div', { class: 'roomgrid-modal-title', style: { marginTop: '14px' } }, tx('最近保存', 'Recent saves', 'Kürzlich gespeichert'))); body.appendChild($('div', { style: { display: 'grid', gap: '4px', fontSize: '12px', color: 'var(--text-muted)' } }, recordingLog.slice(0, 12).map(log => $('div', {}, `${new Date(log.ts).toLocaleTimeString()} · ${log.roomId} · ${log.ext.toUpperCase()} · ${fmtBytes(log.size)}`)))); } } function openRecordingCenter() { openToolPanel(t('recordingCenter'), (body) => { renderRecordingCenterBody(body); const timer = setInterval(() => renderRecordingCenterBody(body), 1000); return () => clearInterval(timer); }); } function openRecordingSettingsPanel() { const curMinutes = clampInt(store.state.settings.recordingSegmentMinutes, 1, 180, 10); const curMbps = Math.round(recordingVideoBitrate() / 100000) / 10; openToolPanel(t('recordingSettingsTitle'), (body, close) => { const minutesInput = $('input', { class: 'ctrl-input', type: 'number', min: '1', max: '180', value: String(curMinutes), style: { width: '100%' } }); const bitrateInput = $('input', { class: 'ctrl-input', type: 'number', min: '0.5', max: '20', step: '0.5', value: String(curMbps), style: { width: '100%' } }); const exitWarn = $('input', { type: 'checkbox', checked: store.state.settings.recordingExitWarn !== false }); body.append( $('div', { class: 'roomgrid-modal-hint' }, t('recordingFormatHint')), $('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } }, [ $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [t('recordingSegmentPrompt'), minutesInput]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [t('recordingBitratePrompt'), bitrateInput]), ]), $('label', { class: 'toggle', style: { marginTop: '10px' } }, [exitWarn, t('recordingExitWarnToggle')]), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn primary', onclick: () => { const minutes = clampInt(minutesInput.value, 1, 180, curMinutes); const bitrate = clampInt(Number(bitrateInput.value) * 1000000, 500000, 20000000, recordingVideoBitrate()); store.patchSettings({ recordingSegmentMinutes: minutes, recordingVideoBitrate: bitrate, recordingExitWarn: !!exitWarn.checked }); toast(t('recordingSettingsSaved')); close(); } }, t('saveSettings')), ]), ); }); } function openLayoutSettings() { openToolPanel(t('layoutSettings'), (body, close) => { const layout = $('select', { class: 'ctrl-input' }, [ ...SUPPORTED_LAYOUT_SIZES.map(n => $('option', { value: String(n), selected: !store.state.settings.useCustomLayout && Number(store.state.settings.layoutSize) === n }, tx(`${n} 窗口`, `${n} rooms`, `${n} Räume`, `${n} salas`, `${n} salas`))), $('option', { value: 'custom', selected: !!store.state.settings.useCustomLayout }, tx('自定义', 'Custom grid', 'Freies Raster')), ]); const customRows = $('input', { class: 'ctrl-input', type: 'number', min: '1', max: '5', value: String(store.state.settings.customLayoutRows || 2) }); const customCols = $('input', { class: 'ctrl-input', type: 'number', min: '1', max: '5', value: String(store.state.settings.customLayoutCols || 2) }); const mainW = $('input', { class: 'ctrl-input', type: 'number', min: '45', max: '76', value: String(store.state.settings.focusMainPct || 62) }); const mainH = $('input', { class: 'ctrl-input', type: 'number', min: '44', max: '78', value: String(store.state.settings.focusMainHPct || 64) }); const thumb = $('input', { class: 'ctrl-input', type: 'number', min: '96', max: '260', value: String(store.state.settings.focusThumbSize || 150) }); const notify = $('input', { type: 'checkbox', checked: !!store.state.settings.notifyOnline }); const notifyOffline = $('input', { type: 'checkbox', checked: !!store.state.settings.notifyOffline }); const darkMode = $('input', { type: 'checkbox', checked: store.state.settings.theme === 'dark' }); const tipSound = $('input', { type: 'checkbox', checked: isTipSoundMuted() }); body.append( $('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '10px' } }, [ $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [tx('单页窗口数', 'Rooms per page', 'Räume pro Seite'), layout]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [t('hintThumbSize'), thumb]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [tx('自定义行数', 'Custom rows', 'Freie Zeilen'), customRows]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [tx('自定义列数', 'Custom columns', 'Freie Spalten'), customCols]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [tx('主屏宽度', 'Main width', 'Hauptbildschirmbreite'), mainW]), $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [tx('主屏高度', 'Main height', 'Hauptbildschirmhöhe'), mainH]), ]), $('label', { class: 'toggle', style: { marginTop: '10px' }, title: t('notifyTitle') }, [notify, t('notifyOnline')]), $('label', { class: 'toggle', style: { marginTop: '4px' }, title: t('notifyOfflineTitle') }, [notifyOffline, t('notifyOffline')]), $('label', { class: 'toggle', style: { marginTop: '10px' }, title: t('darkModeHint') }, [darkMode, t('darkMode')]), $('label', { class: 'toggle', style: { marginTop: '10px' }, title: t('tipSoundMuteHint') }, [tipSound, t('tipSoundMuteLabel')]), $('div', { class: 'roomgrid-modal-hint' }, t('tipSoundMuteHint')), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn', onclick: () => { close(); openFocusRoomManager(); } }, t('menuFocusRoomManager')), $('button', { class: 'ctrl-btn primary', onclick: async () => { const useCustomLayout = layout.value === 'custom'; const rows = clampInt(customRows.value, 1, 5, 2); const cols = clampInt(customCols.value, 1, 5, 2); if (useCustomLayout && rows * cols > MAX_LAYOUT_SLOTS) { alert(tx(`最多 ${MAX_LAYOUT_SLOTS} 个窗口。`, `Custom layouts support up to ${MAX_LAYOUT_SLOTS} rooms.`, `Freie Raster unterstützen höchstens ${MAX_LAYOUT_SLOTS} Räume.`, `Los diseños personalizados admiten hasta ${MAX_LAYOUT_SLOTS} salas.`, `Os layouts personalizados aceitam até ${MAX_LAYOUT_SLOTS} salas.`)); return; } if ((notify.checked || notifyOffline.checked) && !Notify.granted) { const ok = await Notify.request(); if (!ok) alert(t('permDenied')); } store.patchSettings({ useCustomLayout, layoutSize: useCustomLayout ? Number(store.state.settings.layoutSize || 4) : Number(layout.value), customLayoutRows: rows, customLayoutCols: cols, pageIndex: 0, focusMainPct: clampInt(mainW.value, 45, 76, 62), focusMainHPct: clampInt(mainH.value, 44, 78, 64), focusThumbSize: clampInt(thumb.value, 96, 260, 150), notifyOnline: !!notify.checked, notifyOffline: !!notifyOffline.checked, theme: darkMode.checked ? 'dark' : 'light', }); setTipSoundMuted(!!tipSound.checked); close(); } }, t('saveSettings')), ]), ); }); } function openModuleSettingsPanel() { openToolPanel(tx('模块', 'Modules', 'Module'), (body) => { const settings = loadSuiteModuleSettings(); const pageTools = $('input', { type: 'checkbox', checked: settings.pageTools !== false }); const audioMetersEnabled = $('input', { type: 'checkbox', checked: settings.audioMeters !== false }); body.append( $('div', { class: 'roomgrid-modal-hint' }, tx( '模块按页面类型启动。更改后会重新加载当前页面。', 'Modules start only on matching page types. Saving reloads the current page.', 'Module starten nur auf passenden Seitentypen. Nach dem Speichern wird die aktuelle Seite neu geladen.' )), $('label', { class: 'toggle', style: { marginTop: '8px' } }, [pageTools, tx('普通页面工具', 'Tools on normal pages', 'Werkzeuge auf normalen Seiten')]), $('label', { class: 'toggle', style: { marginTop: '8px' } }, [audioMetersEnabled, tx('真实声音电平', 'True audio-level meters', 'Echte Audiopegelanzeigen')]), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn primary', onclick: () => { const ok = setSuiteModuleEnabled('pageTools', pageTools.checked) && setSuiteModuleEnabled('audioMeters', audioMetersEnabled.checked); if (ok) location.reload(); } }, t('saveSettings')), ]), ); }); } function openFocusRoomManager() { openToolPanel(t('focusRoomManager'), (body, close) => { const size = layoutSize(); const current = Array.isArray(store.state.settings.focusRoomSlots) ? store.state.settings.focusRoomSlots : []; const visible = fullVisibleRooms(); const known = [...visible]; current.forEach(id => { const room = id ? findRoomAny(id) : null; if (room && !known.some(item => item.id === room.id)) known.push(room); }); const knownIds = new Set(known.map(room => room.id)); const used = new Set(); const initial = (hasCustomFocusLayout() ? current : pagedRooms(visible).map(room => room.id)) .slice(0, size) .map(id => { const clean = id && knownIds.has(id) && !used.has(id) ? id : null; if (clean) used.add(clean); return clean; }); let slots = Array.from({ length: size }, (_, idx) => initial[idx] || null); const rows = $('div', { style: { display: 'grid', gap: '8px', marginTop: '12px' } }); const summary = $('div', { class: 'roomgrid-modal-hint' }); const slotLabel = (idx) => focusSlotLabel(idx, size); const emptyOption = () => $('option', { value: '' }, `— ${t('focusRoomEmpty')} —`); const render = () => { summary.textContent = t('focusRoomSelected', slots.filter(Boolean).length, size); rows.replaceChildren(); for (let idx = 0; idx < size; idx++) { const select = $('select', { class: 'ctrl-input', title: slotLabel(idx) }, [ emptyOption(), ...known.map(room => $('option', { value: room.id }, `${room.id} · ${statusMeta(room.lastStatus).label}`)), ]); select.value = slots[idx] || ''; select.addEventListener('change', () => { const next = select.value || null; const other = next ? slots.indexOf(next) : -1; const previous = slots[idx]; if (other >= 0 && other !== idx) slots[other] = previous; slots[idx] = next; render(); }); rows.appendChild($('label', { style: { display: 'grid', gridTemplateColumns: '150px minmax(0, 1fr)', gap: '8px', alignItems: 'center' } }, [ $('span', { style: { fontSize: '12px', color: 'var(--text-muted)', fontWeight: '700' } }, `${idx + 1}. ${slotLabel(idx)}`), select, ])); } }; body.append( $('div', { class: 'roomgrid-modal-hint' }, t('focusRoomManagerHint')), summary, visible.length ? rows : $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '12px' } }, t('focusRoomNoRooms')), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: () => { store.patchSettings({ focusRoomSlots: [], focusedRoomId: null, pageIndex: 0 }); close(); } }, t('focusRoomAuto')), $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn primary', onclick: () => { store.patchSettings({ focusRoomSlots: slots, focusedRoomId: slots[0] || null, viewMode: 'focus', pageIndex: 0 }); close(); } }, t('focusRoomApply')), ]), ); if (visible.length) render(); }); } function currentMulticamSceneSnapshot(name, existingId = '') { const settings = store.state.settings; const viewMode = settings.viewMode === 'focus' ? 'focus' : 'grid'; const sceneSlots = viewMode === 'focus' ? focusSlotIdsForCurrentLayout() : gridRoomsForView(pagedRooms(fullVisibleRooms())).map(room => room?.id || null); return { id: existingId || `scene_${uuid().replace(/^g_/u, '')}`, name: String(name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60), workspaceId: settings.activeWorkspaceId || DEFAULT_WORKSPACE_ID, createdAt: Date.now(), modifiedAt: Date.now(), viewMode, layoutSize: layoutSize(), useCustomLayout: !!settings.useCustomLayout, customLayoutRows: Number(settings.customLayoutRows) || 2, customLayoutCols: Number(settings.customLayoutCols) || 2, focusedRoomId: viewMode === 'focus' ? (settings.focusedRoomId || sceneSlots.find(Boolean) || '') : '', focusRoomSlots: viewMode === 'focus' ? sceneSlots : [], gridRoomSlots: viewMode === 'grid' ? sceneSlots : [], audioRoutingMode: settings.audioRoutingMode || 'manual', audioSoloRoomId: settings.audioSoloRoomId || '', }; } function saveCurrentMulticamScene(name) { const cleanName = String(name || '').replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, 60); if (!cleanName) return false; store.update(state => { state.settings.multicamScenes ||= []; const workspaceId = state.settings.activeWorkspaceId || DEFAULT_WORKSPACE_ID; const existing = state.settings.multicamScenes.find(scene => scene.workspaceId === workspaceId && scene.name.toLocaleLowerCase() === cleanName.toLocaleLowerCase()); const snapshot = currentMulticamSceneSnapshot(cleanName, existing?.id || ''); if (existing) Object.assign(existing, snapshot, { createdAt: existing.createdAt || snapshot.createdAt }); else { if (state.settings.multicamScenes.length >= 24) state.settings.multicamScenes.shift(); state.settings.multicamScenes.push(snapshot); } state.settings.activeMulticamSceneId = snapshot.id; }, 'settings:multicamScenes'); toast(tx(`场景“${cleanName}”已保存`, `Scene “${cleanName}” saved`, `Szene „${cleanName}“ gespeichert`)); return true; } function applyMulticamScene(scene) { if (!scene?.id) return false; removedStreamNotice?.remove(); const known = new Set(allRoomsForView().map(room => room.id)); const cleanSlots = values => (Array.isArray(values) ? values : []).slice(0, MAX_LAYOUT_SLOTS).map(id => known.has(normalizeUsername(id)) ? normalizeUsername(id) : null); const base = { viewMode: scene.viewMode === 'focus' ? 'focus' : 'grid', layoutSize: SUPPORTED_LAYOUT_SIZES.includes(Number(scene.layoutSize)) ? Number(scene.layoutSize) : 4, useCustomLayout: !!scene.useCustomLayout, customLayoutRows: clampInt(scene.customLayoutRows, 1, 5, 2), customLayoutCols: clampInt(scene.customLayoutCols, 1, 5, 2), pageIndex: 0, audioRoutingMode: ['manual', 'solo', 'focus'].includes(scene.audioRoutingMode) ? scene.audioRoutingMode : 'manual', audioSoloRoomId: known.has(normalizeUsername(scene.audioSoloRoomId)) ? normalizeUsername(scene.audioSoloRoomId) : '', activeMulticamSceneId: scene.id, }; if (base.viewMode === 'focus') { const focusRoomSlots = cleanSlots(scene.focusRoomSlots); store.patchSettings({ ...base, focusRoomSlots, focusedRoomId: known.has(normalizeUsername(scene.focusedRoomId)) ? normalizeUsername(scene.focusedRoomId) : (focusRoomSlots.find(Boolean) || null) }); } else { store.patchSettings(base); const gridRoomSlots = cleanSlots(scene.gridRoomSlots); requestAnimationFrame(() => store.patchSettings({ gridRoomSlots: { ...(store.state.settings.gridRoomSlots || {}), [gridViewContextKey()]: gridRoomSlots } })); } requestAnimationFrame(() => { applyAllAudioRouting(); service.refreshQuality(); }); toast(tx(`场景“${scene.name}”已加载`, `Scene “${scene.name}” loaded`, `Szene „${scene.name}“ geladen`)); return true; } function removeMulticamScene(id) { store.update(state => { const before = state.settings.multicamScenes?.length || 0; state.settings.multicamScenes = (state.settings.multicamScenes || []).filter(scene => scene.id !== id); if (state.settings.activeMulticamSceneId === id) state.settings.activeMulticamSceneId = ''; return before !== state.settings.multicamScenes.length; }, 'settings:multicamScenes'); } function openMultiCamCenter(initialSection = '') { openToolPanel(tx('多摄像头控制中心', 'MultiCam control center', 'MultiCam-Kontrollzentrum'), (body, close) => { body.dataset.roomgridMulticamCenter = '1'; const grid = $('div', { class: 'multicam-center-grid' }); const healthList = $('div', { 'data-multicam-health-list': '1' }); const healthSummary = $('div', { class: 'roomgrid-modal-hint' }); let lastHealthView = ''; const statusLabels = { playing: tx('播放中', 'Playing', 'Spielt'), paused: tx('暂停', 'Paused', 'Pausiert'), stalled: tx('缓冲中', 'Buffering', 'Puffert'), offline: 'Offline', connecting: tx('连接中', 'Connecting', 'Verbindet'), recovering: tx('重新连接', 'Reconnecting', 'Verbindet erneut'), 'play-blocked': tx('需要手动播放', 'Press play', 'Wiedergabe starten'), error: tx('播放错误', 'Playback error', 'Wiedergabefehler'), }; const renderHealth = () => { const displayedIds = [...cardMap].filter(([, card]) => card.root?.isConnected).map(([id]) => id); const snapshot = service.getHealthSnapshot(displayedIds).map(item => { const room = findRoomAny(item.id); const state = item.paused ? 'paused' : room?.lastStatus === 'offline' ? 'offline' : item.connection.endsWith('-error') ? 'error' : item.connection; return { ...item, label: statusLabels[state] || statusLabels.connecting, score: Math.round(Number(item.healthScore) || 0) }; }); const viewKey = JSON.stringify(snapshot.map(item => [item.id, item.label, item.score, item.healthState])); if (viewKey === lastHealthView) return; lastHealthView = viewKey; const weak = snapshot.filter(item => item.healthState === 'critical').length; const warning = snapshot.filter(item => item.healthState === 'warning').length; const count = snapshot.length; healthSummary.textContent = tx(`${count} 个摄像头 · ${weak} 个不稳定 · ${warning} 个需观察`, `${count} ${count === 1 ? 'camera' : 'cameras'} · ${weak} unstable · ${warning} to watch`, `${count} ${count === 1 ? 'Kamera' : 'Kameras'} · ${weak} instabil · ${warning} beobachten`); healthList.replaceChildren(...(snapshot.length ? snapshot : [{ id: tx('尚无摄像头', 'No cameras yet', 'Noch keine Kameras'), healthScore: 0, healthState: 'paused', connection: 'idle' }]).map(item => $('div', { class: 'multicam-health-row' }, [ $('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.id), $('span', { class: `multicam-health-score ${item.healthState}` }, item.healthState === 'paused' && !snapshot.length ? '—' : `${Math.round(Number(item.healthScore) || 0)}/100`), $('span', { style: { color: 'var(--text-muted)', textAlign: 'right' } }, item.label || '—'), ]))); }; const healthCard = $('section', { class: 'multicam-center-card' }, [ $('h3', {}, tx('流健康度', 'Stream health', 'Stream-Gesundheit')), healthSummary, healthList, ]); const budgetSelect = $('select', { class: 'ctrl-input', 'data-quality-budget': '1' }, [ [0, tx('关闭预算', 'Budget off', 'Budget aus')], [2160, tx('节省 · 2160p 总量', 'Economy · 2160p total', 'Sparsam · 2160p gesamt')], [4320, tx('平衡 · 4320p 总量', 'Balanced · 4320p total', 'Ausgewogen · 4320p gesamt')], [6480, tx('高 · 6480p 总量', 'High · 6480p total', 'Hoch · 6480p gesamt')], [8640, tx('超高 · 8640p 总量', 'Ultra · 8640p total', 'Ultra · 8640p gesamt')], [12960, '12960p'], [17280, '17280p'], ].map(([value, label]) => $('option', { value: String(value) }, label))); budgetSelect.value = String(Number(store.state.settings.qualityBudgetHeight) || 0); const prioritizeFocus = $('input', { type: 'checkbox', checked: store.state.settings.qualityPrioritizeFocused !== false }); const budgetStatus = $('div', { class: 'roomgrid-modal-hint', 'data-quality-budget-status': '1' }); const renderBudget = () => { const state = service.getQualityBudgetStatus(); budgetStatus.textContent = state.enabled ? tx(`${state.active} 个流 · 分配 ${state.assigned}p / ${state.total}p`, `${state.active} streams · ${state.assigned}p / ${state.total}p assigned`, `${state.active} Streams · ${state.assigned}p / ${state.total}p zugeteilt`) : tx('质量预算已关闭;全局和每摄像头限制仍然适用。', 'Quality budget is off; global and per-camera limits still apply.', 'Qualitätsbudget ist aus; globale und individuelle Grenzen gelten weiterhin.'); }; budgetSelect.addEventListener('change', () => { store.patchSettings({ qualityBudgetHeight: Number(budgetSelect.value) || 0 }); service.refreshQuality(); renderBudget(); }); prioritizeFocus.addEventListener('change', () => { store.patchSettings({ qualityPrioritizeFocused: prioritizeFocus.checked }); service.refreshQuality(); renderBudget(); }); const qualityCard = $('section', { class: 'multicam-center-card' }, [ $('h3', {}, tx('质量预算', 'Quality budget', 'Qualitätsbudget')), $('div', { class: 'roomgrid-modal-hint' }, tx('自动在活动摄像头之间分配质量。明确选择的单摄像头质量始终优先。', 'Distributes quality across active cams. An explicit per-camera quality always wins.', 'Verteilt Qualität auf aktive Cams. Eine ausdrücklich gewählte Kameraqualität hat immer Vorrang.')), budgetSelect, $('label', { class: 'toggle' }, [prioritizeFocus, tx('主屏获得双倍权重', 'Give the focused cam double weight', 'Hauptbild doppelt gewichten')]), budgetStatus, ]); const audioMode = $('select', { class: 'ctrl-input', 'data-multicam-audio-mode': '1' }, [ $('option', { value: 'manual' }, tx('手动:每个摄像头单独', 'Manual per camera', 'Manuell pro Kamera')), $('option', { value: 'solo' }, tx('单独:只有一个摄像头', 'Solo: one camera only', 'Solo: nur eine Kamera')), $('option', { value: 'focus' }, tx('跟随主屏', 'Follow focused camera', 'Hauptbild folgen')), ]); audioMode.value = store.state.settings.audioRoutingMode || 'manual'; const audioRoom = $('select', { class: 'ctrl-input', 'data-multicam-audio-room': '1' }, [ ...allRoomsForView().map(room => $('option', { value: room.id }, `${room.id} · ${statusMeta(room.lastStatus).label}`)), ]); audioRoom.value = findRoomAny(store.state.settings.audioSoloRoomId) ? store.state.settings.audioSoloRoomId : (allRoomsForView()[0]?.id || ''); const commitAudio = () => { store.patchSettings({ audioRoutingMode: audioMode.value, audioSoloRoomId: normalizeUsername(audioRoom.value) }); applyAllAudioRouting(); }; audioMode.addEventListener('change', commitAudio); audioRoom.addEventListener('change', commitAudio); const meterVisibility = $('input', { type: 'checkbox', checked: store.state.settings.audioMeterVisible !== false, disabled: store.state.settings.multicamMeterDisplayMode !== 'individual', 'data-multicam-meter-visible': '1', 'aria-label': t('multicamMeterVisibilityGlobal'), }); meterVisibility.addEventListener('change', () => { store.patchSettings({ audioMeterVisible: meterVisibility.checked }); applyAllAudioMeterVisibility(); }); const audioCard = $('section', { class: 'multicam-center-card' }, [ $('h3', {}, tx('声音管理', 'Audio management', 'Audioverwaltung')), $('div', { class: 'roomgrid-modal-hint' }, tx('路由不会覆盖保存的静音状态或单独音量。', 'Routing never overwrites saved mute states or individual volume.', 'Die Weiterleitung überschreibt weder gespeicherte Stummschaltungen noch Einzellautstärken.')), audioMode, audioRoom, createMeterDisplayControl(), $('label', { class: 'toggle', style: { marginTop: '8px' }, title: t('multicamMeterVisibilityHint') }, [ meterVisibility, t('multicamMeterDefaultLabel'), ]), $('div', { class: 'roomgrid-modal-hint' }, t('multicamMeterModeHint')), $('div', { class: 'roomgrid-modal-hint' }, t('multicamMeterVisibilityHint')), $('div', { class: 'roomgrid-modal-actions', style: { justifyContent: 'flex-start' } }, [ $('button', { class: 'ctrl-btn', 'data-multicam-mute-all': '1', onclick: () => setTemporaryAudioMuted(true) }, tx('全部静音', 'Mute all', 'Alle stummschalten')), $('button', { class: 'ctrl-btn', 'data-multicam-restore-audio': '1', onclick: () => setTemporaryAudioMuted(false) }, tx('恢复保存的声音', 'Restore saved audio', 'Gespeicherten Ton wiederherstellen')), ]), ]); const scenesCard = $('section', { class: 'multicam-center-card' }); const sceneName = $('input', { class: 'ctrl-input', maxlength: '60', placeholder: tx('场景名称', 'Scene name', 'Szenenname'), 'data-multicam-scene-name': '1' }); const sceneList = $('div', { 'data-multicam-scene-list': '1' }); const renderScenes = () => { const workspaceId = store.state.settings.activeWorkspaceId || DEFAULT_WORKSPACE_ID; const scenes = (store.state.settings.multicamScenes || []).filter(scene => scene.workspaceId === workspaceId); const rows = scenes.length ? scenes.map(scene => $('div', { class: 'multicam-scene-row' }, [ $('span', {}, [ $('strong', { style: { display: 'block' } }, scene.name), $('small', { style: { color: 'var(--text-muted)' } }, `${scene.viewMode === 'focus' ? tx('主屏', 'Focus', 'Fokus') : tx('网格', 'Grid', 'Raster')} · ${(scene.focusRoomSlots?.length || scene.gridRoomSlots?.length || 0)} ${tx('窗口', 'cams', 'Cams')}`), ]), $('button', { class: 'ctrl-btn', 'data-apply-scene': scene.id, onclick: () => { applyMulticamScene(scene); close(); } }, tx('加载', 'Load', 'Laden')), $('button', { class: 'ctrl-btn', 'data-delete-scene': scene.id, onclick: () => { if (confirm(tx(`删除场景“${scene.name}”?`, `Delete scene “${scene.name}”?`, `Szene „${scene.name}“ löschen?`))) { removeMulticamScene(scene.id); renderScenes(); } } }, '×'), ])) : [$('div', { class: 'roomgrid-modal-hint' }, tx('此工作区尚无场景。', 'No scenes saved for this workspace yet.', 'Für diesen Arbeitsbereich sind noch keine Szenen gespeichert.'))]; sceneList.replaceChildren(...rows); }; scenesCard.append( $('h3', {}, tx('场景', 'Scenes', 'Szenen')), $('div', { class: 'roomgrid-modal-hint' }, tx('保存当前窗口分配、布局和声音路由,并可一键恢复。', 'Save the current slots, layout, and audio routing for one-click restoration.', 'Aktuelle Fensterbelegung, Layout und Audioverwaltung speichern und mit einem Klick wiederherstellen.')), $('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) auto', gap: '7px' } }, [sceneName, $('button', { class: 'ctrl-btn primary', 'data-save-scene': '1', onclick: () => { if (saveCurrentMulticamScene(sceneName.value)) { sceneName.value = ''; renderScenes(); } } }, tx('当前视图保存为场景', 'Save current view as scene', 'Aktuelle Ansicht als Szene speichern'))]), sceneList, ); const workspace = store.state.workspaces?.find(item => item.id === store.state.settings.activeWorkspaceId && !item.archived); const replacementCard = $('section', { class: 'multicam-center-card' }); if (workspace) { const enabled = $('input', { type: 'checkbox', checked: !!workspace.replacement?.enabled }); const delay = $('input', { class: 'ctrl-input', type: 'number', min: '0', max: '600', value: String(workspace.replacement?.delaySeconds ?? 20) }); const group = $('select', { class: 'ctrl-input', 'data-replacement-group': '1' }, [ $('option', { value: '' }, tx('所有保存的在线模型', 'All saved online models', 'Alle gespeicherten Online-Models')), ...store.state.groups.filter(item => !item.system).map(item => $('option', { value: item.id }, String(item.name || item.id))), ]); group.value = workspace.replacement?.groupId || ''; const preferred = [...(workspace.replacement?.fallbackRoomIds || [])]; const preferredList = $('ol', { dataset: { replacementOrder: '1' } }); const replacementLabel = tx('首选替换模型', 'Preferred replacements', 'Bevorzugte Ersatzmodels'); const search = $('input', { type: 'search', class: 'ctrl-input', ariaLabel: replacementLabel, placeholder: tx('搜索模型', 'Find a model', 'Model suchen') }); const fallback = $('select', { class: 'ctrl-input', 'data-replacement-models': '1', ariaLabel: replacementLabel }); const renderChoices = () => { const query = search.value.trim().toLowerCase(); const options = []; for (const room of store.state.rooms) { if (!room.id.includes(query) || room.archived || preferred.includes(room.id)) continue; options.push($('option', { value: room.id }, room.id)); if (options.length === 40) break; } fallback.replaceChildren(...options); }; const renderPreferred = () => { preferredList.replaceChildren(...preferred.map((id, index) => { const move = step => { const to = index + step; [preferred[index], preferred[to]] = [preferred[to], preferred[index]]; const selector = `[data-replacement-move="${step}"]`; renderPreferred(); const row = preferredList.children[to]; (row?.querySelector(`${selector}:not(:disabled)`) || row?.querySelector('button:not(:disabled)'))?.focus(); }; return $('li', {}, [id, $('button', { type: 'button', class: 'ctrl-btn', dataset: { replacementMove: '-1' }, disabled: index === 0, ariaLabel: tx('向上移', 'Move up', 'Nach oben'), onclick: () => move(-1) }, '↑'), $('button', { type: 'button', class: 'ctrl-btn', dataset: { replacementMove: '1' }, disabled: index === preferred.length - 1, ariaLabel: tx('向下移', 'Move down', 'Nach unten'), onclick: () => move(1) }, '↓'), $('button', { type: 'button', class: 'ctrl-btn', ariaLabel: tx('移除', 'Remove', 'Entfernen'), onclick: () => { preferred.splice(index, 1); renderPreferred(); renderChoices(); search.focus(); } }, '×'), ]); })); }; search.addEventListener('input', renderChoices); const replacementTools = $('details', {}, [ $('summary', {}, replacementLabel), preferredList, search, fallback, $('button', { type: 'button', class: 'ctrl-btn', dataset: { addReplacement: '1' }, onclick: () => { if (!fallback.value || preferred.includes(fallback.value) || preferred.length >= 100) return; preferred.push(fallback.value); renderPreferred(); renderChoices(); } }, tx('添加', 'Add', 'Hinzufügen')), ]); replacementTools.addEventListener('toggle', () => { if (replacementTools.open) { renderPreferred(); renderChoices(); } }); replacementCard.append( $('h3', {}, tx('替换模型', 'Replacement models', 'Ersatzmodels')), $('div', { class: 'roomgrid-modal-hint' }, tx('首选模型按列表顺序使用;之后才使用所选组中的在线模型。手动分配始终优先。', 'Preferred models are tried in list order; only then are online models from the selected group used. Manual assignments always win.', 'Bevorzugte Models werden in Listenreihenfolge verwendet; erst danach Online-Models der gewählten Gruppe. Manuelle Belegungen haben immer Vorrang.')), $('label', { class: 'toggle' }, [enabled, tx('离线窗口自动替换', 'Replace offline slots automatically', 'Offline-Fenster automatisch ersetzen')]), $('label', { style: { display: 'grid', gap: '4px', fontSize: '11px' } }, [tx('延迟(秒)', 'Delay (seconds)', 'Verzögerung (Sekunden)'), delay]), $('label', { style: { display: 'grid', gap: '4px', fontSize: '11px' } }, [tx('候选组', 'Candidate group', 'Kandidatengruppe'), group]), replacementTools, $('button', { class: 'ctrl-btn primary', 'data-save-replacements': '1', onclick: () => { store.patchWorkspace(workspace.id, { replacement: { enabled: enabled.checked, onlyOnline: true, delaySeconds: Number(delay.value) || 0, groupId: group.value, fallbackRoomIds: preferred } }); toast(tx('替换规则已保存', 'Replacement rule saved', 'Ersatzregel gespeichert')); } }, tx('保存替换规则', 'Save replacement rule', 'Ersatzregel speichern')), ); } grid.append(healthCard, qualityCard, audioCard, scenesCard, replacementCard); body.append(grid); renderHealth(); renderBudget(); renderScenes(); if (initialSection === 'scenes') requestAnimationFrame(() => sceneName.focus()); const timer = setInterval(() => { if (!body.isConnected || document.hidden) return; renderHealth(); renderBudget(); }, 1000); return () => clearInterval(timer); }); } function openPlaybackSettingsPanel() { openToolPanel(t('playbackSettingsTitle'), (body, close) => { const quality = $('select', { class: 'ctrl-input' }, [ $('option', { value: '0' }, t('maxQualityAuto')), ...[240, 360, 480, 720, 1080, 1440, 2160].map(n => $('option', { value: String(n) }, `${n}p`)), ]); quality.value = String(Number(store.state.settings.maxStreamHeight) || 0); const freeZoom = $('input', { type: 'checkbox', checked: store.state.settings.freeZoom !== false }); body.append( $('div', { style: { display: 'grid', gridTemplateColumns: '1fr', gap: '10px' } }, [ $('label', { style: { display: 'grid', gap: '5px', fontSize: '12px', color: 'var(--text-muted)' } }, [t('maxStreamHeight'), quality]), $('label', { class: 'toggle' }, [freeZoom, t('freeZoomLabel')]), ]), $('div', { class: 'roomgrid-modal-hint', style: { marginTop: '10px' } }, t('freeZoomHint')), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn primary', onclick: () => { store.patchSettings({ maxStreamHeight: Number(quality.value) || 0, freeZoom: !!freeZoom.checked }); service.refreshQuality(); close(); } }, t('saveSettings')), ]), ); }); } function openBackupPanel() { openToolPanel(t('backupPanel'), (body) => { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(CONFIG_BACKUP_PREFIX)) keys.push(key); } keys.sort((a, b) => Number(b.slice(CONFIG_BACKUP_PREFIX.length)) - Number(a.slice(CONFIG_BACKUP_PREFIX.length))); if (!keys.length) { body.appendChild($('div', { class: 'roomgrid-modal-hint' }, t('noBackups'))); return; } keys.forEach(key => { const ts = Number(key.slice(CONFIG_BACKUP_PREFIX.length)); body.appendChild($('div', { style: { display: 'grid', gridTemplateColumns: '1fr auto auto', gap: '8px', alignItems: 'center', borderBottom: '1px solid var(--border)', padding: '8px 0' } }, [ $('div', {}, Number.isFinite(ts) ? new Date(ts).toLocaleString() : key), $('button', { class: 'ctrl-btn', onclick: () => { const raw = localStorage.getItem(key); if (raw) { backupCurrentConfig(); localStorage.setItem(STORE_KEY, raw); location.reload(); } } }, t('restoreBackup')), $('button', { class: 'ctrl-btn danger', onclick: () => { localStorage.removeItem(key); openBackupPanel(); } }, t('deleteBackup')), ])); }); }); } function openStatusHistoryPanel() { openToolPanel(t('statusHistory'), (body) => { const all = loadRoomStatusHistory(); const rows = Object.entries(all).flatMap(([id, list]) => (Array.isArray(list) ? list : []).map(item => ({ id, ...item }))) .sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0)) .slice(0, 120); if (!rows.length) { body.appendChild($('div', { class: 'roomgrid-modal-hint' }, t('noStatusHistory'))); return; } rows.forEach(row => { body.appendChild($('div', { style: { display: 'grid', gridTemplateColumns: '150px 1fr auto', gap: '8px', padding: '6px 0', borderBottom: '1px solid var(--border)', fontSize: '12px' } }, [ $('span', { style: { color: 'var(--text-muted)' } }, new Date(row.ts).toLocaleString()), $('span', {}, row.id), $('span', { style: { fontWeight: '750' } }, row.privateLabel || row.status), ])); }); }); } function openGroupRulesPanel() { openToolPanel(t('groupRules'), (body, close) => { const favorite = $('input', { type: 'checkbox', checked: store.state.settings.favoriteFirst !== false }); body.append( $('label', { class: 'toggle' }, [favorite, t('favoriteFirst')]), $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn primary', onclick: () => { store.patchSettings({ favoriteFirst: !!favorite.checked }); close(); } }, t('saveSettings')), ]), ); }); } function openTemporaryUrlManager() { openToolPanel(t('tempUrlManager'), (body) => { const saved = readJsonStorage(SAVED_TEMP_URLS_KEY, []); const current = tempRooms.filter(r => r.sourceUrl); body.appendChild($('button', { class: 'ctrl-btn primary', onclick: () => { const next = [...new Map([...saved, ...current.map(r => ({ url: r.sourceUrl, name: r.displayName || r.id }))].filter(x => x?.url).map(x => [x.url, x])).values()]; writeJsonStorage(SAVED_TEMP_URLS_KEY, next.slice(0, 80)); toast(t('copied')); } }, t('saveTempUrls'))); const rows = [...saved, ...current.map(r => ({ url: r.sourceUrl, name: r.displayName || r.id }))]; if (!rows.length) body.appendChild($('div', { class: 'roomgrid-modal-hint', style: { marginTop: '10px' } }, t('noTempUrls'))); rows.forEach(row => body.appendChild($('div', { style: { padding: '6px 0', borderBottom: '1px solid var(--border)', fontSize: '12px', wordBreak: 'break-all' } }, `${row.name || ''} ${row.url || ''}`))); }); } function openShortcutPanel() { openToolPanel(t('shortcutPanel'), (body, close) => { const actions = [ ['commandPalette', t('shortcutCommandPalette')], ['globalSearch', t('shortcutGlobalSearch')], ['focusAdd', t('shortcutFocusAdd')], ['refreshAll', t('shortcutRefreshAll')], ['gridView', t('shortcutGridView')], ['focusView', t('shortcutFocusView')], ['pureMode', t('shortcutPureMode')], ['viewerMode', t('shortcutViewerMode')], ['focusThumbs', t('shortcutFocusThumbs')], ...(ROOMGRID_CAPTURE_TOOLS_ENABLED ? [['recordingCenter', t('shortcutRecordingCenter')], ['recordPage', t('shortcutRecordPage')]] : []), ]; const current = sanitizeShortcuts(store.state.settings.shortcuts, defaultShortcuts()); const inputs = {}; const gridBox = $('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(160px,1fr) minmax(140px,220px)', gap: '8px 10px', alignItems: 'center' } }); actions.forEach(([action, label]) => { const input = $('input', { class: 'ctrl-input', value: shortcutLabel(current[action]), readonly: true, onkeydown: (e) => { e.preventDefault(); e.stopPropagation(); if (e.key === 'Backspace' || e.key === 'Delete') { input.dataset.spec = ''; input.value = ''; return; } const spec = shortcutFromEvent(e); if (!spec) return; input.dataset.spec = spec; input.value = shortcutLabel(spec); }, }); input.dataset.spec = current[action]; inputs[action] = input; gridBox.append($('div', { style: { fontSize: '12px', color: 'var(--text-muted)' } }, label), input); }); body.append( $('div', { class: 'roomgrid-modal-hint' }, t('shortcutCaptureHint')), gridBox, $('div', { class: 'roomgrid-modal-actions' }, [ $('button', { class: 'ctrl-btn', onclick: () => { const defaults = defaultShortcuts(); for (const [action, input] of Object.entries(inputs)) { input.dataset.spec = defaults[action] || ''; input.value = shortcutLabel(defaults[action] || ''); } } }, t('resetShortcuts')), $('button', { class: 'ctrl-btn', onclick: close }, t('importReviewCancel')), $('button', { class: 'ctrl-btn primary', onclick: () => { const next = {}; for (const [action, input] of Object.entries(inputs)) next[action] = normalizeShortcutSpec(input.dataset.spec || ''); const used = new Map(); const conflicts = []; for (const [action, spec] of Object.entries(next)) { if (!spec) continue; if (used.has(spec)) conflicts.push(`${actions.find(item => item[0] === used.get(spec))?.[1] || used.get(spec)} ↔ ${actions.find(item => item[0] === action)?.[1] || action} (${shortcutLabel(spec)})`); else used.set(spec, action); } if (conflicts.length) { alert(`${tx('请先解决重复的快捷键:', 'Resolve duplicate shortcuts first:', 'Bitte zuerst doppelte Tastenkürzel auflösen:')}\n\n${conflicts.join('\n')}`); return; } store.patchSettings({ shortcuts: sanitizeShortcuts(next, defaultShortcuts()) }); close(); } }, t('saveSettings')), ]), ); }); } function loadSavedTemporarySources() { const saved = readJsonStorage(SAVED_TEMP_URLS_KEY, []); if (!Array.isArray(saved)) return; saved.slice(0, 80).forEach(item => { if (!isSafeHttpUrl(item?.url) || !isDirectMediaUrl(item.url)) return; const id = tempIdFromUrl(item.url); if (findRoomAny(id)) return; const activeGroup = store.state.settings.activeGroup; const groupId = (!activeGroup || activeGroup === LIBRARY_GROUP_ID || activeGroup === ONLINE_GROUP_ID) ? DEFAULT_GROUP_ID : activeGroup; tempRooms.push({ id, displayName: item.name || new URL(item.url).hostname, sourceUrl: item.url, group: groupId, groups: [groupId], addedAt: Date.now(), order: 100000 + tempRooms.length, lastStatus: 'online', lastSeenOnline: Date.now(), muted: false, temporary: true }); }); } function buildFocusModelPicker(menu, roomId, slotIndex, slotLabelOverride = '', pickerOptions = {}) { const picker = $('div', { class: 'card-focus-picker' }); const targetLabel = slotLabelOverride || focusSlotLabel(slotIndex, layoutSize()); const pickerTitle = pickerOptions.title || t('focusPickerTitle'); const pickerHint = pickerOptions.hint || t('focusPickerHint'); const onSelect = typeof pickerOptions.onSelect === 'function' ? pickerOptions.onSelect : (id) => assignFocusRoomToSlot(id, slotIndex); // Read actual rendered windows once, not the whole room catalogue per row. // Off-page assignments are not cameras visible in this MultiCam view. const displayed = new Map(); grid.querySelectorAll('.cam-card[data-room-id]').forEach(card => { const label = card.dataset.focusSlotLabel || t('gridWindowLabel', Number(card.dataset.gridSlot) + 1); const labels = displayed.get(card.dataset.roomId) || []; labels.push(label); displayed.set(card.dataset.roomId, labels); }); picker.appendChild($('div', { class: 'card-focus-picker-title' }, pickerTitle)); picker.appendChild($('div', { class: 'card-focus-picker-hint' }, `${targetLabel} · ${pickerHint}`)); picker.appendChild($('div', { class: 'card-focus-picker-hint' }, t('modelPickerMoveHint'))); const pickerSearch = $('input', { class: 'ctrl-input', type: 'search', placeholder: tx('名称、备注或自己的标签…', 'Name, note or own tag…', 'Name, Notiz oder eigener Tag…'), style: { width: '100%', marginBottom: '7px' } }); pickerSearch.addEventListener('input', () => { const query = String(pickerSearch.value || '').trim().toLocaleLowerCase(); picker.querySelectorAll('.card-focus-picker-item').forEach(node => { node.hidden = !!query && !String(node.dataset.search || '').includes(query); }); picker.querySelectorAll('.card-focus-picker-group').forEach(group => { const visible = [...group.querySelectorAll('.card-focus-picker-item')].some(node => !node.hidden); group.hidden = !visible; if (query && visible) group.open = true; }); }); picker.appendChild(pickerSearch); const groupDisplayName = (g) => { if (g.name === '__library__') return t('groupLibrary'); if (g.name === '__all__') return t('groupAll'); if (g.name === '__online__') return t('groupOnline'); if (g.name === '__fav__') return t('groupFav'); return g.name; }; const orderedGroups = [...store.state.groups].sort((a, b) => a.order - b.order); let renderedGroups = 0; for (const group of orderedGroups) { const members = (group.id === LIBRARY_GROUP_ID ? allRoomsForView() : allRoomsForView().filter(room => roomInGroup(room, group.id))) .sort((a, b) => roomOrderInGroup(a, group.id) - roomOrderInGroup(b, group.id)); if (!members.length) continue; const list = $('div', { class: 'card-focus-picker-list' }); members.forEach(room => { const isCurrent = room.id === roomId; const occupiedLabels = displayed.get(room.id) || []; const assignment = isCurrent ? `${t('focusPickerCurrent')} · ${targetLabel}` : occupiedLabels.length ? `${t('modelPickerDisplayed')} · ${occupiedLabels.join(', ')}` : ''; const meta = statusMeta(room.lastStatus); const item = $('button', { class: 'card-focus-picker-item' + (assignment ? ' is-in-view' : '') + (isCurrent ? ' is-current' : ''), type: 'button', dataset: { pickerRoomId: room.id, search: `${room.id} ${room.displayName || ''} ${room.notes || ''} ${(room.localTags || []).join(' ')}`.toLocaleLowerCase() }, title: `${room.displayName || room.id} · ${assignment || targetLabel}`, onpointerenter: (ev) => scheduleSidebarPreview(room, ev.currentTarget), onpointerleave: (ev) => cancelSidebarPreview(ev.currentTarget), onfocus: (ev) => scheduleSidebarPreview(room, ev.currentTarget), onblur: (ev) => cancelSidebarPreview(ev.currentTarget), onclick: () => { closeSidebarPreview(); if (!isCurrent) onSelect(room.id); menu.remove(); }, }, [ $('span', { class: 'card-focus-picker-name' }, `${room.temporary ? '临时 · ' : ''}${room.displayName || room.id}`), $('span', { class: 'card-focus-picker-meta' }, [ ...(assignment ? [$('span', { class: 'card-focus-picker-assignment' }, assignment)] : []), $('span', { style: { color: meta.color } }, meta.label), ]), ]); list.appendChild(item); }); const details = $('details', { class: 'card-focus-picker-group', open: renderedGroups === 0 }, [ $('summary', { class: 'card-focus-picker-group-summary' }, `${groupDisplayName(group)} · ${members.length}`), list, ]); picker.appendChild(details); renderedGroups++; } if (!renderedGroups) picker.appendChild($('div', { class: 'card-focus-picker-empty' }, t('focusPickerEmpty'))); return picker; } function buildGridModelPicker(menu, roomId, slotIndex) { return buildFocusModelPicker(menu, roomId, slotIndex, t('gridWindowLabel', Number(slotIndex) + 1), { title: t('gridPickerTitle'), hint: t('gridPickerHint'), onSelect: (id) => assignGridRoomToSlot(id, slotIndex), }); } /* ---- 卡片"更多"操作菜单(PiP / 全屏 / 移动到分组 / 主屏聚焦)---- */ function openCardOpsMenu(e, roomId, card) { // 关闭已有的卡片菜单 const existing = document.querySelector('.card-ops-menu-pop'); if (existing) { closeSidebarPreview(); existing.remove(); return; } const rect = e.currentTarget?.getBoundingClientRect?.() || card?.getBoundingClientRect?.() || { left: e.clientX || 0, right: e.clientX || 0, top: e.clientY || 0, bottom: e.clientY || 0 }; const usePointer = Number.isFinite(e.clientX) && Number.isFinite(e.clientY) && !e.currentTarget?.classList?.contains('icon-btn'); const menuStyle = { position: 'fixed', left: '8px', top: '8px', width: 'min(380px, calc(100vw - 16px))', minWidth: '0', maxWidth: 'calc(100vw - 16px)', maxHeight: 'calc(100dvh - 16px)', overflow: 'auto', }; const menu = $('div', { class: 'menu-pop card-ops-menu-pop', style: menuStyle, tabIndex: '-1', role: 'dialog', 'aria-label': tx('摄像头菜单', 'Camera menu', 'Kamera-Menü'), }); menu.appendChild($('div', { class: 'card-ops-menu-header', 'data-card-menu-header': '1' }, [ $('div', {}, [ $('div', { class: 'card-ops-menu-title' }, roomId), $('div', { class: 'card-ops-menu-subtitle' }, tx('摄像头菜单 · 画质和声音', 'Camera menu · quality and sound', 'Kamera-Menü · Qualität und Ton')), ]), $('button', { class: 'card-ops-menu-close', type: 'button', title: tx('关闭菜单', 'Close menu', 'Menü schließen'), 'aria-label': tx('关闭菜单', 'Close menu', 'Menü schließen'), onclick: event => { event.stopPropagation(); closeSidebarPreview(); menu.remove(); }, }, '×'), ])); const menuCategory = (label, open = false) => { const body = $('div', { class: 'card-menu-category-body' }); const details = $('details', { class: 'card-menu-category', open }, [ $('summary', {}, label), body, ]); menu.appendChild(details); return body; }; const viewSection = menuCategory(tx('视图和窗口', 'View and window', 'Ansicht und Fenster'), true); const playbackSection = menuCategory(tx('播放', 'Playback', 'Wiedergabe'), true); const audioSection = menuCategory(tx('声音', 'Audio', 'Audio')); const qualitySection = menuCategory(tx('画质和图像', 'Quality and image', 'Qualität und Bild')); const organizeSection = menuCategory(tx('整理', 'Organize', 'Organisieren')); const actionsSection = menuCategory(tx('更多操作', 'More actions', 'Weitere Aktionen')); const item = (icon, label, onclick) => setElementHint($('button', { title: label, onclick: () => { closeSidebarPreview(); menu.remove(); onclick(); }, }, label), label); // 主屏切换(focus 模式专属) if (store.state.settings.viewMode === 'focus') { const isFocused = store.state.settings.focusedRoomId === roomId; viewSection.appendChild(item(isFocused ? '' : '', isFocused ? tx('已是主屏', 'Currently main', 'Bereits Hauptbildschirm') : tx('设为主屏', 'Set as main', 'Als Hauptbildschirm festlegen'), () => { if (!isFocused) store.patchSettings({ focusedRoomId: roomId }); })); const focusSlot = Number(card?.dataset?.focusSlot); if (Number.isInteger(focusSlot) && focusSlot >= 0) { viewSection.appendChild(buildFocusModelPicker(menu, roomId, focusSlot, card?.dataset?.focusSlotLabel || '')); } } if (store.state.settings.viewMode === 'grid') { const gridSlot = Number(card?.dataset?.gridSlot); if (Number.isInteger(gridSlot) && gridSlot >= 0) { viewSection.appendChild(buildGridModelPicker(menu, roomId, gridSlot)); viewSection.appendChild(item('', t('gridRemoveFromView'), () => removeGridRoomFromSlot(roomId, gridSlot))); } } viewSection.appendChild(item('', tx('固定实时预览', 'Pin live preview', 'Live-Vorschau anheften'), () => RoomGridUltimate.openPreview(roomId))); viewSection.appendChild(item('', tx('打开模型详情', 'Open model details', 'Modeldetails öffnen'), () => RoomGridUltimate.openInspector(roomId))); playbackSection.appendChild(item('', service.isPaused(roomId) ? t('opResume') : t('opPause'), () => service.togglePause(roomId))); playbackSection.appendChild(item('', tx('重新连接此摄像头', 'Reconnect this camera', 'Diese Kamera neu verbinden'), () => service.refresh(roomId))); const currentRoom = store.state.rooms.find(x => x.id === roomId); if (currentRoom) { const meterVisibility = $('select', { class: 'ctrl-input', 'data-camera-meter-visibility': roomId, 'aria-label': t('multicamMeterVisibilityCamera'), title: t('multicamMeterVisibilityHint'), }, [ $('option', { value: 'global' }, t('multicamMeterVisibilityGlobalOption')), $('option', { value: 'show' }, t('multicamMeterVisibilityShowOption')), $('option', { value: 'hide' }, t('multicamMeterVisibilityHideOption')), ]); meterVisibility.value = typeof currentRoom.audioMeterVisible === 'boolean' ? (currentRoom.audioMeterVisible ? 'show' : 'hide') : 'global'; meterVisibility.addEventListener('change', () => { const value = meterVisibility.value; store.patchRoom(roomId, { audioMeterVisible: value === 'global' ? null : value === 'show' }); applyAudioMeterVisibility(roomId); }); audioSection.appendChild($('label', { style: { display: 'grid', gap: '6px', padding: '8px 10px', color: 'var(--text)', fontSize: '11px' }, }, [ $('strong', {}, t('multicamMeterVisibilityCamera')), meterVisibility, $('span', { class: 'roomgrid-modal-hint' }, t('multicamMeterModeHint')), ])); const individualVolume = currentRoom.volume == null ? 1 : Math.max(0, Math.min(1, Number(currentRoom.volume) || 0)); const volumeValue = $('span', { style: { minWidth: '38px', textAlign: 'right', fontVariantNumeric: 'tabular-nums' } }, `${Math.round(individualVolume * 100)}%`); const volumeSlider = $('input', { type: 'range', min: '0', max: '100', value: String(Math.round(individualVolume * 100)), title: tx('此摄像头的音量系数', 'Volume multiplier for this cam', 'Lautstärkefaktor für diese Cam'), style: { width: '105px', accentColor: 'var(--accent)' }, oninput: (event) => { event.stopPropagation(); const volume = Math.max(0, Math.min(1, Number(event.currentTarget.value) / 100)); volumeValue.textContent = `${Math.round(volume * 100)}%`; store.patchRoom(roomId, { volume }); requestAnimationFrame(() => applyMute(roomId)); }, }); const resetVolume = $('button', { type: 'button', title: tx('个人音量重置为 100%', 'Reset individual volume to 100%', 'Individuelle Lautstärke auf 100 % zurücksetzen'), style: { width: 'auto', padding: '4px 7px', fontSize: '10px' }, onclick: (event) => { event.stopPropagation(); volumeSlider.value = '100'; volumeValue.textContent = '100%'; store.patchRoom(roomId, { volume: null }); requestAnimationFrame(() => applyMute(roomId)); }, }, '100%'); audioSection.appendChild($('div', { style: { display: 'grid', gridTemplateColumns: '1fr auto', gap: '5px 8px', alignItems: 'center', padding: '8px 10px', color: 'var(--text)', fontSize: '11px' }, }, [ $('strong', {}, tx('单独音量', 'Individual volume', 'Individuelle Lautstärke')), volumeValue, volumeSlider, resetVolume, ])); audioSection.appendChild(item('', currentRoom.muted ? tx('取消静音', 'Unmute', 'Ton einschalten') : tx('静音', 'Mute', 'Stummschalten'), () => { store.patchRoom(roomId, { muted: !currentRoom.muted }); requestAnimationFrame(() => applyMute(roomId)); })); audioSection.appendChild(item('', tx('只听这个摄像头', 'Listen to this cam only', 'Nur diese Cam hören'), () => routeAudioToRoom(roomId))); const qualityState = service.getQualityStatus(roomId); const qualityStatus = $('div', { class: 'card-quality-status', 'data-card-quality-status': roomId }); const renderQualityStatus = (selected = Number(currentRoom.qualityHeight) || 0) => { const latest = service.getQualityStatus(roomId); const current = Number(latest.current) || 0; qualityStatus.textContent = selected ? tx( `仅为 ${roomId} 保存:最高 ${selected}p${current ? ` · 当前 ${current}p` : ''}`, `Saved only for ${roomId}: up to ${selected}p${current ? ` · currently ${current}p` : ''}`, `Nur für ${roomId} gespeichert: bis ${selected}p${current ? ` · aktuell ${current}p` : ''}` ) : tx( `此摄像头使用全局/自适应质量${current ? ` · 当前 ${current}p` : ''}`, `This cam uses global/adaptive quality${current ? ` · currently ${current}p` : ''}`, `Diese Kamera nutzt die globale/adaptive Qualität${current ? ` · aktuell ${current}p` : ''}` ); }; const qualitySelect = $('select', { class: 'ctrl-input', title: tx('仅设置此摄像头的画质', 'Set quality for this camera only', 'Qualität nur für diese Kamera einstellen'), 'data-card-quality-select': roomId, style: { width: '100%', minHeight: '31px', padding: '4px 7px' }, onchange: event => { event.stopPropagation(); const qualityHeight = Number(event.currentTarget.value) || 0; store.patchRoom(roomId, { qualityHeight }); service.refreshQuality(roomId); renderQualityStatus(qualityHeight); }, }, [[0, tx('自动(全局/自适应)', 'Automatic (global/adaptive)', 'Automatisch (global/adaptiv)')], [240,'240p'], [360,'360p'], [480,'480p'], [720,'720p'], [1080,'1080p'], [1440,'1440p'], [2160,'2160p']].map(([value, label]) => { const available = qualityState.available.includes(value); const availability = available ? tx(' · 可用', ' · available', ' · verfügbar') : ''; return $('option', { value: String(value), selected: Number(currentRoom.qualityHeight || 0) === value }, `${label}${availability}`); })); renderQualityStatus(); qualitySection.appendChild($('div', { class: 'card-quality-panel' }, [ $('strong', {}, tx('此摄像头的画质', 'Quality for this camera', 'Qualität dieser Kamera')), qualitySelect, qualityStatus, ])); const transform = getVideoTransform(roomId); const imageControl = (label, key, value, min, max, step, format = number => `${Math.round(number * 100)}%`) => { const output = $('span', { style: { minWidth: '42px', textAlign: 'right' } }, format(value)); const slider = $('input', { type: 'range', min, max, step, value, style: { width: '112px', accentColor: 'var(--accent)' }, oninput: event => { event.stopPropagation(); const next = Number(event.currentTarget.value); output.textContent = format(next); patchVideoTransform(roomId, { [key]: next }); } }); return [$('span', {}, label), $('span', { style: { display: 'flex', alignItems: 'center', gap: '5px' } }, [slider, output])]; }; const beforeAfter = $('button', { class: 'ctrl-btn', type: 'button' }, tx('按住看原图', 'Hold for original', 'Für Original gedrückt halten')); const showOriginal = event => { event.stopPropagation(); const video = cardMap.get(roomId)?.video; if (video) video.style.filter = 'none'; }; const showEdited = event => { event.stopPropagation(); applyVideoTransform(roomId); }; beforeAfter.addEventListener('pointerdown', showOriginal); beforeAfter.addEventListener('pointerup', showEdited); beforeAfter.addEventListener('pointercancel', showEdited); beforeAfter.addEventListener('pointerleave', showEdited); const presets = $('span', { style: { gridColumn: '1/-1', display: 'flex', flexWrap: 'wrap', gap: '5px' } }, [ $('button', { class: 'ctrl-btn', type: 'button', onclick: event => { event.stopPropagation(); patchVideoTransform(roomId, { brightness: 1.04, contrast: 1.12, saturation: 1.16, hue: 0, sepia: .03, grayscale: 0, invert: 0, blur: 0 }); closeTransientUi(); } }, tx('电影', 'Cinema', 'Kino')), $('button', { class: 'ctrl-btn', type: 'button', onclick: event => { event.stopPropagation(); patchVideoTransform(roomId, { brightness: 1.08, contrast: 1.08, saturation: 1.38, hue: 0, sepia: 0, grayscale: 0, invert: 0, blur: 0 }); closeTransientUi(); } }, tx('鲜艳', 'Vivid', 'Lebendig')), $('button', { class: 'ctrl-btn', type: 'button', onclick: event => { event.stopPropagation(); patchVideoTransform(roomId, { brightness: 1, contrast: 1.18, saturation: 0, hue: 0, sepia: 0, grayscale: 1, invert: 0, blur: 0 }); closeTransientUi(); } }, tx('黑白', 'Mono', 'Mono')), ]); qualitySection.appendChild($('div', { style: { display: 'grid', gap: '6px', padding: '8px 10px', borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)', color: 'var(--text)', fontSize: '11px' } }, [ $('strong', {}, tx('图像调整', 'Image adjustments', 'Bildanpassung')), ...imageControl(tx('亮度', 'Brightness', 'Helligkeit'), 'brightness', transform.brightness, .25, 2.5, .05), ...imageControl(tx('对比度', 'Contrast', 'Kontrast'), 'contrast', transform.contrast, .25, 2.5, .05), ...imageControl(tx('饱和度', 'Saturation', 'Sättigung'), 'saturation', transform.saturation, 0, 3, .05), ...imageControl(tx('色相', 'Hue', 'Farbton'), 'hue', transform.hue, -180, 180, 5, number => `${Math.round(number)}°`), ...imageControl(tx('棕褐色', 'Sepia', 'Sepia'), 'sepia', transform.sepia, 0, 1, .05), ...imageControl(tx('灰度', 'Grayscale', 'Graustufen'), 'grayscale', transform.grayscale, 0, 1, .05), ...imageControl(tx('反色', 'Invert', 'Invertieren'), 'invert', transform.invert, 0, 1, .05), ...imageControl(tx('模糊', 'Blur', 'Weichzeichnen'), 'blur', transform.blur, 0, 12, .25, number => `${Number(number).toFixed(1)} px`), presets, beforeAfter, $('button', { class: 'ctrl-btn', type: 'button', onclick: event => { event.stopPropagation(); resetVideoTransform(roomId); closeTransientUi(); } }, tx('重置图像', 'Reset image', 'Bild zurücksetzen')), ])); } const inFav = currentRoom ? roomInGroup(currentRoom, FAVORITE_GROUP_ID) : false; organizeSection.appendChild(item(inFav ? '' : '', inFav ? t('opFavoriteRemove') : t('opFavoriteAdd'), () => store.toggleRoomInGroup(roomId, FAVORITE_GROUP_ID))); if (!inFav) organizeSection.appendChild(item('', t('opMoveToFavorites'), () => store.moveOnlyToGroup(roomId, FAVORITE_GROUP_ID))); if (ROOMGRID_CAPTURE_TOOLS_ENABLED) { actionsSection.appendChild(item('', t('opScreenshot'), () => captureCardScreenshot(roomId))); actionsSection.appendChild(item('', recordings.has(roomId) ? t('opRecordStop') : t('opRecordStart'), () => toggleCardRecording(roomId))); } qualitySection.appendChild(item('', t('opMirror'), () => { const cur = getVideoTransform(roomId); patchVideoTransform(roomId, { mirror: !cur.mirror }); })); qualitySection.appendChild(item('', t('opFlip'), () => { const cur = getVideoTransform(roomId); patchVideoTransform(roomId, { flip: !cur.flip }); })); qualitySection.appendChild(item('', t('opRotateLeft'), () => { const cur = getVideoTransform(roomId); patchVideoTransform(roomId, { rotation: (cur.rotation + 270) % 360 }); })); qualitySection.appendChild(item('', t('opRotateRight'), () => { const cur = getVideoTransform(roomId); patchVideoTransform(roomId, { rotation: (cur.rotation + 90) % 360 }); })); qualitySection.appendChild(item('', t('opResetView'), () => resetVideoTransform(roomId))); actionsSection.appendChild(item('', t('opOpenRoom'), () => openNoopener(location.origin + '/' + roomId + '/'))); actionsSection.appendChild(item('', t('opCopyUsername'), async () => { await copyText(roomId); toast(t('copied')); })); viewSection.appendChild(item('', t('opPiP'), async () => { const v = cardMap.get(roomId)?.video; if (!v) return; try { if (document.pictureInPictureElement) await document.exitPictureInPicture(); else await v.requestPictureInPicture(); } catch (_) {} })); viewSection.appendChild(item('', t('opFullscreen'), () => { document.fullscreenElement ? document.exitFullscreen() : card.requestFullscreen().catch(() => {}); })); organizeSection.appendChild(item('', t('opMoveGroup'), () => openMoveMenu(e, roomId))); organizeSection.appendChild(item('', t('opDeleteRoom'), () => { stopCardRecording(roomId, true); service.stop(roomId); store.removeRoom(roomId); })); document.body.appendChild(menu); const placeMenu = () => { if (!menu.isConnected) return; const pad = 8; menu.scrollTop = 0; const menuRect = menu.getBoundingClientRect(); const visualViewport = window.visualViewport; const viewportLeft = Math.max(0, Number(visualViewport?.offsetLeft) || 0); const viewportTop = Math.max(0, Number(visualViewport?.offsetTop) || 0); const viewportWidth = Math.max(320, Number(visualViewport?.width) || window.innerWidth || document.documentElement.clientWidth || 320); const viewportHeight = Math.max(240, Number(visualViewport?.height) || window.innerHeight || document.documentElement.clientHeight || 240); const anchorTop = Number.isFinite(Number(rect.top)) ? Number(rect.top) : (Number(e.clientY) || pad); const anchorBottom = Number.isFinite(Number(rect.bottom)) ? Number(rect.bottom) : (Number(e.clientY) || pad); const spaceAbove = Math.max(0, anchorTop - viewportTop - pad); const spaceBelow = Math.max(0, viewportTop + viewportHeight - anchorBottom - pad); const openAbove = spaceAbove > spaceBelow || (anchorBottom > viewportHeight * 0.58 && spaceAbove >= 140); const desiredTop = openAbove ? anchorTop - menuRect.height - 4 : anchorBottom + 4; const top = Math.max(viewportTop + pad, Math.min(viewportTop + viewportHeight - menuRect.height - pad, desiredTop)); const desiredLeft = usePointer ? Number(e.clientX) : Number(rect.right) - menuRect.width; const left = Math.max(viewportLeft + pad, Math.min(viewportLeft + viewportWidth - menuRect.width - pad, Number.isFinite(desiredLeft) ? desiredLeft : viewportLeft + pad)); Object.assign(menu.style, { left: `${left}px`, top: `${top}px`, right: 'auto', bottom: 'auto' }); menu.scrollTop = 0; try { menu.focus({ preventScroll: true }); } catch (_) {} }; requestAnimationFrame(placeMenu); const close = (ev) => { if (!menu.contains(ev.target)) { closeSidebarPreview(); menu.remove(); document.removeEventListener('click', close); } }; setTimeout(() => document.addEventListener('click', close), 0); } /* ---- 拖拽落点写回 store ---- */ function reorderByDrop(fromId, toId, position /* 'before' | 'after' */) { fromId = normalizeUsername(fromId); toId = normalizeUsername(toId); if (!fromId || !toId || fromId === toId) return; const ag = store.state.settings.activeGroup || DEFAULT_GROUP_ID; const targetGroup = (ag === LIBRARY_GROUP_ID || ag === ONLINE_GROUP_ID) ? undefined : ag; const manualRooms = (ag === LIBRARY_GROUP_ID ? [...store.state.rooms] : store.state.rooms.filter(r => roomInGroup(r, ag))) .sort((a, b) => roomOrderInGroup(a, ag) - roomOrderInGroup(b, ag) || a.id.localeCompare(b.id)); const manualIds = manualRooms.map(r => r.id); // 以当前屏幕实际可见顺序为准进行插入;再把结果合并回完整手动顺序。 // 这样在分页、搜索、隐藏离线/私密时,隐藏房间不会被错误重排或产生重复 order。 const visibleIds = visibleRooms().map(r => r.id).filter(id => manualIds.includes(id)); const fi = visibleIds.indexOf(fromId); let ti = visibleIds.indexOf(toId); if (fi < 0 || ti < 0) return; visibleIds.splice(fi, 1); if (fi < ti) ti--; if (position === 'after') ti++; visibleIds.splice(Math.max(0, Math.min(visibleIds.length, ti)), 0, fromId); const visibleSet = new Set(visibleIds); let cursor = 0; const mergedIds = manualIds.map(id => visibleSet.has(id) ? visibleIds[cursor++] : id); while (cursor < visibleIds.length) mergedIds.push(visibleIds[cursor++]); const patch = {}; if (store.state.settings.sortBy !== 'manual') patch.sortBy = 'manual'; if (store.state.settings.viewMode === 'focus') { const focusedId = store.state.settings.focusedRoomId; // 主屏与副屏互拖时按“交换屏幕位置”处理:拖副屏到主屏,副屏变主屏;拖主屏到副屏,目标变主屏。 if (focusedId === toId && fromId !== focusedId) patch.focusedRoomId = fromId; else if (focusedId === fromId && toId !== focusedId) patch.focusedRoomId = toId; } store.reorderRooms(mergedIds, targetGroup); if (Object.keys(patch).length) store.patchSettings(patch); } function openGridAssignmentMenu(e, roomId, anchor) { if (store.state.settings.viewMode !== 'grid') return; roomId = normalizeUsername(roomId); if (!isLikelyUsername(roomId) || !findRoomAny(roomId)) return; const existing = document.querySelector('.grid-sidebar-assign-menu'); if (existing) { closeSidebarPreview(); existing.remove(); return; } document.querySelectorAll('.card-ops-menu-pop, .more-menu-pop').forEach(el => { try { el.remove(); } catch (_) {} }); closeSidebarPreview(); const rect = anchor?.getBoundingClientRect?.() || { left: e.clientX || 8, bottom: e.clientY || 8 }; const width = 250; const rawX = Number.isFinite(e.clientX) ? e.clientX : rect.left; const rawY = Number.isFinite(e.clientY) ? e.clientY : rect.bottom; const left = Math.max(8, Math.min(rawX, window.innerWidth - width - 8)); const top = Math.max(8, Math.min(rawY, window.innerHeight - 260)); const menu = $('div', { class: 'menu-pop grid-sidebar-assign-menu', style: { left: left + 'px', top: top + 'px', minWidth: width + 'px', maxHeight: 'calc(100vh - 16px)', overflow: 'auto', zIndex: '10020' }, }); const current = gridRoomsForView(pagedRooms(fullVisibleRooms())); menu.append( $('div', { style: { padding: '8px 10px 3px', color: 'var(--text)', fontWeight: '800', fontSize: '12px' } }, t('sidebarAssignMenu')), $('div', { style: { padding: '0 10px 7px', color: 'var(--text-muted)', fontSize: '10px', lineHeight: '1.35' } }, t('sidebarAssignMenuHint')), ); for (let idx = 0; idx < layoutSize(); idx++) { const targetRoom = current[idx]; const isCurrent = targetRoom?.id === roomId; const targetName = targetRoom ? (targetRoom.displayName || targetRoom.id) : t('gridEmptySlot', idx + 1); const label = `${t('gridAssignWindow', idx + 1)} · ${isCurrent ? t('gridAssignCurrent') : targetName}`; menu.appendChild($('button', { type: 'button', title: label, style: isCurrent ? { color: 'var(--accent)', fontWeight: '800' } : {}, onclick: () => { closeSidebarPreview(); menu.remove(); assignGridRoomToSlot(roomId, idx); }, }, label)); } document.body.appendChild(menu); const close = (ev) => { if (!menu.contains(ev.target)) { closeSidebarPreview(); menu.remove(); document.removeEventListener('click', close); } }; setTimeout(() => document.addEventListener('click', close), 0); } function openMoveMenu(e, roomId) { const groups = [...store.state.groups].sort((a, b) => a.order - b.order); const r = store.state.rooms.find(x => x.id === roomId); const groupDisplayName = (g) => { if (g.name === '__library__') return t('groupLibrary'); if (g.name === '__all__') return t('groupAll'); if (g.name === '__online__') return t('groupOnline'); if (g.name === '__fav__') return t('groupFav'); return g.name; }; const menu = $('div', { class: 'menu-pop', style: { left: e.clientX + 'px', top: e.clientY + 'px' } }); groups.filter(g => g.id !== LIBRARY_GROUP_ID && g.id !== ONLINE_GROUP_ID).forEach(g => { const inGroup = roomInGroup(r, g.id); const label = (inGroup ? ' ' : ' ') + groupDisplayName(g); menu.appendChild($('button', { title: groupDisplayName(g), onclick: () => { store.toggleRoomInGroup(roomId, g.id); menu.remove(); }, style: inGroup ? { color: 'var(--accent)' } : {}, }, label)); }); document.body.appendChild(menu); const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close); } }; setTimeout(() => document.addEventListener('click', close), 0); } /* ---- 更多菜单按钮触发 ---- */ function openMoreMenu(anchor) { const existing = document.querySelector('.more-menu-pop'); if (existing) { existing.remove(); clearMoreMenuListeners(anchor); return; } const menu = $('div', { class: 'menu-pop more-menu-pop', role: 'dialog', ariaLabel: tx('更多工具', 'More tools', 'Weitere Werkzeuge'), lang: LANG, tabIndex: '-1', style: { position: 'fixed', left: '8px', right: 'auto', top: '8px', bottom: 'auto', width: 'min(300px, calc(100vw - 16px))', minWidth: '0', maxWidth: 'calc(100vw - 16px)', maxHeight: 'min(620px, calc(100dvh - 16px))', boxSizing: 'border-box', overflowY: 'auto', overscrollBehavior: 'contain', scrollbarGutter: 'stable', }, }); const dismissMenu = (restoreFocus = false) => { menu.remove(); clearMoreMenuListeners(anchor); if (restoreFocus && anchor?.isConnected) requestAnimationFrame(() => anchor.focus?.()); }; const sectionLabel = (zh, en, de = en) => tx(zh, en, de); const divider = () => $('div', { style: { height: '1px', background: 'var(--border)', margin: '6px 0' } }); const sectionTitle = (label) => $('div', { style: { padding: '7px 10px 4px', fontSize: '11px', lineHeight: '1', color: 'var(--text-muted)', fontWeight: '750', letterSpacing: '.02em', }, }, label); const item = (label, onClick, opts = {}) => $('button', { class: opts.danger ? 'danger' : '', type: 'button', title: opts.title || label, onclick: () => { dismissMenu(false); try { onClick?.(); } catch (err) { console.warn('[RoomGrid] menu action failed', err); try { Diagnostics.report('menu', err, 'action failed'); } catch (_) {} } }, }, label); const addSection = (label, items) => { menu.appendChild(sectionTitle(label)); items.filter(Boolean).forEach(el => menu.appendChild(el)); menu.appendChild(divider()); }; const currentPageIds = () => { const list = typeof fullVisibleRooms === 'function' ? pagedRooms(fullVisibleRooms()) : visibleRooms(); return list.map(r => r.id); }; addSection(sectionLabel('界面', 'Interface', 'Oberfläche'), [ item(t('menuLayoutSettings'), () => openLayoutSettings()), item(sectionLabel('模块', 'Modules', 'Module'), () => openModuleSettingsPanel()), item(t('menuFocusRoomManager'), () => openFocusRoomManager(), { title: t('focusRoomManagerHint') }), item(t('menuFollowedSync'), () => syncFollowedRooms({ manual: true }), { title: t('followedSyncHint') }), item(store.state.settings.followedSyncEnabled === false ? t('menuFollowedAutoOff') : t('menuFollowedAutoOn'), () => { const enabled = store.state.settings.followedSyncEnabled === false; store.patchSettings({ followedSyncEnabled: enabled }); scheduleFollowedSync(); toast(enabled ? t('followedSyncEnabled') : t('followedSyncDisabled')); if (enabled) syncFollowedRooms({ manual: true }); }, { title: t('followedSyncHint') }), item(t('menuPlaybackSettings'), () => openPlaybackSettingsPanel()), item(store.state.settings.theme === 'dark' ? t('menuLightMode') : t('menuDarkMode'), () => { const darkMode = store.state.settings.theme !== 'dark'; store.patchSettings({ theme: darkMode ? 'dark' : 'light' }); toast(darkMode ? t('darkModeOn') : t('darkModeOff')); }, { title: t('darkModeHint') }), item((isTipSoundMuted() ? '✓ ' : '☐ ') + t('menuTipSound'), () => { const muted = !isTipSoundMuted(); setTipSoundMuted(muted); toast(muted ? t('tipSoundMuted') : t('tipSoundUnmuted')); }, { title: t('tipSoundMuteHint') }), item(store.state.settings.toolbarCollapsed ? sectionLabel('显示顶部工具栏', 'Show top controls', 'Werkzeugleiste anzeigen') : sectionLabel('收起顶部工具栏', 'Collapse top controls', 'Werkzeugleiste einklappen'), () => { { const v = !store.state.settings.toolbarCollapsed; document.body.classList.toggle('rg-toolbar-collapsed', v); store.patchSettings({ toolbarCollapsed: v }); } }), item(store.state.settings.sidebarCollapsed ? sectionLabel('显示左侧分组', 'Show groups', 'Gruppen anzeigen') : sectionLabel('收起左侧分组', 'Collapse groups', 'Gruppen einklappen'), () => { { const v = !store.state.settings.sidebarCollapsed; document.body.classList.toggle('rg-sidebar-collapsed', v); sidebar.classList.toggle('is-collapsed', v); store.patchSettings({ sidebarCollapsed: v }); } }), item(t('menuViewerMode'), () => toggleViewerMode(), { title: t('viewerModeHint') }), item(t('menuPureMode'), () => togglePureMode(), { title: t('pureModeHint') }), ]); addSection(sectionLabel('窗口', 'Windows', 'Fenster'), [ item(sectionLabel('多摄像头控制中心', 'MultiCam control center', 'MultiCam-Kontrollzentrum'), () => openMultiCamCenter()), item(t('menuToggleFit'), () => toggleVideoFit(), { title: t('videoFitHint') }), item(t('menuToggleThumbs'), () => toggleFocusThumbs(), { title: t('focusThumbsHint') }), item(t('menuPauseVisible'), () => { const ids = currentPageIds(); service.pauseAll(ids); requestAnimationFrame(() => ids.forEach(updateCardButtons)); toast(t('pausedVisible')); }, { title: t('menuPauseVisible') }), item(t('menuResumeVisible'), () => { const ids = currentPageIds(); service.resumeAll(ids); requestAnimationFrame(() => ids.forEach(updateCardButtons)); toast(t('resumedVisible')); }, { title: t('menuResumeVisible') }), item(t('menuMuteAll'), () => { setTemporaryAudioMuted(true); }), item(tx('恢复保存的声音', 'Restore saved audio', 'Gespeicherten Ton wiederherstellen'), () => { setTemporaryAudioMuted(false); }), item(t('batchOpenCurrentPage'), () => openCurrentPageRooms()), item(t('batchMoveCurrentPage'), () => moveCurrentPageToGroup()), item(sectionLabel('在新窗口打开当前工作区', 'Open workspace in new window', 'Arbeitsbereich in neuem Fenster öffnen'), () => { const workspace = store.state.workspaces?.find(item => item.id === store.state.settings.activeWorkspaceId && !item.archived); windowCoordinator.openWindow(workspace?.id || DEFAULT_WORKSPACE_ID, workspace?.name || store.state.settings.windowName || 'RoomGrid', { workspace: true }); }), ]); if (ROOMGRID_CAPTURE_TOOLS_ENABLED) addSection(sectionLabel('录制', 'Recording', 'Aufnahmen'), [ item(t('menuRecordingCenter'), () => openRecordingCenter()), item(t('menuRecordingSettings'), () => openRecordingSettingsPanel()), item(t('recordCurrentPage'), () => recordCurrentPage()), item(t('recordCurrentGroup'), () => recordCurrentGroup()), item(t('recordOnlineRooms'), () => recordOnlineRooms()), item(t('menuStopRecordings'), () => stopAllRecordings({ final: true })), ]); addSection(sectionLabel('数据', 'Data', 'Daten'), [ item(t('menuStatistics'), () => openStatisticsPanel()), item(t('manualImport'), () => openManualImportPrompt(), { title: t('manualImport') }), item(t('menuBackupPanel'), () => openBackupPanel()), item(t('menuStatusHistory'), () => openStatusHistoryPanel()), item(t('menuGroupRules'), () => openGroupRulesPanel()), item(t('menuTempUrlManager'), () => openTemporaryUrlManager()), item(t('menuShareWorkspace'), () => openSharePanel()), item(tx('导入与导出', 'Import and export', 'Import und Export'), () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'data')), item(t('menuCopyUsernames'), async () => { await copyText(store.state.rooms.map(r => r.id).sort().join('\n')); toast(t('copied')); }), item(t('menuExportUsernames'), () => { const names = store.state.rooms.map(r => r.id).sort().join('\n'); downloadBlob(new Blob([names + (names ? '\n' : '')], { type: 'text/plain;charset=utf-8' }), `roomgrid-usernames-${Date.now()}.txt`); }), item(t('menuRepairData'), () => { store.repairData(); alert(t('repairDone')); }), ]); addSection(sectionLabel('语言', 'Language', 'Sprache', 'Idioma', 'Idioma'), [ $('div', { style: { padding: '0 8px 4px' } }, [createLanguageSwitcher('menu-language-select')]), ]); addSection(sectionLabel('帮助', 'Help', 'Hilfe'), [ item(t('menuShortcutPanel'), () => openShortcutPanel(), { title: t('menuShortcutPanel') }), item(t('menuShortcutHelp'), () => alert(t('shortcutsHelp')), { title: t('menuShortcutHelp') }), item(t('menuAbout'), () => showAboutPanel(), { title: t('menuAbout') }), ]); menu.appendChild(item(t('menuClearAll'), () => { if (confirm(t('clearAllConfirm'))) { try { service.stopAll(); } catch (_) { stopAllPageMedia(); } Storage.clearAll(); location.reload(); } }, { danger: true })); document.body.appendChild(menu); anchor.setAttribute('aria-expanded', 'true'); requestAnimationFrame(() => menu.querySelector('button:not(:disabled),select:not(:disabled)')?.focus()); const placeMoreMenu = () => { if (!menu.isConnected || !anchor.isConnected) return; const viewport = window.visualViewport; const viewportLeft = Number(viewport?.offsetLeft) || 0; const viewportTop = Number(viewport?.offsetTop) || 0; const viewportWidth = Math.max(1, Number(viewport?.width) || window.innerWidth || document.documentElement.clientWidth || 1); const viewportHeight = Math.max(1, Number(viewport?.height) || window.innerHeight || document.documentElement.clientHeight || 1); const viewportRight = viewportLeft + viewportWidth; const viewportBottom = viewportTop + viewportHeight; const gap = 6; const padding = Math.min(8, Math.max(0, (viewportWidth - 1) / 2)); const anchorRect = anchor.getBoundingClientRect(); const menuWidth = Math.max(1, Math.min(300, viewportWidth - (padding * 2))); menu.style.width = `${menuWidth}px`; menu.style.maxWidth = `${menuWidth}px`; menu.style.left = `${Math.min( Math.max(anchorRect.left, viewportLeft + padding), Math.max(viewportLeft + padding, viewportRight - menuWidth - padding), )}px`; const spaceBelow = Math.max(1, viewportBottom - anchorRect.bottom - gap - padding); const spaceAbove = Math.max(1, anchorRect.top - viewportTop - gap - padding); const openAbove = spaceBelow < 180 && spaceAbove > spaceBelow; menu.style.maxHeight = `${Math.max(1, Math.min(620, openAbove ? spaceAbove : spaceBelow))}px`; const measuredHeight = Math.min(menu.scrollHeight, parseFloat(menu.style.maxHeight) || menu.scrollHeight); const wantedTop = openAbove ? anchorRect.top - gap - measuredHeight : anchorRect.bottom + gap; menu.style.top = `${Math.min( Math.max(wantedTop, viewportTop + padding), Math.max(viewportTop + padding, viewportBottom - measuredHeight - padding), )}px`; }; placeMoreMenu(); _moreMenuRelayout = placeMoreMenu; window.addEventListener('resize', placeMoreMenu, { passive: true }); window.visualViewport?.addEventListener('resize', placeMoreMenu, { passive: true }); window.visualViewport?.addEventListener('scroll', placeMoreMenu, { passive: true }); const close = (ev) => { if (!menu.contains(ev.target) && ev.target !== anchor) { dismissMenu(); } }; _moreMenuClose = close; _moreMenuKeydown = event => { if (!menu.isConnected) return; if (event.key === 'Escape') { event.preventDefault(); dismissMenu(true); return; } if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || event.target?.matches?.('select,input,textarea')) return; const focusable = [...menu.querySelectorAll('button:not(:disabled),select:not(:disabled)')].filter(node => node.offsetParent !== null); if (!focusable.length) return; const current = Math.max(0, focusable.indexOf(document.activeElement)); const next = event.key === 'Home' ? 0 : event.key === 'End' ? focusable.length - 1 : (current + (event.key === 'ArrowDown' ? 1 : -1) + focusable.length) % focusable.length; event.preventDefault(); focusable[next].focus(); }; document.addEventListener('keydown', _moreMenuKeydown, true); setTimeout(() => document.addEventListener('click', close), 0); } /* ---- About / product identity ---- */ function showAboutPanel() { const overlay = $('div', { style: { position: 'fixed', inset: '0', background: 'rgba(17,24,39,.32)', backdropFilter: 'blur(2px)', zIndex: '10000', display: 'flex', alignItems: 'center', justifyContent: 'center', }, onclick: (e) => { if (e.target === overlay) overlay.remove(); }, }); const card = $('div', { style: { background: 'var(--bg-elevated)', border: '1px solid var(--border)', borderRadius: '14px', padding: '26px 28px', minWidth: '340px', maxWidth: '440px', boxShadow: 'var(--shadow-lg)', color: 'var(--text)', }, }); const row = (label, value) => $('div', { style: { display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid var(--border)', fontSize: '13px' }, }, [ $('span', { style: { color: 'var(--text-muted)' } }, label), $('span', { style: { color: 'var(--text)', fontFamily: 'ui-monospace,monospace' } }, value), ]); card.append( $('div', { style: { fontSize: '20px', fontWeight: '700', marginBottom: '4px' } }, t('aboutTitle')), $('div', { style: { fontSize: '13px', color: 'var(--text-muted)', marginBottom: '14px', lineHeight: '1.45' } }, `${t('title')} · ${t('appTagline')}`), row(t('aboutAuthor'), META.author), row(t('aboutVersion'), 'v' + META.version), row(t('aboutLicense'), META.license), $('div', { style: { marginTop: '14px', padding: '10px 12px', border: '1px solid var(--border)', borderRadius: '9px', background: 'var(--bg)' }, }, [ $('strong', { style: { display: 'block', fontSize: '12px', marginBottom: '6px' } }, tx('本版本更新', 'Changes in this version', 'Änderungen dieser Version')), $('div', { style: { fontSize: '11px', color: 'var(--text-muted)', lineHeight: '1.55' } }, tx( '更快的启动和完整目录加载、共享统计数据初始化、流畅的页面切换以及修复的 SingleCam 和 MultiCam 菜单。', 'Faster startup and complete catalog loading, shared statistics initialization, smooth page switching, and corrected SingleCam and MultiCam menus.', 'Schnellerer Start und vollständiges Katalogladen, gemeinsam initialisierte Statistikdaten, flüssige Seitenwechsel sowie korrigierte SingleCam- und MultiCam-Menüs.' )), ]), $('div', { style: { marginTop: '14px', padding: '12px', background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: '9px', }, }, [ $('strong', { style: { display: 'block', fontSize: '12px', marginBottom: '6px' } }, tx('MIT 许可证是什么意思?', 'What does the MIT license mean?', 'Was bedeutet die MIT-Lizenz?')), $('div', { style: { fontSize: '11px', color: 'var(--text-muted)', lineHeight: '1.55' } }, tx( '它允许使用、修改和分发软件。版权和许可证声明必须保留;软件不附带任何保证。Camclient 的作者是 Emberhost。', 'It permits using, modifying, and distributing the software. Copyright and license notices must remain, and the software comes without warranty. Camclient is authored by Emberhost.', 'Sie erlaubt, die Software zu verwenden, zu verändern und weiterzugeben. Copyright- und Lizenzhinweise müssen erhalten bleiben; eine Gewährleistung gibt es nicht. Autor von Camclient ist Emberhost.' )), ]), $('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '20px' } }, [ $('button', { style: { padding: '8px 18px', background: 'var(--accent)', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '13px' }, onclick: () => overlay.remove(), }, t('aboutClose')), ]), ); overlay.appendChild(card); document.body.appendChild(overlay); } function visibleRooms() { const s = store.state; const ag = s.settings.activeGroup || DEFAULT_GROUP_ID; let list = ag === LIBRARY_GROUP_ID ? [...allRoomsForView()] : allRoomsForView().filter(r => roomInGroup(r, ag)); const now = Date.now(); list = list.filter(room => room.temporary || (!room.archived && Number(room.hiddenUntil || 0) <= now && Number(room.pausedUntil || 0) <= now)); const q = normalizeUsername(s.settings.searchQuery || ''); if (q) list = list.filter(r => normalizeUsername(r.id).includes(q)); const f = s.settings.filter; if (f.hideOffline) list = list.filter(r => r.lastStatus !== 'offline'); if (f.hidePrivate) list = list.filter(r => r.lastStatus !== 'private'); if (f.onlyOnline) list = list.filter(r => r.lastStatus === 'online'); const statFor = (room) => analytics.getModelSync(room?.id) || {}; if (f.onlyWatched) list = list.filter(r => Number(statFor(r).totalMs) > 0); if (f.neverWatched) list = list.filter(r => Number(statFor(r).totalMs || 0) <= 0); if (f.favoritesOnly) list = list.filter(r => roomInGroup(r, FAVORITE_GROUP_ID)); if (f.followedOnly) { const followedId = s.settings.followedGroupId; list = followedId ? list.filter(r => roomInGroup(r, followedId)) : []; } if (f.hasNotes) list = list.filter(r => String(r.notes || '').trim()); if (f.tagInclude) { const needle = String(f.tagInclude).trim().toLowerCase(); if (needle) list = list.filter(r => (Array.isArray(statFor(r).tags) ? statFor(r).tags : []).some(tag => String(tag).toLowerCase().includes(needle))); } if (f.tagExclude) { const needle = String(f.tagExclude).trim().toLowerCase(); if (needle) list = list.filter(r => !(Array.isArray(statFor(r).tags) ? statFor(r).tags : []).some(tag => String(tag).toLowerCase().includes(needle))); } if (Number(f.minWatchMs) > 0) list = list.filter(r => Number(statFor(r).totalMs || 0) >= Number(f.minWatchMs)); if (Number(f.minSessions) > 0) list = list.filter(r => Number(statFor(r).sessions || 0) >= Number(f.minSessions)); if (Number(f.lastWatchedDays) > 0) { const since = Date.now() - Number(f.lastWatchedDays) * 86400000; list = list.filter(r => Number(statFor(r).lastWatchedAt || 0) >= since); } if (Number(f.viewerMin) > 0) list = list.filter(r => r.viewerCount != null && Number(r.viewerCount) >= Number(f.viewerMin)); if (Number(f.viewerMax) > 0) list = list.filter(r => r.viewerCount != null && Number(r.viewerCount) <= Number(f.viewerMax)); if (f.inActiveView) { const activeIds = new Set(); const contextKey = gridViewContextKey(); const savedMap = store.state.settings.gridRoomSlots || {}; const saved = savedMap[contextKey] || Object.entries(savedMap).find(([key]) => key.length === 220 && contextKey.startsWith(key))?.[1]; if (Array.isArray(saved)) saved.filter(Boolean).forEach(id => activeIds.add(id)); else list.slice(clampCurrentPage(list.length) * layoutSize(), clampCurrentPage(list.length) * layoutSize() + layoutSize()).forEach(r => activeIds.add(r.id)); list = list.filter(r => activeIds.has(r.id)); } if (s.settings.showRecordingOnly) list = list.filter(r => recordings.has(r.id)); const sb = s.settings.sortBy; if (sb === 'manual') list.sort((a, b) => roomOrderInGroup(a, ag) - roomOrderInGroup(b, ag)); else if (sb === 'name') list.sort((a, b) => a.id.localeCompare(b.id)); else if (sb === 'addedAt') list.sort((a, b) => b.addedAt - a.addedAt); else if (sb === 'watchTime') list.sort((a, b) => (Number(statFor(b).totalMs) || 0) - (Number(statFor(a).totalMs) || 0)); else if (sb === 'sessions') list.sort((a, b) => (Number(statFor(b).sessions) || 0) - (Number(statFor(a).sessions) || 0)); else if (sb === 'lastWatched') list.sort((a, b) => (Number(statFor(b).lastWatchedAt) || 0) - (Number(statFor(a).lastWatchedAt) || 0)); else if (sb === 'firstWatched') list.sort((a, b) => (Number(statFor(a).firstWatchedAt) || Infinity) - (Number(statFor(b).firstWatchedAt) || Infinity)); else if (sb === 'viewerCount') list.sort((a, b) => (b.viewerCount == null) - (a.viewerCount == null) || (Number(b.viewerCount) || 0) - (Number(a.viewerCount) || 0)); else if (sb === 'lastOnline') list.sort((a, b) => (Number(b.lastSeenOnline) || 0) - (Number(a.lastSeenOnline) || 0)); else if (sb === 'status') { const rank = { online: 0, private: 1, loading: 2, error: 3, offline: 4, unknown: 5 }; list.sort((a, b) => (rank[a.lastStatus] ?? 9) - (rank[b.lastStatus] ?? 9)); } if (s.settings.favoriteFirst !== false && ag !== FAVORITE_GROUP_ID && ['manual', 'status', 'name', 'addedAt'].includes(sb)) { list = list .map((room, idx) => ({ room, idx, fav: roomInGroup(room, FAVORITE_GROUP_ID) ? 1 : 0 })) .sort((a, b) => (b.fav - a.fav) || (a.idx - b.idx)) .map(item => item.room); } return list; } function scheduleWorkspaceReplacement(room) { const id = normalizeUsername(room?.id); if (!id) return; clearTimeout(replacementTimers.get(id)); replacementTimers.delete(id); if (room.lastStatus !== 'offline') return; const workspace = store.state.workspaces?.find(item => item.id === store.state.settings.activeWorkspaceId && !item.archived); const rule = workspace?.replacement; if (!rule?.enabled) return; const focusMode = store.state.settings.viewMode === 'focus'; const contextKey = gridViewContextKey(); const configuredSlots = focusMode ? (Array.isArray(store.state.settings.focusRoomSlots) ? store.state.settings.focusRoomSlots : []) : (Array.isArray(store.state.settings.gridRoomSlots?.[contextKey]) ? store.state.settings.gridRoomSlots[contextKey] : []); const slotIndex = configuredSlots.indexOf(id); if (slotIndex < 0) return; const timer = setTimeout(() => { replacementTimers.delete(id); const latest = store.state.rooms.find(item => item.id === id); if (!latest || latest.lastStatus !== 'offline') return; const liveWorkspace = store.state.workspaces?.find(item => item.id === store.state.settings.activeWorkspaceId && !item.archived); if (liveWorkspace?.id !== workspace.id || !liveWorkspace.replacement?.enabled) return; const liveFocusMode = store.state.settings.viewMode === 'focus'; const liveContextKey = gridViewContextKey(); if (liveFocusMode !== focusMode || (!focusMode && liveContextKey !== contextKey)) return; const liveSlots = liveFocusMode ? (Array.isArray(store.state.settings.focusRoomSlots) ? store.state.settings.focusRoomSlots : []) : (Array.isArray(store.state.settings.gridRoomSlots?.[liveContextKey]) ? store.state.settings.gridRoomSlots[liveContextKey] : []); if (liveSlots[slotIndex] !== id) return; const occupied = new Set(liveSlots.filter(Boolean)); const preferredOrder = new Map((liveWorkspace.replacement?.fallbackRoomIds || []).map((roomId, index) => [normalizeUsername(roomId), index])); const candidates = store.state.rooms.filter(candidate => candidate.id !== id && candidate.lastStatus === 'online' && !candidate.archived && Number(candidate.hiddenUntil || 0) <= Date.now() && Number(candidate.pausedUntil || 0) <= Date.now() && (preferredOrder.has(candidate.id) || !liveWorkspace.replacement.groupId || roomInGroup(candidate, liveWorkspace.replacement.groupId)) && !occupied.has(candidate.id)) .sort((left, right) => { const leftPreferred = preferredOrder.has(left.id) ? preferredOrder.get(left.id) : Number.MAX_SAFE_INTEGER; const rightPreferred = preferredOrder.has(right.id) ? preferredOrder.get(right.id) : Number.MAX_SAFE_INTEGER; return leftPreferred - rightPreferred || Number(right.priority || 0) - Number(left.priority || 0) || Number(right.rating || 0) - Number(left.rating || 0) || Number(right.viewerCount || 0) - Number(left.viewerCount || 0); }); const replacement = candidates[0]; if (!replacement) return; if (liveFocusMode) { store.patchSettings({ focusRoomSlots: liveSlots.map(value => value === id ? replacement.id : value) }); } else { store.patchSettings({ gridRoomSlots: { ...(store.state.settings.gridRoomSlots || {}), [liveContextKey]: liveSlots.map(value => value === id ? replacement.id : value) } }); } analytics.recordEvent({ type: 'workspace', detail: `auto-replace:${id}->${replacement.id}` }); toast(tx(`${id} 已替换为 ${replacement.id}`, `${id} was replaced by ${replacement.id}`, `${id} wurde durch ${replacement.id} ersetzt`, `${id} fue reemplazado por ${replacement.id}`, `${id} foi substituído por ${replacement.id}`)); }, clampInt(rule.delaySeconds, 0, 600, 20) * 1000); replacementTimers.set(id, timer); } function renderCardState(room) { const c = cardMap.get(room.id); if (!c) return; const muted = room.muted; const meta = statusMeta(room.lastStatus); c.badge.replaceChildren(); c.badge.append( $('span', { class: 'dot', style: { background: meta.color } }), $('span', { class: 'pill-text' }, meta.label), ...(muted ? [$('span', { style: { marginLeft: '4px', fontSize: '10px', color: 'var(--text-muted)' } }, tx('静音', 'Muted', 'Stumm'))] : []), ); updateCardButtons(room.id); // 状态覆盖层文本 + video DOM 清理 if (room.lastStatus === 'online') { c.root.classList.remove('not-online'); renderStreamStartupState(room.id); applyMute(room.id); resumeWaitingRecording(room.id); } else { delete c.statusEl.dataset.streamStartup; c.root.classList.add('not-online'); c.statusEl.style.display = 'flex'; pauseRecordingForSourceLoss(room.id); // 关键修复:状态非 online 时彻底清理 video 节点。 // 否则会在卡片中央显示大黑块,且可能残留音频。 if (c.video) { detachAudioMeter(room.id); service.detachVideo(room.id); c.video = null; } else { detachAudioMeter(room.id); service.detachVideo(room.id); } const lines = [ $('div', { class: 'status-icon', style: { color: meta.color } }, [$('span', { class: 'status-dot-large' })]), $('div', { class: 'status-chip', style: { color: meta.color } }, meta.label), room.lastSeenOnline ? $('div', { style: { fontSize: '11px', color: 'var(--text-muted)' } }, t('lastSeen', fmtTime(room.lastSeenOnline))) : null, (room.lastStatus === 'offline' || room.lastStatus === 'private') ? $('div', { style: { fontSize: '10px', color: 'var(--text-muted)', opacity: '.7' } }, t('autoDetect')) : null, ]; c.statusEl.replaceChildren(); lines.forEach(l => l && c.statusEl.appendChild(l)); } } function renderStreamStartupState(id) { const c = cardMap.get(id); if (!c || c.root.classList.contains('not-online')) return; const connection = service.getMetrics(id).connection; const blocked = connection === 'play-blocked'; const failed = ['media-error', 'stream-error', 'request-error'].includes(connection); const ready = !!c.video && c.video.readyState >= 2; const cap = clampInt(store.state.settings.performanceMaxStreams, 1, MAX_LAYOUT_SLOTS, 8); const limited = !c.video && [...cardMap.values()].filter(card => card.video && !card.video.ended).length >= cap; const state = blocked ? 'blocked' : failed ? 'error' : ready ? 'ready' : limited ? 'limit' : 'loading'; c.statusEl.style.display = state === 'ready' ? 'none' : 'flex'; if (c.statusEl.dataset.streamStartup === state) return; c.statusEl.dataset.streamStartup = state; c.statusEl.replaceChildren(); if (state === 'ready') return; const label = limited ? tx('已达到性能上限', 'Performance limit reached', 'Leistungsgrenze erreicht') : statusMeta(failed ? 'error' : 'loading').label; if (!blocked) c.statusEl.appendChild($('div', { class: 'status-chip' }, label)); if (blocked || failed) c.statusEl.appendChild($('button', { class: 'ctrl-btn primary', style: { pointerEvents: 'auto' }, onclick: event => { event.stopPropagation(); if (blocked) service.resume(id); else service.refresh(id); } }, t(blocked ? 'opResume' : 'opRefresh'))); } function audioRouteTargetId() { const mode = store.state.settings.audioRoutingMode || 'manual'; if (mode === 'focus' && store.state.settings.viewMode === 'focus') { const focused = normalizeUsername(store.state.settings.focusedRoomId); if (focused && findRoomAny(focused)) return focused; } const solo = normalizeUsername(store.state.settings.audioSoloRoomId); if (solo && findRoomAny(solo)) return solo; return visibleRooms().find(room => room.lastStatus === 'online')?.id || visibleRooms()[0]?.id || ''; } function audioRouteAllows(id) { const mode = store.state.settings.audioRoutingMode || 'manual'; return mode === 'manual' || normalizeUsername(id) === audioRouteTargetId(); } function applyAllAudioRouting() { cardMap.forEach((_, id) => applyMute(id)); if (audioModeSel) audioModeSel.value = store.state.settings.audioRoutingMode || 'manual'; } function setTemporaryAudioMuted(muted) { temporaryAudioMuted = !!muted; applyAllAudioRouting(); } function routeAudioToRoom(id) { id = normalizeUsername(id); if (!findRoomAny(id)) return; store.patchSettings({ audioRoutingMode: 'solo', audioSoloRoomId: id }); requestAnimationFrame(applyAllAudioRouting); toast(tx(`仅播放 ${id} 的声音`, `Playing audio only from ${id}`, `Nur der Ton von ${id} wird wiedergegeben`)); } function applyMute(id) { const c = cardMap.get(id); const r = findRoomAny(id); if (!c || !c.video || !r) return; const master = Math.max(0, Math.min(1, Number(store.state.settings.volume) || 0)); const individual = r.volume == null ? 1 : Math.max(0, Math.min(1, Number(r.volume) || 0)); const v = master * individual; const routed = audioRouteAllows(id); c.video.volume = temporaryAudioMuted || r.muted || !routed ? 0 : v; c.video.muted = temporaryAudioMuted || r.muted || !routed || v === 0; c.root.classList.toggle('audio-routed-muted', !routed); const meter = audioMeters.get(id); if (meter) updateAudioMeterVisual(meter, meter.level, meter.unavailable ? 'unavailable' : !meter.analyser ? 'waiting' : c.video.paused ? 'paused' : meter.level > .015 ? 'active' : 'still'); scheduleAudioMeterLoop(); } function getVideoTransform(roomId) { return sanitizeVideoTransform(store.state.settings.videoTransforms?.[normalizeUsername(roomId)]); } function buildVideoTransformCss(transform) { const t = sanitizeVideoTransform(transform); const parts = []; if (t.x || t.y) parts.push(`translate(${t.x}px, ${t.y}px)`); if (t.zoom !== 1) parts.push(`scale(${t.zoom})`); if (t.mirror || t.flip) parts.push(`scale(${t.mirror ? -1 : 1}, ${t.flip ? -1 : 1})`); if (t.rotation) parts.push(`rotate(${t.rotation}deg)`); return parts.join(' '); } function patchVideoTransform(roomId, patch) { roomId = normalizeUsername(roomId); if (!roomId) return; const next = sanitizeVideoTransform({ ...getVideoTransform(roomId), ...(patch || {}) }); store.update(s => { s.settings.videoTransforms = sanitizeVideoTransformMap(s.settings.videoTransforms); if (isDefaultVideoTransform(next)) delete s.settings.videoTransforms[roomId]; else s.settings.videoTransforms[roomId] = next; }, 'settings:videoTransforms'); applyVideoTransform(roomId); } function resetVideoTransform(roomId) { patchVideoTransform(roomId, defaultVideoTransform()); } function applyVideoTransform(roomId) { const c = cardMap.get(normalizeUsername(roomId)); if (!c?.video) return; const transform = getVideoTransform(roomId); const css = buildVideoTransformCss(transform); c.video.style.transform = css; c.video.style.transformOrigin = 'center center'; c.video.style.filter = `brightness(${transform.brightness}) contrast(${transform.contrast}) saturate(${transform.saturation}) hue-rotate(${transform.hue}deg) sepia(${transform.sepia}) grayscale(${transform.grayscale}) invert(${transform.invert}) blur(${transform.blur}px)`; c.video.style.cursor = transform.zoom > 1 ? 'grab' : ''; c.root.classList.toggle('video-transformed', !isDefaultVideoTransform(transform)); c.root.classList.toggle('video-zoomed', transform.zoom > 1); } function applyAllVideoTransforms() { cardMap.forEach((_, id) => applyVideoTransform(id)); } function installCardZoomHandlers(card, roomId) { let dragging = false; let lastX = 0; let lastY = 0; const isUiTarget = (target) => !!target?.closest?.('.icon-btn,.drag-handle,.menu-pop,.roomgrid-modal-backdrop,button,input,select,textarea,a'); card.addEventListener('wheel', (e) => { if (store.state.settings.freeZoom === false || (!e.ctrlKey && !e.metaKey) || isUiTarget(e.target)) return; e.preventDefault(); const cur = getVideoTransform(roomId); const nextZoom = Math.max(1, Math.min(20, cur.zoom * (e.deltaY < 0 ? 1.12 : 0.88))); const rounded = Math.round(nextZoom * 100) / 100; patchVideoTransform(roomId, { zoom: rounded, x: rounded === 1 ? 0 : cur.x, y: rounded === 1 ? 0 : cur.y }); }, { passive: false }); card.addEventListener('mousedown', (e) => { if (store.state.settings.freeZoom === false || e.button !== 0 || isUiTarget(e.target)) return; const cur = getVideoTransform(roomId); if (cur.zoom <= 1) return; dragging = true; lastX = e.clientX; lastY = e.clientY; card.classList.add('video-panning'); const c = cardMap.get(roomId); if (c?.video) c.video.style.cursor = 'grabbing'; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; const cur = getVideoTransform(roomId); patchVideoTransform(roomId, { x: cur.x + e.clientX - lastX, y: cur.y + e.clientY - lastY, }); lastX = e.clientX; lastY = e.clientY; }); window.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; card.classList.remove('video-panning'); applyVideoTransform(roomId); }); } function attachVideoElement(roomId) { const c = cardMap.get(roomId); if (!c) return null; const cap = clampInt(store.state.settings.performanceMaxStreams, 1, MAX_LAYOUT_SLOTS, 8); const playing = [...cardMap.values()].filter(card => card.video && !card.video.ended).length; if (!c.video && playing >= cap) { if (c.statusEl) c.statusEl.textContent = tx('已达到性能上限', 'Performance limit reached', 'Leistungsgrenze erreicht'); return null; } // 移除旧 video:必须同步销毁 HLS,否则旧 buffer 可能继续出声。 if (c.video) { pauseRecordingForSourceLoss(roomId); detachAudioMeter(roomId); service.detachVideo(roomId); c.video = null; } const video = $('video', { controls: false, autoplay: true, playsInline: true, draggable: false, disablePictureInPicture: false, crossOrigin: 'anonymous', controlsList: 'nodownload noplaybackrate nofullscreen', dataset: { multicamRoom: roomId, multicamRoomId: roomId }, class: 'cam-video', style: { pointerEvents: 'none', filter: 'none', opacity: '1' }, }); try { video.controls = false; video.removeAttribute('controls'); } catch (_) {} const r = findRoomAny(roomId); const master = Math.max(0, Math.min(1, Number(store.state.settings.volume) || 0)); const individual = r?.volume == null ? 1 : Math.max(0, Math.min(1, Number(r.volume) || 0)); const v = master * individual; const routed = audioRouteAllows(roomId); video.volume = r?.muted || !routed ? 0 : v; video.muted = r?.muted || !routed || v === 0; // 插入到状态层之前 c.root.insertBefore(video, c.statusEl); c.video = video; attachAudioMeter(roomId, video); video.addEventListener('play', () => { resumeAudioContext(); attachAudioMeter(roomId, video, { force: true }); updateCardButtons(roomId); }); video.addEventListener('pause', () => updateCardButtons(roomId)); video.addEventListener('loadeddata', () => { renderStreamStartupState(roomId); resumeWaitingRecording(roomId); }); video.addEventListener('playing', () => { resumeAudioContext(); attachAudioMeter(roomId, video, { force: true }); resumeWaitingRecording(roomId); }); service.attachVideo(roomId, video); applyVideoTransform(roomId); updateCardButtons(roomId); setTimeout(() => resumeWaitingRecording(roomId), 1200); return video; } function attachTemporarySource(room) { if (!room || !room.sourceUrl) return; const c = cardMap.get(room.id); if (!c) return; const video = attachVideoElement(room.id); if (!video) return; video.controls = true; video.style.pointerEvents = 'auto'; try { c.tempHls?.destroy?.(); } catch (_) {} c.tempHls = null; if (isHlsUrl(room.sourceUrl) && window.Hls && Hls.isSupported()) { const hls = new Hls({ lowLatencyMode: true, enableWorker: true }); c.tempHls = hls; hls.loadSource(room.sourceUrl); hls.attachMedia(video); } else { video.src = room.sourceUrl; } video.play?.().catch?.(() => {}); renderCardState(room); } // ---- Grid 渲染(增量,支持 grid / focus 双模式)---- function renderGrid() { if (isRoomDragActive()) { dragGridRenderDeferred = true; return; } const mode = store.state.settings.viewMode; // 切换 grid class grid.classList.toggle('view-grid', mode === 'grid'); grid.classList.toggle('view-focus', mode === 'focus'); applyGridSize(); const fullList = fullVisibleRooms(); clampCurrentPage(fullList.length); syncLayoutControls(); const list = mode === 'focus' ? focusRoomsForView(fullList) : gridRoomsForView(pagedRooms(fullList)); const wantIds = new Set(list.filter(Boolean).map(r => r.id)); // 移除不可见/已删除的卡片:先停流再移 DOM,避免删除后残留声音。 // 如果只是被过滤/分组隐藏,不删除 session,只 detach 媒体,保留后续状态轮询。 const allRoomIds = new Set(allRoomsForView().map(r => r.id)); for (const [id, c] of cardMap) { if (!wantIds.has(id)) { pauseRecordingForSourceLoss(id, { silent: true }); const rr = findRoomAny(id); if (rr && rr.sourceUrl) { try { c.tempHls?.destroy?.(); } catch (_) {} } else if (allRoomIds.has(id)) { detachAudioMeter(id); service.detachVideo(id); } else { detachAudioMeter(id); service.stop(id); } if (rr && rr.sourceUrl) detachAudioMeter(id); try { c.video?.pause(); } catch (_) {} try { c.video?.remove(); } catch (_) {} c.video = null; try { c.resizeObserver?.disconnect(); } catch (_) {} c.resizeObserver = null; try { c.root.remove(); } catch (_) {} cardMap.delete(id); } } // 空态 const hasGridRooms = list.some(Boolean); if (fullList.length === 0 && !hasGridRooms && !(mode === 'grid' && allRoomIds.size)) { if (!grid.querySelector('.empty-state')) { grid.querySelectorAll('.resizer').forEach(el => el.roomgridResizeCleanup?.()); grid.replaceChildren(); grid.style.display = 'flex'; grid.append($('div', { class: 'empty-state' }, [ $('div', { style: { fontSize: '60px', opacity: '.3' } }, ''), $('div', { style: { fontSize: '15px' } }, (mode === 'focus' && hasCustomFocusLayout()) ? t('focusRoomNoRooms') : t('emptyTitle')), $('div', { style: { fontSize: '12px', color: 'var(--text-muted)' } }, (mode === 'focus' && hasCustomFocusLayout()) ? t('focusRoomManagerHint') : t('emptyHint')), ])); } return; } // 清掉空态(如果有) const empty = grid.querySelector('.empty-state'); if (empty) empty.remove(); if (mode === 'focus') { renderFocusLayout(list); } else { renderGridLayout(list); } } /* —— grid 布局:均分 —— */ function renderGridLayout(list) { syncLayoutControls(); // 清掉 focus 模式专属容器 grid.querySelectorAll('.focused-row, .thumbs-row, .focus-side-row, .focus-bottom-row, .resizer').forEach(el => { el.roomgridResizeCleanup?.(); // 先把里面的 cam-card detach 出来,避免一并被 remove el.querySelectorAll('.cam-card').forEach(c => grid.appendChild(c)); el.remove(); }); grid.querySelectorAll('.grid-empty-slot').forEach(el => el.remove()); Array.from({ length: layoutSize() }, (_, idx) => list[idx] || null).forEach((room, idx) => { if (!room) { const emptySlot = $('div', { class: 'grid-empty-slot', dataset: { gridSlot: String(idx) }, style: { display: 'grid', placeItems: 'center', minWidth: '0', minHeight: '0', border: '1px dashed var(--border)', borderRadius: '10px', color: 'var(--text-muted)', background: 'rgba(148,163,184,.05)', fontSize: '11px', }, }, t('gridEmptySlot', idx + 1)); emptySlot.appendChild($('div', { class: 'room-drop-hint', 'aria-hidden': 'true' })); emptySlot.addEventListener('dragover', e => handleSidebarSlotDrop(e, emptySlot)); emptySlot.addEventListener('drop', e => handleSidebarSlotDrop(e, emptySlot, true)); emptySlot.addEventListener('dragleave', e => { if (!emptySlot.contains(e.relatedTarget)) setFocusDropTarget(null); }); const slots = [...grid.children].filter(el => el.classList?.contains('cam-card') || el.classList?.contains('grid-empty-slot')); grid.insertBefore(emptySlot, slots[idx] || null); return; } let c = cardMap.get(room.id); if (!c) { buildCard(room); c = cardMap.get(room.id); grid.appendChild(c.root); if (room.sourceUrl) attachTemporarySource(room); else service.ensure(room.id); } else { resetCardSizing(c.root); if (room.sourceUrl && !c.video) attachTemporarySource(room); else if (room.lastStatus === 'online' && !c.video) service.ensure(room.id); } const slots = [...grid.children].filter(el => el.classList?.contains('cam-card') || el.classList?.contains('grid-empty-slot')); const ref = slots[idx]; if (c.root.parentElement !== grid || ref !== c.root) grid.insertBefore(c.root, ref || null); c.root.dataset.gridSlot = String(idx); if (!c.root.querySelector('.room-drop-hint')) c.root.appendChild($('div', { class: 'room-drop-hint', 'aria-hidden': 'true' })); c.root.removeAttribute('data-focus-slot'); c.root.removeAttribute('data-focus-slot-label'); c.root.classList.remove('focus-slot-target'); c.root.querySelector('.focus-slot-badge')?.remove(); resetCardSizing(c.root); applyCardGridSizing(c.root, room); renderCardState(room); }); } function focusRoom(roomId) { if (!roomId) return; const all = fullVisibleRooms(); const idx = all.findIndex(r => r.id === roomId); const page = idx >= 0 ? Math.floor(idx / layoutSize()) : (store.state.settings.pageIndex || 0); const patch = { viewMode: 'focus', focusedRoomId: roomId, pageIndex: page, focusThumbsCollapsed: false }; if (hasCustomFocusLayout()) { const slots = Array.from({ length: layoutSize() }, (_, slot) => store.state.settings.focusRoomSlots[slot] || null); const target = slots.indexOf(roomId); if (target > 0) { [slots[0], slots[target]] = [slots[target], slots[0]]; } else if (target < 0) { const previousMain = slots[0]; slots[0] = roomId; const empty = slots.findIndex((id, slot) => slot > 0 && !id); if (empty >= 0) slots[empty] = previousMain; } patch.focusRoomSlots = slots; } store.patchSettings(patch); } function setFocusDropMode(active) { const enabled = !!active; document.body.classList.toggle('focus-drag-active', enabled); grid.querySelectorAll('[data-focus-slot], [data-grid-slot]').forEach(el => { el.classList.toggle('focus-drop-available', enabled); if (enabled) setFocusDropCopy(el, 'focusDropHere'); }); } function setFocusDropCopy(target, key) { if (!target) return; const copy = target.querySelector('.room-drop-hint'); if (!copy) return; const label = target.dataset.focusSlotLabel || (target.dataset.gridSlot !== undefined ? t('gridWindowLabel', Number(target.dataset.gridSlot) + 1) : focusSlotLabel(Number(target.dataset.focusSlot))); const roomName = findRoomAny(focusDragRoomId)?.displayName || focusDragRoomId; const empty = target.classList.contains('grid-empty-slot') || target.classList.contains('focus-empty-slot'); const message = t(empty && key === 'focusDropReplace' ? 'focusDropHere' : key, label); const cacheKey = `${roomName}\n${message}`; if (copy.dataset.dropCopy === cacheKey) return; copy.dataset.dropCopy = cacheKey; copy.replaceChildren($('strong', {}, rawUiText(roomName)), $('span', {}, message)); } function setFocusDropTarget(target) { document.querySelectorAll('.focus-drop-target').forEach(el => { if (el !== target) { el.classList.remove('focus-drop-target'); setFocusDropCopy(el, 'focusDropHere'); } }); if (target) { target.classList.add('focus-drop-target'); setFocusDropCopy(target, 'focusDropReplace'); } } function focusDropTargetAtPoint(row, clientX, clientY) { if (!row || !Number.isFinite(clientX) || !Number.isFinite(clientY)) return null; const rowRect = row.getBoundingClientRect(); if (clientX < rowRect.left || clientX > rowRect.right || clientY < rowRect.top || clientY > rowRect.bottom) return null; const targets = [...row.querySelectorAll('[data-focus-slot], [data-grid-slot]')]; const inside = targets.find(target => { const rect = target.getBoundingClientRect(); return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom; }); if (inside) return inside; // The gaps and the clipped last cell are still part of the row's usable drop area. // Choose the nearest visible cell so the lower-right slot remains reachable. return targets.reduce((nearest, target) => { const rect = target.getBoundingClientRect(); const distance = Math.abs(clientX - (rect.left + rect.width / 2)) + Math.abs(clientY - (rect.top + rect.height / 2)); return !nearest || distance < nearest.distance ? { target, distance } : nearest; }, null)?.target || null; } function installFocusRowDropFallback(row) { if (!row || row.dataset.focusDropFallback === 'true') return; row.dataset.focusDropFallback = 'true'; row.addEventListener('dragover', (e) => { if (store.state.settings.viewMode !== 'focus') return; const focusId = focusDragRoomId || e.dataTransfer?.getData('text/roomgrid-focus-id'); if (!focusId) return; const target = focusDropTargetAtPoint(row, e.clientX, e.clientY); if (!target) return; e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; setFocusDropTarget(target); }); row.addEventListener('drop', (e) => { if (store.state.settings.viewMode !== 'focus') return; const focusId = focusDragRoomId || e.dataTransfer?.getData('text/roomgrid-focus-id'); if (!focusId) return; const target = focusDropTargetAtPoint(row, e.clientX, e.clientY); if (!target?.dataset?.focusSlot) return; e.preventDefault(); e.stopPropagation(); setFocusDropTarget(target); assignFocusRoomToSlot(focusId, target.dataset.focusSlot); focusDragRoomId = ''; clearFocusDropIndicators(); }); row.addEventListener('dragleave', (e) => { if (!row.contains(e.relatedTarget)) setFocusDropTarget(null); }); } function clearFocusDropIndicators() { document.querySelectorAll('.focus-drop-target').forEach(el => { el.classList.remove('focus-drop-target'); setFocusDropCopy(el, 'focusDropHere'); }); document.querySelectorAll('.focus-drop-available').forEach(el => el.classList.remove('focus-drop-available')); document.body.classList.remove('focus-drag-active'); } function handleSidebarSlotDrop(event, target, commit = false) { const roomId = focusDragRoomId || event.dataTransfer?.getData('text/roomgrid-focus-id'); const mode = store.state.settings.viewMode; const slot = target?.dataset?.[mode === 'focus' ? 'focusSlot' : 'gridSlot']; if (!roomId || !findRoomAny(roomId) || slot === undefined) return false; event.preventDefault(); event.stopPropagation(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; setFocusDropTarget(target); if (commit) { if (mode === 'focus') assignFocusRoomToSlot(roomId, slot); else { // Use the windows actually held still under the pointer, not a newly // sorted background result that the user has not seen yet. const visibleSlots = Array.from({ length: layoutSize() }, (_, idx) => grid.querySelector(`[data-grid-slot="${idx}"]`)?.dataset.roomId || null); assignGridRoomToSlot(roomId, slot, visibleSlots); } finishRoomDrag(); clearFocusDropIndicators(); } return true; } function assignFocusRoomToSlot(roomId, slotIndex) { roomId = normalizeUsername(roomId); slotIndex = Number(slotIndex); if (!isLikelyUsername(roomId) || !Number.isInteger(slotIndex) || slotIndex < 0 || slotIndex >= layoutSize()) return; if (!ensureSidebarRoom(roomId)) return; const size = layoutSize(); const slots = focusSlotIdsForCurrentLayout(); if (slots.length !== size) return; // A drop is an exact move: the chosen target is the only slot that changes // to the dragged room. The old occurrence is cleared; no implicit swap or // fill prevents the sidebar from matching the visible Focus layout. slots.forEach((id, idx) => { if (id === roomId && idx !== slotIndex) slots[idx] = null; }); slots[slotIndex] = roomId; store.patchSettings({ focusRoomSlots: slots, focusedRoomId: slots[0] || null, viewMode: 'focus', pageIndex: 0 }); toast(tx(`已放入焦点位置 ${slotIndex + 1}`, `Placed in Focus slot ${slotIndex + 1}`, `In Fokusplatz ${slotIndex + 1} platziert`, `Colocado en la posición de foco ${slotIndex + 1}`, `Colocado na posição de foco ${slotIndex + 1}`)); } function assignGridRoomToSlot(roomId, slotIndex, visibleSlots = null) { roomId = normalizeUsername(roomId); slotIndex = Number(slotIndex); if (!isLikelyUsername(roomId) || !Number.isInteger(slotIndex) || slotIndex < 0 || slotIndex >= layoutSize()) return; if (!ensureSidebarRoom(roomId)) return; const base = pagedRooms(fullVisibleRooms()); const current = gridRoomsForView(base); const slots = Array.from({ length: layoutSize() }, (_, idx) => visibleSlots ? (visibleSlots[idx] || null) : (current[idx]?.id || null)); const oldIndex = slots.indexOf(roomId); if (oldIndex >= 0 && oldIndex !== slotIndex) slots[oldIndex] = null; slots[slotIndex] = roomId; store.setGridRoomSlots(gridViewContextKey(), slots); toast(tx(`已放入网格位置 ${slotIndex + 1}`, `Placed in grid window ${slotIndex + 1}`, `In Rasterfenster ${slotIndex + 1} platziert`, `Colocado en la ventana de cuadrícula ${slotIndex + 1}`, `Colocado na janela da grade ${slotIndex + 1}`)); } let removedStreamNotice = null; function offerRemovedStreamUndo(roomId, slotIndex, mode, gridKey = null) { const workspace = store.state.settings.activeWorkspaceId; const size = layoutSize(); const keyboardRemoval = document.activeElement?.matches('.cam-remove-view:focus-visible'); removedStreamNotice?.remove(); const undo = $('button', { type: 'button', class: 'ctrl-btn', dataset: { undoStreamRemoval: '1' }, style: { marginInlineStart: '8px' }, onclick: () => { removedStreamNotice?.remove(); removedStreamNotice = null; const settings = store.state.settings; if (settings.viewMode !== mode || settings.activeWorkspaceId !== workspace || layoutSize() !== size || (gridKey !== null && gridViewContextKey() !== gridKey) || !findRoomAny(roomId)) return; const slots = mode === 'focus' ? focusSlotIdsForCurrentLayout() : settings.gridRoomSlots?.[gridKey]?.slice(); if (!slots || slots[slotIndex] || slots.includes(roomId)) { toast(tx('位置已更改', 'The slot has changed', 'Der Platz wurde inzwischen geändert')); return; } slots[slotIndex] = roomId; if (mode === 'focus') store.patchSettings({ focusRoomSlots: slots, focusedRoomId: slots[0] || null }); else store.setGridRoomSlots(gridKey, slots, true); } }, tx('撤销', 'Undo', 'Rückgängig')); removedStreamNotice = toast(tx('已从当前视图移除直播', 'Stream removed from this view', 'Stream aus dieser Ansicht entfernt'), 8000, undo); if (keyboardRemoval) undo.focus(); } function removeGridRoomFromSlot(roomId, slotIndex) { roomId = normalizeUsername(roomId); slotIndex = Number(slotIndex); if (store.state.settings.viewMode !== 'grid' || !isLikelyUsername(roomId) || !Number.isInteger(slotIndex) || slotIndex < 0 || slotIndex >= layoutSize()) return; const current = gridRoomsForView(pagedRooms(fullVisibleRooms())); if (current[slotIndex]?.id !== roomId) return; const slots = Array.from({ length: layoutSize() }, (_, idx) => current[idx]?.id || null); slots[slotIndex] = null; const gridKey = gridViewContextKey(); store.setGridRoomSlots(gridKey, slots, true); offerRemovedStreamUndo(roomId, slotIndex, 'grid', gridKey); } function removeFocusRoomFromSlot(roomId, slotIndex) { roomId = normalizeUsername(roomId); slotIndex = Number(slotIndex); if (store.state.settings.viewMode !== 'focus' || !isLikelyUsername(roomId) || !Number.isInteger(slotIndex) || slotIndex < 0 || slotIndex >= layoutSize()) return; const slots = focusSlotIdsForCurrentLayout(); if (slots[slotIndex] !== roomId) return; slots[slotIndex] = null; store.patchSettings({ focusRoomSlots: slots, focusedRoomId: slots[0] || null }); offerRemovedStreamUndo(roomId, slotIndex, 'focus'); } function focusStep(delta) { const list = hasCustomFocusLayout() ? focusRoomsForView(fullVisibleRooms()) : visibleRooms(); if (!list.length) return; const cur = store.state.settings.focusedRoomId; let idx = list.findIndex(r => r.id === cur); if (idx < 0) idx = 0; const next = list[(idx + delta + list.length) % list.length]; if (next) focusRoom(next.id); } /* —— focus 模式分隔条拖动:调整主屏宽高,副窗口自适应 —— */ function attachFocusResizerHandlers(resizer, axis) { let dragging = false, startX = 0, startY = 0, startW = 62, startH = 64, currentW = 62, currentH = 64; const apply = (w, h) => { currentW = Math.max(45, Math.min(76, w)); currentH = Math.max(44, Math.min(78, h)); grid.style.gridTemplateColumns = `minmax(260px, ${currentW}fr) 8px minmax(190px, ${100 - currentW}fr)`; grid.style.gridTemplateRows = `minmax(220px, ${currentH}fr) 8px minmax(120px, ${100 - currentH}fr)`; grid.style.setProperty('--focus-main-w', currentW + 'fr'); grid.style.setProperty('--focus-side-w', (100 - currentW) + 'fr'); grid.style.setProperty('--focus-main-h', currentH + 'fr'); grid.style.setProperty('--focus-bottom-h', (100 - currentH) + 'fr'); requestAnimationFrame(applyFocusMainSizing); }; resizer.addEventListener('mousedown', (e) => { if (e.button !== 0 || dragging) return; dragging = true; startX = e.clientX; startY = e.clientY; startW = Math.max(45, Math.min(76, Number(store.state.settings.focusMainPct || 62))); startH = Math.max(44, Math.min(78, Number(store.state.settings.focusMainHPct || 64))); currentW = startW; currentH = startH; resizer.classList.add('dragging'); document.body.style.cursor = axis === 'x' ? 'ew-resize' : 'ns-resize'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); window.addEventListener('blur', up); e.preventDefault(); }); const move = (e) => { if (!dragging) return; if (axis === 'x') { const gridW = Math.max(1, grid.clientWidth || window.innerWidth || 1); apply(startW + ((e.clientX - startX) / gridW) * 100, currentH); } else { const gridH = Math.max(1, grid.clientHeight || window.innerHeight || 1); apply(currentW, startH + ((e.clientY - startY) / gridH) * 100); } }; const cleanup = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); window.removeEventListener('blur', up); if (!dragging) return; dragging = false; resizer.classList.remove('dragging'); document.body.style.cursor = ''; document.body.style.userSelect = ''; }; const up = () => { if (!dragging) return; cleanup(); store.patchSettings({ focusMainPct: Math.round(currentW), focusMainHPct: Math.round(currentH) }); }; resizer.roomgridResizeCleanup = cleanup; } /* —— focus 布局:左上主屏;右侧 / 右下 / 下方为副窗口 —— */ function renderFocusLayout(list) { grid.querySelectorAll('.grid-empty-slot').forEach(el => el.remove()); const full = fullVisibleRooms(); const explicitSlots = focusSlotsForView(full); let focusedId = store.state.settings.focusedRoomId; if (explicitSlots) { focusedId = explicitSlots[0]?.id || null; } else if (!list.some(r => r.id === focusedId)) { const preferred = list.find(r => r.lastStatus === 'online') || list[0]; focusedId = preferred?.id || null; } if (focusedId !== store.state.settings.focusedRoomId) { store.state.settings.focusedRoomId = focusedId; scheduleSidebarRender(); } let focusedRow = grid.querySelector('.focused-row'); let sideRow = grid.querySelector('.focus-side-row'); let bottomRow = grid.querySelector('.focus-bottom-row'); let vResizer = grid.querySelector('.focus-v-resizer'); let hResizer = grid.querySelector('.focus-h-resizer'); if (!focusedRow) focusedRow = $('div', { class: 'focused-row' }); if (!sideRow) sideRow = $('div', { class: 'focus-side-row' }); if (!bottomRow) bottomRow = $('div', { class: 'focus-bottom-row' }); if (!vResizer) { vResizer = $('div', { class: 'resizer focus-v-resizer', title: tx('拖动调整主屏宽度', 'Drag to resize main width', 'Ziehen, um die Breite des Hauptbildschirms anzupassen') }); attachFocusResizerHandlers(vResizer, 'x'); } if (!hResizer) { hResizer = $('div', { class: 'resizer focus-h-resizer', title: tx('拖动调整主屏高度', 'Drag to resize main height', 'Ziehen, um die Höhe des Hauptbildschirms anzupassen') }); attachFocusResizerHandlers(hResizer, 'y'); } focusedRow.style.gridArea = 'main'; vResizer.style.gridArea = 'vbar'; hResizer.style.gridArea = 'hbar'; sideRow.style.gridArea = 'side'; bottomRow.style.gridArea = 'bottom'; [focusedRow, vResizer, hResizer, sideRow, bottomRow].forEach(el => { if (el.parentElement !== grid) grid.appendChild(el); }); [focusedRow, sideRow, bottomRow].forEach(installFocusRowDropFallback); // Remove legacy containers if any grid.querySelectorAll('.thumbs-row').forEach(el => { el.querySelectorAll('.cam-card').forEach(c => sideRow.appendChild(c)); el.remove(); }); // Direct children cards are collected before assigning. [...grid.children].forEach(el => { if (el.classList && el.classList.contains('cam-card')) sideRow.appendChild(el); }); const focusedRoom = explicitSlots ? (explicitSlots[0] || null) : list.find(r => r.id === focusedId); let sideList; let bottomList; if (explicitSlots) { const { side } = focusSecondaryShape(layoutSize()); sideList = explicitSlots.slice(1, 1 + side); bottomList = explicitSlots.slice(1 + side, layoutSize()); } else { const secondary = list.filter(r => r.id !== focusedId); const { side: sideCount } = focusSecondaryShape(secondary.length + 1); sideList = secondary.slice(0, sideCount); bottomList = secondary.slice(sideCount); } grid.classList.toggle('no-bottom', bottomList.length === 0); sideRow.style.gridTemplateRows = sideList.length ? `repeat(${sideList.length}, minmax(0, 1fr))` : '1fr'; sideRow.style.gridTemplateColumns = 'minmax(0, 1fr)'; bottomRow.style.gridTemplateColumns = bottomList.length ? `repeat(${bottomList.length}, minmax(0, 1fr))` : '1fr'; bottomRow.style.gridTemplateRows = 'minmax(0, 1fr)'; if (!bottomList.length) { grid.style.gridTemplateRows = 'minmax(0, 1fr)'; grid.style.gridTemplateAreas = `'main vbar side'`; } else { applyGridSize(); } focusedRow.replaceChildren(); sideRow.replaceChildren(); bottomRow.replaceChildren(); const ensureCard = (room, parent) => { if (!room) return null; let c = cardMap.get(room.id); const created = !c; if (!c) { buildCard(room); c = cardMap.get(room.id); } resetCardSizing(c.root); c.root.removeAttribute('data-grid-slot'); if (!c.root.querySelector('.room-drop-hint')) c.root.appendChild($('div', { class: 'room-drop-hint', 'aria-hidden': 'true' })); parent.appendChild(c.root); if (!c.video && (created || room.lastStatus === 'online')) service.ensure(room.id); renderCardState(room); c.root.classList.toggle('is-focus-main', room.id === focusedId); return c; }; const focusLabelSideCount = explicitSlots ? focusSecondaryShape(layoutSize()).side : focusSecondaryShape(Math.max(1, list.length)).side; const appendFocusSlot = (room, parent, slotIndex) => { const slotLabel = focusSlotLabel(slotIndex, layoutSize(), focusLabelSideCount); if (room) { const c = ensureCard(room, parent); if (c) { c.root.dataset.focusSlot = String(slotIndex); c.root.dataset.focusSlotLabel = slotLabel; c.root.classList.add('focus-slot-target'); let slotBadge = c.root.querySelector('.focus-slot-badge'); if (!slotBadge) { slotBadge = $('div', { class: 'focus-slot-badge' }); c.root.appendChild(slotBadge); } slotBadge.replaceChildren( $('span', { class: 'focus-slot-badge-label' }, slotLabel), $('span', { class: 'focus-slot-badge-drop' }, t('focusDropHere', slotLabel)), ); } return c; } const empty = $('div', { class: 'focus-empty-slot focus-slot-target', dataset: { focusSlot: String(slotIndex), focusSlotLabel: slotLabel }, title: t('focusDropHere', slotLabel), style: { minWidth: '0', minHeight: '0', border: '1px dashed var(--border)', borderRadius: '8px', display: 'grid', placeItems: 'center', color: 'var(--text-muted)', fontSize: '12px', opacity: '.7', }, }, [ $('span', { class: 'focus-empty-slot-label' }, slotLabel), $('span', { class: 'focus-empty-slot-state' }, t('focusRoomEmpty')), $('span', { class: 'focus-empty-slot-drop' }, t('focusDropHere', slotLabel)), $('div', { class: 'room-drop-hint', 'aria-hidden': 'true' }), ]); empty.addEventListener('dragover', (e) => { const focusId = focusDragRoomId || e.dataTransfer?.getData('text/roomgrid-focus-id'); if (!focusId || store.state.settings.viewMode !== 'focus') return; e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; setFocusDropTarget(empty); }); empty.addEventListener('dragleave', (e) => { if (!empty.contains(e.relatedTarget)) { empty.classList.remove('focus-drop-target'); setFocusDropCopy(empty, 'focusDropHere'); } }); empty.addEventListener('drop', (e) => { const focusId = focusDragRoomId || e.dataTransfer.getData('text/roomgrid-focus-id'); if (!focusId) return; e.preventDefault(); e.stopPropagation(); setFocusDropTarget(empty); assignFocusRoomToSlot(focusId, slotIndex); focusDragRoomId = ''; clearFocusDropIndicators(); }); parent.appendChild(empty); return null; }; if (explicitSlots || focusedRoom) { const c = appendFocusSlot(focusedRoom, focusedRow, 0); if (!c) { // An explicitly empty main slot is valid; the selected positions remain stable. requestAnimationFrame(applyFocusMainSizing); } if (c) { c.root.classList.add('is-focus-main'); applyFocusMainSizing(); } } else { [...focusedRow.children].forEach(el => { if (el.classList && el.classList.contains('cam-card')) sideRow.appendChild(el); }); } sideList.forEach((room, idx) => appendFocusSlot(room, sideRow, idx + 1)); bottomList.forEach((room, idx) => appendFocusSlot(room, bottomRow, sideList.length + idx + 1)); /* * Keep the explicit slot mapping intact: empty positions are represented by * placeholders, so later rooms never slide into a different user-selected slot. */ if (!explicitSlots && focusedRoom) { const c = cardMap.get(focusedRoom.id); if (c) c.root.classList.add('is-focus-main'); } const keep = new Set(list.map(r => r.id)); [focusedRow, sideRow, bottomRow].forEach(parent => { [...parent.children].forEach(el => { const id = el.dataset?.roomId; if (id && !keep.has(id)) sideRow.appendChild(el); }); }); if (document.body.classList.contains('focus-drag-active')) setFocusDropMode(true); requestAnimationFrame(applyFocusMainSizing); syncLayoutControls(); } // ---- 渲染调度:合并同一帧内的多次状态变化,减少批量添加、切页和状态刷新时的抖动 ---- let sidebarRenderRaf = 0; let gridRenderRaf = 0; let focusSizingRaf = 0; function scheduleSidebarRender() { if (isRoomDragActive()) { dragSidebarRenderDeferred = true; return; } if (sidebarIsHidden() && sidebar.style.display === 'none') return; if (sidebarPreview || sidebarPreviewAnchor || sidebarPreviewTimer || sidebarPreviewCloseTimer) { sidebarRenderDeferredForPreview = true; return; } if (sidebarRenderRaf) return; sidebarRenderRaf = requestAnimationFrame(() => { sidebarRenderRaf = 0; renderSidebar(); }); } function scheduleGridRender() { if (gridRenderRaf) return; gridRenderRaf = requestAnimationFrame(() => { gridRenderRaf = 0; renderGrid(); }); } function scheduleFocusSizing() { if (focusSizingRaf) return; focusSizingRaf = requestAnimationFrame(() => { focusSizingRaf = 0; applyFocusMainSizing(); }); } analytics.subscribe(() => { scheduleSidebarRender(); if (store.state.settings.sortBy === 'watchTime' || store.state.settings.sortBy === 'sessions' || store.state.settings.sortBy === 'lastWatched' || store.state.settings.filter?.onlyWatched || store.state.settings.filter?.neverWatched || store.state.settings.filter?.tagInclude || store.state.settings.filter?.tagExclude || store.state.settings.filter?.minWatchMs || store.state.settings.filter?.minSessions || store.state.settings.filter?.lastWatchedDays) { scheduleSidebarRender(); scheduleGridRender(); } }); // ---- Store 订阅 ---- store.subscribe((state, path) => { const isSettingsPath = path === 'settings' || (typeof path === 'string' && path.startsWith('settings:')); const settingKeys = isSettingsPath && typeof path === 'string' && path.includes(':') ? path.slice(path.indexOf(':') + 1).split(',').filter(Boolean) : []; const hasSetting = (...keys) => !settingKeys.length || keys.some(k => settingKeys.includes(k) || settingKeys.some(x => x.startsWith(k + '.'))); if (path === 'groups' || path === 'rooms' || path === 'all' || isSettingsPath) { if (path === 'all' || path === 'settings' || hasSetting('theme', 'accentColor', 'secondaryAccentColor', 'brandPreset', 'surfaceStyle', 'density', 'contentWidth', 'cardStyle', 'dashboardWidgetDensity', 'fontFamily', 'fontScale', 'controlScale', 'cornerRadius', 'cardGap', 'shadowStrength', 'surfaceOpacity', 'backgroundStyle', 'animations', 'sidebarWidth')) applySuiteAppearance(store); const fullRefresh = path === 'groups' || path === 'rooms' || path === 'all' || path === 'settings' || !settingKeys.length; if (fullRefresh || hasSetting('sidebarHoverPreview')) { const enabled = state.settings.sidebarHoverPreview !== false; sidebarHoverButton.setAttribute('aria-pressed', String(enabled)); if (!enabled && sidebar.contains(sidebarPreviewAnchor)) closeSidebarPreview(); if (path === 'settings:sidebarHoverPreview') return; } const needsSidebar = fullRefresh || hasSetting( 'activeGroup', 'sidebarCollapsed', 'pureMode', 'viewerMode', 'layoutSize', 'useCustomLayout', 'customLayoutRows', 'customLayoutCols', 'viewMode', 'focusedRoomId', 'focusRoomSlots', 'gridRoomSlots', 'filter', 'sortBy', 'searchQuery', 'showRecordingOnly', 'favoriteFirst', 'sidebarBrowser' ); const needsGrid = fullRefresh || hasSetting( 'activeGroup', 'filter', 'sortBy', 'searchQuery', 'layoutSize', 'useCustomLayout', 'customLayoutRows', 'customLayoutCols', 'pageIndex', 'viewMode', 'focusedRoomId', 'focusRoomSlots', 'gridRoomSlots', 'focusMainPct', 'focusMainHPct', 'focusAspect', 'focusThumbSize', 'showRecordingOnly', 'favoriteFirst' ); if (needsSidebar) scheduleSidebarRender(); if (needsGrid) scheduleGridRender(); sortSel.value = state.settings.sortBy; filterSel.value = filterModeFromState(); sizeSlider.value = String(state.settings.gridSize); if (document.activeElement !== searchInput) searchInput.value = state.settings.searchQuery || ''; focusScaleSlider.value = String(state.settings.focusMainPct || 62); focusAspectSel.value = state.settings.focusAspect || 'auto'; focusThumbSlider.value = String(state.settings.focusThumbSize || 150); syncViewSeg(); syncLayoutControls(); syncShellControls(); applyPureModeState(); if (hasSetting('maxStreamHeight', 'qualityBudgetHeight', 'qualityPrioritizeFocused', 'viewMode', 'focusedRoomId')) service.refreshQuality(); if (isSettingsPath && hasSetting('performanceMaxStreams')) { // A room held back by the old limit should start as soon as the // user raises it, without waiting for the next status poll. for (const [id, card] of cardMap) { if (!card.video && findRoomAny(id)?.lastStatus === 'online') service.ensure(id); } } if (hasSetting('audioRoutingMode', 'audioSoloRoomId', 'focusedRoomId', 'viewMode', 'volume')) applyAllAudioRouting(); if (hasSetting('audioMeterVisible', 'multicamMeterDisplayMode')) { applyAllAudioMeterVisibility(); syncAudioMeterControls(); } if (hasSetting('videoTransforms')) applyAllVideoTransforms(); if (needsGrid || hasSetting('toolbarCollapsed', 'sidebarCollapsed', 'viewerMode', 'pureMode')) scheduleFocusSizing(); } if (path && path.startsWith('room:')) { const id = path.slice(5); const r = state.rooms.find(x => x.id === id); if (r) renderCardState(r); if (r) scheduleWorkspaceReplacement(r); scheduleSidebarRender(); // 状态排序时,单卡状态变化也要重排 if (state.settings.activeGroup === ONLINE_GROUP_ID || ['status', 'viewerCount', 'lastOnline'].includes(state.settings.sortBy) || state.settings.filter?.hideOffline || state.settings.filter?.hidePrivate || state.settings.filter?.onlyOnline || state.settings.filter?.viewerMin || state.settings.filter?.viewerMax) scheduleGridRender(); } if (path === 'settings:activeGroup,pageIndex') analytics.recordEvent({ type: 'workspace', detail: state.settings.activeGroup || DEFAULT_GROUP_ID }); if (typeof path === 'string' && path.startsWith('settings:') && /filter|sortBy|searchQuery/u.test(path)) analytics.recordEvent({ type: 'filter', detail: path.slice(9, 220) }); }); // ---- EventBus 订阅 ---- EventBus.on('room:online', ({ id, hlsSource }) => { if (!findRoomAny(id)) { service.stop(id); return; } const video = attachVideoElement(id); if (!video) return; service.startHls(id, hlsSource); const r = findRoomAny(id); if (r) renderCardState(r); requestAnimationFrame(applyFocusMainSizing); }); EventBus.on('room:flash', (id) => { const c = cardMap.get(id); if (!c) return; c.root.classList.remove('flash'); void c.root.offsetWidth; // 强制重排重启动画 c.root.classList.add('flash'); setTimeout(() => c.root.classList.remove('flash'), 5000); }); const qualityRecordedAt = new Map(); const qualityRecordedConnection = new Map(); EventBus.on('room:metrics', metrics => { const card = cardMap.get(metrics?.id); if (card) renderStreamStartupState(metrics.id); const connection = String(metrics.connection || ''); const now = Date.now(); const last = qualityRecordedAt.get(metrics?.id) || 0; const important = ['stalled', 'stream-error', 'media-error'].includes(connection) && qualityRecordedConnection.get(metrics?.id) !== connection; qualityRecordedConnection.set(metrics?.id, connection); if (metrics?.id && (important || now - last >= 30000)) { qualityRecordedAt.set(metrics.id, now); analytics.recordQualitySample({ ...metrics, at: now, id: metrics.id, profile: store.state.settings.performanceProfile }).catch(() => {}); } }); // ---- 跨标签页实时同步 ---- // 当用户在主播页点击「加入」时,已打开的工作台立刻感知并新增卡片(无需刷新) window.addEventListener('storage', (e) => { if (e.key !== STORE_KEY) return; try { if (!e.newValue) { syncFromExternalState(defaultState()); return; } const ext = sanitizeState(JSON.parse(e.newValue)); if (!ext || !Array.isArray(ext.rooms)) return; syncFromExternalState(ext); } catch (_) {} }); function syncFromExternalState(ext) { const normalized = sanitizeState(ext && typeof ext === 'object' ? ext : defaultState()); const currentById = new Map(store.state.rooms.map(r => [r.id, r])); const extRooms = (Array.isArray(normalized.rooms) ? normalized.rooms : []).map(r => { const local = currentById.get(r.id); if (local && isStableRoomStatus(local.lastStatus) && isTransientRoomStatus(r.lastStatus)) { return { ...r, lastStatus: local.lastStatus, lastSeenOnline: Math.max(numeric(local.lastSeenOnline, 0), numeric(r.lastSeenOnline, 0)), privateLabel: local.lastStatus === 'private' ? (local.privateLabel || r.privateLabel) : r.privateLabel, }; } return r; }); const curIds = new Set(store.state.rooms.map(r => r.id)); const nextIds = new Set(extRooms.map(r => r.id)); const removedRoomIds = [...curIds].filter(id => !nextIds.has(id)); const newRoomIds = [...nextIds].filter(id => !curIds.has(id)); // 多窗口只同步房间 / 分组,不同步 viewMode / focusedRoomId / filter 等本窗口视图设置。 // 否则一个窗口切主屏会把另一个窗口的主屏也顶掉。 const localSettings = JSON.parse(JSON.stringify(store.state.settings || defaultState().settings)); const nextState = { ...normalized, rooms: extRooms, groups: Array.isArray(normalized.groups) && normalized.groups.length ? normalized.groups : store.state.groups, settings: localSettings, }; const groupIds = new Set(nextState.groups.map(g => g.id)); if (!groupIds.has(nextState.settings.activeGroup)) nextState.settings.activeGroup = DEFAULT_GROUP_ID; if (nextState.settings.focusedRoomId && !nextIds.has(nextState.settings.focusedRoomId)) { const preferred = extRooms.find(r => r.lastStatus === 'online') || extRooms[0]; nextState.settings.focusedRoomId = preferred?.id || null; } // 外部标签页同步只替换内存状态,不回写 localStorage,避免多个工作台互相触发 storage ping-pong。 store.replaceState(nextState, 'all'); // service 启停(replaceState 之后,避免 service 提前 fire 状态时找不到对应数据) for (const id of removedRoomIds) service.stop(id); for (const id of newRoomIds) service.start(id); for (const id of nextIds) { if (!service.has(id)) service.start(id); } } // ---- 全局拖动结束 ---- grid.addEventListener('dragover', (e) => { if (focusDragRoomId && store.state.settings.viewMode === 'grid') { const target = focusDropTargetAtPoint(grid, e.clientX, e.clientY); if (handleSidebarSlotDrop(e, target)) return; } if (cardDragRoomId) e.preventDefault(); }); grid.addEventListener('drop', (e) => { if (focusDragRoomId && store.state.settings.viewMode === 'grid') handleSidebarSlotDrop(e, focusDropTargetAtPoint(grid, e.clientX, e.clientY), true); }); grid.addEventListener('dragleave', (e) => { if (!grid.contains(e.relatedTarget)) setFocusDropTarget(null); }); commandPalette = createCommandPalette(store, () => [ { id: 'refresh', icon: '↻', title: tx('刷新所有摄像头', 'Refresh all cameras', 'Alle Cams aktualisieren'), detail: tx('重新连接当前模型', 'Reconnect current models', 'Aktuelle Models neu verbinden'), shortcut: shortcutLabel(store.state.settings.shortcuts?.refreshAll), run: () => service.refreshAll() }, { id: 'grid', icon: '▦', title: tx('切换到网格视图', 'Switch to grid view', 'Zur Rasteransicht wechseln'), run: () => store.patchSettings({ viewMode: 'grid' }) }, { id: 'focus', icon: '▣', title: tx('切换到焦点视图', 'Switch to focus view', 'Zur Fokusansicht wechseln'), run: () => setViewMode('focus') }, { id: 'workspaces', icon: '◫', title: tx('管理工作区', 'Manage workspaces', 'Arbeitsbereiche verwalten'), detail: tx('创建、复制、归档和设备布局', 'Create, duplicate, archive and device layouts', 'Erstellen, duplizieren, archivieren und Gerätelayouts'), run: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'workspaces') }, { id: 'library', icon: '★', title: tx('打开模型库', 'Open model library', 'Modellbibliothek öffnen'), detail: tx('备注、标签、评分和批量操作', 'Notes, tags, ratings and bulk actions', 'Notizen, Tags, Bewertungen und Massenaktionen'), run: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'library') }, { id: 'data', icon: '⇅', title: tx('备份和导入', 'Backup and import', 'Sicherung und Import'), run: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'data') }, { id: 'appearance', icon: '◐', title: tx('设计选项', 'Design options', 'Designoptionen'), run: () => openControlCenter(store, analytics, notificationCenter, windowCoordinator, 'appearance') }, { id: 'discover', icon: '⌁', title: tx('打开发现', 'Open Discover', 'Entdecken öffnen'), run: () => discoverBtn.click() }, { id: 'privacy', icon: '×', title: tx('立即隐藏', 'Hide immediately', 'Sofort ausblenden'), detail: 'Ctrl+Shift+X', run: () => PrivacyShield.activate() }, ], { onModel: id => { store.patchSettings({ activeGroup: LIBRARY_GROUP_ID, searchQuery: id, pageIndex: 0, focusedRoomId: id }); } }); // ---- 快捷键 ---- function shortcutMatches(e, action) { const shortcuts = sanitizeShortcuts(store.state.settings.shortcuts, defaultShortcuts()); return shortcutFromEvent(e) === shortcuts[action]; } document.addEventListener('keydown', (e) => { const key = String(e.key || '').toLowerCase(); if (e.key === 'Escape') { closeSidebarPreview(); closeTransientUi(); } if (shortcutMatches(e, 'pureMode') || (e.altKey && key === 'c')) { e.preventDefault(); togglePureMode(); return; } if (shortcutMatches(e, 'viewerMode')) { e.preventDefault(); toggleViewerMode(); return; } if (shortcutMatches(e, 'focusThumbs')) { e.preventDefault(); toggleFocusThumbs(); return; } if (e.key === 'Escape' && store.state.settings.pureMode) { e.preventDefault(); setPureMode(false); return; } if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (shortcutMatches(e, 'refreshAll')) { e.preventDefault(); service.refreshAll(); return; } if (shortcutMatches(e, 'focusAdd')) { e.preventDefault(); tbInput.focus(); return; } if (ROOMGRID_CAPTURE_TOOLS_ENABLED && shortcutMatches(e, 'recordingCenter')) { e.preventDefault(); openRecordingCenter(); return; } if (ROOMGRID_CAPTURE_TOOLS_ENABLED && shortcutMatches(e, 'recordPage')) { e.preventDefault(); recordCurrentPage(); return; } if (e.key === 'Escape' && document.fullscreenElement) document.exitFullscreen(); if (store.state.settings.viewMode === 'focus') { if (e.code === 'Space') { e.preventDefault(); const id = store.state.settings.focusedRoomId; if (id) { service.togglePause(id); requestAnimationFrame(() => updateCardButtons(id)); } } if (e.key === 'ArrowRight' || e.key === ']') { e.preventDefault(); focusStep(1); } if (e.key === 'ArrowLeft' || e.key === '[') { e.preventDefault(); focusStep(-1); } } // 视图切换:g = grid, f = focus if (shortcutMatches(e, 'gridView')) { e.preventDefault(); store.patchSettings({ viewMode: 'grid' }); return; } if (shortcutMatches(e, 'focusView')) { e.preventDefault(); if (!store.state.settings.focusedRoomId) { const vr = visibleRooms(); const first = vr.find(r => r.lastStatus === 'online') || vr[0]; if (first) store.patchSettings({ viewMode: 'focus', focusedRoomId: first.id, focusThumbsCollapsed: false }); else store.patchSettings({ viewMode: 'focus', focusThumbsCollapsed: false }); } else { store.patchSettings({ viewMode: 'focus', focusThumbsCollapsed: false }); } return; } }); // ---- 首次渲染 + 全量启动 service ---- // service 独立于 UI 运行:所有 store 中的房间都参与轮询, // 这样切到其它分组时,被隐藏房间的上线事件也能被检测到并触发桌面通知。 loadSharedWorkspaceFromHash(); loadSavedTemporarySources(); if (store.state.settings.followedSyncEnabled !== false) { const lastFollowedSync = Number(store.state.settings.followedSyncLastAt || 0); if (!lastFollowedSync || Date.now() - lastFollowedSync >= FOLLOWED_SYNC_INTERVAL_MS) syncFollowedRooms(); scheduleFollowedSync(); } renderSidebar(); renderGrid(); applyPureModeState(); const initialVisibleRoomIds = new Set([...cardMap.keys()]); if (initialVisibleRoomIds.size) { const resolvedRoomIds = new Set(); const label = () => `${tx('正在检查 MultiCam 房间', 'Checking MultiCam rooms', 'MultiCam-Räume werden geprüft')} · ${resolvedRoomIds.size}/${initialVisibleRoomIds.size}`; const progressTask = notificationCenter.beginProgress?.(label(), { total: initialVisibleRoomIds.size, done: 0, delayMs: 280, timeoutMs: 300000 }); const unsubscribeStartupProgress = EventBus.on('room:checked', result => { const id = normalizeUsername(result?.id); if (!initialVisibleRoomIds.has(id) || resolvedRoomIds.has(id)) return; resolvedRoomIds.add(id); progressTask?.update({ label: label(), total: initialVisibleRoomIds.size, done: resolvedRoomIds.size }); if (resolvedRoomIds.size >= initialVisibleRoomIds.size) { unsubscribeStartupProgress(); progressTask?.finish(); } }); window.addEventListener('pagehide', () => { unsubscribeStartupProgress(); progressTask?.finish(); }, { once: true }); } const gradualDelay = store.state.settings.gradualStart === false ? 0 : (store.state.settings.performanceProfile === 'eco' ? 550 : 220); // renderGrid has already started visible rooms together. Avoid filling the // connection with off-screen status probes before their first video frames. // Every background room is still checked, including when a stream fails or // playback never starts; the timeout bounds the notification delay. const startupBackgroundIds = store.state.rooms.map(room => room.id).filter(id => !initialVisibleRoomIds.has(id)); const startupPending = new Set(initialVisibleRoomIds); let releaseStartupTimer = 0; let unsubscribeStartupChecks = () => {}; let unsubscribeStartupErrors = () => {}; let backgroundStarted = false; let nextBackgroundRoom = 0; let backgroundTimer = 0; const startDeferredBackground = () => { if (watchOnlyMode || backgroundTimer || !backgroundStarted) return; const startNextRoom = () => { backgroundTimer = 0; if (watchOnlyMode) return; const end = gradualDelay ? Math.min(startupBackgroundIds.length, nextBackgroundRoom + 1) : startupBackgroundIds.length; for (; nextBackgroundRoom < end; nextBackgroundRoom += 1) { const id = startupBackgroundIds[nextBackgroundRoom]; if (store.state.rooms.some(room => room.id === id)) service.start(id); } if (nextBackgroundRoom < startupBackgroundIds.length) backgroundTimer = setTimeout(startNextRoom, gradualDelay); }; if (nextBackgroundRoom < startupBackgroundIds.length) backgroundTimer = setTimeout(startNextRoom, 0); }; const watchOnlyChanged = () => { if (watchOnlyMode) { if (!sidebar.contains(sidebarPreviewAnchor)) closeSidebarPreview(); } else startDeferredBackground(); }; window.addEventListener('camclient:watch-only', watchOnlyChanged); const releaseStartupBackground = () => { if (backgroundStarted) return; backgroundStarted = true; clearTimeout(releaseStartupTimer); unsubscribeStartupChecks(); unsubscribeStartupErrors(); grid.removeEventListener('loadeddata', onStartupLoadedData, true); startDeferredBackground(); }; const settleStartupRoom = id => { if (!startupPending.delete(normalizeUsername(id))) return; if (!startupPending.size) releaseStartupBackground(); }; const onStartupLoadedData = event => { const id = event.target?.dataset?.multicamRoomId; if (id) settleStartupRoom(id); }; if (startupPending.size && startupBackgroundIds.length) { grid.addEventListener('loadeddata', onStartupLoadedData, true); unsubscribeStartupChecks = EventBus.on('room:checked', result => { if (['offline', 'private', 'error'].includes(result?.status)) settleStartupRoom(result.id); }); unsubscribeStartupErrors = EventBus.on('room:metrics', result => { if (['request-error', 'stream-error', 'media-error', 'play-blocked'].includes(result?.connection)) settleStartupRoom(result.id); }); releaseStartupTimer = setTimeout(releaseStartupBackground, 8000); for (const id of startupPending) if (cardMap.get(id)?.video?.readyState >= 2) settleStartupRoom(id); } else releaseStartupBackground(); if (ROOMGRID_CAPTURE_TOOLS_ENABLED) setTimeout(checkRecordingIntentRecovery, 1200); // 兜底:定期检查 sessions 与 rooms 是否一致(防止某些边缘 case 数据漂移) const backgroundRoomAudit = setInterval(() => { if (watchOnlyMode) return; for (const r of store.state.rooms) { if (!service.has(r.id)) service.start(r.id); } }, 60000); window.addEventListener('pagehide', () => { window.removeEventListener('camclient:watch-only', watchOnlyChanged); clearInterval(backgroundRoomAudit); clearTimeout(backgroundTimer); clearTimeout(releaseStartupTimer); unsubscribeStartupChecks(); unsubscribeStartupErrors(); grid.removeEventListener('loadeddata', onStartupLoadedData, true); }, { once: true }); // 调试入口默认不暴露到页面;需要时先在控制台设置 localStorage.ryujo_multicam_debug = '1' 后刷新。 try { if (localStorage.getItem('ryujo_multicam_debug') === '1') window.__multicam = { store, service, EventBus }; else if (window.__multicam) delete window.__multicam; } catch (_) {} } // Start only after every source fragment has initialized its constants. // Function declarations are hoisted, but Discover's bounded configuration is not. (async () => { try { if (window.self !== window.top && new URLSearchParams(location.search).get('roomgrid_chat_embed') === '1') { document.documentElement.dataset.roomgridChatEmbed = '1'; return; } } catch (_) {} try { if (await restoreRoomGridLanguageTransfer()) LANG = detectLang(); } catch (error) { try { console.warn('[RoomGrid] language transfer restore failed', error); } catch (_) {} } Diagnostics.init(); const directRoomModel = roomGridNativeRoomId(location.href); if (directRoomModel) { stopAllPageMedia(); location.replace(roomGridSingleCamUrl(directRoomModel, { source: 'direct-room' })); return; } RoomGridNavigation.install(); SuiteRuntime.start(); RoomGridRouteHealth.init(); RoomGridPersistentVault.start(); window.addEventListener('pagehide', () => { RoomGridRouteHealth.stop(); RoomGridNavigation.stop(); SuiteRuntime.stop(); }, { once: true }); })(); })();