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:
+141
-23
@@ -1,14 +1,14 @@
|
|||||||
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::Mutex;
|
||||||
|
|
||||||
use tscore::{ClientConfig, IdentityKey, Session, SessionEvent};
|
use tscore::{ClientConfig, IdentityKey, Session, SessionEvent, SessionHandle};
|
||||||
use tsdb::DatabaseManager;
|
use tsdb::DatabaseManager;
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
iced::application("ReTeamSpeak", App::update, App::view)
|
iced::application("ReTeamSpeak", App::update, App::view)
|
||||||
.subscription(App::subscription)
|
.subscription(App::subscription)
|
||||||
.run_with(App::new)
|
.run_with(App::new)
|
||||||
@@ -24,8 +24,6 @@ enum Page {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
PageChanged(Page),
|
PageChanged(Page),
|
||||||
LoadBookmarks,
|
|
||||||
BookmarksLoaded(Vec<BookmarkInfo>),
|
|
||||||
SelectBookmark(Option<usize>),
|
SelectBookmark(Option<usize>),
|
||||||
NicknameChanged(String),
|
NicknameChanged(String),
|
||||||
PasswordChanged(String),
|
PasswordChanged(String),
|
||||||
@@ -34,19 +32,19 @@ enum Message {
|
|||||||
Disconnect,
|
Disconnect,
|
||||||
Disconnected,
|
Disconnected,
|
||||||
ConnectionError(String),
|
ConnectionError(String),
|
||||||
PollEvents,
|
|
||||||
SessionEvent(SessionEvent),
|
SessionEvent(SessionEvent),
|
||||||
MessageInputChanged(String),
|
MessageInputChanged(String),
|
||||||
SendChannelMessage,
|
SendChannelMessage,
|
||||||
QueryAddressChanged(String),
|
QueryAddressChanged(String),
|
||||||
QueryPortChanged(String),
|
QueryPortChanged(String),
|
||||||
RunServerQuery,
|
RunServerQuery,
|
||||||
|
QueryResponse(String),
|
||||||
|
QueryError(String),
|
||||||
Noop,
|
Noop,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct BookmarkInfo {
|
struct BookmarkInfo {
|
||||||
id: String,
|
|
||||||
name: String,
|
name: String,
|
||||||
address: String,
|
address: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -55,8 +53,10 @@ struct BookmarkInfo {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ChannelEntry {
|
struct ChannelEntry {
|
||||||
|
#[allow(dead_code)]
|
||||||
cid: u64,
|
cid: u64,
|
||||||
channel_name: String,
|
channel_name: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
total_clients: u16,
|
total_clients: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +83,15 @@ struct ChatMessage {
|
|||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
page: Page,
|
page: Page,
|
||||||
|
#[allow(dead_code)]
|
||||||
db: Arc<Mutex<DatabaseManager>>,
|
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>>>,
|
||||||
|
session_id: u64,
|
||||||
server_info: Option<ServerInfo>,
|
server_info: Option<ServerInfo>,
|
||||||
channels: Vec<ChannelEntry>,
|
channels: Vec<ChannelEntry>,
|
||||||
clients: Vec<ClientEntry>,
|
clients: Vec<ClientEntry>,
|
||||||
@@ -97,6 +100,7 @@ struct App {
|
|||||||
query_address: String,
|
query_address: String,
|
||||||
query_port: u16,
|
query_port: u16,
|
||||||
query_loading: bool,
|
query_loading: bool,
|
||||||
|
query_response: Option<String>,
|
||||||
query_error: Option<String>,
|
query_error: Option<String>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -112,14 +116,12 @@ impl App {
|
|||||||
db: Arc::new(Mutex::new(db)),
|
db: Arc::new(Mutex::new(db)),
|
||||||
bookmarks: vec![
|
bookmarks: vec![
|
||||||
BookmarkInfo {
|
BookmarkInfo {
|
||||||
id: "1".to_string(),
|
|
||||||
name: "Local Server".to_string(),
|
name: "Local Server".to_string(),
|
||||||
address: "127.0.0.1".to_string(),
|
address: "127.0.0.1".to_string(),
|
||||||
port: 9987,
|
port: 9987,
|
||||||
nickname: Some("User".to_string()),
|
nickname: Some("User".to_string()),
|
||||||
},
|
},
|
||||||
BookmarkInfo {
|
BookmarkInfo {
|
||||||
id: "2".to_string(),
|
|
||||||
name: "KR TeamSpeak".to_string(),
|
name: "KR TeamSpeak".to_string(),
|
||||||
address: "kr.teamspeak.app".to_string(),
|
address: "kr.teamspeak.app".to_string(),
|
||||||
port: 9987,
|
port: 9987,
|
||||||
@@ -130,6 +132,8 @@ impl App {
|
|||||||
nickname: String::new(),
|
nickname: String::new(),
|
||||||
password: String::new(),
|
password: String::new(),
|
||||||
connected: false,
|
connected: false,
|
||||||
|
handle: Arc::new(Mutex::new(None)),
|
||||||
|
session_id: 0,
|
||||||
server_info: None,
|
server_info: None,
|
||||||
channels: Vec::new(),
|
channels: Vec::new(),
|
||||||
clients: Vec::new(),
|
clients: Vec::new(),
|
||||||
@@ -138,6 +142,7 @@ impl App {
|
|||||||
query_address: String::new(),
|
query_address: String::new(),
|
||||||
query_port: 10011,
|
query_port: 10011,
|
||||||
query_loading: false,
|
query_loading: false,
|
||||||
|
query_response: None,
|
||||||
query_error: None,
|
query_error: None,
|
||||||
error: None,
|
error: None,
|
||||||
};
|
};
|
||||||
@@ -151,8 +156,6 @@ impl App {
|
|||||||
self.page = page;
|
self.page = page;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::LoadBookmarks => Task::none(),
|
|
||||||
Message::BookmarksLoaded(_) => Task::none(),
|
|
||||||
Message::SelectBookmark(idx) => {
|
Message::SelectBookmark(idx) => {
|
||||||
self.selected_bookmark = idx;
|
self.selected_bookmark = idx;
|
||||||
if let Some(i) = idx {
|
if let Some(i) = idx {
|
||||||
@@ -187,6 +190,10 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
Some(self.password.clone())
|
Some(self.password.clone())
|
||||||
};
|
};
|
||||||
|
let handle_store = self.handle.clone();
|
||||||
|
|
||||||
|
self.session_id += 1;
|
||||||
|
self.error = None;
|
||||||
|
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
@@ -214,9 +221,13 @@ impl App {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
},
|
},
|
||||||
|result| match result {
|
move |result| match result {
|
||||||
Ok((session, _handle)) => {
|
Ok((session, handle)) => {
|
||||||
let client_id = session.client_id().unwrap_or(0);
|
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 {
|
tokio::spawn(async move {
|
||||||
let mut session = session;
|
let mut session = session;
|
||||||
if let Err(e) = session.run().await {
|
if let Err(e) = session.run().await {
|
||||||
@@ -229,18 +240,32 @@ impl App {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Message::Connected(client_id) => {
|
Message::Connected(_client_id) => {
|
||||||
self.connected = true;
|
self.connected = true;
|
||||||
tracing::info!("Connected as client {client_id}");
|
let h = self.handle.clone();
|
||||||
Task::none()
|
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();
|
||||||
self.connected = false;
|
self.connected = false;
|
||||||
self.server_info = None;
|
self.server_info = None;
|
||||||
self.channels.clear();
|
self.channels.clear();
|
||||||
self.clients.clear();
|
self.clients.clear();
|
||||||
self.messages.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 => {
|
Message::Disconnected => {
|
||||||
self.connected = false;
|
self.connected = false;
|
||||||
@@ -254,9 +279,11 @@ impl App {
|
|||||||
self.error = Some(err);
|
self.error = Some(err);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::PollEvents => Task::none(),
|
|
||||||
Message::SessionEvent(event) => {
|
Message::SessionEvent(event) => {
|
||||||
match event {
|
match event {
|
||||||
|
SessionEvent::Connected { client_id } => {
|
||||||
|
tracing::info!("Connected as client {client_id}");
|
||||||
|
}
|
||||||
SessionEvent::ServerInfo { name, platform, version, max_clients, clients_online, .. } => {
|
SessionEvent::ServerInfo { name, platform, version, max_clients, clients_online, .. } => {
|
||||||
self.server_info = Some(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()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::SendChannelMessage => {
|
Message::SendChannelMessage => {
|
||||||
|
let msg = self.message_input.clone();
|
||||||
self.message_input.clear();
|
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) => {
|
Message::QueryAddressChanged(addr) => {
|
||||||
self.query_address = addr;
|
self.query_address = addr;
|
||||||
@@ -309,8 +349,37 @@ impl App {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::RunServerQuery => {
|
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_loading = true;
|
||||||
self.query_error = None;
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Noop => Task::none(),
|
Message::Noop => Task::none(),
|
||||||
@@ -318,7 +387,37 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
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> {
|
fn view(&self) -> Element<'_, Message> {
|
||||||
@@ -398,7 +497,7 @@ impl App {
|
|||||||
fn view_server_content(&self) -> Element<'_, Message> {
|
fn view_server_content(&self) -> Element<'_, Message> {
|
||||||
if let Some(idx) = self.selected_bookmark {
|
if let Some(idx) = self.selected_bookmark {
|
||||||
if let Some(bookmark) = self.bookmarks.get(idx) {
|
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),
|
text(format!("Connect to {}", bookmark.name)).size(20),
|
||||||
vertical_space().height(12),
|
vertical_space().height(12),
|
||||||
row![
|
row![
|
||||||
@@ -435,6 +534,11 @@ impl App {
|
|||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.padding(16);
|
.padding(16);
|
||||||
|
if let Some(err) = &self.error {
|
||||||
|
connect_form = connect_form.push(
|
||||||
|
text(format!("Error: {err}")).size(12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if self.connected {
|
if self.connected {
|
||||||
let server_info = if let Some(info) = &self.server_info {
|
let server_info = if let Some(info) = &self.server_info {
|
||||||
@@ -516,7 +620,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view_query_content(&self) -> Element<'_, Message> {
|
fn view_query_content(&self) -> Element<'_, Message> {
|
||||||
column![
|
let mut content = column![
|
||||||
text("ServerQuery").size(20),
|
text("ServerQuery").size(20),
|
||||||
vertical_space().height(12),
|
vertical_space().height(12),
|
||||||
row![
|
row![
|
||||||
@@ -545,8 +649,22 @@ impl App {
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.padding(16)
|
.padding(16);
|
||||||
.into()
|
|
||||||
|
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> {
|
fn view_settings_content(&self) -> Element<'_, Message> {
|
||||||
|
|||||||
Reference in New Issue
Block a user