feat(account): add managed credits and cloud gateway

Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
This commit is contained in:
Rocky
2026-08-20 11:43:21 +08:00
parent 0f9280bd00
commit 498f407585
301 changed files with 19221 additions and 10891 deletions
+195
View File
@@ -0,0 +1,195 @@
/*!
* OSGKeyboard website interactions.
* The page remains useful without JavaScript; this file adds language, theme,
* media, and the decorative Three.js hero enhancement.
*/
(function () {
"use strict";
const root = document.documentElement;
const languageButton = document.getElementById("languageToggle");
const themeButton = document.getElementById("themeToggle");
const themeIcon = themeButton?.querySelector(".material-symbols-rounded");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
const supportedLanguages = new Set(["zh", "en"]);
let beams = null;
function preferredLanguage() {
const query = new URLSearchParams(window.location.search).get("lang");
if (supportedLanguages.has(query)) return query;
const saved = localStorage.getItem("osg-site-language");
if (supportedLanguages.has(saved)) return saved;
return navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
}
function applyLanguage(language, persist) {
const lang = supportedLanguages.has(language) ? language : "zh";
root.lang = lang === "zh" ? "zh-Hans" : "en";
root.dataset.lang = lang;
document.querySelectorAll("[data-zh][data-en]").forEach((element) => {
element.textContent = element.dataset[lang] || "";
});
document.querySelectorAll("[data-zh-html][data-en-html]").forEach((element) => {
element.innerHTML = element.dataset[`${lang}Html`] || "";
});
document.querySelectorAll("[data-zh-alt][data-en-alt]").forEach((element) => {
element.alt = element.dataset[`${lang}Alt`] || "";
});
document.querySelectorAll("[data-zh-src][data-en-src]").forEach((element) => {
element.src = element.dataset[`${lang}Src`] || "";
});
document.querySelectorAll("[data-zh-href][data-en-href]").forEach((element) => {
element.href = element.dataset[`${lang}Href`] || "";
});
document.querySelectorAll("[data-zh-label][data-en-label]").forEach((element) => {
element.setAttribute("aria-label", element.dataset[`${lang}Label`] || "");
});
document.querySelectorAll("[data-zh-content][data-en-content]").forEach((element) => {
element.setAttribute("content", element.dataset[`${lang}Content`] || "");
});
document.querySelectorAll("[data-shot]").forEach((image) => {
const theme = root.dataset.theme || "light";
image.src = `assets/screenshots/${lang}/${theme}/${image.dataset.shot}`;
});
document.querySelectorAll("[data-media-name]").forEach((media) => {
const name = media.dataset.mediaName;
const extension = media.dataset.mediaExtension || "mp4";
media.src = `assets/whats-new/${name}-${lang}.${extension}`;
media.load?.();
});
document.title = lang === "zh"
? "OSGKeyboard — 语音、打字与 AI 助手键盘"
: "OSGKeyboard — Voice, typing, and an AI assistant keyboard";
if (languageButton) {
languageButton.textContent = lang === "zh" ? "EN" : "中文";
languageButton.setAttribute(
"aria-label",
lang === "zh" ? "Switch to English" : "切换到中文"
);
}
if (persist) {
localStorage.setItem("osg-site-language", lang);
const url = new URL(window.location.href);
if (lang === "en") {
url.searchParams.set("lang", "en");
} else {
url.searchParams.delete("lang");
}
history.replaceState({}, "", url);
}
}
function storedTheme() {
const saved = localStorage.getItem("osg-site-theme");
return saved === "light" || saved === "dark" ? saved : null;
}
function applyTheme(theme, persist) {
const resolved = theme === "dark" ? "dark" : "light";
root.dataset.theme = resolved;
root.style.colorScheme = resolved;
if (themeIcon) {
themeIcon.textContent = resolved === "dark" ? "light_mode" : "dark_mode";
}
if (themeButton) {
const lang = root.dataset.lang || "zh";
const label = resolved === "dark"
? (lang === "zh" ? "切换到浅色模式" : "Switch to light mode")
: (lang === "zh" ? "切换到深色模式" : "Switch to dark mode");
themeButton.setAttribute("aria-label", label);
}
document.querySelectorAll("[data-shot]").forEach((image) => {
const lang = root.dataset.lang || "zh";
image.src = `assets/screenshots/${lang}/${resolved}/${image.dataset.shot}`;
});
beams?.setBackground(resolved === "dark" ? "#020805" : "#020805");
beams?.setLightColor(resolved === "dark" ? "#72F49D" : "#61E987");
if (persist) localStorage.setItem("osg-site-theme", resolved);
}
function initializeBeams() {
const canvas = document.getElementById("heroAuroraCanvas");
if (!canvas || !window.OSGBeamsHero || !window.THREE) return;
try {
beams = window.OSGBeamsHero.create(canvas, {
beamWidth: 4.5,
beamHeight: 6,
beamNumber: 26,
lightColor: "#72F49D",
speed: 4.5,
noiseIntensity: 2.65,
scale: 0.62,
rotation: 208,
background: "#020805",
reduceMotion
});
} catch (error) {
console.warn("OSGKeyboard hero enhancement unavailable.", error);
canvas.hidden = true;
}
}
function initializeVideos() {
document.querySelectorAll("video[autoplay]").forEach((video) => {
if (reduceMotion) {
video.autoplay = false;
video.pause();
return;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
video.play().catch(() => {});
} else {
video.pause();
}
});
}, { threshold: 0.15 });
observer.observe(video);
});
}
const initialLanguage = preferredLanguage();
const initialTheme = storedTheme() || (themeMedia.matches ? "dark" : "light");
applyLanguage(initialLanguage, false);
applyTheme(initialTheme, false);
initializeBeams();
initializeVideos();
languageButton?.addEventListener("click", () => {
applyLanguage(root.dataset.lang === "zh" ? "en" : "zh", true);
applyTheme(root.dataset.theme, false);
});
themeButton?.addEventListener("click", () => {
applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
});
themeMedia.addEventListener("change", (event) => {
if (!storedTheme()) applyTheme(event.matches ? "dark" : "light", false);
});
document.addEventListener("visibilitychange", () => {
beams?.setVisible(!document.hidden);
});
})();