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()?