diff options
author | metamuffin <metamuffin@disroot.org> | 2022-09-10 00:56:25 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2022-09-10 00:56:25 +0200 |
commit | 429dc2d5375abf8ca9c3861bdc4bdff52a31b0e4 (patch) | |
tree | 2fa8201ebdf45237a9ec90429bd18b5d6cdd9944 /client-web/source | |
parent | 95041256f86745832df42423e889d50d2cff35e7 (diff) | |
download | keks-meet-429dc2d5375abf8ca9c3861bdc4bdff52a31b0e4.tar keks-meet-429dc2d5375abf8ca9c3861bdc4bdff52a31b0e4.tar.bz2 keks-meet-429dc2d5375abf8ca9c3861bdc4bdff52a31b0e4.tar.zst |
overlay rework + settings
Diffstat (limited to 'client-web/source')
-rw-r--r-- | client-web/source/chat.ts | 28 | ||||
-rw-r--r-- | client-web/source/helper.ts | 17 | ||||
-rw-r--r-- | client-web/source/index.ts | 20 | ||||
-rw-r--r-- | client-web/source/logger.ts | 5 | ||||
-rw-r--r-- | client-web/source/menu.ts | 74 | ||||
-rw-r--r-- | client-web/source/preferences.ts | 83 | ||||
-rw-r--r-- | client-web/source/preferences/decl.ts | 26 | ||||
-rw-r--r-- | client-web/source/preferences/mod.ts | 87 | ||||
-rw-r--r-- | client-web/source/preferences/ui.ts | 27 | ||||
-rw-r--r-- | client-web/source/room.ts | 3 | ||||
-rw-r--r-- | client-web/source/user/local.ts | 10 |
11 files changed, 233 insertions, 147 deletions
diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts index d1165ee..dfef73f 100644 --- a/client-web/source/chat.ts +++ b/client-web/source/chat.ts @@ -1,20 +1,10 @@ -import { ediv, espan } from "./helper.ts"; -import { CHAT } from "./index.ts"; +import { ediv, espan, OverlayUi } from "./helper.ts"; import { Room } from "./room.ts"; import { User } from "./user/mod.ts"; -export class Chat { - private _shown = false; - - messages = ediv({ class: "messages" }) - controls = ediv({ class: "controls" }) - - get shown() { return this._shown } - set shown(value: boolean) { - if (value && !this._shown) document.body.prepend(CHAT) - if (!value && this._shown) document.body.removeChild(CHAT) - this._shown = value - } +export class Chat extends OverlayUi { + messages: HTMLElement + controls: HTMLElement constructor(public room: Room) { const send = document.createElement("input") @@ -26,9 +16,13 @@ export class Chat { send.value = "" } } - this.controls.append(send) - this.messages.append(document.createElement("hr")) - CHAT.append(this.messages, this.controls) + const messages = ediv({ class: "messages" }) + const controls = ediv({ class: "controls" }) + controls.append(send) + messages.append(document.createElement("hr")) + super(ediv({ class: "chat" }, messages, controls)) + this.messages = messages + this.controls = controls } send_message(sender: User, message: string) { diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index b4eb20e..1d75b60 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -1,6 +1,7 @@ /// <reference lib="dom" /> + const elem = (s: string) => document.createElement(s) interface Opts { class?: string[] | string, id?: string } @@ -37,3 +38,19 @@ export const ediv = elem_with_children("div") export const espan = elem_with_content("span") export const elabel = elem_with_content("label") +export const OVERLAYS = ediv({ class: "overlays" }) + + +export class OverlayUi { + _shown = false + constructor(public el: HTMLElement, initial = false) { + this.shown = initial + } + get shown() { return this._shown } + set shown(v: boolean) { + if (v && !this._shown) OVERLAYS.append(this.el) + if (!v && this._shown) OVERLAYS.removeChild(this.el) + this._shown = v + } +} + diff --git a/client-web/source/index.ts b/client-web/source/index.ts index 2b7c2a4..f3c5adc 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -1,18 +1,14 @@ /// <reference lib="dom" /> -import { ediv } from "./helper.ts"; -import { log } from "./logger.ts" -import { setup_menus } from "./menu.ts"; -import { load_params, PREFS } from "./preferences.ts"; +import { ediv, OVERLAYS } from "./helper.ts"; +import { log, LOGGER_CONTAINER } from "./logger.ts" +import { BottomMenu, MenuBr } from "./menu.ts"; +import { load_params, PREFS } from "./preferences/mod.ts"; import { SignalingConnection } from "./protocol/mod.ts"; import { Room } from "./room.ts" export const VERSION = "0.1.8" -export const BOTTOM_CONTAINER = ediv({ class: "bottom-container" }) export const ROOM_CONTAINER = ediv({ class: "room" }) -export const MENU_BR = ediv({ class: "menu-br" }) -export const CHAT = ediv({ class: "chat" }) -export const LOGGER_CONTAINER = ediv({ class: "logger-container" }) export const RTC_CONFIG: RTCConfiguration = { // google stun!? @@ -41,6 +37,10 @@ export async function main() { const conn = await (new SignalingConnection().connect(room_name)) const r = new Room(conn) - setup_menus(r) - document.body.append(ROOM_CONTAINER, BOTTOM_CONTAINER, MENU_BR, LOGGER_CONTAINER) + + r.on_ready = () => { + new BottomMenu(r).shown = true + new MenuBr().shown = true + } + document.body.append(ROOM_CONTAINER, OVERLAYS, LOGGER_CONTAINER) } diff --git a/client-web/source/logger.ts b/client-web/source/logger.ts index c44ea6b..8f1b471 100644 --- a/client-web/source/logger.ts +++ b/client-web/source/logger.ts @@ -1,6 +1,8 @@ /// <reference lib="dom" /> -import { LOGGER_CONTAINER } from "./index.ts"; +import { ediv } from "./helper.ts"; + +export const LOGGER_CONTAINER = ediv({ class: "logger-container" }) const log_scope_color = { "*": "#ff4a7c", @@ -15,7 +17,6 @@ const log_scope_color = { export type LogScope = keyof typeof log_scope_color export interface LogDesc { scope: LogScope, error?: boolean, warn?: boolean } - export function log(k: LogScope | LogDesc, message: string, ...data: unknown[]) { for (let i = 0; i < data.length; i++) { const e = data[i]; diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts index 5abb8f0..636fb53 100644 --- a/client-web/source/menu.ts +++ b/client-web/source/menu.ts @@ -1,39 +1,55 @@ /// <reference lib="dom" /> -import { ep } from "./helper.ts" -import { BOTTOM_CONTAINER, MENU_BR, VERSION } from "./index.ts" +import { ediv, ep, OverlayUi } from "./helper.ts" +import { VERSION } from "./index.ts" +import { PrefUi } from "./preferences/ui.ts" import { Room } from "./room.ts" -export function setup_menus(room: Room) { - const item = (name: string, cb: (() => void) | string) => { - const p = document.createElement("p") - const a = document.createElement("a") - a.classList.add("menu-item") - a.target = "_blank" // dont unload this meeting - a.textContent = name - if (typeof cb == "string") a.href = cb - else a.addEventListener("click", cb), a.href = "#" - p.append(a) - return p +export class MenuBr extends OverlayUi { + constructor() { + const item = (name: string, cb: (() => void) | string) => { + const p = document.createElement("p") + const a = document.createElement("a") + a.classList.add("menu-item") + a.target = "_blank" // dont unload this meeting + a.textContent = name + if (typeof cb == "string") a.href = cb + else a.addEventListener("click", cb), a.href = "#" + p.append(a) + return p + } + + super(ediv({ class: "menu-br" }, + ep(`keks-meet ${VERSION}`, { class: "version" }), + item("Licence", "/licence"), + item("Sources / Documentation", "https://codeberg.org/metamuffin/keks-meet"), + ), true) } +} + +export class BottomMenu extends OverlayUi { + constructor(room: Room) { + // TODO this should ideally be a checkbox + const chat_toggle = document.createElement("input") + chat_toggle.type = "button" + chat_toggle.value = "Toggle chat" + chat_toggle.onclick = () => { + room.chat.shown = !room.chat.shown + if (room.chat.shown) chat_toggle.classList.add("active") + else chat_toggle.classList.remove("active") + } - MENU_BR.append( - ep(`keks-meet ${VERSION}`, { class: "version" }), - item("Settings", () => alert("todo, refer to the url parameters in the docs for now")), - item("Licence", "/licence"), - item("Sources / Documentation", "https://codeberg.org/metamuffin/keks-meet"), - ) + const prefs_button = document.createElement("input") + prefs_button.type = "button" + prefs_button.value = "Settings" + const prefs = new PrefUi() + prefs_button.onclick = () => { + prefs.shown = !prefs.shown + if (prefs.shown) prefs_button.classList.add("active") + else prefs_button.classList.remove("active") + } - // TODO this should ideally be a checkbox - const chat_toggle = document.createElement("input") - chat_toggle.type = "button" - chat_toggle.id = "chat_toggle" - chat_toggle.value = "Toggle chat" - chat_toggle.onclick = () => { - room.chat.shown = !room.chat.shown - if (room.chat.shown) chat_toggle.classList.add("active") - else chat_toggle.classList.remove("active") + super(ediv({ class: "bottom-menu" }, chat_toggle, prefs_button, room.local_user.create_controls())) } - BOTTOM_CONTAINER.append(chat_toggle) } diff --git a/client-web/source/preferences.ts b/client-web/source/preferences.ts deleted file mode 100644 index d6b6656..0000000 --- a/client-web/source/preferences.ts +++ /dev/null @@ -1,83 +0,0 @@ -// 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" }, -} - -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"] } -export function register_prefs<T extends Record<string, PrefDecl<unknown>>>(ds: T): PrefMap<T> { - const p: PrefMap<T> = {} as PrefMap<T> - for (const key in ds) { - const d = ds[key]; - const type = typeof d.type; - let value = get_param(type, key) ?? d.default; - if (d.possible_values) if (!d.possible_values.includes(value)) value = d.default - p[key] = value - } - return p -} - -const raw_params = globalThis.Deno ? {} : load_params().p; -export function load_params(): { p: { [key: string]: string }, rname: string } { - const q: Record<string, string> = {} - const [rname, params] = window.location.hash.substring(1).split("?") - if (!params) return { rname, p: {} } - for (const kv of params.split("&")) { - const [key, value] = kv.split("=") - if (key == "prototype") continue - q[decodeURIComponent(key)] = decodeURIComponent(value) - } - return { p: q, rname } -} - -export function get_param<T>(ty: string, key: string): T | undefined { - const v = 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 -} - -export const PREFS = register_prefs(PREF_DECLS) 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)) + } + +} diff --git a/client-web/source/room.ts b/client-web/source/room.ts index 11a5000..207b40d 100644 --- a/client-web/source/room.ts +++ b/client-web/source/room.ts @@ -15,6 +15,8 @@ export class Room { public my_id!: number public chat: Chat = new Chat(this) + public on_ready = () => { }; + constructor(public signaling: SignalingConnection) { this.signaling.control_handler = (a) => this.control_handler(a) this.signaling.relay_handler = (a, b) => this.relay_handler(a, b) @@ -33,6 +35,7 @@ export class Room { log("*", `${p.id} joined`); if (p.id == this.my_id) { this.local_user = new LocalUser(this, p.id); + this.on_ready() } else { const ru = new RemoteUser(this, p.id) this.local_user.add_initial_to_remote(ru) diff --git a/client-web/source/user/local.ts b/client-web/source/user/local.ts index ac52692..72d18a4 100644 --- a/client-web/source/user/local.ts +++ b/client-web/source/user/local.ts @@ -1,13 +1,14 @@ /// <reference lib="dom" /> import { log } from "../logger.ts"; -import { PREFS } from "../preferences.ts"; +import { PREFS } from "../preferences/mod.ts"; import { RemoteUser } from "./remote.ts"; import { get_rnnoise_node } from "../rnnoise.ts"; import { Room } from "../room.ts"; import { TrackHandle } from "../track_handle.ts"; import { User } from "./mod.ts"; -import { BOTTOM_CONTAINER, ROOM_CONTAINER } from "../index.ts"; +import { ROOM_CONTAINER } from "../index.ts"; +import { ediv } from "../helper.ts"; export class LocalUser extends User { mic_gain?: GainNode @@ -80,10 +81,7 @@ export class LocalUser extends User { camera_toggle.addEventListener("click", () => create(camera_toggle, this.create_camera_track())) screen_toggle.addEventListener("click", () => create(screen_toggle, this.create_screencast_track())) - const el = document.createElement("div") - el.classList.add("local-controls") - el.append(mic_toggle, camera_toggle, screen_toggle) - BOTTOM_CONTAINER.append(el) + return ediv({ class: "local-controls" }, mic_toggle, camera_toggle, screen_toggle) } async create_camera_track() { |