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 | |
| 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')
| -rw-r--r-- | client-web/public/assets/style/master.css | 3 | ||||
| -rw-r--r-- | client-web/public/assets/style/prefs.css | 16 | ||||
| -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 | 
13 files changed, 251 insertions, 148 deletions
| diff --git a/client-web/public/assets/style/master.css b/client-web/public/assets/style/master.css index d67be15..8a437c0 100644 --- a/client-web/public/assets/style/master.css +++ b/client-web/public/assets/style/master.css @@ -2,6 +2,7 @@  @import url("./logger.css");  @import url("./chat.css");  @import url("./room.css"); +@import url("./prefs.css");  * {      font-family: "Ubuntu", sans-serif; @@ -70,7 +71,7 @@ input[type="text"] {      border: 1px solid var(--ac-light);  } -.bottom-container { +.bottom-menu {      background-color: var(--bg);      padding: 0.5em;      position: fixed; diff --git a/client-web/public/assets/style/prefs.css b/client-web/public/assets/style/prefs.css new file mode 100644 index 0000000..63ec299 --- /dev/null +++ b/client-web/public/assets/style/prefs.css @@ -0,0 +1,16 @@ +.prefs-overlay { +    position: fixed; +    z-index: 80; + +    left: 50%; +    bottom: 5em; +    transform: translateX(-50%); + +    width: min(50em, 80vw); +    height: min(40em, 60vh); + +    background-color: var(--bg); +    border-radius: 1em; +    padding: 2em; +    overflow-y: auto; +} 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() { | 
