refactor: switch to tsclientlib for TeamSpeak protocol
- Replace custom Session/SessionHandle with tsclientlib SyncConnection - SyncConnection runs in background task, forwards events via mpsc - iced subscription reads events from mpsc channel - SendChannelMessage sends via SyncConnectionHandle::with_connection - Disconnect sends DisconnectOptions via handle - State refresh reads data::Connection (clients, channels, server) - Keep tscore QueryClient for ServerQuery panel - Add tsclientlib as path dependency (from refercence/) - CI checkout with submodules: recursive for tsdeclarations - 69 tests passing, clippy clean
This commit is contained in:
@@ -31,6 +31,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -94,6 +96,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -56,3 +56,5 @@ tscore = { path = "tscore" }
|
||||
tsaudio = { path = "tsaudio" }
|
||||
tsdb = { path = "tsdb" }
|
||||
shared = { path = "shared" }
|
||||
tsclientlib = { path = "../refercence/tsclientlib/tsclientlib", default-features = false, features = ["default-tls"] }
|
||||
tsproto-packets = { path = "../refercence/tsclientlib/utils/tsproto-packets" }
|
||||
|
||||
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
iced = { version = "0.13", features = ["tokio", "debug"] }
|
||||
tokio = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -20,8 +21,9 @@ chrono = { workspace = true }
|
||||
|
||||
shared = { workspace = true }
|
||||
tscore = { workspace = true }
|
||||
tsaudio = { workspace = true }
|
||||
tsdb = { workspace = true }
|
||||
tsclientlib = { workspace = true }
|
||||
tsproto-packets = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
+399
-175
@@ -1,12 +1,15 @@
|
||||
use iced::futures::sink::SinkExt;
|
||||
use iced::futures::StreamExt;
|
||||
use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, vertical_space};
|
||||
use iced::{Element, Length, Subscription, Task};
|
||||
use iced::futures::sink::SinkExt;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use tscore::{ClientConfig, IdentityKey, Session, SessionEvent, SessionHandle};
|
||||
use tsdb::DatabaseManager;
|
||||
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle, SyncStreamItem};
|
||||
use tsclientlib::{ChannelId, ClientId, Connection, DisconnectOptions, MessageTarget};
|
||||
use tsclientlib::events::{Event, PropertyId};
|
||||
use tsclientlib::prelude::*;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
iced::application("ReTeamSpeak", App::update, App::view)
|
||||
@@ -29,11 +32,8 @@ enum Message {
|
||||
NicknameChanged(String),
|
||||
PasswordChanged(String),
|
||||
Connect,
|
||||
Connected(u16),
|
||||
Disconnect,
|
||||
Disconnected,
|
||||
ConnectionError(String),
|
||||
SessionEvent(SessionEvent),
|
||||
TsEvent(TsEvent),
|
||||
MessageInputChanged(String),
|
||||
SendChannelMessage,
|
||||
QueryAddressChanged(String),
|
||||
@@ -44,6 +44,19 @@ enum Message {
|
||||
Noop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TsEvent {
|
||||
Connected,
|
||||
BookEvents(Vec<Event>),
|
||||
MessageEvent(tsclientlib::InMessage),
|
||||
AudioChange(bool, bool),
|
||||
IdentityLevelIncreasing(u8),
|
||||
IdentityLevelIncreased,
|
||||
DisconnectedTemporarily,
|
||||
Disconnected,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BookmarkInfo {
|
||||
name: String,
|
||||
@@ -54,26 +67,21 @@ struct BookmarkInfo {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ChannelEntry {
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
cid: u64,
|
||||
channel_name: String,
|
||||
#[allow(dead_code)]
|
||||
total_clients: u16,
|
||||
parent: ChannelId,
|
||||
order: ChannelId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ClientEntry {
|
||||
clid: u16,
|
||||
client_nickname: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ServerInfo {
|
||||
id: ClientId,
|
||||
name: String,
|
||||
platform: String,
|
||||
version: String,
|
||||
max_clients: u16,
|
||||
clients_online: u16,
|
||||
channel: ChannelId,
|
||||
input_muted: bool,
|
||||
#[allow(dead_code)]
|
||||
output_muted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -84,16 +92,18 @@ struct ChatMessage {
|
||||
|
||||
struct App {
|
||||
page: Page,
|
||||
#[allow(dead_code)]
|
||||
db: Arc<Mutex<DatabaseManager>>,
|
||||
bookmarks: Vec<BookmarkInfo>,
|
||||
selected_bookmark: Option<usize>,
|
||||
nickname: String,
|
||||
password: String,
|
||||
connected: bool,
|
||||
handle: Arc<Mutex<Option<SessionHandle>>>,
|
||||
session_id: u64,
|
||||
server_info: Option<ServerInfo>,
|
||||
handle: Arc<Mutex<Option<SyncConnectionHandle>>>,
|
||||
event_rx: Arc<Mutex<Option<mpsc::Receiver<TsEvent>>>>,
|
||||
server_name: String,
|
||||
server_platform: String,
|
||||
server_version: String,
|
||||
server_max_clients: u16,
|
||||
own_client_id: Option<ClientId>,
|
||||
channels: Vec<ChannelEntry>,
|
||||
clients: Vec<ClientEntry>,
|
||||
messages: Vec<ChatMessage>,
|
||||
@@ -104,17 +114,13 @@ struct App {
|
||||
query_response: Option<String>,
|
||||
query_error: Option<String>,
|
||||
error: Option<String>,
|
||||
identity_level: u8,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let db_path = std::path::PathBuf::from("re-teamspeak.db");
|
||||
let db = DatabaseManager::new(db_path.to_str().unwrap_or("re-teamspeak.db"))
|
||||
.expect("failed to create database");
|
||||
|
||||
let app = Self {
|
||||
page: Page::ServerList,
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
bookmarks: vec![
|
||||
BookmarkInfo {
|
||||
name: "Local Server".to_string(),
|
||||
@@ -134,8 +140,12 @@ impl App {
|
||||
password: String::new(),
|
||||
connected: false,
|
||||
handle: Arc::new(Mutex::new(None)),
|
||||
session_id: 0,
|
||||
server_info: None,
|
||||
event_rx: Arc::new(Mutex::new(None)),
|
||||
server_name: String::new(),
|
||||
server_platform: String::new(),
|
||||
server_version: String::new(),
|
||||
server_max_clients: 0,
|
||||
own_client_id: None,
|
||||
channels: Vec::new(),
|
||||
clients: Vec::new(),
|
||||
messages: Vec::new(),
|
||||
@@ -146,6 +156,7 @@ impl App {
|
||||
query_response: None,
|
||||
query_error: None,
|
||||
error: None,
|
||||
identity_level: 0,
|
||||
};
|
||||
|
||||
(app, Task::none())
|
||||
@@ -182,7 +193,7 @@ impl App {
|
||||
None => return Task::none(),
|
||||
};
|
||||
let nickname = if self.nickname.is_empty() {
|
||||
"User".to_string()
|
||||
"ReTeamSpeak".to_string()
|
||||
} else {
|
||||
self.nickname.clone()
|
||||
};
|
||||
@@ -193,133 +204,109 @@ impl App {
|
||||
};
|
||||
let handle_store = self.handle.clone();
|
||||
|
||||
self.session_id += 1;
|
||||
self.error = None;
|
||||
self.connected = false;
|
||||
|
||||
let event_rx_store = self.event_rx.clone();
|
||||
|
||||
self.error = None;
|
||||
self.connected = false;
|
||||
|
||||
Task::perform(
|
||||
async move {
|
||||
let socket_addr: std::net::SocketAddr =
|
||||
match format!("{}:{}", bookmark.address, bookmark.port).parse() {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
use tokio::net::lookup_host;
|
||||
match lookup_host((&*bookmark.address, bookmark.port)).await {
|
||||
Ok(mut addrs) => match addrs.next() {
|
||||
Some(a) => a,
|
||||
None => return Err("Cannot resolve address".to_string()),
|
||||
},
|
||||
Err(e) => return Err(format!("DNS error: {e}")),
|
||||
}
|
||||
}
|
||||
};
|
||||
let (event_tx, event_rx) = mpsc::channel(100);
|
||||
*event_rx_store.lock().await = Some(event_rx);
|
||||
let mut builder = Connection::build(format!(
|
||||
"{}:{}",
|
||||
bookmark.address, bookmark.port
|
||||
))
|
||||
.name(nickname);
|
||||
|
||||
let identity = IdentityKey::generate();
|
||||
let mut config = ClientConfig::new(socket_addr, nickname);
|
||||
config.server_password = password;
|
||||
config.identity = identity;
|
||||
if let Some(pwd) = password {
|
||||
builder = builder.password(pwd);
|
||||
}
|
||||
|
||||
Session::connect(config, std::time::Duration::from_secs(15))
|
||||
let con = builder.connect().map_err(|e| e.to_string())?;
|
||||
let sync_con: SyncConnection = con.into();
|
||||
let mut handle = sync_con.get_handle();
|
||||
|
||||
*handle_store.lock().await = Some(handle.clone());
|
||||
|
||||
tokio::spawn(run_connection(sync_con, event_tx));
|
||||
|
||||
handle
|
||||
.wait_until_connected()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok::<SyncConnectionHandle, String>(handle)
|
||||
},
|
||||
move |result| match result {
|
||||
Ok((session, handle)) => {
|
||||
let client_id = session.client_id().unwrap_or(0);
|
||||
let handle_arc = handle_store.clone();
|
||||
tokio::spawn(async move {
|
||||
*handle_arc.lock().await = Some(handle);
|
||||
});
|
||||
tokio::spawn(async move {
|
||||
let mut session = session;
|
||||
if let Err(e) = session.run().await {
|
||||
tracing::error!("session error: {e}");
|
||||
}
|
||||
});
|
||||
Message::Connected(client_id)
|
||||
}
|
||||
Err(e) => Message::ConnectionError(e),
|
||||
|result| match result {
|
||||
Ok(_handle) => Message::TsEvent(TsEvent::Connected),
|
||||
Err(e) => Message::TsEvent(TsEvent::Error(e)),
|
||||
},
|
||||
)
|
||||
}
|
||||
Message::Connected(_client_id) => {
|
||||
self.connected = true;
|
||||
let h = self.handle.clone();
|
||||
Task::perform(async move {
|
||||
let guard = h.lock().await;
|
||||
if let Some(ref handle) = *guard {
|
||||
let _ = handle.request_channel_list().await;
|
||||
let _ = handle.request_client_list().await;
|
||||
}
|
||||
}, |_| Message::Noop)
|
||||
}
|
||||
Message::Disconnect => {
|
||||
let h = self.handle.clone();
|
||||
let rx = self.event_rx.clone();
|
||||
self.connected = false;
|
||||
self.server_info = None;
|
||||
self.server_name.clear();
|
||||
self.channels.clear();
|
||||
self.clients.clear();
|
||||
self.messages.clear();
|
||||
self.session_id += 1;
|
||||
Task::perform(async move {
|
||||
self.own_client_id = None;
|
||||
Task::perform(
|
||||
async move {
|
||||
*rx.lock().await = None;
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref handle) = *guard {
|
||||
let _ = handle.disconnect().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let _ = handle.disconnect(DisconnectOptions::new()).await;
|
||||
}
|
||||
*guard = None;
|
||||
}, |_| Message::Disconnected)
|
||||
},
|
||||
|_| Message::Noop,
|
||||
)
|
||||
}
|
||||
Message::Disconnected => {
|
||||
Message::TsEvent(event) => match event {
|
||||
TsEvent::Connected => {
|
||||
self.connected = true;
|
||||
Task::none()
|
||||
}
|
||||
TsEvent::BookEvents(events) => {
|
||||
for event in &events {
|
||||
self.handle_book_event(event);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
TsEvent::MessageEvent(_msg) => Task::none(),
|
||||
TsEvent::AudioChange(_can_send, _can_receive) => Task::none(),
|
||||
TsEvent::IdentityLevelIncreasing(level) => {
|
||||
self.identity_level = level;
|
||||
Task::none()
|
||||
}
|
||||
TsEvent::IdentityLevelIncreased => {
|
||||
self.identity_level = 0;
|
||||
Task::none()
|
||||
}
|
||||
TsEvent::DisconnectedTemporarily => {
|
||||
self.error = Some("Connection lost, reconnecting...".to_string());
|
||||
Task::none()
|
||||
}
|
||||
TsEvent::Disconnected => {
|
||||
self.connected = false;
|
||||
self.server_info = None;
|
||||
self.server_name.clear();
|
||||
self.channels.clear();
|
||||
self.clients.clear();
|
||||
self.messages.clear();
|
||||
self.own_client_id = None;
|
||||
Task::none()
|
||||
}
|
||||
Message::ConnectionError(err) => {
|
||||
self.error = Some(err);
|
||||
Task::none()
|
||||
}
|
||||
Message::SessionEvent(event) => {
|
||||
match event {
|
||||
SessionEvent::Connected { client_id } => {
|
||||
tracing::info!("Connected as client {client_id}");
|
||||
}
|
||||
SessionEvent::ServerInfo { name, platform, version, max_clients, clients_online, .. } => {
|
||||
self.server_info = Some(ServerInfo { name, platform, version, max_clients, clients_online });
|
||||
}
|
||||
SessionEvent::ChannelList(channels) => {
|
||||
self.channels = channels.into_iter().map(|c| ChannelEntry {
|
||||
cid: c.cid,
|
||||
channel_name: c.channel_name,
|
||||
total_clients: c.total_clients,
|
||||
}).collect();
|
||||
}
|
||||
SessionEvent::ClientList(clients) => {
|
||||
self.clients = clients.into_iter().map(|c| ClientEntry {
|
||||
clid: c.clid,
|
||||
client_nickname: c.client_nickname,
|
||||
}).collect();
|
||||
}
|
||||
SessionEvent::TextMessage { invoker_name, message, .. } => {
|
||||
self.messages.push(ChatMessage { invoker_name, message });
|
||||
}
|
||||
SessionEvent::ClientEntered { clid, client_nickname, .. } => {
|
||||
self.clients.push(ClientEntry { clid, client_nickname });
|
||||
}
|
||||
SessionEvent::ClientLeft { clid, .. } => {
|
||||
self.clients.retain(|c| c.clid != clid);
|
||||
}
|
||||
SessionEvent::Error(err) => {
|
||||
self.error = Some(err);
|
||||
}
|
||||
SessionEvent::Disconnected => {
|
||||
return self.update(Message::Disconnected);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
TsEvent::Error(e) => {
|
||||
self.error = Some(e);
|
||||
Task::none()
|
||||
}
|
||||
},
|
||||
Message::MessageInputChanged(input) => {
|
||||
self.message_input = input;
|
||||
Task::none()
|
||||
@@ -331,15 +318,32 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
let h = self.handle.clone();
|
||||
Task::perform(async move {
|
||||
let guard = h.lock().await;
|
||||
if let Some(ref handle) = *guard {
|
||||
if let Err(e) = handle.send_channel_message(&msg).await {
|
||||
return Message::ConnectionError(e.to_string());
|
||||
Task::perform(
|
||||
async move {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let result = handle
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
let cmd = state.send_message(MessageTarget::Channel, &msg);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
return Some(e.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
},
|
||||
|err| {
|
||||
if let Some(e) = err {
|
||||
Message::TsEvent(TsEvent::Error(e))
|
||||
} else {
|
||||
Message::Noop
|
||||
}, |m| m)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Message::QueryAddressChanged(addr) => {
|
||||
self.query_address = addr;
|
||||
@@ -361,9 +365,12 @@ impl App {
|
||||
Task::perform(
|
||||
async move {
|
||||
use tscore::QueryClient;
|
||||
let mut client = QueryClient::connect(&addr).await
|
||||
let mut client = QueryClient::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| format!("Connect: {e}"))?;
|
||||
let resp = client.execute("help").await
|
||||
let resp = client
|
||||
.execute("help")
|
||||
.await
|
||||
.map_err(|e| format!("Query: {e}"))?;
|
||||
Ok::<String, String>(resp.raw)
|
||||
},
|
||||
@@ -387,36 +394,211 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_book_event(&mut self, event: &Event) {
|
||||
match event {
|
||||
Event::PropertyAdded { id, invoker: _, .. } => match id {
|
||||
PropertyId::Server => {
|
||||
self.refresh_server_state_from_handle();
|
||||
}
|
||||
PropertyId::Client(_cid) => {
|
||||
self.refresh_clients_from_handle();
|
||||
}
|
||||
PropertyId::Channel(_chid) => {
|
||||
self.refresh_channels_from_handle();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::PropertyChanged { id, invoker, .. } => match id {
|
||||
PropertyId::ClientName(cid) => {
|
||||
if let Some(c) = self.clients.iter_mut().find(|c| c.id == *cid) {
|
||||
if let Some(inv) = invoker {
|
||||
c.name = inv.name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
PropertyId::ClientChannel(cid) => {
|
||||
if let Some(c) = self.clients.iter_mut().find(|c| c.id == *cid) {
|
||||
if let Some(inv) = invoker {
|
||||
c.name = inv.name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
PropertyId::ClientInputMuted(_cid) => {
|
||||
self.refresh_clients_from_handle();
|
||||
}
|
||||
PropertyId::ChannelName(_chid) => {
|
||||
self.refresh_channels_from_handle();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::PropertyRemoved { id, .. } => match id {
|
||||
PropertyId::Client(cid) => {
|
||||
self.clients.retain(|c| c.id != *cid);
|
||||
}
|
||||
PropertyId::Channel(chid) => {
|
||||
self.channels.retain(|c| c.id != *chid);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Message {
|
||||
invoker,
|
||||
message,
|
||||
..
|
||||
} => {
|
||||
self.messages.push(ChatMessage {
|
||||
invoker_name: invoker.name.clone(),
|
||||
message: message.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_server_state_from_handle(&mut self) {
|
||||
let h = self.handle.clone();
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(async {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let result = handle
|
||||
.with_connection(|con| {
|
||||
if let Ok(state) = con.get_state() {
|
||||
return Some((
|
||||
state.server.name.clone(),
|
||||
state.server.platform.clone(),
|
||||
state.server.version.clone(),
|
||||
state.server.max_clients,
|
||||
state.own_client,
|
||||
state
|
||||
.channels
|
||||
.values()
|
||||
.map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
state
|
||||
.clients
|
||||
.values()
|
||||
.map(|c| ClientEntry {
|
||||
id: c.id,
|
||||
name: c.name.clone(),
|
||||
channel: c.channel,
|
||||
input_muted: c.input_muted,
|
||||
output_muted: c.output_muted,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
if let Ok(Some((name, platform, version, max_clients, own_id, channels, clients))) =
|
||||
result
|
||||
{
|
||||
self.server_name = name;
|
||||
self.server_platform = platform;
|
||||
self.server_version = version;
|
||||
self.server_max_clients = max_clients;
|
||||
self.own_client_id = Some(own_id);
|
||||
self.channels = channels;
|
||||
self.clients = clients;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_clients_from_handle(&mut self) {
|
||||
let h = self.handle.clone();
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(async {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let result = handle
|
||||
.with_connection(|con| {
|
||||
if let Ok(state) = con.get_state() {
|
||||
return Some(
|
||||
state
|
||||
.clients
|
||||
.values()
|
||||
.map(|c| ClientEntry {
|
||||
id: c.id,
|
||||
name: c.name.clone(),
|
||||
channel: c.channel,
|
||||
input_muted: c.input_muted,
|
||||
output_muted: c.output_muted,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
if let Ok(Some(clients)) = result {
|
||||
self.clients = clients;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_channels_from_handle(&mut self) {
|
||||
let h = self.handle.clone();
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
rt.block_on(async {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let result = handle
|
||||
.with_connection(|con| {
|
||||
if let Ok(state) = con.get_state() {
|
||||
return Some(
|
||||
state
|
||||
.channels
|
||||
.values()
|
||||
.map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
if let Ok(Some(channels)) = result {
|
||||
self.channels = channels;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
if !self.connected {
|
||||
return Subscription::none();
|
||||
}
|
||||
let handle = self.handle.clone();
|
||||
let session_id = self.session_id;
|
||||
let event_rx = self.event_rx.clone();
|
||||
Subscription::run_with_id(
|
||||
session_id,
|
||||
1u64,
|
||||
iced::stream::channel(100, move |mut sender| async move {
|
||||
loop {
|
||||
let event = {
|
||||
let mut guard = handle.lock().await;
|
||||
match guard.as_mut() {
|
||||
Some(h) => h.recv_event().await,
|
||||
None => break,
|
||||
}
|
||||
let rx = {
|
||||
let mut guard = event_rx.lock().await;
|
||||
guard.take()
|
||||
};
|
||||
match event {
|
||||
Some(event) => {
|
||||
let is_disconnect = matches!(event, SessionEvent::Disconnected);
|
||||
if sender.send(Message::SessionEvent(event)).await.is_err() {
|
||||
let Some(mut rx) = rx else { return };
|
||||
while let Some(event) = rx.recv().await {
|
||||
let is_disconnect = matches!(
|
||||
event,
|
||||
TsEvent::Disconnected | TsEvent::Error(_)
|
||||
);
|
||||
if sender.send(Message::TsEvent(event)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
if is_disconnect {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -535,30 +717,45 @@ impl App {
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(16);
|
||||
|
||||
if let Some(err) = &self.error {
|
||||
connect_form = connect_form.push(text(format!("Error: {err}")).size(12));
|
||||
}
|
||||
if self.identity_level > 0 {
|
||||
connect_form = connect_form.push(
|
||||
text(format!("Error: {err}")).size(12)
|
||||
text(format!(
|
||||
"Computing identity level {}... this may take a while",
|
||||
self.identity_level
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
|
||||
if self.connected {
|
||||
let server_info = if let Some(info) = &self.server_info {
|
||||
column![
|
||||
text(&info.name).size(18),
|
||||
text(format!("{} / {}", info.platform, info.version)).size(12),
|
||||
text(format!("{}/{} online", info.clients_online, info.max_clients)).size(12),
|
||||
let server_info = column![
|
||||
text(&self.server_name).size(18),
|
||||
text(format!("{} / {}", self.server_platform, self.server_version)).size(12),
|
||||
text(format!("max clients: {}", self.server_max_clients)).size(12),
|
||||
]
|
||||
.spacing(4)
|
||||
} else {
|
||||
column![text("Connected").size(18)]
|
||||
};
|
||||
.spacing(4);
|
||||
|
||||
let mut channel_entries: Vec<(ChannelId, String)> = self
|
||||
.channels
|
||||
.iter()
|
||||
.map(|ch| (ch.id, ch.name.clone()))
|
||||
.collect();
|
||||
// Sort by order (use existing order from channels)
|
||||
let order_map: std::collections::HashMap<ChannelId, u64> =
|
||||
self.channels.iter().map(|ch| (ch.id, ch.order.0)).collect();
|
||||
channel_entries.sort_by_key(|(id, _)| order_map.get(id).copied().unwrap_or(0));
|
||||
|
||||
let mut channels = column![text("Channels").size(14)].spacing(2);
|
||||
for ch in &self.channels {
|
||||
for (ch_id, ch_name) in &channel_entries {
|
||||
let client_count = self.clients.iter().filter(|c| c.channel == *ch_id).count();
|
||||
channels = channels.push(
|
||||
row![
|
||||
text(&ch.channel_name).size(12).width(Length::Fill),
|
||||
text(format!("{}", ch.total_clients)).size(11),
|
||||
text(ch_name.clone()).size(12).width(Length::Fill),
|
||||
text(format!("{}", client_count)).size(11),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
@@ -566,12 +763,14 @@ impl App {
|
||||
|
||||
let mut clients = column![text("Clients").size(14)].spacing(2);
|
||||
for c in &self.clients {
|
||||
clients = clients.push(text(&c.client_nickname).size(12));
|
||||
let mute_indicator = if c.input_muted { " [M]" } else { "" };
|
||||
clients =
|
||||
clients.push(text(format!("{}{}", c.name, mute_indicator)).size(12));
|
||||
}
|
||||
|
||||
let mut messages = column![].spacing(4);
|
||||
let mut messages_col = column![].spacing(4);
|
||||
for msg in &self.messages {
|
||||
messages = messages.push(
|
||||
messages_col = messages_col.push(
|
||||
row![
|
||||
text(format!("{}:", msg.invoker_name)).size(12),
|
||||
text(&msg.message).size(12),
|
||||
@@ -603,7 +802,7 @@ impl App {
|
||||
.spacing(16),
|
||||
horizontal_rule(1),
|
||||
text("Messages").size(14),
|
||||
scrollable(messages).height(120),
|
||||
scrollable(messages_col).height(120),
|
||||
message_input,
|
||||
]
|
||||
.spacing(8)
|
||||
@@ -697,6 +896,31 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>) {
|
||||
let mut stream = con;
|
||||
while let Some(item) = stream.next().await {
|
||||
let ts_event = match item {
|
||||
Ok(SyncStreamItem::BookEvents(events)) => TsEvent::BookEvents(events),
|
||||
Ok(SyncStreamItem::MessageEvent(msg)) => TsEvent::MessageEvent(msg),
|
||||
Ok(SyncStreamItem::AudioChange(change)) => match change {
|
||||
tsclientlib::AudioEvent::CanSendAudio(can) => TsEvent::AudioChange(can, true),
|
||||
tsclientlib::AudioEvent::CanReceiveAudio(can) => TsEvent::AudioChange(false, can),
|
||||
},
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => {
|
||||
TsEvent::IdentityLevelIncreasing(level)
|
||||
}
|
||||
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
|
||||
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
|
||||
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
|
||||
Err(e) => TsEvent::Error(e.to_string()),
|
||||
};
|
||||
if event_tx.send(ts_event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = event_tx.send(TsEvent::Disconnected).await;
|
||||
}
|
||||
|
||||
fn nav_button(label: &str, page: Page, current: Page) -> Element<'static, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
.padding([6, 12])
|
||||
|
||||
Reference in New Issue
Block a user