From b351f2fe13e4991bbbb8cafae30326087e181b11 Mon Sep 17 00:00:00 2001 From: Sam Sartor Date: Sat, 9 Nov 2024 00:55:20 -0700 Subject: [PATCH] massive css rework --- Dioxus.toml | 13 -- public/mic-off-svgrepo-com.svg | 16 ++ public/mic-svgrepo-com.svg | 14 ++ public/speaker-medium-svgrepo-com.svg | 6 + public/speaker-muted-svgrepo-com.svg | 6 + src/app.rs | 250 ++++++++++++++++++++------ src/imp/web.rs | 3 +- 7 files changed, 240 insertions(+), 68 deletions(-) create mode 100644 public/mic-off-svgrepo-com.svg create mode 100644 public/mic-svgrepo-com.svg create mode 100644 public/speaker-medium-svgrepo-com.svg create mode 100644 public/speaker-muted-svgrepo-com.svg diff --git a/Dioxus.toml b/Dioxus.toml index 41c034c..f94be0f 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -1,46 +1,33 @@ [application] - # App (Project) Name name = "Mumble Web 2" - # Dioxus App Default Platform -# desktop, web, mobile, ssr default_platform = "web" - # `build` & `serve` dist path out_dir = "dist" - # resource (public) file folder asset_dir = "public" [web.app] - # HTML title tag content title = "mumble-web" [web.watcher] - # when watcher trigger, regenerate the `index.html` reload_html = true - # which files or dirs will be watcher monitoring watch_path = ["src", "public"] # include `assets` in web platform [web.resource] - # CSS style file style = [] - # Javascript code file script = [] [web.resource.dev] - # serve: [dev-server] only - # CSS style file style = [] - # Javascript code file script = [] diff --git a/public/mic-off-svgrepo-com.svg b/public/mic-off-svgrepo-com.svg new file mode 100644 index 0000000..a2f3ca8 --- /dev/null +++ b/public/mic-off-svgrepo-com.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/mic-svgrepo-com.svg b/public/mic-svgrepo-com.svg new file mode 100644 index 0000000..15745d3 --- /dev/null +++ b/public/mic-svgrepo-com.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/public/speaker-medium-svgrepo-com.svg b/public/speaker-medium-svgrepo-com.svg new file mode 100644 index 0000000..f21ae9c --- /dev/null +++ b/public/speaker-medium-svgrepo-com.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/speaker-muted-svgrepo-com.svg b/public/speaker-muted-svgrepo-com.svg new file mode 100644 index 0000000..e958d64 --- /dev/null +++ b/public/speaker-muted-svgrepo-com.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app.rs b/src/app.rs index 4b7f43b..8f79ca6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,7 @@ #![allow(non_snake_case)] use dioxus::prelude::*; +use manganis::mg; use ordermap::OrderSet; use sir::{css, global_css}; use std::collections::HashMap; @@ -61,6 +62,16 @@ pub struct UserState { pub self_mute: bool, } +impl UserState { + pub fn icon(&self) -> UserIcon { + match (self.mute || self.self_mute, self.deaf || self.self_deaf) { + (false, false) => UserIcon::Normal, + (true, false) => UserIcon::Muted, + (_, true) => UserIcon::Deafened, + } + } +} + pub struct Chat { pub raw: String, pub dangerous_html: String, @@ -91,21 +102,57 @@ pub static STATE: State = State { server: Signal::global(|| Default::default()), }; +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum UserIcon { + Normal, + Muted, + Deafened, + None, +} + +impl UserIcon { + pub fn url(self) -> Option<&'static str> { + // speaker from https://www.svgrepo.com/collection/ikono-bold-line-icons/ + // mic from https://www.svgrepo.com/collection/hashicorp-line-interface-icons/ + + use UserIcon::*; + Some(match self { + Normal => "/mic-svgrepo-com.svg", + Muted => "/mic-off-svgrepo-com.svg", + Deafened => "/speaker-muted-svgrepo-com.svg", + None => return Option::None, + }) + } +} + #[component] -pub fn UserPill(name: String) -> Element { +pub fn UserPill(name: String, icon: UserIcon) -> Element { let pill = css!( " - border: solid 1px black; - border-radius: 8px; - padding: 4px; + border-radius: 100px; + padding: 4px 8px; width: fit-content; + + img { + height: 1em; + vertical-align: text-bottom; + } " ); + let color = match icon { + UserIcon::Normal => "var(--accent-a)", + UserIcon::Muted => "var(--accent-b)", + UserIcon::Deafened => "var(--accent-c)", + UserIcon::None => "var(--accent-a)", + }; + rsx!( div { class: "{pill}", - "{name}" + style: "background-color: {color}", + { icon.url().map(|url| rsx!(img { src: url })) } + "\u{00A0}{name}\u{00A0}" } ) } @@ -115,7 +162,8 @@ pub fn User(id: UserId) -> Element { let server = STATE.server.read(); let state = server.users.get(&id)?; rsx!(UserPill { - name: state.name.clone() + name: state.name.clone(), + icon: state.icon(), }) } @@ -129,18 +177,26 @@ pub fn Channel(id: ChannelId) -> Element { let channel_details = css!( " flex: 0 0 100%; + + summary { + cursor: pointer; + } + + summary:focus-visible { + outline: none; + } " ); let channel_children = css!( " - border-left: solid black 1px; + border-left: solid var(--line-color) var(--line-width); display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; margin-left: 5px; padding-left: 11px; - " + padding-top: 4px; " ); rsx!( @@ -148,16 +204,25 @@ pub fn Channel(id: ChannelId) -> Element { class: "{channel_details}", open: true, summary { - ondoubleclick: move |_| net.send(EnterChannel { channel: id, user }), - "{state.name}" - } - div { - class: "{channel_children}", - for id in state.users.iter() { - User { id: *id } + span { + role: "button", + prevent_default: "onclick", + ondoubleclick: move |evt| { + evt.stop_propagation(); + net.send(EnterChannel { channel: id, user }) + }, + "{state.name}" } - for child in state.children.iter() { - Channel { id: *child } + } + if state.users.len() + state.children.len() > 0 { + div { + class: "{channel_children}", + for id in state.users.iter() { + User { id: *id } + } + for child in state.children.iter() { + Channel { id: *child } + } } } } @@ -193,7 +258,7 @@ pub fn ChatView() -> Element { flex-direction: row; padding: 16px; gap: 8px; - border-top: solid black 1px; + border-top: solid var(--line-color) var(--line-width); input { flex-grow: 1; @@ -218,7 +283,10 @@ pub fn ChatView() -> Element { div { class: "{chat_message}", if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) { - UserPill { name: sender.name.clone() } + UserPill { + name: sender.name.clone(), + icon: UserIcon::None, + } } span { dangerous_inner_html: "{chat.dangerous_html}", @@ -262,7 +330,7 @@ pub fn ServerView() -> Element { r#" display: grid; height: 100%; - background-color: white; + background-color: var(--bg-color); grid-template-rows: auto 1fr; grid-template-columns: 1fr 1fr; @@ -278,16 +346,12 @@ pub fn ServerView() -> Element { "tree" "chat"; } - - gap: 4px; - padding: 4px; "# ); let channel_box = css!( " padding: 16px; - border: solid black 1px; overflow: auto; grid-area: tree; " @@ -295,18 +359,23 @@ pub fn ServerView() -> Element { let chat_box = css!( " - border: solid black 1px; display: flex; flex-direction: column; grid-area: chat; + border-left: solid var(--line-color) var(--line-width); + + @media screen and (max-width: 720px) { + border-left:unset; + border-top: solid var(--line-color) var(--line-width); + } " ); let top_bar = css!( " padding: 16px; - border: solid black 1px; grid-area: bar; + background-color: var(--login-bg-color); display: flex; flex-direction: row; @@ -315,6 +384,11 @@ pub fn ServerView() -> Element { button { padding: 8px; + + img { + height: 1em; + vertical-align: text-bottom; + } } " ); @@ -328,31 +402,27 @@ pub fn ServerView() -> Element { onclick: move |_| net.send(Disconnect), "Disconnect" } - span { - input { - r#type: "checkbox", - id: "mute", - checked: mute || self_mute, - disabled: mute, - onchange: move |_| net.send(SetMute { mute: !self_mute }), - } - label { - r#for: "mute", - "Mute" + button { + role: "switch", + aria_checked: mute || self_mute, + disabled: mute, + onclick: move |_| net.send(SetMute { mute: !self_mute }), + match mute || self_mute { + true => rsx!(img { src: "/mic-off-svgrepo-com.svg" }), + false => rsx!(img { src: "/mic-svgrepo-com.svg" }), } + "\u{00A0}Mute" } - span { - input { - r#type: "checkbox", - id: "deaf", - checked: deaf || self_deaf, - disabled: deaf, - onchange: move |_| net.send(SetDeaf { deaf: !self_deaf }), - } - label { - r#for: "deaf", - "Deafen" + button { + role: "switch", + aria_checked: deaf || self_deaf, + disabled: deaf, + onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }), + match deaf || self_deaf { + true => rsx!(img { src: "/speaker-muted-svgrepo-com.svg" }), + false => rsx!(img { src: "/speaker-medium-svgrepo-com.svg" }), } + "\u{00A0}Deafen" } } div { @@ -382,6 +452,10 @@ pub fn LoginView() -> Element { let error = css!( " + background-color: white; + border-radius: 4px; + overflow: auto; + padding: 4px; color: red; pre { color: black; @@ -393,8 +467,9 @@ pub fn LoginView() -> Element { " max-width: 50vw; align-self: center; - padding: 16px; - background-color: white; + padding: 32px; + border-radius: 16px; + background-color: var(--login-bg-color); display: flex; flex-direction: column; @@ -403,6 +478,18 @@ pub fn LoginView() -> Element { input,button { padding: 8px; } + + h1 { + margin: 0; + color: #b3c6b4; + } + " + ); + + let bttn = css!( + " + font-weight: bold; + font-size: large; " ); @@ -418,17 +505,22 @@ pub fn LoginView() -> Element { let bottom = match &*status.read() { Disconnected => rsx! { button { + class: "{bttn}", onclick: do_connect.clone(), - "Connect!" + "Connect" } }, Connecting => rsx! { - "Connecting..." + div { + class: "{bttn}", + "Connecting..." + } }, Failed(msg) => rsx!( button { + class: "{bttn}", onclick: do_connect.clone(), - "Reconnect!" + "Reconnect" } div { class: "{error}", @@ -443,6 +535,9 @@ pub fn LoginView() -> Element { rsx!( div { class: "{login_box}", + h1 { + "Mumble Web" + } input { placeholder: "username", value: "{username.read()}", @@ -465,6 +560,18 @@ pub fn app() -> Element { global_css!( " + :root { + --txt-color: white; + --bg-color: #372f3a; + --login-bg-color: #5d7680; + --primary-btn-color: #7bad9f; + --accent-a: #8eb29a; + --accent-b: #6a9395; + --accent-c: #464459; + --line-width: 2px; + --line-color: white; + } + body { margin: 0; } @@ -474,8 +581,43 @@ pub fn app() -> Element { display: flex; flex-direction: column; justify-content: space-around; - background-color: grey; + background-color: var(--bg-color); overflow: auto; + color: var(--txt-color); + + font-family: sans-serif; + font-size: large; + } + + button { + font-weight: bold; + font-size: medium; + border: none; + border-radius: 4px; + color: var(--txt-color); + background-color: var(--primary-btn-color); + cursor: pointer; + } + + input { + border: none; + border-radius: 4px; + background-color: white; + color: black; + } + + input:focus,input:focus-visible { + border: none; + outline: solid var(--line-width) var(--accent-a); + outline-offset: -3px; + } + + a:link { + color: var(--accent-a); + } + + a:visited { + color: var(--accent-b); } " ); diff --git a/src/imp/web.rs b/src/imp/web.rs index 77c7d17..85f53d9 100644 --- a/src/imp/web.rs +++ b/src/imp/web.rs @@ -5,6 +5,7 @@ use futures::AsyncRead; use futures::AsyncWrite; use futures_channel::mpsc::UnboundedSender; use gloo_timers::future::TimeoutFuture; +use manganis::mg; use mumble_protocol::control::ClientControlCodec; use mumble_protocol::control::ControlPacket; use mumble_protocol::voice::VoicePacket; @@ -240,7 +241,7 @@ async fn create_encoder_worklet( &wasm_bindgen::module(), )?; - let module = "rust_mic_worklet.js"; + let module = "/rust_mic_worklet.js"; console::log_1(&format!("Loading mic worklet from {module:?}").into()); audio_context .audio_worklet()?