aboutsummaryrefslogtreecommitdiff
path: root/client-web/source
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2022-09-10 00:56:25 +0200
committermetamuffin <metamuffin@disroot.org>2022-09-10 00:56:25 +0200
commit429dc2d5375abf8ca9c3861bdc4bdff52a31b0e4 (patch)
tree2fa8201ebdf45237a9ec90429bd18b5d6cdd9944 /client-web/source
parent95041256f86745832df42423e889d50d2cff35e7 (diff)
downloadkeks-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.ts28
-rw-r--r--client-web/source/helper.ts17
-rw-r--r--client-web/source/index.ts20
-rw-r--r--client-web/source/logger.ts5
-rw-r--r--client-web/source/menu.ts74
-rw-r--r--client-web/source/preferences.ts83
-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
-rw-r--r--client-web/source/room.ts3
-rw-r--r--client-web/source/user/local.ts10
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() {