Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd4620139f | |||
| 37dc3ffa47 | |||
| feaa9f2bda | |||
| aa3fcf09cf |
@@ -18,9 +18,9 @@ jobs:
|
|||||||
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
|
||||||
- name: Install dioxus-cli
|
- name: Install dioxus-cli
|
||||||
run: cargo binstall dioxus-cli --version 0.7.2
|
run: cargo binstall dioxus-cli --version 0.7.3
|
||||||
|
|
||||||
#- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Build dioxus project
|
- name: Build dioxus project
|
||||||
run: dx build --platform web --release -p mumble-web2-gui
|
run: dx build --platform web --release -p mumble-web2-gui
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ RUN yes | /opt/android-tools/cmdline-tools/latest/bin/sdkmanager --install "plat
|
|||||||
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
|
|
||||||
# Install dioxus-cli
|
# Install dioxus-cli
|
||||||
RUN cargo binstall dioxus-cli@0.7.2
|
RUN cargo binstall dioxus-cli@0.7.3
|
||||||
|
|
||||||
# Install bindgen-cli
|
# Install bindgen-cli
|
||||||
RUN cargo binstall bindgen-cli
|
RUN cargo binstall bindgen-cli
|
||||||
|
|||||||
@@ -44,14 +44,12 @@ RUN choco install rustup.install -y --no-progress
|
|||||||
RUN rustup toolchain install stable-x86_64-pc-windows-msvc
|
RUN rustup toolchain install stable-x86_64-pc-windows-msvc
|
||||||
RUN rustup default stable-x86_64-pc-windows-msvc
|
RUN rustup default stable-x86_64-pc-windows-msvc
|
||||||
|
|
||||||
# Install carog binstall
|
# Install cargo binstall
|
||||||
RUN Set-ExecutionPolicy Unrestricted -Scope Process; `
|
RUN Set-ExecutionPolicy Unrestricted -Scope Process; `
|
||||||
iex (Invoke-WebRequest "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1" -UseBasicParsing).Content
|
iex (Invoke-WebRequest "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.ps1" -UseBasicParsing).Content
|
||||||
|
|
||||||
SHELL ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
SHELL ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
||||||
# Install dioxus-cli from git HEAD with cargo
|
# Install dioxus-cli
|
||||||
# This is to work around a bug in the windows builder upstream.
|
RUN cargo binstall dioxus-cli@0.7.3
|
||||||
# Dioxus has released 0.7.2, but it seems to be broken for now.
|
|
||||||
RUN cargo binstall dioxus-cli
|
|
||||||
|
|
||||||
ENTRYPOINT ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
ENTRYPOINT ["C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat", "&&", "powershell.exe", "-NoLogo", "-ExecutionPolicy", "Bypass"]
|
||||||
|
|||||||
+39
-1
@@ -83,6 +83,44 @@ a:visited {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.channel_header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel_arrow {
|
||||||
|
width: 1em;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel_arrow--placeholder {
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The whole right side of the row is the dblclick target */
|
||||||
|
.channel_row_click {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.1rem 0.25rem 0.1rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover highlight for whole row area (title + blank space) */
|
||||||
|
.channel_row_click:hover {
|
||||||
|
background-color: var(--channel-hover-bg, #222); /* pick your color */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* still keep text non-selectable if desired */
|
||||||
|
.channel_details {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.channel {
|
.channel {
|
||||||
&_details {
|
&_details {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
@@ -392,4 +430,4 @@ a:visited {
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+157
-21
@@ -4,7 +4,7 @@ use dioxus::prelude::*;
|
|||||||
use mime_guess::Mime;
|
use mime_guess::Mime;
|
||||||
use mumble_web2_common::{ClientConfig, ServerStatus};
|
use mumble_web2_common::{ClientConfig, ServerStatus};
|
||||||
use ordermap::OrderSet;
|
use ordermap::OrderSet;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::imp;
|
use crate::imp;
|
||||||
|
|
||||||
@@ -54,14 +54,6 @@ pub enum Command {
|
|||||||
use Command::*;
|
use Command::*;
|
||||||
use ConnectionState::*;
|
use ConnectionState::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ChannelState {
|
|
||||||
pub name: String,
|
|
||||||
pub children: OrderSet<ChannelId>,
|
|
||||||
pub users: OrderSet<UserId>,
|
|
||||||
pub parent: Option<ChannelId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UserState {
|
pub struct UserState {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -94,8 +86,121 @@ pub struct Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ServerState {
|
pub struct ChannelState {
|
||||||
|
pub name: String,
|
||||||
|
pub children: OrderSet<ChannelId>,
|
||||||
|
pub users: OrderSet<UserId>,
|
||||||
|
pub parent: Option<ChannelId>,
|
||||||
|
pub position: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelState {
|
||||||
|
pub fn update_from_channel_state(
|
||||||
|
&mut self,
|
||||||
|
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||||
|
) {
|
||||||
|
if channel_state.has_position() {
|
||||||
|
self.position = channel_state.get_position();
|
||||||
|
}
|
||||||
|
if channel_state.has_parent() {
|
||||||
|
self.parent = Some(channel_state.get_parent());
|
||||||
|
}
|
||||||
|
if channel_state.has_name() {
|
||||||
|
self.name = channel_state.get_name().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ChannelsState {
|
||||||
pub channels: HashMap<ChannelId, ChannelState>,
|
pub channels: HashMap<ChannelId, ChannelState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelsState {
|
||||||
|
pub fn update_from_channel_state(
|
||||||
|
&mut self,
|
||||||
|
channel_state: &mumble_protocol::control::msgs::ChannelState,
|
||||||
|
) {
|
||||||
|
self.channels
|
||||||
|
.entry(channel_state.get_channel_id())
|
||||||
|
.or_default()
|
||||||
|
.update_from_channel_state(channel_state);
|
||||||
|
|
||||||
|
self.update_channel_parents();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_from_channel_remove(
|
||||||
|
&mut self,
|
||||||
|
channel_remove: &mumble_protocol::control::msgs::ChannelRemove,
|
||||||
|
) {
|
||||||
|
self.channels.remove(&channel_remove.get_channel_id());
|
||||||
|
self.update_channel_parents();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_channel_parents(&mut self) {
|
||||||
|
// Zero out existing children
|
||||||
|
for state in self.channels.values_mut() {
|
||||||
|
state.children.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut to_sort: Vec<(ChannelId, Option<ChannelId>, i32, String)> = Vec::new();
|
||||||
|
for (id, state) in self.channels.iter() {
|
||||||
|
// Handle channels with no parent (the root channel)
|
||||||
|
let Some(parent_id) = state.parent else {
|
||||||
|
to_sort.push((*id, None, 0, state.name.clone()));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If a channel has a parent that we haven't gotten a channel
|
||||||
|
// state packet for, ignore it
|
||||||
|
if !self.channels.contains_key(&parent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
to_sort.push((*id, Some(parent_id), state.position, state.name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos_name: HashMap<ChannelId, (i32, String)> = self
|
||||||
|
.channels
|
||||||
|
.iter()
|
||||||
|
.map(|(&id, state)| (id, (state.position, state.name.clone())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut updated: HashSet<ChannelId> = HashSet::new();
|
||||||
|
|
||||||
|
while updated.len() < to_sort.len() {
|
||||||
|
for &(id, ref parent_id, position, ref name) in &to_sort {
|
||||||
|
let Some(parent_id) = parent_id else {
|
||||||
|
updated.insert(id);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if updated.contains(&id) || !updated.contains(&parent_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap should never fail here since we pre filter
|
||||||
|
let parent = self.channels.get_mut(&parent_id).unwrap();
|
||||||
|
|
||||||
|
let mut insert_index = parent.children.len();
|
||||||
|
for (i, &child) in parent.children.iter().enumerate() {
|
||||||
|
let (p, ref n) = pos_name[&child];
|
||||||
|
if (position == p && name < n) || p > position {
|
||||||
|
insert_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.insert_before(insert_index, id);
|
||||||
|
updated.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ServerState {
|
||||||
|
pub channels_state: ChannelsState,
|
||||||
pub users: HashMap<UserId, UserState>,
|
pub users: HashMap<UserId, UserState>,
|
||||||
pub chat: Vec<Chat>,
|
pub chat: Vec<Chat>,
|
||||||
pub session: Option<UserId>,
|
pub session: Option<UserId>,
|
||||||
@@ -182,26 +287,57 @@ pub fn Channel(id: ChannelId) -> Element {
|
|||||||
let net: Coroutine<Command> = use_coroutine_handle();
|
let net: Coroutine<Command> = use_coroutine_handle();
|
||||||
let server = STATE.server.read();
|
let server = STATE.server.read();
|
||||||
let user = server.session.unwrap();
|
let user = server.session.unwrap();
|
||||||
let Some(state) = server.channels.get(&id) else {
|
let Some(state) = server.channels_state.channels.get(&id) else {
|
||||||
return rsx!("missing channel {id}");
|
return rsx!("missing channel {id}");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut open = use_signal(|| true);
|
||||||
|
|
||||||
|
let has_children = !state.users.is_empty() || !state.children.is_empty();
|
||||||
|
|
||||||
rsx!(
|
rsx!(
|
||||||
details {
|
div {
|
||||||
class: "channel_details",
|
class: "channel_details",
|
||||||
open: true,
|
|
||||||
summary {
|
div {
|
||||||
span {
|
class: "channel_header",
|
||||||
role: "button",
|
// Arrow: only toggles open
|
||||||
ondoubleclick: move |evt| {
|
if has_children {
|
||||||
|
span {
|
||||||
|
class: "channel_arrow",
|
||||||
|
onclick: move |evt| {
|
||||||
|
evt.stop_propagation();
|
||||||
|
evt.prevent_default();
|
||||||
|
let mut w = open.write();
|
||||||
|
*w = !*w;
|
||||||
|
},
|
||||||
|
if *open.read() { "▾" } else { "▸" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
span {
|
||||||
|
class: "channel_arrow channel_arrow--placeholder",
|
||||||
|
" "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clickable row area (everything except the arrow)
|
||||||
|
div {
|
||||||
|
class: "channel_row_click",
|
||||||
|
ondblclick: move |evt| {
|
||||||
evt.stop_propagation();
|
evt.stop_propagation();
|
||||||
evt.prevent_default();
|
evt.prevent_default();
|
||||||
net.send(EnterChannel { channel: id, user })
|
net.send(EnterChannel { channel: id, user })
|
||||||
},
|
},
|
||||||
"{state.name}"
|
// remove dblclick from the inner span
|
||||||
|
span {
|
||||||
|
class: "channel_title",
|
||||||
|
"{state.name}"
|
||||||
|
}
|
||||||
|
// if you add icons/badges later, put them here too
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if state.users.len() + state.children.len() > 0 {
|
|
||||||
|
if *open.read() && has_children {
|
||||||
div {
|
div {
|
||||||
class: "channel_children",
|
class: "channel_children",
|
||||||
for id in state.users.iter() {
|
for id in state.users.iter() {
|
||||||
@@ -336,7 +472,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
|
|||||||
return rsx!();
|
return rsx!();
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_channel_name = server.channels[&channel].name.clone();
|
let current_channel_name = server.channels_state.channels[&channel].name.clone();
|
||||||
|
|
||||||
let proxy_url = config
|
let proxy_url = config
|
||||||
.read_unchecked()
|
.read_unchecked()
|
||||||
@@ -528,7 +664,7 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
|
|||||||
class: "server_grid",
|
class: "server_grid",
|
||||||
div {
|
div {
|
||||||
class: "server_channel_box",
|
class: "server_channel_box",
|
||||||
for (id, state) in server.channels.iter() {
|
for (id, state) in server.channels_state.channels.iter() {
|
||||||
if state.parent.is_none() {
|
if state.parent.is_none() {
|
||||||
Channel { id: *id }
|
Channel { id: *id }
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-34
@@ -335,41 +335,11 @@ fn accept_packet(
|
|||||||
}
|
}
|
||||||
ControlPacket::ChannelState(u) => {
|
ControlPacket::ChannelState(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = STATE.server.write();
|
||||||
let id = u.get_channel_id();
|
server.channels_state.update_from_channel_state(&u);
|
||||||
|
|
||||||
let state = server.channels.entry(id).or_default();
|
|
||||||
let new_parent = if u.has_parent() {
|
|
||||||
if let Some(parent) = state.parent.and_then(|p| server.channels.get_mut(&p)) {
|
|
||||||
parent.children.remove(&id);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent_id = u.get_parent();
|
|
||||||
let parent = server.channels.entry(parent_id).or_default();
|
|
||||||
if u.has_position() && u.get_position() as usize <= parent.children.len() {
|
|
||||||
// TODO: what if positions are received out of order? we need to sort afterwards?
|
|
||||||
parent.children.insert_before(u.get_position() as usize, id);
|
|
||||||
} else {
|
|
||||||
parent.children.insert(id);
|
|
||||||
}
|
|
||||||
Some(parent_id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = server.channels.entry(id).or_default();
|
|
||||||
state.parent = new_parent;
|
|
||||||
if u.has_name() {
|
|
||||||
state.name = u.get_name().to_string();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ControlPacket::ChannelRemove(u) => {
|
ControlPacket::ChannelRemove(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = STATE.server.write();
|
||||||
let id = u.get_channel_id();
|
server.channels_state.update_from_channel_remove(&u);
|
||||||
if let Some(channel) = server.channels.remove(&id) {
|
|
||||||
if let Some(parent) = channel.parent.and_then(|p| server.channels.get_mut(&p)) {
|
|
||||||
parent.children.remove(&id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ControlPacket::UserState(u) => {
|
ControlPacket::UserState(u) => {
|
||||||
let mut server = STATE.server.write();
|
let mut server = STATE.server.write();
|
||||||
@@ -381,12 +351,13 @@ fn accept_packet(
|
|||||||
let state = state_entry.or_default();
|
let state = state_entry.or_default();
|
||||||
// the server might now send a channel_id if the user is in channel=0
|
// the server might now send a channel_id if the user is in channel=0
|
||||||
if u.has_channel_id() || new {
|
if u.has_channel_id() || new {
|
||||||
if let Some(parent) = server.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
parent.users.remove(&id);
|
parent.users.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let channel_id = u.get_channel_id();
|
let channel_id = u.get_channel_id();
|
||||||
server
|
server
|
||||||
|
.channels_state
|
||||||
.channels
|
.channels
|
||||||
.entry(channel_id)
|
.entry(channel_id)
|
||||||
.or_default()
|
.or_default()
|
||||||
@@ -418,7 +389,7 @@ fn accept_packet(
|
|||||||
let mut server = STATE.server.write();
|
let mut server = STATE.server.write();
|
||||||
let id = u.get_session();
|
let id = u.get_session();
|
||||||
if let Some(state) = server.users.remove(&id) {
|
if let Some(state) = server.users.remove(&id) {
|
||||||
if let Some(parent) = server.channels.get_mut(&state.channel) {
|
if let Some(parent) = server.channels_state.channels.get_mut(&state.channel) {
|
||||||
parent.users.remove(&id);
|
parent.users.remove(&id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user