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:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -94,6 +96,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|||||||
@@ -56,3 +56,5 @@ tscore = { path = "tscore" }
|
|||||||
tsaudio = { path = "tsaudio" }
|
tsaudio = { path = "tsaudio" }
|
||||||
tsdb = { path = "tsdb" }
|
tsdb = { path = "tsdb" }
|
||||||
shared = { path = "shared" }
|
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]
|
[dependencies]
|
||||||
iced = { version = "0.13", features = ["tokio", "debug"] }
|
iced = { version = "0.13", features = ["tokio", "debug"] }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
@@ -20,8 +21,9 @@ chrono = { workspace = true }
|
|||||||
|
|
||||||
shared = { workspace = true }
|
shared = { workspace = true }
|
||||||
tscore = { workspace = true }
|
tscore = { workspace = true }
|
||||||
tsaudio = { workspace = true }
|
|
||||||
tsdb = { workspace = true }
|
tsdb = { workspace = true }
|
||||||
|
tsclientlib = { workspace = true }
|
||||||
|
tsproto-packets = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
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::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, vertical_space};
|
||||||
use iced::{Element, Length, Subscription, Task};
|
use iced::{Element, Length, Subscription, Task};
|
||||||
use iced::futures::sink::SinkExt;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
use tscore::{ClientConfig, IdentityKey, Session, SessionEvent, SessionHandle};
|
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle, SyncStreamItem};
|
||||||
use tsdb::DatabaseManager;
|
use tsclientlib::{ChannelId, ClientId, Connection, DisconnectOptions, MessageTarget};
|
||||||
|
use tsclientlib::events::{Event, PropertyId};
|
||||||
|
use tsclientlib::prelude::*;
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
iced::application("ReTeamSpeak", App::update, App::view)
|
iced::application("ReTeamSpeak", App::update, App::view)
|
||||||
@@ -29,11 +32,8 @@ enum Message {
|
|||||||
NicknameChanged(String),
|
NicknameChanged(String),
|
||||||
PasswordChanged(String),
|
PasswordChanged(String),
|
||||||
Connect,
|
Connect,
|
||||||
Connected(u16),
|
|
||||||
Disconnect,
|
Disconnect,
|
||||||
Disconnected,
|
TsEvent(TsEvent),
|
||||||
ConnectionError(String),
|
|
||||||
SessionEvent(SessionEvent),
|
|
||||||
MessageInputChanged(String),
|
MessageInputChanged(String),
|
||||||
SendChannelMessage,
|
SendChannelMessage,
|
||||||
QueryAddressChanged(String),
|
QueryAddressChanged(String),
|
||||||
@@ -44,6 +44,19 @@ enum Message {
|
|||||||
Noop,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
struct BookmarkInfo {
|
struct BookmarkInfo {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -54,26 +67,21 @@ struct BookmarkInfo {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ChannelEntry {
|
struct ChannelEntry {
|
||||||
|
id: ChannelId,
|
||||||
|
name: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
cid: u64,
|
parent: ChannelId,
|
||||||
channel_name: String,
|
order: ChannelId,
|
||||||
#[allow(dead_code)]
|
|
||||||
total_clients: u16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ClientEntry {
|
struct ClientEntry {
|
||||||
clid: u16,
|
id: ClientId,
|
||||||
client_nickname: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct ServerInfo {
|
|
||||||
name: String,
|
name: String,
|
||||||
platform: String,
|
channel: ChannelId,
|
||||||
version: String,
|
input_muted: bool,
|
||||||
max_clients: u16,
|
#[allow(dead_code)]
|
||||||
clients_online: u16,
|
output_muted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -84,16 +92,18 @@ struct ChatMessage {
|
|||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
page: Page,
|
page: Page,
|
||||||
#[allow(dead_code)]
|
|
||||||
db: Arc<Mutex<DatabaseManager>>,
|
|
||||||
bookmarks: Vec<BookmarkInfo>,
|
bookmarks: Vec<BookmarkInfo>,
|
||||||
selected_bookmark: Option<usize>,
|
selected_bookmark: Option<usize>,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
password: String,
|
password: String,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
handle: Arc<Mutex<Option<SessionHandle>>>,
|
handle: Arc<Mutex<Option<SyncConnectionHandle>>>,
|
||||||
session_id: u64,
|
event_rx: Arc<Mutex<Option<mpsc::Receiver<TsEvent>>>>,
|
||||||
server_info: Option<ServerInfo>,
|
server_name: String,
|
||||||
|
server_platform: String,
|
||||||
|
server_version: String,
|
||||||
|
server_max_clients: u16,
|
||||||
|
own_client_id: Option<ClientId>,
|
||||||
channels: Vec<ChannelEntry>,
|
channels: Vec<ChannelEntry>,
|
||||||
clients: Vec<ClientEntry>,
|
clients: Vec<ClientEntry>,
|
||||||
messages: Vec<ChatMessage>,
|
messages: Vec<ChatMessage>,
|
||||||
@@ -104,17 +114,13 @@ struct App {
|
|||||||
query_response: Option<String>,
|
query_response: Option<String>,
|
||||||
query_error: Option<String>,
|
query_error: Option<String>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
|
identity_level: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> (Self, Task<Message>) {
|
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 {
|
let app = Self {
|
||||||
page: Page::ServerList,
|
page: Page::ServerList,
|
||||||
db: Arc::new(Mutex::new(db)),
|
|
||||||
bookmarks: vec![
|
bookmarks: vec![
|
||||||
BookmarkInfo {
|
BookmarkInfo {
|
||||||
name: "Local Server".to_string(),
|
name: "Local Server".to_string(),
|
||||||
@@ -134,8 +140,12 @@ impl App {
|
|||||||
password: String::new(),
|
password: String::new(),
|
||||||
connected: false,
|
connected: false,
|
||||||
handle: Arc::new(Mutex::new(None)),
|
handle: Arc::new(Mutex::new(None)),
|
||||||
session_id: 0,
|
event_rx: Arc::new(Mutex::new(None)),
|
||||||
server_info: None,
|
server_name: String::new(),
|
||||||
|
server_platform: String::new(),
|
||||||
|
server_version: String::new(),
|
||||||
|
server_max_clients: 0,
|
||||||
|
own_client_id: None,
|
||||||
channels: Vec::new(),
|
channels: Vec::new(),
|
||||||
clients: Vec::new(),
|
clients: Vec::new(),
|
||||||
messages: Vec::new(),
|
messages: Vec::new(),
|
||||||
@@ -146,6 +156,7 @@ impl App {
|
|||||||
query_response: None,
|
query_response: None,
|
||||||
query_error: None,
|
query_error: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
identity_level: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
(app, Task::none())
|
(app, Task::none())
|
||||||
@@ -182,7 +193,7 @@ impl App {
|
|||||||
None => return Task::none(),
|
None => return Task::none(),
|
||||||
};
|
};
|
||||||
let nickname = if self.nickname.is_empty() {
|
let nickname = if self.nickname.is_empty() {
|
||||||
"User".to_string()
|
"ReTeamSpeak".to_string()
|
||||||
} else {
|
} else {
|
||||||
self.nickname.clone()
|
self.nickname.clone()
|
||||||
};
|
};
|
||||||
@@ -193,133 +204,109 @@ impl App {
|
|||||||
};
|
};
|
||||||
let handle_store = self.handle.clone();
|
let handle_store = self.handle.clone();
|
||||||
|
|
||||||
self.session_id += 1;
|
|
||||||
self.error = None;
|
self.error = None;
|
||||||
|
self.connected = false;
|
||||||
|
|
||||||
|
let event_rx_store = self.event_rx.clone();
|
||||||
|
|
||||||
|
self.error = None;
|
||||||
|
self.connected = false;
|
||||||
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let socket_addr: std::net::SocketAddr =
|
let (event_tx, event_rx) = mpsc::channel(100);
|
||||||
match format!("{}:{}", bookmark.address, bookmark.port).parse() {
|
*event_rx_store.lock().await = Some(event_rx);
|
||||||
Ok(a) => a,
|
let mut builder = Connection::build(format!(
|
||||||
Err(_) => {
|
"{}:{}",
|
||||||
use tokio::net::lookup_host;
|
bookmark.address, bookmark.port
|
||||||
match lookup_host((&*bookmark.address, bookmark.port)).await {
|
))
|
||||||
Ok(mut addrs) => match addrs.next() {
|
.name(nickname);
|
||||||
Some(a) => a,
|
|
||||||
None => return Err("Cannot resolve address".to_string()),
|
|
||||||
},
|
|
||||||
Err(e) => return Err(format!("DNS error: {e}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let identity = IdentityKey::generate();
|
if let Some(pwd) = password {
|
||||||
let mut config = ClientConfig::new(socket_addr, nickname);
|
builder = builder.password(pwd);
|
||||||
config.server_password = password;
|
}
|
||||||
config.identity = identity;
|
|
||||||
|
|
||||||
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
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok::<SyncConnectionHandle, String>(handle)
|
||||||
},
|
},
|
||||||
move |result| match result {
|
|result| match result {
|
||||||
Ok((session, handle)) => {
|
Ok(_handle) => Message::TsEvent(TsEvent::Connected),
|
||||||
let client_id = session.client_id().unwrap_or(0);
|
Err(e) => Message::TsEvent(TsEvent::Error(e)),
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
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 => {
|
Message::Disconnect => {
|
||||||
let h = self.handle.clone();
|
let h = self.handle.clone();
|
||||||
|
let rx = self.event_rx.clone();
|
||||||
self.connected = false;
|
self.connected = false;
|
||||||
self.server_info = None;
|
self.server_name.clear();
|
||||||
self.channels.clear();
|
self.channels.clear();
|
||||||
self.clients.clear();
|
self.clients.clear();
|
||||||
self.messages.clear();
|
self.messages.clear();
|
||||||
self.session_id += 1;
|
self.own_client_id = None;
|
||||||
Task::perform(async move {
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
*rx.lock().await = None;
|
||||||
let mut guard = h.lock().await;
|
let mut guard = h.lock().await;
|
||||||
if let Some(ref handle) = *guard {
|
if let Some(ref mut handle) = *guard {
|
||||||
let _ = handle.disconnect().await;
|
let _ = handle.disconnect(DisconnectOptions::new()).await;
|
||||||
}
|
}
|
||||||
*guard = None;
|
*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.connected = false;
|
||||||
self.server_info = None;
|
self.server_name.clear();
|
||||||
self.channels.clear();
|
self.channels.clear();
|
||||||
self.clients.clear();
|
self.clients.clear();
|
||||||
self.messages.clear();
|
self.messages.clear();
|
||||||
|
self.own_client_id = None;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ConnectionError(err) => {
|
TsEvent::Error(e) => {
|
||||||
self.error = Some(err);
|
self.error = Some(e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
Message::MessageInputChanged(input) => {
|
Message::MessageInputChanged(input) => {
|
||||||
self.message_input = input;
|
self.message_input = input;
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -331,15 +318,32 @@ impl App {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let h = self.handle.clone();
|
let h = self.handle.clone();
|
||||||
Task::perform(async move {
|
Task::perform(
|
||||||
let guard = h.lock().await;
|
async move {
|
||||||
if let Some(ref handle) = *guard {
|
let mut guard = h.lock().await;
|
||||||
if let Err(e) = handle.send_channel_message(&msg).await {
|
if let Some(ref mut handle) = *guard {
|
||||||
return Message::ConnectionError(e.to_string());
|
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
|
Message::Noop
|
||||||
}, |m| m)
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Message::QueryAddressChanged(addr) => {
|
Message::QueryAddressChanged(addr) => {
|
||||||
self.query_address = addr;
|
self.query_address = addr;
|
||||||
@@ -361,9 +365,12 @@ impl App {
|
|||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
use tscore::QueryClient;
|
use tscore::QueryClient;
|
||||||
let mut client = QueryClient::connect(&addr).await
|
let mut client = QueryClient::connect(&addr)
|
||||||
|
.await
|
||||||
.map_err(|e| format!("Connect: {e}"))?;
|
.map_err(|e| format!("Connect: {e}"))?;
|
||||||
let resp = client.execute("help").await
|
let resp = client
|
||||||
|
.execute("help")
|
||||||
|
.await
|
||||||
.map_err(|e| format!("Query: {e}"))?;
|
.map_err(|e| format!("Query: {e}"))?;
|
||||||
Ok::<String, String>(resp.raw)
|
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> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
if !self.connected {
|
if !self.connected {
|
||||||
return Subscription::none();
|
return Subscription::none();
|
||||||
}
|
}
|
||||||
let handle = self.handle.clone();
|
let event_rx = self.event_rx.clone();
|
||||||
let session_id = self.session_id;
|
|
||||||
Subscription::run_with_id(
|
Subscription::run_with_id(
|
||||||
session_id,
|
1u64,
|
||||||
iced::stream::channel(100, move |mut sender| async move {
|
iced::stream::channel(100, move |mut sender| async move {
|
||||||
loop {
|
let rx = {
|
||||||
let event = {
|
let mut guard = event_rx.lock().await;
|
||||||
let mut guard = handle.lock().await;
|
guard.take()
|
||||||
match guard.as_mut() {
|
|
||||||
Some(h) => h.recv_event().await,
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
match event {
|
let Some(mut rx) = rx else { return };
|
||||||
Some(event) => {
|
while let Some(event) = rx.recv().await {
|
||||||
let is_disconnect = matches!(event, SessionEvent::Disconnected);
|
let is_disconnect = matches!(
|
||||||
if sender.send(Message::SessionEvent(event)).await.is_err() {
|
event,
|
||||||
|
TsEvent::Disconnected | TsEvent::Error(_)
|
||||||
|
);
|
||||||
|
if sender.send(Message::TsEvent(event)).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if is_disconnect {
|
if is_disconnect {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -535,30 +717,45 @@ impl App {
|
|||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.padding(16);
|
.padding(16);
|
||||||
|
|
||||||
if let Some(err) = &self.error {
|
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(
|
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 {
|
if self.connected {
|
||||||
let server_info = if let Some(info) = &self.server_info {
|
let server_info = column![
|
||||||
column![
|
text(&self.server_name).size(18),
|
||||||
text(&info.name).size(18),
|
text(format!("{} / {}", self.server_platform, self.server_version)).size(12),
|
||||||
text(format!("{} / {}", info.platform, info.version)).size(12),
|
text(format!("max clients: {}", self.server_max_clients)).size(12),
|
||||||
text(format!("{}/{} online", info.clients_online, info.max_clients)).size(12),
|
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4);
|
||||||
} else {
|
|
||||||
column![text("Connected").size(18)]
|
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);
|
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(
|
channels = channels.push(
|
||||||
row![
|
row![
|
||||||
text(&ch.channel_name).size(12).width(Length::Fill),
|
text(ch_name.clone()).size(12).width(Length::Fill),
|
||||||
text(format!("{}", ch.total_clients)).size(11),
|
text(format!("{}", client_count)).size(11),
|
||||||
]
|
]
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
);
|
);
|
||||||
@@ -566,12 +763,14 @@ impl App {
|
|||||||
|
|
||||||
let mut clients = column![text("Clients").size(14)].spacing(2);
|
let mut clients = column![text("Clients").size(14)].spacing(2);
|
||||||
for c in &self.clients {
|
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 {
|
for msg in &self.messages {
|
||||||
messages = messages.push(
|
messages_col = messages_col.push(
|
||||||
row![
|
row![
|
||||||
text(format!("{}:", msg.invoker_name)).size(12),
|
text(format!("{}:", msg.invoker_name)).size(12),
|
||||||
text(&msg.message).size(12),
|
text(&msg.message).size(12),
|
||||||
@@ -603,7 +802,7 @@ impl App {
|
|||||||
.spacing(16),
|
.spacing(16),
|
||||||
horizontal_rule(1),
|
horizontal_rule(1),
|
||||||
text("Messages").size(14),
|
text("Messages").size(14),
|
||||||
scrollable(messages).height(120),
|
scrollable(messages_col).height(120),
|
||||||
message_input,
|
message_input,
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.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> {
|
fn nav_button(label: &str, page: Page, current: Page) -> Element<'static, Message> {
|
||||||
button(text(label.to_string()).size(12))
|
button(text(label.to_string()).size(12))
|
||||||
.padding([6, 12])
|
.padding([6, 12])
|
||||||
|
|||||||
Reference in New Issue
Block a user