Compare commits

...

4 Commits

10 changed files with 1497 additions and 1539 deletions

2451
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
calamine = "0.18.0"
chrono = { version = "0.4.23", features = ["wasmbind"] }
console_error_panic_hook = "0.1.7"
fast-float = "0.2.0"
@ -16,24 +15,12 @@ plotly = { version = "0.8.1", features = ["wasm"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5.9"
indicatif = "0.17.3"
dialoguer = "0.10.3"
notify-rust = "4.7.0"
crossterm = "0.25.0"
rfd = "0.10.0"
egui = { git = "https://github.com/emilk/egui", branch = "master" }
eframe = { git = "https://github.com/emilk/egui", branch = "master" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen-futures = "0.4"
dioxus = "0.3.2"
dioxus-web = "0.3.2"
[profile.release]
opt-level = 2 # fast and small wasm

View File

@ -37,25 +37,9 @@ pub struct Config {
pub height: u32,
}
pub const WGHT_SHEET: usize = 0;
pub const FLOW_SHEET: usize = 1;
pub const TIME_COL: usize = 0;
pub const WGHT_COL: usize = 5;
pub const FLOW_COL: usize = 2;
pub struct Data {
pub weight: Vec<(f64, f64)>,
pub flowrate: Vec<(f64, f64)>,
}
impl Config {
pub fn from_file(path: &str) -> Self {
let config_file = fs::read_to_string(&path).expect("Can't read config.toml");
toml::from_str(&config_file).expect("Can't deserialize config.toml")
}
pub fn from_default() -> Self {
Self::from_file("config.toml")
}
}

View File

@ -74,6 +74,7 @@ impl Database {
Database::from_file(&format!("{}/{}", &config.data_dir, &config.main_json))
}
#[allow(dead_code)]
pub fn bean_names(&self) -> Vec<String> {
self.beans
.iter()
@ -81,6 +82,7 @@ impl Database {
.collect()
}
#[allow(dead_code)]
pub fn bean_names_with_uuids(&self) -> Vec<(String, String)> {
self.beans
.iter()
@ -94,12 +96,14 @@ impl Database {
.find(|bean| bean.config.uuid == uuid.to_owned())
}
#[allow(dead_code)]
pub fn brew_for_uuid(&self, uuid: &str) -> Option<&Brew> {
self.brews
.iter()
.find(|brew| brew.config.uuid == uuid.to_owned())
}
#[allow(dead_code)]
pub fn brews_for_uuids(&self, uuids: &Vec<String>) -> Vec<&Brew> {
self.brews
.iter()
@ -110,6 +114,7 @@ impl Database {
.collect()
}
#[allow(dead_code)]
pub fn bean_for_brew(&self, brew: &Brew) -> Option<&Bean> {
self.beans
.iter()
@ -137,12 +142,13 @@ impl Bean {
}
impl Brew {
#[allow(dead_code)]
pub fn time(&self) -> String {
unix_to_human_time(self.config.unix_timestamp.to_owned())
}
pub fn date_time(&self, database: &Database) -> String {
if let Some(bean) = &database.bean_for_uuid(&self.bean) {
if let Some(_bean) = &database.bean_for_uuid(&self.bean) {
unix_to_human_date_time(self.config.unix_timestamp.to_owned())
} else {
String::default()

View File

@ -66,6 +66,7 @@ impl FlowProfile {
brew
}
#[allow(dead_code)]
pub fn preprocess_json_mut(&mut self) {
self.data_collection = self.process_json();
}

View File

@ -2,35 +2,21 @@ mod config;
mod database;
mod flow_profile;
mod plot;
mod sheets;
mod time;
mod ui;
use ui::Ui;
use crate::plot::database_plot_selected_tui;
use crate::plot::generate_plots;
use dioxus::prelude::*;
extern crate console_error_panic_hook;
use std::panic;
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
panic::set_hook(Box::new(console_error_panic_hook::hook));
// generate_plots();
// database_plot_selected_tui();
let options = eframe::NativeOptions {
drag_and_drop_support: true,
min_window_size: Some(egui::vec2(640.0, 360.0)),
initial_window_size: Some(egui::vec2(640.0, 360.0)),
..Default::default()
};
eframe::run_native(
"RustyBeans",
options,
Box::new(|_cc| Box::new(Ui::default())),
)
fn main() {
dioxus_web::launch(test);
}
fn test(cx: Scope) -> Element {
cx.render(rsx! {
div {
"Hello, world!"
}
})
}

View File

@ -1,23 +1,15 @@
use crate::config::Config;
use crate::database::{Brew, Database};
use crate::flow_profile::FlowProfile;
use crate::sheets::load_data;
use crate::time::{unix_to_human_date, unix_to_human_date_time, unix_to_machine_date};
use dialoguer::{theme::ColorfulTheme, MultiSelect};
use egui::plot::{PlotPoint, PlotPoints};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use notify_rust::Notification;
use plotly::{
common::{Mode, Title},
Layout, Plot, Scatter,
};
use std::collections::HashMap;
use std::fs;
use std::sync::{Arc, Mutex};
pub type RawPlotPoints = Vec<PlotPoint>;
@ -31,72 +23,6 @@ pub struct LoadingProgress {
pub percentage: f32,
}
pub fn generate_plots() -> Vec<(String, Plot)> {
let config_file = fs::read_to_string("config.toml").expect("Can't read config.toml");
let config: Config = toml::from_str(&config_file).expect("Can't deserialize config.toml");
let mut result: Vec<(String, Plot)> = Vec::with_capacity(config.charts.len());
for chart in config.charts {
// println!("Chart: {}\n", chart.1.title);
let filename = format!("{}/{}.html", &config.output_dir, &chart.1.title);
let mut plot = Plot::new();
let _shot_count = chart.1.shots.len();
for shot_nr in chart.1.shots {
if let Some(shot) = config.shots.get(&shot_nr.to_string()) {
// println!("\tShot: {}n", shot.title);
if let Some(shot_json) = &shot.json {
let brew = FlowProfile::from_file(
&format!("{}/{}_flow_profile.json", config.brew_dir, shot_json),
shot.cutoff,
)
.preprocess_json();
let (x, y): (Vec<_>, Vec<_>) = brew
.data_collection
.unwrap_or_else(|| {
panic!("No data_collection present for shot_json: {}", shot_json)
})
.weight
.iter()
.cloned()
.unzip();
let trace = Scatter::new(x, y).name(&shot.title).mode(Mode::Lines);
plot.add_trace(trace);
} else if let Some(shot_filename) = &shot.filename {
if let Some(data) = load_data(
&format!("{}/{}", config.shot_dir, shot_filename),
shot.cutoff,
) {
if let Some(disable) = shot.disable {
if disable {
continue;
}
}
let (x, y): (Vec<_>, Vec<_>) = data.weight.into_iter().unzip();
let trace = Scatter::new(x, y).name(&shot.title).mode(Mode::Lines);
plot.add_trace(trace);
}
}
}
}
let layout = Layout::new().title(Title::new(&chart.1.title));
plot.set_layout(layout);
plot.use_local_plotly();
plot.write_html(filename);
result.push((chart.1.title, plot));
}
result
}
pub fn plot_points_to_owned(plot_points: &RawPlotPoints) -> PlotPoints {
PlotPoints::Owned(plot_points.to_owned())
}
@ -152,190 +78,74 @@ pub fn database_plot_entries(
plot_entries
}
pub fn database_plot_selected(uuids: Vec<String>, config: &Config) -> Vec<(String, Plot)> {
let mut result: Vec<(String, Plot)> = Vec::with_capacity(config.charts.len());
let database = Database::from_config(&config);
let bean_names = database.bean_names();
if !uuids.is_empty() {
for single_selection in uuids {
if let Some(bean) = &database.bean_for_uuid(&single_selection) {
let bean_timestamp = bean.config.unix_timestamp.to_owned();
let title = format!("{} from {}", &bean.name, unix_to_human_date(bean_timestamp));
let filename = format!(
"{}/{}_from_{}.html",
&config.output_dir,
&bean.name,
unix_to_machine_date(bean_timestamp)
)
.replace(" ", "_");
let mut plot = Plot::new();
let brews = &database.brews_for_bean(&bean);
for brew in brews {
if let Some(flow_profile) = &brew.flow_profile {
if !&flow_profile.is_empty() {
let brew_title =
unix_to_human_date_time(brew.config.unix_timestamp.to_owned());
let brew = FlowProfile::from_file(
&format!("{}/{}", &config.data_dir, &flow_profile),
None,
)
.preprocess_json();
let (x, y): (Vec<_>, Vec<_>) = brew
.data_collection
.unwrap_or_else(|| {
panic!(
"No data_collection present for flow_profile: {}",
&flow_profile
)
})
.weight
.iter()
.cloned()
.unzip();
let trace = Scatter::new(x, y).name(&brew_title).mode(Mode::Lines);
plot.add_trace(trace);
// progress_brews.inc(..);
}
}
}
let layout = Layout::new().title(Title::new(&title));
plot.set_layout(layout);
plot.use_local_plotly();
plot.write_html(filename);
result.push((title.to_owned(), plot));
}
// progress_beans.inc(..);
}
}
Notification::new()
.summary("RustyBeans finished")
.body("Successfully generated all selected bean charts automatically, according to the database.")
.timeout(5000)
.show()
.expect("Couldn't show desktop notification");
result
}
pub fn database_plot_selected_tui() -> Vec<(String, Plot)> {
let config = Config::from_default();
let mut result: Vec<(String, Plot)> = Vec::with_capacity(config.charts.len());
let database = Database::from_config(&config);
let bean_names = database.bean_names();
let selection = MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select the Beans you want to automatically generate charts for:")
.items(&bean_names[..])
.interact()
.expect("You need to select at least one bean to proceed");
if !selection.is_empty() {
let multi_progress = MultiProgress::new();
multi_progress.println("Generating brew charts.\nCheck the specified output directory and open the corresponding .html files to view.").unwrap();
let progress_style = ProgressStyle::with_template(
"[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}",
)
.expect("Can't generate progress bar style");
let progress_beans = multi_progress.add(
ProgressBar::new(selection.len() as u64)
.with_style(progress_style.to_owned())
.with_message("Beans"),
);
let mut progress_brews: ProgressBar = ProgressBar::new(0);
for single_selection in selection {
if let Some(bean) = &database.beans.get(single_selection) {
let bean_timestamp = bean.config.unix_timestamp.to_owned();
let title = format!("{} from {}", &bean.name, unix_to_human_date(bean_timestamp));
let filename = format!(
"{}/{}_from_{}.html",
&config.output_dir,
&bean.name,
unix_to_machine_date(bean_timestamp)
)
.replace(" ", "_");
let mut plot = Plot::new();
let brews = &database.brews_for_bean(&bean);
progress_brews = multi_progress.insert_after(
&progress_beans,
ProgressBar::new(brews.len() as u64)
.with_style(progress_style.to_owned())
.with_message("Brews"),
);
for brew in brews {
if let Some(flow_profile) = &brew.flow_profile {
if !&flow_profile.is_empty() {
let brew_title =
unix_to_human_date_time(brew.config.unix_timestamp.to_owned());
let brew = FlowProfile::from_file(
&format!("{}/{}", &config.data_dir, &flow_profile),
None,
)
.preprocess_json();
let (x, y): (Vec<_>, Vec<_>) = brew
.data_collection
.unwrap_or_else(|| {
panic!(
"No data_collection present for flow_profile: {}",
&flow_profile
)
})
.weight
.iter()
.cloned()
.unzip();
let trace = Scatter::new(x, y).name(&brew_title).mode(Mode::Lines);
plot.add_trace(trace);
progress_brews.inc(1);
}
}
}
let layout = Layout::new().title(Title::new(&title));
plot.set_layout(layout);
plot.use_local_plotly();
plot.write_html(filename);
result.push((title.to_owned(), plot));
}
progress_beans.inc(1);
multi_progress.remove(&progress_brews);
}
}
Notification::new()
.summary("RustyBeans finished")
.body("Successfully generated all selected bean charts automatically, according to the database.")
.timeout(5000)
.show()
.expect("Couldn't show desktop notification");
result
}
// pub fn database_plot_selected(uuids: Vec<String>, config: &Config) -> Vec<(String, Plot)> {
// let mut result: Vec<(String, Plot)> = Vec::with_capacity(config.charts.len());
//
// let database = Database::from_config(&config);
//
// let bean_names = database.bean_names();
//
// if !uuids.is_empty() {
// for single_selection in uuids {
// if let Some(bean) = &database.bean_for_uuid(&single_selection) {
// let bean_timestamp = bean.config.unix_timestamp.to_owned();
//
// let title = format!("{} from {}", &bean.name, unix_to_human_date(bean_timestamp));
// let filename = format!(
// "{}/{}_from_{}.html",
// &config.output_dir,
// &bean.name,
// unix_to_machine_date(bean_timestamp)
// )
// .replace(" ", "_");
// let mut plot = Plot::new();
//
// let brews = &database.brews_for_bean(&bean);
//
// for brew in brews {
// if let Some(flow_profile) = &brew.flow_profile {
// if !&flow_profile.is_empty() {
// let brew_title =
// unix_to_human_date_time(brew.config.unix_timestamp.to_owned());
//
// let brew = FlowProfile::from_file(
// &format!("{}/{}", &config.data_dir, &flow_profile),
// None,
// )
// .preprocess_json();
//
// let (x, y): (Vec<_>, Vec<_>) = brew
// .data_collection
// .unwrap_or_else(|| {
// panic!(
// "No data_collection present for flow_profile: {}",
// &flow_profile
// )
// })
// .weight
// .iter()
// .cloned()
// .unzip();
// let trace = Scatter::new(x, y).name(&brew_title).mode(Mode::Lines);
// plot.add_trace(trace);
//
// // progress_brews.inc(..);
// }
// }
// }
//
// let layout = Layout::new().title(Title::new(&title));
// plot.set_layout(layout);
//
// plot.use_local_plotly();
// plot.write_html(filename);
//
// result.push((title.to_owned(), plot));
// }
//
// // progress_beans.inc(..);
// }
// }
//
// result
// }

View File

@ -1,75 +0,0 @@
use crate::config::{Data, FLOW_COL, FLOW_SHEET, TIME_COL, WGHT_COL, WGHT_SHEET};
use crate::time::{cell_to_naivetime, deltatime};
use calamine::{open_workbook, Reader, Xlsx};
use chrono::NaiveTime;
pub fn process_sheet(
path: &str,
worksheet: usize,
time_col: usize,
data_col: usize,
) -> Vec<(f64, f64)> {
let mut workbook: Xlsx<_> =
open_workbook(path).unwrap_or_else(|_| panic!("Cannot open file at path \"{}\"", path));
if let Some(Ok(range)) = workbook.worksheet_range_at(worksheet) {
let starting_time: NaiveTime = cell_to_naivetime(range[(1, time_col)].get_string());
let time_range = range.range(
(1, time_col as u32),
(range.height() as u32 - 1, time_col as u32),
);
let weight_range = range.range(
(1, data_col as u32),
(range.height() as u32 - 1, data_col as u32),
);
// println!("time column cells: {:?}", time_range.cells().next());
// println!("time column strings: {:?}", time_range.cells().map(|c| c.2.get_string().unwrap()).collect::<Vec<&str>>());
let map_time_range = time_range.cells().map(|c| {
let timestamp = cell_to_naivetime(c.2.get_string());
let deltatime = deltatime(timestamp, starting_time);
let std_duration = deltatime.to_std().unwrap();
std_duration.as_secs_f32() as f64
});
let map_weight_range = weight_range.cells().map(|c| {
c.2.get_float().unwrap_or_else(|| {
panic!(
"Can't get float value of weight column at position ({},{})",
c.0, c.1
)
})
});
map_time_range.zip(map_weight_range).collect()
} else {
vec![]
}
}
pub fn load_data(path: &str, cutoff: Option<f64>) -> Option<Data> {
let mut w = process_sheet(path, WGHT_SHEET, TIME_COL, WGHT_COL);
let mut fr = process_sheet(path, FLOW_SHEET, TIME_COL, FLOW_COL);
if let Some(cutoff_val) = cutoff {
if cutoff_val != -1.0 {
w.retain(|x| x.0 < cutoff_val);
fr.retain(|x| x.0 < cutoff_val);
}
}
if !w.is_empty() && !fr.is_empty() {
let data = Data {
weight: w,
flowrate: fr,
};
Some(data)
} else {
None
}
}

View File

@ -1,17 +1,16 @@
use chrono::{Duration, NaiveDateTime, NaiveTime};
#[allow(dead_code)]
pub fn str_to_naivetime(unix_str: &str) -> NaiveTime {
NaiveTime::parse_from_str(unix_str, "%T%.3f").expect("Couldn't parse timestamp")
}
pub fn cell_to_naivetime(cell: Option<&str>) -> NaiveTime {
str_to_naivetime(cell.expect("Timestamp is not a string!"))
}
#[allow(dead_code)]
pub fn deltatime(time: NaiveTime, start: NaiveTime) -> Duration {
time - start
}
#[allow(dead_code)]
pub fn unix_to_naivetime(unix_timestamp: i64) -> Option<NaiveTime> {
if let Some(date_time) = NaiveDateTime::from_timestamp_millis(unix_timestamp) {
Some(date_time.time())
@ -20,10 +19,12 @@ pub fn unix_to_naivetime(unix_timestamp: i64) -> Option<NaiveTime> {
}
}
#[allow(dead_code)]
pub fn is_same_day(time_1: NaiveTime, time_2: NaiveTime) -> bool {
time_1.format("%Y:%m:%d").to_string() == time_2.format("%Y:%m:%d").to_string()
}
#[allow(dead_code)]
pub fn unix_to_human_date_time(unix_timestamp: i64) -> String {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
date_time.format("%b %d, %Y %I:%M %P").to_string()
@ -32,6 +33,7 @@ pub fn unix_to_human_date_time(unix_timestamp: i64) -> String {
}
}
#[allow(dead_code)]
pub fn unix_to_human_date(unix_timestamp: i64) -> String {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
date_time.format("%b %d, %Y").to_string()
@ -40,6 +42,7 @@ pub fn unix_to_human_date(unix_timestamp: i64) -> String {
}
}
#[allow(dead_code)]
pub fn unix_to_human_time(unix_timestamp: i64) -> String {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
date_time.format("%I:%M %P").to_string()
@ -48,6 +51,7 @@ pub fn unix_to_human_time(unix_timestamp: i64) -> String {
}
}
#[allow(dead_code)]
pub fn unix_to_machine_date(unix_timestamp: i64) -> String {
if let Some(date_time) = NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
date_time.format("%F").to_string()

View File

@ -6,8 +6,8 @@ use egui::{
plot::{Legend, Line, Plot},
Align, Layout, ProgressBar,
};
use plotly::layout::Center;
use rfd::FileDialog;
// use rfd::FileDialog;
use crate::{
config::Config,
@ -64,6 +64,7 @@ pub struct Ui {
}
impl Ui {
#[allow(dead_code)]
pub fn get_selected_brew_uuids(&self) -> Vec<String> {
self.brew_checkboxes
.iter()
@ -74,12 +75,13 @@ impl Ui {
.collect::<Vec<String>>()
}
#[allow(dead_code)]
pub fn get_selected_brews(&self) -> Vec<&Brew> {
self.database
.brews_for_uuids(&self.get_selected_brew_uuids())
}
pub fn reload(&mut self, ctx: &egui::Context) {
pub fn reload(&mut self, _ctx: &egui::Context) {
if let Some(loader_thread) = &self.loader_thread {
if loader_thread.is_finished() {
self.data_loaded = false;
@ -94,12 +96,6 @@ impl Ui {
}
impl eframe::App for Ui {
fn on_close_event(&mut self) -> bool {
self.modal = true;
self.show_confirmation_dialog = true;
self.allowed_to_close
}
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.continuous_mode {
ctx.request_repaint();
@ -111,31 +107,31 @@ impl eframe::App for Ui {
});
egui::TopBottomPanel::top("dark_light").show(ctx, |ui| {
ui.vertical(|ui| {
egui::menu::bar(ui, |ui| {
let reload_button = ui.button("Reload");
ui.vertical(|ui| {
egui::menu::bar(ui, |ui| {
let reload_button = ui.button("Reload");
if reload_button.clicked() {
self.reload(ctx);
}
if reload_button.clicked() {
self.reload(ctx);
}
if reload_button.hovered() {
if let Some(loader_thread) = &self.loader_thread {
if !loader_thread.is_finished() {
if reload_button.hovered() {
if let Some(loader_thread) = &self.loader_thread {
if !loader_thread.is_finished() {
egui::show_tooltip(ctx, egui::Id::new("reload_tooltip"), |ui| {
ui.label("Loading is still in progress.\nTo reload, please wait until previous loading has finished!");
});
}
}
}
}
}
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
egui::widgets::global_dark_light_mode_buttons(ui);
ui.heading("RustyBeans");
});
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
egui::widgets::global_dark_light_mode_buttons(ui);
ui.heading("RustyBeans");
});
})
});
});
})
});
if self.modal {
egui::CentralPanel::default().show(ctx, |ui| {
@ -172,7 +168,7 @@ impl eframe::App for Ui {
ui.horizontal(|ui| {
if ui.button("Yes").clicked() {
self.allowed_to_close = true;
frame.close();
// frame.close();
}
if ui.button("No").clicked() {
@ -417,7 +413,7 @@ impl eframe::App for Ui {
Line::new(plot_points_to_owned(
&plot_points.1,
))
.name(plot_points.0.to_owned()),
.name(plot_points.0.to_owned()),
);
}
}
@ -428,21 +424,21 @@ impl eframe::App for Ui {
} else {
ui.label("Select the config.toml to start");
if ui.button("Open file").clicked() {
if let Some(path) = FileDialog::new()
.add_filter("toml", &["toml"])
.set_directory(
match &env::current_dir() {
Ok(path) => path.to_str(),
Err(_) => None,
}
.unwrap_or_default(),
)
.pick_file()
{
self.picked_path = Some(path.display().to_string());
}
}
// if ui.button("Open file").clicked() {
// if let Some(path) = FileDialog::new()
// .add_filter("toml", &["toml"])
// .set_directory(
// match &env::current_dir() {
// Ok(path) => path.to_str(),
// Err(_) => None,
// }
// .unwrap_or_default(),
// )
// .pick_file()
// {
// self.picked_path = Some(path.display().to_string());
// }
// }
if ui.button("Open default config.toml").clicked() {
self.picked_path = Some(String::from("config.toml"));
@ -454,4 +450,10 @@ impl eframe::App for Ui {
});
}
}
// fn on_close_event(&mut self) -> bool {
// self.modal = true;
// self.show_confirmation_dialog = true;
// self.allowed_to_close
// }
}