diff --git a/src/app/datasource.rs b/src/app/datasource.rs index 3712a7a..4b32411 100644 --- a/src/app/datasource.rs +++ b/src/app/datasource.rs @@ -1,30 +1,78 @@ use std::sync::{Arc, Mutex}; use rand::Rng; +use std::io::{Write, Read}; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use eframe::egui::plot::Value; +use serde::{Deserialize, Serialize, de::{DeserializeOwned}}; +use eframe::egui::{plot::Value, Context}; -struct DataSource { +pub fn native_save(name: &str, data:String) -> std::io::Result<()> { + let mut file = std::fs::File::create(name)?; + file.write_all(data.as_bytes())?; + return Ok(()); +} + +pub struct DataSource { data : Arc>>, } +#[derive(Serialize, Deserialize)] +struct SerializableValue { + x : f64, + y : f64, +} + impl DataSource { - fn new() -> Self { + pub fn new() -> Self { Self{ data: Arc::new(Mutex::new(Vec::new())) } } - fn view(&self) -> Vec { // TODO handle errors + pub fn view(&self) -> Vec { // TODO handle errors return self.data.lock().unwrap().clone(); } + + pub fn serialize(&self) -> String { + let mut out : Vec = Vec::new(); + for value in self.view() { + out.push(SerializableValue { x: value.x, y: value.y }); + } + return serde_json::to_string(&out).unwrap(); + } +} + +pub trait PlotValue { + fn as_value(&self) -> Value; } pub trait Data { - fn load(&mut self, url:&str); - fn view(&self) -> Vec; + fn load_remote(&mut self, url:&str, ctx:Context); + fn load_local(&mut self, file:&str, ctx:Context); + + fn read(&mut self, file:&str, storage:Arc>>, ctx:Context) -> std::io::Result<()> { + let mut file = std::fs::File::open(file)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let data : Vec = serde_json::from_str(contents.as_str())?; + for v in data { + storage.lock().unwrap().push(Value { x: v.x, y: v.y }); + } + ctx.request_repaint(); + Ok(()) + } + + fn fetch(&mut self, base:&str, endpoint:&str, storage:Arc>>, ctx:Context) + where T : DeserializeOwned + PlotValue { + let request = ehttp::Request::get(format!("{}/{}", base, endpoint)); + ehttp::fetch(request, move |result: ehttp::Result| { + let data : T = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap(); + storage.lock().unwrap().push(data.as_value()); + ctx.request_repaint(); + }); + + } } pub struct TpsData { - ds: DataSource, + pub ds: DataSource, load_interval : i64, last_load : DateTime, } @@ -34,6 +82,12 @@ struct TpsResponseData { tps: f64 } +impl PlotValue for TpsResponseData { + fn as_value(&self) -> Value { + Value { x: Utc::now().timestamp() as f64, y: self.tps } + } +} + impl TpsData { pub fn new(load_interval:i64) -> Self { Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } @@ -41,22 +95,19 @@ impl TpsData { } impl Data for TpsData{ - fn load(&mut self, url:&str) { + fn load_remote(&mut self, url:&str, ctx:Context) { if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } self.last_load = Utc::now(); - let ds_data = self.ds.data.clone(); - let request = ehttp::Request::get(format!("{}/tps", url)); - ehttp::fetch(request, move |result: ehttp::Result| { - let data : TpsResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap(); - ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.tps}); - }); + self.fetch::(url, "tps", self.ds.data.clone(), ctx); } - fn view(&self) -> Vec { self.ds.view() } + fn load_local(&mut self, file:&str, ctx:Context) { + self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file)); + } } pub struct ChatData { - ds : DataSource, + pub ds : DataSource, load_interval : i64, last_load : DateTime, } @@ -66,6 +117,12 @@ struct ChatResponseData { volume: f64 } +impl PlotValue for ChatResponseData { + fn as_value(&self) -> Value { + Value { x:Utc::now().timestamp() as f64, y: self.volume } + } +} + impl ChatData { pub fn new(load_interval:i64) -> Self { Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } @@ -73,22 +130,19 @@ impl ChatData { } impl Data for ChatData{ - fn load(&mut self, url:&str) { + fn load_remote(&mut self, url:&str, ctx:Context) { if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } self.last_load = Utc::now(); - let ds_data = self.ds.data.clone(); - let request = ehttp::Request::get(format!("{}/chat_activity", url)); - ehttp::fetch(request, move |result: ehttp::Result| { - let data : ChatResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap(); - ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.volume}); - }); + self.fetch::(url, "chat_activity", self.ds.data.clone(), ctx); } - fn view(&self) -> Vec { self.ds.view() } + fn load_local(&mut self, file:&str, ctx:Context) { + self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file)); + } } pub struct PlayerCountData { - ds : DataSource, + pub ds : DataSource, load_interval : i64, last_load : DateTime, } @@ -98,6 +152,12 @@ struct PlayerCountResponseData { count: i32 } +impl PlotValue for PlayerCountResponseData { + fn as_value(&self) -> Value { + Value { x:Utc::now().timestamp() as f64, y: self.count as f64 } + } +} + impl PlayerCountData { pub fn new(load_interval:i64) -> Self { Self { ds: DataSource::new() , last_load: Utc::now(), load_interval } @@ -105,39 +165,38 @@ impl PlayerCountData { } impl Data for PlayerCountData{ - fn load(&mut self, url:&str) { + fn load_remote(&mut self, url:&str, ctx:Context) { if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } self.last_load = Utc::now(); - let ds_data = self.ds.data.clone(); - let request = ehttp::Request::get(format!("{}/chat_activity", url)); - ehttp::fetch(request, move |result: ehttp::Result| { - let data : PlayerCountResponseData = serde_json::from_slice(result.unwrap().bytes.as_slice()).unwrap(); - ds_data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:data.count as f64}); - }); + self.fetch::(url, "player_count", self.ds.data.clone(), ctx); } - fn view(&self) -> Vec { self.ds.view() } + fn load_local(&mut self, file:&str, ctx:Context) { + self.read(file, self.ds.data.clone(), ctx).unwrap_or_else(|_err| println!("Could not load {}", file)); + } } pub struct RandomData { - ds : DataSource, + pub ds : DataSource, load_interval : i64, last_load : DateTime, rng: rand::rngs::ThreadRng, } impl RandomData { + #[allow(dead_code)] pub fn new(load_interval:i64) -> Self { Self { ds: DataSource::new() , last_load: Utc::now(), load_interval, rng : rand::thread_rng() } } } impl Data for RandomData{ - fn load(&mut self, _url:&str) { + fn load_remote(&mut self, _url:&str, ctx:Context) { if (Utc::now() - self.last_load).num_seconds() < self.load_interval { return; } self.last_load = Utc::now(); self.ds.data.lock().unwrap().push(Value {x:Utc::now().timestamp() as f64, y:self.rng.gen()}); + ctx.request_repaint(); } - fn view(&self) -> Vec { self.ds.view() } + fn load_local(&mut self, _file:&str, _ctx:Context) {} } \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index a3b6158..0330c4c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,39 +1,102 @@ mod datasource; use chrono::{DateTime, NaiveDateTime, Utc}; -use datasource::{ChatData, PlayerCountData, TpsData, Data, RandomData}; +use datasource::{ChatData, PlayerCountData, TpsData, Data, native_save}; use eframe::egui; -use eframe::egui::plot::{Line, Plot, Value, Values}; - +use eframe::egui::plot::{Line, Plot, Values}; pub struct App { + servers : Vec, +} + +struct ServerOptions { + title: String, + url: String, player_count: PlayerCountData, tps: TpsData, chat: ChatData, - rand: RandomData, sync_time:bool, } impl App { pub fn new(_cc: &eframe::CreationContext) -> Self { + let mut servers = Vec::new(); + servers.push(ServerOptions::new("9b9t", "https://alemi.dev/mcbots/9b")); + servers.push(ServerOptions::new("const", "https://alemi.dev/mcbots/const")); + servers.push(ServerOptions::new("of", "https://alemi.dev/mcbots/of")); + Self { servers } + } +} + +impl ServerOptions { + fn new(title:&str, url:&str) -> Self { Self { + title: title.to_string(), + url: url.to_string(), player_count: PlayerCountData::new(60), - tps: TpsData::new(30), - chat: ChatData::new(15), - rand: RandomData::new(1), + tps: TpsData::new(15), + chat: ChatData::new(30), sync_time: false, } } + + fn display(&mut self, ui:&mut eframe::egui::Ui) { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.heading(self.title.as_str()); + ui.checkbox(&mut self.sync_time, "Lock X to now"); + }); + let mut p = Plot::new(format!("plot-{}", self.title)).x_axis_formatter(|x, _range| { + format!( + "{}", + DateTime::::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc) + .format("%Y/%m/%d %H:%M:%S") + ) + }).center_x_axis(false).height(260.0); // TODO make it fucking reactive! It fills the whole screen with 1 plot no matter what I do... + + if self.sync_time { + p = p.include_x(Utc::now().timestamp() as f64); + } + + p.show(ui, |plot_ui| { + plot_ui.line( + Line::new(Values::from_values(self.player_count.ds.view())).name("Player Count"), + ); + plot_ui.line(Line::new(Values::from_values(self.tps.ds.view())).name("TPS over 15s")); + plot_ui.line( + Line::new(Values::from_values(self.chat.ds.view())) + .name("Chat messages per minute"), + ); + }); + }); + } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - self.rand.load(""); + for server in &mut self.servers { + server.tps.load_remote(server.url.as_str(), ctx.clone()); + server.player_count.load_remote(server.url.as_str(), ctx.clone()); + server.chat.load_remote(server.url.as_str(), ctx.clone()); + } egui::TopBottomPanel::top("??? wtf").show(ctx, |ui| { ui.horizontal(|ui| { egui::widgets::global_dark_light_mode_switch(ui); ui.heading("nnbot dashboard"); - ui.checkbox(&mut self.sync_time, "Lock X to now"); + if ui.button("save").clicked() { + for server in &self.servers { + native_save(format!("{}-tps.json", server.title).as_str(), server.tps.ds.serialize()).unwrap(); + native_save(format!("{}-chat.json", server.title).as_str(), server.chat.ds.serialize()).unwrap(); + native_save(format!("{}-players.json", server.title).as_str(), server.player_count.ds.serialize()).unwrap(); + } + } + if ui.button("load").clicked() { + for server in &mut self.servers { + server.tps.load_local(format!("{}-tps.json", server.title).as_str(), ctx.clone()); + server.chat.load_local(format!("{}-chat.json", server.title).as_str(), ctx.clone()); + server.player_count.load_local(format!("{}-players.json", server.title).as_str(), ctx.clone()); + } + } ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { if ui.button("x").clicked() { frame.quit(); @@ -42,29 +105,16 @@ impl eframe::App for App { }); }); egui::CentralPanel::default().show(ctx, |ui| { - let mut p = Plot::new("test").x_axis_formatter(|x, _range| { - format!( - "{}", - DateTime::::from_utc(NaiveDateTime::from_timestamp(x as i64, 0), Utc) - .format("%Y/%m/%d %H:%M:%S") - ) - }).center_x_axis(false); - - if self.sync_time { - p = p.include_x(Utc::now().timestamp() as f64); - } - - p.show(ui, |plot_ui| { - plot_ui.line( - Line::new(Values::from_values(self.player_count.view())).name("Player Count"), - ); - plot_ui.line(Line::new(Values::from_values(self.tps.view())).name("TPS over 15s")); - plot_ui.line(Line::new(Values::from_values(self.rand.view())).name("Random Data")); - plot_ui.line( - Line::new(Values::from_values(self.chat.view())) - .name("Chat messages per minute"), - ); + ui.group(|v_ui| { + self.servers[0].display(v_ui); + }); + ui.group(|v_ui| { + self.servers[1].display(v_ui); + }); + ui.group(|v_ui| { + self.servers[2].display(v_ui); }); }); + ctx.request_repaint(); // TODO super jank way to sorta keep drawing } }