diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 623715d..bb6ce3c 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -41,15 +41,12 @@ body: validations: required: true - - type: dropdown - id: browser + - type: input + id: wayfern_version attributes: - label: Which browser is affected? - options: - - Wayfern - - Camoufox - - Both - - Not browser-specific + label: Wayfern version + description: Settings → About, or the version shown when creating a profile. Use "unknown" if not browser-specific. + placeholder: e.g. 138.0.7204.50 or unknown validations: required: true diff --git a/.github/workflows/issue-validation.yml b/.github/workflows/issue-validation.yml index 88e358c..cb59df4 100644 --- a/.github/workflows/issue-validation.yml +++ b/.github/workflows/issue-validation.yml @@ -68,7 +68,7 @@ jobs: const get = (k) => fields[k] || ''; fs.writeFileSync('/tmp/issue-os.txt', get('Operating System')); fs.writeFileSync('/tmp/issue-version.txt', get('Donut Browser version')); - fs.writeFileSync('/tmp/issue-browser.txt', get('Which browser is affected?')); + fs.writeFileSync('/tmp/issue-wayfern-version.txt', get('Wayfern version')); fs.writeFileSync('/tmp/issue-repro.txt', get('Steps to reproduce')); fs.writeFileSync('/tmp/issue-logs.txt', get('Error logs or screenshots')); fs.writeFileSync('/tmp/issue-what.txt', get('What happened?') || get('What do you want?')); @@ -102,17 +102,7 @@ jobs: its API, MCP server, and the bundled `donut-sync` self-hosted server. - **Wayfern** — a Chromium fork maintained by zhom (the same maintainer). Wayfern bugs are in-scope here unless they are obviously upstream Chromium issues. - - **Camoufox** — a Firefox fork by daijro, used by Donut but maintained in a - separate repository. Bugs about Camoufox's *internal* behavior are outside - the scope of this project. - - Bugs about Camoufox's *internal* behavior (page rendering, JS engine, - dropdowns, form widgets, fingerprinting *as Camoufox implements it*, - checkbox/radio quirks) are out of scope here. Ask the user to first - search https://github.com/daijro/camoufox/issues for a matching report, - and if they don't find one, to open it there themselves. - - Bugs about how Donut *launches, configures, or downloads* Camoufox are - in-scope here. - - **Forks of Wayfern or Camoufox** (e.g. CloverLabsAI, VulpineOS) are NOT + - **Forks of Wayfern** (e.g. CloverLabsAI, VulpineOS) are NOT supported. Feature requests asking for them are out of scope. # PAID vs FREE FEATURES @@ -121,7 +111,7 @@ jobs: ## Free (no account required) - Unlimited local profiles - - Chromium (Wayfern) and Firefox (Camoufox) browser engines + - Chromium (Wayfern) anti-detect browser engine - Proxy support (HTTP/SOCKS5) - VPN support (WireGuard) - Profile Management API & MCP (list / create / launch / kill / config) @@ -147,13 +137,8 @@ jobs: differently ("worked in 0.21", "went from 2 to 8 false positives"). Do NOT dismiss as "known issue" / "expected" / "false positive in Tauri apps". Ask which exact version was the last working one and what changed. - - **Out-of-scope (upstream Camoufox)**: report is about Camoufox's own - behavior. Tell the user it's outside the scope of this project and ask - them to search the Camoufox repo and, if no matching issue exists, file - one there. Do NOT say the maintainer doesn't contribute / can't fix it - — keep it strictly about project scope. Do not collect logs. - **Fork-support request**: asks the maintainer to support an alternative - Wayfern/Camoufox fork. Acknowledge in one neutral sentence — do NOT call it + Wayfern fork. Acknowledge in one neutral sentence — do NOT call it "clear", "reasonable", "well-thought-out", etc. - **AI-generated / template-violating report**: report doesn't follow the template, may cite "official documentation" via context7, deepwiki, or any @@ -214,7 +199,7 @@ jobs: Return ONLY valid JSON. No preamble, no code fences. Schema: { "language": "en" or ISO 639-1 code, - "classification": one of ["bug-in-scope", "bug-upstream-camoufox", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"], + "classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"], "operating_system": "macos" | "windows" | "linux" | "unknown", "is_paid_feature": true | false, "user_followed_template": true | false, @@ -225,13 +210,12 @@ jobs: } Classification guidance: - - "bug-upstream-camoufox": Camoufox-internal behavior (rendering, dropdowns, JS, fingerprint impl). NOT how Donut launches it. - "bug-template-violation": missing or filled-in nonsense for required template fields. - "ai-generated-junk": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has the polished AI-spam shape (long, structured, fabricated certainty). - "fork-request": asks for support of CloverLabsAI/VulpineOS/etc. forks. - "regression": user names a prior version that worked. - File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is upstream-Camoufox, fork-request, or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms. + File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is a fork-request or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms. TRIAGE_TAIL } > /tmp/triage-system.txt wc -c /tmp/triage-system.txt @@ -292,7 +276,7 @@ jobs: - name: Read files chosen by triage run: | : > /tmp/file-context.txt - # files_to_read may be empty (e.g. upstream Camoufox) — that's fine. + # files_to_read may be empty (e.g. fork-request or junk) — that's fine. jq -r '.files_to_read[]? // empty' /tmp/triage.json | while IFS= read -r filepath; do filepath=$(echo "$filepath" | xargs) [ -z "$filepath" ] && continue @@ -329,7 +313,7 @@ jobs: ## Output shape - One sentence acknowledging the report. - - Then **Missing information** — only if there is anything actually missing. Skip this section if the user already provided OS, version, browser, repro steps, and any logs the situation calls for. + - Then **Missing information** — only if there is anything actually missing. Skip this section if the user already provided OS, Donut Browser version, Wayfern version, repro steps, and any logs the situation calls for. - Maximum 15 lines. - No labels, no `Label:` line, no markdown headings other than `**Missing information**`. - No closing pleasantries ("please let me know", "happy to help", etc.). @@ -347,8 +331,7 @@ jobs: The triage classification (`triage.classification`) determines the response shape: - `bug-in-scope`: ask for what is missing using the user's reported OS log path. Be concrete about how to obtain logs. - - `bug-upstream-camoufox`: redirect ONLY. One sentence acknowledging, then say this is outside the scope of this project — ask the user to first search https://github.com/daijro/camoufox/issues for a matching report and, if none exists, to open one there themselves. Do NOT phrase it as "the maintainer does not contribute" or anything personal — keep it strictly about scope. Do NOT ask for Donut logs. Stop after that. - - `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Which browser, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com. + - `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com. - `feature-request`: one neutral sentence acknowledging, then ask only what is genuinely needed (concrete use case, whether a workaround would suffice). Do NOT validate. - `fork-request`: one neutral sentence acknowledging the request. Note that this would substantially increase support burden and the maintainer evaluates such requests on a case-by-case basis. Ask whether the alternative fork supports all platforms the user uses (macOS / Windows / Linux). No "clear enhancement" language. - `regression`: do NOT call known/expected. Ask which exact previous version was the last working one, what changed in the user's environment between then and now, and the specific delta in symptoms. diff --git a/.vscode/settings.json b/.vscode/settings.json index 30d7888..26cb559 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,7 +21,6 @@ "Buildx", "busctl", "CAMOU", - "camoufox", "catppuccin", "cdylib", "certifi", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55d814a..9b91087 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,7 +92,7 @@ codeql database analyze /tmp/codeql-rust --format=sarifv2.1.0 --output=/tmp/rust - **Backend**: Tauri (Rust), `src-tauri/src/` - **Proxy Worker**: Detached process for proxy tunneling, `src-tauri/src/bin/proxy_server.rs` - **Sync**: Cloud sync via S3-compatible storage, `src-tauri/src/sync/`, `donut-sync/` -- **Browsers**: Camoufox (Firefox-based) and Wayfern (Chromium-based) +- **Browsers**: Wayfern (Chromium-based anti-detect) ## Getting Help diff --git a/_typos.toml b/_typos.toml index ec93ca5..61497eb 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,7 +1,6 @@ [files] extend-exclude = [ - "src-tauri/src/camoufox/data/*.json", - "src-tauri/src/camoufox/data/*.xml", + "src-tauri/src/territory_info.xml", "src/i18n/locales/*.json", # Auto-generated from commit subjects by release.yml; typos here originate # in commit messages, which are immutable, so don't spell-check it. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b6bef9b..cf907fe 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -194,9 +194,6 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arboard" @@ -641,7 +638,7 @@ dependencies = [ "arrayvec", "cc", "cfg-if", - "constant_time_eq 0.4.2", + "constant_time_eq", "cpufeatures 0.3.0", ] @@ -1223,12 +1220,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "constant_time_eq" version = "0.4.2" @@ -1561,12 +1552,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "defmt" version = "0.3.100" @@ -1627,17 +1612,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -1822,11 +1796,10 @@ dependencies = [ "objc2", "objc2-app-kit", "once_cell", - "playwright", "quick-xml 0.40.1", "rand 0.10.1", "regex-lite", - "reqwest 0.13.4", + "reqwest", "resvg", "ring", "rusqlite", @@ -1869,7 +1842,7 @@ dependencies = [ "windows 0.62.2", "winreg 0.56.0", "wiremock", - "zip 8.6.0", + "zip", ] [[package]] @@ -2584,11 +2557,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -3704,17 +3675,6 @@ dependencies = [ "crc", ] -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "lzxd" version = "0.2.6" @@ -4523,16 +4483,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -4680,29 +4630,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "playwright" -version = "0.0.23" -source = "git+https://github.com/zhom/playwright-rust?branch=master#0482f839aa24507268d9e3ee47c2be859eeef4f2" -dependencies = [ - "base64 0.22.1", - "chrono", - "dirs", - "futures", - "itertools", - "log", - "paste", - "reqwest 0.12.28", - "serde", - "serde_json", - "serde_with", - "strong", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "zip 2.4.2", -] - [[package]] name = "plist" version = "1.9.0" @@ -5259,48 +5186,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tower", - "tower-http 0.6.11", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest" version = "0.13.4" @@ -6315,15 +6200,6 @@ dependencies = [ "quote", ] -[[package]] -name = "strong" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe0fc7652d95bcd84f61cd036181b395f329ef45b25169b69a42f72cb6975f" -dependencies = [ - "serde", -] - [[package]] name = "strsim" version = "0.11.1" @@ -6562,7 +6438,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.4", + "reqwest", "serde", "serde_json", "serde_repr", @@ -7166,18 +7042,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - [[package]] name = "tokio-tfo" version = "0.4.3" @@ -8858,15 +8722,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "y4m" version = "0.8.0" @@ -9051,36 +8906,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "aes 0.8.4", - "arbitrary", - "bzip2", - "constant_time_eq 0.3.1", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "getrandom 0.3.4", - "hmac", - "indexmap 2.14.0", - "lzma-rs", - "memchr", - "pbkdf2", - "sha1", - "thiserror 2.0.18", - "time", - "xz2", - "zeroize", - "zopfli", - "zstd", -] - [[package]] name = "zip" version = "8.6.0" @@ -9100,46 +8925,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zune-core" version = "0.5.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c6e10cf..03acf2f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -91,8 +91,6 @@ http-body-util = "0.1" clap = { version = "4", features = ["derive"] } async-socks5 = "0.6" -# Camoufox/Playwright integration -playwright = { git = "https://github.com/zhom/playwright-rust", branch = "master" } # Wayfern CDP integration tokio-tungstenite = { version = "0.29", features = ["native-tls"] } diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index c80ee3c..33ff636 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -1,145 +1,49 @@ use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::browser::GithubRelease; - #[derive(Debug, Clone, PartialEq, Eq)] pub struct VersionComponent { pub major: u32, pub minor: u32, pub patch: u32, pub build: u32, - pub pre_release: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PreRelease { - pub kind: PreReleaseKind, - pub number: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum PreReleaseKind { - Alpha, - Beta, - RC, - Dev, - Pre, } impl VersionComponent { pub fn parse(version: &str) -> Self { let version = version.trim(); - // Normalize common tag prefixes like 'v1.2.3' -> '1.2.3' let version = if version.starts_with('v') || version.starts_with('V') { &version[1..] } else { version }; - // Handle special case for Zen Browser twilight releases - if version.to_lowercase() == "twilight" { - // Pure twilight release without base version - return VersionComponent { - major: 999, // High major version to indicate it's a rolling release - minor: 0, - patch: 0, - build: 0, - pre_release: Some(PreRelease { - kind: PreReleaseKind::Alpha, - number: Some(999), // High number to indicate it's a rolling release - }), - }; - } + let numeric_part = Self::numeric_prefix(version); - // Split version into numeric and pre-release parts - let (numeric_part, pre_release_part) = Self::split_version(version); - - // Parse numeric parts (major.minor.patch) let parts: Vec = numeric_part .split('.') .filter_map(|part| part.parse().ok()) .collect(); - let major = parts.first().copied().unwrap_or(0); - let minor = parts.get(1).copied().unwrap_or(0); - let patch = parts.get(2).copied().unwrap_or(0); - let build = parts.get(3).copied().unwrap_or(0); - - // Parse pre-release part - let pre_release = pre_release_part - .as_deref() - .and_then(Self::parse_pre_release); - VersionComponent { - major, - minor, - patch, - build, - pre_release, + major: parts.first().copied().unwrap_or(0), + minor: parts.get(1).copied().unwrap_or(0), + patch: parts.get(2).copied().unwrap_or(0), + build: parts.get(3).copied().unwrap_or(0), } } - fn split_version(version: &str) -> (String, Option) { + fn numeric_prefix(version: &str) -> String { let version = version.to_lowercase(); - - // Look for pre-release indicators for (i, ch) in version.char_indices() { if ch.is_alphabetic() && i > 0 { - // Check if this is a pre-release indicator - let remaining = &version[i..]; - if remaining.starts_with('a') - || remaining.starts_with('b') - || remaining.starts_with("alpha") - || remaining.starts_with("beta") - || remaining.starts_with("rc") - || remaining.starts_with("dev") - || remaining.starts_with("pre") - { - return (version[..i].to_string(), Some(remaining.to_string())); - } + return version[..i].to_string(); } } - - (version, None) - } - - fn parse_pre_release(pre_release: &str) -> Option { - let pre_release = pre_release.trim().to_lowercase(); - - if pre_release.is_empty() { - return None; - } - - // Extract kind and number - let (kind, number) = if let Some(stripped) = pre_release.strip_prefix("alpha") { - (PreReleaseKind::Alpha, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix("beta") { - (PreReleaseKind::Beta, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix("rc") { - (PreReleaseKind::RC, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix("dev") { - (PreReleaseKind::Dev, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix("pre") { - (PreReleaseKind::Pre, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix('a') { - (PreReleaseKind::Alpha, Self::extract_number(stripped)) - } else if let Some(stripped) = pre_release.strip_prefix('b') { - (PreReleaseKind::Beta, Self::extract_number(stripped)) - } else { - return None; - }; - - Some(PreRelease { kind, number }) - } - - fn extract_number(s: &str) -> Option { - let numeric_part: String = s.chars().filter(|c| c.is_ascii_digit()).collect(); - numeric_part.parse().ok() + version } } @@ -151,90 +55,23 @@ impl PartialOrd for VersionComponent { impl Ord for VersionComponent { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - use std::cmp::Ordering; - - // Check for twilight versions - let self_is_twilight = self - .pre_release - .as_ref() - .map(|pr| pr.kind == PreReleaseKind::Alpha && pr.number == Some(999)) - .unwrap_or(false); - let other_is_twilight = other - .pre_release - .as_ref() - .map(|pr| pr.kind == PreReleaseKind::Alpha && pr.number == Some(999)) - .unwrap_or(false); - - // If one is twilight and the other isn't, twilight always has priority - if self_is_twilight && !other_is_twilight { - return Ordering::Greater; // twilight > non-twilight - } - if !self_is_twilight && other_is_twilight { - return Ordering::Less; // non-twilight < twilight - } - - // Both are twilight or both are not twilight - use normal comparison - match (self_is_twilight, other_is_twilight) { - (true, true) => { - // Both are twilight, compare by base version - return (self.major, self.minor, self.patch, self.build).cmp(&( - other.major, - other.minor, - other.patch, - other.build, - )); - } - (false, false) => { - // Neither is twilight, continue with normal comparison - } - _ => unreachable!(), // Already handled above - } - - // Compare major.minor.patch.build first - match (self.major, self.minor, self.patch, self.build).cmp(&( + (self.major, self.minor, self.patch, self.build).cmp(&( other.major, other.minor, other.patch, other.build, - )) { - Ordering::Equal => { - // If numeric parts are equal, compare pre-release - match (&self.pre_release, &other.pre_release) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Greater, // Stable > pre-release - (Some(_), None) => Ordering::Less, // Pre-release < stable - (Some(a), Some(b)) => { - // Compare pre-release kinds first - match a.kind.cmp(&b.kind) { - Ordering::Equal => { - // Same kind, compare numbers - match (&a.number, &b.number) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - (Some(a_num), Some(b_num)) => a_num.cmp(b_num), - } - } - other => other, - } - } - } - } - other => other, - } + )) } } -// Helper function to sort versions properly pub fn sort_versions(versions: &mut [String]) { versions.sort_by(|a, b| { let version_a = VersionComponent::parse(a); let version_b = VersionComponent::parse(b); - version_b.cmp(&version_a) // Descending order (newest first) + version_b.cmp(&version_a) }); } -// Helper function to compare two versions pub fn compare_versions(version1: &str, version2: &str) -> std::cmp::Ordering { let version_a = VersionComponent::parse(version1); let version_b = VersionComponent::parse(version2); @@ -242,116 +79,22 @@ pub fn compare_versions(version1: &str, version2: &str) -> std::cmp::Ordering { } pub fn is_version_newer(version1: &str, version2: &str) -> bool { - // Use the proper VersionComponent comparison from api_client.rs let version_a = VersionComponent::parse(version1); let version_b = VersionComponent::parse(version2); version_a > version_b } -// Helper function to sort GitHub releases -pub fn sort_github_releases(releases: &mut [GithubRelease]) { - releases.sort_by(|a, b| { - // Normalize tags like "v1.81.9" -> "1.81.9" for correct ordering - let tag_a = a.tag_name.trim_start_matches('v'); - let tag_b = b.tag_name.trim_start_matches('v'); - let version_a = VersionComponent::parse(tag_a); - let version_b = VersionComponent::parse(tag_b); - version_b.cmp(&version_a) // Descending order (newest first) - }); -} - -pub fn is_nightly_version(version: &str) -> bool { - let version_comp = VersionComponent::parse(version); - version_comp.pre_release.is_some() -} - -/// Centralized function to determine if a browser version/release is nightly/prerelease -/// This is the single source of truth for nightly detection across the entire codebase -pub fn is_browser_version_nightly( - browser: &str, - version: &str, - release_name: Option<&str>, -) -> bool { - match browser { - "zen" => { - // For Zen Browser, only "twilight" is considered nightly - version.to_lowercase() == "twilight" - } - "brave" => { - // For Brave Browser, only releases whose name starts with "Release" (case-insensitive) are stable. - if let Some(name) = release_name { - let normalized = name.trim_start().to_ascii_lowercase(); - return !normalized.starts_with("release"); - } - - // Fallback: try cached GitHub releases - if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") { - if let Some(found) = releases.iter().find(|r| r.tag_name == version) { - let normalized = found.name.trim_start().to_ascii_lowercase(); - return !normalized.starts_with("release"); - } - } - - // Last resort: when no name available, treat as nightly (non-Release) - true - } - "firefox-developer" => { - // For Firefox Developer Edition, always treat as nightly/prerelease - // This ensures consistent behavior regardless of cache state or API response parsing - true - } - "firefox" => { - // For Firefox, use the category from the API response to determine stability - // This will be handled in the API parsing, so this fallback is for cached versions - is_nightly_version(version) - } - "chromium" => { - // Chromium builds are generally stable snapshots - false - } - "camoufox" => { - // For Camoufox, beta versions are actually the stable releases - false - } - "wayfern" => { - // For Wayfern, all releases from version.json are stable - false - } - _ => { - // Default fallback - is_nightly_version(version) - } - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct FirefoxRelease { - pub build_number: u32, - pub category: String, - pub date: String, - pub description: Option, - pub is_security_driven: bool, - pub product: String, - pub version: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FirefoxApiResponse { - pub releases: HashMap, -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrowserRelease { pub version: String, pub date: String, - pub is_prerelease: bool, } /// Wayfern version info from https://donutbrowser.com/wayfern.json #[derive(Debug, Serialize, Deserialize, Clone)] pub struct WayfernVersionInfo { pub version: String, - pub downloads: HashMap>, + pub downloads: std::collections::HashMap>, } #[derive(Debug, Serialize, Deserialize)] @@ -360,12 +103,6 @@ struct CachedVersionData { timestamp: u64, } -#[derive(Debug, Serialize, Deserialize)] -struct CachedGithubData { - releases: Vec, - timestamp: u64, -} - #[derive(Debug, Serialize, Deserialize)] struct CachedWayfernData { version_info: WayfernVersionInfo, @@ -374,10 +111,6 @@ struct CachedWayfernData { pub struct ApiClient { client: Client, - firefox_api_base: String, - firefox_dev_api_base: String, - github_api_base: String, - chromium_api_base: String, } impl ApiClient { @@ -387,95 +120,13 @@ impl ApiClient { .build() .unwrap_or_else(|_| Client::new()); - Self { - client, - firefox_api_base: "https://product-details.mozilla.org/1.0".to_string(), - firefox_dev_api_base: "https://product-details.mozilla.org/1.0".to_string(), - github_api_base: "https://api.github.com".to_string(), - chromium_api_base: "https://commondatastorage.googleapis.com/chromium-browser-snapshots" - .to_string(), - } - } - - async fn fetch_github_releases_multiple_pages( - &self, - base_releases_url: &str, - ) -> Result, Box> { - let mut all_releases: Vec = Vec::new(); - - // For now, only fetch 1 page - for page in 1..=1 { - let url = format!("{base_releases_url}?per_page=100&page={page}"); - let response = self - .client - .get(&url) - .header( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", - ) - .send() - .await?; - - if !response.status().is_success() { - // If the first page fails, propagate error; otherwise stop pagination - if page == 1 { - return Err( - format!( - "GitHub API returned status for page {}: {}", - page, - response.status() - ) - .into(), - ); - } else { - break; - } - } - - let text = response.text().await?; - let mut page_releases: Vec = serde_json::from_str(&text).map_err(|e| { - log::error!("Failed to parse GitHub API response (page {page}): {e}"); - log::error!( - "Response text (first 500 chars): {}", - if text.len() > 500 { - &text[..500] - } else { - &text - } - ); - format!("Failed to parse GitHub API response: {e}") - })?; - - if page_releases.is_empty() { - break; - } - - all_releases.append(&mut page_releases); - } - - Ok(all_releases) + Self { client } } pub fn instance() -> &'static ApiClient { &API_CLIENT } - #[cfg(test)] - pub fn new_with_base_urls( - firefox_api_base: String, - firefox_dev_api_base: String, - github_api_base: String, - chromium_api_base: String, - ) -> Self { - Self { - client: Client::new(), - firefox_api_base, - firefox_dev_api_base, - github_api_base, - chromium_api_base, - } - } - fn get_cache_dir() -> Result> { let cache_dir = crate::app_dirs::cache_dir().join("version_cache"); fs::create_dir_all(&cache_dir)?; @@ -491,7 +142,7 @@ impl ApiClient { fn is_cache_valid(timestamp: u64) -> bool { let current_time = Self::get_current_timestamp(); - let cache_duration = 10 * 60; // 10 minutes in seconds + let cache_duration = 10 * 60; current_time - timestamp < cache_duration } @@ -505,18 +156,15 @@ impl ApiClient { let content = fs::read_to_string(&cache_file).ok()?; if let Ok(cached) = serde_json::from_str::(&content) { - // Always return cached releases regardless of age - they're always valid log::info!("Using cached versions for {browser}"); return Some(cached.releases); } - // Backward compatibility: legacy caches stored just an array of version strings if let Ok(legacy_versions) = serde_json::from_str::>(&content) { log::info!("Using legacy cached versions for {browser}; upgrading in-memory"); let releases: Vec = legacy_versions .into_iter() .map(|version| BrowserRelease { - is_prerelease: is_browser_version_nightly(browser, &version, None), version, date: "".to_string(), }) @@ -530,25 +178,24 @@ impl ApiClient { pub fn is_cache_expired(&self, browser: &str) -> bool { let cache_dir = match Self::get_cache_dir() { Ok(dir) => dir, - Err(_) => return true, // If we can't get cache dir, consider expired + Err(_) => return true, }; let cache_file = cache_dir.join(format!("{browser}_versions.json")); if !cache_file.exists() { - return true; // No cache file means expired + return true; } let content = match fs::read_to_string(&cache_file) { Ok(content) => content, - Err(_) => return true, // Can't read cache, consider expired + Err(_) => return true, }; let cached_data: CachedVersionData = match serde_json::from_str(&content) { Ok(data) => data, - Err(_) => return true, // Can't parse cache, consider expired + Err(_) => return true, }; - // Check if cache is older than 10 minutes !Self::is_cache_valid(cached_data.timestamp) } @@ -571,524 +218,6 @@ impl ApiClient { Ok(()) } - fn load_cached_github_releases(&self, browser: &str) -> Option> { - let cache_dir = Self::get_cache_dir().ok()?; - let cache_file = cache_dir.join(format!("{browser}_github.json")); - - if !cache_file.exists() { - return None; - } - - let content = fs::read_to_string(&cache_file).ok()?; - let cached_data: CachedGithubData = serde_json::from_str(&content).ok()?; - - // Always use cached GitHub releases - cache never expires, only gets updated with new versions - Some(cached_data.releases) - } - - /// Public accessor for cached GitHub releases (used by other modules for classification) - pub fn get_cached_github_releases(&self, browser: &str) -> Option> { - self.load_cached_github_releases(browser) - } - - fn save_cached_github_releases( - &self, - browser: &str, - releases: &[GithubRelease], - ) -> Result<(), Box> { - let cache_dir = Self::get_cache_dir()?; - let cache_file = cache_dir.join(format!("{browser}_github.json")); - - let cached_data = CachedGithubData { - releases: releases.to_vec(), - timestamp: Self::get_current_timestamp(), - }; - - let content = serde_json::to_string_pretty(&cached_data)?; - fs::write(&cache_file, content)?; - log::info!("Cached {} GitHub releases for {}", releases.len(), browser); - Ok(()) - } - - pub async fn fetch_firefox_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_versions("firefox") { - return Ok(cached_releases); - } - } - - log::info!("Fetching Firefox releases from Mozilla API..."); - let url = format!("{}/firefox.json", self.firefox_api_base); - - let response = self - .client - .get(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("Failed to fetch Firefox versions: {}", response.status()).into()); - } - - let firefox_response: FirefoxApiResponse = response.json().await?; - - // Extract releases and filter for stable versions - let mut releases: Vec = firefox_response - .releases - .into_iter() - .filter_map(|(key, release)| { - // Only include releases that start with "firefox-" and have proper version format - if key.starts_with("firefox-") && !release.version.is_empty() { - let is_stable = matches!(release.category.as_str(), "major" | "stability"); - Some(BrowserRelease { - version: release.version.clone(), - date: release.date, - is_prerelease: !is_stable, - }) - } else { - None - } - }) - .collect(); - - // Sort by version number in descending order (newest first) - releases.sort_by(|a, b| { - let version_a = VersionComponent::parse(&a.version); - let version_b = VersionComponent::parse(&b.version); - version_b.cmp(&version_a) - }); - - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_versions("firefox", &releases) { - log::error!("Failed to cache Firefox versions: {e}"); - } - } - - Ok(releases) - } - - pub async fn fetch_firefox_developer_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_versions("firefox-developer") { - return Ok(cached_releases); - } - } - - log::info!("Fetching Firefox Developer Edition releases from Mozilla API..."); - let url = format!("{}/devedition.json", self.firefox_dev_api_base); - - let response = self - .client - .get(&url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - let error_msg = format!( - "Failed to fetch Firefox Developer Edition versions: {} - URL: {}", - response.status(), - url - ); - log::error!("{error_msg}"); - return Err(error_msg.into()); - } - - let firefox_response: FirefoxApiResponse = response.json().await?; - - // Extract releases and filter for developer edition versions - let mut releases: Vec = firefox_response - .releases - .into_iter() - .filter_map(|(key, release)| { - // Only include releases that start with "devedition-" and have proper version format - if key.starts_with("devedition-") && !release.version.is_empty() { - let is_stable = matches!(release.category.as_str(), "major" | "stability"); - Some(BrowserRelease { - version: release.version.clone(), - date: release.date, - is_prerelease: !is_stable, - }) - } else { - None - } - }) - .collect(); - - // Sort by version number in descending order (newest first) - releases.sort_by(|a, b| { - let version_a = VersionComponent::parse(&a.version); - let version_b = VersionComponent::parse(&b.version); - version_b.cmp(&version_a) - }); - - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_versions("firefox-developer", &releases) { - log::error!("Failed to cache Firefox Developer versions: {e}"); - } - } - - Ok(releases) - } - - pub async fn fetch_zen_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_github_releases("zen") { - return Ok(cached_releases); - } - } - - log::info!("Fetching Zen releases from GitHub API"); - let base_url = format!( - "{}/repos/zen-browser/desktop/releases", - self.github_api_base - ); - let mut releases: Vec = - self.fetch_github_releases_multiple_pages(&base_url).await?; - - // Check for twilight updates and mark alpha releases - for release in &mut releases { - // Use browser-specific alpha detection for Zen Browser - only "twilight" is nightly - release.is_nightly = - is_browser_version_nightly("zen", &release.tag_name, Some(&release.name)); - - // Check for twilight update if this is a twilight release - if release.tag_name.to_lowercase() == "twilight" { - if let Ok(has_update) = self.check_twilight_update(release).await { - if has_update { - log::info!( - "Detected update for Zen twilight release: {}", - release.tag_name - ); - } - } - } - } - - // Sort releases using the new version sorting system - sort_github_releases(&mut releases); - - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_github_releases("zen", &releases) { - log::error!("Failed to cache Zen releases: {e}"); - } - } - - Ok(releases) - } - - pub async fn fetch_brave_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_github_releases("brave") { - return Ok(cached_releases); - } - } - - log::info!("Fetching Brave releases from GitHub API"); - let base_url = format!( - "{}/repos/brave/brave-browser/releases", - self.github_api_base - ); - let releases: Vec = self.fetch_github_releases_multiple_pages(&base_url).await?; - - // Get platform info to filter appropriate releases - let (os, _) = Self::get_platform_info(); - - // Filter releases that have assets compatible with the current platform - let mut filtered_releases: Vec = releases - .into_iter() - .filter_map(|mut release| { - // Check if this release has compatible assets for the current platform - let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os); - - if has_compatible_asset { - // Use the centralized nightly detection function - release.is_nightly = - is_browser_version_nightly("brave", &release.tag_name, Some(&release.name)); - Some(release) - } else { - None - } - }) - .collect(); - - // Sort releases using the new version sorting system - sort_github_releases(&mut filtered_releases); - - if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) { - log::error!("Failed to cache Brave releases: {e}"); - } - - Ok(filtered_releases) - } - - /// Check if a Camoufox release has compatible assets for the given platform and architecture - fn has_compatible_camoufox_asset( - &self, - assets: &[crate::browser::GithubAsset], - os: &str, - arch: &str, - ) -> bool { - let (os_name, arch_name) = match (os, arch) { - ("windows", "x64") => ("win", "x86_64"), - ("windows", "arm64") => ("win", "arm64"), - ("linux", "x64") => ("lin", "x86_64"), - ("linux", "arm64") => ("lin", "arm64"), - ("macos", "x64") => ("mac", "x86_64"), - ("macos", "arm64") => ("mac", "arm64"), - _ => return false, - }; - - // Look for assets matching the pattern: camoufox-{version}-beta.{number}-{os}.{arch}.zip - // The separator before OS is a dash, e.g., camoufox-135.0.1-beta.24-lin.x86_64.zip - let pattern = format!("-{os_name}.{arch_name}.zip"); - assets.iter().any(|asset| { - let name = asset.name.to_lowercase(); - name.starts_with("camoufox-") && name.ends_with(&pattern) - }) - } - - fn has_compatible_brave_asset(assets: &[crate::browser::GithubAsset], os: &str) -> bool { - match os { - "windows" => { - // For Windows, look for standalone setup EXE (not the auto-updater one) - assets.iter().any(|asset| { - let name = asset.name.to_lowercase(); - name.contains("standalone") && name.ends_with(".exe") && !name.contains("silent") - }) || assets.iter().any(|asset| asset.name.ends_with(".exe")) - } - "macos" => { - // For macOS, prefer universal DMG - assets.iter().any(|asset| { - let name = asset.name.to_lowercase(); - name.contains("universal") && name.ends_with(".dmg") - }) || assets.iter().any(|asset| asset.name.ends_with(".dmg")) - } - "linux" => { - if assets.iter().any(|asset| { - let name = asset.name.to_lowercase(); - name.contains("lin") - }) { - return true; - } - - false - } - _ => false, - } - } - - /// Get platform and architecture information - fn get_platform_info() -> (String, String) { - let os = if cfg!(target_os = "windows") { - "windows" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "macos") { - "macos" - } else { - "unknown" - }; - - let arch = if cfg!(target_arch = "x86_64") { - "x64" - } else if cfg!(target_arch = "aarch64") { - "arm64" - } else { - "unknown" - }; - - (os.to_string(), arch.to_string()) - } - - pub async fn fetch_chromium_latest_version( - &self, - ) -> Result> { - // Use platform-aware URL for Chromium to match download URL generation - let (os, arch) = Self::get_platform_info(); - let platform_str = match (&os[..], &arch[..]) { - ("windows", "x64") => "Win_x64", - ("windows", "arm64") => "Win_Arm64", - ("linux", "x64") => "Linux_x64", - ("linux", "arm64") => return Err("Chromium doesn't support ARM64 on Linux".into()), - ("macos", "x64") => "Mac", - ("macos", "arm64") => "Mac_Arm", - _ => { - return Err(format!("Unsupported platform/architecture for Chromium: {os}/{arch}").into()) - } - }; - let url = format!("{}/{platform_str}/LAST_CHANGE", self.chromium_api_base); - let version = self - .client - .get(&url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await? - .text() - .await? - .trim() - .to_string(); - - Ok(version) - } - - pub async fn fetch_chromium_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_versions("chromium") { - return Ok(cached_releases); - } - } - - log::info!("Fetching Chromium releases..."); - - // Get the latest version first - let latest_version = self.fetch_chromium_latest_version().await?; - let latest_num: u32 = latest_version.parse().unwrap_or(0); - - // Generate a list of recent versions (last 20 builds, going back by 1000 each time) - let mut versions = Vec::new(); - for i in 0..20 { - let version_num = latest_num.saturating_sub(i * 1000); - if version_num > 0 { - versions.push(version_num.to_string()); - } - } - - // Convert to BrowserRelease objects - let releases: Vec = versions - .into_iter() - .map(|version| BrowserRelease { - version: version.clone(), - date: "".to_string(), - is_prerelease: false, - }) - .collect(); - - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_versions("chromium", &releases) { - log::error!("Failed to cache Chromium versions: {e}"); - } - } - - Ok(releases) - } - - pub async fn fetch_camoufox_releases_with_caching( - &self, - no_caching: bool, - ) -> Result, Box> { - // Check cache first (unless bypassing) - if !no_caching { - if let Some(cached_releases) = self.load_cached_github_releases("camoufox") { - log::info!( - "Using cached Camoufox releases, count: {}", - cached_releases.len() - ); - return Ok(cached_releases); - } - } - - log::info!("Fetching Camoufox releases from GitHub API"); - let base_url = format!("{}/repos/daijro/camoufox/releases", self.github_api_base); - let releases: Vec = self.fetch_github_releases_multiple_pages(&base_url).await?; - - log::info!( - "Fetched {} total Camoufox releases from GitHub", - releases.len() - ); - - // Get platform info to filter appropriate releases - let (os, arch) = Self::get_platform_info(); - log::info!("Filtering for platform: {os}/{arch}"); - - // Filter releases that have assets compatible with the current platform - let mut compatible_releases: Vec = releases - .into_iter() - .enumerate() - .filter_map(|(i, release)| { - let has_compatible = self.has_compatible_camoufox_asset(&release.assets, &os, &arch); - if !has_compatible { - log::info!( - "Release {} ({}) has no compatible assets for {}/{}", - i, - release.tag_name, - os, - arch - ); - log::info!( - " Available assets: {:?}", - release.assets.iter().map(|a| &a.name).collect::>() - ); - } - if has_compatible { - Some(release) - } else { - None - } - }) - .collect(); - - log::info!( - "After platform filtering: {} compatible releases", - compatible_releases.len() - ); - - // Sort by version (latest first) with debugging - log::info!( - "Before sorting: {:?}", - compatible_releases - .iter() - .map(|r| &r.tag_name) - .take(10) - .collect::>() - ); - sort_github_releases(&mut compatible_releases); - log::info!( - "After sorting: {:?}", - compatible_releases - .iter() - .map(|r| &r.tag_name) - .take(10) - .collect::>() - ); - - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_github_releases("camoufox", &compatible_releases) { - log::error!("Failed to cache Camoufox releases: {e}"); - } else { - log::info!("Cached {} Camoufox releases", compatible_releases.len()); - } - } - - Ok(compatible_releases) - } - fn load_cached_wayfern_version(&self) -> Option { let cache_dir = Self::get_cache_dir().ok()?; let cache_file = cache_dir.join("wayfern_version.json"); @@ -1100,7 +229,6 @@ impl ApiClient { let content = fs::read_to_string(&cache_file).ok()?; let cached_data: CachedWayfernData = serde_json::from_str(&content).ok()?; - // Always use cached Wayfern version - cache never expires, only gets updated Some(cached_data.version_info) } @@ -1127,7 +255,6 @@ impl ApiClient { &self, no_caching: bool, ) -> Result> { - // Check cache first (unless bypassing) if !no_caching { if let Some(cached_version) = self.load_cached_wayfern_version() { log::info!("Using cached Wayfern version: {}", cached_version.version); @@ -1181,7 +308,6 @@ impl ApiClient { })?; log::info!("Fetched Wayfern version: {}", version_info.version); - // Cache the results (unless bypassing cache) if !no_caching { if let Err(e) = self.save_cached_wayfern_version(&version_info) { log::error!("Failed to cache Wayfern version: {e}"); @@ -1207,68 +333,32 @@ impl ApiClient { self.get_wayfern_download_url(version_info).is_some() } - /// Check if a Zen twilight release has been updated by comparing file size - pub async fn check_twilight_update( - &self, - release: &GithubRelease, - ) -> Result> { - if release.tag_name.to_lowercase() != "twilight" { - return Ok(false); // Not a twilight release - } - - // Find the macOS universal DMG asset - let asset = release - .assets - .iter() - .find(|asset| asset.name == "zen.macos-universal.dmg") - .ok_or("No macOS universal asset found for twilight release")?; - - // Check if we have cached file size information - let cache_dir = Self::get_cache_dir()?; - let twilight_cache_file = cache_dir.join("zen_twilight_info.json"); - - #[derive(serde::Serialize, serde::Deserialize)] - struct TwilightInfo { - file_size: u64, - last_updated: u64, - } - - let current_info = TwilightInfo { - file_size: asset.size, - last_updated: Self::get_current_timestamp(), + fn get_platform_info() -> (String, String) { + let os = if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "macos") { + "macos" + } else { + "unknown" }; - if !twilight_cache_file.exists() { - // No cache exists, save current info and return true (new) - let content = serde_json::to_string_pretty(¤t_info)?; - fs::write(&twilight_cache_file, content)?; - return Ok(true); - } + let arch = if cfg!(target_arch = "x86_64") { + "x64" + } else if cfg!(target_arch = "aarch64") { + "arm64" + } else { + "unknown" + }; - let cached_content = fs::read_to_string(&twilight_cache_file)?; - let cached_info: TwilightInfo = serde_json::from_str(&cached_content)?; - - // Check if file size has changed - if cached_info.file_size != current_info.file_size { - // File size changed, update cache and return true - let content = serde_json::to_string_pretty(¤t_info)?; - fs::write(&twilight_cache_file, content)?; - log::info!( - "Zen twilight release updated: file size changed from {} to {}", - cached_info.file_size, - current_info.file_size - ); - return Ok(true); - } - - Ok(false) // No update detected + (os.to_string(), arch.to_string()) } pub fn clear_all_cache(&self) -> Result<(), Box> { let cache_dir = Self::get_cache_dir()?; if cache_dir.exists() { - // Remove all cache files for entry in fs::read_dir(&cache_dir)? { let entry = entry?; let path = entry.path(); @@ -1284,7 +374,6 @@ impl ApiClient { } } -// Global singleton instance lazy_static::lazy_static! { static ref API_CLIENT: ApiClient = ApiClient::new(); } @@ -1292,585 +381,47 @@ lazy_static::lazy_static! { #[cfg(test)] mod tests { use super::*; - use wiremock::matchers::{method, path, query_param}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - async fn setup_mock_server() -> MockServer { - MockServer::start().await - } - - fn create_test_client(server: &MockServer) -> ApiClient { - let base_url = server.uri(); - ApiClient::new_with_base_urls( - base_url.clone(), // firefox_api_base - base_url.clone(), // firefox_dev_api_base - base_url.clone(), // github_api_base - base_url.clone(), // chromium_api_base - ) - } #[test] fn test_version_parsing() { - // Test basic version parsing let v1 = VersionComponent::parse("1.2.3"); assert_eq!(v1.major, 1); assert_eq!(v1.minor, 2); assert_eq!(v1.patch, 3); - assert!(v1.pre_release.is_none()); - // Test alpha version - let v2 = VersionComponent::parse("1.2.3a1"); - assert_eq!(v2.major, 1); - assert_eq!(v2.minor, 2); - assert_eq!(v2.patch, 3); - assert!(v2.pre_release.is_some()); - let pre = v2.pre_release.unwrap(); - assert_eq!(pre.kind, PreReleaseKind::Alpha); - assert_eq!(pre.number, Some(1)); + let v2 = VersionComponent::parse("138.0.7204.50"); + assert_eq!(v2.major, 138); + assert_eq!(v2.minor, 0); + assert_eq!(v2.patch, 7204); + assert_eq!(v2.build, 50); - // Test beta version let v3 = VersionComponent::parse("137.0b5"); assert_eq!(v3.major, 137); assert_eq!(v3.minor, 0); assert_eq!(v3.patch, 0); - assert!(v3.pre_release.is_some()); - let pre = v3.pre_release.unwrap(); - assert_eq!(pre.kind, PreReleaseKind::Beta); - assert_eq!(pre.number, Some(5)); - - // Test twilight version (Zen Browser) - let v4 = VersionComponent::parse("twilight"); - assert_eq!(v4.major, 999); - assert_eq!(v4.minor, 0); - assert_eq!(v4.patch, 0); - assert!(v4.pre_release.is_some()); - let pre = v4.pre_release.unwrap(); - assert_eq!(pre.kind, PreReleaseKind::Alpha); - assert_eq!(pre.number, Some(999)); } #[test] fn test_version_comparison() { - // Test basic version comparison - let v1 = VersionComponent::parse("1.2.3"); - let v2 = VersionComponent::parse("1.2.4"); - assert!(v2 > v1); - - // Test major version difference - let v3 = VersionComponent::parse("2.0.0"); - let v4 = VersionComponent::parse("1.9.9"); - assert!(v3 > v4); - - // Test stable vs pre-release - let v5 = VersionComponent::parse("1.2.3"); - let v6 = VersionComponent::parse("1.2.3b1"); - assert!(v5 > v6); // Stable > beta - - // Test different pre-release types - let v7 = VersionComponent::parse("1.2.3a1"); - let v8 = VersionComponent::parse("1.2.3b1"); - assert!(v8 > v7); // Beta > alpha - - // Test pre-release numbers - let v9 = VersionComponent::parse("137.0b4"); - let v10 = VersionComponent::parse("137.0b5"); - assert!(v10 > v9); // b5 > b4 - - // Test twilight version (should have highest priority) - let v11 = VersionComponent::parse("twilight"); - let v12 = VersionComponent::parse("1.0.0"); - assert!(v11 > v12); // twilight > stable due to high major version - - // Test twilight vs other pre-releases - let v13 = VersionComponent::parse("twilight"); - let v14 = VersionComponent::parse("1.0.0a1"); - assert!(v13 > v14); // twilight > a1 due to high major version + assert!(VersionComponent::parse("1.2.4") > VersionComponent::parse("1.2.3")); + assert!(VersionComponent::parse("2.0.0") > VersionComponent::parse("1.9.9")); + assert!(VersionComponent::parse("138.0.7204.50") > VersionComponent::parse("138.0.7204.49")); } #[test] fn test_version_sorting() { let mut versions = vec![ - "1.9.9b".to_string(), - "1.12.6b".to_string(), - "1.10.0".to_string(), - "137.0b4".to_string(), - "137.0b5".to_string(), - "137.0".to_string(), - "twilight".to_string(), - "2.0.0a1".to_string(), + "138.0.7204.50".to_string(), + "138.0.7204.49".to_string(), + "139.0.7204.1".to_string(), + "137.0.7204.99".to_string(), ]; sort_versions(&mut versions); - // Expected order with twilight priority: twilight first due to high major version (999), then normal semantic versioning - assert_eq!(versions[0], "twilight"); - assert_eq!(versions[1], "137.0"); - assert_eq!(versions[2], "137.0b5"); - assert_eq!(versions[3], "137.0b4"); - assert_eq!(versions[4], "2.0.0a1"); - assert_eq!(versions[5], "1.12.6b"); - assert_eq!(versions[6], "1.10.0"); - assert_eq!(versions[7], "1.9.9b"); - } - - #[test] - fn test_sort_versions_comprehensive() { - let mut versions = vec![ - "1.0.0".to_string(), - "1.0.1".to_string(), - "1.1.0".to_string(), - "2.0.0a1".to_string(), - "2.0.0b1".to_string(), - "2.0.0rc1".to_string(), - "2.0.0".to_string(), - "10.0.0".to_string(), - "twilight".to_string(), - ]; - - sort_versions(&mut versions); - - // Expected order with twilight priority: twilight first due to high major version (999), then normal semantic versioning - assert_eq!(versions[0], "twilight"); - assert_eq!(versions[1], "10.0.0"); - assert_eq!(versions[2], "2.0.0"); - assert_eq!(versions[3], "2.0.0rc1"); - assert_eq!(versions[4], "2.0.0b1"); - assert_eq!(versions[5], "2.0.0a1"); - } - - #[tokio::test] - async fn test_firefox_api() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let mock_response = r#"{ - "releases": { - "firefox-139.0": { - "build_number": 1, - "category": "major", - "date": "2024-01-15", - "description": "Firefox 139.0 Release", - "is_security_driven": false, - "product": "firefox", - "version": "139.0" - }, - "firefox-138.0": { - "build_number": 1, - "category": "major", - "date": "2024-01-01", - "description": "Firefox 138.0 Release", - "is_security_driven": false, - "product": "firefox", - "version": "138.0" - } - } - }"#; - - Mock::given(method("GET")) - .and(path("/firefox.json")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(mock_response) - .insert_header("content-type", "application/json"), - ) - .mount(&server) - .await; - - let result = client.fetch_firefox_releases_with_caching(true).await; - - if let Err(e) = &result { - log::info!("Firefox API test error: {e}"); - } - assert!(result.is_ok()); - let releases = result.unwrap(); - assert!(!releases.is_empty()); - assert_eq!(releases[0].version, "139.0"); - } - - #[tokio::test] - async fn test_firefox_developer_api() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let mock_response = r#"{ - "releases": { - "devedition-140.0b1": { - "build_number": 1, - "category": "major", - "date": "2024-01-20", - "description": "Firefox Developer Edition 140.0b1", - "is_security_driven": false, - "product": "devedition", - "version": "140.0b1" - } - } - }"#; - - Mock::given(method("GET")) - .and(path("/devedition.json")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(mock_response) - .insert_header("content-type", "application/json"), - ) - .mount(&server) - .await; - - let result = client - .fetch_firefox_developer_releases_with_caching(true) - .await; - - if let Err(e) = &result { - log::info!("Firefox Developer API test error: {e}"); - } - assert!(result.is_ok()); - let releases = result.unwrap(); - assert!(!releases.is_empty()); - assert_eq!(releases[0].version, "140.0b1"); - } - - #[tokio::test] - async fn test_zen_api() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let mock_response = r#"[ - { - "tag_name": "twilight", - "name": "Zen Browser Twilight", - "prerelease": false, - "published_at": "2024-01-15T10:00:00Z", - "assets": [ - { - "name": "zen.macos-universal.dmg", - "browser_download_url": "https://example.com/zen-twilight.dmg", - "size": 120000000 - } - ] - } - ]"#; - - Mock::given(method("GET")) - .and(path("/repos/zen-browser/desktop/releases")) - .and(query_param("per_page", "100")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(mock_response) - .insert_header("content-type", "application/json"), - ) - .mount(&server) - .await; - - let result = client.fetch_zen_releases_with_caching(true).await; - - assert!(result.is_ok()); - let releases = result.unwrap(); - assert!(!releases.is_empty()); - assert_eq!(releases[0].tag_name, "twilight"); - } - - #[tokio::test] - async fn test_brave_api() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let mock_response = r#"[ - { - "tag_name": "v1.81.9", - "name": "Release v1.81.9 (Chromium 137.0.7151.104)", - "prerelease": false, - "published_at": "2024-01-15T10:00:00Z", - "draft": false, - "assets": [ - { - "name": "brave-v1.81.9-universal.dmg", - "browser_download_url": "https://example.com/brave-1.81.9-universal.dmg", - "size": 200000000 - }, - { - "name": "brave-browser-1.81.9-linux-amd64.zip", - "browser_download_url": "https://example.com/brave-1.81.9-linux-amd64.zip", - "size": 180000000 - }, - { - "name": "BraveBrowserStandaloneSetup.exe", - "browser_download_url": "https://example.com/brave-1.81.9-setup.exe", - "size": 150000000 - } - ] - } - ]"#; - - Mock::given(method("GET")) - .and(path("/repos/brave/brave-browser/releases")) - .and(query_param("per_page", "100")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(mock_response) - .insert_header("content-type", "application/json"), - ) - .mount(&server) - .await; - - let result = client.fetch_brave_releases_with_caching(true).await; - - if let Err(e) = &result { - log::info!("Brave API test error: {e}"); - } - assert!(result.is_ok()); - let releases = result.unwrap(); - assert!(!releases.is_empty()); - assert_eq!(releases[0].tag_name, "v1.81.9"); - assert!(!releases[0].is_nightly); // "Release v1.81.9 (Chromium 137.0.7151.104)" starts with "Release" so it should be stable - } - - #[tokio::test] - async fn test_chromium_api() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let (os, arch) = ApiClient::get_platform_info(); - let platform_str = match (&os[..], &arch[..]) { - ("windows", "x64") => "Win_x64", - ("windows", "arm64") => "Win_Arm64", - ("linux", "x64") => "Linux_x64", - ("linux", "arm64") => return, - ("macos", "x64") => "Mac", - ("macos", "arm64") => "Mac_Arm", - _ => return, - }; - - Mock::given(method("GET")) - .and(path(format!("/{platform_str}/LAST_CHANGE"))) - .respond_with( - ResponseTemplate::new(200) - .set_body_string("1465660") - .insert_header("content-type", "text/plain"), - ) - .mount(&server) - .await; - - let result = client.fetch_chromium_latest_version().await; - - assert!(result.is_ok()); - let version = result.unwrap(); - assert_eq!(version, "1465660"); - } - - #[tokio::test] - async fn test_chromium_releases_with_caching() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - let (os, arch) = ApiClient::get_platform_info(); - let platform_str = match (&os[..], &arch[..]) { - ("windows", "x64") => "Win_x64", - ("windows", "arm64") => "Win_Arm64", - ("linux", "x64") => "Linux_x64", - ("linux", "arm64") => return, - ("macos", "x64") => "Mac", - ("macos", "arm64") => "Mac_Arm", - _ => return, - }; - - Mock::given(method("GET")) - .and(path(format!("/{platform_str}/LAST_CHANGE"))) - .respond_with( - ResponseTemplate::new(200) - .set_body_string("1465660") - .insert_header("content-type", "text/plain"), - ) - .mount(&server) - .await; - - let result = client.fetch_chromium_releases_with_caching(true).await; - - assert!(result.is_ok()); - let releases = result.unwrap(); - assert!(!releases.is_empty()); - assert_eq!(releases[0].version, "1465660"); - assert!(!releases[0].is_prerelease); - } - - #[test] - fn test_is_nightly_version() { - assert!(is_nightly_version("1.2.3a1")); - assert!(is_nightly_version("137.0b5")); - assert!(is_nightly_version("140.0rc1")); - assert!(!is_nightly_version("139.0")); - assert!(!is_nightly_version("1.2.3")); - } - - #[test] - fn test_is_zen_nightly_version() { - // Only "twilight" should be considered nightly for Zen Browser - assert!(is_browser_version_nightly("zen", "twilight", None)); - assert!(is_browser_version_nightly("zen", "TWILIGHT", None)); // Case insensitive - - // Versions with "b" should NOT be considered nightly for Zen Browser - assert!(!is_browser_version_nightly("zen", "1.12.8b", None)); - assert!(!is_browser_version_nightly("zen", "1.0.0b1", None)); - assert!(!is_browser_version_nightly("zen", "2.0.0", None)); - } - - #[tokio::test] - async fn test_error_handling_404() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - Mock::given(method("GET")) - .and(path("/firefox.json")) - .respond_with(ResponseTemplate::new(404)) - .mount(&server) - .await; - - let result = client.fetch_firefox_releases_with_caching(true).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_error_handling_invalid_json() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - Mock::given(method("GET")) - .and(path("/firefox.json")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string("invalid json") - .insert_header("content-type", "application/json"), - ) - .mount(&server) - .await; - - let result = client.fetch_firefox_releases_with_caching(true).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_github_api_rate_limit() { - let server = setup_mock_server().await; - let client = create_test_client(&server); - - Mock::given(method("GET")) - .and(path("/repos/zen-browser/desktop/releases")) - .and(query_param("per_page", "100")) - .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "60")) - .mount(&server) - .await; - - let result = client.fetch_zen_releases_with_caching(true).await; - assert!(result.is_err()); - } - - #[test] - fn test_camoufox_beta_version_parsing() { - // Test specific Camoufox beta versions that are causing issues - let v22 = VersionComponent::parse("135.0.5beta22"); - let v24 = VersionComponent::parse("135.0.5beta24"); - - log::info!("v22: {v22:?}"); - log::info!("v24: {v24:?}"); - - // v24 should be greater than v22 - assert!( - v24 > v22, - "135.0.5beta24 should be greater than 135.0.5beta22" - ); - - // Test other beta version combinations - let v1 = VersionComponent::parse("135.0.5beta1"); - let v2 = VersionComponent::parse("135.0.5beta2"); - assert!(v2 > v1, "135.0.5beta2 should be greater than 135.0.5beta1"); - - // Test sorting of multiple versions - let mut versions = vec![ - "135.0.5beta22".to_string(), - "135.0.5beta24".to_string(), - "135.0.5beta23".to_string(), - "135.0.5beta21".to_string(), - ]; - - sort_versions(&mut versions); - - log::info!("Sorted versions: {versions:?}"); - - // Should be sorted from newest to oldest - assert_eq!(versions[0], "135.0.5beta24"); - assert_eq!(versions[1], "135.0.5beta23"); - assert_eq!(versions[2], "135.0.5beta22"); - assert_eq!(versions[3], "135.0.5beta21"); - } - - #[test] - fn test_camoufox_user_reported_versions() { - // Test the exact versions reported by the user: 135.0.1beta24 vs 135.0beta22 - let v22 = VersionComponent::parse("135.0beta22"); - let v24 = VersionComponent::parse("135.0.1beta24"); - - log::info!("User reported v22: {v22:?}"); - log::info!("User reported v24: {v24:?}"); - - // 135.0.1beta24 should be greater than 135.0beta22 (newer patch version) - assert!( - v24 > v22, - "135.0.1beta24 should be greater than 135.0beta22, but got: v24={v24:?} vs v22={v22:?}" - ); - - // Test sorting of the exact user-reported versions - let mut versions = vec!["135.0beta22".to_string(), "135.0.1beta24".to_string()]; - - sort_versions(&mut versions); - - log::info!("User reported sorted versions: {versions:?}"); - - // Should be sorted from newest to oldest - assert_eq!( - versions[0], "135.0.1beta24", - "135.0.1beta24 should be first (newest)" - ); - assert_eq!( - versions[1], "135.0beta22", - "135.0beta22 should be second (older)" - ); - } - - #[test] - fn test_camoufox_version_classification() { - // Test that Camoufox beta versions are now correctly classified as stable (not nightly) - assert!( - !is_browser_version_nightly("camoufox", "135.0beta22", None), - "135.0beta22 should be classified as stable for Camoufox" - ); - assert!( - !is_browser_version_nightly("camoufox", "135.0.1beta24", None), - "135.0.1beta24 should be classified as stable for Camoufox" - ); - - // Test with release names too - beta releases should be stable - assert!( - !is_browser_version_nightly("camoufox", "135.0beta22", Some("Release Beta 22")), - "Release with 'Beta' in name should be classified as stable for Camoufox" - ); - - // Test that stable versions are not classified as nightly - assert!( - !is_browser_version_nightly("camoufox", "135.0", None), - "135.0 should be classified as stable" - ); - assert!( - !is_browser_version_nightly("camoufox", "135.0.1", None), - "135.0.1 should be classified as stable" - ); - - // Test alpha and RC versions are still considered nightly - assert!( - !is_browser_version_nightly("camoufox", "136.0alpha1", None), - "136.0alpha1 should not be classified as nightly/prerelease" - ); - assert!( - !is_browser_version_nightly("camoufox", "136.0rc1", None), - "136.0rc1 should not be classified as nightly/prerelease" - ); + assert_eq!(versions[0], "139.0.7204.1"); + assert_eq!(versions[1], "138.0.7204.50"); + assert_eq!(versions[2], "138.0.7204.49"); + assert_eq!(versions[3], "137.0.7204.99"); } } diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index c8db515..c6e659b 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -1,5 +1,4 @@ use crate::browser::ProxySettings; -use crate::camoufox_manager::CamoufoxConfig; use crate::events; use crate::group_manager::GROUP_MANAGER; use crate::profile::manager::ProfileManager; @@ -35,7 +34,6 @@ pub struct ApiProfile { pub last_launch: Option, pub release_type: String, #[schema(value_type = Object)] - pub camoufox_config: Option, pub group_id: Option, pub tags: Vec, pub is_running: bool, @@ -57,7 +55,7 @@ pub struct ApiProfileResponse { #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct CreateProfileRequest { pub name: String, - /// Browser engine. Must be `"wayfern"` (anti-detect Chromium) or `"camoufox"` + /// Browser engine. Must be `"wayfern"` (anti-detect Chromium) /// (anti-detect Firefox). Any other value (e.g. `"chromium"`) is rejected with /// 400. pub browser: String, @@ -70,12 +68,11 @@ pub struct CreateProfileRequest { pub vpn_id: Option, pub launch_hook: Option, pub release_type: Option, - /// Camoufox fingerprint/config. Send only when `browser` is `"camoufox"`. + /// Wayfern fingerprint/config. Send only when `browser` is `"wayfern"`. /// Omit it, or pass an empty object `{}`, to have a fresh fingerprint /// generated automatically at creation. Provide a `fingerprint` field to /// pin a specific one. #[schema(value_type = Object)] - pub camoufox_config: Option, /// Wayfern fingerprint/config. Send only when `browser` is `"wayfern"`. /// Omit it, or pass an empty object `{}`, to have a fresh fingerprint /// generated automatically at creation. Provide a `fingerprint` field to @@ -98,7 +95,6 @@ pub struct UpdateProfileRequest { pub launch_hook: Option, pub release_type: Option, #[schema(value_type = Object)] - pub camoufox_config: Option, pub group_id: Option, pub tags: Option>, pub extension_group_id: Option, @@ -682,14 +678,6 @@ pub async fn get_api_server_status() -> Result, String> { Ok(server_guard.get_port()) } -/// Serialize a browser config (camoufox/wayfern) to JSON for an API response. -/// Viewing a profile's fingerprint is available to every API caller; only -/// editing it (via `update_profile`) and launching/killing profiles -/// programmatically require an active paid plan. -fn config_to_api_value(config: Option<&T>) -> Option { - serde_json::to_value(config?).ok() -} - // API Handlers - Profiles #[utoipa::path( get, @@ -720,7 +708,6 @@ async fn get_profiles() -> Result, StatusCode> { process_id: profile.process_id, last_launch: profile.last_launch, release_type: profile.release_type.clone(), - camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()), group_id: profile.group_id.clone(), tags: profile.tags.clone(), is_running: profile.process_id.is_some(), // Simple check based on process_id @@ -774,7 +761,6 @@ async fn get_profile( process_id: profile.process_id, last_launch: profile.last_launch, release_type: profile.release_type.clone(), - camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()), group_id: profile.group_id.clone(), tags: profile.tags.clone(), is_running: profile.process_id.is_some(), // Simple check based on process_id @@ -792,12 +778,12 @@ async fn get_profile( /// Create a profile. /// -/// - `browser` must be `"wayfern"` or `"camoufox"`; any other value is rejected +/// - `browser` must be `"wayfern"`; any other value is rejected /// with 400. /// - `version` is optional: omit it or pass `"latest"` to use the newest /// already-downloaded version of that browser. The version must be present /// locally (this endpoint does not download new versions); 400 if none is. -/// - Omitting the matching `wayfern_config`/`camoufox_config`, or passing an +/// - Omitting the matching `wayfern_config`, or passing an /// empty object `{}`, generates a fresh fingerprint automatically. #[utoipa::path( post, @@ -821,16 +807,16 @@ async fn create_profile( ) -> Result, (StatusCode, String)> { let profile_manager = ProfileManager::instance(); - // Only Wayfern and Camoufox profiles are launchable; the rest of the system + // Only Wayfern profiles are launchable; the rest of the system // (fingerprint generation, launch, run) supports nothing else. Reject anything // else up front — otherwise the profile is created with no fingerprint and an // unrecognized browser, then crashes with a 500 on /run. Mirrors the MCP // create_profile validation. - if request.browser != "wayfern" && request.browser != "camoufox" { + if request.browser != "wayfern" { return Err(( StatusCode::BAD_REQUEST, format!( - "Invalid browser \"{}\". Must be \"wayfern\" (anti-detect Chromium) or \"camoufox\" (anti-detect Firefox).", + "Invalid browser \"{}\". Must be \"wayfern\" (anti-detect Chromium).", request.browser ), )); @@ -863,13 +849,6 @@ async fn create_profile( } }; - // Parse camoufox config if provided - let camoufox_config = if let Some(config) = &request.camoufox_config { - serde_json::from_value(config.clone()).ok() - } else { - None - }; - // Parse wayfern config if provided let wayfern_config = if let Some(config) = &request.wayfern_config { serde_json::from_value(config.clone()).ok() @@ -905,7 +884,6 @@ async fn create_profile( request.release_type.as_deref().unwrap_or("stable"), request.proxy_id.clone(), request.vpn_id.clone(), - camoufox_config, wayfern_config, request.group_id.clone(), false, @@ -947,7 +925,6 @@ async fn create_profile( process_id: profile.process_id, last_launch: profile.last_launch, release_type: profile.release_type, - camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()), group_id: profile.group_id, tags: profile.tags, is_running: false, @@ -1054,30 +1031,6 @@ async fn update_profile( } } - if let Some(camoufox_config) = request.camoufox_config { - // Editing a profile's fingerprint config is part of the cross-OS fingerprint - // capability (GUI, API, MCP). Viewing it is free; mutating it is not. - if !crate::cloud_auth::CLOUD_AUTH - .can_use_cross_os_fingerprints() - .await - { - return Err(StatusCode::PAYMENT_REQUIRED); - } - let config: Result = serde_json::from_value(camoufox_config); - match config { - Ok(config) => { - if profile_manager - .update_camoufox_config(state.app_handle.clone(), &id, config) - .await - .is_err() - { - return Err(StatusCode::BAD_REQUEST); - } - } - Err(_) => return Err(StatusCode::BAD_REQUEST), - } - } - if let Some(group_id) = request.group_id { if profile_manager .assign_profiles_to_group(&state.app_handle, vec![id.clone()], Some(group_id)) @@ -2444,7 +2397,7 @@ mod tests { #[test] fn create_profile_request_allows_omitting_version_and_configs() { - // Minimal body: no version, no wayfern_config/camoufox_config. Must + // Minimal body: no version, no wayfern_config. Must // deserialize (version resolves to latest-downloaded at the handler; an // absent config triggers fresh-fingerprint generation). let json = r#"{"name": "p", "browser": "wayfern"}"#; @@ -2453,16 +2406,14 @@ mod tests { assert_eq!(parsed.browser, "wayfern"); assert!(parsed.version.is_none()); assert!(parsed.wayfern_config.is_none()); - assert!(parsed.camoufox_config.is_none()); } #[test] fn create_profile_browser_validation_matches_supported_engines() { // The handler rejects anything that isn't a launchable engine; this is the // same predicate it uses, kept in lockstep with MCP's create_profile. - let is_valid = |b: &str| b == "wayfern" || b == "camoufox"; + let is_valid = |b: &str| b == "wayfern"; assert!(is_valid("wayfern")); - assert!(is_valid("camoufox")); assert!(!is_valid("chromium")); assert!(!is_valid("firefox")); assert!(!is_valid("")); diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index b4b8c94..74c5651 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -13,7 +13,6 @@ pub struct UpdateNotification { pub current_version: String, pub new_version: String, pub affected_profiles: Vec, - pub is_stable_update: bool, pub timestamp: u64, } @@ -231,18 +230,10 @@ impl AutoUpdater { available_versions: &[BrowserVersionInfo], ) -> Result, Box> { let current_version = &profile.version; - let is_current_nightly = - crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None); - // Find the best available update let best_update = available_versions .iter() - .filter(|v| { - // Only consider versions newer than current - self.is_version_newer(&v.version, current_version) - && crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None) - == is_current_nightly - }) + .filter(|v| self.is_version_newer(&v.version, current_version)) .max_by(|a, b| self.compare_versions(&a.version, &b.version)); if let Some(update_version) = best_update { @@ -255,7 +246,6 @@ impl AutoUpdater { current_version: current_version.clone(), new_version: update_version.version.clone(), affected_profiles: vec![profile.name.clone()], - is_stable_update: !update_version.is_prerelease, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -291,12 +281,7 @@ impl AutoUpdater { let mut result: Vec = grouped.into_values().collect(); - // Sort by priority: stable updates first, then by timestamp - result.sort_by(|a, b| match (a.is_stable_update, b.is_stable_update) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => b.timestamp.cmp(&a.timestamp), - }); + result.sort_by_key(|b| std::cmp::Reverse(b.timestamp)); result } @@ -338,7 +323,6 @@ impl AutoUpdater { current_version: profile.version.clone(), new_version: new_version.to_string(), affected_profiles: vec![profile.name.clone()], - is_stable_update: true, timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() @@ -510,15 +494,6 @@ impl AutoUpdater { return None; } - // Only update stable->stable and nightly->nightly - let is_profile_nightly = - crate::api_client::is_browser_version_nightly(&profile.browser, &profile.version, None); - let is_latest_nightly = - crate::api_client::is_browser_version_nightly(&profile.browser, &latest, None); - if is_profile_nightly != is_latest_nightly { - return None; - } - match self .profile_manager .update_profile_version(app_handle, &profile.id.to_string(), &latest) @@ -595,15 +570,6 @@ impl AutoUpdater { continue; } - // Only update stable->stable and nightly->nightly - let is_profile_nightly = - crate::api_client::is_browser_version_nightly(&browser, &profile.version, None); - let is_latest_nightly = - crate::api_client::is_browser_version_nightly(&browser, &latest_version, None); - if is_profile_nightly != is_latest_nightly { - continue; - } - match self.profile_manager.update_profile_version( app_handle, &profile.id.to_string(), @@ -686,7 +652,6 @@ mod tests { launch_hook: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), @@ -708,10 +673,9 @@ mod tests { } } - fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo { + fn create_test_version_info(version: &str) -> BrowserVersionInfo { BrowserVersionInfo { version: version.to_string(), - is_prerelease, date: "2024-01-01".to_string(), } } @@ -753,111 +717,26 @@ mod tests { } #[test] - fn test_camoufox_beta_version_comparison() { + fn test_check_profile_update_picks_newer_wayfern_version() { let updater = AutoUpdater::instance(); - - // Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22 - assert!( - updater.is_version_newer("135.0.1beta24", "135.0beta22"), - "135.0.1beta24 should be newer than 135.0beta22" - ); - - assert_eq!( - updater.compare_versions("135.0.1beta24", "135.0beta22"), - std::cmp::Ordering::Greater, - "135.0.1beta24 should compare as greater than 135.0beta22" - ); - - // Test other camoufox beta version combinations - assert!( - updater.is_version_newer("135.0.5beta24", "135.0.5beta22"), - "135.0.5beta24 should be newer than 135.0.5beta22" - ); - - assert!( - updater.is_version_newer("135.0.1beta1", "135.0beta1"), - "135.0.1beta1 should be newer than 135.0beta1 due to patch version" - ); - - // Test that older versions are not considered newer - assert!( - !updater.is_version_newer("135.0beta22", "135.0.1beta24"), - "135.0beta22 should NOT be newer than 135.0.1beta24" - ); - } - - #[test] - fn test_beta_version_ordering_comprehensive() { - let updater = AutoUpdater::instance(); - - // Test various beta version patterns that could appear in camoufox - let test_cases = vec![ - ("135.0.1beta24", "135.0beta22", true), // User reported case - ("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta - ("135.1beta1", "135.0beta99", true), // Higher minor beats beta number - ("136.0beta1", "135.9.9beta99", true), // Higher major beats everything - ("135.0.1beta1", "135.0beta1", true), // Patch version matters - ("135.0beta22", "135.0.1beta24", false), // Reverse of user case - ]; - - for (newer, older, should_be_newer) in test_cases { - let result = updater.is_version_newer(newer, older); - assert_eq!( - result, - should_be_newer, - "Expected {} {} {} but got {}", - newer, - if should_be_newer { ">" } else { "<=" }, - older, - if result { "true" } else { "false" } - ); - } - } - - #[test] - fn test_check_profile_update_stable_to_stable() { - let updater = AutoUpdater::instance(); - let profile = create_test_profile("test", "firefox", "1.0.0"); + let profile = create_test_profile("test", "wayfern", "138.0.7204.49"); let versions = vec![ - create_test_version_info("1.0.1", false), // stable, newer - create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored - create_test_version_info("0.9.0", false), // stable, older + create_test_version_info("138.0.7204.50"), + create_test_version_info("138.0.7204.48"), ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); assert!(result.is_some()); - - let update = result.unwrap(); - assert_eq!(update.new_version, "1.0.1"); - assert!(update.is_stable_update); - } - - #[test] - fn test_check_profile_update_alpha_to_alpha() { - let updater = AutoUpdater::instance(); - let profile = create_test_profile("test", "firefox", "1.0.0-alpha"); - let versions = vec![ - create_test_version_info("1.0.1", false), // stable, should be included - create_test_version_info("1.1.0-alpha", true), // alpha, newer - create_test_version_info("0.9.0-alpha", true), // alpha, older - ]; - - let result = updater.check_profile_update(&profile, &versions).unwrap(); - assert!(result.is_some()); - - let update = result.unwrap(); - // Should pick the newest version (alpha user can upgrade to stable or newer alpha) - assert_eq!(update.new_version, "1.1.0-alpha"); - assert!(!update.is_stable_update); + assert_eq!(result.unwrap().new_version, "138.0.7204.50"); } #[test] fn test_check_profile_update_no_update_available() { let updater = AutoUpdater::instance(); - let profile = create_test_profile("test", "firefox", "1.0.0"); + let profile = create_test_profile("test", "wayfern", "138.0.7204.50"); let versions = vec![ - create_test_version_info("0.9.0", false), // older - create_test_version_info("1.0.0", false), // same version + create_test_version_info("138.0.7204.49"), + create_test_version_info("138.0.7204.50"), ]; let result = updater.check_profile_update(&profile, &versions).unwrap(); @@ -869,50 +748,27 @@ mod tests { let updater = AutoUpdater::instance(); let notifications = vec![ UpdateNotification { - id: "firefox_1.0.0_to_1.1.0_profile1".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), + id: "wayfern_138.0.7204.49_to_138.0.7204.50_profile1".to_string(), + browser: "wayfern".to_string(), + current_version: "138.0.7204.49".to_string(), + new_version: "138.0.7204.50".to_string(), affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, timestamp: 1000, }, UpdateNotification { - id: "firefox_1.0.0_to_1.1.0_profile2".to_string(), - browser: "firefox".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0".to_string(), + id: "wayfern_138.0.7204.49_to_138.0.7204.50_profile2".to_string(), + browser: "wayfern".to_string(), + current_version: "138.0.7204.49".to_string(), + new_version: "138.0.7204.50".to_string(), affected_profiles: vec!["profile2".to_string()], - is_stable_update: true, timestamp: 1001, }, - UpdateNotification { - id: "chrome_1.0.0_to_1.1.0-alpha".to_string(), - browser: "chrome".to_string(), - current_version: "1.0.0".to_string(), - new_version: "1.1.0-alpha".to_string(), - affected_profiles: vec!["profile3".to_string()], - is_stable_update: false, - timestamp: 1002, - }, ]; let grouped = updater.group_update_notifications(notifications); - assert_eq!(grouped.len(), 2); - - // Find the Firefox notification - let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap(); - assert_eq!(firefox_notification.affected_profiles.len(), 2); - assert!(firefox_notification - .affected_profiles - .contains(&"profile1".to_string())); - assert!(firefox_notification - .affected_profiles - .contains(&"profile2".to_string())); - - // Stable updates should come first - assert!(grouped[0].is_stable_update); + assert_eq!(grouped.len(), 1); + assert_eq!(grouped[0].affected_profiles.len(), 2); } #[test] @@ -956,7 +812,6 @@ mod tests { current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, timestamp: 1000, }); @@ -1094,7 +949,6 @@ mod tests { current_version: "1.0.0".to_string(), new_version: "1.1.0".to_string(), affected_profiles: vec!["profile1".to_string()], - is_stable_update: true, timestamp: 1000, }); diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index a0addb0..6f0654c 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -13,21 +13,18 @@ pub struct ProxySettings { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum BrowserType { - Camoufox, Wayfern, } impl BrowserType { pub fn as_str(&self) -> &'static str { match self { - BrowserType::Camoufox => "camoufox", BrowserType::Wayfern => "wayfern", } } pub fn from_str(s: &str) -> Result { match s { - "camoufox" => Ok(BrowserType::Camoufox), "wayfern" => Ok(BrowserType::Wayfern), _ => Err(format!("Unknown browser type: {s}")), } @@ -54,135 +51,6 @@ pub trait Browser: Send + Sync { mod macos { use super::*; - pub fn get_firefox_executable_path( - install_dir: &Path, - ) -> Result> { - // Find the .app directory - let app_path = std::fs::read_dir(install_dir)? - .filter_map(Result::ok) - .find(|entry| entry.path().extension().is_some_and(|ext| ext == "app")) - .ok_or("Browser app not found")?; - - // Construct the browser executable path - let mut executable_dir = app_path.path(); - executable_dir.push("Contents"); - executable_dir.push("MacOS"); - - // Find executables matching the browser name pattern - let candidates: Vec<_> = std::fs::read_dir(&executable_dir)? - .filter_map(Result::ok) - .filter(|entry| { - let binding = entry.file_name(); - let name = binding.to_string_lossy(); - name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("Browser") - }) - .map(|entry| entry.path()) - .collect(); - - if candidates.is_empty() { - return Err("No executable found in MacOS directory".into()); - } - - // For Camoufox, validate architecture compatibility - let executable_path = if candidates.iter().any(|p| { - p.file_name() - .and_then(|n| n.to_str()) - .map(|n| n.starts_with("camoufox")) - .unwrap_or(false) - }) { - // Find the executable that matches the current architecture - let current_arch = if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "arm64" - } else { - return Err("Unsupported architecture".into()); - }; - - // Try to find an executable that matches the current architecture - // Use file command to check architecture - let mut found_executable = None; - let mut file_command_available = true; - - for candidate in &candidates { - match std::process::Command::new("file").arg(candidate).output() { - Ok(output) => { - if output.status.success() { - if let Ok(output_str) = String::from_utf8(output.stdout) { - let is_compatible = if current_arch == "x86_64" { - output_str.contains("x86_64") || output_str.contains("i386") - } else { - output_str.contains("arm64") || output_str.contains("aarch64") - }; - - if is_compatible { - found_executable = Some(candidate.clone()); - log::info!( - "Found compatible Camoufox executable for {}: {}", - current_arch, - candidate.display() - ); - break; - } else { - log::warn!( - "Skipping incompatible Camoufox executable: {} (architecture: {})", - candidate.display(), - output_str.trim() - ); - } - } - } else { - log::warn!( - "Failed to check architecture for {}: file command returned non-zero exit code", - candidate.display() - ); - } - } - Err(e) => { - log::warn!( - "Failed to check architecture for {} using file command: {}", - candidate.display(), - e - ); - file_command_available = false; - // Continue checking other candidates - } - } - } - - // If no compatible executable found but we have candidates, use the first one - // (fallback for cases where file command isn't available or failed) - if found_executable.is_none() && !candidates.is_empty() { - if !file_command_available { - log::warn!( - "file command not available, using first candidate: {}", - candidates[0].display() - ); - } else { - log::warn!( - "No compatible executable found for architecture {}, using first candidate: {}", - current_arch, - candidates[0].display() - ); - } - found_executable = Some(candidates[0].clone()); - } - - found_executable.ok_or_else(|| { - format!( - "No compatible Camoufox executable found for architecture {}. Available executables: {:?}", - current_arch, - candidates - ) - })? - } else { - // For other browsers, use the first matching executable - candidates[0].clone() - }; - - Ok(executable_path) - } - pub fn get_wayfern_executable_path( install_dir: &Path, ) -> Result> { @@ -224,18 +92,6 @@ mod macos { false } - pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool { - // On macOS, check for .app files - if let Ok(entries) = std::fs::read_dir(install_dir) { - for entry in entries.flatten() { - if entry.path().extension().is_some_and(|ext| ext == "app") { - return true; - } - } - } - false - } - #[allow(dead_code)] pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box> { // On macOS, no special preparation needed @@ -248,43 +104,6 @@ mod linux { use super::*; use std::os::unix::fs::PermissionsExt; - pub fn get_firefox_executable_path( - install_dir: &Path, - browser_type: &BrowserType, - ) -> Result> { - // Expected structure examples: - // - Firefox/Firefox Developer on Linux often extract to: install_dir/firefox/firefox - // - Some archives may extract directly under: install_dir/firefox or install_dir/firefox-bin - // - For some flavors we may have: install_dir// - let _browser_subdir = install_dir.join(browser_type.as_str()); - - // Try common firefox executable locations (nested and flat) - let possible_executables = match browser_type { - BrowserType::Camoufox => { - vec![ - install_dir.join("camoufox-bin"), - install_dir.join("camoufox"), - ] - } - _ => vec![], - }; - - for executable_path in &possible_executables { - if executable_path.exists() && executable_path.is_file() { - return Ok(executable_path.clone()); - } - } - - Err( - format!( - "Executable not found for {} in {}", - browser_type.as_str(), - install_dir.display(), - ) - .into(), - ) - } - pub fn get_chromium_executable_path( install_dir: &Path, browser_type: &BrowserType, @@ -317,32 +136,6 @@ mod linux { ) } - pub fn is_firefox_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { - // Expected structure (most common): - // install_dir// - // However, Firefox Developer tarballs often extract to a "firefox" subfolder - // rather than "firefox-developer". Support both layouts. - let _browser_subdir = install_dir.join(browser_type.as_str()); - - let possible_executables = match browser_type { - BrowserType::Camoufox => { - vec![ - install_dir.join("camoufox-bin"), - install_dir.join("camoufox"), - ] - } - _ => vec![], - }; - - for exe_path in &possible_executables { - if exe_path.exists() && exe_path.is_file() { - return true; - } - } - - false - } - pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { let possible_executables = match browser_type { BrowserType::Wayfern => vec![ @@ -391,43 +184,6 @@ mod linux { mod windows { use super::*; - pub fn get_firefox_executable_path( - install_dir: &Path, - ) -> Result> { - // On Windows, look for firefox.exe - let possible_paths = [ - install_dir.join("firefox.exe"), - install_dir.join("firefox").join("firefox.exe"), - install_dir.join("bin").join("firefox.exe"), - ]; - - for path in &possible_paths { - if path.exists() && path.is_file() { - return Ok(path.clone()); - } - } - - // Look for any .exe file that might be the browser - if let Ok(entries) = std::fs::read_dir(install_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "exe") { - let name = path - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_lowercase(); - if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser") - { - return Ok(path); - } - } - } - } - - Err("Firefox executable not found in Windows installation directory".into()) - } - pub fn get_chromium_executable_path( install_dir: &Path, browser_type: &BrowserType, @@ -472,42 +228,6 @@ mod windows { Err("Chromium/Wayfern executable not found in Windows installation directory".into()) } - pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool { - // On Windows, check for .exe files - let possible_executables = [ - install_dir.join("firefox.exe"), - install_dir.join("firefox").join("firefox.exe"), - install_dir.join("bin").join("firefox.exe"), - ]; - - for exe_path in &possible_executables { - if exe_path.exists() && exe_path.is_file() { - return true; - } - } - - // Check for any .exe file that looks like a browser - if let Ok(entries) = std::fs::read_dir(install_dir) { - for entry in entries.flatten() { - let path = entry.path(); - - if path.extension().is_some_and(|ext| ext == "exe") { - let name = path - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_lowercase(); - if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser") - { - return true; - } - } - } - } - - false - } - pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { // On Windows, check for .exe files let possible_executables = match browser_type { @@ -557,94 +277,6 @@ mod windows { } } -pub struct CamoufoxBrowser; - -impl CamoufoxBrowser { - pub fn new() -> Self { - Self - } -} - -impl Browser for CamoufoxBrowser { - fn get_executable_path(&self, install_dir: &Path) -> Result> { - #[cfg(target_os = "macos")] - return macos::get_firefox_executable_path(install_dir); - - #[cfg(target_os = "linux")] - return linux::get_firefox_executable_path(install_dir, &BrowserType::Camoufox); - - #[cfg(target_os = "windows")] - return windows::get_firefox_executable_path(install_dir); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } - - fn create_launch_args( - &self, - profile_path: &str, - _proxy_settings: Option<&ProxySettings>, - url: Option, - remote_debugging_port: Option, - headless: bool, - ) -> Result, Box> { - // For Camoufox, we handle launching through the camoufox launcher - // This method won't be used directly, but we provide basic Firefox args as fallback - let mut args = vec![ - "-profile".to_string(), - profile_path.to_string(), - "-no-remote".to_string(), - ]; - - // Add remote debugging if requested - if let Some(port) = remote_debugging_port { - args.push("--start-debugger-server".to_string()); - args.push(port.to_string()); - } - - // Add headless mode if requested - if headless { - args.push("--headless".to_string()); - } - - if let Some(url) = url { - args.push(url); - } - - Ok(args) - } - - fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { - let install_dir = binaries_dir.join("camoufox").join(version); - - #[cfg(target_os = "macos")] - return macos::is_firefox_version_downloaded(&install_dir); - - #[cfg(target_os = "linux")] - return linux::is_firefox_version_downloaded(&install_dir, &BrowserType::Camoufox); - - #[cfg(target_os = "windows")] - return windows::is_firefox_version_downloaded(&install_dir); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - false - } - - fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box> { - #[cfg(target_os = "macos")] - return macos::prepare_executable(executable_path); - - #[cfg(target_os = "linux")] - return linux::prepare_executable(executable_path); - - #[cfg(target_os = "windows")] - return windows::prepare_executable(executable_path); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } -} - /// Wayfern is a Chromium-based anti-detect browser with CDP-based fingerprint injection pub struct WayfernBrowser; @@ -765,7 +397,6 @@ impl BrowserFactory { pub fn create_browser(&self, browser_type: BrowserType) -> Box { match browser_type { - BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), BrowserType::Wayfern => Box::new(WayfernBrowser::new()), } } @@ -852,292 +483,6 @@ pub struct GithubAsset { #[cfg(test)] mod tests { use super::*; - use std::fs; - use tempfile::TempDir; - - #[test] - fn test_browser_type_conversions() { - // Test as_str - assert_eq!(BrowserType::Camoufox.as_str(), "camoufox"); - assert_eq!(BrowserType::Wayfern.as_str(), "wayfern"); - - // Test from_str - assert_eq!( - BrowserType::from_str("camoufox").expect("camoufox should be valid"), - BrowserType::Camoufox - ); - assert_eq!( - BrowserType::from_str("wayfern").expect("wayfern should be valid"), - BrowserType::Wayfern - ); - - // Test invalid browser type - these should properly fail - let invalid_result = BrowserType::from_str("invalid"); - assert!( - invalid_result.is_err(), - "Invalid browser type should return error" - ); - - let empty_result = BrowserType::from_str(""); - assert!(empty_result.is_err(), "Empty string should return error"); - - assert!( - BrowserType::from_str("firefox").is_err(), - "Removed browser types should return error" - ); - assert!( - BrowserType::from_str("chromium").is_err(), - "Removed browser types should return error" - ); - } - - #[test] - fn test_camoufox_launch_args() { - let browser = CamoufoxBrowser::new(); - let args = browser - .create_launch_args("/path/to/profile", None, None, None, false) - .expect("Failed to create launch args for Camoufox"); - assert!(args.contains(&"-profile".to_string())); - assert!(args.contains(&"/path/to/profile".to_string())); - assert!(args.contains(&"-no-remote".to_string())); - - let args = browser - .create_launch_args( - "/path/to/profile", - None, - Some("https://example.com".to_string()), - None, - false, - ) - .expect("Failed to create launch args for Camoufox with URL"); - assert!(args.contains(&"https://example.com".to_string())); - - // Test with remote debugging - let args = browser - .create_launch_args("/path/to/profile", None, None, Some(9222), false) - .expect("Failed to create launch args for Camoufox with remote debugging"); - assert!(args.contains(&"--start-debugger-server".to_string())); - assert!(args.contains(&"9222".to_string())); - - // Test headless mode - let args = browser - .create_launch_args("/path/to/profile", None, None, None, true) - .expect("Failed to create launch args for Camoufox headless"); - assert!( - args.contains(&"--headless".to_string()), - "Browser should include headless flag when requested" - ); - } - - #[test] - fn test_wayfern_launch_args() { - let browser = WayfernBrowser::new(); - let args = browser - .create_launch_args("/path/to/profile", None, None, None, false) - .expect("Failed to create launch args for Wayfern"); - - assert!( - args.contains(&"--user-data-dir=/path/to/profile".to_string()), - "Wayfern args should contain user-data-dir" - ); - assert!( - args.contains(&"--no-default-browser-check".to_string()), - "Wayfern args should contain no-default-browser-check" - ); - assert!( - args.contains(&"--disable-background-mode".to_string()), - "Wayfern args should contain disable-background-mode" - ); - assert!( - args.contains(&"--disable-component-update".to_string()), - "Wayfern args should contain disable-component-update" - ); - - let args_with_url = browser - .create_launch_args( - "/path/to/profile", - None, - Some("https://example.com".to_string()), - None, - false, - ) - .expect("Failed to create launch args for Wayfern with URL"); - assert!( - args_with_url.contains(&"https://example.com".to_string()), - "Wayfern args should contain the URL" - ); - assert_eq!( - args_with_url.last().expect("Args should not be empty"), - "https://example.com" - ); - - // Test remote debugging - let args_with_debug = browser - .create_launch_args("/path/to/profile", None, None, Some(9222), false) - .expect("Failed to create launch args for Wayfern with remote debugging"); - assert!( - args_with_debug.contains(&"--remote-debugging-port=9222".to_string()), - "Wayfern args should contain remote debugging port" - ); - - // Test headless mode - let args_headless = browser - .create_launch_args("/path/to/profile", None, None, None, true) - .expect("Failed to create launch args for Wayfern headless"); - assert!( - args_headless.contains(&"--headless=new".to_string()), - "Wayfern args should contain headless flag when requested" - ); - } - - #[test] - fn test_proxy_settings_creation() { - let proxy = ProxySettings { - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: 8080, - username: None, - password: None, - }; - - assert_eq!(proxy.proxy_type, "http"); - assert_eq!(proxy.host, "127.0.0.1"); - assert_eq!(proxy.port, 8080); - - // Test different proxy types - let socks_proxy = ProxySettings { - proxy_type: "socks5".to_string(), - host: "proxy.example.com".to_string(), - port: 1080, - username: None, - password: None, - }; - - assert_eq!(socks_proxy.proxy_type, "socks5"); - assert_eq!(socks_proxy.host, "proxy.example.com"); - assert_eq!(socks_proxy.port, 1080); - } - - #[test] - fn test_version_downloaded_check() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let binaries_dir = temp_dir.path(); - - // Create a mock Camoufox browser installation - let browser_dir = binaries_dir.join("camoufox").join("135.0.1"); - fs::create_dir_all(&browser_dir).expect("Failed to create browser directory"); - - #[cfg(target_os = "macos")] - { - let app_dir = browser_dir.join("Camoufox.app"); - fs::create_dir_all(&app_dir).expect("Failed to create Camoufox.app directory"); - } - - #[cfg(target_os = "linux")] - { - let executable_path = browser_dir.join("camoufox"); - fs::write(&executable_path, "mock executable").expect("Failed to write mock executable"); - - use std::os::unix::fs::PermissionsExt; - let mut permissions = executable_path - .metadata() - .expect("Failed to get file metadata") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&executable_path, permissions) - .expect("Failed to set executable permissions"); - } - - #[cfg(target_os = "windows")] - { - let executable_path = browser_dir.join("firefox.exe"); - fs::write(&executable_path, "mock executable").expect("Failed to write mock executable"); - } - - let browser = CamoufoxBrowser::new(); - assert!(browser.is_version_downloaded("135.0.1", binaries_dir)); - assert!(!browser.is_version_downloaded("999.0", binaries_dir)); - - // Test with Wayfern browser - let wayfern_dir = binaries_dir.join("wayfern").join("1.0.0"); - fs::create_dir_all(&wayfern_dir).expect("Failed to create wayfern directory"); - - #[cfg(target_os = "macos")] - { - let wayfern_app_dir = wayfern_dir.join("Chromium.app"); - fs::create_dir_all(wayfern_app_dir.join("Contents").join("MacOS")) - .expect("Failed to create Chromium.app structure"); - - let executable_path = wayfern_app_dir - .join("Contents") - .join("MacOS") - .join("Chromium"); - fs::write(&executable_path, "mock executable") - .expect("Failed to write mock Wayfern executable"); - } - - #[cfg(target_os = "linux")] - { - let executable_path = wayfern_dir.join("chromium"); - fs::write(&executable_path, "mock executable") - .expect("Failed to write mock wayfern executable"); - - use std::os::unix::fs::PermissionsExt; - let mut permissions = executable_path - .metadata() - .expect("Failed to get wayfern metadata") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&executable_path, permissions) - .expect("Failed to set wayfern permissions"); - } - - #[cfg(target_os = "windows")] - { - let executable_path = wayfern_dir.join("chromium.exe"); - fs::write(&executable_path, "mock executable").expect("Failed to write mock chromium.exe"); - } - - let wayfern_browser = WayfernBrowser::new(); - assert!( - wayfern_browser.is_version_downloaded("1.0.0", binaries_dir), - "Wayfern version should be detected as downloaded" - ); - assert!( - !wayfern_browser.is_version_downloaded("9.9.9", binaries_dir), - "Non-existent Wayfern version should not be detected as downloaded" - ); - } - - #[test] - fn test_version_downloaded_no_app_directory() { - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let binaries_dir = temp_dir.path(); - - // Create browser directory but no proper executable structure - let browser_dir = binaries_dir.join("camoufox").join("135.0.1"); - fs::create_dir_all(&browser_dir).expect("Failed to create browser directory"); - - // Create some other files but no proper executable structure - fs::write(browser_dir.join("readme.txt"), "Some content").expect("Failed to write readme file"); - - let browser = CamoufoxBrowser::new(); - assert!( - !browser.is_version_downloaded("135.0.1", binaries_dir), - "Camoufox version should not be detected without proper executable structure" - ); - } - - #[test] - fn test_browser_type_clone_and_debug() { - let browser_type = BrowserType::Camoufox; - let cloned = browser_type.clone(); - assert_eq!(browser_type, cloned); - - // Test Debug trait - let debug_str = format!("{browser_type:?}"); - assert!(debug_str.contains("Camoufox")); - } #[test] fn test_proxy_settings_serialization() { @@ -1177,17 +522,6 @@ mod tests { ); } - #[test] - fn test_camoufox_config_has_no_executable_path() { - // Verify CamoufoxConfig does not store executable_path - let config = crate::camoufox_manager::CamoufoxConfig::default(); - let json = serde_json::to_value(&config).unwrap(); - assert!( - json.get("executable_path").is_none(), - "CamoufoxConfig should not have executable_path field" - ); - } - #[test] fn test_profile_data_path_is_dynamic() { use crate::profile::BrowserProfile; @@ -1203,7 +537,6 @@ mod tests { process_id: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index cbe0996..dfa512c 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1,22 +1,18 @@ use crate::browser::ProxySettings; -use crate::camoufox_manager::{CamoufoxConfig, CamoufoxManager}; use crate::cloud_auth::CLOUD_AUTH; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::events; -use crate::platform_browser; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use crate::wayfern_manager::{WayfernConfig, WayfernManager}; use serde::Serialize; use std::path::PathBuf; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use sysinfo::System; pub struct BrowserRunner { pub profile_manager: &'static ProfileManager, pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry, auto_updater: &'static crate::auto_updater::AutoUpdater, - camoufox_manager: &'static CamoufoxManager, wayfern_manager: &'static WayfernManager, } @@ -26,7 +22,6 @@ impl BrowserRunner { profile_manager: ProfileManager::instance(), downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(), auto_updater: crate::auto_updater::AutoUpdater::instance(), - camoufox_manager: CamoufoxManager::instance(), wayfern_manager: WayfernManager::instance(), } } @@ -200,281 +195,6 @@ impl BrowserRunner { remote_debugging_port: Option, headless: bool, ) -> Result> { - // Handle Camoufox profiles using CamoufoxManager - if profile.browser == "camoufox" { - // Get or create camoufox config - let mut camoufox_config = profile.camoufox_config.clone().unwrap_or_else(|| { - log::info!( - "No camoufox config found for profile {}, using default", - profile.name - ); - CamoufoxConfig::default() - }); - - // Always start a local proxy for Camoufox (for traffic monitoring and geoip support) - let mut upstream_proxy = self - .resolve_launch_proxy(profile) - .await - .map_err(|e| -> Box { e.into() })?; - - // If profile has a VPN instead of proxy, start VPN worker and use it as upstream - if upstream_proxy.is_none() { - if let Some(ref vpn_id) = profile.vpn_id { - match crate::vpn_worker_runner::start_vpn_worker(vpn_id).await { - Ok(vpn_worker) => { - if let Some(port) = vpn_worker.local_port { - upstream_proxy = Some(ProxySettings { - proxy_type: "socks5".to_string(), - host: "127.0.0.1".to_string(), - port, - username: None, - password: None, - }); - log::info!("VPN worker started for Camoufox profile on port {}", port); - } - } - Err(e) => { - return Err(format!("Failed to start VPN worker: {e}").into()); - } - } - } - } - - log::info!( - "Starting local proxy for Camoufox profile: {} (upstream: {})", - profile.name, - upstream_proxy - .as_ref() - .map(|p| format!("{}:{}", p.host, p.port)) - .unwrap_or_else(|| "DIRECT".to_string()) - ); - - // Start the proxy and get local proxy settings - // If proxy startup fails, DO NOT launch Camoufox - it requires local proxy - let profile_id_str = profile.id.to_string(); - let blocklist_file = Self::resolve_blocklist_file(profile).await?; - let local_proxy = PROXY_MANAGER - .start_proxy( - app_handle.clone(), - upstream_proxy.as_ref(), - 0, // Use 0 as temporary PID, will be updated later - Some(&profile_id_str), - profile.proxy_bypass_rules.clone(), - blocklist_file, - // Camoufox (Firefox 150, and Firefox 135 on the not-yet-updated - // Windows build) keeps the local HTTP proxy: Firefox's QUIC stack - // bypasses a configured proxy, so QUIC is disabled and HTTP CONNECT - // covers everything. SOCKS5 is reserved for Wayfern. - "http", - ) - .await - .map_err(|e| { - let error_msg = format!("Failed to start local proxy for Camoufox: {e}"); - log::error!("{}", error_msg); - error_msg - })?; - - // Format proxy URL for camoufox - always use HTTP for the local proxy - let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port); - - // Set proxy in camoufox config - camoufox_config.proxy = Some(proxy_url); - - // Ensure geoip is always enabled for proper geolocation spoofing - if camoufox_config.geoip.is_none() { - camoufox_config.geoip = Some(serde_json::Value::Bool(true)); - } - - log::info!( - "Configured local proxy for Camoufox: {:?}, geoip: {:?}", - camoufox_config.proxy, - camoufox_config.geoip - ); - - // Check if we need to generate a new fingerprint on every launch - let mut updated_profile = profile.clone(); - if camoufox_config.randomize_fingerprint_on_launch == Some(true) { - log::info!( - "Generating random fingerprint for Camoufox profile: {}", - profile.name - ); - - // Create a config copy without the existing fingerprint to force generation of a new one - let mut config_for_generation = camoufox_config.clone(); - config_for_generation.fingerprint = None; - - // Generate a new fingerprint - let new_fingerprint = self - .camoufox_manager - .generate_fingerprint_config(&app_handle, profile, &config_for_generation) - .await - .map_err(|e| format!("Failed to generate random fingerprint: {e}"))?; - - log::info!( - "New fingerprint generated, length: {} chars", - new_fingerprint.len() - ); - - // Update the config with the new fingerprint for launching - camoufox_config.fingerprint = Some(new_fingerprint.clone()); - - // Save the updated fingerprint to the profile so it persists - // We need to preserve all existing config fields and only update the fingerprint - let mut updated_camoufox_config = - updated_profile.camoufox_config.clone().unwrap_or_default(); - updated_camoufox_config.fingerprint = Some(new_fingerprint); - // Preserve the randomize flag so it persists across launches - updated_camoufox_config.randomize_fingerprint_on_launch = Some(true); - // Preserve the OS setting so it's used for future fingerprint generation - if camoufox_config.os.is_some() { - updated_camoufox_config.os = camoufox_config.os.clone(); - } - updated_profile.camoufox_config = Some(updated_camoufox_config.clone()); - - log::info!( - "Updated profile camoufox_config with new fingerprint for profile: {}, fingerprint length: {}", - profile.name, - updated_camoufox_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0) - ); - } - - // Create ephemeral dir for ephemeral or password-protected profiles - let override_profile_path = if profile.password_protected { - let dir = crate::profile::password::prepare_for_launch(profile) - .map_err(|e| -> Box { e.into() })?; - Some(dir) - } else if profile.ephemeral { - let dir = crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string()) - .map_err(|e| -> Box { e.into() })?; - Some(dir) - } else { - None - }; - - // Install extensions if an extension group is assigned - if updated_profile.extension_group_id.is_some() { - let profiles_dir = self.profile_manager.get_profiles_dir(); - let ext_profile_path = if let Some(ref override_path) = override_profile_path { - override_path.clone() - } else { - updated_profile.get_profile_data_path(&profiles_dir) - }; - let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); - match mgr.install_extensions_for_profile(&updated_profile, &ext_profile_path) { - Ok(paths) => { - if !paths.is_empty() { - log::info!( - "Installed {} Firefox extensions for profile: {}", - paths.len(), - updated_profile.name - ); - } - } - Err(e) => { - log::warn!("Failed to install extensions for Camoufox profile: {e}"); - } - } - } - - // Launch Camoufox browser - log::info!("Launching Camoufox for profile: {}", profile.name); - let camoufox_result = self - .camoufox_manager - .launch_camoufox_profile( - app_handle.clone(), - updated_profile.clone(), - camoufox_config, - url, - override_profile_path, - remote_debugging_port, - headless, - ) - .await - .map_err(|e| -> Box { - format!("Failed to launch Camoufox: {e}").into() - })?; - - // For server-based Camoufox, we use the process_id - let process_id = camoufox_result.processId.unwrap_or(0); - log::info!("Camoufox launched successfully with PID: {process_id}"); - - // Update profile with the process info from camoufox result - updated_profile.process_id = Some(process_id); - updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); - - // Update the proxy manager with the correct PID - if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, process_id) { - log::warn!("Warning: Failed to update proxy PID mapping: {e}"); - } else { - log::info!("Updated proxy PID mapping from temp (0) to actual PID: {process_id}"); - } - - // Persist the real browser PID so the detached proxy worker self-reaps - // when this browser dies, even after the GUI exits/restarts. - PROXY_MANAGER.set_browser_pid_for_profile(&updated_profile.id.to_string(), process_id); - - // Save the updated profile (includes new fingerprint if randomize is enabled) - log::info!( - "Saving profile {} with camoufox_config fingerprint length: {}", - updated_profile.name, - updated_profile - .camoufox_config - .as_ref() - .and_then(|c| c.fingerprint.as_ref()) - .map(|f| f.len()) - .unwrap_or(0) - ); - self.save_process_info(&updated_profile)?; - // Ensure tag suggestions include any tags from this profile - let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default()); - }); - log::info!( - "Successfully saved profile with process info: {}", - updated_profile.name - ); - - // Emit profiles-changed to trigger frontend to reload profiles from disk - // This ensures the UI displays the newly generated fingerprint - if let Err(e) = events::emit_empty("profiles-changed") { - log::warn!("Warning: Failed to emit profiles-changed event: {e}"); - } - - log::info!( - "Emitting profile events for successful Camoufox launch: {}", - updated_profile.name - ); - - // Emit profile update event to frontend - if let Err(e) = events::emit("profile-updated", &updated_profile) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - - // Emit minimal running changed event to frontend with a small delay - #[derive(Serialize)] - struct RunningChangedPayload { - id: String, - is_running: bool, - } - - let payload = RunningChangedPayload { - id: updated_profile.id.to_string(), - is_running: updated_profile.process_id.is_some(), - }; - - if let Err(e) = events::emit("profile-running-changed", &payload) { - log::warn!("Warning: Failed to emit profile running changed event: {e}"); - } else { - log::info!( - "Successfully emitted profile-running-changed event for Camoufox {}: running={}", - updated_profile.name, - payload.is_running - ); - } - - return Ok(updated_profile); - } - // Handle Wayfern profiles using WayfernManager if profile.browser == "wayfern" { // Get or create wayfern config @@ -838,62 +558,6 @@ impl BrowserRunner { url: &str, _internal_proxy_settings: Option<&ProxySettings>, ) -> Result<(), Box> { - // Handle Camoufox profiles using CamoufoxManager - if profile.browser == "camoufox" { - // Get the profile path based on the UUID - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = - crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy(); - - // Check if the process is running - match self - .camoufox_manager - .find_camoufox_by_profile(&profile_path_str) - .await - { - Ok(Some(_camoufox_process)) => { - log::info!( - "Opening URL in existing Camoufox process for profile: {} (ID: {})", - profile.name, - profile.id - ); - - // Get Camoufox executable path and use Firefox-like remote mechanism - let executable_path = self - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; - - // Launch Camoufox with -profile and -new-tab to open URL in existing instance - // This works because we no longer use -no-remote flag - let output = std::process::Command::new(&executable_path) - .arg("-profile") - .arg(&*profile_path_str) - .arg("-new-tab") - .arg(url) - .output() - .map_err(|e| format!("Failed to execute Camoufox: {e}"))?; - - if output.status.success() { - log::info!("Successfully opened URL in existing Camoufox instance"); - return Ok(()); - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - log::warn!("Camoufox -new-tab command failed: {stderr}"); - return Err( - format!("Failed to open URL in existing Camoufox instance: {stderr}").into(), - ); - } - } - Ok(None) => { - return Err("Camoufox browser is not running".into()); - } - Err(e) => { - return Err(format!("Error checking Camoufox process: {e}").into()); - } - } - } - // Handle Wayfern profiles using WayfernManager if profile.browser == "wayfern" { let profiles_dir = self.profile_manager.get_profiles_dir(); @@ -938,7 +602,7 @@ impl BrowserRunner { remote_debugging_port: Option, headless: bool, ) -> Result> { - // Camoufox and Wayfern start (and PID-reconcile) their own local proxy + // Wayfern starts (and PID-reconciles) its own local proxy // inside `launch_browser_internal`, so we hand it None here rather than // staging a second, orphaned proxy worker. self @@ -1105,478 +769,6 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result<(), Box> { - // Handle Camoufox profiles using CamoufoxManager - if profile.browser == "camoufox" { - // Search by profile path to find the running Camoufox instance - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = - crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy(); - - log::info!( - "Attempting to kill Camoufox process for profile: {} (ID: {})", - profile.name, - profile.id - ); - - // Stop the proxy associated with this profile first - let profile_id_str = profile.id.to_string(); - if let Err(e) = PROXY_MANAGER - .stop_proxy_by_profile_id(app_handle.clone(), &profile_id_str) - .await - { - log::warn!( - "Warning: Failed to stop proxy for profile {}: {e}", - profile_id_str - ); - } - - let mut process_actually_stopped = false; - match self - .camoufox_manager - .find_camoufox_by_profile(&profile_path_str) - .await - { - Ok(Some(camoufox_process)) => { - log::info!( - "Found Camoufox process: {} (PID: {:?})", - camoufox_process.id, - camoufox_process.processId - ); - - match self - .camoufox_manager - .stop_camoufox(&app_handle, &camoufox_process.id) - .await - { - Ok(stopped) => { - if let Some(pid) = camoufox_process.processId { - if stopped { - // Verify the process actually died by checking after a short delay - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - - if process_actually_stopped { - log::info!( - "Successfully stopped Camoufox process: {} (PID: {:?}) - verified process is dead", - camoufox_process.id, - pid - ); - } else { - log::warn!( - "Camoufox stop command returned success but process {} (PID: {:?}) is still running - forcing kill", - camoufox_process.id, - pid - ); - // Force kill the process - #[cfg(target_os = "macos")] - { - use crate::platform_browser; - if let Err(e) = platform_browser::macos::kill_browser_process_impl( - pid, - Some(&profile_path_str), - ) - .await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = - system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - #[cfg(target_os = "linux")] - { - use crate::platform_browser; - if let Err(e) = platform_browser::linux::kill_browser_process_impl( - pid, - Some(&profile_path_str), - ) - .await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = - system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - #[cfg(target_os = "windows")] - { - use crate::platform_browser; - if let Err(e) = - platform_browser::windows::kill_browser_process_impl(pid).await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = - system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - } - } else { - // stop_camoufox returned false, try to force kill the process - log::warn!( - "Camoufox stop command returned false for process {} (PID: {:?}) - attempting force kill", - camoufox_process.id, - pid - ); - #[cfg(target_os = "macos")] - { - use crate::platform_browser; - if let Err(e) = platform_browser::macos::kill_browser_process_impl( - pid, - Some(&profile_path_str), - ) - .await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - #[cfg(target_os = "linux")] - { - use crate::platform_browser; - if let Err(e) = platform_browser::linux::kill_browser_process_impl( - pid, - Some(&profile_path_str), - ) - .await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - #[cfg(target_os = "windows")] - { - use crate::platform_browser; - if let Err(e) = platform_browser::windows::kill_browser_process_impl(pid).await - { - log::error!("Failed to force kill Camoufox process {}: {}", pid, e); - } else { - // Verify the process is actually dead after force kill - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - if process_actually_stopped { - log::info!( - "Successfully force killed Camoufox process {} (PID: {:?})", - camoufox_process.id, - pid - ); - } - } - } - } - } else { - // No PID available, assume stopped if stop_camoufox returned true - process_actually_stopped = stopped; - if !stopped { - log::warn!( - "Failed to stop Camoufox process {} but no PID available for force kill", - camoufox_process.id - ); - } - } - } - Err(e) => { - log::error!( - "Error stopping Camoufox process {}: {}", - camoufox_process.id, - e - ); - // Try to force kill if we have a PID - if let Some(pid) = camoufox_process.processId { - log::info!( - "Attempting force kill after stop_camoufox error for PID: {}", - pid - ); - #[cfg(target_os = "macos")] - { - use crate::platform_browser; - if let Err(kill_err) = - platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str)) - .await - { - log::error!( - "Failed to force kill Camoufox process {}: {}", - pid, - kill_err - ); - } else { - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - } - } - #[cfg(target_os = "linux")] - { - use crate::platform_browser; - if let Err(kill_err) = - platform_browser::linux::kill_browser_process_impl(pid, Some(&profile_path_str)) - .await - { - log::error!( - "Failed to force kill Camoufox process {}: {}", - pid, - kill_err - ); - } else { - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - } - } - #[cfg(target_os = "windows")] - { - use crate::platform_browser; - if let Err(kill_err) = - platform_browser::windows::kill_browser_process_impl(pid).await - { - log::error!( - "Failed to force kill Camoufox process {}: {}", - pid, - kill_err - ); - } else { - use tokio::time::{sleep, Duration}; - sleep(Duration::from_millis(500)).await; - use sysinfo::{Pid, System}; - let system = System::new_all(); - process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); - } - } - } - } - } - } - Ok(None) => { - log::info!( - "No running Camoufox process found for profile: {} (ID: {})", - profile.name, - profile.id - ); - process_actually_stopped = true; // No process found, consider it stopped - } - Err(e) => { - log::error!( - "Error finding Camoufox process for profile {}: {}", - profile.name, - e - ); - } - } - - // If process wasn't confirmed stopped, return an error - if !process_actually_stopped { - log::error!( - "Failed to stop Camoufox process for profile: {} (ID: {}) - process may still be running", - profile.name, - profile.id - ); - return Err( - format!( - "Failed to stop Camoufox process for profile {} - process may still be running", - profile.name - ) - .into(), - ); - } - - // Clear the process ID from the profile and save immediately so that - // subsequent calls to update_profile_version (which re-reads from disk) - // see the cleared process_id. - let mut updated_profile = profile.clone(); - updated_profile.process_id = None; - self - .save_process_info(&updated_profile) - .map_err(|e| format!("Failed to update profile: {e}"))?; - - // Check for pending updates and apply them for Camoufox profiles too - if let Ok(Some(pending_update)) = self - .auto_updater - .get_pending_update(&profile.browser, &profile.version) - { - log::info!( - "Found pending update for Camoufox profile {}: {} -> {}", - profile.name, - profile.version, - pending_update.new_version - ); - - match self.profile_manager.update_profile_version( - &app_handle, - &profile.id.to_string(), - &pending_update.new_version, - ) { - Ok(updated_profile_after_update) => { - log::info!( - "Successfully updated Camoufox profile {} from version {} to {}", - profile.name, - profile.version, - pending_update.new_version - ); - updated_profile = updated_profile_after_update; - - if let Err(e) = self - .auto_updater - .dismiss_update_notification(&pending_update.id) - { - log::warn!("Warning: Failed to dismiss pending update notification: {e}"); - } - } - Err(e) => { - log::error!( - "Failed to apply pending update for Camoufox profile {}: {}", - profile.name, - e - ); - } - } - } - - // If no pending update was applied, check if a newer installed version exists - if updated_profile.version == profile.version { - if let Some(p) = self - .auto_updater - .update_profile_to_latest_installed(&app_handle, &updated_profile) - { - updated_profile = p; - } - } - - log::info!( - "Emitting profile events for successful Camoufox kill: {}", - updated_profile.name - ); - - // Emit profile update event to frontend - if let Err(e) = events::emit("profile-updated", &updated_profile) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - - // Emit minimal running changed event to frontend immediately - #[derive(Serialize)] - struct RunningChangedPayload { - id: String, - is_running: bool, - } - let payload = RunningChangedPayload { - id: updated_profile.id.to_string(), - is_running: false, // Explicitly set to false since we just killed it - }; - - if let Err(e) = events::emit("profile-running-changed", &payload) { - log::warn!("Warning: Failed to emit profile running changed event: {e}"); - } else { - log::info!( - "Successfully emitted profile-running-changed event for Camoufox {}: running={}", - updated_profile.name, - payload.is_running - ); - } - - if profile.password_protected { - // Await the re-encryption so the queued sync (released later by - // `mark_profile_stopped` in `kill_browser`) sees fresh ciphertext on - // disk instead of the previous snapshot. - crate::profile::password::complete_after_quit_and_wait(profile).await; - } else if profile.ephemeral { - crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); - } - - log::info!( - "Camoufox process cleanup completed for profile: {} (ID: {})", - profile.name, - profile.id - ); - - // Consolidate browser versions after stopping a browser - if let Ok(consolidated) = self - .downloaded_browsers_registry - .consolidate_browser_versions(&app_handle) - { - if !consolidated.is_empty() { - log::info!("Post-stop version consolidation results:"); - for action in &consolidated { - log::info!(" {action}"); - } - } - } - - return Ok(()); - } - // Handle Wayfern profiles using WayfernManager if profile.browser == "wayfern" { let profiles_dir = self.profile_manager.get_profiles_dir(); @@ -1922,394 +1114,10 @@ impl BrowserRunner { return Ok(()); } - // For non-camoufox/wayfern browsers, use the existing logic - let pid = if let Some(pid) = profile.process_id { - // First verify the stored PID is still valid and belongs to our profile - let system = System::new_all(); - if let Some(process) = system.process(sysinfo::Pid::from(pid as usize)) { - let cmd = process.cmd(); - let exe_name = process.name().to_string_lossy(); - - // Verify this process is actually our browser - let is_correct_browser = match profile.browser.as_str() { - "firefox" => { - exe_name.contains("firefox") - && !exe_name.contains("developer") - && !exe_name.contains("camoufox") - } - "firefox-developer" => { - // More flexible detection for Firefox Developer Edition - (exe_name.contains("firefox") && exe_name.contains("developer")) - || (exe_name.contains("firefox") - && cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Developer") - || arg_str.contains("developer") - || arg_str.contains("FirefoxDeveloperEdition") - || arg_str.contains("firefox-developer") - })) - || exe_name == "firefox" // Firefox Developer might just show as "firefox" - } - "zen" => exe_name.contains("zen"), - "chromium" => exe_name.contains("chromium") || exe_name.contains("chrome"), - "brave" => exe_name.contains("brave") || exe_name.contains("Brave"), - _ => false, - }; - - if is_correct_browser { - // Verify profile path match - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_data_path_str = profile_data_path.to_string_lossy(); - - let profile_path_match = if matches!( - profile.browser.as_str(), - "firefox" | "firefox-developer" | "zen" - ) { - // Firefox-based browsers: look for -profile argument followed by path - let mut found_profile_arg = false; - for (i, arg) in cmd.iter().enumerate() { - if let Some(arg_str) = arg.to_str() { - if arg_str == "-profile" && i + 1 < cmd.len() { - if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { - if next_arg == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - // Also check for combined -profile=path format - if arg_str == format!("-profile={profile_data_path_str}") { - found_profile_arg = true; - break; - } - // Check if the argument is the profile path directly - if arg_str == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - found_profile_arg - } else { - // Chromium-based browsers: look for --user-data-dir argument - cmd.iter().any(|s| { - if let Some(arg) = s.to_str() { - arg == format!("--user-data-dir={profile_data_path_str}") - || arg == profile_data_path_str - } else { - false - } - }) - }; - - if profile_path_match { - log::info!( - "Verified stored PID {} is valid for profile {} (ID: {})", - pid, - profile.name, - profile.id - ); - pid - } else { - log::info!("Stored PID {} doesn't match profile path for {} (ID: {}), searching for correct process", pid, profile.name, profile.id); - // Fall through to search for correct process - self.find_browser_process_by_profile(profile)? - } - } else { - log::info!("Stored PID {} doesn't match browser type for {} (ID: {}), searching for correct process", pid, profile.name, profile.id); - // Fall through to search for correct process - self.find_browser_process_by_profile(profile)? - } - } else { - log::info!( - "Stored PID {} is no longer valid for profile {} (ID: {}), searching for correct process", - pid, - profile.name, - profile.id - ); - // Fall through to search for correct process - self.find_browser_process_by_profile(profile)? - } - } else { - // No stored PID, search for the process - self.find_browser_process_by_profile(profile)? - }; - - log::info!("Attempting to kill browser process with PID: {pid}"); - - // Stop any associated proxy first - if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await { - log::warn!("Warning: Failed to stop proxy for PID {pid}: {e}"); - } - - #[cfg(target_os = "macos")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy().to_string(); - platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str)).await?; - } - - #[cfg(target_os = "windows")] - platform_browser::windows::kill_browser_process_impl(pid).await?; - - #[cfg(target_os = "linux")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy().to_string(); - platform_browser::linux::kill_browser_process_impl(pid, Some(&profile_path_str)).await?; - } - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - return Err("Unsupported platform".into()); - - let system = System::new_all(); - if system.process(sysinfo::Pid::from(pid as usize)).is_some() { - log::error!( - "Browser process {} is still running after kill attempt for profile: {} (ID: {})", - pid, - profile.name, - profile.id - ); - return Err( - format!( - "Browser process {} is still running after kill attempt", - pid - ) - .into(), - ); - } - - log::info!( - "Verified browser process {} is terminated for profile: {} (ID: {})", - pid, - profile.name, - profile.id - ); - - // Clear the process ID from the profile and save immediately so that - // subsequent calls to update_profile_version (which re-reads from disk) - // see the cleared process_id. - let mut updated_profile = profile.clone(); - updated_profile.process_id = None; - self - .save_process_info(&updated_profile) - .map_err(|e| format!("Failed to update profile: {e}"))?; - - // Check for pending updates and apply them - if let Ok(Some(pending_update)) = self - .auto_updater - .get_pending_update(&profile.browser, &profile.version) - { - log::info!( - "Found pending update for profile {}: {} -> {}", - profile.name, - profile.version, - pending_update.new_version - ); - - match self.profile_manager.update_profile_version( - &app_handle, - &profile.id.to_string(), - &pending_update.new_version, - ) { - Ok(updated_profile_after_update) => { - log::info!( - "Successfully updated profile {} from version {} to {}", - profile.name, - profile.version, - pending_update.new_version - ); - updated_profile = updated_profile_after_update; - - if let Err(e) = self - .auto_updater - .dismiss_update_notification(&pending_update.id) - { - log::warn!("Warning: Failed to dismiss pending update notification: {e}"); - } - } - Err(e) => { - log::error!( - "Failed to apply pending update for profile {}: {}", - profile.name, - e - ); - } - } - } - - // If no pending update was applied, check if a newer installed version exists - if updated_profile.version == profile.version { - if let Some(p) = self - .auto_updater - .update_profile_to_latest_installed(&app_handle, &updated_profile) - { - updated_profile = p; - } - } - - log::info!( - "Emitting profile events for successful kill: {}", - updated_profile.name - ); - - // Emit profile update event to frontend - if let Err(e) = events::emit("profile-updated", &updated_profile) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - - // Emit minimal running changed event to frontend immediately - #[derive(Serialize)] - struct RunningChangedPayload { - id: String, - is_running: bool, - } - let payload = RunningChangedPayload { - id: updated_profile.id.to_string(), - is_running: false, // Explicitly set to false since we just killed it - }; - - if let Err(e) = events::emit("profile-running-changed", &payload) { - log::warn!("Warning: Failed to emit profile running changed event: {e}"); - } else { - log::info!( - "Successfully emitted profile-running-changed event for {}: running={}", - updated_profile.name, - payload.is_running - ); - } - - // Consolidate browser versions after stopping a browser - if let Ok(consolidated) = self - .downloaded_browsers_registry - .consolidate_browser_versions(&app_handle) - { - if !consolidated.is_empty() { - log::info!("Post-stop version consolidation results:"); - for action in &consolidated { - log::info!(" {action}"); - } - } - } - - Ok(()) - } - - /// Helper method to find browser process by profile path - fn find_browser_process_by_profile( - &self, - profile: &BrowserProfile, - ) -> Result> { - let system = System::new_all(); - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_data_path_str = profile_data_path.to_string_lossy(); - - log::info!( - "Searching for {} browser process with profile path: {}", - profile.browser, - profile_data_path_str - ); - - for (pid, process) in system.processes() { - let cmd = process.cmd(); - if cmd.is_empty() { - continue; - } - - // Check if this is the right browser executable first - let exe_name = process.name().to_string_lossy().to_lowercase(); - let is_correct_browser = match profile.browser.as_str() { - "firefox" => { - exe_name.contains("firefox") - && !exe_name.contains("developer") - && !exe_name.contains("camoufox") - } - "firefox-developer" => { - // More flexible detection for Firefox Developer Edition - (exe_name.contains("firefox") && exe_name.contains("developer")) - || (exe_name.contains("firefox") - && cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Developer") - || arg_str.contains("developer") - || arg_str.contains("FirefoxDeveloperEdition") - || arg_str.contains("firefox-developer") - })) - || exe_name == "firefox" // Firefox Developer might just show as "firefox" - } - "zen" => exe_name.contains("zen"), - "chromium" => exe_name.contains("chromium") || exe_name.contains("chrome"), - "brave" => exe_name.contains("brave") || exe_name.contains("Brave"), - _ => false, - }; - - if !is_correct_browser { - continue; - } - - // Check for profile path match with improved logic - let profile_path_match = if matches!( - profile.browser.as_str(), - "firefox" | "firefox-developer" | "zen" - ) { - // Firefox-based browsers: look for -profile argument followed by path - let mut found_profile_arg = false; - for (i, arg) in cmd.iter().enumerate() { - if let Some(arg_str) = arg.to_str() { - if arg_str == "-profile" && i + 1 < cmd.len() { - if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { - if next_arg == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - // Also check for combined -profile=path format - if arg_str == format!("-profile={profile_data_path_str}") { - found_profile_arg = true; - break; - } - // Check if the argument is the profile path directly - if arg_str == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - found_profile_arg - } else { - // Chromium-based browsers: look for --user-data-dir argument - cmd.iter().any(|s| { - if let Some(arg) = s.to_str() { - arg == format!("--user-data-dir={profile_data_path_str}") - || arg == profile_data_path_str - } else { - false - } - }) - }; - - if profile_path_match { - let pid_u32 = pid.as_u32(); - log::info!( - "Found matching {} browser process with PID: {} for profile: {} (ID: {})", - profile.browser, - pid_u32, - profile.name, - profile.id - ); - return Ok(pid_u32); - } - } - Err( format!( - "No running {} browser process found for profile: {} (ID: {})", - profile.browser, profile.name, profile.id + "Unsupported browser '{}' for profile '{}' — only Wayfern is supported", + profile.browser, profile.name ) .into(), ) @@ -2427,10 +1235,9 @@ pub async fn launch_browser_profile_impl( profile_for_launch.id ); - // Launch browser or open URL in existing instance. Camoufox and Wayfern - // start their own local proxies inside `launch_browser_internal`; any - // other browser type is rejected there (we only support those for import, - // not launch), so no proxy needs to be staged here. + // Launch browser or open URL in existing instance. Wayfern starts its + // own local proxy inside `launch_browser_internal`; other browser types + // are rejected there, so no proxy needs to be staged here. // // `force_new` callers (API/MCP) always start a fresh instance with the // requested debug port and headless mode, bypassing the "open URL in the @@ -2485,7 +1292,7 @@ pub async fn launch_browser_profile_impl( // Now update the proxy with the correct PID if we have one if let Some(actual_pid) = updated_profile.process_id { - // Update the proxy manager with the correct PID (we always started with temp pid 1 for non-Camoufox) + // Update the proxy manager with the correct PID (we always started with temp pid 1) let _ = PROXY_MANAGER.update_proxy_pid(1u32, actual_pid); } diff --git a/src-tauri/src/browser_version_manager.rs b/src-tauri/src/browser_version_manager.rs index 23893dc..af13b1c 100644 --- a/src-tauri/src/browser_version_manager.rs +++ b/src-tauri/src/browser_version_manager.rs @@ -1,12 +1,10 @@ use crate::api_client::{sort_versions, ApiClient, BrowserRelease}; -use crate::browser::GithubRelease; use serde::{Deserialize, Serialize}; use std::collections::HashSet; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrowserVersionInfo { pub version: String, - pub is_prerelease: bool, pub date: String, } @@ -20,7 +18,6 @@ pub struct BrowserVersionsResult { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrowserReleaseTypes { pub stable: Option, - pub nightly: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -53,33 +50,8 @@ impl BrowserVersionManager { let (os, arch) = Self::get_platform_info(); match browser { - "firefox" | "firefox-developer" => Ok(true), - "zen" => { - // Zen supports all platforms and architectures - Ok(true) - } - "brave" => { - // Brave supports all platforms and architectures - Ok(true) - } - "chromium" => { - // Chromium doesn't support ARM64 on Linux - if arch == "arm64" && os == "linux" { - Ok(false) - } else { - Ok(true) - } - } - "camoufox" => { - // Camoufox supports all platforms and architectures according to the JS code - Ok(true) - } "wayfern" => { - // Wayfern support depends on version.json downloads availability - // Currently supports macos-arm64 and linux-x64 let platform_key = format!("{os}-{arch}"); - // Check dynamically, but allow the browser to appear even if platform not available yet - // The actual download will fail gracefully if not supported Ok(matches!( platform_key.as_str(), "macos-arm64" @@ -96,15 +68,7 @@ impl BrowserVersionManager { /// Get list of browsers supported on the current platform pub fn get_supported_browsers(&self) -> Vec { - let all_browsers = vec![ - "firefox", - "firefox-developer", - "zen", - "brave", - "chromium", - "camoufox", - "wayfern", - ]; + let all_browsers = vec!["wayfern"]; all_browsers .into_iter() @@ -115,13 +79,6 @@ impl BrowserVersionManager { /// Get cached browser versions immediately (returns None if no cache exists) pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { - if browser == "brave" { - return self - .api_client - .get_cached_github_releases("brave") - .map(|releases| releases.into_iter().map(|r| r.tag_name).collect()); - } - self .api_client .load_cached_versions(browser) @@ -133,20 +90,6 @@ impl BrowserVersionManager { &self, browser: &str, ) -> Option> { - if browser == "brave" { - if let Some(releases) = self.api_client.get_cached_github_releases("brave") { - let detailed_info: Vec = releases - .into_iter() - .map(|r| BrowserVersionInfo { - version: r.tag_name, - is_prerelease: r.is_nightly, - date: r.published_at, - }) - .collect(); - return Some(detailed_info); - } - } - let cached_releases = self.api_client.load_cached_versions(browser)?; // Convert cached versions to detailed info (without dates since cache doesn't store them) @@ -154,7 +97,6 @@ impl BrowserVersionManager { .into_iter() .map(|r| BrowserVersionInfo { version: r.version, - is_prerelease: r.is_prerelease, date: r.date, }) .collect(); @@ -167,44 +109,25 @@ impl BrowserVersionManager { self.api_client.is_cache_expired(browser) } - /// Get latest stable and nightly versions for a browser (cached first) + /// Get the latest Wayfern version (cached first) pub async fn get_browser_release_types( &self, browser: &str, ) -> Result> { - // Try to get from cache first + if browser != "wayfern" { + return Err(format!("Unsupported browser: {browser}").into()); + } + if let Some(cached_versions) = self.get_cached_browser_versions_detailed(browser) { - let latest_stable = cached_versions - .iter() - .find(|v| !v.is_prerelease) - .map(|v| v.version.clone()); - - let latest_nightly = cached_versions - .iter() - .find(|v| v.is_prerelease) - .map(|v| v.version.clone()); - return Ok(BrowserReleaseTypes { - stable: latest_stable, - nightly: latest_nightly, + stable: cached_versions.first().map(|v| v.version.clone()), }); } let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?; - let latest_stable = detailed_versions - .iter() - .find(|v| !v.is_prerelease) - .map(|v| v.version.clone()); - - let latest_nightly = detailed_versions - .iter() - .find(|v| v.is_prerelease) - .map(|v| v.version.clone()); - Ok(BrowserReleaseTypes { - stable: latest_stable, - nightly: latest_nightly, + stable: detailed_versions.first().map(|v| v.version.clone()), }) } @@ -235,12 +158,6 @@ impl BrowserVersionManager { // Fetch fresh versions from API let fresh_versions = match browser { - "firefox" => self.fetch_firefox_versions(true).await?, // Always fetch fresh for merging - "firefox-developer" => self.fetch_firefox_developer_versions(true).await?, - "zen" => self.fetch_zen_versions(true).await?, - "brave" => self.fetch_brave_versions(true).await?, - "chromium" => self.fetch_chromium_versions(true).await?, - "camoufox" => self.fetch_camoufox_versions(true).await?, "wayfern" => self.fetch_wayfern_versions(true).await?, _ => return Err(format!("Unsupported browser: {browser}").into()), }; @@ -262,13 +179,12 @@ impl BrowserVersionManager { crate::api_client::sort_versions(&mut merged_versions); // Save the merged cache (unless explicitly bypassing cache) - if !no_caching && browser != "brave" { + if !no_caching { let merged_releases: Vec = merged_versions .iter() .map(|v| BrowserRelease { version: v.clone(), date: "".to_string(), - is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None), }) .collect(); if let Err(e) = self @@ -305,157 +221,14 @@ impl BrowserVersionManager { // Since we don't have detailed date/prerelease info for cached versions, // we'll fetch fresh detailed info and map it to our merged versions let detailed_info: Vec = match browser { - "firefox" => { - let releases = self.fetch_firefox_releases_detailed(true).await?; - merged_versions - .into_iter() - .map(|version| { - // Try to find matching release info, otherwise create basic info - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_browser_version_nightly( - "firefox", &version, None, - ), - date: "".to_string(), - } - } - }) - .collect() - } - "firefox-developer" => { - let releases = self.fetch_firefox_developer_releases_detailed(true).await?; - merged_versions - .into_iter() - .map(|version| { - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_browser_version_nightly( - "firefox-developer", - &version, - None, - ), - date: "".to_string(), - } - } - }) - .collect() - } - "zen" => { - let releases = self.fetch_zen_releases_detailed(true).await?; - merged_versions - .into_iter() - // Filter out twilight releases at the detailed level too - .filter(|version| version.to_lowercase() != "twilight") - .map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.is_nightly, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_browser_version_nightly("zen", &version, None), - date: "".to_string(), - } - } - }) - .collect() - } - "brave" => { - let releases = self.fetch_brave_releases_detailed(true).await?; - merged_versions - .into_iter() - .map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.is_nightly, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_browser_version_nightly( - "brave", &version, None, - ), - date: "".to_string(), - } - } - }) - .collect() - } - "chromium" => { - let releases = self.fetch_chromium_releases_detailed(true).await?; - merged_versions - .into_iter() - .map(|version| { - if let Some(release) = releases.iter().find(|r| r.version == version) { - BrowserVersionInfo { - version: release.version.clone(), - is_prerelease: release.is_prerelease, - date: release.date.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: false, // Chromium usually stable releases - date: "".to_string(), - } - } - }) - .collect() - } - "camoufox" => { - let releases = self.fetch_camoufox_releases_detailed(true).await?; - merged_versions - .into_iter() - .map(|version| { - if let Some(release) = releases.iter().find(|r| r.tag_name == version) { - BrowserVersionInfo { - version: release.tag_name.clone(), - is_prerelease: release.is_nightly, - date: release.published_at.clone(), - } - } else { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: false, // Camoufox usually stable releases - date: "".to_string(), - } - } - }) - .collect() - } - "wayfern" => { - // Wayfern only has one version from version.json - merged_versions - .into_iter() - .map(|version| BrowserVersionInfo { - version: version.clone(), - is_prerelease: false, // Wayfern releases are always stable - date: "".to_string(), - }) - .collect() - } - _ => { - return Err(format!("Unsupported browser: {browser}").into()); - } + "wayfern" => merged_versions + .into_iter() + .map(|version| BrowserVersionInfo { + version: version.clone(), + date: "".to_string(), + }) + .collect(), + _ => return Err(format!("Unsupported browser: {browser}").into()), }; Ok(detailed_info) @@ -493,7 +266,6 @@ impl BrowserVersionManager { .map(|v| BrowserRelease { version: v.clone(), date: "".to_string(), - is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None), }) .collect(); if let Err(e) = self.api_client.save_cached_versions(browser, &releases) { @@ -512,170 +284,6 @@ impl BrowserVersionManager { let (os, arch) = Self::get_platform_info(); match browser { - "firefox" => { - let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) { - ("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false), - ("windows", "arm64") => ( - "win64-aarch64", - format!("Firefox Setup {version}.exe"), - false, - ), - ("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true), - ("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true), - ("macos", _) => ("mac", format!("Firefox {version}.dmg"), true), - _ => { - return Err( - format!("Unsupported platform/architecture for Firefox: {os}/{arch}").into(), - ) - } - }; - - Ok(DownloadInfo { - url: format!( - "https://download-installer.cdn.mozilla.net/pub/firefox/releases/{version}/{platform_path}/en-US/{filename}" - ), - filename, - is_archive, - }) - } - "firefox-developer" => { - let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) { - ("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false), - ("windows", "arm64") => ( - "win64-aarch64", - format!("Firefox Setup {version}.exe"), - false, - ), - ("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true), - ("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true), - ("macos", _) => ("mac", format!("Firefox {version}.dmg"), true), - _ => { - return Err( - format!("Unsupported platform/architecture for Firefox Developer: {os}/{arch}") - .into(), - ) - } - }; - - Ok(DownloadInfo { - url: format!( - "https://download-installer.cdn.mozilla.net/pub/devedition/releases/{version}/{platform_path}/en-US/{filename}" - ), - filename, - is_archive, - }) - } - "zen" => { - let (asset_name, filename, is_archive) = match (&os[..], &arch[..]) { - ("windows", "x64") => ("zen.installer.exe", format!("zen-{version}.exe"), false), - ("windows", "arm64") => ( - "zen.installer-arm64.exe", - format!("zen-{version}-arm64.exe"), - false, - ), - ("linux", "x64") => ( - "zen.linux-x86_64.tar.xz", - format!("zen-{version}-x86_64.tar.xz"), - true, - ), - ("linux", "arm64") => ( - "zen.linux-aarch64.tar.xz", - format!("zen-{version}-aarch64.tar.xz"), - true, - ), - ("macos", _) => ( - "zen.macos-universal.dmg", - format!("zen-{version}.dmg"), - true, - ), - _ => { - return Err(format!("Unsupported platform/architecture for Zen: {os}/{arch}").into()) - } - }; - - Ok(DownloadInfo { - url: format!( - "https://github.com/zen-browser/desktop/releases/download/{version}/{asset_name}" - ), - filename, - is_archive, - }) - } - "brave" => { - let (filename, is_archive) = match (&os[..], &arch[..]) { - ("windows", _) => (format!("brave-{version}.exe"), false), - ("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true), - ("linux", "arm64") => (format!("brave-browser-{version}-linux-arm64.zip"), true), - ("macos", _) => ("Brave-Browser-universal.dmg".to_string(), true), - _ => { - return Err(format!("Unsupported platform/architecture for Brave: {os}/{arch}").into()) - } - }; - - Ok(DownloadInfo { - url: format!( - "https://github.com/brave/brave-browser/releases/download/{version}/{filename}" - ), - filename, - is_archive, - }) - } - "chromium" => { - let platform_str = match (&os[..], &arch[..]) { - ("windows", "x64") => "Win_x64", - ("windows", "arm64") => "Win_Arm64", - ("linux", "x64") => "Linux_x64", - ("linux", "arm64") => return Err("Chromium doesn't support ARM64 on Linux".into()), - ("macos", "x64") => "Mac", - ("macos", "arm64") => "Mac_Arm", - _ => { - return Err( - format!("Unsupported platform/architecture for Chromium: {os}/{arch}").into(), - ) - } - }; - - let (archive_name, filename) = match os.as_str() { - "windows" => ("chrome-win.zip", format!("chromium-{version}-win.zip")), - "linux" => ("chrome-linux.zip", format!("chromium-{version}-linux.zip")), - "macos" => ("chrome-mac.zip", format!("chromium-{version}-mac.zip")), - _ => return Err(format!("Unsupported platform for Chromium: {os}").into()), - }; - - Ok(DownloadInfo { - url: format!( - "https://commondatastorage.googleapis.com/chromium-browser-snapshots/{platform_str}/{version}/{archive_name}" - ), - filename, - is_archive: true, - }) - } - "camoufox" => { - // Camoufox downloads from GitHub releases with pattern: camoufox-{version}-{release}-{os}.{arch}.zip - let (os_name, arch_name) = match (&os[..], &arch[..]) { - ("windows", "x64") => ("win", "x86_64"), - ("windows", "arm64") => ("win", "arm64"), - ("linux", "x64") => ("lin", "x86_64"), - ("linux", "arm64") => ("lin", "arm64"), - ("macos", "x64") => ("mac", "x86_64"), - ("macos", "arm64") => ("mac", "arm64"), - _ => { - return Err( - format!("Unsupported platform/architecture for Camoufox: {os}/{arch}").into(), - ) - } - }; - - // Note: We provide a placeholder URL here since Camoufox requires dynamic resolution - // The actual URL will be resolved in download.rs resolve_download_url - Ok(DownloadInfo { - url: format!( - "https://github.com/daijro/camoufox/releases/download/{version}/camoufox-{{version}}-{{release}}-{os_name}.{arch_name}.zip" - ), - filename: format!("camoufox-{version}-{os_name}.{arch_name}.zip"), - is_archive: true, - }) - } "wayfern" => { // Wayfern downloads from https://download.wayfern.com/ // File naming: wayfern-{chromium_version}-{platform}-{arch}.{ext} @@ -728,153 +336,6 @@ impl BrowserVersionManager { (os.to_string(), arch.to_string()) } - // Private helper methods for each browser type - - async fn fetch_firefox_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self.fetch_firefox_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } - - async fn fetch_firefox_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - self - .api_client - .fetch_firefox_releases_with_caching(no_caching) - .await - } - - async fn fetch_firefox_developer_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self - .fetch_firefox_developer_releases_detailed(no_caching) - .await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } - - async fn fetch_firefox_developer_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - self - .api_client - .fetch_firefox_developer_releases_with_caching(no_caching) - .await - } - - async fn fetch_zen_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self.fetch_zen_releases_detailed(no_caching).await?; - Ok( - releases - .into_iter() - .filter(|r| r.tag_name.to_lowercase() != "twilight") - .map(|r| r.tag_name) - .collect(), - ) - } - - async fn fetch_zen_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - self - .api_client - .fetch_zen_releases_with_caching(no_caching) - .await - } - - async fn fetch_brave_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self.fetch_brave_releases_detailed(no_caching).await?; - // Persist a lightweight versions cache with accurate prerelease info for Brave - let converted: Vec = releases - .iter() - .map(|r| BrowserRelease { - version: r.tag_name.clone(), - date: r.published_at.clone(), - is_prerelease: r.is_nightly, - }) - .collect(); - // Always save so that other callers without release_name can classify correctly - if let Err(e) = self.api_client.save_cached_versions("brave", &converted) { - log::error!("Failed to persist Brave versions cache: {e}"); - } - - Ok(releases.into_iter().map(|r| r.tag_name).collect()) - } - - async fn fetch_brave_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self - .api_client - .fetch_brave_releases_with_caching(no_caching) - .await?; - - // Save a parallel versions cache for Brave with accurate prerelease flags - let converted: Vec = releases - .iter() - .map(|r| BrowserRelease { - version: r.tag_name.clone(), - date: r.published_at.clone(), - is_prerelease: r.is_nightly, - }) - .collect(); - if let Err(e) = self.api_client.save_cached_versions("brave", &converted) { - log::error!("Failed to persist Brave versions cache: {e}"); - } - - Ok(releases) - } - - async fn fetch_chromium_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self.fetch_chromium_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.version).collect()) - } - - async fn fetch_chromium_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - self - .api_client - .fetch_chromium_releases_with_caching(no_caching) - .await - } - - async fn fetch_camoufox_versions( - &self, - no_caching: bool, - ) -> Result, Box> { - let releases = self.fetch_camoufox_releases_detailed(no_caching).await?; - Ok(releases.into_iter().map(|r| r.tag_name).collect()) - } - - async fn fetch_camoufox_releases_detailed( - &self, - no_caching: bool, - ) -> Result, Box> { - self - .api_client - .fetch_camoufox_releases_with_caching(no_caching) - .await - } - async fn fetch_wayfern_versions( &self, no_caching: bool, @@ -912,37 +373,14 @@ pub async fn get_browser_release_types( mod tests { use super::*; - use wiremock::MockServer; - - async fn setup_mock_server() -> MockServer { - MockServer::start().await - } - - fn create_test_api_client(server: &MockServer) -> ApiClient { - let base_url = server.uri(); - ApiClient::new_with_base_urls( - base_url.clone(), // firefox_api_base - base_url.clone(), // firefox_dev_api_base - base_url.clone(), // github_api_base - base_url.clone(), // chromium_api_base - ) - } - - fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionManager { - BrowserVersionManager::instance() - } - #[tokio::test] async fn test_browser_version_manager_creation() { let _ = BrowserVersionManager::instance(); - // Test passes if we can create the service without panicking } #[tokio::test] async fn test_unsupported_browser() { - let server = setup_mock_server().await; - let api_client = create_test_api_client(&server); - let service = create_test_service(api_client); + let service = BrowserVersionManager::instance(); let result = service.fetch_browser_versions("unsupported", false).await; assert!( @@ -962,141 +400,48 @@ mod tests { fn test_get_download_info() { let service = BrowserVersionManager::instance(); - // Test Firefox - platform-specific expectations - let firefox_info = service.get_download_info("firefox", "139.0").unwrap(); + let wayfern_info = service.get_download_info("wayfern", "1.0.0").unwrap(); - #[cfg(target_os = "macos")] + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] { - assert_eq!(firefox_info.filename, "Firefox 139.0.dmg"); - assert!(firefox_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-macos-arm64.dmg"); + assert!(wayfern_info.is_archive); } - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "macos", target_arch = "x86_64"))] { - assert_eq!(firefox_info.filename, "firefox-139.0.tar.xz"); - assert!(firefox_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-macos-x64.dmg"); + assert!(wayfern_info.is_archive); } - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] { - assert_eq!(firefox_info.filename, "Firefox Setup 139.0.exe"); - assert!(!firefox_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-linux-x64.tar.xz"); + assert!(wayfern_info.is_archive); } - assert!(firefox_info - .url - .contains("download-installer.cdn.mozilla.net")); - assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/")); - - // Test Firefox Developer - let firefox_dev_info = service - .get_download_info("firefox-developer", "139.0b1") - .unwrap(); - - #[cfg(target_os = "macos")] + #[cfg(all(target_os = "linux", target_arch = "aarch64"))] { - assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg"); - assert!(firefox_dev_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-linux-arm64.tar.xz"); + assert!(wayfern_info.is_archive); } - #[cfg(target_os = "linux")] + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] { - assert_eq!(firefox_dev_info.filename, "firefox-139.0b1.tar.xz"); - assert!(firefox_dev_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-windows-x64.zip"); + assert!(wayfern_info.is_archive); } - #[cfg(target_os = "windows")] + #[cfg(all(target_os = "windows", target_arch = "aarch64"))] { - assert_eq!(firefox_dev_info.filename, "Firefox Setup 139.0b1.exe"); - assert!(!firefox_dev_info.is_archive); + assert_eq!(wayfern_info.filename, "wayfern-1.0.0-windows-arm64.zip"); + assert!(wayfern_info.is_archive); } - assert!(firefox_dev_info - .url - .contains("download-installer.cdn.mozilla.net")); - assert!(firefox_dev_info - .url - .contains("/pub/devedition/releases/139.0b1/")); + assert!(wayfern_info.url.contains("download.wayfern.com")); - // Test Zen Browser - let zen_info = service.get_download_info("zen", "1.11b").unwrap(); - - #[cfg(target_os = "macos")] - { - assert_eq!(zen_info.filename, "zen-1.11b.dmg"); - assert!(zen_info.url.contains("zen.macos-universal.dmg")); - assert!(zen_info.is_archive); - } - - #[cfg(target_os = "linux")] - { - assert_eq!(zen_info.filename, "zen-1.11b-x86_64.tar.xz"); - assert!(zen_info.url.contains("zen.linux-x86_64.tar.xz")); - assert!(zen_info.is_archive); - } - - #[cfg(target_os = "windows")] - { - assert_eq!(zen_info.filename, "zen-1.11b.exe"); - assert!(zen_info.url.contains("zen.installer.exe")); - assert!(!zen_info.is_archive); - } - - // Test Chromium - let chromium_info = service.get_download_info("chromium", "1465660").unwrap(); - - #[cfg(target_os = "macos")] - { - assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip"); - assert!(chromium_info.url.contains("chrome-mac.zip")); - } - - #[cfg(target_os = "linux")] - { - assert_eq!(chromium_info.filename, "chromium-1465660-linux.zip"); - assert!(chromium_info.url.contains("chrome-linux.zip")); - } - - #[cfg(target_os = "windows")] - { - assert_eq!(chromium_info.filename, "chromium-1465660-win.zip"); - assert!(chromium_info.url.contains("chrome-win.zip")); - } - - assert!(chromium_info.is_archive); - - // Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL - let brave_info = service.get_download_info("brave", "v1.81.9").unwrap(); - - #[cfg(target_os = "macos")] - { - assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg"); - assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg"); - assert!(brave_info.is_archive); - } - - #[cfg(target_os = "linux")] - { - assert_eq!(brave_info.filename, "brave-browser-v1.81.9-linux-amd64.zip"); - assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-browser-v1.81.9-linux-amd64.zip"); - assert!(brave_info.is_archive); - } - - #[cfg(target_os = "windows")] - { - assert_eq!(brave_info.filename, "brave-v1.81.9.exe"); - assert_eq!( - brave_info.url, - "https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-v1.81.9.exe" - ); - assert!(!brave_info.is_archive); - } - - // Test unsupported browser - let unsupported_result = service.get_download_info("unsupported", "1.0.0"); + let unsupported_result = service.get_download_info("firefox", "1.0.0"); assert!(unsupported_result.is_err()); - - log::info!("Download info test passed for all browsers"); } } diff --git a/src-tauri/src/camoufox/config.rs b/src-tauri/src/camoufox/config.rs deleted file mode 100644 index 1942d16..0000000 --- a/src-tauri/src/camoufox/config.rs +++ /dev/null @@ -1,677 +0,0 @@ -//! Camoufox configuration builder. -//! -//! Converts fingerprints to Camoufox configuration format and builds launch options. - -use rand::RngExt; -use serde_yaml; -use std::collections::HashMap; -use std::path::Path; - -use crate::camoufox::data; -use crate::camoufox::env_vars; -use crate::camoufox::fingerprint::types::*; -use crate::camoufox::fonts; -use crate::camoufox::geolocation; -use crate::camoufox::presets; -use crate::camoufox::webgl; - -/// Browserforge mapping from YAML. -type BrowserforgeMapping = HashMap; - -/// Load the browserforge mapping from embedded YAML. -fn load_browserforge_mapping() -> BrowserforgeMapping { - serde_yaml::from_str(data::BROWSERFORGE_YML).unwrap_or_default() -} - -/// Convert a fingerprint to Camoufox configuration. -pub fn from_browserforge( - fingerprint: &Fingerprint, - ff_version: Option, -) -> HashMap { - let mapping = load_browserforge_mapping(); - let mut config = HashMap::new(); - - // Convert fingerprint to a JSON value for easier traversal - let fp_json = serde_json::to_value(fingerprint).unwrap_or_default(); - - // Apply mappings recursively - cast_to_properties(&mut config, &mapping, &fp_json, ff_version); - - // Handle window.screenX and window.screenY - handle_screen_xy(&mut config, &fingerprint.screen); - - config -} - -/// Recursively cast fingerprint properties to Camoufox config format. -fn cast_to_properties( - config: &mut HashMap, - mapping: &BrowserforgeMapping, - fingerprint: &serde_json::Value, - ff_version: Option, -) { - if let serde_json::Value::Object(fp_obj) = fingerprint { - for (key, mapping_value) in mapping { - let fp_value = fp_obj.get(key); - - match mapping_value { - serde_yaml::Value::String(target_key) => { - if let Some(value) = fp_value { - let mut final_value = value.clone(); - - // Handle negative screen values - if target_key.starts_with("screen.") { - if let Some(num) = final_value.as_i64() { - if num < 0 { - final_value = serde_json::json!(0); - } - } - } - - // Replace Firefox version in user agent strings - if let (Some(version), Some(s)) = (ff_version, final_value.as_str()) { - let replaced = replace_ff_version(s, version); - final_value = serde_json::json!(replaced); - } - - config.insert(target_key.clone(), final_value); - } - } - serde_yaml::Value::Mapping(nested_mapping) => { - if let Some(nested_fp) = fp_value { - let nested: BrowserforgeMapping = nested_mapping - .iter() - .filter_map(|(k, v)| k.as_str().map(|ks| (ks.to_string(), v.clone()))) - .collect(); - cast_to_properties(config, &nested, nested_fp, ff_version); - } - } - _ => {} - } - } - } -} - -/// Replace Firefox version in user agent and related strings. -fn replace_ff_version(s: &str, version: u32) -> String { - // Match patterns like "135.0" (Firefox version) and replace with new version - let re = regex_lite::Regex::new(r"(?, screen: &ScreenFingerprint) { - if config.contains_key("window.screenY") { - return; - } - - let screen_x = screen.screen_x; - if screen_x == 0 { - config.insert("window.screenX".to_string(), serde_json::json!(0)); - config.insert("window.screenY".to_string(), serde_json::json!(0)); - return; - } - - if (-50..=50).contains(&screen_x) { - config.insert("window.screenY".to_string(), serde_json::json!(screen_x)); - return; - } - - let screen_y = screen.avail_height as i32 - screen.outer_height as i32; - let mut rng = rand::rng(); - - let y = if screen_y == 0 { - 0 - } else if screen_y > 0 { - rng.random_range(0..=screen_y) - } else { - rng.random_range(screen_y..=0) - }; - - config.insert("window.screenY".to_string(), serde_json::json!(y)); -} - -/// GeoIP option - can be an IP address string or auto-detect. -#[derive(Debug, Clone)] -pub enum GeoIPOption { - /// Auto-detect IP (fetch public IP, optionally through proxy) - Auto, - /// Use a specific IP address - IP(String), -} - -/// Configuration builder for Camoufox launch. -#[derive(Debug, Clone)] -pub struct CamoufoxConfigBuilder { - fingerprint: Option, - operating_system: Option, - screen_constraints: Option, - block_images: bool, - block_webrtc: bool, - block_webgl: bool, - custom_fonts: Option>, - custom_fonts_only: bool, - firefox_prefs: HashMap, - proxy: Option, - headless: bool, - ff_version: Option, - extra_config: HashMap, - geoip: Option, -} - -/// Proxy configuration. -#[derive(Debug, Clone)] -pub struct ProxyConfig { - pub server: String, - pub username: Option, - pub password: Option, - pub bypass: Option, -} - -impl ProxyConfig { - /// Parse a proxy URL string into ProxyConfig. - /// Supports formats like: - /// - "http://host:port" - /// - "http://user:pass@host:port" - /// - "socks5://user:pass@host:port" - pub fn from_url(url: &str) -> Result { - let parsed = url::Url::parse(url).map_err(|e| ConfigError::InvalidProxy(e.to_string()))?; - - let host = parsed - .host_str() - .ok_or_else(|| ConfigError::InvalidProxy("Missing host".to_string()))?; - - let port = parsed.port().unwrap_or(8080); - let scheme = parsed.scheme(); - - let server = format!("{scheme}://{host}:{port}"); - - let username = if !parsed.username().is_empty() { - Some(parsed.username().to_string()) - } else { - None - }; - - let password = parsed.password().map(String::from); - - Ok(Self { - server, - username, - password, - bypass: None, - }) - } -} - -impl Default for CamoufoxConfigBuilder { - fn default() -> Self { - Self::new() - } -} - -impl CamoufoxConfigBuilder { - pub fn new() -> Self { - Self { - fingerprint: None, - operating_system: None, - screen_constraints: None, - block_images: false, - block_webrtc: false, - block_webgl: false, - custom_fonts: None, - custom_fonts_only: false, - firefox_prefs: HashMap::new(), - proxy: None, - headless: false, - ff_version: None, - extra_config: HashMap::new(), - geoip: None, - } - } - - pub fn fingerprint(mut self, fp: Fingerprint) -> Self { - self.fingerprint = Some(fp); - self - } - - pub fn operating_system(mut self, os: &str) -> Self { - self.operating_system = Some(os.to_string()); - self - } - - pub fn screen_constraints(mut self, constraints: ScreenConstraints) -> Self { - self.screen_constraints = Some(constraints); - self - } - - pub fn block_images(mut self, block: bool) -> Self { - self.block_images = block; - self - } - - pub fn block_webrtc(mut self, block: bool) -> Self { - self.block_webrtc = block; - self - } - - pub fn block_webgl(mut self, block: bool) -> Self { - self.block_webgl = block; - self - } - - pub fn custom_fonts(mut self, fonts: Vec) -> Self { - self.custom_fonts = Some(fonts); - self - } - - pub fn custom_fonts_only(mut self, only: bool) -> Self { - self.custom_fonts_only = only; - self - } - - pub fn firefox_pref>(mut self, key: &str, value: V) -> Self { - self.firefox_prefs.insert(key.to_string(), value.into()); - self - } - - pub fn proxy(mut self, proxy: ProxyConfig) -> Self { - self.proxy = Some(proxy); - self - } - - pub fn headless(mut self, headless: bool) -> Self { - self.headless = headless; - self - } - - pub fn ff_version(mut self, version: u32) -> Self { - self.ff_version = Some(version); - self - } - - pub fn extra_config>(mut self, key: &str, value: V) -> Self { - self.extra_config.insert(key.to_string(), value.into()); - self - } - - /// Set GeoIP option for geolocation-based fingerprinting. - /// Use `GeoIPOption::Auto` to auto-detect public IP (optionally through proxy). - /// Use `GeoIPOption::IP(ip_string)` to use a specific IP address. - pub fn geoip(mut self, option: GeoIPOption) -> Self { - self.geoip = Some(option); - self - } - - /// Build the complete Camoufox launch configuration. - /// - /// Prefers a real-fingerprint preset (matched against the Camoufox build's - /// Firefox version via `presets::preset_line_for`) when no explicit - /// fingerprint was passed. Falls back to the Bayesian network-based - /// synthesizer when presets are unavailable, so callers without a known - /// Firefox version (or with no preset for the requested OS) still get a - /// valid config — matching pre-v150 behaviour byte-for-byte. - pub fn build(self) -> Result { - let mut rng = rand::rng(); - let ff_version = self.ff_version; - - // 1) The caller supplied a fingerprint outright — honour it and skip - // presets entirely. This is the path tests and advanced consumers - // use to inject deterministic fixtures. - // 2) Otherwise, try a bundled preset for the requested OS / FF line. - // 3) Fall back to the Bayesian generator. This is also the path that - // runs for users whose Camoufox binary has no readable `version.json` - // (`ff_version == None`), or whose OS has no presets bundled. - let (mut config, target_os) = if let Some(fp) = self.fingerprint { - let target_os = env_vars::determine_ua_os(&fp.navigator.user_agent); - // `from_browserforge` already runs `handle_screen_xy` internally. - let config = from_browserforge(&fp, ff_version); - (config, target_os) - } else if let Some(preset) = - presets::get_random_preset(self.operating_system.as_deref(), ff_version) - { - let mut config = presets::from_preset(&preset, ff_version); - let target_os = config - .get("navigator.userAgent") - .and_then(|v| v.as_str()) - .map(env_vars::determine_ua_os) - .or_else(|| { - // Last-resort heuristic from the platform string — keeps target_os - // sensible even if a preset somehow omits the user agent. - config - .get("navigator.platform") - .and_then(|v| v.as_str()) - .map(|p| match p { - "Win32" => "windows", - "MacIntel" => "macos", - _ => "linux", - }) - }) - .unwrap_or("macos"); - // Presets don't carry multi-monitor offsets, so default screenX/Y to - // (0, 0) — matches what real single-display users send. - config - .entry("window.screenX".to_string()) - .or_insert(serde_json::json!(0)); - config - .entry("window.screenY".to_string()) - .or_insert(serde_json::json!(0)); - (config, target_os) - } else { - let generator = crate::camoufox::fingerprint::FingerprintGenerator::new()?; - let options = FingerprintOptions { - operating_system: self.operating_system.clone(), - browsers: Some(vec!["firefox".to_string()]), - devices: Some(vec!["desktop".to_string()]), - screen: self.screen_constraints, - ..Default::default() - }; - let fingerprint = generator.get_fingerprint(&options)?.fingerprint; - let target_os = env_vars::determine_ua_os(&fingerprint.navigator.user_agent); - let config = from_browserforge(&fingerprint, ff_version); - (config, target_os) - }; - - // Note: we used to spoof `window.history.length` to a random value in - // [1, 5] here. Newer Camoufox builds clamp the docShell session history - // to this value, which disables the toolbar back/forward buttons when - // the spoof rolls a small number. The fingerprint value drifts on every - // user navigation anyway, so a constant spoof is detectable and not - // worth the broken navigation UX. - - // Add fonts - if !self.custom_fonts_only { - let system_fonts = fonts::get_fonts_for_os(target_os); - let fonts = if let Some(custom) = &self.custom_fonts { - let mut all_fonts = system_fonts; - for font in custom { - if !all_fonts.contains(font) { - all_fonts.push(font.clone()); - } - } - all_fonts - } else { - system_fonts - }; - config.insert("fonts".to_string(), serde_json::json!(fonts)); - } else if let Some(custom) = &self.custom_fonts { - config.insert("fonts".to_string(), serde_json::json!(custom)); - } - - // Add font spacing seed - config.insert( - "fonts:spacing_seed".to_string(), - serde_json::json!(rng.random_range(0..1_073_741_824u32)), - ); - - // Build Firefox preferences - let mut firefox_prefs = self.firefox_prefs; - - if self.block_images { - firefox_prefs.insert( - "permissions.default.image".to_string(), - serde_json::json!(2), - ); - } - - if self.block_webrtc { - firefox_prefs.insert( - "media.peerconnection.enabled".to_string(), - serde_json::json!(false), - ); - } - - if self.block_webgl { - firefox_prefs.insert("webgl.disabled".to_string(), serde_json::json!(true)); - } else { - // Sample and add WebGL configuration - match webgl::sample_webgl(target_os, None, None) { - Ok(webgl_data) => { - for (key, value) in webgl_data.config { - config.insert(key, value); - } - firefox_prefs.insert("webgl.force-enabled".to_string(), serde_json::json!(true)); - } - Err(e) => { - log::warn!("Failed to sample WebGL config: {}", e); - } - } - } - - // Canvas anti-fingerprinting - config.insert( - "canvas:aaOffset".to_string(), - serde_json::json!(rng.random_range(-50..=50)), - ); - config.insert("canvas:aaCapOffset".to_string(), serde_json::json!(true)); - - // Add extra config (user-provided) - for (key, value) in self.extra_config { - config.insert(key, value); - } - - // Hardcoded Camoufox settings (cannot be overridden) - // Disable theming to prevent fingerprinting via browser theme - config.insert("disableTheming".to_string(), serde_json::json!(true)); - // Hide cursor in headless mode - config.insert("showcursor".to_string(), serde_json::json!(false)); - - Ok(CamoufoxLaunchConfig { - fingerprint_config: config, - firefox_prefs, - proxy: self.proxy, - headless: self.headless, - target_os: target_os.to_string(), - }) - } - - /// Build the complete Camoufox launch configuration with async geolocation support. - /// This method should be used when geoip option is set to Auto. - pub async fn build_async(self) -> Result { - // Get full proxy URL (with credentials) for IP detection - let proxy_url = self.proxy.as_ref().map(|p| { - if let (Some(user), Some(pass)) = (&p.username, &p.password) { - // Reconstruct URL with credentials: scheme://user:pass@host:port - if let Ok(mut parsed) = url::Url::parse(&p.server) { - let _ = parsed.set_username(user); - let _ = parsed.set_password(Some(pass)); - parsed.to_string() - } else { - p.server.clone() - } - } else if let Some(user) = &p.username { - if let Ok(mut parsed) = url::Url::parse(&p.server) { - let _ = parsed.set_username(user); - parsed.to_string() - } else { - p.server.clone() - } - } else { - p.server.clone() - } - }); - let geoip_option = self.geoip.clone(); - let block_webrtc = self.block_webrtc; - - // Build base config first - let mut launch_config = self.build()?; - - // Handle geolocation if geoip option is set - if let Some(geoip) = geoip_option { - let ip = match geoip { - GeoIPOption::Auto => { - // Fetch public IP, optionally through proxy - geolocation::fetch_public_ip(proxy_url.as_deref()) - .await - .map_err(geolocation::GeolocationError::from)? - } - GeoIPOption::IP(ip_str) => { - if !geolocation::validate_ip(&ip_str) { - return Err(ConfigError::Geolocation( - geolocation::GeolocationError::InvalidIP(ip_str), - )); - } - ip_str - } - }; - - // Get geolocation from IP - match geolocation::get_geolocation(&ip) { - Ok(geo) => { - // Add geolocation config - for (key, value) in geo.as_config() { - launch_config.fingerprint_config.insert(key, value); - } - - // Add WebRTC IP spoofing if not blocked - if !block_webrtc { - if geolocation::is_ipv4(&ip) { - launch_config - .fingerprint_config - .insert("webrtc:ipv4".to_string(), serde_json::json!(ip)); - } else if geolocation::is_ipv6(&ip) { - launch_config - .fingerprint_config - .insert("webrtc:ipv6".to_string(), serde_json::json!(ip)); - } - } - - log::info!( - "Applied geolocation from IP {}: {} ({})", - ip, - geo.locale.as_string(), - geo.timezone - ); - } - Err(e) => { - log::warn!("Failed to get geolocation for IP {}: {}", ip, e); - // Continue without geolocation rather than failing - } - } - } - - Ok(launch_config) - } -} - -/// Complete Camoufox launch configuration. -#[derive(Debug, Clone)] -pub struct CamoufoxLaunchConfig { - pub fingerprint_config: HashMap, - pub firefox_prefs: HashMap, - pub proxy: Option, - pub headless: bool, - pub target_os: String, -} - -impl CamoufoxLaunchConfig { - /// Get environment variables for launching Camoufox. - pub fn get_env_vars(&self) -> Result, serde_json::Error> { - env_vars::config_to_env_vars(&self.fingerprint_config) - } - - /// Get the config as JSON string. - pub fn config_json(&self) -> Result { - serde_json::to_string(&self.fingerprint_config) - } -} - -/// Error type for configuration operations. -#[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("Fingerprint generation error: {0}")] - Fingerprint(#[from] crate::camoufox::fingerprint::FingerprintError), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), - - #[error("WebGL error: {0}")] - WebGL(#[from] webgl::WebGLError), - - #[error("Invalid proxy configuration: {0}")] - InvalidProxy(String), - - #[error("Geolocation error: {0}")] - Geolocation(#[from] crate::camoufox::geolocation::GeolocationError), -} - -/// Get Firefox version from executable path. -pub fn get_firefox_version(executable_path: &Path) -> Option { - // Try to read version.json from the same directory - let version_path = executable_path.parent()?.join("version.json"); - - if let Ok(content) = std::fs::read_to_string(&version_path) { - if let Ok(json) = serde_json::from_str::(&content) { - if let Some(version_str) = json.get("version").and_then(|v| v.as_str()) { - // Parse major version from "135.0" or similar - let major: u32 = version_str.split('.').next()?.parse().ok()?; - return Some(major); - } - } - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_config_builder() { - let config = CamoufoxConfigBuilder::new() - .operating_system("windows") - .block_images(true) - .build(); - - assert!(config.is_ok()); - let config = config.unwrap(); - assert!(config - .firefox_prefs - .contains_key("permissions.default.image")); - } - - #[test] - fn test_replace_ff_version() { - let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"; - let replaced = replace_ff_version(ua, 140); - assert!(replaced.contains("140.0")); - } - - #[test] - fn test_from_browserforge() { - let fingerprint = Fingerprint { - screen: ScreenFingerprint { - width: 1920, - height: 1080, - avail_width: 1920, - avail_height: 1040, - color_depth: 24, - pixel_depth: 24, - inner_width: 1903, - inner_height: 969, - outer_width: 1920, - outer_height: 1040, - ..Default::default() - }, - navigator: NavigatorFingerprint { - user_agent: "Mozilla/5.0 Firefox/135.0".to_string(), - platform: "Win32".to_string(), - language: "en-US".to_string(), - languages: vec!["en-US".to_string()], - hardware_concurrency: 8, - ..Default::default() - }, - ..Default::default() - }; - - let config = from_browserforge(&fingerprint, Some(140)); - - assert!(config.contains_key("navigator.userAgent")); - assert!(config.contains_key("screen.width")); - } -} diff --git a/src-tauri/src/camoufox/data/browser-helper-file.json b/src-tauri/src/camoufox/data/browser-helper-file.json deleted file mode 100644 index 49c114a..0000000 --- a/src-tauri/src/camoufox/data/browser-helper-file.json +++ /dev/null @@ -1,120 +0,0 @@ -[ - "chrome/143.0.0.0|2", - "safari/18.3.1|2", - "chrome/101.0.4951.54|2", - "chrome/139.0.0.0|2", - "safari/16.6|2", - "safari/26.2|2", - "safari/18.6|2", - "safari/26.1|2", - "chrome/142.0.0.0|2", - "chrome/141.0.0.0|2", - "safari/18.7.3|2", - "edge/143.0.0.0|2", - "safari/18.4|2", - "safari/17.3.1|2", - "chrome/135.0.0.0|2", - "safari/18.5|2", - "safari/18.7.2|2", - "chrome/143.0.0.0|1", - "chrome/128.0.0.0|2", - "chrome/131.0.0.0|2", - "safari/26.3|2", - "safari/26.0.1|2", - "chrome/114.0.0.0|2", - "safari/18.1|2", - "firefox/147.0|2", - "safari/17.5|2", - "chrome/140.0.0.0|2", - "safari/16.6.1|2", - "firefox/146.0|2", - "chrome/124.0.0.0|1", - "chrome/34.0.1847.114|2", - "chrome/130.0.0.0|2", - "safari/15.6.7|2", - "chrome/144.0.0.0|2", - "safari/18.3|2", - "safari/16.4|2", - "chrome/141.0.7390.122|1", - "firefox/140.0|2", - "chrome/138.0.0.0|2", - "firefox/135.0|2", - "safari/17.6|2", - "chrome/132.0.0.0|2", - "chrome/109.0.0.0|2", - "chrome/92.0.4515.131|2", - "chrome/136.0.0.0|2", - "edge/142.0.0.0|2", - "chrome/125.0.0.0|2", - "safari/17.8|2", - "edge/143.0.0.0|1", - "chrome/123.0.0.0|2", - "chrome/137.0.0.0|2", - "chrome/129.0.0.0|2", - "chrome/126.0.0.0|2", - "safari/26.0|2", - "chrome/133.0.0.0|2", - "chrome/119.0.0.0|2", - "chrome/145.0.0.0|2", - "firefox/145.0|2", - "safari/17.3|2", - "safari/18.2|2", - "safari/16.5.2|2", - "safari/17.4|2", - "chrome/120.0.0.0|2", - "chrome/116.0.0.0|2", - "firefox/141.0|2", - "safari/17.4.1|2", - "chrome/134.0.0.0|2", - "safari/15.4|2", - "safari/18.1.1|2", - "edge/144.0.0.0|2", - "firefox/144.0|2", - "safari/16.3|2", - "safari/13.0.3|2", - "chrome/131.0.6778.33|2", - "edge/145.0.0.0|2", - "edge/139.0.0.0|2", - "safari/17.1|2", - "chrome/133.0.0.0|1", - "chrome/121.0.0.0|2", - "chrome/124.0.0.0|2", - "chrome/127.0.0.0|2", - "chrome/122.0.6261.95|2", - "chrome/91.0.4450.0|2", - "edge/134.0.0.0|2", - "chrome/134.0.6998.179|2", - "chrome/122.0.0.0|2", - "firefox/128.0|2", - "chrome/142.0.0.0|1", - "safari/18.7|2", - "safari/17.8.1|2", - "firefox/115.0|2", - "safari/17.2|2", - "chrome/117.0.0.0|2", - "safari/18.0.1|2", - "chrome/139.0.7258.5|2", - "edge/140.0.0.0|2", - "safari/16.5|2", - "safari/18.6.2|2", - "firefox/136.0|2", - "safari/17.2.1|2", - "safari/18.0|2", - "safari/15.6.1|2", - "safari/26.2|1", - "safari/17.1.2|2", - "safari/17.7|2", - "safari/16.2|2", - "edge/122.0.0.0|2", - "chrome/139.0.0.0|1", - "safari/17.0|2", - "firefox/139.0|2", - "chrome/101.0.9316.173|2", - "chrome/101.0.4951.64|2", - "chrome/141.0.0.0|1", - "safari/15.5|2", - "safari/18.6|1", - "chrome/112.0.0.0|2", - "edge/135.0.0.0|2", - "chrome/140.0.0.0|1" -] diff --git a/src-tauri/src/camoufox/data/browserforge.yml b/src-tauri/src/camoufox/data/browserforge.yml deleted file mode 100644 index 7f7df90..0000000 --- a/src-tauri/src/camoufox/data/browserforge.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Mappings of Browserforge fingerprints to Camoufox config properties. - -navigator: - # Note: Browserforge tends to have outdated UAs. - # The version will be replaced in Camoufox. - userAgent: navigator.userAgent - # userAgentData not in Firefox - doNotTrack: navigator.doNotTrack - appCodeName: navigator.appCodeName - appName: navigator.appName - appVersion: navigator.appVersion - oscpu: navigator.oscpu - # webdriver is always True - # Locale is now implemented separately: - # language: navigator.language - # languages: navigator.languages - platform: navigator.platform - # deviceMemory not in Firefox - hardwareConcurrency: navigator.hardwareConcurrency - product: navigator.product - # Never override productSub #105 - # productSub: navigator.productSub - # vendor is not necessary - # vendorSub is not necessary - maxTouchPoints: navigator.maxTouchPoints - extraProperties: - # Note: Changing pdfViewerEnabled is not recommended. This will be kept to True. - globalPrivacyControl: navigator.globalPrivacyControl - -screen: - # hasHDR is not implemented in Camoufox - availLeft: screen.availLeft - availTop: screen.availTop - availWidth: screen.availWidth - availHeight: screen.availHeight - height: screen.height - width: screen.width - colorDepth: screen.colorDepth - pixelDepth: screen.pixelDepth - # devicePixelRatio is not recommended. Any value other than 1.0 is suspicious. - pageXOffset: screen.pageXOffset - pageYOffset: screen.pageYOffset - outerHeight: window.outerHeight - outerWidth: window.outerWidth - innerHeight: window.innerHeight - innerWidth: window.innerWidth - screenX: window.screenX - screenY: window.screenY - # Tends to generate out of bounds (network inconsistencies): - # clientWidth: document.body.clientWidth - # clientHeight: document.body.clientHeight - -# videoCard: -# renderer: webgl:renderer -# vendor: webgl:vendor - -headers: - # headers.User-Agent is redundant with navigator.userAgent - # headers.Accept-Language is redundant with locale:* - Accept-Encoding: headers.Accept-Encoding - -battery: - charging: battery:charging - chargingTime: battery:chargingTime - dischargingTime: battery:dischargingTime - -# Unsupported: videoCodecs, audioCodecs, pluginsData, multimediaDevices -# Fonts are listed through the launcher. diff --git a/src-tauri/src/camoufox/data/fingerprint-network-definition.zip b/src-tauri/src/camoufox/data/fingerprint-network-definition.zip deleted file mode 100644 index 06000f9..0000000 Binary files a/src-tauri/src/camoufox/data/fingerprint-network-definition.zip and /dev/null differ diff --git a/src-tauri/src/camoufox/data/fingerprint-presets-v135.json b/src-tauri/src/camoufox/data/fingerprint-presets-v135.json deleted file mode 100644 index 004d323..0000000 --- a/src-tauri/src/camoufox/data/fingerprint-presets-v135.json +++ /dev/null @@ -1,6029 +0,0 @@ -{ - "version": 1, - "generated_at": "2026-03-11T00:45:14.521Z", - "presets": { - "macos": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (U.K.)):en-GB:local", - "Eddy (English (U.S.)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (U.K.)):en-GB:local", - "Flo (English (U.S.)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (U.K.)):en-GB:local", - "Grandma (English (U.S.)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (U.K.)):en-GB:local", - "Grandpa (English (U.S.)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (U.K.)):en-GB:local", - "Reed (English (U.S.)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (U.K.)):en-GB:local", - "Rocko (English (U.S.)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (U.K.)):en-GB:local", - "Sandy (English (U.S.)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (U.K.)):en-GB:local", - "Shelley (English (U.S.)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1170 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 985 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 30, - "availWidth": 2048, - "availHeight": 1045, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (U.K.)):en-GB:local", - "Eddy (English (U.S.)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (U.K.)):en-GB:local", - "Flo (English (U.S.)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (U.K.)):en-GB:local", - "Grandma (English (U.S.)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (U.K.)):en-GB:local", - "Grandpa (English (U.S.)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (U.K.)):en-GB:local", - "Reed (English (U.S.)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (U.K.)):en-GB:local", - "Rocko (English (U.S.)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (U.K.)):en-GB:local", - "Sandy (English (U.S.)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (U.K.)):en-GB:local", - "Shelley (English (U.S.)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:109.0) Gecko/20100101 Firefox/115.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 714 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:146.0) Gecko/20100101 Firefox/146.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1800, - "height": 1169, - "colorDepth": 24, - "availWidth": 1800, - "availHeight": 1169, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 832, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 746, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1470, - "height": 956, - "colorDepth": 30, - "availWidth": 1470, - "availHeight": 923, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1011 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 480, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "MacIntel", - "hardwareConcurrency": 14, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1512, - "height": 982, - "colorDepth": 30, - "availWidth": 1512, - "availHeight": 886, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1050 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1470, - "height": 956, - "colorDepth": 30, - "availWidth": 1470, - "availHeight": 923, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1710, - "height": 1107, - "colorDepth": 30, - "availWidth": 1710, - "availHeight": 1068, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1440 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1055, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 827, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "MacIntel", - "hardwareConcurrency": 14, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1355, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 987 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1440 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1330, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1415 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 30, - "availWidth": 1680, - "availHeight": 947, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 870, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1792, - "height": 1120, - "colorDepth": 30, - "availWidth": 1792, - "availHeight": 1090, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1470, - "height": 956, - "colorDepth": 30, - "availWidth": 1470, - "availHeight": 924, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1170, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "MacIntel", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 1117, - "colorDepth": 30, - "availWidth": 1728, - "availHeight": 1084, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - } - ], - "windows": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1112, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 14, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 912, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1392 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 5 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 912, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1160 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1248, - "height": 702, - "colorDepth": 24, - "availWidth": 1248, - "availHeight": 663, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1632, - "height": 918, - "colorDepth": 24, - "availWidth": 1632, - "availHeight": 877, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 972, - "colorDepth": 24, - "availWidth": 1728, - "availHeight": 936 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Asaf - Hebrew (Israel):he-IL:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1024, - "height": 768, - "colorDepth": 24, - "availWidth": 1024, - "availHeight": 728 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Win32", - "hardwareConcurrency": 24, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 860 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Jakub - Czech (Czech Republic):cs-CZ:local", - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Stefanos - Greek (Greece):el-GR:local", - "Microsoft Naayf - Arabic (Saudi):ar-SA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Cosimo - Italian (Italy):it-IT:local", - "Microsoft Elsa - Italian (Italy):it-IT:local", - "Microsoft Frank - Dutch (Netherlands):nl-NL:local", - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Helia - Portuguese (Portugal):pt-PT:local", - "Microsoft Tolga - Turkish (Turkey):tr-TR:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Elsa Desktop - Italian (Italy):it-IT:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1504, - "height": 1003, - "colorDepth": 24, - "availWidth": 1504, - "availHeight": 955, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Huihui - Chinese (Simplified, PRC):zh-CN:local", - "Microsoft Kangkang - Chinese (Simplified, PRC):zh-CN:local", - "Microsoft Yaoyao - Chinese (Simplified, PRC):zh-CN:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Huihui Desktop - Chinese (Simplified):zh-CN:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel 945GM Direct3D11 vs_4_0 ps_4_0)" - }, - "speechVoices": ["Microsoft Anna - English (United States):en-US:local"] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 760, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1392, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1400, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 900 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 875, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 875, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Mozilla", - "unmaskedRenderer": "Mozilla" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 860 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1010 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_4_0 ps_4_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft James - English (Australia):en-AU:local", - "Microsoft Linda - English (Canada):en-CA:local", - "Microsoft Richard - English (Canada):en-CA:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Sean - English (Ireland):en-IE:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft Catherine - English (Australia):en-AU:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft George - English (Great Britain):en-GB:local", - "Microsoft Sarah Mobile - English (Great Britain):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 680, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1104, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1400, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1696, - "height": 954, - "colorDepth": 24, - "availWidth": 1696, - "availHeight": 910, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1963, - "height": 1104, - "colorDepth": 24, - "availWidth": 1963, - "availHeight": 1067, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2400, - "height": 1200, - "colorDepth": 24, - "availWidth": 2400, - "availHeight": 1200, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Mozilla", - "unmaskedRenderer": "Mozilla" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2293, - "height": 960, - "colorDepth": 24, - "availWidth": 2293, - "availHeight": 928, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 680, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1080, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1032, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 1 - }, - "screen": { - "width": 2400, - "height": 1350, - "colorDepth": 24, - "availWidth": 2400, - "availHeight": 1290 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 860 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1184, - "height": 666, - "colorDepth": 24, - "availWidth": 1184, - "availHeight": 636, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1382, - "height": 864, - "colorDepth": 24, - "availWidth": 1382, - "availHeight": 816, - "devicePixelRatio": 2.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2432, - "height": 1368, - "colorDepth": 24, - "availWidth": 2432, - "availHeight": 1307, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1360, - "height": 768, - "colorDepth": 24, - "availWidth": 1360, - "availHeight": 720 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 680, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Adam - Polish (Poland):pl-PL:local", - "Microsoft Paulina - Polish (Poland):pl-PL:local", - "Microsoft Paulina Desktop - Polish:pl-PL:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Adam - Polish (Poland):pl-PL:local", - "Microsoft Paulina - Polish (Poland):pl-PL:local", - "Microsoft Paulina Desktop - Polish:pl-PL:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Naayf - Arabic (Saudi):ar-SA:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Frank - Dutch (Netherlands):nl-NL:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Frank - Dutch (Netherlands):nl-NL:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - } - ], - "linux": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1368, - "height": 912, - "colorDepth": 24, - "availWidth": 1368, - "availHeight": 912, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1531, - "height": 957, - "colorDepth": 24, - "availWidth": 1531, - "availHeight": 957, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 5120, - "height": 1440, - "colorDepth": 24, - "availWidth": 5120, - "availHeight": 1440 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - }, - "speechVoices": ["dummy:en:remote"] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1854, - "availHeight": 1048 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 760, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1010 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "GeForce GTX 480, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 685 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 735 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 1152, - "colorDepth": 24, - "availWidth": 1728, - "availHeight": 1152, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1408 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - } - ] - } -} diff --git a/src-tauri/src/camoufox/data/fingerprint-presets-v150.json b/src-tauri/src/camoufox/data/fingerprint-presets-v150.json deleted file mode 100644 index ce31510..0000000 --- a/src-tauri/src/camoufox/data/fingerprint-presets-v150.json +++ /dev/null @@ -1,16511 +0,0 @@ -{ - "version": 1, - "generated_at": "2026-05-11T18:23:31.169717Z", - "source": "roverfox_fingerprints", - "min_firefox_version": 149, - "presets": { - "macos": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 720, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 800, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2240, - "height": 1260, - "colorDepth": 30, - "availWidth": 2240, - "availHeight": 1230, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1849, - "availHeight": 1050, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3200, - "height": 1800, - "colorDepth": 30, - "availWidth": 3200, - "availHeight": 1680, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 832, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 726, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 736, - "height": 414, - "colorDepth": 30, - "availWidth": 736, - "availHeight": 414, - "devicePixelRatio": 2.60869565217391 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2056, - "height": 1329, - "colorDepth": 30, - "availWidth": 2056, - "availHeight": 1253, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1410, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3360, - "height": 1890, - "colorDepth": 30, - "availWidth": 3360, - "availHeight": 1865, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 1117, - "colorDepth": 30, - "availWidth": 1728, - "availHeight": 1010, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1344, - "height": 840, - "colorDepth": 30, - "availWidth": 1344, - "availHeight": 764, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1170, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 30, - "availWidth": 2048, - "availHeight": 1122, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 30, - "availWidth": 3440, - "availHeight": 1364, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1360, - "height": 768, - "colorDepth": 30, - "availWidth": 1360, - "availHeight": 676, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 18, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 2160, - "colorDepth": 30, - "availWidth": 3840, - "availHeight": 2052, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1710, - "height": 1107, - "colorDepth": 30, - "availWidth": 1710, - "availHeight": 1031, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 715, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1710, - "height": 1107, - "colorDepth": 30, - "availWidth": 1710, - "availHeight": 1074, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 730, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 2560, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 2484, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Brutte notizie:en-US:local", - "Bahh:en-US:local", - "Campane:en-US:local", - "Boing:en-US:local", - "Bollicine:en-US:local", - "Carmit:he-IL:local", - "Violoncelli:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Tremolio:en-US:local", - "Eddy (Tedesco (Germania)):de-DE:local", - "Eddy (Inglese (UK)):en-GB:local", - "Eddy (Inglese (USA)):en-US:local", - "Eddy (Spagnolo (Spagna)):es-ES:local", - "Eddy (Spagnolo (Messico)):es-MX:local", - "Eddy (Finlandese (Finlandia)):fi-FI:local", - "Eddy (Francese (Canada)):fr-CA:local", - "Eddy (Francese (Francia)):fr-FR:local", - "Eddy (Italiano (Italia)):it-IT:local", - "Eddy (Giapponese (Giappone)):ja-JP:local", - "Eddy (Coreano (Corea del Sud)):ko-KR:local", - "Eddy (Portoghese (Brasile)):pt-BR:local", - "Eddy (Cinese (Cina continentale)):zh-CN:local", - "Eddy (Cinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (Tedesco (Germania)):de-DE:local", - "Flo (Inglese (UK)):en-GB:local", - "Flo (Inglese (USA)):en-US:local", - "Flo (Spagnolo (Spagna)):es-ES:local", - "Flo (Spagnolo (Messico)):es-MX:local", - "Flo (Finlandese (Finlandia)):fi-FI:local", - "Flo (Francese (Canada)):fr-CA:local", - "Flo (Francese (Francia)):fr-FR:local", - "Flo (Italiano (Italia)):it-IT:local", - "Flo (Giapponese (Giappone)):ja-JP:local", - "Flo (Coreano (Corea del Sud)):ko-KR:local", - "Flo (Portoghese (Brasile)):pt-BR:local", - "Flo (Cinese (Cina continentale)):zh-CN:local", - "Flo (Cinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Buone notizie:en-US:local", - "Grandma (Tedesco (Germania)):de-DE:local", - "Grandma (Inglese (UK)):en-GB:local", - "Grandma (Inglese (USA)):en-US:local", - "Grandma (Spagnolo (Spagna)):es-ES:local", - "Grandma (Spagnolo (Messico)):es-MX:local", - "Grandma (Finlandese (Finlandia)):fi-FI:local", - "Grandma (Francese (Canada)):fr-CA:local", - "Grandma (Francese (Francia)):fr-FR:local", - "Grandma (Italiano (Italia)):it-IT:local", - "Grandma (Giapponese (Giappone)):ja-JP:local", - "Grandma (Coreano (Corea del Sud)):ko-KR:local", - "Grandma (Portoghese (Brasile)):pt-BR:local", - "Grandma (Cinese (Cina continentale)):zh-CN:local", - "Grandma (Cinese (Taiwan)):zh-TW:local", - "Grandpa (Tedesco (Germania)):de-DE:local", - "Grandpa (Inglese (UK)):en-GB:local", - "Grandpa (Inglese (USA)):en-US:local", - "Grandpa (Spagnolo (Spagna)):es-ES:local", - "Grandpa (Spagnolo (Messico)):es-MX:local", - "Grandpa (Finlandese (Finlandia)):fi-FI:local", - "Grandpa (Francese (Canada)):fr-CA:local", - "Grandpa (Francese (Francia)):fr-FR:local", - "Grandpa (Italiano (Italia)):it-IT:local", - "Grandpa (Giapponese (Giappone)):ja-JP:local", - "Grandpa (Coreano (Corea del Sud)):ko-KR:local", - "Grandpa (Portoghese (Brasile)):pt-BR:local", - "Grandpa (Cinese (Cina continentale)):zh-CN:local", - "Grandpa (Cinese (Taiwan)):zh-TW:local", - "Giullare:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organo:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (Tedesco (Germania)):de-DE:local", - "Reed (Inglese (UK)):en-GB:local", - "Reed (Inglese (USA)):en-US:local", - "Reed (Spagnolo (Spagna)):es-ES:local", - "Reed (Spagnolo (Messico)):es-MX:local", - "Reed (Finlandese (Finlandia)):fi-FI:local", - "Reed (Francese (Canada)):fr-CA:local", - "Reed (Italiano (Italia)):it-IT:local", - "Reed (Giapponese (Giappone)):ja-JP:local", - "Reed (Coreano (Corea del Sud)):ko-KR:local", - "Reed (Portoghese (Brasile)):pt-BR:local", - "Reed (Cinese (Cina continentale)):zh-CN:local", - "Reed (Cinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (Tedesco (Germania)):de-DE:local", - "Rocko (Inglese (UK)):en-GB:local", - "Rocko (Inglese (USA)):en-US:local", - "Rocko (Spagnolo (Spagna)):es-ES:local", - "Rocko (Spagnolo (Messico)):es-MX:local", - "Rocko (Finlandese (Finlandia)):fi-FI:local", - "Rocko (Francese (Canada)):fr-CA:local", - "Rocko (Francese (Francia)):fr-FR:local", - "Rocko (Italiano (Italia)):it-IT:local", - "Rocko (Giapponese (Giappone)):ja-JP:local", - "Rocko (Coreano (Corea del Sud)):ko-KR:local", - "Rocko (Portoghese (Brasile)):pt-BR:local", - "Rocko (Cinese (Cina continentale)):zh-CN:local", - "Rocko (Cinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (Tedesco (Germania)):de-DE:local", - "Sandy (Inglese (UK)):en-GB:local", - "Sandy (Inglese (USA)):en-US:local", - "Sandy (Spagnolo (Spagna)):es-ES:local", - "Sandy (Spagnolo (Messico)):es-MX:local", - "Sandy (Finlandese (Finlandia)):fi-FI:local", - "Sandy (Francese (Canada)):fr-CA:local", - "Sandy (Francese (Francia)):fr-FR:local", - "Sandy (Italiano (Italia)):it-IT:local", - "Sandy (Giapponese (Giappone)):ja-JP:local", - "Sandy (Coreano (Corea del Sud)):ko-KR:local", - "Sandy (Portoghese (Brasile)):pt-BR:local", - "Sandy (Cinese (Cina continentale)):zh-CN:local", - "Sandy (Cinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (Tedesco (Germania)):de-DE:local", - "Shelley (Inglese (UK)):en-GB:local", - "Shelley (Inglese (USA)):en-US:local", - "Shelley (Spagnolo (Spagna)):es-ES:local", - "Shelley (Spagnolo (Messico)):es-MX:local", - "Shelley (Finlandese (Finlandia)):fi-FI:local", - "Shelley (Francese (Canada)):fr-CA:local", - "Shelley (Francese (Francia)):fr-FR:local", - "Shelley (Italiano (Italia)):it-IT:local", - "Shelley (Giapponese (Giappone)):ja-JP:local", - "Shelley (Coreano (Corea del Sud)):ko-KR:local", - "Shelley (Portoghese (Brasile)):pt-BR:local", - "Shelley (Cinese (Cina continentale)):zh-CN:local", - "Shelley (Cinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Ting-Ting:zh-CN:local", - "Trinoid:en-US:local", - "Vani:ta-IN:local", - "Sussurro:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 870, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1792, - "height": 1120, - "colorDepth": 30, - "availWidth": 1792, - "availHeight": 1010, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 960, - "height": 540, - "colorDepth": 30, - "availWidth": 960, - "availHeight": 528, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 30, - "availWidth": 2048, - "availHeight": 1033, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 800, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel 945GM, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1080, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1080, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1512, - "height": 982, - "colorDepth": 30, - "availWidth": 1512, - "availHeight": 893, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1470, - "height": 956, - "colorDepth": 30, - "availWidth": 1470, - "availHeight": 880, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 18, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1410, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Ellen:nl-BE:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1055, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Malas noticias:en-US:local", - "Bahh:en-US:local", - "Campanas:en-US:local", - "Boing:en-US:local", - "Burbujas:en-US:local", - "Carmit:he-IL:local", - "Violonchelos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (Alemán (Alemania)):de-DE:local", - "Eddy (Inglés (RU)):en-GB:local", - "Eddy (Inglés (EE. UU.)):en-US:local", - "Eddy (Español (España)):es-ES:local", - "Eddy (Español (México)):es-MX:local", - "Eddy (Finés (Finlandia)):fi-FI:local", - "Eddy (Francés (Canadá)):fr-CA:local", - "Eddy (Francés (Francia)):fr-FR:local", - "Eddy (Italiano (Italia)):it-IT:local", - "Eddy (Portugués (Brasil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (Alemán (Alemania)):de-DE:local", - "Flo (Inglés (RU)):en-GB:local", - "Flo (Inglés (EE. UU.)):en-US:local", - "Flo (Español (España)):es-ES:local", - "Flo (Español (México)):es-MX:local", - "Flo (Finés (Finlandia)):fi-FI:local", - "Flo (Francés (Canadá)):fr-CA:local", - "Flo (Francés (Francia)):fr-FR:local", - "Flo (Italiano (Italia)):it-IT:local", - "Flo (Portugués (Brasil)):pt-BR:local", - "Fred:en-US:local", - "Buenas noticias:en-US:local", - "Grandma (Alemán (Alemania)):de-DE:local", - "Grandma (Inglés (RU)):en-GB:local", - "Grandma (Inglés (EE. UU.)):en-US:local", - "Grandma (Español (España)):es-ES:local", - "Grandma (Español (México)):es-MX:local", - "Grandma (Finés (Finlandia)):fi-FI:local", - "Grandma (Francés (Canadá)):fr-CA:local", - "Grandma (Francés (Francia)):fr-FR:local", - "Grandma (Italiano (Italia)):it-IT:local", - "Grandma (Portugués (Brasil)):pt-BR:local", - "Grandpa (Alemán (Alemania)):de-DE:local", - "Grandpa (Inglés (RU)):en-GB:local", - "Grandpa (Inglés (EE. UU.)):en-US:local", - "Grandpa (Español (España)):es-ES:local", - "Grandpa (Español (México)):es-MX:local", - "Grandpa (Finés (Finlandia)):fi-FI:local", - "Grandpa (Francés (Canadá)):fr-CA:local", - "Grandpa (Francés (Francia)):fr-FR:local", - "Grandpa (Italiano (Italia)):it-IT:local", - "Grandpa (Portugués (Brasil)):pt-BR:local", - "Bufón:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Órgano:en-US:local", - "Paulina:es-MX:local", - "Superestrella:en-US:local", - "Ralph:en-US:local", - "Reed (Alemán (Alemania)):de-DE:local", - "Reed (Inglés (RU)):en-GB:local", - "Reed (Inglés (EE. UU.)):en-US:local", - "Reed (Español (España)):es-ES:local", - "Reed (Español (México)):es-MX:local", - "Reed (Finés (Finlandia)):fi-FI:local", - "Reed (Francés (Canadá)):fr-CA:local", - "Reed (Italiano (Italia)):it-IT:local", - "Reed (Portugués (Brasil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (Alemán (Alemania)):de-DE:local", - "Rocko (Inglés (RU)):en-GB:local", - "Rocko (Inglés (EE. UU.)):en-US:local", - "Rocko (Español (España)):es-ES:local", - "Rocko (Español (México)):es-MX:local", - "Rocko (Finés (Finlandia)):fi-FI:local", - "Rocko (Francés (Canadá)):fr-CA:local", - "Rocko (Francés (Francia)):fr-FR:local", - "Rocko (Italiano (Italia)):it-IT:local", - "Rocko (Portugués (Brasil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (Alemán (Alemania)):de-DE:local", - "Sandy (Inglés (RU)):en-GB:local", - "Sandy (Inglés (EE. UU.)):en-US:local", - "Sandy (Español (España)):es-ES:local", - "Sandy (Español (México)):es-MX:local", - "Sandy (Finés (Finlandia)):fi-FI:local", - "Sandy (Francés (Canadá)):fr-CA:local", - "Sandy (Francés (Francia)):fr-FR:local", - "Sandy (Italiano (Italia)):it-IT:local", - "Sandy (Portugués (Brasil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (Alemán (Alemania)):de-DE:local", - "Shelley (Inglés (RU)):en-GB:local", - "Shelley (Inglés (EE. UU.)):en-US:local", - "Shelley (Español (España)):es-ES:local", - "Shelley (Español (México)):es-MX:local", - "Shelley (Finés (Finlandia)):fi-FI:local", - "Shelley (Francés (Canadá)):fr-CA:local", - "Shelley (Francés (Francia)):fr-FR:local", - "Shelley (Italiano (Italia)):it-IT:local", - "Shelley (Portugués (Brasil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Susurro:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1600, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1480, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 1117, - "colorDepth": 30, - "availWidth": 1728, - "availHeight": 1083, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 832, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 803, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Malas noticias:en-US:local", - "Bahh:en-US:local", - "Campanas:en-US:local", - "Boing:en-US:local", - "Burbujas:en-US:local", - "Carmit:he-IL:local", - "Violonchelos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (Alemán (Alemania)):de-DE:local", - "Eddy (Inglés (RU)):en-GB:local", - "Eddy (Inglés (EE. UU.)):en-US:local", - "Eddy (Español (España)):es-ES:local", - "Eddy (Español (México)):es-MX:local", - "Eddy (Finés (Finlandia)):fi-FI:local", - "Eddy (Francés (Canadá)):fr-CA:local", - "Eddy (Francés (Francia)):fr-FR:local", - "Eddy (Italiano (Italia)):it-IT:local", - "Eddy (Japonés (Japón)):ja-JP:local", - "Eddy (Coreano (Corea del Sur)):ko-KR:local", - "Eddy (Portugués (Brasil)):pt-BR:local", - "Eddy (Chino (China continental)):zh-CN:local", - "Eddy (Chino (Taiwán)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (Alemán (Alemania)):de-DE:local", - "Flo (Inglés (RU)):en-GB:local", - "Flo (Inglés (EE. UU.)):en-US:local", - "Flo (Español (España)):es-ES:local", - "Flo (Español (México)):es-MX:local", - "Flo (Finés (Finlandia)):fi-FI:local", - "Flo (Francés (Canadá)):fr-CA:local", - "Flo (Francés (Francia)):fr-FR:local", - "Flo (Italiano (Italia)):it-IT:local", - "Flo (Japonés (Japón)):ja-JP:local", - "Flo (Coreano (Corea del Sur)):ko-KR:local", - "Flo (Portugués (Brasil)):pt-BR:local", - "Flo (Chino (China continental)):zh-CN:local", - "Flo (Chino (Taiwán)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Buenas noticias:en-US:local", - "Grandma (Alemán (Alemania)):de-DE:local", - "Grandma (Inglés (RU)):en-GB:local", - "Grandma (Inglés (EE. UU.)):en-US:local", - "Grandma (Español (España)):es-ES:local", - "Grandma (Español (México)):es-MX:local", - "Grandma (Finés (Finlandia)):fi-FI:local", - "Grandma (Francés (Canadá)):fr-CA:local", - "Grandma (Francés (Francia)):fr-FR:local", - "Grandma (Italiano (Italia)):it-IT:local", - "Grandma (Japonés (Japón)):ja-JP:local", - "Grandma (Coreano (Corea del Sur)):ko-KR:local", - "Grandma (Portugués (Brasil)):pt-BR:local", - "Grandma (Chino (China continental)):zh-CN:local", - "Grandma (Chino (Taiwán)):zh-TW:local", - "Grandpa (Alemán (Alemania)):de-DE:local", - "Grandpa (Inglés (RU)):en-GB:local", - "Grandpa (Inglés (EE. UU.)):en-US:local", - "Grandpa (Español (España)):es-ES:local", - "Grandpa (Español (México)):es-MX:local", - "Grandpa (Finés (Finlandia)):fi-FI:local", - "Grandpa (Francés (Canadá)):fr-CA:local", - "Grandpa (Francés (Francia)):fr-FR:local", - "Grandpa (Italiano (Italia)):it-IT:local", - "Grandpa (Japonés (Japón)):ja-JP:local", - "Grandpa (Coreano (Corea del Sur)):ko-KR:local", - "Grandpa (Portugués (Brasil)):pt-BR:local", - "Grandpa (Chino (China continental)):zh-CN:local", - "Grandpa (Chino (Taiwán)):zh-TW:local", - "Bufón:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Órgano:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superestrella:en-US:local", - "Ralph:en-US:local", - "Reed (Alemán (Alemania)):de-DE:local", - "Reed (Inglés (RU)):en-GB:local", - "Reed (Inglés (EE. UU.)):en-US:local", - "Reed (Español (España)):es-ES:local", - "Reed (Español (México)):es-MX:local", - "Reed (Finés (Finlandia)):fi-FI:local", - "Reed (Francés (Canadá)):fr-CA:local", - "Reed (Italiano (Italia)):it-IT:local", - "Reed (Japonés (Japón)):ja-JP:local", - "Reed (Coreano (Corea del Sur)):ko-KR:local", - "Reed (Portugués (Brasil)):pt-BR:local", - "Reed (Chino (China continental)):zh-CN:local", - "Reed (Chino (Taiwán)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (Alemán (Alemania)):de-DE:local", - "Rocko (Inglés (RU)):en-GB:local", - "Rocko (Inglés (EE. UU.)):en-US:local", - "Rocko (Español (España)):es-ES:local", - "Rocko (Español (México)):es-MX:local", - "Rocko (Finés (Finlandia)):fi-FI:local", - "Rocko (Francés (Canadá)):fr-CA:local", - "Rocko (Francés (Francia)):fr-FR:local", - "Rocko (Italiano (Italia)):it-IT:local", - "Rocko (Japonés (Japón)):ja-JP:local", - "Rocko (Coreano (Corea del Sur)):ko-KR:local", - "Rocko (Portugués (Brasil)):pt-BR:local", - "Rocko (Chino (China continental)):zh-CN:local", - "Rocko (Chino (Taiwán)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (Alemán (Alemania)):de-DE:local", - "Sandy (Inglés (RU)):en-GB:local", - "Sandy (Inglés (EE. UU.)):en-US:local", - "Sandy (Español (España)):es-ES:local", - "Sandy (Español (México)):es-MX:local", - "Sandy (Finés (Finlandia)):fi-FI:local", - "Sandy (Francés (Canadá)):fr-CA:local", - "Sandy (Francés (Francia)):fr-FR:local", - "Sandy (Italiano (Italia)):it-IT:local", - "Sandy (Japonés (Japón)):ja-JP:local", - "Sandy (Coreano (Corea del Sur)):ko-KR:local", - "Sandy (Portugués (Brasil)):pt-BR:local", - "Sandy (Chino (China continental)):zh-CN:local", - "Sandy (Chino (Taiwán)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (Alemán (Alemania)):de-DE:local", - "Shelley (Inglés (RU)):en-GB:local", - "Shelley (Inglés (EE. UU.)):en-US:local", - "Shelley (Español (España)):es-ES:local", - "Shelley (Español (México)):es-MX:local", - "Shelley (Finés (Finlandia)):fi-FI:local", - "Shelley (Francés (Canadá)):fr-CA:local", - "Shelley (Francés (Francia)):fr-FR:local", - "Shelley (Italiano (Italia)):it-IT:local", - "Shelley (Japonés (Japón)):ja-JP:local", - "Shelley (Coreano (Corea del Sur)):ko-KR:local", - "Shelley (Portugués (Brasil)):pt-BR:local", - "Shelley (Chino (China continental)):zh-CN:local", - "Shelley (Chino (Taiwán)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Susurro:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 30, - "availWidth": 1680, - "availHeight": 934, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 1004, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 719, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 984, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1710, - "height": 1112, - "colorDepth": 30, - "availWidth": 1710, - "availHeight": 1073, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman (English (India)):en-IN:local", - "Aman (English (India)):en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Yelda:tr-TR:local", - "Ioana:ro-RO:local", - "Yuna:ko-KR:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Krzysztof (Enhanced):pl-PL:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona ((null)):lt-LT:local", - "Ona ((null)):lt-LT:local", - "Organ:en-US:local", - "Piya:bn-IN:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 824, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:151.0) Gecko/20100101 Firefox/151.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 30, - "availWidth": 1680, - "availHeight": 974, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1080, - "colorDepth": 30, - "availWidth": 2048, - "availHeight": 1080, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1080, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 957, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1024, - "height": 768, - "colorDepth": 30, - "availWidth": 1024, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2304, - "height": 1296, - "colorDepth": 30, - "availWidth": 2304, - "availHeight": 1266, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1512, - "height": 982, - "colorDepth": 30, - "availWidth": 1512, - "availHeight": 860, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1935, - "height": 1251, - "colorDepth": 30, - "availWidth": 1935, - "availHeight": 1160, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1710, - "height": 1112, - "colorDepth": 30, - "availWidth": 1710, - "availHeight": 1112, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1470, - "height": 956, - "colorDepth": 30, - "availWidth": 1470, - "availHeight": 849, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 30, - "availWidth": 1440, - "availHeight": 824, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2056, - "height": 1329, - "colorDepth": 30, - "availWidth": 2056, - "availHeight": 1285, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 30, - "availWidth": 1280, - "availHeight": 720, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel (English (UK)):en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 5120, - "height": 1440, - "colorDepth": 30, - "availWidth": 5120, - "availHeight": 1364, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Soumya:kn-IN:local", - "Alva:sv-SE:local", - "Aman:en-IN:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Aru:kk-KZ:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Geeta:te-IN:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Ona:lt-LT:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Piya:bn-IN:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tara:en-IN:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 30, - "availWidth": 1536, - "availHeight": 935, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1800, - "height": 1169, - "colorDepth": 30, - "availWidth": 1800, - "availHeight": 1061, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 30, - "availWidth": 2560, - "availHeight": 1415, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2133, - "height": 1200, - "colorDepth": 30, - "availWidth": 2133, - "availHeight": 1179, - "devicePixelRatio": 2.4 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 30, - "availWidth": 1920, - "availHeight": 984, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel 945GM, or similar" - }, - "speechVoices": [ - "Alex:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amelie:fr-CA:local", - "Anna:de-DE:local", - "Carmit:he-IL:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Diego:es-AR:local", - "Ellen:nl-BE:local", - "Fiona:en-scotland:local", - "Fred:en-US:local", - "Ioana:ro-RO:local", - "Joana:pt-PT:local", - "Jorge:es-ES:local", - "Juan:es-MX:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kyoko:ja-JP:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Luca:it-IT:local", - "Luciana:pt-BR:local", - "Maged:ar-SA:local", - "Mariska:hu-HU:local", - "Mei-Jia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Monica:es-ES:local", - "Nora:nb-NO:local", - "Paulina:es-MX:local", - "Rishi:en-IN:local", - "Samantha:en-US:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Sin-ji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Ting-Ting:zh-CN:local", - "Veena:en-IN:local", - "Victoria:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Yuri:ru-RU:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 30, - "availWidth": 2048, - "availHeight": 1026, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "ATI Technologies Inc.", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Vani:ta-IN:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 30, - "availWidth": 1366, - "availHeight": 670, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Inc.", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "MacIntel", - "hardwareConcurrency": 10, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1225, - "height": 797, - "colorDepth": 30, - "availWidth": 1225, - "availHeight": 765, - "devicePixelRatio": 2.4 - }, - "webgl": { - "unmaskedVendor": "Apple", - "unmaskedRenderer": "Apple M1, or similar" - }, - "speechVoices": [ - "Albert:en-US:local", - "Alice:it-IT:local", - "Alva:sv-SE:local", - "Amélie:fr-CA:local", - "Amira:ms-MY:local", - "Anna:de-DE:local", - "Bad News:en-US:local", - "Bahh:en-US:local", - "Bells:en-US:local", - "Boing:en-US:local", - "Bubbles:en-US:local", - "Carmit:he-IL:local", - "Cellos:en-US:local", - "Damayanti:id-ID:local", - "Daniel:en-GB:local", - "Daria:bg-BG:local", - "Wobble:en-US:local", - "Eddy (German (Germany)):de-DE:local", - "Eddy (English (UK)):en-GB:local", - "Eddy (English (US)):en-US:local", - "Eddy (Spanish (Spain)):es-ES:local", - "Eddy (Spanish (Mexico)):es-MX:local", - "Eddy (Finnish (Finland)):fi-FI:local", - "Eddy (French (Canada)):fr-CA:local", - "Eddy (French (France)):fr-FR:local", - "Eddy (Italian (Italy)):it-IT:local", - "Eddy (Japanese (Japan)):ja-JP:local", - "Eddy (Korean (South Korea)):ko-KR:local", - "Eddy (Portuguese (Brazil)):pt-BR:local", - "Eddy (Chinese (China mainland)):zh-CN:local", - "Eddy (Chinese (Taiwan)):zh-TW:local", - "Ellen:nl-BE:local", - "Flo (German (Germany)):de-DE:local", - "Flo (English (UK)):en-GB:local", - "Flo (English (US)):en-US:local", - "Flo (Spanish (Spain)):es-ES:local", - "Flo (Spanish (Mexico)):es-MX:local", - "Flo (Finnish (Finland)):fi-FI:local", - "Flo (French (Canada)):fr-CA:local", - "Flo (French (France)):fr-FR:local", - "Flo (Italian (Italy)):it-IT:local", - "Flo (Japanese (Japan)):ja-JP:local", - "Flo (Korean (South Korea)):ko-KR:local", - "Flo (Portuguese (Brazil)):pt-BR:local", - "Flo (Chinese (China mainland)):zh-CN:local", - "Flo (Chinese (Taiwan)):zh-TW:local", - "Fred:en-US:local", - "Good News:en-US:local", - "Grandma (German (Germany)):de-DE:local", - "Grandma (English (UK)):en-GB:local", - "Grandma (English (US)):en-US:local", - "Grandma (Spanish (Spain)):es-ES:local", - "Grandma (Spanish (Mexico)):es-MX:local", - "Grandma (Finnish (Finland)):fi-FI:local", - "Grandma (French (Canada)):fr-CA:local", - "Grandma (French (France)):fr-FR:local", - "Grandma (Italian (Italy)):it-IT:local", - "Grandma (Japanese (Japan)):ja-JP:local", - "Grandma (Korean (South Korea)):ko-KR:local", - "Grandma (Portuguese (Brazil)):pt-BR:local", - "Grandma (Chinese (China mainland)):zh-CN:local", - "Grandma (Chinese (Taiwan)):zh-TW:local", - "Grandpa (German (Germany)):de-DE:local", - "Grandpa (English (UK)):en-GB:local", - "Grandpa (English (US)):en-US:local", - "Grandpa (Spanish (Spain)):es-ES:local", - "Grandpa (Spanish (Mexico)):es-MX:local", - "Grandpa (Finnish (Finland)):fi-FI:local", - "Grandpa (French (Canada)):fr-CA:local", - "Grandpa (French (France)):fr-FR:local", - "Grandpa (Italian (Italy)):it-IT:local", - "Grandpa (Japanese (Japan)):ja-JP:local", - "Grandpa (Korean (South Korea)):ko-KR:local", - "Grandpa (Portuguese (Brazil)):pt-BR:local", - "Grandpa (Chinese (China mainland)):zh-CN:local", - "Grandpa (Chinese (Taiwan)):zh-TW:local", - "Jester:en-US:local", - "Ioana:ro-RO:local", - "Jacques:fr-FR:local", - "Joana:pt-PT:local", - "Junior:en-US:local", - "Kanya:th-TH:local", - "Karen:en-AU:local", - "Kathy:en-US:local", - "Kyoko:ja-JP:local", - "Lana:hr-HR:local", - "Laura:sk-SK:local", - "Lekha:hi-IN:local", - "Lesya:uk-UA:local", - "Linh:vi-VN:local", - "Luciana:pt-BR:local", - "Majed:ar-001:local", - "Tünde:hu-HU:local", - "Meijia:zh-TW:local", - "Melina:el-GR:local", - "Milena:ru-RU:local", - "Moira:en-IE:local", - "Mónica:es-ES:local", - "Montse:ca-ES:local", - "Nora:nb-NO:local", - "Organ:en-US:local", - "Paulina:es-MX:local", - "Superstar:en-US:local", - "Ralph:en-US:local", - "Reed (German (Germany)):de-DE:local", - "Reed (English (UK)):en-GB:local", - "Reed (English (US)):en-US:local", - "Reed (Spanish (Spain)):es-ES:local", - "Reed (Spanish (Mexico)):es-MX:local", - "Reed (Finnish (Finland)):fi-FI:local", - "Reed (French (Canada)):fr-CA:local", - "Reed (Italian (Italy)):it-IT:local", - "Reed (Japanese (Japan)):ja-JP:local", - "Reed (Korean (South Korea)):ko-KR:local", - "Reed (Portuguese (Brazil)):pt-BR:local", - "Reed (Chinese (China mainland)):zh-CN:local", - "Reed (Chinese (Taiwan)):zh-TW:local", - "Rishi:en-IN:local", - "Rocko (German (Germany)):de-DE:local", - "Rocko (English (UK)):en-GB:local", - "Rocko (English (US)):en-US:local", - "Rocko (Spanish (Spain)):es-ES:local", - "Rocko (Spanish (Mexico)):es-MX:local", - "Rocko (Finnish (Finland)):fi-FI:local", - "Rocko (French (Canada)):fr-CA:local", - "Rocko (French (France)):fr-FR:local", - "Rocko (Italian (Italy)):it-IT:local", - "Rocko (Japanese (Japan)):ja-JP:local", - "Rocko (Korean (South Korea)):ko-KR:local", - "Rocko (Portuguese (Brazil)):pt-BR:local", - "Rocko (Chinese (China mainland)):zh-CN:local", - "Rocko (Chinese (Taiwan)):zh-TW:local", - "Samantha:en-US:local", - "Sandy (German (Germany)):de-DE:local", - "Sandy (English (UK)):en-GB:local", - "Sandy (English (US)):en-US:local", - "Sandy (Spanish (Spain)):es-ES:local", - "Sandy (Spanish (Mexico)):es-MX:local", - "Sandy (Finnish (Finland)):fi-FI:local", - "Sandy (French (Canada)):fr-CA:local", - "Sandy (French (France)):fr-FR:local", - "Sandy (Italian (Italy)):it-IT:local", - "Sandy (Japanese (Japan)):ja-JP:local", - "Sandy (Korean (South Korea)):ko-KR:local", - "Sandy (Portuguese (Brazil)):pt-BR:local", - "Sandy (Chinese (China mainland)):zh-CN:local", - "Sandy (Chinese (Taiwan)):zh-TW:local", - "Sara:da-DK:local", - "Satu:fi-FI:local", - "Shelley (German (Germany)):de-DE:local", - "Shelley (English (UK)):en-GB:local", - "Shelley (English (US)):en-US:local", - "Shelley (Spanish (Spain)):es-ES:local", - "Shelley (Spanish (Mexico)):es-MX:local", - "Shelley (Finnish (Finland)):fi-FI:local", - "Shelley (French (Canada)):fr-CA:local", - "Shelley (French (France)):fr-FR:local", - "Shelley (Italian (Italy)):it-IT:local", - "Shelley (Japanese (Japan)):ja-JP:local", - "Shelley (Korean (South Korea)):ko-KR:local", - "Shelley (Portuguese (Brazil)):pt-BR:local", - "Shelley (Chinese (China mainland)):zh-CN:local", - "Shelley (Chinese (Taiwan)):zh-TW:local", - "Sinji:zh-HK:local", - "Tessa:en-ZA:local", - "Thomas:fr-FR:local", - "Tina:sl-SI:local", - "Tingting:zh-CN:local", - "Trinoids:en-US:local", - "Whisper:en-US:local", - "Xander:nl-NL:local", - "Yelda:tr-TR:local", - "Yuna:ko-KR:local", - "Zarvox:en-US:local", - "Zosia:pl-PL:local", - "Zuzana:cs-CZ:local" - ] - } - ], - "windows": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 2 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1632, - "height": 918, - "colorDepth": 24, - "availWidth": 1632, - "availHeight": 918, - "devicePixelRatio": 1.17647058823529 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Tolga - Turkish (Turkey):tr-TR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Naayf - Arabic (Saudi):ar-SA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1027, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 5 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Jakub - Czech (Czech Republic):cs-CZ:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1152, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 1080, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 5 - }, - "screen": { - "width": 1280, - "height": 853, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 805, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Caroline - French (Canada):fr-CA:local", - "Microsoft Claude - French (Canada):fr-CA:local", - "Microsoft Nathalie - French (Canada):fr-CA:local", - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1408, - "height": 792, - "colorDepth": 24, - "availWidth": 1408, - "availHeight": 748, - "devicePixelRatio": 1.36363636363636 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1728, - "height": 972, - "colorDepth": 24, - "availWidth": 1728, - "availHeight": 921, - "devicePixelRatio": 1.11111111111111 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 920, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Heami - Korean (Korean):ko-KR:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Heami Desktop - Korean:ko-KR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1718, - "height": 1277, - "colorDepth": 24, - "availWidth": 1718, - "availHeight": 1277, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Mozilla", - "unmaskedRenderer": "Mozilla" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1472, - "height": 828, - "colorDepth": 24, - "availWidth": 1472, - "availHeight": 791, - "devicePixelRatio": 1.30434782608696 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 1024, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 984, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1002, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 852, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hoda - Arabic (Egypt):ar-EG:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Cosimo - Italian (Italy):it-IT:local", - "Microsoft Elsa - Italian (Italy):it-IT:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Elsa Desktop - Italian (Italy):it-IT:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1392, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 912, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1451, - "height": 907, - "colorDepth": 24, - "availWidth": 1451, - "availHeight": 907, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 840, - "devicePixelRatio": 1.2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 900, - "devicePixelRatio": 1.2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 1 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 800, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1600, - "height": 1000, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 940, - "devicePixelRatio": 1.2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2293, - "height": 960, - "colorDepth": 24, - "availWidth": 2293, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Ivan - Bulgarian (Bulgaria):bg-BG:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1664, - "height": 936, - "colorDepth": 24, - "availWidth": 1664, - "availHeight": 894, - "devicePixelRatio": 1.15384615384615 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_4_0 ps_4_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1360, - "height": 768, - "colorDepth": 24, - "availWidth": 1360, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 860, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Richard - English (Canada):en-CA:local", - "Microsoft Linda - English (Canada):en-CA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1344, - "height": 756, - "colorDepth": 24, - "availWidth": 1344, - "availHeight": 714, - "devicePixelRatio": 1.42857142857143 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1504, - "height": 846, - "colorDepth": 24, - "availWidth": 1504, - "availHeight": 846, - "devicePixelRatio": 1.27659574468085 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3456, - "height": 1440, - "colorDepth": 24, - "availWidth": 3456, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 960, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 900, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1278, - "height": 852, - "colorDepth": 24, - "availWidth": 1278, - "availHeight": 805, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hortense Desktop - French:fr-FR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2293, - "height": 960, - "colorDepth": 24, - "availWidth": 2293, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1792, - "height": 1008, - "colorDepth": 24, - "availWidth": 1792, - "availHeight": 1008, - "devicePixelRatio": 2.14285714285714 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 1000, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 952, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Adam - Polish (Poland):pl-PL:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Paulina - Polish (Poland):pl-PL:local", - "Microsoft Paulina Desktop - Polish:pl-PL:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1160, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1216, - "height": 684, - "colorDepth": 24, - "availWidth": 1216, - "availHeight": 646, - "devicePixelRatio": 1.57894736842105 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helia - Portuguese (Portugal):pt-PT:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0 AtContent/93.5.6604.5", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 834, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1451, - "height": 907, - "colorDepth": 24, - "availWidth": 1451, - "availHeight": 859, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 1024, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 984, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1080, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1088, - "height": 614, - "colorDepth": 24, - "availWidth": 1088, - "availHeight": 566, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 40 - }, - "screen": { - "width": 1360, - "height": 768, - "colorDepth": 24, - "availWidth": 1360, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2176, - "height": 1224, - "colorDepth": 24, - "availWidth": 2176, - "availHeight": 1176, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1500, - "height": 1000, - "colorDepth": 24, - "availWidth": 1500, - "availHeight": 952, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2112, - "height": 1320, - "colorDepth": 24, - "availWidth": 2112, - "availHeight": 1320, - "devicePixelRatio": 1.36363636363636 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Stefanos - Greek (Greece):el-GR:local", - "Microsoft Tolga - Turkish (Turkey):tr-TR:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1824, - "height": 1026, - "colorDepth": 24, - "availWidth": 1824, - "availHeight": 980, - "devicePixelRatio": 1.05263157894737 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Heera - English (India):en-IN:local", - "Microsoft Ravi - English (India):en-IN:local", - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 5120, - "height": 1440, - "colorDepth": 24, - "availWidth": 5120, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1440, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 900, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1296, - "height": 810, - "colorDepth": 24, - "availWidth": 1296, - "availHeight": 810, - "devicePixelRatio": 2.22222222222222 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Richard - English (Canada):en-CA:local", - "Microsoft Linda - English (Canada):en-CA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 852, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1360, - "height": 768, - "colorDepth": 24, - "availWidth": 1360, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 852, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 852, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1299, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1088, - "height": 612, - "colorDepth": 24, - "availWidth": 1088, - "availHeight": 564, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Heami - Korean (Korean):ko-KR:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Heami Desktop - Korean:ko-KR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2560, - "height": 1080, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1112, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 5 - }, - "screen": { - "width": 2731, - "height": 768, - "colorDepth": 24, - "availWidth": 2731, - "availHeight": 730, - "devicePixelRatio": 1.875 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Jakub - Czech (Czech Republic):cs-CZ:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1010, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_4_0 ps_4_0), or similar" - }, - "speechVoices": [ - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 1600, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 1552, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1152, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1), or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2176, - "height": 1224, - "colorDepth": 24, - "availWidth": 2176, - "availHeight": 1224, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Cosimo - Italian (Italy):it-IT:local", - "Microsoft Elsa - Italian (Italy):it-IT:local", - "Microsoft Elsa Desktop - Italian (Italy):it-IT:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1093, - "height": 614, - "colorDepth": 24, - "availWidth": 1093, - "availHeight": 566, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 5850 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1184, - "height": 666, - "colorDepth": 24, - "availWidth": 1184, - "availHeight": 629, - "devicePixelRatio": 1.62162162162162 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 920, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Guillaume - French (Switzerland):fr-CH:local", - "Microsoft Karsten - German (Switzerland):de-CH:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hortense Desktop - French:fr-FR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1152, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1760, - "height": 990, - "colorDepth": 24, - "availWidth": 1760, - "availHeight": 953, - "devicePixelRatio": 1.09090909090909 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Hedda Desktop - German:de-DE:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1696, - "height": 954, - "colorDepth": 24, - "availWidth": 1696, - "availHeight": 901, - "devicePixelRatio": 1.13207547169811 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 5 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 912, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 5 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1728, - "height": 972, - "colorDepth": 24, - "availWidth": 1728, - "availHeight": 929, - "devicePixelRatio": 1.11111111111111 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1696, - "height": 954, - "colorDepth": 24, - "availWidth": 1696, - "availHeight": 954, - "devicePixelRatio": 1.13207547169811 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1504, - "height": 846, - "colorDepth": 24, - "availWidth": 1504, - "availHeight": 846, - "devicePixelRatio": 1.27659574468085 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 28, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1200, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1440, - "height": 900, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 852, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft David Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Richard - English (Canada):en-CA:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Linda - English (Canada):en-CA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Caroline - French (Canada):fr-CA:local", - "Microsoft Claude - French (Canada):fr-CA:local", - "Microsoft Nathalie - French (Canada):fr-CA:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1343, - "height": 755, - "colorDepth": 24, - "availWidth": 1343, - "availHeight": 726, - "devicePixelRatio": 1.01694915254237 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 1024, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 984, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1152, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1122, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena Mobile - Spanish (Spain):es-ES:local", - "Microsoft Laura Mobile - Spanish (Spain):es-ES:local", - "Microsoft Pablo Mobile - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1138, - "height": 640, - "colorDepth": 24, - "availWidth": 1138, - "availHeight": 600, - "devicePixelRatio": 1.2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1184, - "height": 666, - "colorDepth": 24, - "availWidth": 1184, - "availHeight": 666, - "devicePixelRatio": 1.62162162162162 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Stefanos - Greek (Greece):el-GR:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Unknown)", - "unmaskedRenderer": "ANGLE (Unknown, Generic Renderer Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Karsten - German (Switzerland):de-CH:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Guillaume - French (Switzerland):fr-CH:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1010, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Unknown)", - "unmaskedRenderer": "ANGLE (Unknown, Adreno (TM) 650 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Jon - Norwegian (Bokmål):nb-NO:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 5120, - "height": 1440, - "colorDepth": 24, - "availWidth": 5120, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2049, - "height": 1152, - "colorDepth": 24, - "availWidth": 2049, - "availHeight": 1092, - "devicePixelRatio": 0.666666666666667 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 860, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 1 - }, - "screen": { - "width": 1707, - "height": 1067, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 1019, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 14, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1408, - "height": 880, - "colorDepth": 24, - "availWidth": 1408, - "availHeight": 827, - "devicePixelRatio": 1.36363636363636 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1010, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1400, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 22, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1600, - "height": 1000, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 952, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 1080, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 1032, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Karsten - German (Switzerland):de-CH:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Guillaume - French (Switzerland):fr-CH:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 720, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Microsoft)", - "unmaskedRenderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 912, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1112, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Szabolcs - Hungarian (Hungary):hu-HU:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Andika - Indonesian (Indonesia):id-ID:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1504, - "height": 1003, - "colorDepth": 24, - "availWidth": 1504, - "availHeight": 1003, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1440, - "height": 960, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 912, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Unknown)", - "unmaskedRenderer": "ANGLE (Unknown, Generic Renderer Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_0 ps_4_0), or similar" - }, - "speechVoices": [ - "Microsoft Raul - Spanish (Mexico):es-MX:local", - "Microsoft Sabina - Spanish (Mexico):es-MX:local", - "Microsoft Sabina Desktop - Spanish (Mexico):es-MX:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 816, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Unknown)", - "unmaskedRenderer": "ANGLE (Unknown, Adreno (TM) 650 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) Arc(TM) A750 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Hortense Desktop - French:fr-FR:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 912, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 20, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3096, - "height": 1296, - "colorDepth": 24, - "availWidth": 3096, - "availHeight": 1248, - "devicePixelRatio": 1.11111111111111 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 1 - }, - "screen": { - "width": 2859, - "height": 1608, - "colorDepth": 24, - "availWidth": 2859, - "availHeight": 1608, - "devicePixelRatio": 0.895522388059702 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Michael - German (Austria):de-AT:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1275, - "height": 717, - "colorDepth": 24, - "availWidth": 1275, - "availHeight": 672, - "devicePixelRatio": 1.07142857142857 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1392, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1525, - "height": 858, - "colorDepth": 24, - "availWidth": 1525, - "availHeight": 858, - "devicePixelRatio": 0.895522388059702 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1088, - "height": 612, - "colorDepth": 24, - "availWidth": 1088, - "availHeight": 578, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Cosimo - Italian (Italy):it-IT:local", - "Microsoft Stefanos - Greek (Greece):el-GR:local", - "Microsoft Elsa - Italian (Italy):it-IT:local", - "Microsoft Elsa Desktop - Italian (Italy):it-IT:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 672, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 920, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Hedda - German (Germany):de-DE:local", - "Microsoft Naayf - Arabic (Saudi):ar-SA:local", - "Microsoft Katja - German (Germany):de-DE:local", - "Microsoft Stefan - German (Germany):de-DE:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Hortense - French (France):fr-FR:local", - "Microsoft Julie - French (France):fr-FR:local", - "Microsoft Paul - French (France):fr-FR:local", - "Microsoft Adam - Polish (Poland):pl-PL:local", - "Microsoft Paulina - Polish (Poland):pl-PL:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft Tolga - Turkish (Turkey):tr-TR:local", - "Microsoft Hedda Desktop - German:de-DE:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Hortense Desktop - French:fr-FR:local", - "Microsoft Paulina Desktop - Polish:pl-PL:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 10 - }, - "screen": { - "width": 2144, - "height": 1340, - "colorDepth": 24, - "availWidth": 2144, - "availHeight": 1307, - "devicePixelRatio": 0.895522388059702 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft James - English (Australia):en-AU:local", - "Microsoft Catherine - English (Australia):en-AU:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1248, - "height": 702, - "colorDepth": 24, - "availWidth": 1248, - "availHeight": 663, - "devicePixelRatio": 1.53846153846154 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1392, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1400, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Daniel - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria - Portuguese (Brazil):pt-BR:local", - "Microsoft Maria Desktop - Portuguese(Brazil):pt-BR:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 800, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 752, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (AMD)", - "unmaskedRenderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Hoda - Arabic (Egypt):ar-EG:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 2, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1504, - "height": 1003, - "colorDepth": 24, - "availWidth": 1504, - "availHeight": 1003, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Karsten - German (Switzerland):de-CH:local", - "Microsoft George - English (United Kingdom):en-GB:local", - "Microsoft Hazel - English (United Kingdom):en-GB:local", - "Microsoft Susan - English (United Kingdom):en-GB:local", - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Hazel Desktop - English (Great Britain):en-GB:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 28, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 1200, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 1152, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Richard - English (Canada):en-CA:local", - "Microsoft Linda - English (Canada):en-CA:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1040, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (NVIDIA)", - "unmaskedRenderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Helena - Spanish (Spain):es-ES:local", - "Microsoft Laura - Spanish (Spain):es-ES:local", - "Microsoft Pablo - Spanish (Spain):es-ES:local", - "Microsoft Helena Desktop - Spanish (Spain):es-ES:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1408, - "height": 792, - "colorDepth": 24, - "availWidth": 1408, - "availHeight": 748, - "devicePixelRatio": 1.36363636363636 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 14, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1104, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft David - English (United States):en-US:local", - "Microsoft Mark - English (United States):en-US:local", - "Microsoft Zira - English (United States):en-US:local", - "Microsoft David Desktop - English (United States):en-US:local", - "Microsoft Zira Desktop - English (United States):en-US:local" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Win32", - "hardwareConcurrency": 8, - "maxTouchPoints": 10 - }, - "screen": { - "width": 1088, - "height": 612, - "colorDepth": 24, - "availWidth": 1088, - "availHeight": 571, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Google Inc. (Intel)", - "unmaskedRenderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar" - }, - "speechVoices": [ - "Microsoft Szabolcs - Hungarian (Hungary):hu-HU:local", - "Microsoft Irina - Russian (Russia):ru-RU:local", - "Microsoft Pavel - Russian (Russia):ru-RU:local", - "Microsoft Zira Desktop - English (United States):en-US:local", - "Microsoft Irina Desktop - Russian:ru-RU:local" - ] - } - ], - "linux": [ - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce 8800 GTX, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1200, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1575, - "availHeight": 880, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1760, - "height": 990, - "colorDepth": 24, - "availWidth": 1760, - "availHeight": 990, - "devicePixelRatio": 1.09090909090909 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1400, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 32, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 2160, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 2160, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 712, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 6, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 5120, - "height": 1440, - "colorDepth": 24, - "availWidth": 5120, - "availHeight": 1408, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1368, - "height": 768, - "colorDepth": 24, - "availWidth": 1368, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Mesa", - "unmaskedRenderer": "GeForce 8800 GTX, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1408, - "height": 792, - "colorDepth": 24, - "availWidth": 1408, - "availHeight": 792, - "devicePixelRatio": 1.36363636363636 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 717, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Open Source Technology Center", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - }, - "speechVoices": [ - "Catalan:ca:remote", - "Bishnupriya Manipuri:bpy:remote", - "Nahuatl (Classical):nci:remote", - "Dutch:nl:remote", - "Azerbaijani:az:remote", - "Kyrgyz:ky:remote", - "English (Great Britain):en:remote", - "Chinese (Cantonese):yue:remote", - "English (Lancaster):en:remote", - "Vietnamese (Northern):vi:remote", - "Amharic:am:remote", - "Gaelic (Irish):ga:remote", - "Latvian:lv:remote", - "Swahili:sw:remote", - "Afrikaans:af:remote", - "Malay:ms:remote", - "Tatar:tt:remote", - "Nepali:ne:remote", - "Serbian:sr:remote", - "Oriya:or:remote", - "Arabic:ar:remote", - "Interlingua:ia:remote", - "Italian:it:remote", - "Aragonese:an:remote", - "German:de:remote", - "Assamese:as:remote", - "Bengali:bn:remote", - "Georgian:ka:remote", - "Lithuanian:lt:remote", - "Danish:da:remote", - "English (Caribbean):en:remote", - "Papiamento:pap:remote", - "Chinese (Mandarin):cmn:remote", - "Bosnian:bs:remote", - "Guarani:gn:remote", - "Marathi:mr:remote", - "Persian:fa:remote", - "Hindi:hi:remote", - "Bulgarian:bg:remote", - "Lingua Franca Nova:lfn:remote", - "Hungarian:hu:remote", - "Icelandic:is:remote", - "Burmese:my:remote", - "Maltese:mt:remote", - "Esperanto:eo:remote", - "Portuguese (Portugal):pt:remote", - "Spanish (Spain):es:remote", - "Kurdish:ku:remote", - "Lojban:jbo:remote", - "Czech:cs:remote", - "Turkish:tr:remote", - "Tamil:ta:remote", - "Spanish (Latin America):es:remote", - "Indonesian:id:remote", - "Armenian (West Armenia):hy:remote", - "Kannada:kn:remote", - "Greek (Ancient):grc:remote", - "Armenian (East Armenia):hy:remote", - "Greenlandic:kl:remote", - "French (Switzerland):fr:remote", - "Norwegian Bokmål:nb:remote", - "Portuguese (Brazil):pt:remote", - "Urdu:ur:remote", - "Punjabi:pa:remote", - "Japanese:ja:remote", - "Romanian:ro:remote", - "Gujarati:gu:remote", - "Latin:la:remote", - "Polish:pl:remote", - "French (France):fr:remote", - "Slovenian:sl:remote", - "Malayalam:ml:remote", - "Russian:ru:remote", - "Croatian:hr:remote", - "Korean:ko:remote", - "Welsh:cy:remote", - "Slovak:sk:remote", - "poz/mi:mi:remote", - "English (Scotland):en:remote", - "Vietnamese (Southern):vi:remote", - "English (West Midlands):en:remote", - "Persian (Pinglish):fa:remote", - "Albanian:sq:remote", - "Finnish:fi:remote", - "English (Received Pronunciation):en:remote", - "Greek:el:remote", - "French (Belgium):fr:remote", - "Gaelic (Scottish):gd:remote", - "Telugu:te:remote", - "Konkani:kok:remote", - "Sindhi:sd:remote", - "Estonian:et:remote", - "Basque:eu:remote", - "Oromo:om:remote", - "English (America):en:remote", - "Swedish:sv:remote", - "Macedonian:mk:remote", - "Setswana:tn:remote", - "Sinhala:si:remote", - "Vietnamese (Central):vi:remote" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1760, - "height": 990, - "colorDepth": 24, - "availWidth": 1760, - "availHeight": 990, - "devicePixelRatio": 1.09090909090909 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 728, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1664, - "height": 936, - "colorDepth": 24, - "availWidth": 1664, - "availHeight": 936, - "devicePixelRatio": 1.15384615384615 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 718, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel Open Source Technology Center", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3072, - "height": 1728, - "colorDepth": 24, - "availWidth": 3072, - "availHeight": 1728, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce 8800 GTX, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 768, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 24, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2048, - "height": 1152, - "colorDepth": 24, - "availWidth": 2048, - "availHeight": 1152, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 2 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3440, - "height": 1440, - "colorDepth": 24, - "availWidth": 3440, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Mesa", - "unmaskedRenderer": "llvmpipe, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 28, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 1080, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1600, - "height": 900, - "colorDepth": 24, - "availWidth": 1600, - "availHeight": 900, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - }, - "speechVoices": ["en_US-ryan-high:en-US:remote"] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1200, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1680, - "height": 1050, - "colorDepth": 24, - "availWidth": 1680, - "availHeight": 1050, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1200, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1200, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1620, - "height": 1080, - "colorDepth": 24, - "availWidth": 1620, - "availHeight": 1080, - "devicePixelRatio": 1.33333333333333 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 720, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 22, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1088, - "height": 612, - "colorDepth": 24, - "availWidth": 1088, - "availHeight": 612, - "devicePixelRatio": 1.76470588235294 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3840, - "height": 1080, - "colorDepth": 24, - "availWidth": 3840, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Mesa", - "unmaskedRenderer": "llvmpipe, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1280, - "height": 720, - "colorDepth": 24, - "availWidth": 1280, - "availHeight": 720, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1440, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - }, - "speechVoices": [ - "virginie:fr-FR:remote", - "isabel:es-ES:remote", - "serena:en-GB:remote", - "samantha:en-US:remote", - "sabrina:de-DE:remote", - "silvia:it-IT:remote" - ] - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 22, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 960, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 960, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 712, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1760, - "height": 990, - "colorDepth": 24, - "availWidth": 1760, - "availHeight": 953, - "devicePixelRatio": 1.09090909090909 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1536, - "availHeight": 864, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 3200, - "height": 1800, - "colorDepth": 24, - "availWidth": 3200, - "availHeight": 1800, - "devicePixelRatio": 1.2 - }, - "webgl": { - "unmaskedVendor": "NVIDIA Corporation", - "unmaskedRenderer": "NVIDIA GeForce GTX 980, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 12, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1862, - "availHeight": 1048, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon R9 200 Series, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1707, - "height": 960, - "colorDepth": 24, - "availWidth": 1707, - "availHeight": 960, - "devicePixelRatio": 1.5 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1440, - "height": 810, - "colorDepth": 24, - "availWidth": 1440, - "availHeight": 810, - "devicePixelRatio": 1.33333333333333 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 2, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1366, - "availHeight": 735, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Mesa", - "unmaskedRenderer": "Radeon HD 5850, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 2560, - "height": 1440, - "colorDepth": 24, - "availWidth": 2560, - "availHeight": 1413, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 4, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1525, - "height": 858, - "colorDepth": 24, - "availWidth": 1525, - "availHeight": 813, - "devicePixelRatio": 0.895522388059702 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1536, - "height": 864, - "colorDepth": 24, - "availWidth": 1483, - "availHeight": 832, - "devicePixelRatio": 1.25 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 8, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1366, - "height": 768, - "colorDepth": 24, - "availWidth": 1296, - "availHeight": 741, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "Intel", - "unmaskedRenderer": "Intel(R) HD Graphics 400, or similar" - } - }, - { - "navigator": { - "userAgent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0", - "platform": "Linux x86_64", - "hardwareConcurrency": 16, - "maxTouchPoints": 0 - }, - "screen": { - "width": 1920, - "height": 1080, - "colorDepth": 24, - "availWidth": 1920, - "availHeight": 1080, - "devicePixelRatio": 1 - }, - "webgl": { - "unmaskedVendor": "AMD", - "unmaskedRenderer": "Radeon HD 3200 Graphics, or similar" - } - } - ] - } -} diff --git a/src-tauri/src/camoufox/data/fonts.json b/src-tauri/src/camoufox/data/fonts.json deleted file mode 100644 index 0f16348..0000000 --- a/src-tauri/src/camoufox/data/fonts.json +++ /dev/null @@ -1,822 +0,0 @@ -{ - "win": [ - "Arial", - "Arial Black", - "Bahnschrift", - "Calibri", - "Calibri Light", - "Cambria", - "Cambria Math", - "Candara", - "Candara Light", - "Comic Sans MS", - "Consolas", - "Constantia", - "Corbel", - "Corbel Light", - "Courier New", - "Ebrima", - "Franklin Gothic Medium", - "Gabriola", - "Gadugi", - "Georgia", - "HoloLens MDL2 Assets", - "Impact", - "Ink Free", - "Javanese Text", - "Leelawadee UI", - "Leelawadee UI Semilight", - "Lucida Console", - "Lucida Sans Unicode", - "MS Gothic", - "MS PGothic", - "MS UI Gothic", - "MV Boli", - "Malgun Gothic", - "Malgun Gothic Semilight", - "Marlett", - "Microsoft Himalaya", - "Microsoft JhengHei", - "Microsoft JhengHei Light", - "Microsoft JhengHei UI", - "Microsoft JhengHei UI Light", - "Microsoft New Tai Lue", - "Microsoft PhagsPa", - "Microsoft Sans Serif", - "Microsoft Tai Le", - "Microsoft YaHei", - "Microsoft YaHei Light", - "Microsoft YaHei UI", - "Microsoft YaHei UI Light", - "Microsoft Yi Baiti", - "MingLiU-ExtB", - "MingLiU_HKSCS-ExtB", - "Mongolian Baiti", - "Myanmar Text", - "NSimSun", - "Nirmala UI", - "Nirmala UI Semilight", - "PMingLiU-ExtB", - "Palatino Linotype", - "Segoe Fluent Icons", - "Segoe MDL2 Assets", - "Segoe Print", - "Segoe Script", - "Segoe UI", - "Segoe UI Black", - "Segoe UI Emoji", - "Segoe UI Historic", - "Segoe UI Light", - "Segoe UI Semibold", - "Segoe UI Semilight", - "Segoe UI Symbol", - "Segoe UI Variable", - "SimSun", - "SimSun-ExtB", - "Sitka", - "Sitka Text", - "Sylfaen", - "Symbol", - "Tahoma", - "Times New Roman", - "Trebuchet MS", - "Twemoji Mozilla", - "Verdana", - "Webdings", - "Wingdings", - "Yu Gothic", - "Yu Gothic Light", - "Yu Gothic Medium", - "Yu Gothic UI", - "Yu Gothic UI Light", - "Yu Gothic UI Semibold", - "Yu Gothic UI Semilight", - "\u5b8b\u4f53", - "\u5fae\u8edf\u6b63\u9ed1\u9ad4", - "\u5fae\u8edf\u6b63\u9ed1\u9ad4 Light", - "\u5fae\u8f6f\u96c5\u9ed1", - "\u5fae\u8f6f\u96c5\u9ed1 Light", - "\u65b0\u5b8b\u4f53", - "\u65b0\u7d30\u660e\u9ad4-ExtB", - "\u6e38\u30b4\u30b7\u30c3\u30af", - "\u6e38\u30b4\u30b7\u30c3\u30af Light", - "\u6e38\u30b4\u30b7\u30c3\u30af Medium", - "\u7d30\u660e\u9ad4-ExtB", - "\u7d30\u660e\u9ad4_HKSCS-ExtB", - "\ub9d1\uc740 \uace0\ub515", - "\ub9d1\uc740 \uace0\ub515 Semilight", - "\uff2d\uff33 \u30b4\u30b7\u30c3\u30af", - "\uff2d\uff33 \uff30\u30b4\u30b7\u30c3\u30af" - ], - "mac": [ - ".Al Bayan PUA", - ".Al Nile PUA", - ".Al Tarikh PUA", - ".Apple Color Emoji UI", - ".Apple SD Gothic NeoI", - ".Aqua Kana", - ".Aqua Kana Bold", - ".Aqua \u304b\u306a", - ".Aqua \u304b\u306a \u30dc\u30fc\u30eb\u30c9", - ".Arial Hebrew Desk Interface", - ".Baghdad PUA", - ".Beirut PUA", - ".Damascus PUA", - ".DecoType Naskh PUA", - ".Diwan Kufi PUA", - ".Farah PUA", - ".Geeza Pro Interface", - ".Geeza Pro PUA", - ".Helvetica LT MM", - ".Hiragino Kaku Gothic Interface", - ".Hiragino Sans GB Interface", - ".Keyboard", - ".KufiStandardGK PUA", - ".LastResort", - ".Lucida Grande UI", - ".Muna PUA", - ".Nadeem PUA", - ".New York", - ".Noto Nastaliq Urdu UI", - ".PingFang HK", - ".PingFang SC", - ".PingFang TC", - ".SF Arabic", - ".SF Arabic Rounded", - ".SF Compact", - ".SF Compact Rounded", - ".SF NS", - ".SF NS Mono", - ".SF NS Rounded", - ".Sana PUA", - ".Savoye LET CC.", - ".ThonburiUI", - ".ThonburiUIWatch", - ".\u82f9\u65b9-\u6e2f", - ".\u82f9\u65b9-\u7b80", - ".\u82f9\u65b9-\u7e41", - ".\u860b\u65b9-\u6e2f", - ".\u860b\u65b9-\u7c21", - ".\u860b\u65b9-\u7e41", - "Academy Engraved LET", - "Al Bayan", - "Al Nile", - "Al Tarikh", - "American Typewriter", - "Andale Mono", - "Apple Braille", - "Apple Chancery", - "Apple Color Emoji", - "Apple SD Gothic Neo", - "Apple SD \uc0b0\ub3cc\uace0\ub515 Neo", - "Apple Symbols", - "AppleGothic", - "AppleMyungjo", - "Arial", - "Arial Black", - "Arial Hebrew", - "Arial Hebrew Scholar", - "Arial Narrow", - "Arial Rounded MT Bold", - "Arial Unicode MS", - "Athelas", - "Avenir", - "Avenir Black", - "Avenir Black Oblique", - "Avenir Book", - "Avenir Heavy", - "Avenir Light", - "Avenir Medium", - "Avenir Next", - "Avenir Next Condensed", - "Avenir Next Condensed Demi Bold", - "Avenir Next Condensed Heavy", - "Avenir Next Condensed Medium", - "Avenir Next Condensed Ultra Light", - "Avenir Next Demi Bold", - "Avenir Next Heavy", - "Avenir Next Medium", - "Avenir Next Ultra Light", - "Ayuthaya", - "Baghdad", - "Bangla MN", - "Bangla Sangam MN", - "Baskerville", - "Beirut", - "Big Caslon", - "Bodoni 72", - "Bodoni 72 Oldstyle", - "Bodoni 72 Smallcaps", - "Bodoni Ornaments", - "Bradley Hand", - "Brush Script MT", - "Chalkboard", - "Chalkboard SE", - "Chalkduster", - "Charter", - "Charter Black", - "Cochin", - "Comic Sans MS", - "Copperplate", - "Corsiva Hebrew", - "Courier", - "Courier New", - "Czcionka systemowa", - "DIN Alternate", - "DIN Condensed", - "Damascus", - "DecoType Naskh", - "Devanagari MT", - "Devanagari Sangam MN", - "Didot", - "Diwan Kufi", - "Diwan Thuluth", - "Euphemia UCAS", - "Farah", - "Farisi", - "Font Sistem", - "Font de sistem", - "Font di sistema", - "Font sustava", - "Fonte do Sistema", - "Futura", - "GB18030 Bitmap", - "Galvji", - "Geeza Pro", - "Geneva", - "Georgia", - "Gill Sans", - "Grantha Sangam MN", - "Gujarati MT", - "Gujarati Sangam MN", - "Gurmukhi MN", - "Gurmukhi MT", - "Gurmukhi Sangam MN", - "Heiti SC", - "Heiti TC", - "Heiti-\uac04\uccb4", - "Heiti-\ubc88\uccb4", - "Helvetica", - "Helvetica Neue", - "Herculanum", - "Hiragino Kaku Gothic Pro", - "Hiragino Kaku Gothic Pro W3", - "Hiragino Kaku Gothic Pro W6", - "Hiragino Kaku Gothic ProN", - "Hiragino Kaku Gothic ProN W3", - "Hiragino Kaku Gothic ProN W6", - "Hiragino Kaku Gothic Std", - "Hiragino Kaku Gothic Std W8", - "Hiragino Kaku Gothic StdN", - "Hiragino Kaku Gothic StdN W8", - "Hiragino Maru Gothic Pro", - "Hiragino Maru Gothic Pro W4", - "Hiragino Maru Gothic ProN", - "Hiragino Maru Gothic ProN W4", - "Hiragino Mincho Pro", - "Hiragino Mincho Pro W3", - "Hiragino Mincho Pro W6", - "Hiragino Mincho ProN", - "Hiragino Mincho ProN W3", - "Hiragino Mincho ProN W6", - "Hiragino Sans", - "Hiragino Sans GB", - "Hiragino Sans GB W3", - "Hiragino Sans GB W6", - "Hiragino Sans W0", - "Hiragino Sans W1", - "Hiragino Sans W2", - "Hiragino Sans W3", - "Hiragino Sans W4", - "Hiragino Sans W5", - "Hiragino Sans W6", - "Hiragino Sans W7", - "Hiragino Sans W8", - "Hiragino Sans W9", - "Hoefler Text", - "Hoefler Text Ornaments", - "ITF Devanagari", - "ITF Devanagari Marathi", - "Impact", - "InaiMathi", - "Iowan Old Style", - "Iowan Old Style Black", - "J\u00e4rjestelm\u00e4fontti", - "Kailasa", - "Kannada MN", - "Kannada Sangam MN", - "Kefa", - "Khmer MN", - "Khmer Sangam MN", - "Kohinoor Bangla", - "Kohinoor Devanagari", - "Kohinoor Gujarati", - "Kohinoor Telugu", - "Kokonor", - "Krungthep", - "KufiStandardGK", - "Lao MN", - "Lao Sangam MN", - "Lucida Grande", - "Luminari", - "Malayalam MN", - "Malayalam Sangam MN", - "Marion", - "Marker Felt", - "Menlo", - "Microsoft Sans Serif", - "Mishafi", - "Mishafi Gold", - "Monaco", - "Mshtakan", - "Mukta Mahee", - "MuktaMahee Bold", - "MuktaMahee ExtraBold", - "MuktaMahee ExtraLight", - "MuktaMahee Light", - "MuktaMahee Medium", - "MuktaMahee Regular", - "MuktaMahee SemiBold", - "Muna", - "Myanmar MN", - "Myanmar Sangam MN", - "Nadeem", - "New Peninim MT", - "Noteworthy", - "Noto Nastaliq Urdu", - "Noto Sans Adlam", - "Noto Sans Armenian", - "Noto Sans Armenian Blk", - "Noto Sans Armenian ExtBd", - "Noto Sans Armenian ExtLt", - "Noto Sans Armenian Light", - "Noto Sans Armenian Med", - "Noto Sans Armenian SemBd", - "Noto Sans Armenian Thin", - "Noto Sans Avestan", - "Noto Sans Bamum", - "Noto Sans Bassa Vah", - "Noto Sans Batak", - "Noto Sans Bhaiksuki", - "Noto Sans Brahmi", - "Noto Sans Buginese", - "Noto Sans Buhid", - "Noto Sans CanAborig", - "Noto Sans Canadian Aboriginal", - "Noto Sans Carian", - "Noto Sans CaucAlban", - "Noto Sans Caucasian Albanian", - "Noto Sans Chakma", - "Noto Sans Cham", - "Noto Sans Coptic", - "Noto Sans Cuneiform", - "Noto Sans Cypriot", - "Noto Sans Duployan", - "Noto Sans EgyptHiero", - "Noto Sans Egyptian Hieroglyphs", - "Noto Sans Elbasan", - "Noto Sans Glagolitic", - "Noto Sans Gothic", - "Noto Sans Gunjala Gondi", - "Noto Sans Hanifi Rohingya", - "Noto Sans HanifiRohg", - "Noto Sans Hanunoo", - "Noto Sans Hatran", - "Noto Sans ImpAramaic", - "Noto Sans Imperial Aramaic", - "Noto Sans InsPahlavi", - "Noto Sans InsParthi", - "Noto Sans Inscriptional Pahlavi", - "Noto Sans Inscriptional Parthian", - "Noto Sans Javanese", - "Noto Sans Kaithi", - "Noto Sans Kannada", - "Noto Sans Kannada Black", - "Noto Sans Kannada ExtraBold", - "Noto Sans Kannada ExtraLight", - "Noto Sans Kannada Light", - "Noto Sans Kannada Medium", - "Noto Sans Kannada SemiBold", - "Noto Sans Kannada Thin", - "Noto Sans Kayah Li", - "Noto Sans Kharoshthi", - "Noto Sans Khojki", - "Noto Sans Khudawadi", - "Noto Sans Lepcha", - "Noto Sans Limbu", - "Noto Sans Linear A", - "Noto Sans Linear B", - "Noto Sans Lisu", - "Noto Sans Lycian", - "Noto Sans Lydian", - "Noto Sans Mahajani", - "Noto Sans Mandaic", - "Noto Sans Manichaean", - "Noto Sans Marchen", - "Noto Sans Masaram Gondi", - "Noto Sans Meetei Mayek", - "Noto Sans Mende Kikakui", - "Noto Sans Meroitic", - "Noto Sans Miao", - "Noto Sans Modi", - "Noto Sans Mongolian", - "Noto Sans Mro", - "Noto Sans Multani", - "Noto Sans Myanmar", - "Noto Sans Myanmar Blk", - "Noto Sans Myanmar ExtBd", - "Noto Sans Myanmar ExtLt", - "Noto Sans Myanmar Light", - "Noto Sans Myanmar Med", - "Noto Sans Myanmar SemBd", - "Noto Sans Myanmar Thin", - "Noto Sans NKo", - "Noto Sans Nabataean", - "Noto Sans New Tai Lue", - "Noto Sans Newa", - "Noto Sans Ol Chiki", - "Noto Sans Old Hungarian", - "Noto Sans Old Italic", - "Noto Sans Old North Arabian", - "Noto Sans Old Permic", - "Noto Sans Old Persian", - "Noto Sans Old South Arabian", - "Noto Sans Old Turkic", - "Noto Sans OldHung", - "Noto Sans OldNorArab", - "Noto Sans OldSouArab", - "Noto Sans Oriya", - "Noto Sans Osage", - "Noto Sans Osmanya", - "Noto Sans Pahawh Hmong", - "Noto Sans Palmyrene", - "Noto Sans Pau Cin Hau", - "Noto Sans PhagsPa", - "Noto Sans Phoenician", - "Noto Sans PsaPahlavi", - "Noto Sans Psalter Pahlavi", - "Noto Sans Rejang", - "Noto Sans Samaritan", - "Noto Sans Saurashtra", - "Noto Sans Sharada", - "Noto Sans Siddham", - "Noto Sans Sora Sompeng", - "Noto Sans SoraSomp", - "Noto Sans Sundanese", - "Noto Sans Syloti Nagri", - "Noto Sans Syriac", - "Noto Sans Tagalog", - "Noto Sans Tagbanwa", - "Noto Sans Tai Le", - "Noto Sans Tai Tham", - "Noto Sans Tai Viet", - "Noto Sans Takri", - "Noto Sans Thaana", - "Noto Sans Tifinagh", - "Noto Sans Tirhuta", - "Noto Sans Ugaritic", - "Noto Sans Vai", - "Noto Sans Wancho", - "Noto Sans Warang Citi", - "Noto Sans Yi", - "Noto Sans Zawgyi", - "Noto Sans Zawgyi Blk", - "Noto Sans Zawgyi ExtBd", - "Noto Sans Zawgyi ExtLt", - "Noto Sans Zawgyi Light", - "Noto Sans Zawgyi Med", - "Noto Sans Zawgyi SemBd", - "Noto Sans Zawgyi Thin", - "Noto Serif Ahom", - "Noto Serif Balinese", - "Noto Serif Hmong Nyiakeng", - "Noto Serif Myanmar", - "Noto Serif Myanmar Blk", - "Noto Serif Myanmar ExtBd", - "Noto Serif Myanmar ExtLt", - "Noto Serif Myanmar Light", - "Noto Serif Myanmar Med", - "Noto Serif Myanmar SemBd", - "Noto Serif Myanmar Thin", - "Noto Serif Yezidi", - "Optima", - "Oriya MN", - "Oriya Sangam MN", - "PT Mono", - "PT Sans", - "PT Sans Caption", - "PT Sans Narrow", - "PT Serif", - "PT Serif Caption", - "Palatino", - "Papyrus", - "Party LET", - "Phosphate", - "Ph\u00f4ng ch\u1eef H\u1ec7 th\u1ed1ng", - "PingFang HK", - "PingFang SC", - "PingFang TC", - "Plantagenet Cherokee", - "Police syst\u00e8me", - "Raanana", - "Rendszerbet\u0171t\u00edpus", - "Rockwell", - "STIX Two Math", - "STIX Two Text", - "STIXGeneral", - "STIXIntegralsD", - "STIXIntegralsSm", - "STIXIntegralsUp", - "STIXIntegralsUpD", - "STIXIntegralsUpSm", - "STIXNonUnicode", - "STIXSizeFiveSym", - "STIXSizeFourSym", - "STIXSizeOneSym", - "STIXSizeThreeSym", - "STIXSizeTwoSym", - "STIXVariants", - "STSong", - "Sana", - "Sathu", - "Savoye LET", - "Seravek", - "Seravek ExtraLight", - "Seravek Light", - "Seravek Medium", - "Shree Devanagari 714", - "SignPainter", - "SignPainter-HouseScript", - "Silom", - "Sinhala MN", - "Sinhala Sangam MN", - "Sistem Fontu", - "Skia", - "Snell Roundhand", - "Songti SC", - "Songti TC", - "Sukhumvit Set", - "Superclarendon", - "Symbol", - "Systeemlettertype", - "System Font", - "Systemschrift", - "Systemskrift", - "Systemtypsnitt", - "Syst\u00e9mov\u00e9 p\u00edsmo", - "Tahoma", - "Tamil MN", - "Tamil Sangam MN", - "Telugu MN", - "Telugu Sangam MN", - "Thonburi", - "Times", - "Times New Roman", - "Tipo de letra del sistema", - "Tipo de letra do sistema", - "Tipus de lletra del sistema", - "Trattatello", - "Trebuchet MS", - "Verdana", - "Waseem", - "Webdings", - "Wingdings", - "Wingdings 2", - "Wingdings 3", - "Zapf Dingbats", - "Zapfino", - "\u0393\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", - "\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u0448\u0440\u0438\u0444\u0442", - "\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442", - "\u05d2\u05d5\u05e4\u05df \u05de\u05e2\u05e8\u05db\u05ea", - "\u0627\u0644\u0628\u064a\u0627\u0646", - "\u0627\u0644\u062a\u0627\u0631\u064a\u062e", - "\u0627\u0644\u0646\u064a\u0644", - "\u0628\u063a\u062f\u0627\u062f", - "\u0628\u064a\u0631\u0648\u062a", - "\u062c\u064a\u0632\u0629", - "\u062e\u0637 \u0627\u0644\u0646\u0638\u0627\u0645", - "\u062f\u0645\u0634\u0642", - "\u062f\u064a\u0648\u0627\u0646 \u062b\u0644\u062b", - "\u062f\u064a\u0648\u0627\u0646 \u0643\u0648\u0641\u064a", - "\u0635\u0646\u0639\u0627\u0621", - "\u0641\u0627\u0631\u0633\u064a", - "\u0641\u0631\u062d", - "\u0643\u0648\u0641\u064a", - "\u0645\u0646\u0649", - "\u0645\u0650\u0635\u062d\u0641\u064a", - "\u0645\u0650\u0635\u062d\u0641\u064a \u0630\u0647\u0628\u064a", - "\u0646\u062f\u064a\u0645", - "\u0646\u0633\u062e", - "\u0648\u0633\u064a\u0645", - "\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940", - "\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u092e\u0930\u093e\u0920\u0940", - "\u0915\u094b\u0939\u093f\u0928\u0942\u0930 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940", - "\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u090f\u092e\u0970\u091f\u0940\u0970", - "\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u0938\u0902\u0917\u092e \u090f\u092e\u0970\u090f\u0928\u0970", - "\u0936\u094d\u0930\u0940 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u096d\u0967\u096a", - "\u0e41\u0e1a\u0e1a\u0e2d\u0e31\u0e01\u0e29\u0e23\u0e23\u0e30\u0e1a\u0e1a", - "\u2e41\u7175\u6120\u82a9\u82c8", - "\u30b7\u30b9\u30c6\u30e0\u30d5\u30a9\u30f3\u30c8", - "\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro", - "\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro W4", - "\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN", - "\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN W4", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W3", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W6", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W3", - "\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W6", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W3", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W6", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W3", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W6", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std W8", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN W8", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W3", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W6", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W0", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W1", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W2", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W3", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W4", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W5", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W6", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W7", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W8", - "\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W9", - "\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587", - "\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W3", - "\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W6", - "\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587", - "\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W3", - "\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W6", - "\u5b8b\u4f53-\u7b80", - "\u5b8b\u4f53-\u7e41", - "\u5b8b\u9ad4-\u7c21", - "\u5b8b\u9ad4-\u7e41", - "\u7cfb\u7d71\u5b57\u9ad4", - "\u7cfb\u7edf\u5b57\u4f53", - "\u82f9\u65b9-\u6e2f", - "\u82f9\u65b9-\u7b80", - "\u82f9\u65b9-\u7e41", - "\u8371\u8389\u834d\u836d\u8a70\u8353\u2050\u726f", - "\u8371\u8389\u834d\u836d\u8a70\u8353\u2053\u7464", - "\u8371\u8389\u834d\u836d\u8a70\u8353\u8356\u8362\u834e", - "\u8371\u8389\u834d\u836d\u8adb\u8353\u2050\u726f", - "\u8371\u8389\u834d\u836d\u96be\u92a9\u2050\u726f", - "\u860b\u65b9-\u6e2f", - "\u860b\u65b9-\u7c21", - "\u860b\u65b9-\u7e41", - "\u9ed1\u4f53-\u7b80", - "\u9ed1\u4f53-\u7e41", - "\u9ed1\u9ad4-\u7c21", - "\u9ed1\u9ad4-\u7e41", - "\u9ed2\u4f53-\u7c21", - "\u9ed2\u4f53-\u7e41", - "\uc2dc\uc2a4\ud15c \uc11c\uccb4" - ], - "lin": [ - "Arimo", - "Cousine", - "Noto Naskh Arabic", - "Noto Sans Adlam", - "Noto Sans Armenian", - "Noto Sans Balinese", - "Noto Sans Bamum", - "Noto Sans Bassa Vah", - "Noto Sans Batak", - "Noto Sans Bengali", - "Noto Sans Buginese", - "Noto Sans Buhid", - "Noto Sans Canadian Aboriginal", - "Noto Sans Chakma", - "Noto Sans Cham", - "Noto Sans Cherokee", - "Noto Sans Coptic", - "Noto Sans Deseret", - "Noto Sans Devanagari", - "Noto Sans Elbasan", - "Noto Sans Ethiopic", - "Noto Sans Georgian", - "Noto Sans Grantha", - "Noto Sans Gujarati", - "Noto Sans Gunjala Gondi", - "Noto Sans Gurmukhi", - "Noto Sans Hanifi Rohingya", - "Noto Sans Hanunoo", - "Noto Sans Hebrew", - "Noto Sans JP", - "Noto Sans Javanese", - "Noto Sans KR", - "Noto Sans Kannada", - "Noto Sans Kayah Li", - "Noto Sans Khmer", - "Noto Sans Khojki", - "Noto Sans Khudawadi", - "Noto Sans Lao", - "Noto Sans Lepcha", - "Noto Sans Limbu", - "Noto Sans Lisu", - "Noto Sans Mahajani", - "Noto Sans Malayalam", - "Noto Sans Mandaic", - "Noto Sans Masaram Gondi", - "Noto Sans Medefaidrin", - "Noto Sans Meetei Mayek", - "Noto Sans Mende Kikakui", - "Noto Sans Miao", - "Noto Sans Modi", - "Noto Sans Mongolian", - "Noto Sans Mro", - "Noto Sans Multani", - "Noto Sans Myanmar", - "Noto Sans NKo", - "Noto Sans New Tai Lue", - "Noto Sans Newa", - "Noto Sans Ol Chiki", - "Noto Sans Oriya", - "Noto Sans Osage", - "Noto Sans Osmanya", - "Noto Sans Pahawh Hmong", - "Noto Sans Pau Cin Hau", - "Noto Sans Rejang", - "Noto Sans Runic", - "Noto Sans SC", - "Noto Sans Samaritan", - "Noto Sans Saurashtra", - "Noto Sans Sharada", - "Noto Sans Shavian", - "Noto Sans Sinhala", - "Noto Sans Sora Sompeng", - "Noto Sans Soyombo", - "Noto Sans Sundanese", - "Noto Sans Syloti Nagri", - "Noto Sans Symbols", - "Noto Sans Symbols 2", - "Noto Sans Syriac", - "Noto Sans TC", - "Noto Sans Tagalog", - "Noto Sans Tagbanwa", - "Noto Sans Tai Le", - "Noto Sans Tai Tham", - "Noto Sans Tai Viet", - "Noto Sans Takri", - "Noto Sans Tamil", - "Noto Sans Telugu", - "Noto Sans Thaana", - "Noto Sans Thai", - "Noto Sans Tifinagh", - "Noto Sans Tifinagh APT", - "Noto Sans Tifinagh Adrar", - "Noto Sans Tifinagh Agraw Imazighen", - "Noto Sans Tifinagh Ahaggar", - "Noto Sans Tifinagh Air", - "Noto Sans Tifinagh Azawagh", - "Noto Sans Tifinagh Ghat", - "Noto Sans Tifinagh Hawad", - "Noto Sans Tifinagh Rhissa Ixa", - "Noto Sans Tifinagh SIL", - "Noto Sans Tifinagh Tawellemmet", - "Noto Sans Tirhuta", - "Noto Sans Vai", - "Noto Sans Wancho", - "Noto Sans Warang Citi", - "Noto Sans Yi", - "Noto Sans Zanabazar Square", - "Noto Serif Armenian", - "Noto Serif Balinese", - "Noto Serif Bengali", - "Noto Serif Devanagari", - "Noto Serif Dogra", - "Noto Serif Ethiopic", - "Noto Serif Georgian", - "Noto Serif Grantha", - "Noto Serif Gujarati", - "Noto Serif Gurmukhi", - "Noto Serif Hebrew", - "Noto Serif Kannada", - "Noto Serif Khmer", - "Noto Serif Khojki", - "Noto Serif Lao", - "Noto Serif Malayalam", - "Noto Serif Myanmar", - "Noto Serif NP Hmong", - "Noto Serif Sinhala", - "Noto Serif Tamil", - "Noto Serif Telugu", - "Noto Serif Thai", - "Noto Serif Tibetan", - "Noto Serif Yezidi", - "STIX Two Math", - "Tinos", - "Twemoji Mozilla" - ] -} diff --git a/src-tauri/src/camoufox/data/header-network-definition.zip b/src-tauri/src/camoufox/data/header-network-definition.zip deleted file mode 100644 index 423228a..0000000 Binary files a/src-tauri/src/camoufox/data/header-network-definition.zip and /dev/null differ diff --git a/src-tauri/src/camoufox/data/headers-order.json b/src-tauri/src/camoufox/data/headers-order.json deleted file mode 100644 index a25f2a1..0000000 --- a/src-tauri/src/camoufox/data/headers-order.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "safari": [ - "Referer", - "Origin", - "Content-Type", - "Accept", - "Upgrade-Insecure-Requests", - "User-Agent", - "Content-Length", - "Accept-Encoding", - "Accept-Language", - "Connection", - "Host", - "Cookie", - "Sec-Fetch-Dest", - "Sec-Fetch-Mode", - "Sec-Fetch-Site", - ":method", - ":scheme", - ":authority", - ":path", - "referer", - "origin", - "content-type", - "accept", - "user-agent", - "content-length", - "accept-encoding", - "accept-language", - "cookie", - "sec-fetch-dest", - "sec-fetch-mode", - "sec-fetch-site" - ], - "chrome": [ - "Host", - "Connection", - "Content-Length", - "Cache-Control", - "sec-ch-ua", - "sec-ch-ua-mobile", - "sec-ch-ua-platform", - "Origin", - "Content-Type", - "Upgrade-Insecure-Requests", - "User-Agent", - "Accept", - "Sec-Fetch-Site", - "Sec-Fetch-Mode", - "Sec-Fetch-User", - "Sec-Fetch-Dest", - "Referer", - "Accept-Encoding", - "Accept-Language", - "Cookie", - ":method", - ":authority", - ":scheme", - ":path", - "content-length", - "cache-control", - "sec-ch-ua", - "sec-ch-ua-mobile", - "sec-ch-ua-platform", - "origin", - "content-type", - "upgrade-insecure-requests", - "user-agent", - "accept", - "sec-fetch-site", - "sec-fetch-mode", - "sec-fetch-user", - "sec-fetch-dest", - "referer", - "accept-encoding", - "accept-language", - "cookie", - "priority" - ], - "firefox": [ - "Host", - "User-Agent", - "Accept", - "Accept-Language", - "Accept-Encoding", - "Content-Type", - "Content-Length", - "Origin", - "Connection", - "Referer", - "Cookie", - "Upgrade-Insecure-Requests", - "Sec-Fetch-Dest", - "Sec-Fetch-Mode", - "Sec-Fetch-Site", - "Sec-Fetch-User", - "Priority", - ":method", - ":path", - ":authority", - ":scheme", - "user-agent", - "accept", - "accept-language", - "accept-encoding", - "content-type", - "content-length", - "origin", - "referer", - "cookie", - "upgrade-insecure-requests", - "sec-fetch-dest", - "sec-fetch-mode", - "sec-fetch-site", - "sec-fetch-user", - "priority", - "te" - ], - "edge": [ - "Host", - "Connection", - "Content-Length", - "Cache-Control", - "sec-ch-ua", - "sec-ch-ua-mobile", - "sec-ch-ua-platform", - "Origin", - "Content-Type", - "Upgrade-Insecure-Requests", - "User-Agent", - "Accept", - "Sec-Fetch-Site", - "Sec-Fetch-Mode", - "Sec-Fetch-User", - "Sec-Fetch-Dest", - "Referer", - "Accept-Encoding", - "Accept-Language", - "Cookie", - ":method", - ":authority", - ":scheme", - ":path", - "content-length", - "cache-control", - "sec-ch-ua", - "sec-ch-ua-mobile", - "sec-ch-ua-platform", - "origin", - "content-type", - "upgrade-insecure-requests", - "user-agent", - "accept", - "sec-fetch-site", - "sec-fetch-mode", - "sec-fetch-user", - "sec-fetch-dest", - "referer", - "accept-encoding", - "accept-language", - "cookie", - "priority" - ] -} diff --git a/src-tauri/src/camoufox/data/input-network-definition.zip b/src-tauri/src/camoufox/data/input-network-definition.zip deleted file mode 100644 index 3d45833..0000000 Binary files a/src-tauri/src/camoufox/data/input-network-definition.zip and /dev/null differ diff --git a/src-tauri/src/camoufox/data/mod.rs b/src-tauri/src/camoufox/data/mod.rs deleted file mode 100644 index 9d9d111..0000000 --- a/src-tauri/src/camoufox/data/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub const FINGERPRINT_NETWORK_ZIP: &[u8] = include_bytes!("fingerprint-network-definition.zip"); -pub const INPUT_NETWORK_ZIP: &[u8] = include_bytes!("input-network-definition.zip"); -pub const HEADER_NETWORK_ZIP: &[u8] = include_bytes!("header-network-definition.zip"); -pub const BROWSER_HELPER_JSON: &str = include_str!("browser-helper-file.json"); -pub const HEADERS_ORDER_JSON: &str = include_str!("headers-order.json"); -pub const FONTS_JSON: &str = include_str!("fonts.json"); -pub const BROWSERFORGE_YML: &str = include_str!("browserforge.yml"); -pub const WEBGL_DATA_DB: &[u8] = include_bytes!("webgl_data.db"); -pub const TERRITORY_INFO_XML: &str = include_str!("territoryInfo.xml"); - -/// Real fingerprint presets bundled with the original Camoufox v135 line -/// (Firefox <= 148). Frozen upstream — kept around so users who haven't -/// upgraded their Camoufox binary keep getting matched fingerprints. -/// Mirrors `pythonlib/camoufox/fingerprint-presets.json` upstream. -pub const FINGERPRINT_PRESETS_V135_JSON: &str = include_str!("fingerprint-presets-v135.json"); - -/// Real fingerprint presets for every Camoufox release after the v135 line -/// (currently Firefox 149+ via the v150 build). This file is expected to -/// be refreshed regularly as upstream Camoufox tracks newer Firefox -/// releases — we keep the upstream filename here so each refresh is a -/// straight `cp` from `pythonlib/camoufox/fingerprint-presets-v150.json`. -pub const FINGERPRINT_PRESETS_NEWER_JSON: &str = include_str!("fingerprint-presets-v150.json"); - -/// Firefox major version at which the newer preset bundle takes over from -/// the frozen v135 bundle. Matches `PRESETS_V150_MIN_FF` in -/// `pythonlib/camoufox/fingerprints.py`. -pub const PRESETS_NEWER_MIN_FF: u32 = 149; diff --git a/src-tauri/src/camoufox/data/webgl_data.db b/src-tauri/src/camoufox/data/webgl_data.db deleted file mode 100644 index f8448aa..0000000 Binary files a/src-tauri/src/camoufox/data/webgl_data.db and /dev/null differ diff --git a/src-tauri/src/camoufox/env_vars.rs b/src-tauri/src/camoufox/env_vars.rs deleted file mode 100644 index 09b3489..0000000 --- a/src-tauri/src/camoufox/env_vars.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Environment variable handling for Camoufox configuration. -//! -//! Camoufox reads its configuration from environment variables named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc. -//! The configuration JSON is chunked to fit within environment variable size limits. - -use std::collections::HashMap; - -/// Maximum chunk size for environment variables on Windows. -const CHUNK_SIZE_WINDOWS: usize = 2047; - -/// Maximum chunk size for environment variables on Unix systems. -const CHUNK_SIZE_UNIX: usize = 32767; - -/// Get the chunk size for the current platform. -fn get_chunk_size() -> usize { - if cfg!(windows) { - CHUNK_SIZE_WINDOWS - } else { - CHUNK_SIZE_UNIX - } -} - -/// Convert a Camoufox config map to environment variables. -/// -/// The config is serialized to JSON and split into chunks that fit within -/// environment variable size limits. Each chunk is stored in a variable -/// named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc. -pub fn config_to_env_vars( - config: &HashMap, -) -> Result, serde_json::Error> { - let config_json = serde_json::to_string(config)?; - Ok(chunk_config_string(&config_json)) -} - -/// Split a config string into chunks and create environment variable map. -pub fn chunk_config_string(config_str: &str) -> HashMap { - let chunk_size = get_chunk_size(); - let mut env_vars = HashMap::new(); - - for (i, chunk) in config_str.as_bytes().chunks(chunk_size).enumerate() { - let chunk_str = String::from_utf8_lossy(chunk).to_string(); - let env_name = format!("CAMOU_CONFIG_{}", i + 1); - env_vars.insert(env_name, chunk_str); - } - - env_vars -} - -/// Determine the target OS from a user agent string. -pub fn determine_ua_os(user_agent: &str) -> &'static str { - let ua_lower = user_agent.to_lowercase(); - - if ua_lower.contains("mac os") || ua_lower.contains("macos") || ua_lower.contains("macintosh") { - "mac" - } else if ua_lower.contains("windows") { - "win" - } else { - "lin" - } -} - -/// Get the fontconfig path environment variable for Linux. -pub fn get_fontconfig_env(target_os: &str, camoufox_path: &std::path::Path) -> Option { - if cfg!(target_os = "linux") { - let fontconfig_dir = camoufox_path.join("fontconfig").join(target_os); - if fontconfig_dir.exists() { - return Some(fontconfig_dir.to_string_lossy().to_string()); - } - } - None -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_chunk_small_config() { - let config = r#"{"navigator.userAgent": "Mozilla/5.0"}"#; - let env_vars = chunk_config_string(config); - - assert_eq!(env_vars.len(), 1); - assert!(env_vars.contains_key("CAMOU_CONFIG_1")); - assert_eq!(env_vars.get("CAMOU_CONFIG_1").unwrap(), config); - } - - #[test] - fn test_chunk_large_config() { - // Create a config string larger than the chunk size - let chunk_size = get_chunk_size(); - let large_value = "x".repeat(chunk_size * 2 + 100); - let config = format!(r#"{{"key": "{}"}}"#, large_value); - - let env_vars = chunk_config_string(&config); - - // Should have at least 2 chunks - assert!(env_vars.len() >= 2); - assert!(env_vars.contains_key("CAMOU_CONFIG_1")); - assert!(env_vars.contains_key("CAMOU_CONFIG_2")); - - // Reconstruct and verify - let mut reconstructed = String::new(); - let mut i = 1; - while let Some(chunk) = env_vars.get(&format!("CAMOU_CONFIG_{}", i)) { - reconstructed.push_str(chunk); - i += 1; - } - assert_eq!(reconstructed, config); - } - - #[test] - fn test_determine_ua_os_windows() { - let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"; - assert_eq!(determine_ua_os(ua), "win"); - } - - #[test] - fn test_determine_ua_os_macos() { - let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0"; - assert_eq!(determine_ua_os(ua), "mac"); - } - - #[test] - fn test_determine_ua_os_linux() { - let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"; - assert_eq!(determine_ua_os(ua), "lin"); - } - - #[test] - fn test_config_to_env_vars() { - let mut config = HashMap::new(); - config.insert( - "navigator.userAgent".to_string(), - serde_json::json!("Mozilla/5.0 Firefox/135.0"), - ); - config.insert("screen.width".to_string(), serde_json::json!(1920)); - - let env_vars = config_to_env_vars(&config).unwrap(); - assert!(!env_vars.is_empty()); - assert!(env_vars.contains_key("CAMOU_CONFIG_1")); - } -} diff --git a/src-tauri/src/camoufox/fingerprint/bayesian_network.rs b/src-tauri/src/camoufox/fingerprint/bayesian_network.rs deleted file mode 100644 index 75cce0c..0000000 --- a/src-tauri/src/camoufox/fingerprint/bayesian_network.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Bayesian network for fingerprint generation. -//! -//! Loads pre-trained probability distributions from ZIP files and samples fingerprints. - -use super::bayesian_node::{BayesianNode, NodeDefinition}; -use serde::Deserialize; -use std::collections::HashMap; -use std::io::{Cursor, Read}; -use zip::ZipArchive; - -/// Network definition structure from the ZIP file. -#[derive(Debug, Deserialize)] -pub struct NetworkDefinition { - pub nodes: Vec, -} - -/// A Bayesian network for generating consistent fingerprints. -pub struct BayesianNetwork { - nodes_in_sampling_order: Vec, - nodes_by_name: HashMap, -} - -impl BayesianNetwork { - /// Load a Bayesian network from embedded ZIP file bytes. - pub fn from_zip_bytes(zip_bytes: &[u8]) -> Result { - let cursor = Cursor::new(zip_bytes); - let mut archive = ZipArchive::new(cursor)?; - - // Find and read the JSON file from the ZIP - let mut json_content = String::new(); - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - if file.name().ends_with(".json") { - file.read_to_string(&mut json_content)?; - break; - } - } - - if json_content.is_empty() { - return Err(BayesianNetworkError::NoJsonInZip); - } - - let definition: NetworkDefinition = serde_json::from_str(&json_content)?; - - let mut nodes_in_sampling_order = Vec::with_capacity(definition.nodes.len()); - let mut nodes_by_name = HashMap::with_capacity(definition.nodes.len()); - - for (i, node_def) in definition.nodes.into_iter().enumerate() { - nodes_by_name.insert(node_def.name.clone(), i); - nodes_in_sampling_order.push(BayesianNode::new(node_def)); - } - - Ok(Self { - nodes_in_sampling_order, - nodes_by_name, - }) - } - - /// Get a node by name. - pub fn get_node(&self, name: &str) -> Option<&BayesianNode> { - self - .nodes_by_name - .get(name) - .map(|&i| &self.nodes_in_sampling_order[i]) - } - - /// Get possible values for a node. - pub fn get_possible_values(&self, name: &str) -> Option> { - self - .get_node(name) - .map(|node| node.possible_values().to_vec()) - } - - /// Generate a random sample from the network. - /// - /// `input_values` contains already known node values that should not be overwritten. - pub fn generate_sample(&self, input_values: &HashMap) -> HashMap { - let mut sample = input_values.clone(); - - for node in &self.nodes_in_sampling_order { - if !sample.contains_key(node.name()) { - let value = node.sample(&sample); - sample.insert(node.name().to_string(), value); - } - } - - sample - } - - /// Generate a random sample consistent with the given value restrictions. - /// - /// Uses backtracking to find a valid configuration. - /// Returns `None` if no consistent sample can be generated. - pub fn generate_consistent_sample_when_possible( - &self, - value_possibilities: &HashMap>, - ) -> Option> { - self.recursively_generate_consistent_sample(HashMap::new(), value_possibilities, 0) - } - - fn recursively_generate_consistent_sample( - &self, - sample_so_far: HashMap, - value_possibilities: &HashMap>, - depth: usize, - ) -> Option> { - if depth >= self.nodes_in_sampling_order.len() { - return Some(sample_so_far); - } - - let node = &self.nodes_in_sampling_order[depth]; - let mut banned_values: Vec = Vec::new(); - let mut sample_so_far = sample_so_far; - - loop { - let sample_value = node.sample_according_to_restrictions( - &sample_so_far, - value_possibilities.get(node.name()).map(|v| v.as_slice()), - &banned_values, - ); - - let Some(value) = sample_value else { - break; - }; - - sample_so_far.insert(node.name().to_string(), value.clone()); - - if let Some(complete_sample) = self.recursively_generate_consistent_sample( - sample_so_far.clone(), - value_possibilities, - depth + 1, - ) { - return Some(complete_sample); - } - - banned_values.push(value); - } - - None - } -} - -/// Errors that can occur when working with Bayesian networks. -#[derive(Debug, thiserror::Error)] -pub enum BayesianNetworkError { - #[error("ZIP file error: {0}")] - Zip(#[from] zip::result::ZipError), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("JSON parsing error: {0}")] - Json(#[from] serde_json::Error), - - #[error("No JSON file found in ZIP archive")] - NoJsonInZip, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_load_input_network() { - let zip_bytes = include_bytes!("../data/input-network-definition.zip"); - let network = BayesianNetwork::from_zip_bytes(zip_bytes); - assert!( - network.is_ok(), - "Failed to load input network: {:?}", - network.err() - ); - } - - #[test] - fn test_generate_sample_from_input_network() { - let zip_bytes = include_bytes!("../data/input-network-definition.zip"); - let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap(); - - let sample = network.generate_sample(&HashMap::new()); - assert!(!sample.is_empty(), "Sample should not be empty"); - } - - #[test] - fn test_generate_consistent_sample() { - let zip_bytes = include_bytes!("../data/input-network-definition.zip"); - let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap(); - - let mut constraints = HashMap::new(); - constraints.insert("*OPERATING_SYSTEM".to_string(), vec!["windows".to_string()]); - - let sample = network.generate_consistent_sample_when_possible(&constraints); - assert!(sample.is_some(), "Should generate a consistent sample"); - - if let Some(s) = sample { - assert_eq!(s.get("*OPERATING_SYSTEM"), Some(&"windows".to_string())); - } - } -} diff --git a/src-tauri/src/camoufox/fingerprint/bayesian_node.rs b/src-tauri/src/camoufox/fingerprint/bayesian_node.rs deleted file mode 100644 index 44bdbdf..0000000 --- a/src-tauri/src/camoufox/fingerprint/bayesian_node.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Bayesian network node implementation for fingerprint generation. -//! -//! Implements weighted random sampling from conditional probability distributions. - -use rand::RngExt; -use serde::Deserialize; -use std::collections::HashMap; - -/// Node definition from the network JSON file. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NodeDefinition { - pub name: String, - pub parent_names: Vec, - pub possible_values: Vec, - pub conditional_probabilities: ConditionalProbabilities, -} - -/// Conditional probability structure - can be nested or terminal. -#[derive(Debug, Clone, Deserialize)] -pub struct ConditionalProbabilities { - #[serde(default)] - pub deeper: Option>, - #[serde(default)] - pub skip: Option>, - #[serde(flatten)] - pub probabilities: HashMap, -} - -impl ConditionalProbabilities { - /// Check if this is a terminal node (has actual probabilities, not deeper nesting) - pub fn is_terminal(&self) -> bool { - self.deeper.is_none() - } -} - -/// A single node in the Bayesian network. -pub struct BayesianNode { - definition: NodeDefinition, -} - -impl BayesianNode { - pub fn new(definition: NodeDefinition) -> Self { - Self { definition } - } - - pub fn name(&self) -> &str { - &self.definition.name - } - - pub fn parent_names(&self) -> &[String] { - &self.definition.parent_names - } - - pub fn possible_values(&self) -> &[String] { - &self.definition.possible_values - } - - /// Get the probability distribution given parent node values. - fn get_probabilities_given_known_values( - &self, - parent_values: &HashMap, - ) -> HashMap { - let mut probabilities = &self.definition.conditional_probabilities; - - for parent_name in &self.definition.parent_names { - if let Some(deeper) = &probabilities.deeper { - if let Some(parent_value) = parent_values.get(parent_name) { - if let Some(next_level) = deeper.get(parent_value) { - probabilities = next_level; - continue; - } - } - // Use skip if parent value not found in deeper - if let Some(skip) = &probabilities.skip { - probabilities = skip; - } - } - } - - probabilities.probabilities.clone() - } - - /// Randomly sample from the given values using the given probabilities. - fn sample_random_value_from_possibilities( - possible_values: &[String], - total_probability: f64, - probabilities: &HashMap, - ) -> String { - if possible_values.is_empty() { - return String::new(); - } - - let mut rng = rand::rng(); - let anchor = rng.random::() * total_probability; - let mut cumulative = 0.0; - - for value in possible_values { - if let Some(&prob) = probabilities.get(value) { - cumulative += prob; - if cumulative > anchor { - return value.clone(); - } - } - } - - possible_values.first().cloned().unwrap_or_default() - } - - /// Sample a value from the conditional distribution given parent values. - pub fn sample(&self, parent_values: &HashMap) -> String { - let probabilities = self.get_probabilities_given_known_values(parent_values); - let possible_values: Vec = probabilities.keys().cloned().collect(); - - Self::sample_random_value_from_possibilities(&possible_values, 1.0, &probabilities) - } - - /// Sample according to restrictions on possible values. - /// - /// Returns `None` if no valid value can be sampled. - pub fn sample_according_to_restrictions( - &self, - parent_values: &HashMap, - value_possibilities: Option<&[String]>, - banned_values: &[String], - ) -> Option { - let probabilities = self.get_probabilities_given_known_values(parent_values); - let values_in_distribution: Vec = probabilities.keys().cloned().collect(); - - let possible_values = value_possibilities.unwrap_or(&values_in_distribution); - - let mut valid_values = Vec::new(); - let mut total_probability = 0.0; - - for value in possible_values { - if !banned_values.contains(value) && values_in_distribution.contains(value) { - if let Some(&prob) = probabilities.get(value) { - valid_values.push(value.clone()); - total_probability += prob; - } - } - } - - if valid_values.is_empty() { - return None; - } - - Some(Self::sample_random_value_from_possibilities( - &valid_values, - total_probability, - &probabilities, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_node() -> BayesianNode { - let mut probs = HashMap::new(); - probs.insert("1920".to_string(), 0.5); - probs.insert("1366".to_string(), 0.3); - probs.insert("1536".to_string(), 0.2); - - let definition = NodeDefinition { - name: "screen.width".to_string(), - parent_names: vec![], - possible_values: vec!["1920".to_string(), "1366".to_string(), "1536".to_string()], - conditional_probabilities: ConditionalProbabilities { - deeper: None, - skip: None, - probabilities: probs, - }, - }; - - BayesianNode::new(definition) - } - - #[test] - fn test_sample_returns_valid_value() { - let node = create_test_node(); - let parent_values = HashMap::new(); - - for _ in 0..100 { - let value = node.sample(&parent_values); - assert!( - node.possible_values().contains(&value), - "Sampled value '{}' not in possible values", - value - ); - } - } - - #[test] - fn test_sample_with_restrictions() { - let node = create_test_node(); - let parent_values = HashMap::new(); - - let allowed = vec!["1920".to_string()]; - let banned = vec![]; - - let value = node.sample_according_to_restrictions(&parent_values, Some(&allowed), &banned); - - assert_eq!(value, Some("1920".to_string())); - } - - #[test] - fn test_sample_with_banned_values() { - let node = create_test_node(); - let parent_values = HashMap::new(); - - let banned = vec!["1920".to_string(), "1366".to_string()]; - - for _ in 0..100 { - let value = node.sample_according_to_restrictions(&parent_values, None, &banned); - assert_eq!(value, Some("1536".to_string())); - } - } - - #[test] - fn test_sample_returns_none_when_all_banned() { - let node = create_test_node(); - let parent_values = HashMap::new(); - - let banned = vec!["1920".to_string(), "1366".to_string(), "1536".to_string()]; - - let value = node.sample_according_to_restrictions(&parent_values, None, &banned); - assert!(value.is_none()); - } -} diff --git a/src-tauri/src/camoufox/fingerprint/mod.rs b/src-tauri/src/camoufox/fingerprint/mod.rs deleted file mode 100644 index 52c8e5f..0000000 --- a/src-tauri/src/camoufox/fingerprint/mod.rs +++ /dev/null @@ -1,569 +0,0 @@ -//! Fingerprint generation module. -//! -//! Generates realistic browser fingerprints using Bayesian networks trained on real browser data. - -pub mod bayesian_network; -pub mod bayesian_node; -pub mod types; - -use bayesian_network::{BayesianNetwork, BayesianNetworkError}; -use std::collections::HashMap; -use types::*; - -use crate::camoufox::data; - -/// Fingerprint generator using Bayesian networks. -pub struct FingerprintGenerator { - fingerprint_network: BayesianNetwork, - input_network: BayesianNetwork, - header_network: BayesianNetwork, - browser_helper: Vec, - headers_order: HashMap>, -} - -/// Parsed browser/HTTP version info. -#[derive(Debug, Clone)] -pub struct BrowserHttpInfo { - pub name: String, - pub version: Vec, - pub http_version: String, - pub complete_string: String, -} - -impl BrowserHttpInfo { - fn parse(s: &str) -> Option { - if s == MISSING_VALUE_DATASET_TOKEN { - return None; - } - - let parts: Vec<&str> = s.split('|').collect(); - if parts.len() != 2 { - return None; - } - - let browser_string = parts[0]; - let http_version = parts[1].to_string(); - - let browser_parts: Vec<&str> = browser_string.split('/').collect(); - if browser_parts.len() != 2 { - return None; - } - - let name = browser_parts[0].to_string(); - let version: Vec = browser_parts[1] - .split('.') - .filter_map(|v| v.parse().ok()) - .collect(); - - Some(Self { - name, - version, - http_version, - complete_string: s.to_string(), - }) - } - - pub fn major_version(&self) -> u32 { - self.version.first().copied().unwrap_or(0) - } -} - -/// Error type for fingerprint generation. -#[derive(Debug, thiserror::Error)] -pub enum FingerprintError { - #[error("Bayesian network error: {0}")] - Network(#[from] BayesianNetworkError), - - #[error("JSON parsing error: {0}")] - Json(#[from] serde_json::Error), - - #[error("Failed to generate consistent fingerprint after {0} attempts")] - GenerationFailed(u32), - - #[error("No valid fingerprint generated")] - NoValidFingerprint, -} - -impl FingerprintGenerator { - /// Create a new fingerprint generator. - pub fn new() -> Result { - let fingerprint_network = BayesianNetwork::from_zip_bytes(data::FINGERPRINT_NETWORK_ZIP)?; - let input_network = BayesianNetwork::from_zip_bytes(data::INPUT_NETWORK_ZIP)?; - let header_network = BayesianNetwork::from_zip_bytes(data::HEADER_NETWORK_ZIP)?; - - let browser_strings: Vec = serde_json::from_str(data::BROWSER_HELPER_JSON)?; - let browser_helper: Vec = browser_strings - .iter() - .filter_map(|s| BrowserHttpInfo::parse(s)) - .collect(); - - let headers_order: HashMap> = - serde_json::from_str(data::HEADERS_ORDER_JSON)?; - - Ok(Self { - fingerprint_network, - input_network, - header_network, - browser_helper, - headers_order, - }) - } - - /// Generate a fingerprint with matching headers. - pub fn get_fingerprint( - &self, - options: &FingerprintOptions, - ) -> Result { - const MAX_RETRIES: u32 = 10; - - // Build constraints from options - let mut value_possibilities = self.build_constraints(options); - - // Handle screen constraints - let screen_values = if let Some(screen_constraints) = &options.screen { - self.filter_screen_values(screen_constraints) - } else { - None - }; - - if let Some(sv) = screen_values { - value_possibilities.insert("screen".to_string(), sv); - } - - for attempt in 0..MAX_RETRIES { - // Generate input sample consistent with constraints - let input_sample = self - .input_network - .generate_consistent_sample_when_possible(&value_possibilities); - - let Some(input_sample) = input_sample else { - continue; - }; - - // Generate header sample from input - let header_sample = self.header_network.generate_sample(&input_sample); - - // Extract user agent - let user_agent = header_sample - .get("user-agent") - .or_else(|| header_sample.get("User-Agent")) - .cloned() - .unwrap_or_default(); - - // Build fingerprint constraints with the generated user agent - let mut fp_constraints = value_possibilities.clone(); - fp_constraints.insert("userAgent".to_string(), vec![user_agent.clone()]); - - // Generate fingerprint sample - let fingerprint_sample = self - .fingerprint_network - .generate_consistent_sample_when_possible(&fp_constraints); - - let Some(fp_sample) = fingerprint_sample else { - log::debug!( - "Failed to generate fingerprint on attempt {}, retrying", - attempt + 1 - ); - continue; - }; - - // Transform the sample to a Fingerprint struct - match self.transform_sample(&fp_sample, &header_sample, options) { - Ok(result) => return Ok(result), - Err(e) => { - log::debug!( - "Failed to transform fingerprint on attempt {}: {}", - attempt + 1, - e - ); - continue; - } - } - } - - Err(FingerprintError::GenerationFailed(MAX_RETRIES)) - } - - /// Build constraint map from options. - fn build_constraints(&self, options: &FingerprintOptions) -> HashMap> { - let mut constraints = HashMap::new(); - - // Operating system constraint - if let Some(os) = &options.operating_system { - constraints.insert(OPERATING_SYSTEM_NODE_NAME.to_string(), vec![os.clone()]); - } - - // Device constraint (default to desktop) - let devices = options - .devices - .clone() - .unwrap_or_else(|| vec!["desktop".to_string()]); - constraints.insert(DEVICE_NODE_NAME.to_string(), devices); - - // Browser constraint - let browsers = options - .browsers - .clone() - .unwrap_or_else(|| SUPPORTED_BROWSERS.iter().map(|s| s.to_string()).collect()); - - let http_version = options - .http_version - .clone() - .unwrap_or_else(|| "2".to_string()); - - // Filter browser helper entries by browser names and HTTP version - let browser_http_values: Vec = self - .browser_helper - .iter() - .filter(|bh| browsers.contains(&bh.name) && bh.http_version == http_version) - .map(|bh| bh.complete_string.clone()) - .collect(); - - if !browser_http_values.is_empty() { - constraints.insert(BROWSER_HTTP_NODE_NAME.to_string(), browser_http_values); - } - - constraints - } - - /// Filter screen values based on constraints. - fn filter_screen_values(&self, constraints: &ScreenConstraints) -> Option> { - let possible_values = self.fingerprint_network.get_possible_values("screen")?; - - let filtered: Vec = possible_values - .into_iter() - .filter(|screen_str| { - // Screen values are stored as "*STRINGIFIED*{...json...}" - if let Some(json_str) = screen_str.strip_prefix(STRINGIFIED_PREFIX) { - if let Ok(screen) = serde_json::from_str::(json_str) { - let width = screen["width"].as_u64().unwrap_or(0) as u32; - let height = screen["height"].as_u64().unwrap_or(0) as u32; - return constraints.matches(width, height); - } - } - true - }) - .collect(); - - if filtered.is_empty() { - None - } else { - Some(filtered) - } - } - - /// Transform raw sample data into a Fingerprint struct. - fn transform_sample( - &self, - fp_sample: &HashMap, - header_sample: &HashMap, - options: &FingerprintOptions, - ) -> Result { - // Parse values, handling STRINGIFIED prefix and MISSING_VALUE token - let mut parsed: HashMap = HashMap::new(); - - for (key, value) in fp_sample { - if value == MISSING_VALUE_DATASET_TOKEN { - continue; - } - - let parsed_value = if let Some(json_str) = value.strip_prefix(STRINGIFIED_PREFIX) { - serde_json::from_str(json_str)? - } else { - serde_json::Value::String(value.clone()) - }; - - parsed.insert(key.clone(), parsed_value); - } - - // Check if screen was generated - let screen_value = parsed.get("screen"); - if screen_value.is_none() { - return Err(FingerprintError::NoValidFingerprint); - } - - // Extract screen fingerprint - let screen = if let Some(screen_val) = screen_value { - serde_json::from_value(screen_val.clone()).unwrap_or_default() - } else { - ScreenFingerprint::default() - }; - - // Build languages from Accept-Language header - let accept_language = header_sample - .get("accept-language") - .or_else(|| header_sample.get("Accept-Language")) - .cloned() - .unwrap_or_else(|| "en-US".to_string()); - - let languages: Vec = accept_language - .split(',') - .map(|s| s.split(';').next().unwrap_or(s).trim().to_string()) - .collect(); - - let language = languages - .first() - .cloned() - .unwrap_or_else(|| "en-US".to_string()); - - // Build navigator fingerprint - let navigator = NavigatorFingerprint { - user_agent: get_string(&parsed, "userAgent"), - user_agent_data: parsed - .get("userAgentData") - .and_then(|v| serde_json::from_value(v.clone()).ok()), - do_not_track: parsed - .get("doNotTrack") - .and_then(|v| v.as_str().map(String::from)), - app_code_name: get_string_or(&parsed, "appCodeName", "Mozilla"), - app_name: get_string_or(&parsed, "appName", "Netscape"), - app_version: get_string(&parsed, "appVersion"), - oscpu: parsed - .get("oscpu") - .and_then(|v| v.as_str().map(String::from)), - webdriver: parsed - .get("webdriver") - .and_then(|v| v.as_str().map(String::from)), - language, - languages, - platform: get_string(&parsed, "platform"), - device_memory: parsed - .get("deviceMemory") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()), - hardware_concurrency: parsed - .get("hardwareConcurrency") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()) - .unwrap_or(4), - product: get_string_or(&parsed, "product", "Gecko"), - product_sub: get_string(&parsed, "productSub"), - vendor: get_string(&parsed, "vendor"), - vendor_sub: get_string(&parsed, "vendorSub"), - max_touch_points: parsed - .get("maxTouchPoints") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()) - .unwrap_or(0), - extra_properties: parsed - .get("extraProperties") - .and_then(|v| serde_json::from_value(v.clone()).ok()), - }; - - // Build video card (will be filled later by WebGL sampler) - let video_card = parsed - .get("videoCard") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - // Build other components - let audio_codecs = parsed - .get("audioCodecs") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - let video_codecs = parsed - .get("videoCodecs") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - let plugins_data = parsed - .get("pluginsData") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - let battery = parsed - .get("battery") - .and_then(|v| serde_json::from_value(v.clone()).ok()); - - let multimedia_devices = parsed - .get("multimediaDevices") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - let fonts = parsed - .get("fonts") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_default(); - - let fingerprint = Fingerprint { - screen, - navigator, - video_codecs, - audio_codecs, - plugins_data, - battery, - video_card, - multimedia_devices, - fonts, - mock_web_rtc: options.mock_web_rtc, - slim: options.slim, - }; - - // Build headers (filter out internal nodes and missing values) - let headers: Headers = header_sample - .iter() - .filter(|(k, v)| !k.starts_with('*') && *v != MISSING_VALUE_DATASET_TOKEN) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - // Order headers - let ordered_headers = self.order_headers(&headers, &fingerprint.navigator.user_agent); - - Ok(FingerprintWithHeaders { - fingerprint, - headers: ordered_headers, - }) - } - - /// Order headers according to browser-specific ordering. - fn order_headers(&self, headers: &Headers, user_agent: &str) -> Headers { - let browser = detect_browser_from_ua(user_agent); - let order = self.headers_order.get(browser).cloned().unwrap_or_default(); - - let mut ordered = HashMap::new(); - - // Add headers in order - for header_name in &order { - if let Some(value) = headers.get(header_name) { - ordered.insert(header_name.clone(), value.clone()); - } - } - - // Add remaining headers not in order - for (key, value) in headers { - if !order.contains(key) { - ordered.insert(key.clone(), value.clone()); - } - } - - ordered - } -} - -fn get_string(map: &HashMap, key: &str) -> String { - map - .get(key) - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_default() -} - -fn get_string_or(map: &HashMap, key: &str, default: &str) -> String { - map - .get(key) - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| default.to_string()) -} - -fn detect_browser_from_ua(user_agent: &str) -> &str { - let ua_lower = user_agent.to_lowercase(); - if ua_lower.contains("firefox") { - "firefox" - } else if ua_lower.contains("edg/") || ua_lower.contains("edge") { - "edge" - } else if ua_lower.contains("chrome") { - "chrome" - } else if ua_lower.contains("safari") { - "safari" - } else { - "chrome" - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_generator() { - let generator = FingerprintGenerator::new(); - assert!( - generator.is_ok(), - "Failed to create generator: {:?}", - generator.err() - ); - } - - #[test] - fn test_generate_fingerprint() { - let generator = FingerprintGenerator::new().unwrap(); - let options = FingerprintOptions::default(); - - let result = generator.get_fingerprint(&options); - assert!( - result.is_ok(), - "Failed to generate fingerprint: {:?}", - result.err() - ); - - if let Ok(fp) = result { - assert!(!fp.fingerprint.navigator.user_agent.is_empty()); - assert!(fp.fingerprint.screen.width > 0); - assert!(fp.fingerprint.screen.height > 0); - } - } - - #[test] - fn test_generate_firefox_fingerprint() { - let generator = FingerprintGenerator::new().unwrap(); - let options = FingerprintOptions { - browsers: Some(vec!["firefox".to_string()]), - ..Default::default() - }; - - let result = generator.get_fingerprint(&options); - assert!(result.is_ok(), "Failed to generate Firefox fingerprint"); - - if let Ok(fp) = result { - assert!( - fp.fingerprint - .navigator - .user_agent - .to_lowercase() - .contains("firefox"), - "User agent should contain Firefox: {}", - fp.fingerprint.navigator.user_agent - ); - } - } - - #[test] - fn test_generate_with_screen_constraints() { - let generator = FingerprintGenerator::new().unwrap(); - let options = FingerprintOptions { - screen: Some(ScreenConstraints { - min_width: Some(1900), - max_width: Some(1920), - min_height: Some(1000), - max_height: Some(1100), - }), - ..Default::default() - }; - - let result = generator.get_fingerprint(&options); - assert!( - result.is_ok(), - "Failed to generate fingerprint with screen constraints" - ); - - if let Ok(fp) = result { - assert!( - fp.fingerprint.screen.width >= 1900 && fp.fingerprint.screen.width <= 1920, - "Screen width {} should be between 1900 and 1920", - fp.fingerprint.screen.width - ); - } - } - - #[test] - fn test_browser_http_info_parse() { - let info = BrowserHttpInfo::parse("chrome/143.0.0.0|2"); - assert!(info.is_some()); - let info = info.unwrap(); - assert_eq!(info.name, "chrome"); - assert_eq!(info.major_version(), 143); - assert_eq!(info.http_version, "2"); - } -} diff --git a/src-tauri/src/camoufox/fingerprint/types.rs b/src-tauri/src/camoufox/fingerprint/types.rs deleted file mode 100644 index e348ef4..0000000 --- a/src-tauri/src/camoufox/fingerprint/types.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Fingerprint type definitions. -//! -//! These types represent browser fingerprints that can be injected into Camoufox. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// A complete browser fingerprint. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct Fingerprint { - pub screen: ScreenFingerprint, - pub navigator: NavigatorFingerprint, - #[serde(default)] - pub video_codecs: HashMap, - #[serde(default)] - pub audio_codecs: HashMap, - #[serde(default)] - pub plugins_data: HashMap, - #[serde(default)] - pub battery: Option, - pub video_card: VideoCard, - #[serde(default)] - pub multimedia_devices: Vec, - #[serde(default)] - pub fonts: Vec, - #[serde(default)] - pub mock_web_rtc: bool, - #[serde(default)] - pub slim: bool, -} - -/// Screen-related fingerprint properties. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct ScreenFingerprint { - pub width: u32, - pub height: u32, - pub avail_width: u32, - pub avail_height: u32, - #[serde(default)] - pub avail_top: u32, - #[serde(default)] - pub avail_left: u32, - pub color_depth: u32, - pub pixel_depth: u32, - #[serde(default = "default_device_pixel_ratio")] - pub device_pixel_ratio: f64, - #[serde(default)] - pub page_x_offset: f64, - #[serde(default)] - pub page_y_offset: f64, - pub inner_width: u32, - pub inner_height: u32, - pub outer_width: u32, - pub outer_height: u32, - #[serde(default)] - pub screen_x: i32, - #[serde(default)] - pub screen_y: i32, - #[serde(default)] - pub client_width: Option, - #[serde(default)] - pub client_height: Option, - #[serde(default)] - pub has_hdr: bool, -} - -fn default_device_pixel_ratio() -> f64 { - 1.0 -} - -/// Brand information for User-Agent Client Hints. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Brand { - pub brand: String, - pub version: String, -} - -/// User-Agent Client Hints data. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct UserAgentData { - #[serde(default)] - pub brands: Vec, - #[serde(default)] - pub mobile: bool, - #[serde(default)] - pub platform: String, - #[serde(default)] - pub architecture: String, - #[serde(default)] - pub bitness: String, - #[serde(default)] - pub full_version_list: Vec, - #[serde(default)] - pub model: String, - #[serde(default)] - pub platform_version: String, - #[serde(default)] - pub ua_full_version: String, -} - -/// Extra navigator properties. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct ExtraProperties { - #[serde(default)] - pub vendor_flavors: Vec, - #[serde(default)] - pub is_bluetooth_supported: bool, - #[serde(default)] - pub global_privacy_control: Option, - #[serde(default = "default_pdf_viewer_enabled")] - pub pdf_viewer_enabled: bool, - #[serde(default)] - pub installed_apps: Vec, -} - -fn default_pdf_viewer_enabled() -> bool { - true -} - -/// Navigator-related fingerprint properties. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct NavigatorFingerprint { - pub user_agent: String, - #[serde(default)] - pub user_agent_data: Option, - #[serde(default)] - pub do_not_track: Option, - #[serde(default = "default_app_code_name")] - pub app_code_name: String, - #[serde(default = "default_app_name")] - pub app_name: String, - #[serde(default)] - pub app_version: String, - #[serde(default)] - pub oscpu: Option, - #[serde(default)] - pub webdriver: Option, - pub language: String, - pub languages: Vec, - pub platform: String, - #[serde(default)] - pub device_memory: Option, - pub hardware_concurrency: u32, - #[serde(default = "default_product")] - pub product: String, - #[serde(default)] - pub product_sub: String, - #[serde(default)] - pub vendor: String, - #[serde(default)] - pub vendor_sub: String, - #[serde(default)] - pub max_touch_points: u32, - #[serde(default)] - pub extra_properties: Option, -} - -fn default_app_code_name() -> String { - "Mozilla".to_string() -} - -fn default_app_name() -> String { - "Netscape".to_string() -} - -fn default_product() -> String { - "Gecko".to_string() -} - -/// WebGL video card information. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct VideoCard { - pub vendor: String, - pub renderer: String, -} - -/// Battery status fingerprint. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BatteryFingerprint { - pub charging: bool, - pub charging_time: f64, - pub discharging_time: f64, - pub level: f64, -} - -/// HTTP headers for a fingerprint. -pub type Headers = HashMap; - -/// A fingerprint combined with matching HTTP headers. -#[derive(Debug, Clone)] -pub struct FingerprintWithHeaders { - pub fingerprint: Fingerprint, - pub headers: Headers, -} - -/// Options for generating fingerprints. -#[derive(Debug, Clone, Default)] -pub struct FingerprintOptions { - /// Target operating system: "windows", "macos", "linux" - pub operating_system: Option, - /// Target browser: "firefox", "chrome", "safari", "edge" - pub browsers: Option>, - /// Target device type: "desktop", "mobile" - pub devices: Option>, - /// Locales for Accept-Language header - pub locales: Option>, - /// HTTP version: "1" or "2" - pub http_version: Option, - /// Screen dimension constraints - pub screen: Option, - /// Whether to mock WebRTC - pub mock_web_rtc: bool, - /// Slim mode (fewer evasions) - pub slim: bool, -} - -/// Constraints for screen dimensions. -#[derive(Debug, Clone, Default)] -pub struct ScreenConstraints { - pub min_width: Option, - pub max_width: Option, - pub min_height: Option, - pub max_height: Option, -} - -impl ScreenConstraints { - pub fn new() -> Self { - Self::default() - } - - pub fn with_min_width(mut self, width: u32) -> Self { - self.min_width = Some(width); - self - } - - pub fn with_max_width(mut self, width: u32) -> Self { - self.max_width = Some(width); - self - } - - pub fn with_min_height(mut self, height: u32) -> Self { - self.min_height = Some(height); - self - } - - pub fn with_max_height(mut self, height: u32) -> Self { - self.max_height = Some(height); - self - } - - /// Check if a screen size matches these constraints. - pub fn matches(&self, width: u32, height: u32) -> bool { - if let Some(min_w) = self.min_width { - if width < min_w { - return false; - } - } - if let Some(max_w) = self.max_width { - if width > max_w { - return false; - } - } - if let Some(min_h) = self.min_height { - if height < min_h { - return false; - } - } - if let Some(max_h) = self.max_height { - if height > max_h { - return false; - } - } - true - } -} - -/// Constants used in fingerprint generation. -pub const MISSING_VALUE_DATASET_TOKEN: &str = "*MISSING_VALUE*"; -pub const STRINGIFIED_PREFIX: &str = "*STRINGIFIED*"; - -/// Special node names in the Bayesian networks. -pub const BROWSER_HTTP_NODE_NAME: &str = "*BROWSER_HTTP"; -pub const OPERATING_SYSTEM_NODE_NAME: &str = "*OPERATING_SYSTEM"; -pub const DEVICE_NODE_NAME: &str = "*DEVICE"; - -/// Supported browsers. -pub const SUPPORTED_BROWSERS: &[&str] = &["chrome", "firefox", "safari", "edge"]; - -/// Supported operating systems. -pub const SUPPORTED_OPERATING_SYSTEMS: &[&str] = &["windows", "macos", "linux", "android", "ios"]; - -/// Supported devices. -pub const SUPPORTED_DEVICES: &[&str] = &["desktop", "mobile"]; - -/// Supported HTTP versions. -pub const SUPPORTED_HTTP_VERSIONS: &[&str] = &["1", "2"]; diff --git a/src-tauri/src/camoufox/fonts.rs b/src-tauri/src/camoufox/fonts.rs deleted file mode 100644 index 2a4c2b5..0000000 --- a/src-tauri/src/camoufox/fonts.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! OS-specific font lists for Camoufox. -//! -//! Provides default system fonts for Windows, macOS, and Linux. - -use std::collections::HashMap; - -use crate::camoufox::data; - -/// Get fonts for the target OS. -pub fn get_fonts_for_os(target_os: &str) -> Vec { - let fonts_map: HashMap> = - serde_json::from_str(data::FONTS_JSON).unwrap_or_default(); - - let os_key = match target_os { - "win" | "windows" => "win", - "mac" | "macos" => "mac", - "lin" | "linux" => "lin", - _ => "win", // Default to Windows fonts - }; - - fonts_map.get(os_key).cloned().unwrap_or_default() -} - -/// Get fonts for the target OS with additional custom fonts. -pub fn get_fonts_with_custom(target_os: &str, custom_fonts: Option<&[String]>) -> Vec { - let mut fonts = get_fonts_for_os(target_os); - - if let Some(custom) = custom_fonts { - // Add custom fonts, avoiding duplicates - for font in custom { - if !fonts.contains(font) { - fonts.push(font.clone()); - } - } - } - - fonts -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_fonts_for_windows() { - let fonts = get_fonts_for_os("win"); - assert!(!fonts.is_empty()); - assert!(fonts.contains(&"Arial".to_string())); - assert!(fonts.contains(&"Calibri".to_string())); - } - - #[test] - fn test_get_fonts_for_macos() { - let fonts = get_fonts_for_os("mac"); - assert!(!fonts.is_empty()); - assert!(fonts.contains(&"Helvetica".to_string())); - } - - #[test] - fn test_get_fonts_for_linux() { - let fonts = get_fonts_for_os("lin"); - assert!(!fonts.is_empty()); - } - - #[test] - fn test_get_fonts_with_custom() { - let custom = vec!["MyCustomFont".to_string()]; - let fonts = get_fonts_with_custom("win", Some(&custom)); - - assert!(fonts.contains(&"MyCustomFont".to_string())); - assert!(fonts.contains(&"Arial".to_string())); - } - - #[test] - fn test_fonts_no_duplicates() { - let custom = vec!["Arial".to_string()]; // Arial already exists in Windows fonts - let fonts = get_fonts_with_custom("win", Some(&custom)); - - // Count occurrences of Arial - let arial_count = fonts.iter().filter(|f| *f == "Arial").count(); - assert_eq!(arial_count, 1); - } -} diff --git a/src-tauri/src/camoufox/launcher.rs b/src-tauri/src/camoufox/launcher.rs deleted file mode 100644 index 99c13be..0000000 --- a/src-tauri/src/camoufox/launcher.rs +++ /dev/null @@ -1,338 +0,0 @@ -//! Camoufox browser launcher using playwright-rust. -//! -//! Provides functionality to launch Camoufox browser instances with fingerprint injection. - -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use playwright::api::{Browser, BrowserContext, Playwright, ProxySettings}; -use playwright::Error as PlaywrightError; - -use crate::camoufox::config::{CamoufoxConfigBuilder, CamoufoxLaunchConfig, ProxyConfig}; -use crate::camoufox::fingerprint::types::{Fingerprint, ScreenConstraints}; - -/// Camoufox launcher for creating browser instances. -pub struct CamoufoxLauncher { - playwright: Arc, - executable_path: PathBuf, -} - -/// Error type for launcher operations. -#[derive(Debug, thiserror::Error)] -pub enum LauncherError { - #[error("Playwright error: {0}")] - Playwright(PlaywrightError), - - #[error("Playwright Arc error: {0}")] - PlaywrightArc(#[from] Arc), - - #[error("Configuration error: {0}")] - Config(#[from] crate::camoufox::config::ConfigError), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Camoufox executable not found at: {0}")] - ExecutableNotFound(PathBuf), - - #[error("Failed to generate environment variables: {0}")] - EnvVars(#[from] serde_json::Error), -} - -/// Options for launching Camoufox. -#[derive(Debug, Clone, Default)] -pub struct LaunchOptions { - /// Operating system to spoof: "windows", "macos", "linux" - pub os: Option, - /// Block all images - pub block_images: bool, - /// Block WebRTC entirely - pub block_webrtc: bool, - /// Block WebGL (not recommended unless necessary) - pub block_webgl: bool, - /// Screen dimension constraints - pub screen: Option, - /// Fixed window size [width, height] - pub window: Option<(u32, u32)>, - /// Custom fingerprint (if not provided, one will be generated) - pub fingerprint: Option, - /// Run in headless mode - pub headless: bool, - /// Custom fonts to load - pub fonts: Option>, - /// Only use custom fonts (disable OS fonts) - pub custom_fonts_only: bool, - /// Firefox user preferences - pub firefox_user_prefs: Option>, - /// Proxy configuration - pub proxy: Option, - /// Additional browser arguments - pub args: Option>, - /// Additional environment variables - pub env: Option>, - /// Profile/user data directory - pub user_data_dir: Option, - /// Enable debug output - pub debug: bool, -} - -impl CamoufoxLauncher { - /// Create a new Camoufox launcher. - pub async fn new(executable_path: impl AsRef) -> Result { - let executable_path = executable_path.as_ref().to_path_buf(); - - if !executable_path.exists() { - return Err(LauncherError::ExecutableNotFound(executable_path)); - } - - let playwright = Playwright::initialize() - .await - .map_err(LauncherError::Playwright)?; - - Ok(Self { - playwright: Arc::new(playwright), - executable_path, - }) - } - - /// Launch a new Camoufox browser instance. - pub async fn launch(&self, options: LaunchOptions) -> Result { - let config = self.build_config(&options)?; - - if options.debug { - log::debug!("Camoufox config: {:?}", config.fingerprint_config); - } - - // Get environment variables - let env_vars = config.get_env_vars()?; - - // Build launch arguments - let mut args = options.args.clone().unwrap_or_default(); - - // Add headless flag if needed - if options.headless { - args.push("--headless".to_string()); - } - - // Merge environment variables - let mut env = options.env.clone().unwrap_or_default(); - for (key, value) in env_vars { - env.insert(key, value); - } - - // Handle fontconfig on Linux - if cfg!(target_os = "linux") { - if let Some(fontconfig_path) = - crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path) - { - env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path); - } - } - - // Build Firefox user prefs - let mut firefox_prefs = config.firefox_prefs.clone(); - if let Some(user_prefs) = options.firefox_user_prefs { - for (key, value) in user_prefs { - firefox_prefs.insert(key, value); - } - } - - // Get the Firefox browser type - let firefox = self.playwright.firefox(); - - // Build launch options - let mut launch_options = firefox.launcher(); - launch_options = launch_options.executable(&self.executable_path); - launch_options = launch_options.headless(options.headless); - - // Add args - if !args.is_empty() { - launch_options = launch_options.args(&args); - } - - // Add environment as serde_json::Map - if !env.is_empty() { - let env_map: serde_json::Map = env - .into_iter() - .map(|(k, v)| (k, serde_json::Value::String(v))) - .collect(); - launch_options = launch_options.env(env_map); - } - - // Add proxy if configured - if let Some(proxy) = &config.proxy { - let proxy_settings = ProxySettings { - server: proxy.server.clone(), - username: proxy.username.clone(), - password: proxy.password.clone(), - bypass: proxy.bypass.clone(), - }; - launch_options = launch_options.proxy(proxy_settings); - } - - // Add Firefox preferences - if !firefox_prefs.is_empty() { - let prefs_map: serde_json::Map = - firefox_prefs.into_iter().collect(); - launch_options = launch_options.firefox_user_prefs(prefs_map); - } - - // Launch the browser - let browser = launch_options.launch().await?; - - Ok(browser) - } - - /// Launch a persistent browser context. - pub async fn launch_persistent_context( - &self, - user_data_dir: impl AsRef, - options: LaunchOptions, - ) -> Result { - let config = self.build_config(&options)?; - - if options.debug { - log::debug!("Camoufox config: {:?}", config.fingerprint_config); - } - - // Get environment variables - let env_vars = config.get_env_vars()?; - - // Build launch arguments - let mut args = options.args.clone().unwrap_or_default(); - - if options.headless { - args.push("--headless".to_string()); - } - - // Merge environment variables - let mut env = options.env.clone().unwrap_or_default(); - for (key, value) in env_vars { - env.insert(key, value); - } - - // Handle fontconfig on Linux - if cfg!(target_os = "linux") { - if let Some(fontconfig_path) = - crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path) - { - env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path); - } - } - - // Build Firefox user prefs - let mut firefox_prefs = config.firefox_prefs.clone(); - if let Some(user_prefs) = options.firefox_user_prefs { - for (key, value) in user_prefs { - firefox_prefs.insert(key, value); - } - } - - // Get the Firefox browser type - let firefox = self.playwright.firefox(); - - // Build persistent context options - let mut context_options = firefox.persistent_context_launcher(user_data_dir.as_ref()); - context_options = context_options.executable(&self.executable_path); - context_options = context_options.headless(options.headless); - - // Add args - if !args.is_empty() { - context_options = context_options.args(&args); - } - - // Add environment as serde_json::Map - if !env.is_empty() { - let env_map: serde_json::Map = env - .into_iter() - .map(|(k, v)| (k, serde_json::Value::String(v))) - .collect(); - context_options = context_options.env(env_map); - } - - // Add proxy if configured - if let Some(proxy) = &config.proxy { - let proxy_settings = ProxySettings { - server: proxy.server.clone(), - username: proxy.username.clone(), - password: proxy.password.clone(), - bypass: proxy.bypass.clone(), - }; - context_options = context_options.proxy(proxy_settings); - } - - // Note: PersistentContextLauncher doesn't support firefox_user_prefs - // Firefox preferences should be set via about:config or prefs.js in the profile - - // Launch the persistent context - let context = context_options.launch().await?; - - Ok(context) - } - - /// Build Camoufox configuration from launch options. - fn build_config(&self, options: &LaunchOptions) -> Result { - let mut builder = CamoufoxConfigBuilder::new(); - - if let Some(os) = &options.os { - builder = builder.operating_system(os); - } - - if let Some(screen) = &options.screen { - builder = builder.screen_constraints(screen.clone()); - } - - if let Some(fingerprint) = &options.fingerprint { - builder = builder.fingerprint(fingerprint.clone()); - } - - builder = builder.block_images(options.block_images); - builder = builder.block_webrtc(options.block_webrtc); - builder = builder.block_webgl(options.block_webgl); - builder = builder.headless(options.headless); - - if let Some(fonts) = &options.fonts { - builder = builder.custom_fonts(fonts.clone()); - } - - builder = builder.custom_fonts_only(options.custom_fonts_only); - - if let Some(proxy) = &options.proxy { - builder = builder.proxy(proxy.clone()); - } - - // Get Firefox version from executable - if let Some(version) = crate::camoufox::config::get_firefox_version(&self.executable_path) { - builder = builder.ff_version(version); - } - - Ok(builder.build()?) - } - - /// Get the executable path. - pub fn executable_path(&self) -> &Path { - &self.executable_path - } -} - -/// Convenience function to launch Camoufox with default settings. -pub async fn launch_camoufox( - executable_path: impl AsRef, - options: LaunchOptions, -) -> Result { - let launcher = CamoufoxLauncher::new(executable_path).await?; - launcher.launch(options).await -} - -/// Convenience function to launch a persistent Camoufox context. -pub async fn launch_persistent_camoufox( - executable_path: impl AsRef, - user_data_dir: impl AsRef, - options: LaunchOptions, -) -> Result { - let launcher = CamoufoxLauncher::new(executable_path).await?; - launcher - .launch_persistent_context(user_data_dir, options) - .await -} diff --git a/src-tauri/src/camoufox/mod.rs b/src-tauri/src/camoufox/mod.rs deleted file mode 100644 index e4b43d8..0000000 --- a/src-tauri/src/camoufox/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Camoufox browser integration module. -//! -//! Provides native Rust support for launching Camoufox browsers with realistic -//! fingerprint injection using playwright-rust. -//! -//! # Overview -//! -//! This module replaces the previous Node.js-based nodecar implementation with -//! a pure Rust solution. Key components: -//! -//! - **Fingerprint Generation**: Bayesian network-based fingerprint generation -//! - **WebGL Sampling**: Realistic WebGL configurations from a SQLite database -//! - **Configuration Builder**: Converts fingerprints to Camoufox config format -//! - **Launcher**: playwright-rust integration for browser launching -//! -//! # Example -//! -//! ```rust,ignore -//! use donutbrowser_lib::camoufox::{CamoufoxLauncher, LaunchOptions}; -//! -//! async fn launch_browser() -> Result<(), Box> { -//! let launcher = CamoufoxLauncher::new("/path/to/camoufox").await?; -//! -//! let options = LaunchOptions { -//! os: Some("windows".to_string()), -//! headless: false, -//! ..Default::default() -//! }; -//! -//! let browser = launcher.launch(options).await?; -//! -//! // Use the browser... -//! -//! browser.close().await?; -//! Ok(()) -//! } -//! ``` - -pub mod config; -pub mod data; -pub mod env_vars; -pub mod fingerprint; -pub mod fonts; -pub mod geolocation; -pub mod launcher; -pub mod presets; -pub mod webgl; - -// Re-export main types for convenience -pub use config::{ - CamoufoxConfigBuilder, CamoufoxLaunchConfig, ConfigError, GeoIPOption, ProxyConfig, -}; -pub use fingerprint::types::{ - Fingerprint, FingerprintOptions, FingerprintWithHeaders, NavigatorFingerprint, ScreenConstraints, - ScreenFingerprint, VideoCard, -}; -pub use fingerprint::{FingerprintError, FingerprintGenerator}; -pub use geolocation::{ - fetch_public_ip, get_geolocation, is_geoip_available, is_ipv4, is_ipv6, validate_ip, Geolocation, - GeolocationError, Locale, LocaleSelector, -}; -pub use launcher::{ - launch_camoufox, launch_persistent_camoufox, CamoufoxLauncher, LaunchOptions, LauncherError, -}; -pub use webgl::{sample_webgl, WebGLData, WebGLError}; - -/// Unified error type for all Camoufox operations. -#[derive(Debug, thiserror::Error)] -pub enum CamoufoxError { - #[error("Launcher error: {0}")] - Launcher(#[from] LauncherError), - - #[error("Configuration error: {0}")] - Config(#[from] ConfigError), - - #[error("Fingerprint error: {0}")] - Fingerprint(#[from] FingerprintError), - - #[error("WebGL error: {0}")] - WebGL(#[from] WebGLError), - - #[error("Geolocation error: {0}")] - Geolocation(#[from] GeolocationError), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fingerprint_generation() { - let generator = FingerprintGenerator::new().unwrap(); - let options = FingerprintOptions { - browsers: Some(vec!["firefox".to_string()]), - operating_system: Some("windows".to_string()), - ..Default::default() - }; - - let result = generator.get_fingerprint(&options); - assert!(result.is_ok()); - - let fp = result.unwrap(); - assert!(!fp.fingerprint.navigator.user_agent.is_empty()); - assert!(fp.fingerprint.screen.width > 0); - } - - #[test] - fn test_config_builder() { - let config = CamoufoxConfigBuilder::new() - .operating_system("windows") - .block_images(false) - .build(); - - assert!(config.is_ok()); - - let config = config.unwrap(); - assert!(!config.fingerprint_config.is_empty()); - assert!(config - .fingerprint_config - .contains_key("navigator.userAgent")); - } - - #[test] - fn test_webgl_sampling() { - let result = webgl::sample_webgl("win", None, None); - assert!(result.is_ok()); - - let webgl_data = result.unwrap(); - assert!(!webgl_data.vendor.is_empty()); - assert!(!webgl_data.renderer.is_empty()); - } - - #[test] - fn test_fonts() { - let fonts = fonts::get_fonts_for_os("win"); - assert!(!fonts.is_empty()); - assert!(fonts.contains(&"Arial".to_string())); - } - - #[test] - fn test_env_vars() { - let mut config = std::collections::HashMap::new(); - config.insert( - "navigator.userAgent".to_string(), - serde_json::json!("Mozilla/5.0"), - ); - - let env_vars = env_vars::config_to_env_vars(&config).unwrap(); - assert!(!env_vars.is_empty()); - assert!(env_vars.contains_key("CAMOU_CONFIG_1")); - } -} diff --git a/src-tauri/src/camoufox/presets.rs b/src-tauri/src/camoufox/presets.rs deleted file mode 100644 index eaecd48..0000000 --- a/src-tauri/src/camoufox/presets.rs +++ /dev/null @@ -1,405 +0,0 @@ -//! Real-fingerprint preset support for Camoufox. -//! -//! Mirrors the preset-selection logic from -//! `pythonlib/camoufox/fingerprints.py` (`_select_presets_file`, -//! `load_presets`, `get_random_preset`, `from_preset`). -//! -//! Camoufox ships two bundled preset files: -//! - `fingerprint-presets-v135.json` — real fingerprints harvested from -//! browsers running Firefox ≤148. The frozen "v135 line" — kept around -//! so users who haven't upgraded their Camoufox binary keep getting -//! consistent fingerprints. -//! - `fingerprint-presets-v150.json` — the *newer* bundle, refreshed by -//! upstream as Camoufox tracks newer Firefox versions. This is the -//! bundle every newer Camoufox release uses; we make no assumption that -//! Firefox 150 is the ceiling. -//! -//! At launch we know the bundled Firefox version (see -//! `config::get_firefox_version`) and pick `v135` or `newer` accordingly. -//! The split point lives in `data::PRESETS_NEWER_MIN_FF` (currently 149) -//! and is the only number we hard-code — anything ≥ that gets the newer -//! bundle, regardless of how far Firefox itself has moved on. -//! -//! Falling back to Bayesian-network synthesis (the previous default) is -//! still possible when no preset matches the requested OS. - -use rand::prelude::IndexedRandom; -use regex_lite::Regex; -use serde::Deserialize; -use std::collections::HashMap; -use std::sync::OnceLock; - -use crate::camoufox::data; - -#[derive(Debug, Clone, Deserialize)] -pub struct Navigator { - #[serde(rename = "userAgent")] - pub user_agent: Option, - pub platform: Option, - #[serde(rename = "hardwareConcurrency")] - pub hardware_concurrency: Option, - #[serde(rename = "maxTouchPoints")] - pub max_touch_points: Option, - pub oscpu: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Screen { - pub width: Option, - pub height: Option, - #[serde(rename = "colorDepth")] - pub color_depth: Option, - #[serde(rename = "availWidth")] - pub avail_width: Option, - #[serde(rename = "availHeight")] - pub avail_height: Option, - #[serde(rename = "devicePixelRatio")] - pub device_pixel_ratio: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct WebGl { - #[serde(rename = "unmaskedVendor")] - pub unmasked_vendor: Option, - #[serde(rename = "unmaskedRenderer")] - pub unmasked_renderer: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Preset { - #[serde(default)] - pub navigator: Option, - #[serde(default)] - pub screen: Option, - #[serde(default)] - pub webgl: Option, - #[serde(default)] - pub timezone: Option, - #[serde(default)] - pub fonts: Option>, - #[serde(rename = "speechVoices", default)] - pub speech_voices: Option>, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct PresetBundle { - /// Bundle schema version — upstream writes this as a JSON integer (e.g. - /// `1`), so we accept any JSON shape here and ignore it. Only the - /// `presets` map matters at runtime. - #[allow(dead_code)] - #[serde(default)] - pub version: Option, - #[serde(default)] - pub presets: HashMap>, -} - -/// Which Camoufox release line the active binary belongs to. Determines -/// which preset bundle to load. The set is intentionally just two-valued: -/// the legacy v135 line and "everything newer" — upstream refreshes the -/// newer bundle as Firefox versions advance, but our routing logic stays -/// the same. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PresetLine { - V135, - Newer, -} - -/// Pick the preset line that matches a Firefox major version, mirroring -/// `_select_presets_file` in the Python lib. Unknown / very old versions -/// fall back to the v135 bundle so the older Camoufox builds keep working. -pub fn preset_line_for(ff_version: Option) -> PresetLine { - match ff_version { - Some(v) if v >= data::PRESETS_NEWER_MIN_FF => PresetLine::Newer, - _ => PresetLine::V135, - } -} - -/// Cache the parsed bundles forever — they're static, embedded data and -/// parsing the newer file twice would waste a few megs of CPU work on -/// every launch. -static V135_BUNDLE: OnceLock> = OnceLock::new(); -static NEWER_BUNDLE: OnceLock> = OnceLock::new(); - -fn parse_bundle(json: &str) -> Option { - match serde_json::from_str::(json) { - Ok(b) => Some(b), - Err(e) => { - log::warn!("camoufox preset bundle failed to parse: {e}"); - None - } - } -} - -pub fn load_presets(line: PresetLine) -> Option<&'static PresetBundle> { - let slot = match line { - PresetLine::V135 => &V135_BUNDLE, - PresetLine::Newer => &NEWER_BUNDLE, - }; - slot - .get_or_init(|| match line { - PresetLine::V135 => parse_bundle(data::FINGERPRINT_PRESETS_V135_JSON), - PresetLine::Newer => parse_bundle(data::FINGERPRINT_PRESETS_NEWER_JSON), - }) - .as_ref() -} - -/// Normalize the OS string the rest of the codebase uses ("macos", "windows", -/// "linux") to the preset key. Returns `None` for OSes that don't have any -/// presets bundled. -fn normalize_os(os: &str) -> Option<&'static str> { - match os { - "windows" | "win" => Some("windows"), - "macos" | "mac" | "darwin" => Some("macos"), - "linux" | "lin" => Some("linux"), - _ => None, - } -} - -/// Pick a random preset for the requested OS. `None` if there are no -/// presets bundled for that OS (which can happen in tests with reduced -/// fixtures, or if a new OS is added before its preset bundle ships). -pub fn get_random_preset(os: Option<&str>, ff_version: Option) -> Option { - let bundle = load_presets(preset_line_for(ff_version))?; - - let candidates: Vec<&Preset> = match os.and_then(normalize_os) { - Some(os_key) => bundle.presets.get(os_key).map(|v| v.iter().collect())?, - None => bundle.presets.values().flatten().collect(), - }; - if candidates.is_empty() { - return None; - } - candidates.choose(&mut rand::rng()).map(|p| (*p).clone()) -} - -/// Match python's `from_preset` — translate a real-fingerprint preset into -/// the CAMOU_CONFIG-style HashMap the rest of the launcher expects. -/// -/// The caller is responsible for filling in fonts, voices, and the random -/// seeds; those are intentionally left out here so each call site can layer -/// its own RNG and font policy. -pub fn from_preset(preset: &Preset, ff_version: Option) -> HashMap { - let mut config: HashMap = HashMap::new(); - - if let Some(nav) = &preset.navigator { - if let Some(ua) = &nav.user_agent { - let ua = if let Some(v) = ff_version { - rewrite_ua_firefox_version(ua, v) - } else { - ua.clone() - }; - config.insert("navigator.userAgent".to_string(), serde_json::json!(ua)); - } - if let Some(p) = &nav.platform { - config.insert("navigator.platform".to_string(), serde_json::json!(p)); - } - if let Some(hc) = nav.hardware_concurrency { - config.insert( - "navigator.hardwareConcurrency".to_string(), - serde_json::json!(hc), - ); - } - if let Some(mtp) = nav.max_touch_points { - config.insert( - "navigator.maxTouchPoints".to_string(), - serde_json::json!(mtp), - ); - } - // navigator.oscpu — explicit, or derived from the platform. - let oscpu = nav.oscpu.clone().or_else(|| { - nav.platform.as_deref().and_then(|plat| match plat { - "MacIntel" => Some("Intel Mac OS X 10.15".to_string()), - "Win32" => Some("Windows NT 10.0; Win64; x64".to_string()), - p if p.to_ascii_lowercase().contains("linux") => Some("Linux x86_64".to_string()), - _ => None, - }) - }); - if let Some(o) = oscpu { - config.insert("navigator.oscpu".to_string(), serde_json::json!(o)); - } - } - - if let Some(s) = &preset.screen { - if let Some(w) = s.width { - config.insert("screen.width".to_string(), serde_json::json!(w)); - } - if let Some(h) = s.height { - config.insert("screen.height".to_string(), serde_json::json!(h)); - } - if let Some(cd) = s.color_depth { - config.insert("screen.colorDepth".to_string(), serde_json::json!(cd)); - config.insert("screen.pixelDepth".to_string(), serde_json::json!(cd)); - } - if let Some(aw) = s.avail_width { - config.insert("screen.availWidth".to_string(), serde_json::json!(aw)); - } - if let Some(ah) = s.avail_height { - config.insert("screen.availHeight".to_string(), serde_json::json!(ah)); - } - } - - if let Some(w) = &preset.webgl { - if let Some(v) = &w.unmasked_vendor { - config.insert("webGl:vendor".to_string(), serde_json::json!(v)); - } - if let Some(r) = &w.unmasked_renderer { - config.insert("webGl:renderer".to_string(), serde_json::json!(r)); - } - } - - if let Some(tz) = &preset.timezone { - config.insert("timezone".to_string(), serde_json::json!(tz)); - } - - config -} - -fn rewrite_ua_firefox_version(ua: &str, version: u32) -> String { - let firefox_re = Regex::new(r"Firefox/\d+\.0").expect("static regex"); - let rv_re = Regex::new(r"rv:\d+\.0").expect("static regex"); - let first = firefox_re.replace_all(ua, format!("Firefox/{version}.0")); - rv_re - .replace_all(&first, format!("rv:{version}.0")) - .into_owned() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn picks_v135_for_old_firefox() { - assert_eq!(preset_line_for(Some(135)), PresetLine::V135); - assert_eq!(preset_line_for(Some(148)), PresetLine::V135); - assert_eq!(preset_line_for(None), PresetLine::V135); - } - - #[test] - fn picks_newer_for_anything_past_the_legacy_line() { - // The threshold is data::PRESETS_NEWER_MIN_FF (currently 149). - // Future Firefox versions all share the same bundle — there's - // intentionally no per-version routing past v135. - assert_eq!(preset_line_for(Some(149)), PresetLine::Newer); - assert_eq!(preset_line_for(Some(150)), PresetLine::Newer); - assert_eq!(preset_line_for(Some(160)), PresetLine::Newer); - assert_eq!(preset_line_for(Some(200)), PresetLine::Newer); - } - - #[test] - fn both_bundles_parse_and_cover_all_platforms() { - for (line, json) in [ - (PresetLine::V135, data::FINGERPRINT_PRESETS_V135_JSON), - (PresetLine::Newer, data::FINGERPRINT_PRESETS_NEWER_JSON), - ] { - let bundle: PresetBundle = - serde_json::from_str(json).unwrap_or_else(|e| panic!("bundle {line:?} parse error: {e}")); - for os in ["macos", "windows", "linux"] { - let presets = bundle.presets.get(os).unwrap_or_else(|| { - panic!("bundle {line:?} is missing presets for {os}"); - }); - assert!( - !presets.is_empty(), - "bundle {line:?} has zero presets for {os}" - ); - } - } - } - - #[test] - fn random_preset_returns_for_each_os() { - for os in ["macos", "windows", "linux"] { - let preset = get_random_preset(Some(os), Some(150)).expect("preset"); - assert!(preset.navigator.is_some(), "navigator present for {os}"); - } - } - - #[test] - fn from_preset_rewrites_firefox_version() { - let preset = Preset { - navigator: Some(Navigator { - user_agent: Some( - "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0".to_string(), - ), - platform: Some("Linux x86_64".to_string()), - hardware_concurrency: Some(8), - max_touch_points: Some(0), - oscpu: None, - }), - screen: None, - webgl: None, - timezone: None, - fonts: None, - speech_voices: None, - }; - let config = from_preset(&preset, Some(150)); - let ua = config - .get("navigator.userAgent") - .and_then(|v| v.as_str()) - .unwrap(); - assert!(ua.contains("Firefox/150.0"), "got: {ua}"); - assert!(ua.contains("rv:150.0"), "got: {ua}"); - // oscpu derived from "Linux x86_64" platform - assert_eq!( - config - .get("navigator.oscpu") - .and_then(|v| v.as_str()) - .unwrap(), - "Linux x86_64" - ); - } - - #[test] - fn from_preset_derives_oscpu_for_mac_and_win() { - let mut preset = Preset { - navigator: Some(Navigator { - user_agent: None, - platform: Some("MacIntel".to_string()), - hardware_concurrency: None, - max_touch_points: None, - oscpu: None, - }), - screen: None, - webgl: None, - timezone: None, - fonts: None, - speech_voices: None, - }; - assert_eq!( - from_preset(&preset, None) - .get("navigator.oscpu") - .and_then(|v| v.as_str()) - .unwrap(), - "Intel Mac OS X 10.15" - ); - preset.navigator.as_mut().unwrap().platform = Some("Win32".to_string()); - assert_eq!( - from_preset(&preset, None) - .get("navigator.oscpu") - .and_then(|v| v.as_str()) - .unwrap(), - "Windows NT 10.0; Win64; x64" - ); - } - - #[test] - fn screen_color_depth_fills_both_keys() { - let preset = Preset { - navigator: None, - screen: Some(Screen { - width: Some(1920), - height: Some(1080), - color_depth: Some(24), - avail_width: Some(1920), - avail_height: Some(1050), - device_pixel_ratio: Some(1.0), - }), - webgl: None, - timezone: None, - fonts: None, - speech_voices: None, - }; - let config = from_preset(&preset, None); - assert_eq!(config.get("screen.colorDepth").unwrap(), 24); - assert_eq!(config.get("screen.pixelDepth").unwrap(), 24); - assert_eq!(config.get("screen.availWidth").unwrap(), 1920); - } -} diff --git a/src-tauri/src/camoufox/webgl.rs b/src-tauri/src/camoufox/webgl.rs deleted file mode 100644 index 7cf5597..0000000 --- a/src-tauri/src/camoufox/webgl.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! WebGL fingerprint sampling from SQLite database. -//! -//! Samples realistic WebGL configurations based on OS-specific probability distributions. - -use rand::RngExt; -use rusqlite::{Connection, Result as SqliteResult}; -use std::collections::HashMap; -use std::io::Write; -use tempfile::NamedTempFile; - -use crate::camoufox::data; - -/// WebGL fingerprint data. -#[derive(Debug, Clone)] -pub struct WebGLData { - pub vendor: String, - pub renderer: String, - pub config: HashMap, -} - -/// Error type for WebGL operations. -#[derive(Debug, thiserror::Error)] -pub enum WebGLError { - #[error("SQLite error: {0}")] - Sqlite(#[from] rusqlite::Error), - - #[error("JSON parsing error: {0}")] - Json(#[from] serde_json::Error), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("No WebGL data found for OS: {0}")] - NoDataForOS(String), - - #[error("Invalid vendor/renderer combination for OS {os}: {vendor}/{renderer}")] - InvalidCombination { - os: String, - vendor: String, - renderer: String, - }, -} - -/// Sample a WebGL configuration for the given OS. -/// -/// If `vendor` and `renderer` are provided, returns the specific configuration. -/// Otherwise, randomly samples based on OS-specific probability weights. -pub fn sample_webgl( - os: &str, - vendor: Option<&str>, - renderer: Option<&str>, -) -> Result { - // Write embedded database to a temporary file - let mut temp_file = NamedTempFile::new()?; - temp_file.write_all(data::WEBGL_DATA_DB)?; - let db_path = temp_file.path(); - - let conn = Connection::open(db_path)?; - - // Validate OS - let os_column = match os { - "win" | "windows" => "win", - "mac" | "macos" => "mac", - "lin" | "linux" => "lin", - _ => return Err(WebGLError::NoDataForOS(os.to_string())), - }; - - if let (Some(v), Some(r)) = (vendor, renderer) { - sample_specific(&conn, os_column, v, r) - } else { - sample_random(&conn, os_column) - } -} - -fn sample_specific( - conn: &Connection, - os_column: &str, - vendor: &str, - renderer: &str, -) -> Result { - let query = format!( - "SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE vendor = ?1 AND renderer = ?2", - os_column - ); - - let mut stmt = conn.prepare(&query)?; - let mut rows = stmt.query([vendor, renderer])?; - - if let Some(row) = rows.next()? { - let weight: f64 = row.get(3)?; - if weight <= 0.0 { - return Err(WebGLError::InvalidCombination { - os: os_column.to_string(), - vendor: vendor.to_string(), - renderer: renderer.to_string(), - }); - } - - let data_json: String = row.get(2)?; - let config: HashMap = serde_json::from_str(&data_json)?; - - Ok(WebGLData { - vendor: vendor.to_string(), - renderer: renderer.to_string(), - config, - }) - } else { - Err(WebGLError::InvalidCombination { - os: os_column.to_string(), - vendor: vendor.to_string(), - renderer: renderer.to_string(), - }) - } -} - -fn sample_random(conn: &Connection, os_column: &str) -> Result { - let query = format!( - "SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE {} > 0", - os_column, os_column - ); - - let mut stmt = conn.prepare(&query)?; - let rows: Vec<(String, String, String, f64)> = stmt - .query_map([], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, f64>(3)?, - )) - })? - .collect::>>()?; - - if rows.is_empty() { - return Err(WebGLError::NoDataForOS(os_column.to_string())); - } - - // Calculate total weight - let total_weight: f64 = rows.iter().map(|(_, _, _, w)| w).sum(); - - // Weighted random selection - let mut rng = rand::rng(); - let threshold = rng.random::() * total_weight; - let mut cumulative = 0.0; - - for (vendor, renderer, data_json, weight) in &rows { - cumulative += *weight; - if cumulative >= threshold { - let config: HashMap = serde_json::from_str(data_json)?; - return Ok(WebGLData { - vendor: vendor.clone(), - renderer: renderer.clone(), - config, - }); - } - } - - // Fallback to last row - let (vendor, renderer, data_json, _) = rows.last().unwrap(); - let config: HashMap = serde_json::from_str(data_json)?; - Ok(WebGLData { - vendor: vendor.clone(), - renderer: renderer.clone(), - config, - }) -} - -/// Get all possible vendor/renderer pairs for each OS. -pub fn get_possible_pairs() -> Result>, WebGLError> { - // Write embedded database to a temporary file - let mut temp_file = NamedTempFile::new()?; - temp_file.write_all(data::WEBGL_DATA_DB)?; - let db_path = temp_file.path(); - - let conn = Connection::open(db_path)?; - let mut result = HashMap::new(); - - for os in &["win", "mac", "lin"] { - let query = format!( - "SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE {} > 0 ORDER BY {} DESC", - os, os - ); - - let mut stmt = conn.prepare(&query)?; - let pairs: Vec<(String, String)> = stmt - .query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })? - .collect::>>()?; - - result.insert(os.to_string(), pairs); - } - - Ok(result) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sample_webgl_windows() { - let result = sample_webgl("win", None, None); - assert!( - result.is_ok(), - "Failed to sample WebGL for Windows: {:?}", - result.err() - ); - - let data = result.unwrap(); - assert!(!data.vendor.is_empty()); - assert!(!data.renderer.is_empty()); - assert!(!data.config.is_empty()); - } - - #[test] - fn test_sample_webgl_macos() { - let result = sample_webgl("mac", None, None); - assert!( - result.is_ok(), - "Failed to sample WebGL for macOS: {:?}", - result.err() - ); - } - - #[test] - fn test_sample_webgl_linux() { - let result = sample_webgl("lin", None, None); - assert!( - result.is_ok(), - "Failed to sample WebGL for Linux: {:?}", - result.err() - ); - } - - #[test] - fn test_get_possible_pairs() { - let result = get_possible_pairs(); - assert!( - result.is_ok(), - "Failed to get possible pairs: {:?}", - result.err() - ); - - let pairs = result.unwrap(); - assert!(pairs.contains_key("win")); - assert!(pairs.contains_key("mac")); - assert!(pairs.contains_key("lin")); - assert!(!pairs.get("win").unwrap().is_empty()); - } -} diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs deleted file mode 100644 index 6b388a3..0000000 --- a/src-tauri/src/camoufox_manager.rs +++ /dev/null @@ -1,855 +0,0 @@ -use crate::browser_runner::BrowserRunner; -use crate::camoufox::{CamoufoxConfigBuilder, GeoIPOption, ScreenConstraints}; -use crate::profile::BrowserProfile; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; -use std::process::Stdio; -use std::sync::Arc; -use tauri::AppHandle; -use tokio::process::Command as TokioCommand; -use tokio::sync::Mutex as AsyncMutex; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CamoufoxConfig { - pub proxy: Option, - pub screen_max_width: Option, - pub screen_max_height: Option, - pub screen_min_width: Option, - pub screen_min_height: Option, - pub geoip: Option, // Can be String or bool - pub block_images: Option, - pub block_webrtc: Option, - pub block_webgl: Option, - pub fingerprint: Option, // JSON string of the complete fingerprint config - pub randomize_fingerprint_on_launch: Option, // Generate new fingerprint on every launch - pub os: Option, // Operating system for fingerprint generation: "windows", "macos", or "linux" -} - -impl Default for CamoufoxConfig { - fn default() -> Self { - Self { - proxy: None, - screen_max_width: None, - screen_max_height: None, - screen_min_width: None, - screen_min_height: None, - geoip: Some(serde_json::Value::Bool(true)), - block_images: None, - block_webrtc: None, - block_webgl: None, - fingerprint: None, - randomize_fingerprint_on_launch: None, - os: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct CamoufoxLaunchResult { - pub id: String, - #[serde(alias = "process_id")] - pub processId: Option, - #[serde(alias = "profile_path")] - pub profilePath: Option, - pub url: Option, - pub cdp_port: Option, -} - -#[derive(Debug)] -struct CamoufoxInstance { - #[allow(dead_code)] - id: String, - process_id: Option, - profile_path: Option, - url: Option, - cdp_port: Option, -} - -struct CamoufoxManagerInner { - instances: HashMap, -} - -pub struct CamoufoxManager { - inner: Arc>, -} - -impl CamoufoxManager { - fn new() -> Self { - Self { - inner: Arc::new(AsyncMutex::new(CamoufoxManagerInner { - instances: HashMap::new(), - })), - } - } - - pub fn instance() -> &'static CamoufoxManager { - &CAMOUFOX_LAUNCHER - } - - async fn find_free_port() -> Result> { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; - let port = listener.local_addr()?.port(); - drop(listener); - Ok(port) - } - - #[allow(dead_code)] - pub async fn get_cdp_port(&self, profile_path: &str) -> Option { - let inner = self.inner.lock().await; - let target_path = std::path::Path::new(profile_path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); - - for instance in inner.instances.values() { - if let Some(path) = &instance.profile_path { - let instance_path = std::path::Path::new(path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(path).to_path_buf()); - if instance_path == target_path { - return instance.cdp_port; - } - } - } - None - } - - pub fn get_profiles_dir(&self) -> PathBuf { - crate::app_dirs::profiles_dir() - } - - /// Generate Camoufox fingerprint configuration during profile creation - pub async fn generate_fingerprint_config( - &self, - _app_handle: &AppHandle, - profile: &crate::profile::BrowserProfile, - config: &CamoufoxConfig, - ) -> Result> { - // Get executable path - let executable_path = BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; - - // Build the config using CamoufoxConfigBuilder - let mut builder = CamoufoxConfigBuilder::new() - .block_images(config.block_images.unwrap_or(false)) - .block_webrtc(config.block_webrtc.unwrap_or(false)) - .block_webgl(config.block_webgl.unwrap_or(false)); - - // Set operating system - if let Some(os) = &config.os { - builder = builder.operating_system(os); - } - - // Build screen constraints if provided - if config.screen_min_width.is_some() - || config.screen_max_width.is_some() - || config.screen_min_height.is_some() - || config.screen_max_height.is_some() - { - let screen_constraints = ScreenConstraints { - min_width: config.screen_min_width, - max_width: config.screen_max_width, - min_height: config.screen_min_height, - max_height: config.screen_max_height, - }; - builder = builder.screen_constraints(screen_constraints); - } - - // Parse proxy if provided - if let Some(proxy_str) = &config.proxy { - let proxy_config = crate::camoufox::ProxyConfig::from_url(proxy_str) - .map_err(|e| format!("Failed to parse proxy URL: {e}"))?; - builder = builder.proxy(proxy_config); - } - - // Set Firefox version from executable - if let Some(version) = crate::camoufox::config::get_firefox_version(&executable_path) { - builder = builder.ff_version(version); - } - - // Handle geoip option - if let Some(geoip_value) = &config.geoip { - match geoip_value { - serde_json::Value::Bool(true) => { - // Auto-detect IP (through proxy if set) - builder = builder.geoip(GeoIPOption::Auto); - } - serde_json::Value::String(ip) => { - // Use specific IP - builder = builder.geoip(GeoIPOption::IP(ip.clone())); - } - _ => { - // geoip: false or other values - don't apply geolocation - } - } - } - - // Build the config (async to handle geoip) - let launch_config = builder - .build_async() - .await - .map_err(|e| format!("Failed to build Camoufox config: {e}"))?; - - // Return the fingerprint config as JSON - let config_json = serde_json::to_string(&launch_config.fingerprint_config) - .map_err(|e| format!("Failed to serialize config: {e}"))?; - - Ok(config_json) - } - - /// Launch Camoufox browser by directly spawning the process - #[allow(clippy::too_many_arguments)] - pub async fn launch_camoufox( - &self, - _app_handle: &AppHandle, - profile: &crate::profile::BrowserProfile, - profile_path: &str, - config: &CamoufoxConfig, - url: Option<&str>, - remote_debugging_port: Option, - headless: bool, - ) -> Result> { - let custom_config = if let Some(existing_fingerprint) = &config.fingerprint { - log::info!("Using existing fingerprint from profile metadata"); - existing_fingerprint.clone() - } else { - return Err("No fingerprint provided".into()); - }; - - // Get executable path - let executable_path = BrowserRunner::instance() - .get_browser_executable_path(profile) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; - - // Parse the fingerprint config JSON - let mut fingerprint_config: HashMap = - serde_json::from_str(&custom_config) - .map_err(|e| format!("Failed to parse fingerprint config: {e}"))?; - - // Strip `window.history.length` even when present in a previously-saved - // fingerprint. Newer Camoufox clamps the docShell session history to the - // spoofed value, which disables the toolbar back/forward buttons. See - // the matching note in camoufox/config.rs. - fingerprint_config.remove("window.history.length"); - - // Convert to environment variables using CAMOU_CONFIG chunking - let env_vars = crate::camoufox::env_vars::config_to_env_vars(&fingerprint_config) - .map_err(|e| format!("Failed to convert config to env vars: {e}"))?; - - // Build command arguments - // Note: We intentionally do NOT use -no-remote to allow opening URLs in existing instances - // via Firefox's remote messaging mechanism. This enables "open in new tab" functionality - // when Donut is set as the default browser. - let mut args = vec![ - "-profile".to_string(), - std::path::Path::new(profile_path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()) - .to_string_lossy() - .to_string(), - ]; - - let cdp_port = match remote_debugging_port { - Some(p) => p, - None => Self::find_free_port().await?, - }; - args.push(format!("--remote-debugging-port={cdp_port}")); - - // Add URL if provided - if let Some(url) = url { - args.push("-new-tab".to_string()); - args.push(url.to_string()); - } - - // Add headless flag when requested via the API or via the CAMOUFOX_HEADLESS - // env var (used by integration tests) - if headless || std::env::var("CAMOUFOX_HEADLESS").is_ok() { - args.push("--headless".to_string()); - } - - log::info!( - "Launching Camoufox: {:?} with args: {:?}", - executable_path, - args - ); - - // Spawn the browser process. Camoufox prints NSS/PSM and proxy failures - // to stderr (e.g. cert errors, CONNECT failures) and the user otherwise - // sees only an opaque "Secure Connection Failed" page — capture stderr - // to a per-launch file so diagnostics survive without a TTY. - let stderr_log_path = std::env::temp_dir().join(format!("camoufox-stderr-{}.log", profile.id)); - let mut command = TokioCommand::new(&executable_path); - command - .args(&args) - .stdin(Stdio::null()) - .stdout(Stdio::null()); - - match std::fs::File::create(&stderr_log_path) { - Ok(file) => { - log::info!( - "Camoufox stderr will be logged to: {}", - stderr_log_path.display() - ); - command.stderr(Stdio::from(file)); - } - Err(e) => { - log::warn!( - "Failed to open Camoufox stderr log {}: {e}", - stderr_log_path.display() - ); - command.stderr(Stdio::null()); - } - } - - // Add environment variables - for (key, value) in &env_vars { - command.env(key, value); - } - - // Handle fontconfig on Linux - if cfg!(target_os = "linux") { - let target_os = config.os.as_deref().unwrap_or("linux"); - if let Some(fontconfig_path) = - crate::camoufox::env_vars::get_fontconfig_env(target_os, &executable_path) - { - command.env("FONTCONFIG_PATH", fontconfig_path); - } - } - - let mut child = command - .spawn() - .map_err(|e| format!("Failed to spawn Camoufox process: {e}"))?; - - let process_id = child.id(); - let instance_id = format!("camoufox_{}", process_id.unwrap_or(0)); - - log::info!("Camoufox launched with PID: {:?}", process_id); - - // Watch the child so its exit status (signal / non-zero code) lands in - // the log. Without this, all we see is "PID X is no longer running" via - // the periodic sysinfo poll, with no clue why it died. - let watch_profile_path = profile_path.to_string(); - tokio::spawn(async move { - match child.wait().await { - Ok(status) => { - if status.success() { - log::info!( - "Camoufox PID {:?} for {} exited cleanly (status=0)", - process_id, - watch_profile_path - ); - } else { - log::warn!( - "Camoufox PID {:?} for {} exited abnormally: {}", - process_id, - watch_profile_path, - status - ); - } - } - Err(e) => { - log::warn!("Failed to await Camoufox PID {:?} exit: {}", process_id, e); - } - } - }); - - // Store the instance - let instance = CamoufoxInstance { - id: instance_id.clone(), - process_id, - profile_path: Some(profile_path.to_string()), - url: url.map(String::from), - cdp_port: Some(cdp_port), - }; - - let launch_result = CamoufoxLaunchResult { - id: instance_id.clone(), - processId: process_id, - profilePath: Some(profile_path.to_string()), - url: url.map(String::from), - cdp_port: Some(cdp_port), - }; - - { - let mut inner = self.inner.lock().await; - inner.instances.insert(instance_id, instance); - } - - Ok(launch_result) - } - - /// Stop a Camoufox process by ID - pub async fn stop_camoufox( - &self, - _app_handle: &AppHandle, - id: &str, - ) -> Result> { - // Get the process ID from our tracking - let process_id = { - let inner = self.inner.lock().await; - inner - .instances - .get(id) - .and_then(|instance| instance.process_id) - }; - - if let Some(pid) = process_id { - // Kill the process - let success = self.kill_process(pid); - - if success { - // Remove from our tracking - let mut inner = self.inner.lock().await; - inner.instances.remove(id); - log::info!("Stopped Camoufox instance {} (PID: {})", id, pid); - } - - Ok(success) - } else { - // No process ID found, just remove from tracking - let mut inner = self.inner.lock().await; - inner.instances.remove(id); - Ok(true) - } - } - - /// Kill a process by PID - fn kill_process(&self, pid: u32) -> bool { - #[cfg(unix)] - { - use std::os::unix::process::ExitStatusExt; - let result = std::process::Command::new("kill") - .args(["-TERM", &pid.to_string()]) - .status(); - - match result { - Ok(status) => status.success() || status.signal() == Some(0), - Err(e) => { - log::warn!("Failed to kill process {}: {}", pid, e); - false - } - } - } - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let result = std::process::Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/T"]) - .creation_flags(CREATE_NO_WINDOW) - .status(); - - match result { - Ok(status) => status.success(), - Err(e) => { - log::warn!("Failed to kill process {}: {}", pid, e); - false - } - } - } - } - - /// Find Camoufox server by profile path (for integration with browser_runner) - /// This method first checks in-memory instances, then scans system processes - /// to detect Camoufox instances that may have been started before the app restarted. - pub async fn find_camoufox_by_profile( - &self, - profile_path: &str, - ) -> Result, Box> { - // First clean up any dead instances - self.cleanup_dead_instances().await?; - - // Convert paths to canonical form for comparison - let target_path = std::path::Path::new(profile_path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); - - // Check in-memory instances first - { - let inner = self.inner.lock().await; - - for (id, instance) in inner.instances.iter() { - if let Some(instance_profile_path) = &instance.profile_path { - let instance_path = std::path::Path::new(instance_profile_path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf()); - - if instance_path == target_path { - // Verify the server is actually running by checking the process - if let Some(process_id) = instance.process_id { - if self.is_server_running(process_id).await { - // Found running Camoufox instance - return Ok(Some(CamoufoxLaunchResult { - id: id.clone(), - processId: instance.process_id, - profilePath: instance.profile_path.clone(), - url: instance.url.clone(), - cdp_port: instance.cdp_port, - })); - } - } - } - } - } - } - - // If not found in in-memory instances, scan system processes - // This handles the case where the app was restarted but Camoufox is still running - if let Some((pid, found_profile_path, cdp_port)) = - self.find_camoufox_process_by_profile(&target_path) - { - log::info!( - "Found running Camoufox process (PID: {}) for profile path via system scan", - pid - ); - - // Register this instance in our tracking - let instance_id = format!("recovered_{}", pid); - let mut inner = self.inner.lock().await; - inner.instances.insert( - instance_id.clone(), - CamoufoxInstance { - id: instance_id.clone(), - process_id: Some(pid), - profile_path: Some(found_profile_path.clone()), - url: None, - cdp_port, - }, - ); - - return Ok(Some(CamoufoxLaunchResult { - id: instance_id, - processId: Some(pid), - profilePath: Some(found_profile_path), - url: None, - cdp_port, - })); - } - - Ok(None) - } - - /// Scan system processes to find a Camoufox process using a specific profile path - fn find_camoufox_process_by_profile( - &self, - target_path: &std::path::Path, - ) -> Option<(u32, String, Option)> { - use sysinfo::{ProcessRefreshKind, RefreshKind, System}; - - let system = System::new_with_specifics( - RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), - ); - - let target_path_str = target_path.to_string_lossy(); - - for (pid, process) in system.processes() { - let cmd = process.cmd(); - if cmd.is_empty() { - continue; - } - - // Check if this is a Camoufox/Firefox process - let exe_name = process.name().to_string_lossy().to_lowercase(); - let is_firefox_like = exe_name.contains("firefox") - || exe_name.contains("camoufox") - || exe_name.contains("firefox-bin"); - - if !is_firefox_like { - continue; - } - - let mut matched = false; - let mut found_profile_path = None; - let mut cdp_port: Option = None; - - // Check if the command line contains our profile path - for (i, arg) in cmd.iter().enumerate() { - if let Some(arg_str) = arg.to_str() { - // Check for -profile argument followed by our path - if arg_str == "-profile" && i + 1 < cmd.len() { - if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { - let cmd_path = std::path::Path::new(next_arg) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(next_arg).to_path_buf()); - - if cmd_path == target_path { - matched = true; - found_profile_path = Some(next_arg.to_string()); - } - } - } - - // Also check if the argument contains the profile path directly - if !matched && arg_str.contains(&*target_path_str) { - matched = true; - found_profile_path = Some(target_path_str.to_string()); - } - - if let Some(port_val) = arg_str.strip_prefix("--remote-debugging-port=") { - cdp_port = port_val.parse().ok(); - } - } - } - - if matched { - if let Some(profile_path) = found_profile_path { - return Some((pid.as_u32(), profile_path, cdp_port)); - } - } - } - - None - } - - /// Check if servers are still alive and clean up dead instances - pub async fn cleanup_dead_instances( - &self, - ) -> Result, Box> { - let mut dead_instances = Vec::new(); - let mut instances_to_remove = Vec::new(); - - { - let inner = self.inner.lock().await; - - for (id, instance) in inner.instances.iter() { - if let Some(process_id) = instance.process_id { - if !self.is_server_running(process_id).await { - log::info!( - "Camoufox instance {} (PID {}) is no longer running; profile_path={:?}", - id, - process_id, - instance.profile_path - ); - dead_instances.push(id.clone()); - instances_to_remove.push(id.clone()); - } - } else { - log::info!("Camoufox instance {} has no PID, marking as dead", id); - dead_instances.push(id.clone()); - instances_to_remove.push(id.clone()); - } - } - } - - if !instances_to_remove.is_empty() { - let mut inner = self.inner.lock().await; - for id in &instances_to_remove { - inner.instances.remove(id); - } - } - - Ok(dead_instances) - } - - /// Check if a Camoufox server is running with the given process ID - async fn is_server_running(&self, process_id: u32) -> bool { - // Check if the process is still running - use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System}; - - let system = System::new_with_specifics( - RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), - ); - if let Some(process) = system.process(Pid::from(process_id as usize)) { - // Check if this is actually a Camoufox process by looking at the command line - let cmd = process.cmd(); - let is_camoufox = cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("camoufox-worker") || arg_str.contains("camoufox") - }); - - if is_camoufox { - // Found running Camoufox process - return true; - } - } - - false - } -} - -impl CamoufoxManager { - #[allow(clippy::too_many_arguments)] - pub async fn launch_camoufox_profile( - &self, - app_handle: AppHandle, - profile: BrowserProfile, - config: CamoufoxConfig, - url: Option, - override_profile_path: Option, - remote_debugging_port: Option, - headless: bool, - ) -> Result { - // Get profile path - let profile_path = if let Some(ref override_path) = override_profile_path { - override_path.clone() - } else { - let profiles_dir = self.get_profiles_dir(); - profile.get_profile_data_path(&profiles_dir) - }; - let profile_path_str = profile_path.to_string_lossy(); - - // Check if there's already a running instance for this profile - if let Ok(Some(existing)) = self.find_camoufox_by_profile(&profile_path_str).await { - // If there's an existing instance, stop it first to avoid conflicts - let _ = self.stop_camoufox(&app_handle, &existing.id).await; - } - - // Clean up any dead instances before launching - let _ = self.cleanup_dead_instances().await; - - // For ephemeral profiles, write Firefox prefs to minimize disk writes - if override_profile_path.is_some() { - let user_js_path = profile_path.join("user.js"); - let prefs = concat!( - "user_pref(\"browser.cache.disk.enable\", false);\n", - "user_pref(\"browser.cache.memory.enable\", true);\n", - "user_pref(\"browser.sessionstore.resume_from_crash\", false);\n", - "user_pref(\"browser.sessionstore.max_tabs_undo\", 0);\n", - "user_pref(\"browser.sessionstore.max_windows_undo\", 0);\n", - "user_pref(\"places.history.enabled\", false);\n", - "user_pref(\"browser.formfill.enable\", false);\n", - "user_pref(\"signon.rememberSignons\", false);\n", - "user_pref(\"browser.bookmarks.max_backups\", 0);\n", - "user_pref(\"browser.shell.checkDefaultBrowser\", false);\n", - "user_pref(\"toolkit.crashreporter.enabled\", false);\n", - "user_pref(\"browser.pagethumbnails.capturing_disabled\", true);\n", - "user_pref(\"browser.download.manager.addToRecentDocs\", false);\n", - ); - if let Err(e) = std::fs::write(&user_js_path, prefs) { - log::warn!("Failed to write ephemeral user.js: {e}"); - } - } - - // Patch user.js with Camoufox-specific overrides on every launch. This - // always runs (not gated on the proxy being set) because Camoufox's - // bundled camoufox.cfg ships defaults that break basic browser features - // and we need to override them per-profile. - { - let user_js_path = profile_path.join("user.js"); - let mut prefs = String::new(); - - // Preserve existing user.js lines, but strip any keys we're about to - // re-emit so they never duplicate. - let managed_keys = [ - "network.proxy.", - "network.http.http3.enable", - "network.http.http3.enabled", - "xpinstall.signatures.required", - "extensions.startupScanScopes", - "browser.sessionhistory.max_entries", - "browser.sessionhistory.max_total_viewers", - ]; - if let Ok(existing) = std::fs::read_to_string(&user_js_path) { - for line in existing.lines() { - if !managed_keys.iter().any(|k| line.contains(k)) { - prefs.push_str(line); - prefs.push('\n'); - } - } - } - - // Camoufox's bundled camoufox.cfg sets these to 0, which makes - // docShell remember zero prior pages and leaves the toolbar - // back/forward buttons permanently disabled no matter how much - // the user navigates. Restore Firefox defaults. - prefs.push_str( - "user_pref(\"browser.sessionhistory.max_entries\", 50);\n\ - user_pref(\"browser.sessionhistory.max_total_viewers\", -1);\n", - ); - - // Required for sideloaded extensions: - // - signatures.required=false accepts unsigned .xpi (Camoufox is built - // without MOZ_REQUIRE_SIGNING so this is honored). - // - startupScanScopes=1 rescans SCOPE_PROFILE on each launch so newly - // dropped .xpi files in /extensions/ get registered. - prefs.push_str( - "user_pref(\"xpinstall.signatures.required\", false);\n\ - user_pref(\"extensions.startupScanScopes\", 1);\n", - ); - - // Disable HTTP/3 / QUIC. Camoufox always sits behind the local - // donut-proxy, and Firefox-150's QUIC stack bypasses configured HTTP - // proxies and goes direct UDP to the remote host. With an upstream - // proxy that's the only allowed egress, that traffic silently fails - // and pages won't load. (Chromium suppresses QUIC under a proxy on - // its own, so Wayfern doesn't need the equivalent toggle.) Both - // pref names are emitted because they've been renamed across FF - // versions and either could be the active one at runtime. - prefs.push_str( - "user_pref(\"network.http.http3.enable\", false);\n\ - user_pref(\"network.http.http3.enabled\", false);\n", - ); - - if let Some(proxy_str) = &config.proxy { - if let Ok(parsed) = url::Url::parse(proxy_str) { - let host = parsed.host_str().unwrap_or("127.0.0.1"); - let port = parsed.port().unwrap_or(8080); - let scheme = parsed.scheme(); - - if scheme == "socks5" || scheme == "socks4" { - prefs.push_str(&format!( - "user_pref(\"network.proxy.type\", 1);\n\ - user_pref(\"network.proxy.socks\", \"{host}\");\n\ - user_pref(\"network.proxy.socks_port\", {port});\n\ - user_pref(\"network.proxy.socks_version\", {});\n\ - user_pref(\"network.proxy.socks_remote_dns\", true);\n", - if scheme == "socks5" { 5 } else { 4 } - )); - } else { - // HTTP/HTTPS proxy - prefs.push_str(&format!( - "user_pref(\"network.proxy.type\", 1);\n\ - user_pref(\"network.proxy.http\", \"{host}\");\n\ - user_pref(\"network.proxy.http_port\", {port});\n\ - user_pref(\"network.proxy.ssl\", \"{host}\");\n\ - user_pref(\"network.proxy.ssl_port\", {port});\n\ - user_pref(\"network.proxy.no_proxies_on\", \"\");\n" - )); - } - } - } - - if let Err(e) = std::fs::write(&user_js_path, prefs) { - log::warn!("Failed to write user.js: {e}"); - } - } - - self - .launch_camoufox( - &app_handle, - &profile, - &profile_path_str, - &config, - url.as_deref(), - remote_debugging_port, - headless, - ) - .await - .map_err(|e| format!("Failed to launch Camoufox: {e}")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let default_config = CamoufoxConfig::default(); - - // Verify defaults - assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true))); - assert_eq!(default_config.proxy, None); - assert_eq!(default_config.fingerprint, None); - assert_eq!(default_config.randomize_fingerprint_on_launch, None); - assert_eq!(default_config.os, None); - } -} - -// Global singleton instance -lazy_static::lazy_static! { - static ref CAMOUFOX_LAUNCHER: CamoufoxManager = CamoufoxManager::new(); -} diff --git a/src-tauri/src/cookie_manager.rs b/src-tauri/src/cookie_manager.rs index 4427995..7c2a009 100644 --- a/src-tauri/src/cookie_manager.rs +++ b/src-tauri/src/cookie_manager.rs @@ -220,14 +220,6 @@ impl CookieManager { Err(format!("Cookie database not found at: {}", path.display())) } } - "camoufox" => { - let path = profile_data_path.join("cookies.sqlite"); - if path.exists() { - Ok(path) - } else { - Err(format!("Cookie database not found at: {}", path.display())) - } - } _ => Err(format!( "Unsupported browser type for cookie operations: {}", profile.browser @@ -253,13 +245,6 @@ impl CookieManager { } Ok(path) } - "camoufox" => { - let path = profile_data_path.join("cookies.sqlite"); - if !path.exists() { - Self::create_empty_firefox_cookies_db(&path)?; - } - Ok(path) - } _ => Err(format!( "Unsupported browser type for cookie operations: {}", profile.browser @@ -318,39 +303,6 @@ impl CookieManager { Ok(()) } - /// Create an empty Firefox-format cookies.sqlite database at `path`. - fn create_empty_firefox_cookies_db(path: &Path) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create cookie directory: {e}"))?; - } - let conn = - Connection::open(path).map_err(|e| format!("Failed to create cookie database: {e}"))?; - conn - .execute_batch( - "CREATE TABLE moz_cookies ( - id INTEGER PRIMARY KEY, - originAttributes TEXT NOT NULL DEFAULT '', - name TEXT, - value TEXT, - host TEXT, - path TEXT, - expiry INTEGER, - lastAccessed INTEGER, - creationTime INTEGER, - isSecure INTEGER, - isHttpOnly INTEGER, - inBrowserElement INTEGER DEFAULT 0, - sameSite INTEGER DEFAULT 0, - rawSameSite INTEGER DEFAULT 0, - schemeMap INTEGER DEFAULT 0, - CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes) - );", - ) - .map_err(|e| format!("Failed to initialize cookie database schema: {e}"))?; - Ok(()) - } - /// Convert Chrome timestamp (Windows epoch, microseconds) to Unix timestamp (seconds) fn chrome_time_to_unix(chrome_time: i64) -> i64 { if chrome_time == 0 { @@ -367,40 +319,6 @@ impl CookieManager { (unix_time + Self::WINDOWS_EPOCH_DIFF) * 1_000_000 } - /// Read cookies from a Firefox/Camoufox profile - fn read_firefox_cookies(db_path: &Path) -> Result, String> { - let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?; - - let mut stmt = conn - .prepare( - "SELECT name, value, host, path, expiry, isSecure, isHttpOnly, - sameSite, creationTime, lastAccessed - FROM moz_cookies", - ) - .map_err(|e| format!("Failed to prepare statement: {e}"))?; - - let cookies = stmt - .query_map([], |row| { - Ok(UnifiedCookie { - name: row.get(0)?, - value: row.get(1)?, - domain: row.get(2)?, - path: row.get(3)?, - expires: row.get(4)?, - is_secure: row.get::<_, i32>(5)? != 0, - is_http_only: row.get::<_, i32>(6)? != 0, - same_site: row.get(7)?, - creation_time: row.get::<_, i64>(8)? / 1_000_000, - last_accessed: row.get::<_, i64>(9)? / 1_000_000, - }) - }) - .map_err(|e| format!("Failed to query cookies: {e}"))? - .collect::, _>>() - .map_err(|e| format!("Failed to collect cookies: {e}"))?; - - Ok(cookies) - } - /// Read cookies from a Chrome/Wayfern profile. /// Handles encrypted cookies by decrypting encrypted_value using the profile's encryption key. fn read_chrome_cookies( @@ -464,98 +382,6 @@ impl CookieManager { Ok(cookies) } - /// Write cookies to a Firefox/Camoufox profile. - /// - /// Firefox's `moz_cookies.expiry` is "seconds since Unix epoch", so `expiry = 0` - /// is interpreted as 1970-01-01 and purged on read. To let imported session - /// cookies survive browser restart, we rewrite them to a far-future expiry. - /// - /// `schemeMap` is a bitfield (1 = HTTP, 2 = HTTPS, 3 = both). Setting it based - /// on `is_secure` preserves Firefox's scheme-bound cookie enforcement. - fn write_firefox_cookies( - db_path: &Path, - cookies: &[UnifiedCookie], - ) -> Result<(usize, usize), String> { - let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?; - - let mut copied = 0; - let mut replaced = 0; - - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - // Session cookies get 30 days of persistence so they survive restart. - let session_cookie_expiry = now + 30 * 86400; - - for cookie in cookies { - let expiry = if cookie.expires > 0 { - cookie.expires - } else { - session_cookie_expiry - }; - // schemeMap bitfield: 1 = HTTP, 2 = HTTPS - let scheme_map: i32 = if cookie.is_secure { 2 } else { 1 }; - - let existing: Option = conn - .query_row( - "SELECT id FROM moz_cookies WHERE host = ?1 AND name = ?2 AND path = ?3", - params![&cookie.domain, &cookie.name, &cookie.path], - |row| row.get(0), - ) - .ok(); - - if existing.is_some() { - conn - .execute( - "UPDATE moz_cookies SET value = ?1, expiry = ?2, isSecure = ?3, - isHttpOnly = ?4, sameSite = ?5, rawSameSite = ?5, - lastAccessed = ?6, schemeMap = ?7 - WHERE host = ?8 AND name = ?9 AND path = ?10", - params![ - &cookie.value, - expiry, - cookie.is_secure as i32, - cookie.is_http_only as i32, - cookie.same_site, - cookie.last_accessed * 1_000_000, - scheme_map, - &cookie.domain, - &cookie.name, - &cookie.path, - ], - ) - .map_err(|e| format!("Failed to update cookie: {e}"))?; - replaced += 1; - } else { - conn - .execute( - "INSERT INTO moz_cookies - (originAttributes, name, value, host, path, expiry, lastAccessed, - creationTime, isSecure, isHttpOnly, sameSite, rawSameSite, schemeMap) - VALUES ('', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11)", - params![ - &cookie.name, - &cookie.value, - &cookie.domain, - &cookie.path, - expiry, - cookie.last_accessed * 1_000_000, - cookie.creation_time * 1_000_000, - cookie.is_secure as i32, - cookie.is_http_only as i32, - cookie.same_site, - scheme_map, - ], - ) - .map_err(|e| format!("Failed to insert cookie: {e}"))?; - copied += 1; - } - } - - Ok((copied, replaced)) - } - /// Write cookies to a Chrome/Wayfern profile. /// /// Always writes values as plaintext in the `value` column with an empty @@ -578,8 +404,7 @@ impl CookieManager { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as i64; - // Session cookies get 30 days of persistence so they survive restart, - // mirroring write_firefox_cookies. A login imported from another browser is + // Session cookies get 30 days of persistence so they survive restart. // routinely exported as a session cookie; writing it as memory-only // (is_persistent = 0) makes Chromium drop it on the next flush, so the // imported account silently signs out on relaunch. Persisting it with a real @@ -684,7 +509,6 @@ impl CookieManager { let db_path = Self::get_cookie_db_path(profile, &profiles_dir)?; let cookies = match profile.browser.as_str() { - "camoufox" => Self::read_firefox_cookies(&db_path)?, "wayfern" => { let key = Self::get_chrome_encryption_key(profile, &profiles_dir); Self::read_chrome_cookies(&db_path, key.as_ref())? @@ -791,10 +615,6 @@ impl CookieManager { let conn = Self::open_cookie_db_readonly(&db_path)?; let (count_sql, domain_sql) = match profile.browser.as_str() { - "camoufox" => ( - "SELECT COUNT(*) FROM moz_cookies", - "SELECT host, COUNT(*) FROM moz_cookies GROUP BY host ORDER BY COUNT(*) DESC, host ASC", - ), "wayfern" => ( "SELECT COUNT(*) FROM cookies", "SELECT host_key, COUNT(*) FROM cookies GROUP BY host_key ORDER BY COUNT(*) DESC, host_key ASC", @@ -869,7 +689,6 @@ impl CookieManager { let source_db_path = Self::get_cookie_db_path(source, &profiles_dir)?; let all_cookies = match source.browser.as_str() { - "camoufox" => Self::read_firefox_cookies(&source_db_path)?, "wayfern" => { let key = Self::get_chrome_encryption_key(source, &profiles_dir); Self::read_chrome_cookies(&source_db_path, key.as_ref())? @@ -941,7 +760,6 @@ impl CookieManager { }; let write_result = match target.browser.as_str() { - "camoufox" => Self::write_firefox_cookies(&target_db_path, &cookies_to_copy), "wayfern" => Self::write_chrome_cookies(&target_db_path, &cookies_to_copy), _ => { results.push(CookieCopyResult { @@ -1207,7 +1025,6 @@ impl CookieManager { let db_path = Self::ensure_cookie_db_path(profile, &profiles_dir)?; let write_result = match profile.browser.as_str() { - "camoufox" => Self::write_firefox_cookies(&db_path, &cookies), "wayfern" => Self::write_chrome_cookies(&db_path, &cookies), _ => return Err(format!("Unsupported browser type: {}", profile.browser)), }; @@ -1504,33 +1321,6 @@ mod tests { .unwrap(); } - /// Set up a minimal Firefox moz_cookies SQLite schema for testing writes. - fn create_firefox_cookies_db(path: &Path) { - let conn = Connection::open(path).unwrap(); - conn - .execute_batch( - "CREATE TABLE moz_cookies ( - id INTEGER PRIMARY KEY, - originAttributes TEXT NOT NULL DEFAULT '', - name TEXT, - value TEXT, - host TEXT, - path TEXT, - expiry INTEGER, - lastAccessed INTEGER, - creationTime INTEGER, - isSecure INTEGER, - isHttpOnly INTEGER, - inBrowserElement INTEGER DEFAULT 0, - sameSite INTEGER DEFAULT 0, - rawSameSite INTEGER DEFAULT 0, - schemeMap INTEGER DEFAULT 0, - CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes) - );", - ) - .unwrap(); - } - #[test] fn test_write_chrome_cookies_stores_plaintext_values() { let tmp = std::env::temp_dir().join(format!("donut_cookie_test_{}.db", uuid::Uuid::new_v4())); @@ -1692,168 +1482,6 @@ mod tests { let _ = std::fs::remove_file(&tmp); } - /// Wayfern → Camoufox: write cookies to a Chrome DB, read them back, and - /// verify they land in a Firefox DB with values intact, correct schemeMap, - /// and non-expired timestamps. This is the path exercised by the - /// "copy cookies between profiles of different browser types" feature. - #[test] - fn test_wayfern_cookies_transfer_to_camoufox() { - let chrome_db = - std::env::temp_dir().join(format!("donut_xbrowser_chrome_{}.db", uuid::Uuid::new_v4())); - let ff_db = std::env::temp_dir().join(format!("donut_xbrowser_ff_{}.db", uuid::Uuid::new_v4())); - create_chrome_cookies_db(&chrome_db); - create_firefox_cookies_db(&ff_db); - - // Simulate cookies in a Wayfern profile: a persistent cookie and a - // session cookie, both from a real-world HTTPS site. - let source_cookies = vec![ - UnifiedCookie { - name: "c_user".to_string(), - value: "100012345678".to_string(), - domain: ".facebook.com".to_string(), - path: "/".to_string(), - expires: 1900000000, // persistent, far in the future - is_secure: true, - is_http_only: true, - same_site: 0, - creation_time: 1700000000, - last_accessed: 1700000000, - }, - UnifiedCookie { - name: "xs".to_string(), - value: "sessionvalue".to_string(), - domain: ".facebook.com".to_string(), - path: "/".to_string(), - expires: 0, // session cookie - is_secure: true, - is_http_only: true, - same_site: 1, - creation_time: 1700000000, - last_accessed: 1700000000, - }, - ]; - CookieManager::write_chrome_cookies(&chrome_db, &source_cookies).unwrap(); - - // Read back from the Chrome DB (as if reading from the Wayfern profile). - let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap(); - assert_eq!(from_chrome.len(), 2); - let c_user_src = from_chrome.iter().find(|c| c.name == "c_user").unwrap(); - assert_eq!(c_user_src.value, "100012345678"); - let xs_src = from_chrome.iter().find(|c| c.name == "xs").unwrap(); - assert_eq!(xs_src.value, "sessionvalue"); - - // Write them into the Camoufox (Firefox) DB. - let (inserted, replaced) = CookieManager::write_firefox_cookies(&ff_db, &from_chrome).unwrap(); - assert_eq!(inserted, 2); - assert_eq!(replaced, 0); - - // Read back from Firefox and verify values survived the round trip. - let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap(); - assert_eq!(from_ff.len(), 2); - let c_user = from_ff.iter().find(|c| c.name == "c_user").unwrap(); - assert_eq!(c_user.value, "100012345678"); - assert_eq!(c_user.domain, ".facebook.com"); - assert!(c_user.is_secure); - assert!(c_user.is_http_only); - let xs = from_ff.iter().find(|c| c.name == "xs").unwrap(); - assert_eq!(xs.value, "sessionvalue"); - - // Raw DB checks against the Firefox schema — these would catch the bugs - // on the Chrome path (plaintext, correct expiry, correct schemeMap). - let conn = Connection::open(&ff_db).unwrap(); - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - let (c_user_expiry, c_user_scheme): (i64, i32) = conn - .query_row( - "SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1", - params!["c_user"], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .unwrap(); - assert!( - c_user_expiry > now, - "persistent cookie must not be expired in firefox (expiry={c_user_expiry}, now={now})" - ); - assert_eq!(c_user_scheme, 2, "HTTPS cookie must have schemeMap=2"); - - let (xs_expiry, xs_scheme): (i64, i32) = conn - .query_row( - "SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1", - params!["xs"], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .unwrap(); - assert!( - xs_expiry > now, - "session cookie must be rewritten to a future expiry (got {xs_expiry}, now={now})" - ); - assert_eq!(xs_scheme, 2); - - let _ = std::fs::remove_file(&chrome_db); - let _ = std::fs::remove_file(&ff_db); - } - - /// Camoufox → Wayfern: the reverse direction. Ensures the Chrome writer - /// still produces plaintext values / empty encrypted_value when fed cookies - /// that originated in Firefox. - #[test] - fn test_camoufox_cookies_transfer_to_wayfern() { - let ff_db = - std::env::temp_dir().join(format!("donut_xbrowser_rev_ff_{}.db", uuid::Uuid::new_v4())); - let chrome_db = std::env::temp_dir().join(format!( - "donut_xbrowser_rev_chrome_{}.db", - uuid::Uuid::new_v4() - )); - create_firefox_cookies_db(&ff_db); - create_chrome_cookies_db(&chrome_db); - - let source_cookies = vec![UnifiedCookie { - name: "sessionid".to_string(), - value: "abc123def456".to_string(), - domain: ".example.com".to_string(), - path: "/".to_string(), - expires: 1900000000, - is_secure: true, - is_http_only: false, - same_site: 1, - creation_time: 1700000000, - last_accessed: 1700000000, - }]; - CookieManager::write_firefox_cookies(&ff_db, &source_cookies).unwrap(); - - let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap(); - assert_eq!(from_ff.len(), 1); - assert_eq!(from_ff[0].value, "abc123def456"); - - CookieManager::write_chrome_cookies(&chrome_db, &from_ff).unwrap(); - - let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap(); - assert_eq!(from_chrome.len(), 1); - assert_eq!(from_chrome[0].value, "abc123def456"); - - // Verify the raw DB state on the Chrome side — plaintext value, empty - // encrypted_value, persistent, HTTPS. - let conn = Connection::open(&chrome_db).unwrap(); - let (value, encrypted, is_persistent, source_scheme): (String, Vec, i32, i32) = conn - .query_row( - "SELECT value, encrypted_value, is_persistent, source_scheme - FROM cookies WHERE name = ?1", - params!["sessionid"], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), - ) - .unwrap(); - assert_eq!(value, "abc123def456"); - assert!(encrypted.is_empty()); - assert_eq!(is_persistent, 1); - assert_eq!(source_scheme, 2); - - let _ = std::fs::remove_file(&ff_db); - let _ = std::fs::remove_file(&chrome_db); - } - /// Regression: decrypting a real v10-encrypted Chromium cookie with the /// correct PBKDF2 iterations and the `SHA-256(host_key)` integrity-prefix /// strip. Captured from a real Wayfern profile: @@ -1975,37 +1603,4 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } - - /// Same regression, Firefox side: a fresh Camoufox profile has no - /// `cookies.sqlite` until the browser launches. - #[test] - fn test_create_empty_firefox_cookies_db_then_write() { - let dir = std::env::temp_dir().join(format!("donut_empty_ff_{}", uuid::Uuid::new_v4())); - let db_path = dir.join("cookies.sqlite"); - assert!(!db_path.exists()); - - CookieManager::create_empty_firefox_cookies_db(&db_path).unwrap(); - assert!(db_path.exists()); - - let cookies = vec![UnifiedCookie { - name: "sid".to_string(), - value: "ff-session".to_string(), - domain: ".example.org".to_string(), - path: "/".to_string(), - expires: 1900000000, - is_secure: true, - is_http_only: false, - same_site: 1, - creation_time: 1700000000, - last_accessed: 1700000000, - }]; - let (inserted, _) = CookieManager::write_firefox_cookies(&db_path, &cookies).unwrap(); - assert_eq!(inserted, 1); - - let read = CookieManager::read_firefox_cookies(&db_path).unwrap(); - assert_eq!(read.len(), 1); - assert_eq!(read[0].value, "ff-session"); - - let _ = std::fs::remove_dir_all(&dir); - } } diff --git a/src-tauri/src/downloaded_browsers_registry.rs b/src-tauri/src/downloaded_browsers_registry.rs index 95d632c..dc68012 100644 --- a/src-tauri/src/downloaded_browsers_registry.rs +++ b/src-tauri/src/downloaded_browsers_registry.rs @@ -921,9 +921,9 @@ impl DownloadedBrowsersRegistry { } } - // Check if GeoIP database is missing for Camoufox profiles + // Check if GeoIP database is missing for Wayfern profiles if self.geoip_downloader.check_missing_geoip_database()? { - log::info!("GeoIP database is missing for Camoufox profiles, downloading..."); + log::info!("GeoIP database is missing for Wayfern profiles, downloading..."); match self .geoip_downloader @@ -931,7 +931,7 @@ impl DownloadedBrowsersRegistry { .await { Ok(_) => { - downloaded.push("GeoIP database for Camoufox".to_string()); + downloaded.push("GeoIP database".to_string()); log::info!("GeoIP database downloaded successfully"); } Err(e) => { @@ -1265,7 +1265,7 @@ pub async fn ensure_active_browsers_downloaded( let version_manager = crate::browser_version_manager::BrowserVersionManager::instance(); let mut downloaded = Vec::new(); - for browser in &["wayfern", "camoufox"] { + for browser in &["wayfern"] { // Check if any version is already downloaded let existing = registry.get_downloaded_versions(browser); if !existing.is_empty() { @@ -1300,7 +1300,7 @@ pub async fn ensure_active_browsers_downloaded( // Retry transient failures a few times. Each attempt is wrapped in an overall // timeout so that a hang anywhere in the download pipeline (version resolution, // a stalled stream, extraction) cannot block the next browser forever. This is - // the core of the bug fix: Wayfern going first must never starve Camoufox. + // the core of the bug fix: Wayfern downloads proceed independently. const MAX_ATTEMPTS: u32 = 3; const ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600); let mut succeeded = false; @@ -1359,7 +1359,7 @@ pub async fn ensure_active_browsers_downloaded( } if !succeeded { - // Do NOT abort the whole routine: continue so the next browser (Camoufox) + // Do NOT abort the whole routine: continue with remaining browsers // still gets its chance even though this one failed/timed out. log::warn!("Giving up on auto-download of {browser} {version} after {MAX_ATTEMPTS} attempts"); } diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 779a757..78f8f0e 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -41,7 +41,6 @@ pub struct Downloader { registry: &'static crate::downloaded_browsers_registry::DownloadedBrowsersRegistry, version_service: &'static crate::browser_version_manager::BrowserVersionManager, extractor: &'static crate::extraction::Extractor, - geoip_downloader: &'static crate::geoip_downloader::GeoIPDownloader, } impl Downloader { @@ -60,7 +59,6 @@ impl Downloader { registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(), version_service: crate::browser_version_manager::BrowserVersionManager::instance(), extractor: crate::extraction::Extractor::instance(), - geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(), } } @@ -76,7 +74,6 @@ impl Downloader { registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(), version_service: crate::browser_version_manager::BrowserVersionManager::instance(), extractor: crate::extraction::Extractor::instance(), - geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(), } } @@ -127,34 +124,6 @@ impl Downloader { _download_info: &DownloadInfo, ) -> Result> { match browser_type { - BrowserType::Camoufox => { - // For Camoufox, verify the asset exists and find the correct download URL - let releases = self - .api_client - .fetch_camoufox_releases_with_caching(true) - .await?; - - let release = releases - .iter() - .find(|r| r.tag_name == version) - .or_else(|| { - log::info!("Camoufox: requested version {version} not found, using latest available"); - releases.first() - }) - .ok_or("No Camoufox releases found".to_string())?; - - // Get platform and architecture info - let (os, arch) = Self::get_platform_info(); - - // Find the appropriate asset - let asset_url = self - .find_camoufox_asset(&release.assets, &os, &arch) - .ok_or(format!( - "No compatible asset found for Camoufox version {version} on {os}/{arch}" - ))?; - - Ok(asset_url) - } BrowserType::Wayfern => { // For Wayfern, get the download URL from version.json let version_info = self @@ -214,97 +183,6 @@ impl Downloader { (os.to_string(), arch.to_string()) } - /// Find the appropriate Camoufox asset for the current platform and architecture - fn find_camoufox_asset( - &self, - assets: &[crate::browser::GithubAsset], - os: &str, - arch: &str, - ) -> Option { - // Camoufox asset naming pattern: camoufox-{version}-beta.{number}-{os}.{arch}.zip - // Example: camoufox-135.0.1-beta.24-lin.x86_64.zip - let (os_name, arch_name) = match (os, arch) { - ("windows", "x64") => ("win", "x86_64"), - ("windows", "arm64") => ("win", "arm64"), - ("linux", "x64") => ("lin", "x86_64"), - ("linux", "arm64") => ("lin", "arm64"), - ("macos", "x64") => ("mac", "x86_64"), - ("macos", "arm64") => ("mac", "arm64"), - _ => return None, - }; - - // Use ends_with for precise matching to avoid false positives - // The separator before OS is a dash: -lin.x86_64.zip, -mac.arm64.zip, etc. - let pattern = format!("-{os_name}.{arch_name}.zip"); - let asset = assets.iter().find(|asset| { - let name = asset.name.to_lowercase(); - name.starts_with("camoufox-") && name.ends_with(&pattern) - }); - - if let Some(asset) = asset { - log::info!( - "Selected Camoufox asset for {}/{}: {}", - os, - arch, - asset.name - ); - Some(asset.browser_download_url.clone()) - } else { - log::warn!( - "No matching Camoufox asset found for {}/{} with pattern '{}'. Available assets: {:?}", - os, - arch, - pattern, - assets.iter().map(|a| &a.name).collect::>() - ); - None - } - } - - /// Ensure version.json exists in the Camoufox installation directory. - /// Creates the file if it doesn't exist, using the version from the tag name. - async fn ensure_camoufox_version_json( - &self, - browser_dir: &Path, - version: &str, - ) -> Result<(), Box> { - // The browser_dir is typically: binaries/camoufox// - // Find the executable directory within it - let version_json_locations = vec![ - browser_dir.join("version.json"), - browser_dir.join("camoufox").join("version.json"), - ]; - - // Check if version.json already exists in any expected location - for location in &version_json_locations { - if location.exists() { - log::info!("version.json already exists at: {}", location.display()); - return Ok(()); - } - } - - // Parse the Firefox version from the Camoufox version tag - // Format: "135.0.1-beta.24" -> Firefox version is "135.0.1" (or just "135.0") - let firefox_version = version.split('-').next().unwrap_or(version); - - // Create version.json in the browser directory - let version_json_path = browser_dir.join("version.json"); - let version_data = serde_json::json!({ - "version": firefox_version - }); - - let version_json_str = serde_json::to_string_pretty(&version_data)?; - tokio::fs::write(&version_json_path, version_json_str).await?; - - log::info!( - "Created version.json at {} with Firefox version: {}", - version_json_path.display(), - firefox_version - ); - - Ok(()) - } - pub async fn download_browser( &self, _app_handle: &tauri::AppHandle, @@ -626,7 +504,7 @@ impl Downloader { return Err("Please accept Wayfern Terms and Conditions before downloading browsers".into()); } - // For Wayfern/Camoufox, resolve the actual available version from the API + // For Wayfern, resolve the actual available version from the API let version = if browser_str == "wayfern" { match self .api_client @@ -642,21 +520,6 @@ impl Downloader { } _ => version, } - } else if browser_str == "camoufox" { - match self - .api_client - .fetch_camoufox_releases_with_caching(true) - .await - { - Ok(releases) if !releases.is_empty() && releases[0].tag_name != version => { - log::info!( - "Camoufox: requested {version}, using available {}", - releases[0].tag_name - ); - releases[0].tag_name.clone() - } - _ => version, - } } else { version }; @@ -681,8 +544,6 @@ impl Downloader { BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; let browser = create_browser(browser_type.clone()); - // Use injected registry instance - let binaries_dir = crate::app_dirs::binaries_dir(); // Check if registry thinks it's downloaded, but also verify files actually exist @@ -899,34 +760,6 @@ impl Downloader { error_details.push_str("\nDirectory does not exist!"); } - // For Camoufox on Linux, provide specific expected files - if browser_str == "camoufox" && cfg!(target_os = "linux") { - let camoufox_subdir = browser_dir.join("camoufox"); - error_details.push_str("\nExpected Camoufox executable locations:"); - error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display())); - error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display())); - - if camoufox_subdir.exists() { - error_details.push_str(&format!( - "\nCamoufox subdirectory exists: {}", - camoufox_subdir.display() - )); - if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) { - error_details.push_str("\nFiles in camoufox subdirectory:"); - for entry in entries.flatten() { - let path = entry.path(); - let file_type = if path.is_dir() { "DIR" } else { "FILE" }; - error_details.push_str(&format!("\n {} {}", file_type, path.display())); - } - } - } else { - error_details.push_str(&format!( - "\nCamoufox subdirectory does not exist: {}", - camoufox_subdir.display() - )); - } - } - // Do not delete files on verification failure; keep archive for manual retry. let _ = self.registry.remove_browser(&browser_str, &version); let _ = self.registry.save(); @@ -979,38 +812,6 @@ impl Downloader { } } - // If this is Camoufox, automatically download GeoIP database and create version.json - if browser_str == "camoufox" { - // Check if GeoIP database is already available - if !crate::geoip_downloader::GeoIPDownloader::is_geoip_database_available() { - log::info!("Downloading GeoIP database for Camoufox..."); - - match self - .geoip_downloader - .download_geoip_database(app_handle) - .await - { - Ok(_) => { - log::info!("GeoIP database downloaded successfully"); - } - Err(e) => { - log::error!("Failed to download GeoIP database: {e}"); - // Don't fail the browser download if GeoIP download fails - } - } - } else { - log::info!("GeoIP database already available"); - } - - // Create version.json if it doesn't exist - if let Err(e) = self - .ensure_camoufox_version_json(&browser_dir, &version) - .await - { - log::warn!("Failed to create version.json for Camoufox: {e}"); - } - } - // Emit completion let progress = DownloadProgress { browser: browser_str.clone(), @@ -1245,7 +1046,7 @@ mod tests { } // A different browser's in-progress state must be left untouched. - let other = "camoufox-9.9.9".to_string(); + let other = "chromium-9.9.9".to_string(); { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); downloading.insert(other.clone()); @@ -1265,12 +1066,13 @@ mod tests { ); } assert!( - is_downloading("camoufox", "9.9.9"), + is_downloading("chromium", "9.9.9"), "unrelated browser's download state must be preserved" ); // Cleanup so we don't leak global state into other tests. - clear_download_state_for_browser("camoufox"); + clear_download_state_for_browser("wayfern"); + clear_download_state_for_browser("chromium"); } } diff --git a/src-tauri/src/ephemeral_dirs.rs b/src-tauri/src/ephemeral_dirs.rs index 04ef1fb..19f2705 100644 --- a/src-tauri/src/ephemeral_dirs.rs +++ b/src-tauri/src/ephemeral_dirs.rs @@ -256,7 +256,7 @@ mod tests { BrowserProfile { id, name: "test".to_string(), - browser: "camoufox".to_string(), + browser: "wayfern".to_string(), version: "1.0".to_string(), proxy_id: None, vpn_id: None, @@ -264,7 +264,6 @@ mod tests { process_id: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), diff --git a/src-tauri/src/extension_manager.rs b/src-tauri/src/extension_manager.rs index 39a2d2d..72166cb 100644 --- a/src-tauri/src/extension_manager.rs +++ b/src-tauri/src/extension_manager.rs @@ -27,11 +27,6 @@ pub struct Extension { pub author: Option, #[serde(default)] pub homepage_url: Option, - /// Firefox extension ID from `browser_specific_settings.gecko.id` (or - /// `applications.gecko.id` in old manifests). Firefox refuses to load a - /// sideloaded .xpi unless the filename matches this value. - #[serde(default)] - pub gecko_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -69,9 +64,7 @@ fn extension_groups_file() -> PathBuf { fn determine_browser_compatibility(file_type: &str) -> Vec { match file_type { - "xpi" => vec!["firefox".to_string()], - "crx" => vec!["chromium".to_string()], - "zip" => vec!["chromium".to_string(), "firefox".to_string()], + "crx" | "zip" => vec!["chromium".to_string()], _ => vec![], } } @@ -79,7 +72,7 @@ fn determine_browser_compatibility(file_type: &str) -> Vec { fn get_file_type(file_name: &str) -> Option { let ext = file_name.rsplit('.').next()?.to_lowercase(); match ext.as_str() { - "xpi" | "crx" | "zip" => Some(ext), + "crx" | "zip" => Some(ext), _ => None, } } @@ -162,32 +155,6 @@ fn extract_manifest_metadata( (name, version, description, author, homepage_url) } -/// Read `browser_specific_settings.gecko.id` (or the legacy -/// `applications.gecko.id`) from the extension's manifest.json. Firefox uses -/// this value as the canonical add-on ID; sideloaded .xpi files must be named -/// `.xpi` to be picked up. -fn extract_gecko_id(file_data: &[u8], file_type: &str) -> Option { - let zip_start = if file_type == "crx" { - find_zip_start(file_data) - } else { - 0 - }; - let cursor = std::io::Cursor::new(&file_data[zip_start..]); - let mut archive = zip::ZipArchive::new(cursor).ok()?; - let mut manifest_content = String::new(); - std::io::Read::read_to_string( - &mut archive.by_name("manifest.json").ok()?, - &mut manifest_content, - ) - .ok()?; - let manifest: serde_json::Value = serde_json::from_str(&manifest_content).ok()?; - manifest - .pointer("/browser_specific_settings/gecko/id") - .or_else(|| manifest.pointer("/applications/gecko/id")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) -} - fn extract_icon_from_archive(file_data: &[u8], file_type: &str) -> Option<(Vec, String)> { let zip_start = if file_type == "crx" { find_zip_start(file_data) @@ -305,6 +272,9 @@ impl ExtensionManager { get_file_type(&file_name).ok_or_else(|| format!("Unsupported file type: {file_name}"))?; let browser_compatibility = determine_browser_compatibility(&file_type); + if browser_compatibility.is_empty() { + return Err(format!("Unsupported file type: {file_name}").into()); + } let now = now_secs(); let (manifest_name, version, description, author, homepage_url) = @@ -316,7 +286,6 @@ impl ExtensionManager { name }; - let gecko_id = extract_gecko_id(&file_data, &file_type); let ext = Extension { id: uuid::Uuid::new_v4().to_string(), name: final_name, @@ -331,7 +300,6 @@ impl ExtensionManager { description, author, homepage_url, - gecko_id, }; let file_dir = self.get_file_dir(&ext.id); @@ -448,7 +416,6 @@ impl ExtensionManager { ext.name = mn; } } - ext.gecko_id = extract_gecko_id(&data, &new_file_type); if let Some((icon_data, icon_ext)) = extract_icon_from_archive(&data, &new_file_type) { let icon_path = self.get_extension_dir(id).join(format!("icon.{icon_ext}")); @@ -863,7 +830,6 @@ impl ExtensionManager { ) -> Result<(), Box> { let group = self.get_group(group_id)?; let browser_type = match browser { - "camoufox" => "firefox", "wayfern" => "chromium", _ => return Err(format!("Extensions are not supported for browser '{browser}'").into()), }; @@ -892,7 +858,7 @@ impl ExtensionManager { pub fn install_extensions_for_profile( &self, profile: &crate::profile::BrowserProfile, - profile_data_path: &std::path::Path, + _profile_data_path: &std::path::Path, ) -> Result, Box> { let group_id = match &profile.extension_group_id { Some(id) => id, @@ -904,91 +870,40 @@ impl ExtensionManager { return Ok(Vec::new()); } - let browser_type = match profile.browser.as_str() { - "camoufox" => "firefox", - "wayfern" => "chromium", - _ => return Ok(Vec::new()), - }; + if profile.browser.as_str() != "wayfern" { + return Ok(Vec::new()); + } let mut extension_paths = Vec::new(); - match browser_type { - "firefox" => { - let extensions_dir = profile_data_path.join("extensions"); - // Clear existing extensions - if extensions_dir.exists() { - fs::remove_dir_all(&extensions_dir)?; + // Unpack Chromium extensions and return paths for --load-extension + let unpacked_base = extensions_base_dir().join("unpacked"); + if unpacked_base.exists() { + fs::remove_dir_all(&unpacked_base)?; + } + fs::create_dir_all(&unpacked_base)?; + + for ext_id in &group.extension_ids { + if let Ok(ext) = self.get_extension(ext_id) { + if !ext.browser_compatibility.contains(&"chromium".to_string()) { + continue; } - fs::create_dir_all(&extensions_dir)?; + let src_file = self.get_file_dir(ext_id).join(&ext.file_name); + if src_file.exists() { + let unpack_dir = unpacked_base.join(ext_id); + fs::create_dir_all(&unpack_dir)?; - for ext_id in &group.extension_ids { - if let Ok(ext) = self.get_extension(ext_id) { - if !ext.browser_compatibility.contains(&"firefox".to_string()) { - continue; + // Extract .crx or .zip + match Self::unpack_extension(&src_file, &unpack_dir) { + Ok(()) => { + extension_paths.push(unpack_dir.to_string_lossy().to_string()); } - let src_file = self.get_file_dir(ext_id).join(&ext.file_name); - if !src_file.exists() { - continue; - } - - // Firefox/Camoufox only loads sideloaded .xpi files whose filename - // matches `browser_specific_settings.gecko.id` from the manifest. - // Prefer the cached value; fall back to reading the manifest now - // for extensions added before the field existed. - let gecko_id = if let Some(ref id) = ext.gecko_id { - Some(id.clone()) - } else if let Ok(data) = fs::read(&src_file) { - extract_gecko_id(&data, &ext.file_type) - } else { - None - }; - - let Some(gecko_id) = gecko_id else { - log::warn!( - "Skipping Firefox extension '{}': could not determine gecko id from manifest.json", - ext.name - ); - continue; - }; - - let dest = extensions_dir.join(format!("{gecko_id}.xpi")); - fs::copy(&src_file, &dest)?; - extension_paths.push(dest.to_string_lossy().to_string()); - } - } - } - "chromium" => { - // For Chromium, unpack extensions and return paths for --load-extension - let unpacked_base = extensions_base_dir().join("unpacked"); - if unpacked_base.exists() { - fs::remove_dir_all(&unpacked_base)?; - } - fs::create_dir_all(&unpacked_base)?; - - for ext_id in &group.extension_ids { - if let Ok(ext) = self.get_extension(ext_id) { - if !ext.browser_compatibility.contains(&"chromium".to_string()) { - continue; - } - let src_file = self.get_file_dir(ext_id).join(&ext.file_name); - if src_file.exists() { - let unpack_dir = unpacked_base.join(ext_id); - fs::create_dir_all(&unpack_dir)?; - - // Extract .crx or .zip - match Self::unpack_extension(&src_file, &unpack_dir) { - Ok(()) => { - extension_paths.push(unpack_dir.to_string_lossy().to_string()); - } - Err(e) => { - log::warn!("Failed to unpack extension '{}': {}", ext.name, e); - } - } + Err(e) => { + log::warn!("Failed to unpack extension '{}': {}", ext.name, e); } } } } - _ => {} } Ok(extension_paths) @@ -1066,10 +981,8 @@ impl ExtensionManager { } let needs_meta_backfill = ext.version.is_none() && ext.description.is_none(); - let needs_gecko_backfill = - ext.gecko_id.is_none() && ext.browser_compatibility.iter().any(|b| b == "firefox"); - if needs_meta_backfill || needs_gecko_backfill { + if needs_meta_backfill { let file_path = file_dir.join(&ext.file_name); if let Ok(file_data) = fs::read(&file_path) { let mut updated_ext = ext.clone(); @@ -1100,13 +1013,6 @@ impl ExtensionManager { } } - if needs_gecko_backfill { - if let Some(gid) = extract_gecko_id(&file_data, &ext.file_type) { - updated_ext.gecko_id = Some(gid); - changed = true; - } - } - if changed { let metadata_path = self.get_metadata_path(&ext.id); if let Ok(json) = serde_json::to_string_pretty(&updated_ext) { @@ -1321,27 +1227,24 @@ mod tests { #[test] fn test_get_file_type() { - assert_eq!(get_file_type("ublock.xpi"), Some("xpi".to_string())); assert_eq!(get_file_type("ext.crx"), Some("crx".to_string())); assert_eq!(get_file_type("ext.zip"), Some("zip".to_string())); + assert_eq!(get_file_type("ublock.xpi"), None); assert_eq!(get_file_type("readme.txt"), None); assert_eq!(get_file_type("noext"), None); } #[test] fn test_determine_browser_compatibility() { - assert_eq!( - determine_browser_compatibility("xpi"), - vec!["firefox".to_string()] - ); assert_eq!( determine_browser_compatibility("crx"), vec!["chromium".to_string()] ); assert_eq!( determine_browser_compatibility("zip"), - vec!["chromium".to_string(), "firefox".to_string()] + vec!["chromium".to_string()] ); + assert_eq!(determine_browser_compatibility("xpi"), Vec::::new()); } #[test] @@ -1359,13 +1262,13 @@ mod tests { let ext = mgr .add_extension( "Test Ext".to_string(), - "test.xpi".to_string(), + "test.zip".to_string(), vec![0, 1, 2, 3], ) .unwrap(); assert_eq!(ext.name, "Test Ext"); - assert_eq!(ext.file_type, "xpi"); - assert_eq!(ext.browser_compatibility, vec!["firefox".to_string()]); + assert_eq!(ext.file_type, "zip"); + assert_eq!(ext.browser_compatibility, vec!["chromium".to_string()]); // Get let fetched = mgr.get_extension(&ext.id).unwrap(); @@ -1407,7 +1310,7 @@ mod tests { let ext = mgr .add_extension( "Test Ext".to_string(), - "test.xpi".to_string(), + "test.zip".to_string(), vec![0, 1, 2, 3], ) .unwrap(); @@ -1432,26 +1335,21 @@ mod tests { let mgr = ExtensionManager::new(); - let ext = mgr + let chrome_ext = mgr .add_extension( - "Firefox Ext".to_string(), - "test.xpi".to_string(), + "Chromium Ext".to_string(), + "test.crx".to_string(), vec![0, 1, 2, 3], ) .unwrap(); + let chrome_group = mgr.create_group("Chromium Group".to_string()).unwrap(); + mgr + .add_extension_to_group(&chrome_group.id, &chrome_ext.id) + .unwrap(); - let group = mgr.create_group("Firefox Group".to_string()).unwrap(); - mgr.add_extension_to_group(&group.id, &ext.id).unwrap(); - - // Compatible with camoufox (firefox-based) assert!(mgr - .validate_group_compatibility(&group.id, "camoufox") + .validate_group_compatibility(&chrome_group.id, "wayfern") .is_ok()); - - // Incompatible with wayfern (chromium-based) - assert!(mgr - .validate_group_compatibility(&group.id, "wayfern") - .is_err()); } #[test] @@ -1474,7 +1372,7 @@ mod tests { let mgr = ExtensionManager::new(); let ext = mgr - .add_extension("Test".to_string(), "test.xpi".to_string(), vec![0, 1, 2, 3]) + .add_extension("Test".to_string(), "test.zip".to_string(), vec![0, 1, 2, 3]) .unwrap(); let group = mgr.create_group("G1".to_string()).unwrap(); diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index cb4b5bb..fea8e04 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -67,17 +67,15 @@ impl Extractor { exe_path: &Path, ) -> Result<(), Box> { // Determine browser type from the destination directory path - let browser_type = if dest_dir.to_string_lossy().contains("camoufox") { - "camoufox" - } else if dest_dir.to_string_lossy().contains("wayfern") { + let browser_type = if dest_dir.to_string_lossy().contains("wayfern") { "wayfern" } else { return Ok(()); }; - // For Camoufox and Wayfern on Linux, we expect the executable directly under version directory - // e.g., binaries/camoufox//camoufox, without an extra subdirectory - if browser_type == "camoufox" || browser_type == "wayfern" { + // For Wayfern on Linux, we expect the executable directly under version directory + // e.g., binaries/wayfern//wayfern, without an extra subdirectory + if browser_type == "wayfern" { return Ok(()); } @@ -977,13 +975,7 @@ impl Extractor { ); // Look for .exe files, preferring main browser executables - let priority_exe_names = [ - "firefox.exe", - "chrome.exe", - "chromium.exe", - "camoufox.exe", - "wayfern.exe", - ]; + let priority_exe_names = ["firefox.exe", "chrome.exe", "chromium.exe", "wayfern.exe"]; // First try priority executable names for exe_name in &priority_exe_names { @@ -1049,7 +1041,6 @@ impl Extractor { || file_name.contains("chrome") || file_name.contains("chromium") || file_name.contains("browser") - || file_name.contains("camoufox") || file_name.contains("wayfern") { return Ok(path); @@ -1098,7 +1089,7 @@ impl Extractor { // Enhanced list of common browser executable names let exe_names = [ - // Firefox variants (used by Camoufox) + // Firefox variants "firefox", "firefox-bin", // Chrome/Chromium variants (used by Wayfern) @@ -1106,10 +1097,6 @@ impl Extractor { "chromium", "chromium-browser", "chromium-bin", - // Camoufox variants - "camoufox", - "camoufox-bin", - "camoufox-browser", // Wayfern variants "wayfern", "wayfern-bin", @@ -1136,16 +1123,13 @@ impl Extractor { "firefox", "chrome", "chromium", - "camoufox", "wayfern", ".", "./", "Browser", "browser", - "opt/camoufox", "usr/lib/firefox", "usr/lib/chromium", - "usr/lib/camoufox", "usr/share/applications", "usr/bin", "AppRun", @@ -1239,7 +1223,6 @@ impl Extractor { || name_lower.contains("chrome") || name_lower.contains("brave") || name_lower.contains("zen") - || name_lower.contains("camoufox") || name_lower.contains("wayfern") || name_lower.ends_with(".appimage") || !name_lower.contains('.') @@ -1295,7 +1278,6 @@ impl Extractor { || name_lower.contains("chrome") || name_lower.contains("brave") || name_lower.contains("zen") - || name_lower.contains("camoufox") || name_lower.contains("wayfern") || file_name.ends_with(".AppImage") { diff --git a/src-tauri/src/geoip_downloader.rs b/src-tauri/src/geoip_downloader.rs index 2238203..58cc2d0 100644 --- a/src-tauri/src/geoip_downloader.rs +++ b/src-tauri/src/geoip_downloader.rs @@ -1,7 +1,6 @@ use crate::browser::GithubRelease; use crate::events; use crate::profile::manager::ProfileManager; -use directories::BaseDirs; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -46,30 +45,12 @@ impl GeoIPDownloader { Self { client } } - fn get_cache_dir() -> Result> { - let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?; - - #[cfg(target_os = "windows")] - let cache_dir = base_dirs - .data_local_dir() - .join("camoufox") - .join("camoufox") - .join("Cache"); - - #[cfg(target_os = "macos")] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - #[cfg(target_os = "linux")] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - Ok(cache_dir) + fn get_cache_dir() -> PathBuf { + crate::app_dirs::cache_dir() } - fn get_mmdb_file_path() -> Result> { - Ok(Self::get_cache_dir()?.join("GeoLite2-City.mmdb")) + pub fn get_mmdb_file_path() -> Result> { + Ok(Self::get_cache_dir().join("GeoLite2-City.mmdb")) } pub fn is_geoip_database_available() -> bool { @@ -100,7 +81,7 @@ impl GeoIPDownloader { now.saturating_sub(timestamp) > SEVEN_DAYS } - /// Check if GeoIP database is missing or stale for Camoufox profiles + /// Check if GeoIP database is missing or stale for Wayfern fingerprint geo. pub fn check_missing_geoip_database( &self, ) -> Result> { @@ -108,9 +89,9 @@ impl GeoIPDownloader { .list_profiles() .map_err(|e| format!("Failed to list profiles: {e}"))?; - let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox"); + let needs_geoip = profiles.iter().any(|profile| profile.browser == "wayfern"); - if has_camoufox_profiles { + if needs_geoip { return Ok(!Self::is_geoip_database_available() || Self::is_geoip_stale()); } @@ -169,7 +150,7 @@ impl GeoIPDownloader { .ok_or("No compatible GeoIP database asset found")?; // Create cache directory - let cache_dir = Self::get_cache_dir()?; + let cache_dir = Self::get_cache_dir(); fs::create_dir_all(&cache_dir).await?; let mmdb_path = Self::get_mmdb_file_path()?; @@ -406,10 +387,7 @@ mod tests { #[test] fn test_get_cache_dir() { let cache_dir = GeoIPDownloader::get_cache_dir(); - assert!(cache_dir.is_ok()); - - let path = cache_dir.unwrap(); - assert!(path.to_string_lossy().contains("camoufox")); + assert!(!cache_dir.as_os_str().is_empty()); } #[test] diff --git a/src-tauri/src/camoufox/geolocation.rs b/src-tauri/src/geolocation.rs similarity index 63% rename from src-tauri/src/camoufox/geolocation.rs rename to src-tauri/src/geolocation.rs index c95fb99..ce0646d 100644 --- a/src-tauri/src/camoufox/geolocation.rs +++ b/src-tauri/src/geolocation.rs @@ -1,24 +1,19 @@ -//! Geolocation support for Camoufox fingerprinting. -//! -//! This module provides IP-based geolocation lookup using the MaxMind GeoLite2 database, +//! IP-based geolocation lookup using the MaxMind GeoLite2 database, //! and locale generation based on country/territory information. -use crate::camoufox::data; use crate::geoip_downloader::GeoIPDownloader; -use directories::BaseDirs; use maxminddb::{geoip2, Reader}; use quick_xml::events::Event; use quick_xml::Reader as XmlReader; use rand::RngExt; use std::collections::HashMap; use std::net::IpAddr; -use std::path::PathBuf; use std::str::FromStr; -// Re-export IP utilities for backward compatibility -pub use crate::ip_utils::{fetch_public_ip, is_ipv4, is_ipv6, validate_ip, IpError}; +const TERRITORY_INFO_XML: &str = include_str!("territory_info.xml"); + +pub use crate::ip_utils::IpError; -/// Geolocation error type. #[derive(Debug, thiserror::Error)] pub enum GeolocationError { #[error("GeoIP database not found. Please download it first.")] @@ -42,23 +37,17 @@ pub enum GeolocationError { #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("Network error: {0}")] - Network(String), - #[error("IP error: {0}")] Ip(#[from] IpError), } -/// Locale information. #[derive(Debug, Clone)] pub struct Locale { pub language: String, pub region: Option, - pub script: Option, } impl Locale { - /// Format locale as a string (e.g., "en-US"). pub fn as_string(&self) -> String { if let Some(region) = &self.region { format!("{}-{}", self.language, region) @@ -66,84 +55,30 @@ impl Locale { self.language.clone() } } - - /// Convert to config format for Camoufox. - pub fn as_config(&self) -> HashMap { - let mut config = HashMap::new(); - - if let Some(region) = &self.region { - config.insert( - "locale:region".to_string(), - serde_json::json!(region.to_uppercase()), - ); - } - - config.insert( - "locale:language".to_string(), - serde_json::json!(self.language.clone()), - ); - - if let Some(script) = &self.script { - config.insert("locale:script".to_string(), serde_json::json!(script)); - } - - config - } } -/// Geolocation information. #[derive(Debug, Clone)] pub struct Geolocation { pub locale: Locale, pub longitude: f64, pub latitude: f64, pub timezone: String, - pub accuracy: Option, } -impl Geolocation { - /// Convert to config format for Camoufox. - pub fn as_config(&self) -> HashMap { - let mut config = self.locale.as_config(); - - config.insert( - "geolocation:longitude".to_string(), - serde_json::json!(self.longitude), - ); - config.insert( - "geolocation:latitude".to_string(), - serde_json::json!(self.latitude), - ); - config.insert("timezone".to_string(), serde_json::json!(self.timezone)); - - if let Some(accuracy) = self.accuracy { - config.insert( - "geolocation:accuracy".to_string(), - serde_json::json!(accuracy), - ); - } - - config - } -} - -/// Territory language population data. struct LanguagePopulation { language: String, population_percent: f64, } -/// Statistical locale selector based on territory language populations. pub struct LocaleSelector { territories: HashMap>, } impl LocaleSelector { - /// Create a new locale selector by parsing territory info XML. pub fn new() -> Result { let mut territories: HashMap> = HashMap::new(); - let mut reader = XmlReader::from_str(data::TERRITORY_INFO_XML); + let mut reader = XmlReader::from_str(TERRITORY_INFO_XML); reader.config_mut().trim_text(true); let mut current_territory: Option = None; @@ -158,14 +93,12 @@ impl LocaleSelector { let name_str = std::str::from_utf8(name.as_ref()).unwrap_or(""); if name_str == "territory" { - // Save previous territory if exists if let Some(code) = current_territory.take() { if !current_languages.is_empty() { territories.insert(code, std::mem::take(&mut current_languages)); } } - // Get territory type attribute for attr in e.attributes().flatten() { if attr.key.as_ref() == b"type" { current_territory = Some(String::from_utf8_lossy(&attr.value).to_uppercase()); @@ -199,7 +132,6 @@ impl LocaleSelector { let name_ref = e.name(); let name = std::str::from_utf8(name_ref.as_ref()).unwrap_or(""); if name == "territory" { - // Save territory if let Some(code) = current_territory.take() { if !current_languages.is_empty() { territories.insert(code, std::mem::take(&mut current_languages)); @@ -220,7 +152,7 @@ impl LocaleSelector { Ok(Self { territories }) } - /// Get a locale for a given region/country code. + #[allow(clippy::wrong_self_convention)] pub fn from_region(&self, region: &str) -> Result { let region_upper = region.to_uppercase(); @@ -233,7 +165,6 @@ impl LocaleSelector { return Err(GeolocationError::NoLanguageData(region.to_string())); } - // Weighted random selection based on population percentage let total: f64 = languages.iter().map(|l| l.population_percent).sum(); let mut rng = rand::rng(); let target = rng.random::() * total; @@ -249,7 +180,6 @@ impl LocaleSelector { } } - // Fallback to first language let first_lang = &languages[0].language; Ok(normalize_locale(&format!( "{}-{}", @@ -266,8 +196,6 @@ impl Default for LocaleSelector { } } -/// Normalize a locale string to standard format. -/// Handles formats like "en-US", "zh-Hant-US", "zh-Hans-CN". fn normalize_locale(locale: &str) -> Locale { let parts: Vec<&str> = locale.split('-').collect(); @@ -276,73 +204,22 @@ fn normalize_locale(locale: &str) -> Locale { .map(|s| s.to_lowercase()) .unwrap_or_else(|| "en".to_string()); - // A 4-letter part is a script subtag (e.g. "Hant", "Hans", "Cyrl"). - // A 2-letter or 3-digit part is a region subtag (e.g. "US", "CN"). - let mut explicit_script: Option = None; - let mut region: Option = None; + let mut region = None; for part in parts.iter().skip(1) { if part.len() == 4 && part.chars().all(|c| c.is_ascii_alphabetic()) { - explicit_script = Some(part[..1].to_uppercase() + &part[1..].to_lowercase()); - } else { - region = Some(part.to_uppercase()); + // Script subtag (e.g. Hans/Hant) — ignored; Wayfern fingerprint uses language+region only. + continue; } + region = Some(part.to_uppercase()); } - let script = if explicit_script.is_some() { - explicit_script - } else { - match language.as_str() { - "zh" => { - if region.as_deref() == Some("TW") || region.as_deref() == Some("HK") { - Some("Hant".to_string()) - } else { - Some("Hans".to_string()) - } - } - "sr" => Some("Cyrl".to_string()), - _ => None, - } - }; - - Locale { - language, - region, - script, - } + Locale { language, region } } -/// Get the path to the GeoIP MMDB file. -fn get_mmdb_path() -> Result { - let base_dirs = BaseDirs::new().ok_or(GeolocationError::DatabaseNotFound)?; - - #[cfg(target_os = "windows")] - let cache_dir = base_dirs - .data_local_dir() - .join("camoufox") - .join("camoufox") - .join("Cache"); - - #[cfg(target_os = "macos")] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - #[cfg(target_os = "linux")] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - let cache_dir = base_dirs.cache_dir().join("camoufox"); - - Ok(cache_dir.join("GeoLite2-City.mmdb")) -} - -/// Check if the GeoIP database is available. -pub fn is_geoip_available() -> bool { - GeoIPDownloader::is_geoip_database_available() -} - -/// Get geolocation information for an IP address. pub fn get_geolocation(ip: &str) -> Result { - let mmdb_path = get_mmdb_path()?; + let mmdb_path = + GeoIPDownloader::get_mmdb_file_path().map_err(|_| GeolocationError::DatabaseNotFound)?; if !mmdb_path.exists() { return Err(GeolocationError::DatabaseNotFound); @@ -362,7 +239,6 @@ pub fn get_geolocation(ip: &str) -> Result { .map_err(|e| GeolocationError::LocationNotFound(e.to_string()))? .ok_or_else(|| GeolocationError::LocationNotFound(ip.to_string()))?; - // Extract location data let location = &city.location; let longitude = location @@ -376,14 +252,12 @@ pub fn get_geolocation(ip: &str) -> Result { .ok_or_else(|| GeolocationError::LocationNotFound("No timezone".to_string()))? .to_string(); - // Get country code let country = &city.country; let iso_code = country .iso_code .ok_or_else(|| GeolocationError::LocationNotFound("No country code".to_string()))? .to_uppercase(); - // Get locale from territory data let selector = LocaleSelector::new()?; let locale = selector.from_region(&iso_code)?; @@ -392,7 +266,6 @@ pub fn get_geolocation(ip: &str) -> Result { longitude, latitude, timezone, - accuracy: location.accuracy_radius.map(|r| r as f64), }) } @@ -410,7 +283,6 @@ mod tests { fn test_locale_from_region() { let selector = LocaleSelector::new().unwrap(); - // Test common regions let us_locale = selector.from_region("US"); assert!(us_locale.is_ok()); let us = us_locale.unwrap(); @@ -427,14 +299,12 @@ mod tests { let locale = Locale { language: "en".to_string(), region: Some("US".to_string()), - script: None, }; assert_eq!(locale.as_string(), "en-US"); let locale_no_region = Locale { language: "en".to_string(), region: None, - script: None, }; assert_eq!(locale_no_region.as_string(), "en"); } @@ -444,25 +314,13 @@ mod tests { let locale = normalize_locale("en-US"); assert_eq!(locale.language, "en"); assert_eq!(locale.region, Some("US".to_string())); - assert!(locale.script.is_none()); let zh_tw = normalize_locale("zh-TW"); assert_eq!(zh_tw.language, "zh"); assert_eq!(zh_tw.region, Some("TW".to_string())); - assert_eq!(zh_tw.script, Some("Hant".to_string())); - let zh_cn = normalize_locale("zh-CN"); - assert_eq!(zh_cn.script, Some("Hans".to_string())); - - // 3-part locale: language-script-region let zh_hant_us = normalize_locale("zh-Hant-US"); assert_eq!(zh_hant_us.language, "zh"); assert_eq!(zh_hant_us.region, Some("US".to_string())); - assert_eq!(zh_hant_us.script, Some("Hant".to_string())); - - let zh_hans_us = normalize_locale("zh-Hans-US"); - assert_eq!(zh_hans_us.language, "zh"); - assert_eq!(zh_hans_us.region, Some("US".to_string())); - assert_eq!(zh_hans_us.script, Some("Hans".to_string())); } } diff --git a/src-tauri/src/ip_utils.rs b/src-tauri/src/ip_utils.rs index 3e35c44..0689f09 100644 --- a/src-tauri/src/ip_utils.rs +++ b/src-tauri/src/ip_utils.rs @@ -10,9 +10,6 @@ use std::str::FromStr; pub enum IpError { #[error("Network error: {0}")] Network(String), - - #[error("Invalid IP address: {0}")] - InvalidIP(String), } /// Validate an IP address (IPv4 or IPv6). @@ -20,24 +17,6 @@ pub fn validate_ip(ip: &str) -> bool { IpAddr::from_str(ip).is_ok() } -/// Check if an IP is IPv4. -pub fn is_ipv4(ip: &str) -> bool { - if let Ok(addr) = IpAddr::from_str(ip) { - addr.is_ipv4() - } else { - false - } -} - -/// Check if an IP is IPv6. -pub fn is_ipv6(ip: &str) -> bool { - if let Ok(addr) = IpAddr::from_str(ip) { - addr.is_ipv6() - } else { - false - } -} - /// Fetch public IP address, optionally through a proxy. pub async fn fetch_public_ip(proxy: Option<&str>) -> Result { let urls = [ @@ -114,18 +93,4 @@ mod tests { assert!(!validate_ip("invalid")); assert!(!validate_ip("256.256.256.256")); } - - #[test] - fn test_is_ipv4() { - assert!(is_ipv4("8.8.8.8")); - assert!(!is_ipv4("2001:4860:4860::8888")); - assert!(!is_ipv4("invalid")); - } - - #[test] - fn test_is_ipv6() { - assert!(is_ipv6("2001:4860:4860::8888")); - assert!(!is_ipv6("8.8.8.8")); - assert!(!is_ipv6("invalid")); - } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b81fbba..8f36f0c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,8 +22,6 @@ mod auto_updater; mod browser; mod browser_runner; mod browser_version_manager; -pub mod camoufox; -mod camoufox_manager; mod default_browser; pub mod dns_blocklist; mod downloaded_browsers_registry; @@ -32,6 +30,7 @@ mod ephemeral_dirs; mod extension_manager; mod extraction; mod geoip_downloader; +mod geolocation; mod group_manager; mod human_typing; mod ip_utils; @@ -69,10 +68,9 @@ use browser_runner::{ use profile::manager::{ check_browser_status, clone_profile, create_browser_profile_new, delete_profile, - list_browser_profiles, rename_profile, update_camoufox_config, update_profile_dns_blocklist, - update_profile_launch_hook, update_profile_note, update_profile_proxy, - update_profile_proxy_bypass_rules, update_profile_tags, update_profile_vpn, - update_profile_window_color, update_wayfern_config, + list_browser_profiles, rename_profile, update_profile_dns_blocklist, update_profile_launch_hook, + update_profile_note, update_profile_proxy, update_profile_proxy_bypass_rules, + update_profile_tags, update_profile_vpn, update_profile_window_color, update_wayfern_config, }; use profile::password::{ @@ -1188,7 +1186,6 @@ async fn generate_sample_fingerprint( launch_hook: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), @@ -1209,15 +1206,7 @@ async fn generate_sample_fingerprint( updated_at: None, }; - if browser == "camoufox" { - let config: crate::camoufox_manager::CamoufoxConfig = - serde_json::from_str(&config_json).map_err(|e| format!("Failed to parse config: {e}"))?; - let manager = crate::camoufox_manager::CamoufoxManager::instance(); - manager - .generate_fingerprint_config(&app_handle, &temp_profile, &config) - .await - .map_err(|e| format!("Failed to generate fingerprint: {e}")) - } else if browser == "wayfern" { + if browser == "wayfern" { let config: crate::wayfern_manager::WayfernConfig = serde_json::from_str(&config_json).map_err(|e| format!("Failed to parse config: {e}"))?; let manager = crate::wayfern_manager::WayfernManager::instance(); @@ -1849,26 +1838,6 @@ pub fn run() { } }); - // Start Camoufox cleanup task - let _app_handle_cleanup = app.handle().clone(); - tauri::async_runtime::spawn(async move { - let camoufox_manager = crate::camoufox_manager::CamoufoxManager::instance(); - let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60)); - - loop { - interval.tick().await; - - match camoufox_manager.cleanup_dead_instances().await { - Ok(_) => { - // Cleanup completed silently - } - Err(e) => { - log::error!("Error during Camoufox cleanup: {e}"); - } - } - } - }); - // Check and download GeoIP database at startup if needed let app_handle_geoip = app.handle().clone(); tauri::async_runtime::spawn(async move { @@ -1879,7 +1848,7 @@ pub fn run() { match geoip_downloader.check_missing_geoip_database() { Ok(true) => { log::info!( - "GeoIP database is missing for Camoufox profiles, downloading at startup..." + "GeoIP database is missing for Wayfern profiles, downloading at startup..." ); let geoip_downloader = GeoIPDownloader::instance(); if let Err(e) = geoip_downloader @@ -1892,7 +1861,7 @@ pub fn run() { } } Ok(false) => { - // No Camoufox profiles or GeoIP database already available + // No Wayfern profiles or GeoIP database already available } Err(e) => { log::error!("Failed to check GeoIP database status at startup: {e}"); @@ -2301,7 +2270,6 @@ pub fn run() { import_proxies_json, parse_txt_proxies, import_proxies_from_parsed, - update_camoufox_config, update_wayfern_config, generate_sample_fingerprint, get_profile_groups, diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index e3c1958..4ac16e6 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -507,7 +507,7 @@ impl McpServer { vec![ McpTool { name: "list_profiles".to_string(), - description: "List all Wayfern and Camoufox browser profiles".to_string(), + description: "List all Wayfern browser profiles".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, @@ -614,7 +614,7 @@ impl McpServer { }, "browser": { "type": "string", - "enum": ["wayfern", "camoufox"], + "enum": ["wayfern"], "description": "Browser engine to use" }, "proxy_id": { @@ -1049,7 +1049,7 @@ impl McpServer { // Fingerprint management tools McpTool { name: "get_profile_fingerprint".to_string(), - description: "Get the fingerprint configuration for a Wayfern or Camoufox profile" + description: "Get the fingerprint configuration for a Wayfern profile" .to_string(), input_schema: serde_json::json!({ "type": "object", @@ -1065,7 +1065,7 @@ impl McpServer { McpTool { name: "update_profile_fingerprint".to_string(), description: - "Update the fingerprint configuration for a Wayfern or Camoufox profile. Requires an active Pro subscription." + "Update the fingerprint configuration for a Wayfern profile. Requires an active Pro subscription." .to_string(), input_schema: serde_json::json!({ "type": "object", @@ -1208,7 +1208,7 @@ impl McpServer { // Cookie management tools McpTool { name: "import_profile_cookies".to_string(), - description: "Import cookies into a Wayfern or Camoufox profile from a JSON array (Puppeteer / EditThisCookie format) or a Netscape cookies.txt. Format is auto-detected. The browser must not be running.".to_string(), + description: "Import cookies into a Wayfern profile from a JSON array (Puppeteer / EditThisCookie format) or a Netscape cookies.txt. Format is auto-detected. The browser must not be running.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { @@ -1902,11 +1902,9 @@ impl McpServer { message: format!("Failed to list profiles: {e}"), })?; - // Filter to only Wayfern and Camoufox profiles - let filtered: Vec<&BrowserProfile> = profiles - .iter() - .filter(|p| p.browser == "wayfern" || p.browser == "camoufox") - .collect(); + // Filter to only Wayfern profiles + let filtered: Vec<&BrowserProfile> = + profiles.iter().filter(|p| p.browser == "wayfern").collect(); Ok(serde_json::json!({ "content": [{ @@ -1943,11 +1941,11 @@ impl McpServer { message: format!("Profile not found: {profile_id}"), })?; - // Check if it's a Wayfern or Camoufox profile - if profile.browser != "wayfern" && profile.browser != "camoufox" { + // Check if it's a Wayfern profile + if profile.browser != "wayfern" { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }); } @@ -2000,11 +1998,11 @@ impl McpServer { message: format!("Profile not found: {profile_id}"), })?; - // Check if it's a Wayfern or Camoufox profile - if profile.browser != "wayfern" && profile.browser != "camoufox" { + // Check if it's a Wayfern profile + if profile.browser != "wayfern" { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }); } @@ -2082,11 +2080,11 @@ impl McpServer { message: format!("Profile not found: {profile_id}"), })?; - // Check if it's a Wayfern or Camoufox profile - if profile.browser != "wayfern" && profile.browser != "camoufox" { + // Check if it's a Wayfern profile + if profile.browser != "wayfern" { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }); } @@ -2173,9 +2171,9 @@ impl McpServer { lines.push(format!("{profile_id}: not found")); continue; }; - if profile.browser != "wayfern" && profile.browser != "camoufox" { + if profile.browser != "wayfern" { lines.push(format!( - "{profile_id}: unsupported browser (MCP supports Wayfern/Camoufox)" + "{profile_id}: unsupported browser (MCP supports Wayfern)" )); continue; } @@ -2298,10 +2296,10 @@ impl McpServer { message: "Missing browser".to_string(), })?; - if browser != "wayfern" && browser != "camoufox" { + if browser != "wayfern" { return Err(McpError { code: -32602, - message: "browser must be 'wayfern' or 'camoufox'".to_string(), + message: "browser must be 'wayfern'".to_string(), }); } @@ -2350,7 +2348,6 @@ impl McpServer { proxy_id, None, None, - None, group_id, false, None, @@ -2594,11 +2591,11 @@ impl McpServer { message: format!("Profile not found: {profile_id}"), })?; - // Check if it's a Wayfern or Camoufox profile - if profile.browser != "wayfern" && profile.browser != "camoufox" { + // Check if it's a Wayfern profile + if profile.browser != "wayfern" { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }); } @@ -3507,23 +3504,6 @@ impl McpServer { })?; let fingerprint_info = match profile.browser.as_str() { - "camoufox" => { - let config = profile - .camoufox_config - .as_ref() - .cloned() - .unwrap_or_default(); - serde_json::json!({ - "browser": "camoufox", - "fingerprint": config.fingerprint, - "os": config.os, - "randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch, - "screen_max_width": config.screen_max_width, - "screen_max_height": config.screen_max_height, - "screen_min_width": config.screen_min_width, - "screen_min_height": config.screen_min_height, - }) - } "wayfern" => { let config = profile.wayfern_config.as_ref().cloned().unwrap_or_default(); serde_json::json!({ @@ -3540,7 +3520,7 @@ impl McpServer { _ => { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }) } }; @@ -3612,29 +3592,6 @@ impl McpServer { })?; match profile.browser.as_str() { - "camoufox" => { - let mut config = profile - .camoufox_config - .as_ref() - .cloned() - .unwrap_or_default(); - if let Some(fp) = fingerprint { - config.fingerprint = Some(fp.to_string()); - } - if let Some(os_val) = os { - config.os = Some(os_val.to_string()); - } - if let Some(r) = randomize { - config.randomize_fingerprint_on_launch = Some(r); - } - ProfileManager::instance() - .update_camoufox_config(app_handle.clone(), profile_id, config) - .await - .map_err(|e| McpError { - code: -32000, - message: format!("Failed to update camoufox config: {e}"), - })?; - } "wayfern" => { let mut config = profile.wayfern_config.as_ref().cloned().unwrap_or_default(); if let Some(fp) = fingerprint { @@ -3657,7 +3614,7 @@ impl McpServer { _ => { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }) } } @@ -4007,10 +3964,6 @@ impl McpServer { crate::wayfern_manager::WayfernManager::instance() .get_cdp_port(&profile_path_str) .await - } else if profile.browser == "camoufox" { - crate::camoufox_manager::CamoufoxManager::instance() - .get_cdp_port(&profile_path_str) - .await } else { None }; @@ -4409,10 +4362,10 @@ impl McpServer { message: format!("Profile not found: {profile_id}"), })?; - if profile.browser != "wayfern" && profile.browser != "camoufox" { + if profile.browser != "wayfern" { return Err(McpError { code: -32000, - message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + message: "MCP only supports Wayfern profiles".to_string(), }); } diff --git a/src-tauri/src/platform_browser.rs b/src-tauri/src/platform_browser.rs index 14849b5..d20dbfa 100644 --- a/src-tauri/src/platform_browser.rs +++ b/src-tauri/src/platform_browser.rs @@ -15,7 +15,7 @@ use std::process::Command; fn cmd_matches_profile_path(cmd: &[std::ffi::OsString], profile_path: &str) -> bool { let args: Vec<&str> = cmd.iter().filter_map(|a| a.to_str()).collect(); for (i, arg) in args.iter().enumerate() { - // Exact argument equality (Firefox/Camoufox: `-profile `; some launchers + // Exact argument equality (Firefox: `-profile `; some launchers // pass the path as its own arg). if *arg == profile_path { return true; diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 1b8c1c6..1570f60 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -1,6 +1,4 @@ -use crate::api_client::is_browser_version_nightly; -use crate::browser::{create_browser, BrowserType, ProxySettings}; -use crate::camoufox_manager::CamoufoxConfig; +use crate::browser::{create_browser, BrowserType}; use crate::cloud_auth::CLOUD_AUTH; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::events; @@ -27,14 +25,12 @@ fn atomic_write(path: &Path, data: &[u8]) -> std::io::Result<()> { } pub struct ProfileManager { - camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager, wayfern_manager: &'static crate::wayfern_manager::WayfernManager, } impl ProfileManager { fn new() -> Self { Self { - camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(), wayfern_manager: crate::wayfern_manager::WayfernManager::instance(), } } @@ -80,7 +76,6 @@ impl ProfileManager { release_type: &str, proxy_id: Option, vpn_id: Option, - camoufox_config: Option, wayfern_config: Option, group_id: Option, ephemeral: bool, @@ -103,6 +98,14 @@ impl ProfileManager { log::info!("Attempting to create profile: {name}"); + if browser == "camoufox" { + return Err( + serde_json::json!({ "code": "CAMOUFOX_REMOVED" }) + .to_string() + .into(), + ); + } + // Check if a profile with this name already exists (case insensitive) let existing_profiles = self.list_profiles()?; if existing_profiles @@ -125,113 +128,6 @@ impl ProfileManager { create_dir_all(&profile_data_dir)?; } - // For Camoufox profiles, generate fingerprint during creation - let final_camoufox_config = if browser == "camoufox" { - let mut config = camoufox_config.unwrap_or_else(|| { - log::info!("Creating default Camoufox config for profile: {name}"); - crate::camoufox_manager::CamoufoxConfig::default() - }); - - // Pass upstream proxy information to config for fingerprint generation - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - // For fingerprint generation, pass upstream proxy directly with credentials if present - let proxy_url = if let (Some(username), Some(password)) = - (&proxy_settings.username, &proxy_settings.password) - { - format!( - "{}://{}:{}@{}:{}", - proxy_settings.proxy_type.to_lowercase(), - username, - password, - proxy_settings.host, - proxy_settings.port - ) - } else { - format!( - "{}://{}:{}", - proxy_settings.proxy_type.to_lowercase(), - proxy_settings.host, - proxy_settings.port - ) - }; - config.proxy = Some(proxy_url); - log::info!( - "Using upstream proxy for Camoufox fingerprint generation: {}://{}:{}", - proxy_settings.proxy_type.to_lowercase(), - proxy_settings.host, - proxy_settings.port - ); - } - } - - // Generate fingerprint if not already provided - if config.fingerprint.is_none() { - log::info!("Generating fingerprint for Camoufox profile: {name}"); - - // Use the camoufox launcher to generate the config - - // Create a temporary profile for fingerprint generation - let temp_profile = BrowserProfile { - id: uuid::Uuid::new_v4(), - name: name.to_string(), - browser: browser.to_string(), - version: version.to_string(), - proxy_id: proxy_id.clone(), - vpn_id: None, - launch_hook: launch_hook.clone(), - process_id: None, - last_launch: None, - release_type: release_type.to_string(), - camoufox_config: None, - wayfern_config: None, - group_id: group_id.clone(), - tags: Vec::new(), - note: None, - window_color: None, - sync_mode: SyncMode::Disabled, - encryption_salt: None, - last_sync: None, - host_os: None, - ephemeral: false, - extension_group_id: None, - proxy_bypass_rules: Vec::new(), - created_by_id: None, - created_by_email: None, - dns_blocklist: None, - password_protected: false, - created_at: None, - updated_at: None, - }; - - match self - .camoufox_manager - .generate_fingerprint_config(app_handle, &temp_profile, &config) - .await - { - Ok(generated_fingerprint) => { - config.fingerprint = Some(generated_fingerprint); - log::info!("Successfully generated fingerprint for profile: {name}"); - } - Err(e) => { - return Err( - format!("Failed to generate fingerprint for Camoufox profile '{name}': {e}").into(), - ); - } - } - } else { - log::info!("Using provided fingerprint for Camoufox profile: {name}"); - } - - // Clear the proxy from config after fingerprint generation - // Browser launch should always use local proxy, never direct to upstream - config.proxy = None; - - Some(config) - } else { - camoufox_config.clone() - }; - // For Wayfern profiles, generate fingerprint during creation let final_wayfern_config = if browser == "wayfern" { let mut config = wayfern_config.unwrap_or_else(|| { @@ -288,7 +184,6 @@ impl ProfileManager { process_id: None, last_launch: None, release_type: release_type.to_string(), - camoufox_config: None, wayfern_config: None, group_id: group_id.clone(), tags: Vec::new(), @@ -360,7 +255,6 @@ impl ProfileManager { process_id: None, last_launch: None, release_type: release_type.to_string(), - camoufox_config: final_camoufox_config, wayfern_config: final_wayfern_config, group_id: group_id.clone(), tags: Vec::new(), @@ -398,31 +292,6 @@ impl ProfileManager { log::info!("Profile '{name}' created successfully with ID: {profile_id}"); - // `apply_proxy_settings_to_profile` writes a Firefox-style user.js - // with the upstream proxy host. That is wrong for both supported - // browser types: - // - Camoufox: camoufox_manager rewrites user.js at every launch with - // the local donut-proxy host; writing the upstream here leaves a - // stale, wrong proxy in user.js until the next launch. - // - Wayfern: Chromium gets its proxy via `--proxy-pac-url=` at launch - // (see wayfern_manager.rs) and never reads user.js. - // So we only call it for any unrecognized browser type that might be - // a true Firefox-family target (none currently). Ephemeral profiles - // skip regardless because their data dir is created at launch time. - if !ephemeral && !matches!(browser, "camoufox" | "wayfern") { - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?; - } else { - // Proxy ID provided but not found, disable proxy - self.disable_proxy_settings_in_profile(&profile_data_dir)?; - } - } else { - // Create user.js with common Firefox preferences but no proxy - self.disable_proxy_settings_in_profile(&profile_data_dir)?; - } - } - // Emit profile creation event if let Err(e) = events::emit_empty("profiles-changed") { log::warn!("Warning: Failed to emit profiles-changed event: {e}"); @@ -698,12 +567,7 @@ impl ProfileManager { // Update version profile.version = version.to_string(); - // Update the release_type based on the version and browser - profile.release_type = if is_browser_version_nightly(&profile.browser, version, None) { - "nightly".to_string() - } else { - "stable".to_string() - }; + profile.release_type = "stable".to_string(); // Save the updated profile self.save_profile(&profile)?; @@ -1096,7 +960,6 @@ impl ProfileManager { process_id: None, last_launch: None, release_type: source.release_type, - camoufox_config: source.camoufox_config, wayfern_config: source.wayfern_config, group_id: source.group_id, tags: source.tags, @@ -1146,68 +1009,6 @@ impl ProfileManager { Ok(new_profile) } - pub async fn update_camoufox_config( - &self, - app_handle: tauri::AppHandle, - profile_id: &str, - config: CamoufoxConfig, - ) -> Result<(), Box> { - // Find the profile by ID - let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( - |_| -> Box { - format!("Invalid profile ID: {profile_id}").into() - }, - )?; - let profiles = - self - .list_profiles() - .map_err(|e| -> Box { - format!("Failed to list profiles: {e}").into() - })?; - let mut profile = profiles - .into_iter() - .find(|p| p.id == profile_uuid) - .ok_or_else(|| -> Box { - format!("Profile with ID '{profile_id}' not found").into() - })?; - - // Check if the browser is currently running using the comprehensive status check - let is_running = self - .check_browser_status(app_handle.clone(), &profile) - .await?; - - if is_running { - return Err( - "Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(), - ); - } - - // Update the Camoufox configuration - profile.camoufox_config = Some(config); - - // Save the updated profile - self - .save_profile(&profile) - .map_err(|e| -> Box { - format!("Failed to save profile: {e}").into() - })?; - - crate::sync::queue_profile_sync_if_eligible(&profile); - - log::info!( - "Camoufox configuration updated for profile '{}' (ID: {}).", - profile.name, - profile_id - ); - - // Emit profile config update event - if let Err(e) = events::emit_empty("profiles-changed") { - log::warn!("Warning: Failed to emit profiles-changed event: {e}"); - } - - Ok(()) - } - pub async fn update_wayfern_config( &self, app_handle: tauri::AppHandle, @@ -1323,44 +1124,6 @@ impl ProfileManager { } } - // Update on-disk browser profile config immediately. - // Both supported browser types ignore this write (Camoufox rewrites - // user.js at launch with the local donut-proxy host, Wayfern takes its - // proxy via `--proxy-pac-url=` and never reads user.js), and for - // Camoufox specifically writing the upstream host here would leave a - // stale, wrong proxy in user.js until the next launch. - if !matches!(profile.browser.as_str(), "camoufox" | "wayfern") { - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; - } else { - // Proxy ID provided but proxy not found, disable proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .disable_proxy_settings_in_profile(&profile_path) - .map_err(|e| -> Box { - format!("Failed to disable proxy settings: {e}").into() - })?; - } - } else { - // No proxy ID provided, disable proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .disable_proxy_settings_in_profile(&profile_path) - .map_err(|e| -> Box { - format!("Failed to disable proxy settings: {e}").into() - })?; - } - } - // Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager) if let Err(e) = events::emit("profile-updated", &profile) { log::warn!("Warning: Failed to emit profile update event: {e}"); @@ -1481,17 +1244,12 @@ impl ProfileManager { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - // Handle Camoufox profiles using CamoufoxManager-based status checking - if profile.browser == "camoufox" { - return self.check_camoufox_status(&app_handle, profile).await; - } - // Handle Wayfern profiles using WayfernManager-based status checking if profile.browser == "wayfern" { return self.check_wayfern_status(&app_handle, profile).await; } - // For non-camoufox browsers, use the existing PID-based logic + // For non-wayfern browsers, use the existing PID-based logic let inner_profile = profile.clone(); let system = System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), @@ -1510,18 +1268,8 @@ impl ProfileManager { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "camoufox" { - arg == profile_data_path_str - || arg == format!("-profile={profile_data_path_str}") - || (arg == "-profile" - && cmd - .iter() - .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) - } else { - // For Chromium-based browsers (Wayfern), check for user-data-dir - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str }); if profile_path_match { @@ -1539,7 +1287,6 @@ impl ProfileManager { // Check if this is the right browser executable first let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { - "camoufox" => exe_name.contains("camoufox") || exe_name.contains("firefox"), "wayfern" => { exe_name.contains("wayfern") || exe_name.contains("chromium") @@ -1559,18 +1306,8 @@ impl ProfileManager { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "camoufox" { - arg == profile_data_path_str - || arg == format!("-profile={profile_data_path_str}") - || (arg == "-profile" - && cmd - .iter() - .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) - } else { - // For Chromium-based browsers (Wayfern), check for user-data-dir - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str }); if profile_path_match { @@ -1644,146 +1381,6 @@ impl ProfileManager { Ok(is_running) } - // Check Camoufox status using CamoufoxManager - async fn check_camoufox_status( - &self, - app_handle: &tauri::AppHandle, - profile: &BrowserProfile, - ) -> Result> { - let launcher = self.camoufox_manager; - let profiles_dir = self.get_profiles_dir(); - let profile_data_path = - crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy(); - - // Check if there's a running Camoufox instance for this profile - match launcher.find_camoufox_by_profile(&profile_path_str).await { - Ok(Some(camoufox_process)) => { - // Found a running instance, update profile with process info if changed - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - let metadata_file = profile_uuid_dir.join("metadata.json"); - let metadata_exists = metadata_file.exists(); - - if metadata_exists { - // Load latest to avoid overwriting other fields - let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - { - Some(p) => p, - None => profile.clone(), - }; - - if latest.process_id != camoufox_process.processId { - let old_pid = latest.process_id; - latest.process_id = camoufox_process.processId; - if let Err(e) = self.save_profile(&latest) { - log::warn!("Warning: Failed to update Camoufox profile with process info: {e}"); - } - if let (Some(prev), Some(new)) = (old_pid, camoufox_process.processId) { - let _ = crate::proxy_manager::PROXY_MANAGER.update_proxy_pid(prev, new); - } - - // Emit profile update event to frontend - if let Err(e) = events::emit("profile-updated", &latest) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - - log::info!( - "Camoufox process has started for profile '{}' with PID: {:?}", - profile.name, - camoufox_process.processId - ); - } - } - Ok(true) - } - Ok(None) => { - // No running instance found, clear process ID if set and stop proxy - if profile.ephemeral { - crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); - } - - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - let metadata_file = profile_uuid_dir.join("metadata.json"); - let metadata_exists = metadata_file.exists(); - - if metadata_exists { - let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - { - Some(p) => p, - None => profile.clone(), - }; - - if latest.process_id.is_some() { - latest.process_id = None; - if let Err(e) = self.save_profile(&latest) { - log::warn!("Warning: Failed to clear Camoufox profile process info: {e}"); - } - - if let Some(updated) = crate::auto_updater::AutoUpdater::instance() - .update_profile_to_latest_installed(app_handle, &latest) - { - latest = updated; - } - - if let Err(e) = events::emit("profile-updated", &latest) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - } - } - Ok(false) - } - Err(e) => { - // Error checking status, assume not running and clear process ID - log::warn!("Warning: Failed to check Camoufox status: {e}"); - if profile.ephemeral { - crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); - } - - let profiles_dir = self.get_profiles_dir(); - let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); - let metadata_file = profile_uuid_dir.join("metadata.json"); - let metadata_exists = metadata_file.exists(); - - if metadata_exists { - let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - { - Some(p) => p, - None => profile.clone(), - }; - - if latest.process_id.is_some() { - latest.process_id = None; - if let Err(e2) = self.save_profile(&latest) { - log::warn!( - "Warning: Failed to clear Camoufox profile process info after error: {e2}" - ); - } - - if let Some(updated) = crate::auto_updater::AutoUpdater::instance() - .update_profile_to_latest_installed(app_handle, &latest) - { - latest = updated; - } - - // Emit profile update event to frontend - if let Err(e3) = events::emit("profile-updated", &latest) { - log::warn!("Warning: Failed to emit profile update event: {e3}"); - } - } - } - Ok(false) - } - } - } - // Check Wayfern status using WayfernManager async fn check_wayfern_status( &self, @@ -1880,212 +1477,6 @@ impl ProfileManager { } } } - - fn get_common_firefox_preferences(&self) -> Vec { - vec![ - // Disable default browser check - "user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(), - "user_pref(\"browser.shell.skipDefaultBrowserCheckOnFirstRun\", true);".to_string(), - "user_pref(\"browser.preferences.moreFromMozilla\", false);".to_string(), - "user_pref(\"services.sync.prefs.sync.browser.startup.upgradeDialog.enabled\", false);" - .to_string(), - // Disable welcome / first-run screens - "user_pref(\"browser.aboutwelcome.enabled\", false);".to_string(), - "user_pref(\"browser.startup.homepage_override.mstone\", \"ignore\");".to_string(), - "user_pref(\"startup.homepage_welcome_url\", \"\");".to_string(), - "user_pref(\"startup.homepage_welcome_url.additional\", \"\");".to_string(), - "user_pref(\"startup.homepage_override_url\", \"\");".to_string(), - // Keep extension updates enabled and allow sideloaded extensions. - // - autoDisableScopes=0: profile-installed extensions are enabled by default. - // - startupScanScopes=1: rescan SCOPE_PROFILE on each launch so freshly - // dropped .xpi files in /extensions/ get registered. - // - signatures.required=false: accept unsigned/dev .xpi files. Camoufox - // is built without MOZ_REQUIRE_SIGNING so this is honored. - "user_pref(\"extensions.update.enabled\", true);".to_string(), - "user_pref(\"extensions.update.autoUpdateDefault\", true);".to_string(), - "user_pref(\"extensions.autoDisableScopes\", 0);".to_string(), - "user_pref(\"extensions.startupScanScopes\", 1);".to_string(), - "user_pref(\"xpinstall.signatures.required\", false);".to_string(), - // Completely disable browser update checking - "user_pref(\"app.update.enabled\", false);".to_string(), - "user_pref(\"app.update.auto\", false);".to_string(), - "user_pref(\"app.update.mode\", 0);".to_string(), - "user_pref(\"app.update.service.enabled\", false);".to_string(), - "user_pref(\"app.update.staging.enabled\", false);".to_string(), - "user_pref(\"app.update.silent\", true);".to_string(), - "user_pref(\"app.update.disabledForTesting\", true);".to_string(), - // Prevent update URL access entirely - "user_pref(\"app.update.url\", \"\");".to_string(), - "user_pref(\"app.update.url.manual\", \"\");".to_string(), - "user_pref(\"app.update.url.details\", \"\");".to_string(), - // Disable update timing/scheduling - "user_pref(\"app.update.timerFirstInterval\", 999999999);".to_string(), - "user_pref(\"app.update.interval\", 999999999);".to_string(), - "user_pref(\"app.update.background.interval\", 999999999);".to_string(), - "user_pref(\"app.update.idletime\", 999999999);".to_string(), - "user_pref(\"app.update.promptWaitTime\", 999999999);".to_string(), - // Disable update attempts - "user_pref(\"app.update.download.maxAttempts\", 0);".to_string(), - "user_pref(\"app.update.elevate.maxAttempts\", 0);".to_string(), - "user_pref(\"app.update.checkInstallTime\", false);".to_string(), - // Suppress update UI/prompts/notifications - "user_pref(\"app.update.doorhanger\", false);".to_string(), - "user_pref(\"app.update.badge\", false);".to_string(), - "user_pref(\"app.update.notifyDuringDownload\", false);".to_string(), - "user_pref(\"app.update.background.scheduling.enabled\", false);".to_string(), - "user_pref(\"app.update.background.enabled\", false);".to_string(), - // Disable BITS (Windows Background Intelligent Transfer Service) updates - "user_pref(\"app.update.BITS.enabled\", false);".to_string(), - // Disable language pack updates - "user_pref(\"app.update.langpack.enabled\", false);".to_string(), - // Suppress upgrade dialogs on startup - "user_pref(\"browser.startup.upgradeDialog.enabled\", false);".to_string(), - // Disable update ping telemetry - "user_pref(\"toolkit.telemetry.updatePing.enabled\", false);".to_string(), - // Zen browser specific - disable welcome screen and updates - "user_pref(\"zen.welcome-screen.seen\", true);".to_string(), - "user_pref(\"zen.updates.enabled\", false);".to_string(), - "user_pref(\"zen.updates.check-for-updates\", false);".to_string(), - // Additional first-run suppressions - "user_pref(\"app.normandy.first_run\", false);".to_string(), - "user_pref(\"trailhead.firstrun.didSeeAboutWelcome\", true);".to_string(), - "user_pref(\"datareporting.policy.dataSubmissionPolicyBypassNotification\", true);" - .to_string(), - "user_pref(\"toolkit.telemetry.reportingpolicy.firstRun\", false);".to_string(), - // Disable quit confirmation dialogs - "user_pref(\"browser.warnOnQuit\", false);".to_string(), - "user_pref(\"browser.showQuitWarning\", false);".to_string(), - "user_pref(\"browser.tabs.warnOnClose\", false);".to_string(), - "user_pref(\"browser.tabs.warnOnCloseOtherTabs\", false);".to_string(), - "user_pref(\"browser.sessionstore.warnOnQuit\", false);".to_string(), - ] - } - - pub fn apply_proxy_settings_to_profile( - &self, - profile_data_path: &Path, - proxy: &ProxySettings, - internal_proxy: Option<&ProxySettings>, - ) -> Result<(), Box> { - let user_js_path = profile_data_path.join("user.js"); - let prefs_js_path = profile_data_path.join("prefs.js"); - - // Remove prefs.js if it exists to ensure Firefox reads user.js instead - // Firefox may cache proxy settings in prefs.js, so we need to clear it - if prefs_js_path.exists() { - log::info!("Removing prefs.js to ensure Firefox reads updated user.js settings"); - let _ = fs::remove_file(&prefs_js_path); - } - - let mut preferences = Vec::new(); - - // Add common Firefox preferences (like disabling default browser check) - preferences.extend(self.get_common_firefox_preferences()); - - // Determine which proxy settings to use - let effective_proxy = internal_proxy.unwrap_or(proxy); - let proxy_host = &effective_proxy.host; - let proxy_port = effective_proxy.port; - - // Check if this is a SOCKS proxy (only possible when using upstream directly) - let is_socks = - internal_proxy.is_none() && (proxy.proxy_type == "socks4" || proxy.proxy_type == "socks5"); - - log::info!( - "Applying manual proxy settings to Firefox profile: {}:{} (is_internal: {}, is_socks: {})", - proxy_host, - proxy_port, - internal_proxy.is_some(), - is_socks - ); - - // Use MANUAL proxy configuration (type 1) instead of PAC file (type 2) - // PAC files with file:// URLs are blocked by privacy-focused browsers like Zen - // Manual proxy configuration works reliably across all Firefox variants - preferences.push("user_pref(\"network.proxy.type\", 1);".to_string()); - - if is_socks { - // SOCKS proxy configuration - preferences.extend([ - format!("user_pref(\"network.proxy.socks\", \"{}\");", proxy_host), - format!("user_pref(\"network.proxy.socks_port\", {});", proxy_port), - format!( - "user_pref(\"network.proxy.socks_version\", {});", - if proxy.proxy_type == "socks5" { 5 } else { 4 } - ), - "user_pref(\"network.proxy.http\", \"\");".to_string(), - "user_pref(\"network.proxy.http_port\", 0);".to_string(), - "user_pref(\"network.proxy.ssl\", \"\");".to_string(), - "user_pref(\"network.proxy.ssl_port\", 0);".to_string(), - ]); - } else { - // HTTP/HTTPS proxy configuration (including our internal local proxy) - preferences.extend([ - format!("user_pref(\"network.proxy.http\", \"{}\");", proxy_host), - format!("user_pref(\"network.proxy.http_port\", {});", proxy_port), - format!("user_pref(\"network.proxy.ssl\", \"{}\");", proxy_host), - format!("user_pref(\"network.proxy.ssl_port\", {});", proxy_port), - format!("user_pref(\"network.proxy.ftp\", \"{}\");", proxy_host), - format!("user_pref(\"network.proxy.ftp_port\", {});", proxy_port), - "user_pref(\"network.proxy.socks\", \"\");".to_string(), - "user_pref(\"network.proxy.socks_port\", 0);".to_string(), - ]); - } - - // Common proxy settings - keep it simple like proxy-chain expected - preferences.extend([ - "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), - "user_pref(\"network.proxy.autoconfig_url\", \"\");".to_string(), - // Disable QUIC/HTTP3 - it bypasses HTTP proxy - "user_pref(\"network.http.http3.enable\", false);".to_string(), - "user_pref(\"network.http.http3.enabled\", false);".to_string(), - ]); - - // Write settings to user.js file - let user_js_content = preferences.join("\n"); - fs::write(user_js_path, &user_js_content)?; - log::info!( - "Updated user.js with manual proxy settings: {}:{}", - proxy_host, - proxy_port - ); - - Ok(()) - } - - pub fn disable_proxy_settings_in_profile( - &self, - profile_data_path: &Path, - ) -> Result<(), Box> { - let user_js_path = profile_data_path.join("user.js"); - let mut preferences = Vec::new(); - - // Get the UUID directory (parent of profile data directory) - let uuid_dir = profile_data_path - .parent() - .ok_or("Invalid profile path - cannot find UUID directory")?; - - // Add common Firefox preferences (like disabling default browser check) - preferences.extend(self.get_common_firefox_preferences()); - - preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); - preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); - - // Create a direct proxy PAC file in UUID directory - let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }"; - let pac_path = uuid_dir.join("proxy.pac"); - fs::write(&pac_path, pac_content)?; - let pac_url = - url::Url::from_file_path(&pac_path).map_err(|_| "Failed to convert PAC path to file URL")?; - preferences.push(format!( - "user_pref(\"network.proxy.autoconfig_url\", \"{}\");", - pac_url.as_str() - )); - - fs::write(user_js_path, preferences.join("\n"))?; - - Ok(()) - } } #[cfg(test)] @@ -2127,25 +1518,6 @@ mod tests { ); } - #[test] - fn test_get_common_firefox_preferences() { - let (manager, _temp_dir) = create_test_profile_manager(); - - let prefs = manager.get_common_firefox_preferences(); - assert!(!prefs.is_empty(), "Should return non-empty preferences"); - - // Check for some expected preferences - let prefs_string = prefs.join("\n"); - assert!( - prefs_string.contains("browser.shell.checkDefaultBrowser"), - "Should contain default browser check preference" - ); - assert!( - prefs_string.contains("app.update.enabled"), - "Should contain update preference" - ); - } - #[test] fn test_get_binaries_dir() { let (manager, _temp_dir) = create_test_profile_manager(); @@ -2163,109 +1535,6 @@ mod tests { ); } - #[test] - fn test_disable_proxy_settings_in_profile() { - let (manager, temp_dir) = create_test_profile_manager(); - - // Create a test profile directory - let profile_dir = temp_dir.path().join("test_profile"); - fs::create_dir_all(&profile_dir).expect("Should create profile directory"); - - let result = manager.disable_proxy_settings_in_profile(&profile_dir); - assert!(result.is_ok(), "Should successfully disable proxy settings"); - - // Check that user.js was created - let user_js_path = profile_dir.join("user.js"); - assert!(user_js_path.exists(), "user.js should be created"); - - let content = fs::read_to_string(&user_js_path).expect("Should read user.js"); - assert!( - content.contains("network.proxy.type"), - "Should contain proxy type setting" - ); - assert!( - content.contains("0"), - "Should set proxy type to 0 (no proxy)" - ); - } - - #[test] - fn test_apply_proxy_settings_to_profile() { - let (manager, temp_dir) = create_test_profile_manager(); - - // Create a test profile directory structure - let uuid_dir = temp_dir.path().join("test_uuid"); - let profile_dir = uuid_dir.join("profile"); - fs::create_dir_all(&profile_dir).expect("Should create profile directory"); - - let proxy_settings = ProxySettings { - proxy_type: "http".to_string(), - host: "proxy.example.com".to_string(), - port: 8080, - username: Some("user".to_string()), - password: Some("pass".to_string()), - }; - - let result = manager.apply_proxy_settings_to_profile(&profile_dir, &proxy_settings, None); - assert!(result.is_ok(), "Should successfully apply proxy settings"); - - // Check that user.js was created - let user_js_path = profile_dir.join("user.js"); - assert!(user_js_path.exists(), "user.js should be created"); - - let content = fs::read_to_string(&user_js_path).expect("Should read user.js"); - - // Check for manual proxy configuration (type 1) instead of PAC (type 2) - // Manual proxy is used because PAC file:// URLs are blocked by privacy browsers like Zen - assert!( - content.contains("network.proxy.type\", 1"), - "Should set proxy type to 1 (manual)" - ); - assert!( - content.contains("network.proxy.http\", \"proxy.example.com\""), - "Should set HTTP proxy host" - ); - assert!( - content.contains("network.proxy.http_port\", 8080"), - "Should set HTTP proxy port" - ); - assert!( - content.contains("network.proxy.ssl\", \"proxy.example.com\""), - "Should set SSL proxy host" - ); - assert!( - content.contains("network.proxy.ssl_port\", 8080"), - "Should set SSL proxy port" - ); - } - - #[test] - fn test_pac_url_encodes_spaces_in_path() { - let (manager, temp_dir) = create_test_profile_manager(); - - let uuid_dir = temp_dir.path().join("path with spaces"); - let profile_dir = uuid_dir.join("profile"); - fs::create_dir_all(&profile_dir).expect("Should create profile directory"); - - let result = manager.disable_proxy_settings_in_profile(&profile_dir); - assert!(result.is_ok(), "Should handle paths with spaces"); - - let user_js = fs::read_to_string(profile_dir.join("user.js")).unwrap(); - let pac_line = user_js - .lines() - .find(|l| l.contains("autoconfig_url")) - .expect("Should have autoconfig_url preference"); - - assert!( - !pac_line.contains("path with spaces"), - "PAC URL should not contain raw spaces: {pac_line}" - ); - assert!( - pac_line.contains("path%20with%20spaces"), - "PAC URL should percent-encode spaces: {pac_line}" - ); - } - #[test] fn test_normalize_launch_hook_accepts_http_and_https() { let http = @@ -2339,7 +1608,6 @@ pub async fn create_browser_profile_with_group( release_type: String, proxy_id: Option, vpn_id: Option, - camoufox_config: Option, wayfern_config: Option, group_id: Option, ephemeral: bool, @@ -2356,7 +1624,6 @@ pub async fn create_browser_profile_with_group( &release_type, proxy_id, vpn_id, - camoufox_config, wayfern_config, group_id, ephemeral, @@ -2527,17 +1794,13 @@ pub async fn create_browser_profile_new( release_type: String, proxy_id: Option, vpn_id: Option, - camoufox_config: Option, wayfern_config: Option, group_id: Option, ephemeral: Option, dns_blocklist: Option, launch_hook: Option, ) -> Result { - let fingerprint_os = camoufox_config - .as_ref() - .and_then(|c| c.os.as_deref()) - .or_else(|| wayfern_config.as_ref().and_then(|c| c.os.as_deref())); + let fingerprint_os = wayfern_config.as_ref().and_then(|c| c.os.as_deref()); if !crate::cloud_auth::CLOUD_AUTH .is_fingerprint_os_allowed(fingerprint_os) @@ -2560,7 +1823,6 @@ pub async fn create_browser_profile_new( release_type, proxy_id, vpn_id, - camoufox_config, wayfern_config, group_id, ephemeral.unwrap_or(false), @@ -2570,34 +1832,6 @@ pub async fn create_browser_profile_new( .await } -#[tauri::command] -pub async fn update_camoufox_config( - app_handle: tauri::AppHandle, - profile_id: String, - config: CamoufoxConfig, -) -> Result<(), String> { - if config.fingerprint.is_some() - && !crate::cloud_auth::CLOUD_AUTH - .can_use_cross_os_fingerprints() - .await - { - return Err(serde_json::json!({ "code": "FINGERPRINT_REQUIRES_PRO" }).to_string()); - } - - if !crate::cloud_auth::CLOUD_AUTH - .is_fingerprint_os_allowed(config.os.as_deref()) - .await - { - return Err("Fingerprint OS spoofing requires an active Pro subscription".to_string()); - } - - let profile_manager = ProfileManager::instance(); - profile_manager - .update_camoufox_config(app_handle, &profile_id, config) - .await - .map_err(|e| format!("Failed to update Camoufox config: {e}")) -} - #[tauri::command] pub async fn update_wayfern_config( app_handle: tauri::AppHandle, diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs index 81867dc..cdf278e 100644 --- a/src-tauri/src/profile/types.rs +++ b/src-tauri/src/profile/types.rs @@ -1,4 +1,3 @@ -use crate::camoufox_manager::CamoufoxConfig; use crate::wayfern_manager::WayfernConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -38,9 +37,7 @@ pub struct BrowserProfile { #[serde(default)] pub last_launch: Option, #[serde(default = "default_release_type")] - pub release_type: String, // "stable" or "nightly" - #[serde(default)] - pub camoufox_config: Option, // Camoufox configuration + pub release_type: String, #[serde(default)] pub wayfern_config: Option, // Wayfern configuration #[serde(default)] @@ -115,7 +112,6 @@ impl BrowserProfile { self .host_os .as_deref() - .or_else(|| self.camoufox_config.as_ref().and_then(|c| c.os.as_deref())) .or_else(|| self.wayfern_config.as_ref().and_then(|c| c.os.as_deref())) } diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 08123f1..e68795c 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -4,7 +4,6 @@ use std::collections::HashSet; use std::fs::{self, create_dir_all}; use std::path::Path; -use crate::camoufox_manager::CamoufoxConfig; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::profile::types::{get_host_os, BrowserProfile, SyncMode}; use crate::profile::ProfileManager; @@ -21,11 +20,9 @@ pub struct DetectedProfile { } fn map_browser_type(browser: &str) -> &str { - // Firefox-based sources map to the now-deprecated Camoufox. They are no longer - // detected for import; the mapping is kept only so the import command can - // recognize and REJECT them. Everything else maps to Wayfern. + // Legacy Firefox-family sources map to Wayfern at import time. match browser { - "firefox" | "firefox-developer" | "zen" | "camoufox" => "camoufox", + "firefox" | "firefox-developer" | "zen" | "camoufox" => "wayfern", _ => "wayfern", } } @@ -218,7 +215,7 @@ impl ProfileImporter { "chromium" => "Chrome/Chromium", "brave" => "Brave", "zen" => "Zen Browser", - "camoufox" => "Camoufox", + "wayfern" => "Wayfern", _ => "Unknown Browser", } @@ -232,7 +229,6 @@ impl ProfileImporter { browser_type: &str, new_profile_name: &str, proxy_id: Option, - _camoufox_config: Option, wayfern_config: Option, ) -> Result<(), Box> { let source_path = Path::new(source_path); @@ -268,10 +264,6 @@ impl ProfileImporter { let version = self.get_default_version_for_browser(mapped)?; - // Camoufox import is removed; only Wayfern profiles are imported now, so the - // imported profile never carries a Camoufox config. - let final_camoufox_config: Option = None; - let final_wayfern_config = if mapped == "wayfern" { let mut config = wayfern_config.unwrap_or_default(); @@ -312,7 +304,6 @@ impl ProfileImporter { process_id: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, wayfern_config: None, group_id: None, tags: Vec::new(), @@ -367,7 +358,6 @@ impl ProfileImporter { process_id: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: final_camoufox_config, wayfern_config: final_wayfern_config, group_id: None, tags: Vec::new(), @@ -465,19 +455,9 @@ pub async fn import_browser_profile( browser_type: String, new_profile_name: String, proxy_id: Option, - camoufox_config: Option, wayfern_config: Option, ) -> Result<(), String> { - // Camoufox is deprecated — Firefox-based profiles (which map to Camoufox) can - // no longer be imported. Reject them before doing any work. - if map_browser_type(&browser_type) == "camoufox" { - return Err(serde_json::json!({ "code": "CAMOUFOX_IMPORT_DEPRECATED" }).to_string()); - } - - let fingerprint_os = camoufox_config - .as_ref() - .and_then(|c| c.os.as_deref()) - .or_else(|| wayfern_config.as_ref().and_then(|c| c.os.as_deref())); + let fingerprint_os = wayfern_config.as_ref().and_then(|c| c.os.as_deref()); if !crate::cloud_auth::CLOUD_AUTH .is_fingerprint_os_allowed(fingerprint_os) @@ -494,7 +474,6 @@ pub async fn import_browser_profile( &browser_type, &new_profile_name, proxy_id, - camoufox_config, wayfern_config, ) .await @@ -546,12 +525,12 @@ mod tests { #[test] fn test_map_browser_type() { - assert_eq!(map_browser_type("firefox"), "camoufox"); - assert_eq!(map_browser_type("firefox-developer"), "camoufox"); - assert_eq!(map_browser_type("zen"), "camoufox"); + assert_eq!(map_browser_type("firefox"), "wayfern"); + assert_eq!(map_browser_type("firefox-developer"), "wayfern"); + assert_eq!(map_browser_type("zen"), "wayfern"); assert_eq!(map_browser_type("chromium"), "wayfern"); assert_eq!(map_browser_type("brave"), "wayfern"); - assert_eq!(map_browser_type("camoufox"), "camoufox"); + assert_eq!(map_browser_type("camoufox"), "wayfern"); assert_eq!(map_browser_type("wayfern"), "wayfern"); assert_eq!(map_browser_type("something_else"), "wayfern"); } diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 9e4c4ec..73953fb 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -1487,9 +1487,8 @@ impl ProxyManager { profile_id: Option<&str>, bypass_rules: Vec, blocklist_file: Option, - // Protocol the local worker serves the browser: "http" (Camoufox) or - // "socks5" (Wayfern). Reflected in the returned ProxySettings.proxy_type - // so the caller formats the right local proxy URL scheme. + // Protocol the local worker serves the browser: "socks5" (Wayfern). Reflected in + // the returned ProxySettings.proxy_type so the caller formats the right local proxy URL scheme. local_protocol: &str, ) -> Result { if let Some(name) = profile_id { diff --git a/src-tauri/src/proxy_storage.rs b/src-tauri/src/proxy_storage.rs index aa39844..4198ea1 100644 --- a/src-tauri/src/proxy_storage.rs +++ b/src-tauri/src/proxy_storage.rs @@ -16,8 +16,7 @@ pub struct ProxyConfig { pub bypass_rules: Vec, #[serde(default)] pub blocklist_file: Option, - /// Protocol the local worker serves to the browser: "http" (default, used - /// by Camoufox/Firefox) or "socks5" (used by Wayfern/Chromium so QUIC and + /// Protocol the local worker serves to the browser: "socks5" (Wayfern/Chromium so QUIC and /// WebRTC UDP can be proxied without leaking the real IP). Independent of /// `upstream_url`, which is the real upstream proxy/VPN this worker dials. #[serde(default)] diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs index e3aa3b7..92e91bf 100644 --- a/src-tauri/src/sync/engine.rs +++ b/src-tauri/src/sync/engine.rs @@ -73,7 +73,7 @@ const CRITICAL_FILE_PATTERNS: &[&str] = &[ "Secure Preferences", "Web Data", "Extension Cookies", - // Firefox/Camoufox equivalents + // Chromium profile equivalents "cookies.sqlite", "key4.db", "logins.json", diff --git a/src-tauri/src/synchronizer.rs b/src-tauri/src/synchronizer.rs index d89de58..cca309d 100644 --- a/src-tauri/src/synchronizer.rs +++ b/src-tauri/src/synchronizer.rs @@ -111,10 +111,7 @@ impl SynchronizerManager { .clone(); if leader.browser != "wayfern" { - return Err( - "Synchronizer only supports Wayfern profiles. Camoufox profiles cannot be used." - .to_string(), - ); + return Err("Synchronizer only supports Wayfern profiles.".to_string()); } // Check leader is not already running diff --git a/src-tauri/src/camoufox/data/territoryInfo.xml b/src-tauri/src/territory_info.xml similarity index 100% rename from src-tauri/src/camoufox/data/territoryInfo.xml rename to src-tauri/src/territory_info.xml diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index 61ae265..8945102 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -254,9 +254,9 @@ impl VersionUpdater { ) -> Result, Box> { let supported_browsers = self.browser_version_manager.get_supported_browsers(); - // Only fetch versions for active browsers (wayfern, camoufox) plus any + // Only fetch versions for active browsers (wayfern) plus any // deprecated browsers that still have existing profiles - let active_browsers = ["wayfern", "camoufox"]; + let active_browsers = ["wayfern"]; let browsers_with_profiles: std::collections::HashSet = crate::profile::ProfileManager::instance() .list_profiles() diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 487cc00..bf85374 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -154,7 +154,7 @@ impl WayfernManager { /// /// Keys are the camelCase fields Wayfern uses in its fingerprint /// (`windowOuterWidth`, `screenAvailWidth`, …) — NOT the dotted - /// Camoufox-style keys. Preference order, matching how the fingerprint + /// Preference order, matching how the fingerprint /// describes the window: /// 1. `windowOuterWidth` / `windowOuterHeight` — the real window size. /// 2. `screenAvailWidth` / `screenAvailHeight` — usable screen area. @@ -325,7 +325,7 @@ impl WayfernManager { .await .map_err(|e| format!("Failed to fetch public IP: {e}"))?, }; - crate::camoufox::geolocation::get_geolocation(&ip) + crate::geolocation::get_geolocation(&ip) .map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}")) } .await; diff --git a/src-tauri/tests/sync_e2e.rs b/src-tauri/tests/sync_e2e.rs index 4e1578c..5f2e023 100644 --- a/src-tauri/tests/sync_e2e.rs +++ b/src-tauri/tests/sync_e2e.rs @@ -246,7 +246,7 @@ fn create_test_profile_bundle_with_bypass_rules(temp_dir: &Path, bypass_rules: & let metadata = json!({ "id": "test-bypass-profile-id", "name": "Bypass Rules Profile", - "browser": "camoufox", + "browser": "wayfern", "version": "120.0.0", "release_type": "stable", "sync_enabled": true, @@ -802,7 +802,7 @@ async fn test_profile_bypass_rules_sync() { let metadata = extract_bundle(&downloaded, &extract_dir); assert_eq!(metadata["name"], "Bypass Rules Profile"); - assert_eq!(metadata["browser"], "camoufox"); + assert_eq!(metadata["browser"], "wayfern"); let synced_rules = metadata["proxy_bypass_rules"] .as_array() diff --git a/src/app/page.tsx b/src/app/page.tsx index 0d16515..74ebf58 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,8 +7,6 @@ import { useOnborda } from "onborda"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { AccountPage } from "@/components/account-page"; -import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; -import { CamoufoxDeprecationDialog } from "@/components/camoufox-deprecation-dialog"; import { CloneProfileDialog } from "@/components/clone-profile-dialog"; import { CloseConfirmDialog } from "@/components/close-confirm-dialog"; import { CommandPalette } from "@/components/command-palette"; @@ -43,6 +41,7 @@ import { SyncAllDialog } from "@/components/sync-all-dialog"; import { SyncConfigDialog } from "@/components/sync-config-dialog"; import { SyncFollowerDialog } from "@/components/sync-follower-dialog"; import { ThankYouDialog } from "@/components/thank-you-dialog"; +import { WayfernConfigDialog } from "@/components/wayfern-config-dialog"; import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog"; import { WelcomeDialog } from "@/components/welcome-dialog"; import { WindowResizeWarningDialog } from "@/components/window-resize-warning-dialog"; @@ -78,14 +77,9 @@ import { showSyncProgressToast, showToast, } from "@/lib/toast-utils"; -import type { - BrowserProfile, - CamoufoxConfig, - SyncSettings, - WayfernConfig, -} from "@/types"; +import type { BrowserProfile, SyncSettings, WayfernConfig } from "@/types"; -type BrowserTypeString = "camoufox" | "wayfern"; +type BrowserTypeString = "wayfern"; interface PendingUrl { id: string; @@ -268,8 +262,7 @@ export default function Home() { const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); const [proxyManagementDialogOpen, setProxyManagementDialogOpen] = useState(false); - const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] = - useState(false); + const [wayfernConfigDialogOpen, setWayfernConfigDialogOpen] = useState(false); const [groupManagementDialogOpen, setGroupManagementDialogOpen] = useState(false); const [extensionManagementDialogOpen, setExtensionManagementDialogOpen] = @@ -306,7 +299,7 @@ export default function Home() { const [selectedProfiles, setSelectedProfiles] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [pendingUrls, setPendingUrls] = useState([]); - const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] = + const [currentProfileForWayfernConfig, setCurrentProfileForWayfernConfig] = useState(null); const [cloneProfile, setCloneProfile] = useState(null); const [passwordDialogProfile, setPasswordDialogProfile] = @@ -315,8 +308,6 @@ export default function Home() { useState("set"); const pendingLaunchAfterUnlockRef = useRef(null); const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false); - const [windowResizeWarningBrowserType, setWindowResizeWarningBrowserType] = - useState(undefined); const windowResizeWarningResolver = useRef< ((proceed: boolean) => void) | null >(null); @@ -534,7 +525,7 @@ export default function Home() { console.log("Found missing binaries:", missingBinaries); } if (missingGeoIP) { - console.log("Found missing GeoIP database for Camoufox"); + console.log("Found missing GeoIP database"); } // Group missing binaries by browser type to avoid concurrent downloads @@ -556,9 +547,9 @@ export default function Home() { if (missingGeoIP) { if (missingList) { - missingList += ", GeoIP database for Camoufox"; + missingList += ", GeoIP database"; } else { - missingList = "GeoIP database for Camoufox"; + missingList = "GeoIP database"; } } @@ -776,33 +767,11 @@ export default function Home() { } }, [handleUrlOpen, t]); - const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => { - setCurrentProfileForCamoufoxConfig(profile); - setCamoufoxConfigDialogOpen(true); + const handleConfigureWayfern = useCallback((profile: BrowserProfile) => { + setCurrentProfileForWayfernConfig(profile); + setWayfernConfigDialogOpen(true); }, []); - const handleSaveCamoufoxConfig = useCallback( - async (profile: BrowserProfile, config: CamoufoxConfig) => { - try { - await invoke("update_camoufox_config", { - profileId: profile.id, - config, - }); - // No need to manually reload - useProfileEvents will handle the update - setCamoufoxConfigDialogOpen(false); - } catch (err: unknown) { - console.error("Failed to update camoufox config:", err); - showErrorToast( - t("errors.updateCamoufoxConfigFailed", { - error: JSON.stringify(err), - }), - ); - throw err; - } - }, - [t], - ); - const handleSaveWayfernConfig = useCallback( async (profile: BrowserProfile, config: WayfernConfig) => { try { @@ -811,7 +780,7 @@ export default function Home() { config, }); // No need to manually reload - useProfileEvents will handle the update - setCamoufoxConfigDialogOpen(false); + setWayfernConfigDialogOpen(false); } catch (err: unknown) { console.error("Failed to update wayfern config:", err); showErrorToast( @@ -831,7 +800,6 @@ export default function Home() { releaseType: string; proxyId?: string; vpnId?: string; - camoufoxConfig?: CamoufoxConfig; wayfernConfig?: WayfernConfig; groupId?: string; extensionGroupId?: string; @@ -850,7 +818,6 @@ export default function Home() { releaseType: profileData.releaseType, proxyId: profileData.proxyId, vpnId: profileData.vpnId, - camoufoxConfig: profileData.camoufoxConfig, wayfernConfig: profileData.wayfernConfig, groupId: profileData.groupId ?? @@ -926,7 +893,7 @@ export default function Home() { } // Show one-time warning about window resizing for fingerprinted browsers - if (profile.browser === "camoufox" || profile.browser === "wayfern") { + if (profile.browser === "wayfern") { try { const dismissed = await invoke( "get_window_resize_warning_dismissed", @@ -934,7 +901,6 @@ export default function Home() { if (!dismissed) { const proceed = await new Promise((resolve) => { windowResizeWarningResolver.current = resolve; - setWindowResizeWarningBrowserType(profile.browser); setWindowResizeWarningOpen(true); }); if (!proceed) { @@ -1138,9 +1104,7 @@ export default function Home() { const handleBulkCopyCookies = useCallback(() => { if (selectedProfiles.length === 0) return; const eligibleProfiles = profiles.filter( - (p) => - selectedProfiles.includes(p.id) && - (p.browser === "wayfern" || p.browser === "camoufox"), + (p) => selectedProfiles.includes(p.id) && p.browser === "wayfern", ); if (eligibleProfiles.length === 0) { showErrorToast(t("errors.cookieCopyUnsupportedBrowser")); @@ -1400,7 +1364,7 @@ export default function Home() { void checkMissingBinaries(); } - // Proactively download Wayfern and Camoufox if not already available + // Proactively download Wayfern if not already available if (!profilesLoading) { void invoke("ensure_active_browsers_downloaded").catch((err: unknown) => { console.error("Failed to auto-download browsers:", err); @@ -1523,40 +1487,6 @@ export default function Home() { }; }, [t]); - // Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026) - useEffect(() => { - if (profiles.length === 0) return; - - const unsupportedProfiles = profiles.filter( - (p) => p.browser !== "wayfern" && p.browser !== "camoufox", - ); - - if (unsupportedProfiles.length > 0) { - const unsupportedNames = unsupportedProfiles - .map((p) => p.name) - .join(", "); - - showToast({ - id: "browser-support-ending-warning", - type: "error", - title: t("browserSupport.endingSoonTitle"), - description: t("browserSupport.endingSoonDescription", { - profiles: unsupportedNames, - }), - duration: 15000, - action: { - label: t("common.buttons.learnMore"), - onClick: () => { - const event = new CustomEvent("url-open-request", { - detail: "https://github.com/zhom/donutbrowser/discussions", - }); - window.dispatchEvent(event); - }, - }, - }); - } - }, [profiles, t]); - // Re-check Wayfern terms when a browser download completes useEffect(() => { let unlisten: (() => void) | null = null; @@ -1639,7 +1569,6 @@ export default function Home() { return (
- - { - setCamoufoxConfigDialogOpen(false); + setWayfernConfigDialogOpen(false); }} - profile={currentProfileForCamoufoxConfig} - onSave={handleSaveCamoufoxConfig} - onSaveWayfern={handleSaveWayfernConfig} + profile={currentProfileForWayfernConfig} + onSave={handleSaveWayfernConfig} isRunning={ - currentProfileForCamoufoxConfig - ? runningProfiles.has(currentProfileForCamoufoxConfig.id) + currentProfileForWayfernConfig + ? runningProfiles.has(currentProfileForWayfernConfig.id) : false } crossOsUnlocked={crossOsUnlocked} @@ -2116,7 +2044,6 @@ export default function Home() { { setWindowResizeWarningOpen(false); windowResizeWarningResolver.current?.(proceed); diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx deleted file mode 100644 index 086aa8d..0000000 --- a/src/components/camoufox-config-dialog.tsx +++ /dev/null @@ -1,213 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { WayfernConfigForm } from "@/components/wayfern-config-form"; -import type { - BrowserProfile, - CamoufoxConfig, - CamoufoxOS, - WayfernConfig, -} from "@/types"; - -const getCurrentOS = (): CamoufoxOS => { - if (typeof navigator === "undefined") return "linux"; - const platform = navigator.platform.toLowerCase(); - if (platform.includes("win")) return "windows"; - if (platform.includes("mac")) return "macos"; - return "linux"; -}; - -import { LoadingButton } from "./loading-button"; -import { RippleButton } from "./ui/ripple"; - -interface CamoufoxConfigDialogProps { - isOpen: boolean; - onClose: () => void; - profile: BrowserProfile | null; - onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise; - onSaveWayfern?: ( - profile: BrowserProfile, - config: CamoufoxConfig, - ) => Promise; - isRunning?: boolean; - crossOsUnlocked?: boolean; -} - -export function CamoufoxConfigDialog({ - isOpen, - onClose, - profile, - onSave, - onSaveWayfern, - isRunning = false, - crossOsUnlocked = false, -}: CamoufoxConfigDialogProps) { - const { t } = useTranslation(); - // Use union type to support both Camoufox and Wayfern configs - const [config, setConfig] = useState(() => ({ - geoip: true, - os: getCurrentOS(), - })); - const [isSaving, setIsSaving] = useState(false); - - const isAntiDetectBrowser = - profile?.browser === "camoufox" || profile?.browser === "wayfern"; - - // Initialize config when profile changes - useEffect(() => { - if (profile && isAntiDetectBrowser) { - const profileConfig = - profile.browser === "wayfern" - ? profile.wayfern_config - : profile.camoufox_config; - setConfig( - profileConfig || { - geoip: true, - os: getCurrentOS(), - }, - ); - } - }, [profile, isAntiDetectBrowser]); - - const updateConfig = ( - key: keyof CamoufoxConfig | keyof WayfernConfig, - value: unknown, - ) => { - setConfig((prev) => ({ ...prev, [key]: value })); - }; - - const handleSave = async () => { - if (!profile) return; - - // Validate fingerprint JSON if it exists - if (config.fingerprint) { - try { - JSON.parse(config.fingerprint); - } catch (_error) { - const { toast } = await import("sonner"); - toast.error(t("camoufoxDialog.invalidFingerprint"), { - description: t("camoufoxDialog.invalidFingerprintDescription"), - }); - return; - } - } - - setIsSaving(true); - try { - if (profile.browser === "wayfern" && onSaveWayfern) { - await onSaveWayfern(profile, config as CamoufoxConfig); - } else { - await onSave(profile, config as CamoufoxConfig); - } - onClose(); - } catch (error) { - console.error("Failed to save config:", error); - const { toast } = await import("sonner"); - toast.error(t("camoufoxDialog.saveFailed"), { - description: - error instanceof Error - ? error.message - : t("camoufoxDialog.unknownError"), - }); - } finally { - setIsSaving(false); - } - }; - - const handleClose = () => { - // Reset config to original when closing without saving - if (profile && isAntiDetectBrowser) { - const profileConfig = - profile.browser === "wayfern" - ? profile.wayfern_config - : profile.camoufox_config; - setConfig( - profileConfig || { - geoip: true, - os: getCurrentOS(), - }, - ); - } - onClose(); - }; - - if (!profile || !isAntiDetectBrowser) { - return null; - } - - const browserName = profile.browser === "wayfern" ? "Wayfern" : "Camoufox"; - - return ( - - - - - {isRunning - ? t("camoufoxDialog.titleView", { - name: profile.name, - browser: browserName, - }) - : t("camoufoxDialog.titleConfigure", { - name: profile.name, - browser: browserName, - })} - - - - -
- {profile.browser === "wayfern" ? ( - - ) : ( - - )} -
-
- - - - {isRunning ? t("common.buttons.close") : t("common.buttons.cancel")} - - {!isRunning && ( - - {t("common.buttons.save")} - - )} - -
-
- ); -} diff --git a/src/components/camoufox-deprecation-dialog.tsx b/src/components/camoufox-deprecation-dialog.tsx deleted file mode 100644 index e901aff..0000000 --- a/src/components/camoufox-deprecation-dialog.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -import { openUrl } from "@tauri-apps/plugin-opener"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { LuTriangleAlert } from "react-icons/lu"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import type { BrowserProfile } from "@/types"; -import { RippleButton } from "./ui/ripple"; - -interface CamoufoxDeprecationDialogProps { - profiles: BrowserProfile[]; -} - -/** - * Warns users who still have Camoufox profiles that Camoufox support is ending. - * Shown once per app session (this component mounts for the app lifetime), only - * when at least one Camoufox profile exists. Not a toast — a blocking dialog so - * the deprecation can't be missed. - */ -export function CamoufoxDeprecationDialog({ - profiles, -}: CamoufoxDeprecationDialogProps) { - const { t } = useTranslation(); - const [isOpen, setIsOpen] = useState(false); - const [shown, setShown] = useState(false); - - useEffect(() => { - if (shown) return; - const hasCamoufox = profiles.some((p) => p.browser === "camoufox"); - if (hasCamoufox) { - setIsOpen(true); - setShown(true); - } - }, [profiles, shown]); - - return ( - - - - - - {t("camoufoxDeprecation.title")} - - - {t("camoufoxDeprecation.description")} - - - - { - void openUrl( - "https://github.com/zhom/donutbrowser/discussions/426", - ); - }} - > - {t("common.buttons.learnMore")} - - { - setIsOpen(false); - }} - > - {t("camoufoxDeprecation.acknowledge")} - - - - - ); -} diff --git a/src/components/cookie-copy-dialog.tsx b/src/components/cookie-copy-dialog.tsx index aa37139..6005d6f 100644 --- a/src/components/cookie-copy-dialog.tsx +++ b/src/components/cookie-copy-dialog.tsx @@ -84,9 +84,7 @@ export function CookieCopyDialog({ // dead-end state (source picked = target list empty = copy button disabled). const eligibleSourceProfiles = useMemo(() => { return profiles.filter( - (p) => - !selectedProfiles.includes(p.id) && - (p.browser === "wayfern" || p.browser === "camoufox"), + (p) => !selectedProfiles.includes(p.id) && p.browser === "wayfern", ); }, [profiles, selectedProfiles]); @@ -95,7 +93,7 @@ export function CookieCopyDialog({ (p) => selectedProfiles.includes(p.id) && p.id !== sourceProfileId && - (p.browser === "wayfern" || p.browser === "camoufox"), + p.browser === "wayfern", ); }, [profiles, selectedProfiles, sourceProfileId]); diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index afc19a4..d00800e 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -66,7 +66,7 @@ const getCurrentOS = (): WayfernOS => { import { RippleButton } from "./ui/ripple"; -type BrowserTypeString = "camoufox" | "wayfern"; +type BrowserTypeString = "wayfern"; interface CreateProfileDialogProps { isOpen: boolean; @@ -113,8 +113,8 @@ export function CreateProfileDialog({ const proxyListboxIdAntiDetect = useId(); const proxyListboxIdRegular = useId(); const [profileName, setProfileName] = useState(""); - // Camoufox is deprecated: only Wayfern profiles can be created, so the dialog - // opens straight into the Wayfern config step (no browser-selection screen). + // Only Wayfern profiles can be created, so the dialog opens straight into + // the Wayfern config step (no browser-selection screen). const [currentStep, setCurrentStep] = useState< "browser-selection" | "browser-config" >("browser-config"); @@ -139,8 +139,7 @@ export function CreateProfileDialog({ setCurrentStep("browser-config"); }; - // Reset the form fields without leaving the Wayfern config step — Camoufox is - // deprecated, so there is no browser-selection screen to go back to. + // Reset the form fields without leaving the Wayfern config step. const resetForm = () => { setSelectedBrowser("wayfern"); setProfileName(""); @@ -286,8 +285,7 @@ export function CreateProfileDialog({ useEffect(() => { if (isOpen) { void loadSupportedBrowsers(); - // Load downloaded Wayfern versions up front so the availability gate is - // accurate. Camoufox is deprecated and no longer creatable. + // Load downloaded Wayfern versions up front so the availability gate is accurate. void loadDownloadedVersions("wayfern"); // Load release types when a browser is selected if (selectedBrowser) { @@ -395,7 +393,7 @@ export function CreateProfileDialog({ : undefined; try { if (activeTab === "anti-detect") { - // Camoufox is deprecated — only Wayfern anti-detect profiles are created. + // Only Wayfern anti-detect profiles are created. const bestWayfernVersion = getCreatableVersion("wayfern"); if (!bestWayfernVersion) { console.error("No Wayfern version available"); @@ -430,7 +428,7 @@ export function CreateProfileDialog({ return; } - // Use the best available version (stable preferred, nightly as fallback) + // Use the latest available Wayfern version const bestVersion = getCreatableVersion(selectedBrowser); if (!bestVersion) { console.error("No version available"); @@ -465,8 +463,7 @@ export function CreateProfileDialog({ // Cancel any ongoing loading loadingBrowserRef.current = null; - // Reset all states. Stay on the Wayfern config step — Camoufox is - // deprecated, so the browser-selection screen is gone. + // Reset all states. Stay on the Wayfern config step. setProfileName(""); setCurrentStep("browser-config"); setActiveTab("anti-detect"); @@ -535,10 +532,7 @@ export function CreateProfileDialog({ {currentStep === "browser-selection" ? t("createProfile.title") : t("createProfile.configureTitle", { - browser: - selectedBrowser === "wayfern" - ? t("createProfile.chromiumLabel") - : t("createProfile.firefoxLabel"), + browser: t("createProfile.chromiumLabel"), })} @@ -591,9 +585,6 @@ export function CreateProfileDialog({
- {/* Camoufox is deprecated — no longer offered for new - profiles. Only Wayfern can be created. */} - {!getCreatableVersion("wayfern") && (

{t("createProfile.browsersDownloading")} @@ -616,7 +607,6 @@ export function CreateProfileDialog({

{regularBrowsers.map((browser) => { - if (browser.value === "camoufox") return null; // Skip camoufox as it's handled in anti-detect tab const IconComponent = getBrowserIcon(browser.value); return (
) : ( - // Regular Browser Configuration (should not happen in - // the anti-detect tab; Camoufox creation is removed). + // Regular Browser Configuration (should not happen in the anti-detect tab).
{selectedBrowser && (
diff --git a/src/components/extension-management-dialog.tsx b/src/components/extension-management-dialog.tsx index d588ea6..35cfa5d 100644 --- a/src/components/extension-management-dialog.tsx +++ b/src/components/extension-management-dialog.tsx @@ -13,7 +13,7 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { FaChrome, FaFirefox } from "react-icons/fa"; +import { FaChrome } from "react-icons/fa"; import { GoPlus } from "react-icons/go"; import { LuChevronDown, @@ -652,34 +652,19 @@ export function ExtensionManagementDialog({ const renderCompatIcons = useCallback( (compat: string[]) => { const hasChromium = compat.includes("chromium"); - const hasFirefox = compat.includes("firefox"); - if (!hasChromium && !hasFirefox) return null; + if (!hasChromium) return null; return (
- {hasChromium && ( - - - - - - - - {t("extensions.compatibility.chromium")} - - - )} - {hasFirefox && ( - - - - - - - - {t("extensions.compatibility.firefox")} - - - )} + + + + + + + + {t("extensions.compatibility.chromium")} + +
); }, diff --git a/src/components/import-profile-dialog.tsx b/src/components/import-profile-dialog.tsx index 401aa20..b87639f 100644 --- a/src/components/import-profile-dialog.tsx +++ b/src/components/import-profile-dialog.tsx @@ -39,9 +39,7 @@ import { cn } from "@/lib/utils"; import type { DetectedProfile, WayfernConfig } from "@/types"; import { RippleButton } from "./ui/ripple"; -const getMappedBrowser = (browser: string): "camoufox" | "wayfern" => { - if (["firefox", "firefox-developer", "zen"].includes(browser)) - return "camoufox"; +const getMappedBrowser = (_browser: string): "wayfern" => { return "wayfern"; }; @@ -90,8 +88,7 @@ export function ImportProfileDialog({ useBrowserSupport(); const { storedProxies } = useProxyEvents(); - // Firefox-based browsers map to the deprecated Camoufox and can no longer be - // imported (the backend rejects them); only offer Chromium-family sources. + // Only Chromium-family browsers can be imported as Wayfern profiles. const importableBrowsers = supportedBrowsers.filter( (browser) => getMappedBrowser(browser) === "wayfern", ); @@ -189,8 +186,6 @@ export function ImportProfileDialog({ browserType, newProfileName, proxyId: selectedProxyId ?? null, - // Camoufox import is deprecated/blocked; only Wayfern configs are sent. - camoufoxConfig: null, wayfernConfig: mappedBrowser === "wayfern" ? wayfernConfig : null, }); @@ -587,8 +582,6 @@ export function ImportProfileDialog({
- {/* Only Wayfern profiles are importable now (Camoufox/Firefox - import is deprecated and blocked). */} { diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 374be8c..2d3920d 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -209,7 +209,7 @@ interface TableMeta { // Overflow actions onAssignProfilesToGroup?: (profileIds: string[]) => void; - onConfigureCamoufox?: (profile: BrowserProfile) => void; + onConfigureWayfern?: (profile: BrowserProfile) => void; onCloneProfile?: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; onOpenCookieManagement?: (profile: BrowserProfile) => void; @@ -1127,7 +1127,7 @@ interface ProfilesDataTableProps { onCloneProfile: (profile: BrowserProfile) => void | Promise; onDeleteProfile: (profile: BrowserProfile) => void | Promise; onRenameProfile: (profileId: string, newName: string) => Promise; - onConfigureCamoufox: (profile: BrowserProfile) => void; + onConfigureWayfern: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; onOpenCookieManagement?: (profile: BrowserProfile) => void; runningProfiles: Set; @@ -1177,7 +1177,7 @@ export function ProfilesDataTable({ onCloneProfile, onDeleteProfile, onRenameProfile, - onConfigureCamoufox, + onConfigureWayfern, onCopyCookiesToProfile, onOpenCookieManagement, runningProfiles, @@ -1952,7 +1952,7 @@ export function ProfilesDataTable({ void onCloneProfile(profile); } : undefined, - onConfigureCamoufox, + onConfigureWayfern, onCopyCookiesToProfile, onOpenCookieManagement, @@ -2030,7 +2030,7 @@ export function ProfilesDataTable({ onLaunchProfile, onAssignProfilesToGroup, onCloneProfile, - onConfigureCamoufox, + onConfigureWayfern, onCopyCookiesToProfile, onOpenCookieManagement, syncStatuses, @@ -2086,10 +2086,7 @@ export function ProfilesDataTable({ // Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are if (isCrossOs && !meta.showCheckboxes && !isSelected) { - const resolvedOs = - profile.host_os || - profile.camoufox_config?.os || - profile.wayfern_config?.os; + const resolvedOs = profile.host_os || profile.wayfern_config?.os; const osName = resolvedOs ? getOSDisplayName(resolvedOs) : "another OS"; @@ -2128,10 +2125,7 @@ export function ProfilesDataTable({ // Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete) if (isCrossOs && (meta.showCheckboxes || isSelected)) { - const resolvedOs = - profile.host_os || - profile.camoufox_config?.os || - profile.wayfern_config?.os; + const resolvedOs = profile.host_os || profile.wayfern_config?.os; const osName = resolvedOs ? getOSDisplayName(resolvedOs) : "another OS"; @@ -3183,7 +3177,6 @@ export function ProfilesDataTable({ ? t("crossOs.viewOnly", { os: getOSDisplayName( row.original.host_os || - row.original.camoufox_config?.os || row.original.wayfern_config?.os || "", ), @@ -3273,7 +3266,7 @@ export function ProfilesDataTable({ }} onOpenProfileSyncDialog={onOpenProfileSyncDialog} onAssignProfilesToGroup={onAssignProfilesToGroup} - onConfigureCamoufox={onConfigureCamoufox} + onConfigureWayfern={onConfigureWayfern} onCopyCookiesToProfile={onCopyCookiesToProfile} onOpenCookieManagement={onOpenCookieManagement} onAssignExtensionGroup={onAssignExtensionGroup} diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx index df58615..14cad66 100644 --- a/src/components/profile-info-dialog.tsx +++ b/src/components/profile-info-dialog.tsx @@ -33,7 +33,7 @@ import { LuUsers, LuX, } from "react-icons/lu"; -import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; +import { SharedFingerprintConfigForm } from "@/components/shared-fingerprint-config-form"; import { Button } from "@/components/ui/button"; import { ColorPicker, @@ -71,7 +71,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { WayfernConfigForm } from "@/components/wayfern-config-form"; import { translateBackendError } from "@/lib/backend-errors"; import { getProfileIcon } from "@/lib/browser-utils"; import { formatRelativeTime } from "@/lib/flag-utils"; @@ -79,7 +78,6 @@ import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import { cn } from "@/lib/utils"; import type { BrowserProfile, - CamoufoxConfig, ProfileGroup, StoredProxy, VpnConfig, @@ -95,7 +93,7 @@ interface ProfileInfoDialogProps { onOpenTrafficDialog?: (profileId: string) => void; onOpenProfileSyncDialog?: (profile: BrowserProfile) => void; onAssignProfilesToGroup?: (profileIds: string[]) => void; - onConfigureCamoufox?: (profile: BrowserProfile) => void; + onConfigureWayfern?: (profile: BrowserProfile) => void; onCopyCookiesToProfile?: (profile: BrowserProfile) => void; onOpenCookieManagement?: (profile: BrowserProfile) => void; onAssignExtensionGroup?: (profileIds: string[]) => void; @@ -271,7 +269,7 @@ export function ProfileInfoDialog({ onOpenTrafficDialog, onOpenProfileSyncDialog, onAssignProfilesToGroup, - onConfigureCamoufox, + onConfigureWayfern, onCopyCookiesToProfile, onOpenCookieManagement, onAssignExtensionGroup, @@ -340,8 +338,7 @@ export function ProfileInfoDialog({ if (!profile) return null; const ProfileIcon = getProfileIcon(profile); - const isCamoufoxOrWayfern = - profile.browser === "camoufox" || profile.browser === "wayfern"; + const isWayfern = profile.browser === "wayfern"; const isDeleteDisabled = isRunning; const proxyName = profile.proxy_id @@ -435,13 +432,13 @@ export function ProfileInfoDialog({ icon: , label: t("profiles.actions.changeFingerprint"), onClick: () => { - handleAction(() => onConfigureCamoufox?.(profile)); + handleAction(() => onConfigureWayfern?.(profile)); }, // Viewing and editing fingerprints both require an active paid plan. disabled: isDisabled || !crossOsUnlocked, proBadge: !crossOsUnlocked, runningBadge: isRunning, - hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox, + hidden: !isWayfern || !onConfigureWayfern, }, { icon: , @@ -463,9 +460,7 @@ export function ProfileInfoDialog({ disabled: isDisabled, runningBadge: isRunning, hidden: - !isCamoufoxOrWayfern || - profile.ephemeral === true || - !onCopyCookiesToProfile, + !isWayfern || profile.ephemeral === true || !onCopyCookiesToProfile, }, { id: "cookiesManage", @@ -477,9 +472,7 @@ export function ProfileInfoDialog({ disabled: isDisabled, runningBadge: isRunning, hidden: - !isCamoufoxOrWayfern || - profile.ephemeral === true || - !onOpenCookieManagement, + !isWayfern || profile.ephemeral === true || !onOpenCookieManagement, }, { icon: , @@ -1772,9 +1765,8 @@ function CookiesSectionInline({ // Inline password set / change / remove form. Replaces three separate // nested modal dialogs with one in-page form that branches on the current // `password_protected` state of the profile. -// Inline fingerprint editor. Reuses SharedCamoufoxConfigForm (Camoufox/Firefox -// engine) and WayfernConfigForm (Chromium engine) so the same field set as -// the standalone dialog is available without opening a nested modal. +// Inline fingerprint editor. Reuses SharedFingerprintConfigForm so the same +// field set as the standalone dialog is available without opening a nested modal. function FingerprintSectionInline({ profile, isDisabled, @@ -1788,9 +1780,6 @@ function FingerprintSectionInline({ onSaved: () => void; t: (key: string, options?: Record) => string; }) { - const [camoufoxConfig, setCamoufoxConfig] = React.useState( - () => profile.camoufox_config ?? {}, - ); const [wayfernConfig, setWayfernConfig] = React.useState( () => profile.wayfern_config ?? {}, ); @@ -1798,19 +1787,15 @@ function FingerprintSectionInline({ const [error, setError] = React.useState(null); const [success, setSuccess] = React.useState(null); - // When the underlying profile changes (e.g. a different profile is opened - // in the dialog) reset the local form state to match. React.useEffect(() => { - setCamoufoxConfig(profile.camoufox_config ?? {}); setWayfernConfig(profile.wayfern_config ?? {}); setError(null); setSuccess(null); - }, [profile.camoufox_config, profile.wayfern_config]); + }, [profile.wayfern_config]); - const isCamoufox = profile.browser === "camoufox"; const isWayfern = profile.browser === "wayfern"; - if (!isCamoufox && !isWayfern) { + if (!isWayfern) { return (
@@ -1824,9 +1809,6 @@ function FingerprintSectionInline({ ); } - // Viewing and editing fingerprints both require an active paid plan - // (`crossOsUnlocked` is that paid flag here). Render a locked state instead of - // the editor so free users can neither see nor change the fingerprint. if (!crossOsUnlocked) { return (
@@ -1841,10 +1823,6 @@ function FingerprintSectionInline({ ); } - const onCamoufoxChange = (key: keyof CamoufoxConfig, value: unknown) => { - setCamoufoxConfig((prev) => ({ ...prev, [key]: value })); - setSuccess(null); - }; const onWayfernChange = (key: keyof WayfernConfig, value: unknown) => { setWayfernConfig((prev) => ({ ...prev, [key]: value })); setSuccess(null); @@ -1855,19 +1833,11 @@ function FingerprintSectionInline({ setError(null); setSuccess(null); try { - if (isCamoufox) { - await invoke("update_camoufox_config", { - profileId: profile.id, - config: camoufoxConfig, - }); - } else { - await invoke("update_wayfern_config", { - profileId: profile.id, - config: wayfernConfig, - }); - } + await invoke("update_wayfern_config", { + profileId: profile.id, + config: wayfernConfig, + }); setSuccess(t("common.buttons.saved")); - // Close the dialog once the fingerprint is saved. onSaved(); } catch (e) { setError(translateBackendError(t as never, e)); @@ -1876,12 +1846,8 @@ function FingerprintSectionInline({ } }; - const initial = isCamoufox - ? JSON.stringify(profile.camoufox_config ?? {}) - : JSON.stringify(profile.wayfern_config ?? {}); - const current = isCamoufox - ? JSON.stringify(camoufoxConfig) - : JSON.stringify(wayfernConfig); + const initial = JSON.stringify(profile.wayfern_config ?? {}); + const current = JSON.stringify(wayfernConfig); const dirty = current !== initial; return ( @@ -1894,30 +1860,16 @@ function FingerprintSectionInline({ {t("profileInfo.sectionDesc.fingerprint")}

- {isCamoufox && ( - - )} - {isWayfern && ( - - )} + {error &&

{error}

} {success && !error &&

{success}

} @@ -1939,7 +1891,6 @@ function FingerprintSectionInline({ variant="ghost" className="h-7 text-xs" onClick={() => { - setCamoufoxConfig(profile.camoufox_config ?? {}); setWayfernConfig(profile.wayfern_config ?? {}); setError(null); setSuccess(null); diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx deleted file mode 100644 index 669a3b1..0000000 --- a/src/components/release-type-selector.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import { useId, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu"; -import { LoadingButton } from "@/components/loading-button"; -import { Badge } from "@/components/ui/badge"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { cn } from "@/lib/utils"; -import type { BrowserReleaseTypes } from "@/types"; -import { RippleButton } from "./ui/ripple"; - -interface ReleaseTypeSelectorProps { - selectedReleaseType: "stable" | "nightly" | null; - onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void; - availableReleaseTypes: BrowserReleaseTypes; - isDownloading: boolean; - onDownload: () => void; - placeholder?: string; - showDownloadButton?: boolean; - downloadedVersions?: string[]; -} - -export function ReleaseTypeSelector({ - selectedReleaseType, - onReleaseTypeSelect, - availableReleaseTypes, - isDownloading, - onDownload, - placeholder, - showDownloadButton = true, - downloadedVersions = [], -}: ReleaseTypeSelectorProps) { - const { t } = useTranslation(); - const [popoverOpen, setPopoverOpen] = useState(false); - const listboxId = useId(); - const effectivePlaceholder = - placeholder ?? t("releaseTypeSelector.placeholder"); - - const releaseOptions = [ - ...(availableReleaseTypes.stable - ? [{ type: "stable" as const, version: availableReleaseTypes.stable }] - : []), - ...(availableReleaseTypes.nightly - ? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }] - : []), - ]; - - // Only show dropdown if there are multiple release types available - const showDropdown = releaseOptions.length > 1; - - // If only one release type is available, auto-select it - if (!showDropdown && releaseOptions.length === 1 && !selectedReleaseType) { - setTimeout(() => { - onReleaseTypeSelect(releaseOptions[0].type); - }, 0); - } - - const selectedDisplayText = selectedReleaseType - ? selectedReleaseType === "stable" - ? t("releaseTypeSelector.stable") - : t("releaseTypeSelector.nightly") - : effectivePlaceholder; - - const selectedVersion = - selectedReleaseType === "stable" - ? availableReleaseTypes.stable - : selectedReleaseType === "nightly" - ? availableReleaseTypes.nightly - : null; - - const isVersionDownloaded = - selectedVersion && downloadedVersions.includes(selectedVersion); - - return ( -
- {showDropdown ? ( - - - - {selectedDisplayText} - - - - - - - {t("releaseTypeSelector.noReleaseTypes")} - - - - {releaseOptions.map((option) => { - const isDownloaded = downloadedVersions.includes( - option.version, - ); - return ( - { - const selectedType = currentValue as - | "stable" - | "nightly"; - onReleaseTypeSelect( - selectedType === selectedReleaseType - ? null - : selectedType, - ); - setPopoverOpen(false); - }} - > - -
- {option.type} - {option.type === "nightly" && ( - - {t("releaseTypeSelector.nightly")} - - )} - - {option.version} - - {isDownloaded && ( - - {t("releaseTypeSelector.downloaded")} - - )} -
-
- ); - })} -
-
-
-
-
- ) : ( - // Show a simple display when only one release type is available - releaseOptions.length === 1 && ( -
- - {releaseOptions[0].type} - - - {releaseOptions[0].version} - - {downloadedVersions.includes(releaseOptions[0].version) && ( - - {t("releaseTypeSelector.downloaded")} - - )} -
- ) - )} - - {showDownloadButton && - selectedReleaseType && - selectedVersion && - !isVersionDownloaded && ( - { - onDownload(); - }} - variant="outline" - className="w-full" - > - - {isDownloading - ? t("releaseTypeSelector.downloading") - : t("releaseTypeSelector.downloadBrowser")} - - )} -
- ); -} diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx deleted file mode 100644 index a87055b..0000000 --- a/src/components/shared-camoufox-config-form.tsx +++ /dev/null @@ -1,1382 +0,0 @@ -"use client"; - -import { invoke } from "@tauri-apps/api/core"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { LoadingButton } from "@/components/loading-button"; -import MultipleSelector, { type Option } from "@/components/multiple-selector"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ProBadge } from "@/components/ui/pro-badge"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; -import type { - CamoufoxConfig, - CamoufoxFingerprintConfig, - CamoufoxOS, -} from "@/types"; - -interface SharedCamoufoxConfigFormProps { - config: CamoufoxConfig; - onConfigChange: (key: keyof CamoufoxConfig, value: unknown) => void; - className?: string; - isCreating?: boolean; // Flag to indicate if this is for creating a new profile - forceAdvanced?: boolean; // Force advanced mode (for editing) - readOnly?: boolean; // Flag to indicate if the form should be read-only - browserType?: "camoufox" | "wayfern"; // Browser type to customize form options - crossOsUnlocked?: boolean; // Allow selecting non-current OS (paid feature) - limitedMode?: boolean; // Blur and disable advanced fields while keeping basic options accessible - profileVersion?: string; - profileBrowser?: string; -} - -// Determine if fingerprint editing should be disabled -const isFingerprintEditingDisabled = (config: CamoufoxConfig): boolean => { - return config.randomize_fingerprint_on_launch === true; -}; - -// Detect the current operating system -const getCurrentOS = (): CamoufoxOS => { - if (typeof navigator === "undefined") return "linux"; - const platform = navigator.platform.toLowerCase(); - if (platform.includes("win")) return "windows"; - if (platform.includes("mac")) return "macos"; - return "linux"; -}; - -// OS display labels -const osLabels: Record = { - windows: "Windows", - macos: "macOS", - linux: "Linux", -}; - -// Component for editing nested objects like webGl:parameters -interface ObjectEditorProps { - value: Record | undefined; - onChange: (value: Record | undefined) => void; - title: string; - readOnly?: boolean; -} - -function ObjectEditor({ - value, - onChange, - title, - readOnly = false, -}: ObjectEditorProps) { - const { t } = useTranslation(); - const [jsonString, setJsonString] = useState(""); - - useEffect(() => { - setJsonString(JSON.stringify(value ?? {}, null, 2)); - }, [value]); - - const handleChange = (newValue: string) => { - if (readOnly) return; - setJsonString(newValue); - try { - if (newValue.trim() === "" || newValue.trim() === "{}") { - onChange(undefined); // Treat empty objects as undefined - return; - } - const parsed = JSON.parse(newValue); - if ( - typeof parsed === "object" && - parsed !== null && - Object.keys(parsed).length === 0 - ) { - onChange(undefined); - return; - } - onChange(parsed as Record); - } catch (err) { - console.warn("Invalid JSON:", err); - } - }; - - return ( -
- -