aboutsummaryrefslogtreecommitdiff
path: root/client-web/source/preferences
diff options
context:
space:
mode:
Diffstat (limited to 'client-web/source/preferences')
-rw-r--r--client-web/source/preferences/decl.ts26
-rw-r--r--client-web/source/preferences/mod.ts87
-rw-r--r--client-web/source/preferences/ui.ts27
3 files changed, 140 insertions, 0 deletions
diff --git a/client-web/source/preferences/decl.ts b/client-web/source/preferences/decl.ts
new file mode 100644
index 0000000..5718a44
--- /dev/null
+++ b/client-web/source/preferences/decl.ts
@@ -0,0 +1,26 @@
+// there should be no deps to dom APIs in this file for the tablegen to work
+
+export function hex_id(len = 8): string {
+ if (len > 8) return hex_id() + hex_id(len - 8)
+ return Math.floor(Math.random() * 16 ** len).toString(16).padStart(len, "0")
+}
+
+// TODO this could be simpler
+const string = "", bool = false, number = 0; // example types for ts
+export const PREF_DECLS = {
+ username: { type: string, default: "guest-" + hex_id(), description: "Username" },
+ warn_redirect: { type: bool, default: false, description: "Interal option that is set by a server redirect." },
+
+ /* MEDIA */
+ microphone_enabled: { type: bool, default: false, description: "Add one microphone track on startup" },
+ screencast_enabled: { type: bool, default: false, description: "Add one screencast track on startup" },
+ camera_enabled: { type: bool, default: false, description: "Add one camera track on startup" },
+ rnnoise: { type: bool, default: true, description: "Use RNNoise for noise suppression" },
+ native_noise_suppression: { type: bool, default: false, description: "Suggest the browser to do noise suppression" },
+ microphone_gain: { type: number, default: 1, description: "Amplify microphone volume" },
+ video_fps: { type: number, description: "Preferred framerate (in 1/s) for screencast and camera" },
+ video_resolution: { type: number, description: "Preferred width for screencast and camera" },
+ camera_facing_mode: { type: string, possible_values: ["environment", "user"], description: "Prefer user-facing or env-facing camera" },
+ auto_gain_control: { type: bool, description: "Automatically adjust mic gain" },
+ echo_cancellation: { type: bool, description: "Cancel echo" },
+}
diff --git a/client-web/source/preferences/mod.ts b/client-web/source/preferences/mod.ts
new file mode 100644
index 0000000..5b33924
--- /dev/null
+++ b/client-web/source/preferences/mod.ts
@@ -0,0 +1,87 @@
+import { PREF_DECLS } from "./decl.ts";
+
+
+export interface PrefDecl<T> {
+ default?: T,
+ type: T,
+ description?: string,
+ possible_values?: T[]
+ optional?: boolean,
+}
+
+type Type = "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
+type TypeMapper = { "string": string, "number": number, "boolean": boolean }
+
+type PrefMap<T extends { [key: string]: { type: unknown } }> = { [Key in keyof T]: T[Key]["type"] }
+type Optional<T extends { [key: string]: unknown }> = { [Key in keyof T]?: T[Key] }
+export const { prefs: PREFS, explicit: PREFS_EXPLICIT } = register_prefs(PREF_DECLS)
+export const on_pref_change_handlers: ((key: keyof typeof PREFS) => void)[] = []
+
+export function register_prefs<T extends Record<string, PrefDecl<unknown>>>(ds: T): { prefs: PrefMap<T>, explicit: Optional<PrefMap<T>> } {
+ const prefs: PrefMap<T> = {} as PrefMap<T>
+ const explicit: Optional<PrefMap<T>> = {}
+ for (const key in ds) {
+ const d = ds[key];
+ const type = typeof d.type;
+ let value = get_param(type, key)
+ if (value !== undefined) explicit[key] = value
+ value ??= d.default;
+ if (d.possible_values) if (!d.possible_values.includes(value)) value = d.default
+ prefs[key] = value
+ }
+ return { prefs, explicit }
+}
+
+export function change_pref<T extends keyof typeof PREFS>(key: T, value: typeof PREFS[T]) {
+ PREFS[key] = value
+ if ((PREF_DECLS as Record<string, PrefDecl<unknown>>)[key].default != value)
+ PREFS_EXPLICIT[key] = value
+ else delete PREFS_EXPLICIT[key]
+ window.location.hash = "#" + generate_section()
+}
+export function generate_section(): string {
+ const section = []
+ for (const key in PREFS_EXPLICIT) {
+ section.push(encodeURIComponent(key) + "=" + encodeURIComponent(param_to_string(
+ PREFS_EXPLICIT[key as unknown as keyof typeof PREFS_EXPLICIT]
+ )))
+ }
+ return load_params().rname + "?" + section.join("&")
+}
+
+export function load_params(): { raw_params: { [key: string]: string }, rname: string } {
+ const raw_params: Record<string, string> = {}
+ const [rname, param_str] = window.location.hash.substring(1).split("?")
+ if (!param_str) return { rname, raw_params: {} }
+ for (const kv of param_str.split("&")) {
+ const [key, value] = kv.split("=")
+ if (key == "prototype") continue
+ raw_params[decodeURIComponent(key)] = decodeURIComponent(value)
+ }
+ return { raw_params, rname }
+}
+
+function param_to_string<T>(p: T): string {
+ if (typeof p == "string") return p
+ else if (typeof p == "boolean") return JSON.stringify(p)
+ else if (typeof p == "number") return JSON.stringify(p)
+ throw new Error("impossible");
+}
+
+function get_param<T>(ty: string, key: string): T | undefined {
+ const v = load_params().raw_params[key]
+ if (v == undefined) return undefined
+ if (ty == "string") return v as unknown as T
+ else if (ty == "number") {
+ const n = parseInt(v)
+ if (!Number.isNaN(n)) return n as unknown as T
+ console.warn("invalid number parameter");
+ } else if (ty == "boolean") {
+ if (v == "0" || v == "false" || v == "no") return false as unknown as T
+ if (v == "1" || v == "true" || v == "yes") return true as unknown as T
+ console.warn("invalid boolean parameter");
+ } else {
+ throw new Error("invalid param type");
+ }
+ return undefined
+}
diff --git a/client-web/source/preferences/ui.ts b/client-web/source/preferences/ui.ts
new file mode 100644
index 0000000..1aaaca0
--- /dev/null
+++ b/client-web/source/preferences/ui.ts
@@ -0,0 +1,27 @@
+import { ediv, elabel, espan, OverlayUi } from "../helper.ts";
+import { PREF_DECLS } from "./decl.ts";
+import { change_pref, PrefDecl, PREFS } from "./mod.ts";
+
+export class PrefUi extends OverlayUi {
+ constructor() {
+ const elements = Object.entries(PREF_DECLS as Record<string, PrefDecl<unknown>>).map(([key_, decl]) => {
+ const key = key_ as keyof typeof PREF_DECLS
+
+ if (typeof decl.type == "boolean") {
+ const id = `pref-check-${key}`
+ const checkbox = document.createElement("input")
+ checkbox.type = "checkbox"
+ checkbox.id = id
+ checkbox.checked = PREFS[key] as boolean
+ checkbox.onchange = () => {
+ change_pref(key, checkbox.checked)
+ }
+ const label = elabel(decl.description ?? `[${key}]`, { id })
+ return ediv({ class: "pref" }, checkbox, label)
+ }
+ return espan(`(not implemented)`)
+ })
+ super(ediv({ class: "prefs-overlay" }, ...elements))
+ }
+
+}