diff --git a/src/iced-app/src/main.rs b/src/iced-app/src/main.rs index 534cf7f..347c065 100644 --- a/src/iced-app/src/main.rs +++ b/src/iced-app/src/main.rs @@ -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), SelectBookmark(Option), 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>, bookmarks: Vec, selected_bookmark: Option, nickname: String, password: String, connected: bool, + handle: Arc>>, + session_id: u64, server_info: Option, channels: Vec, clients: Vec, @@ -97,6 +100,7 @@ struct App { query_address: String, query_port: u16, query_loading: bool, + query_response: Option, query_error: Option, error: Option, } @@ -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::(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 { - 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> {