fix: wire SessionHandle, event subscription, and message sending

- Store SessionHandle in Arc<Mutex> instead of dropping it
- Implement iced subscription with stream::channel to forward SessionEvent
- SendChannelMessage now sends via SessionHandle
- Disconnect sends clientdisconnect command then clears state
- ServerQuery panel now executes 'help' command via QueryClient
- Remove tracing_subscriber to avoid console window on Windows
- Request channel/client lists on connect
- Show connection errors and query responses in UI
This commit is contained in:
ReTeamSpeak
2026-05-12 21:12:22 +09:00
parent c4113373c8
commit b816b1308f
+141 -23
View File
@@ -1,14 +1,14 @@
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 tscore::{ClientConfig, IdentityKey, Session, SessionEvent};
use tscore::{ClientConfig, IdentityKey, Session, SessionEvent, SessionHandle};
use tsdb::DatabaseManager;
fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::application("ReTeamSpeak", App::update, App::view)
.subscription(App::subscription)
.run_with(App::new)
@@ -24,8 +24,6 @@ enum Page {
#[derive(Debug, Clone)]
enum Message {
PageChanged(Page),
LoadBookmarks,
BookmarksLoaded(Vec<BookmarkInfo>),
SelectBookmark(Option<usize>),
NicknameChanged(String),
PasswordChanged(String),
@@ -34,19 +32,19 @@ enum Message {
Disconnect,
Disconnected,
ConnectionError(String),
PollEvents,
SessionEvent(SessionEvent),
MessageInputChanged(String),
SendChannelMessage,
QueryAddressChanged(String),
QueryPortChanged(String),
RunServerQuery,
QueryResponse(String),
QueryError(String),
Noop,
}
#[derive(Debug, Clone)]
struct BookmarkInfo {
id: String,
name: String,
address: String,
port: u16,
@@ -55,8 +53,10 @@ struct BookmarkInfo {
#[derive(Debug, Clone)]
struct ChannelEntry {
#[allow(dead_code)]
cid: u64,
channel_name: String,
#[allow(dead_code)]
total_clients: u16,
}
@@ -83,12 +83,15 @@ 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>,
channels: Vec<ChannelEntry>,
clients: Vec<ClientEntry>,
@@ -97,6 +100,7 @@ struct App {
query_address: String,
query_port: u16,
query_loading: bool,
query_response: Option<String>,
query_error: Option<String>,
error: Option<String>,
}
@@ -112,14 +116,12 @@ impl App {
db: Arc::new(Mutex::new(db)),
bookmarks: vec![
BookmarkInfo {
id: "1".to_string(),
name: "Local Server".to_string(),
address: "127.0.0.1".to_string(),
port: 9987,
nickname: Some("User".to_string()),
},
BookmarkInfo {
id: "2".to_string(),
name: "KR TeamSpeak".to_string(),
address: "kr.teamspeak.app".to_string(),
port: 9987,
@@ -130,6 +132,8 @@ impl App {
nickname: String::new(),
password: String::new(),
connected: false,
handle: Arc::new(Mutex::new(None)),
session_id: 0,
server_info: None,
channels: Vec::new(),
clients: Vec::new(),
@@ -138,6 +142,7 @@ impl App {
query_address: String::new(),
query_port: 10011,
query_loading: false,
query_response: None,
query_error: None,
error: None,
};
@@ -151,8 +156,6 @@ impl App {
self.page = page;
Task::none()
}
Message::LoadBookmarks => Task::none(),
Message::BookmarksLoaded(_) => Task::none(),
Message::SelectBookmark(idx) => {
self.selected_bookmark = idx;
if let Some(i) = idx {
@@ -187,6 +190,10 @@ impl App {
} else {
Some(self.password.clone())
};
let handle_store = self.handle.clone();
self.session_id += 1;
self.error = None;
Task::perform(
async move {
@@ -214,9 +221,13 @@ impl App {
.await
.map_err(|e| e.to_string())
},
|result| match result {
Ok((session, _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 {
@@ -229,18 +240,32 @@ impl App {
},
)
}
Message::Connected(client_id) => {
Message::Connected(_client_id) => {
self.connected = true;
tracing::info!("Connected as client {client_id}");
Task::none()
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();
self.connected = false;
self.server_info = None;
self.channels.clear();
self.clients.clear();
self.messages.clear();
Task::none()
self.session_id += 1;
Task::perform(async move {
let mut guard = h.lock().await;
if let Some(ref handle) = *guard {
let _ = handle.disconnect().await;
}
*guard = None;
}, |_| Message::Disconnected)
}
Message::Disconnected => {
self.connected = false;
@@ -254,9 +279,11 @@ impl App {
self.error = Some(err);
Task::none()
}
Message::PollEvents => 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 });
}
@@ -297,8 +324,21 @@ impl App {
Task::none()
}
Message::SendChannelMessage => {
let msg = self.message_input.clone();
self.message_input.clear();
Task::none()
if msg.is_empty() {
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());
}
}
Message::Noop
}, |m| m)
}
Message::QueryAddressChanged(addr) => {
self.query_address = addr;
@@ -309,8 +349,37 @@ impl App {
Task::none()
}
Message::RunServerQuery => {
if self.query_address.is_empty() {
self.query_error = Some("Address required".to_string());
return Task::none();
}
self.query_loading = true;
self.query_error = None;
self.query_response = None;
let addr = format!("{}:{}", self.query_address, self.query_port);
Task::perform(
async move {
use tscore::QueryClient;
let mut client = QueryClient::connect(&addr).await
.map_err(|e| format!("Connect: {e}"))?;
let resp = client.execute("help").await
.map_err(|e| format!("Query: {e}"))?;
Ok::<String, String>(resp.raw)
},
|result| match result {
Ok(raw) => Message::QueryResponse(raw),
Err(e) => Message::QueryError(e),
},
)
}
Message::QueryResponse(raw) => {
self.query_loading = false;
self.query_response = Some(raw);
Task::none()
}
Message::QueryError(e) => {
self.query_loading = false;
self.query_error = Some(e);
Task::none()
}
Message::Noop => Task::none(),
@@ -318,7 +387,37 @@ impl App {
}
fn subscription(&self) -> Subscription<Message> {
Subscription::none()
if !self.connected {
return Subscription::none();
}
let handle = self.handle.clone();
let session_id = self.session_id;
Subscription::run_with_id(
session_id,
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,
}
};
match event {
Some(event) => {
let is_disconnect = matches!(event, SessionEvent::Disconnected);
if sender.send(Message::SessionEvent(event)).await.is_err() {
break;
}
if is_disconnect {
break;
}
}
None => break,
}
}
}),
)
}
fn view(&self) -> Element<'_, Message> {
@@ -398,7 +497,7 @@ impl App {
fn view_server_content(&self) -> Element<'_, Message> {
if let Some(idx) = self.selected_bookmark {
if let Some(bookmark) = self.bookmarks.get(idx) {
let connect_form = column![
let mut connect_form = column![
text(format!("Connect to {}", bookmark.name)).size(20),
vertical_space().height(12),
row![
@@ -435,6 +534,11 @@ 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.connected {
let server_info = if let Some(info) = &self.server_info {
@@ -516,7 +620,7 @@ impl App {
}
fn view_query_content(&self) -> Element<'_, Message> {
column![
let mut content = column![
text("ServerQuery").size(20),
vertical_space().height(12),
row![
@@ -545,8 +649,22 @@ impl App {
],
]
.spacing(4)
.padding(16)
.into()
.padding(16);
if let Some(resp) = &self.query_response {
content = content.push(
column![
text("Response:").size(14),
scrollable(text(resp).size(12)).height(300),
]
.spacing(4),
);
}
if let Some(err) = &self.query_error {
content = content.push(text(format!("Error: {err}")).size(12));
}
content.into()
}
fn view_settings_content(&self) -> Element<'_, Message> {