mirror of
https://github.com/ApfelTeeSaft/LiveSplitOne.git
synced 2026-08-26 19:23:40 +00:00
Added WS for Local "API"
Added Tokio to handle Websocket Requests from external programs, for example SpltNotes, did not have time to dig through the core, so someone else needs to finish this impkementation :p
This commit is contained in:
@@ -19,6 +19,10 @@ serde_json = "1"
|
||||
livesplit-core = { path = "../livesplit-core" }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.20"
|
||||
futures-util = "0.3"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
@@ -28,4 +32,4 @@ custom-protocol = ["tauri/custom-protocol"]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
strip = true
|
||||
+245
-6
@@ -2,22 +2,70 @@
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
net::SocketAddr,
|
||||
str::FromStr,
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use livesplit_core::{
|
||||
event::{CommandSink, Event, Result},
|
||||
hotkey::KeyCode,
|
||||
networking::server_protocol::Command,
|
||||
HotkeyConfig, HotkeySystem, TimeSpan, TimingMethod,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{Emitter, Manager, WebviewWindow};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum WebSocketMessage {
|
||||
#[serde(rename = "heartbeat")]
|
||||
Heartbeat {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "split")]
|
||||
Split {
|
||||
split_index: u32,
|
||||
split_name: String,
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "start")]
|
||||
Start {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "reset")]
|
||||
Reset {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "pause")]
|
||||
Pause {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "resume")]
|
||||
Resume {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "undo_split")]
|
||||
UndoSplit {
|
||||
timestamp: u64,
|
||||
},
|
||||
#[serde(rename = "skip_split")]
|
||||
SkipSplit {
|
||||
timestamp: u64,
|
||||
},
|
||||
}
|
||||
|
||||
struct State {
|
||||
hotkey_system: RwLock<Option<HotkeySystem<TauriCommandSink>>>,
|
||||
window: RwLock<Option<WebviewWindow>>,
|
||||
websocket_tx: broadcast::Sender<WebSocketMessage>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -69,12 +117,146 @@ fn settings_changed(state: tauri::State<'_, State>, always_on_top: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_websocket_server(
|
||||
state: tauri::State<'_, State>,
|
||||
port: u16,
|
||||
) -> Result<String, String> {
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
let listener = tokio::net::TcpListener::bind(&addr)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
|
||||
|
||||
let tx = state.websocket_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
println!("WebSocket server listening on: {}", addr);
|
||||
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(handle_connection(stream, addr, tx));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(format!("WebSocket server started on {}", addr))
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
stream: tokio::net::TcpStream,
|
||||
addr: SocketAddr,
|
||||
tx: broadcast::Sender<WebSocketMessage>,
|
||||
) {
|
||||
let ws_stream = match accept_async(stream).await {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
println!("WebSocket connection error from {}: {}", addr, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("New WebSocket connection from: {}", addr);
|
||||
|
||||
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
// Send initial heartbeat
|
||||
let heartbeat = WebSocketMessage::Heartbeat {
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
};
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&heartbeat) {
|
||||
if ws_sender.send(Message::Text(json)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn heartbeat task
|
||||
let mut heartbeat_sender = ws_sender.clone();
|
||||
let heartbeat_task = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let heartbeat = WebSocketMessage::Heartbeat {
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
};
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&heartbeat) {
|
||||
if heartbeat_sender.send(Message::Text(json)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle incoming messages and broadcast events
|
||||
let broadcast_task = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Handle incoming WebSocket messages (if any)
|
||||
msg = ws_receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(_))) => {
|
||||
// For now, we just acknowledge text messages
|
||||
// Could be used for client requests in the future
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => {
|
||||
println!("WebSocket connection closed: {}", addr);
|
||||
break;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
println!("WebSocket error from {}: {}", addr, e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Broadcast events to client
|
||||
event = rx.recv() => {
|
||||
match event {
|
||||
Ok(msg) => {
|
||||
if let Ok(json) = serde_json::to_string(&msg) {
|
||||
if ws_sender.send(Message::Text(json)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {
|
||||
// Skip lagged messages
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for either task to complete
|
||||
tokio::select! {
|
||||
_ = heartbeat_task => {},
|
||||
_ = broadcast_task => {},
|
||||
}
|
||||
|
||||
println!("WebSocket connection handler finished for: {}", addr);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TauriCommandSink(Arc<RwLock<Option<WebviewWindow>>>);
|
||||
struct TauriCommandSink {
|
||||
window: Arc<RwLock<Option<WebviewWindow>>>,
|
||||
websocket_tx: broadcast::Sender<WebSocketMessage>,
|
||||
}
|
||||
|
||||
impl TauriCommandSink {
|
||||
fn send(&self, command: Command) {
|
||||
self.0
|
||||
self.window
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
@@ -82,51 +264,98 @@ impl TauriCommandSink {
|
||||
.emit("command", command)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_websocket_event(&self, event: WebSocketMessage) {
|
||||
let _ = self.websocket_tx.send(event);
|
||||
}
|
||||
|
||||
fn get_timestamp() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandSink for TauriCommandSink {
|
||||
fn start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Start);
|
||||
self.send_websocket_event(WebSocketMessage::Start {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Split);
|
||||
|
||||
// For now, we'll use placeholder values for split info
|
||||
// In a real implementation, you'd get this from the timer state
|
||||
self.send_websocket_event(WebSocketMessage::Split {
|
||||
split_index: 0, // This should come from actual timer state
|
||||
split_name: "Split".to_string(), // This should come from actual split data
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn split_or_start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SplitOrStart);
|
||||
// This could be either a start or split, ideally you'd check timer state
|
||||
self.send_websocket_event(WebSocketMessage::Split {
|
||||
split_index: 0,
|
||||
split_name: "Split".to_string(),
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn reset(&self, save_attempt: Option<bool>) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Reset { save_attempt });
|
||||
self.send_websocket_event(WebSocketMessage::Reset {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn undo_split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::UndoSplit);
|
||||
self.send_websocket_event(WebSocketMessage::UndoSplit {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn skip_split(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::SkipSplit);
|
||||
self.send_websocket_event(WebSocketMessage::SkipSplit {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn toggle_pause_or_start(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::TogglePauseOrStart);
|
||||
// This could be either pause or resume, ideally you'd check timer state
|
||||
self.send_websocket_event(WebSocketMessage::Pause {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn pause(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Pause);
|
||||
self.send_websocket_event(WebSocketMessage::Pause {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
fn resume(&self) -> impl Future<Output = Result> + 'static {
|
||||
self.send(Command::Resume);
|
||||
self.send_websocket_event(WebSocketMessage::Resume {
|
||||
timestamp: Self::get_timestamp(),
|
||||
});
|
||||
async { Ok(Event::Unknown) }
|
||||
}
|
||||
|
||||
@@ -203,15 +432,24 @@ impl CommandSink for TauriCommandSink {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sink = TauriCommandSink(Arc::new(RwLock::new(None)));
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let (websocket_tx, _) = broadcast::channel(100);
|
||||
|
||||
let sink = TauriCommandSink {
|
||||
window: Arc::new(RwLock::new(None)),
|
||||
websocket_tx: websocket_tx.clone(),
|
||||
};
|
||||
|
||||
let hotkey_system = RwLock::new(HotkeySystem::new(sink.clone()).ok());
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(State {
|
||||
hotkey_system,
|
||||
window: RwLock::new(None),
|
||||
websocket_tx,
|
||||
})
|
||||
.setup(move |app| {
|
||||
let main_window = app.webview_windows().values().next().unwrap().clone();
|
||||
@@ -220,7 +458,7 @@ fn main() {
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace(main_window.clone());
|
||||
*sink.0.write().unwrap() = Some(main_window);
|
||||
*sink.window.write().unwrap() = Some(main_window);
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -229,7 +467,8 @@ fn main() {
|
||||
get_hotkey_config,
|
||||
resolve_hotkey,
|
||||
settings_changed,
|
||||
start_websocket_server,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user