mirror of https://github.com/terhechte/postsack
Large change, multiple related changes
- Split up the app into different app_state states - Added the `startup` state and the `visualize` state - lots of changes to support the startup screen / state - The startup screen is still a hot mess - changed the config handlingpull/1/head
parent
9092cae220
commit
975eda71bd
@ -0,0 +1,15 @@
|
||||
mod import;
|
||||
mod startup;
|
||||
mod visualize;
|
||||
|
||||
pub use startup::Startup;
|
||||
pub use visualize::{UIState, Visualize};
|
||||
|
||||
pub fn make_temporary_ui_config() -> crate::types::Config {
|
||||
crate::types::Config::new(
|
||||
"./db6.sql",
|
||||
"",
|
||||
"terhechte@me.com".to_string(),
|
||||
crate::types::FormatType::AppleMail,
|
||||
)
|
||||
}
|
@ -0,0 +1,332 @@
|
||||
use eframe::egui::epaint::Shadow;
|
||||
use eframe::egui::{
|
||||
self, vec2, Color32, Painter, Pos2, Rect, Response, Shape, Stroke, TextStyle, Vec2, Widget,
|
||||
};
|
||||
use rfd;
|
||||
|
||||
use std::ops::Rem;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::super::platform::platform_colors;
|
||||
use crate::types::FormatType;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Startup {
|
||||
/// Which importer format are we using
|
||||
format: FormatType,
|
||||
/// Where are the emails located
|
||||
email_folder: Option<PathBuf>,
|
||||
/// Should we keep them in memory,
|
||||
/// or save them to disk, to this location
|
||||
database_path: Option<PathBuf>,
|
||||
/// Should we save to disk as a flag
|
||||
save_to_disk: bool,
|
||||
/// The email address of the user
|
||||
email_address: Option<String>,
|
||||
/// time counter
|
||||
timer: f64,
|
||||
/// recursive offset counter
|
||||
offset_counter: usize,
|
||||
}
|
||||
|
||||
impl Widget for &mut Startup {
|
||||
fn ui(self, ui: &mut egui::Ui) -> Response {
|
||||
let available = ui.available_size();
|
||||
|
||||
self.draw_background(ui, available);
|
||||
|
||||
// I did not find an easy solution to center a frame in
|
||||
// on the vertical and horizontal position.
|
||||
// I tried `ui.centered_and_justified`,
|
||||
// `ui.allocate_exact_size`
|
||||
// `ui.allocate_with_layout`
|
||||
// and variations. This, at least, worked.
|
||||
let desired_size = egui::vec2(330.0, 370.0);
|
||||
|
||||
let paint_rect = Rect::from_min_size(
|
||||
Pos2 {
|
||||
x: available.x / 2.0 - desired_size.x / 2.0,
|
||||
y: available.y / 2.0 - desired_size.y / 2.0,
|
||||
},
|
||||
desired_size,
|
||||
);
|
||||
|
||||
// calculate in margin
|
||||
let center = paint_rect.shrink(15.0);
|
||||
|
||||
let colors = platform_colors();
|
||||
|
||||
let corner_radius = 12.0;
|
||||
|
||||
let frame_shape = Shape::Rect {
|
||||
rect: paint_rect,
|
||||
corner_radius,
|
||||
fill: colors.window_background_dark,
|
||||
stroke: Stroke::new(1.0, Color32::from_gray(90)),
|
||||
};
|
||||
|
||||
let shadow = Shadow::big_dark().tessellate(paint_rect, 8.0);
|
||||
let shadow = Shape::Mesh(shadow);
|
||||
let shape = Shape::Vec(vec![shadow, frame_shape]);
|
||||
ui.painter().add(shape);
|
||||
|
||||
// placeholder text
|
||||
let mut txt = "john@example.org".to_string();
|
||||
|
||||
let response = ui.allocate_ui_at_rect(center, |ui| {
|
||||
// We use a grid as that gives us more spacing opportunities
|
||||
egui::Grid::new("filter_grid")
|
||||
.spacing(vec2(15.0, 12.0))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::widgets::Label::new("Choose Import Format:")
|
||||
.text_color(Color32::WHITE)
|
||||
.text_style(TextStyle::Body),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
self.format_selection(ui, center.width() * 0.7);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(
|
||||
egui::widgets::Label::new("Email Folder:")
|
||||
.text_color(Color32::WHITE)
|
||||
.text_style(TextStyle::Body),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Browse...").clicked() {
|
||||
self.open_email_folder_dialog()
|
||||
}
|
||||
if self.format == FormatType::AppleMail {
|
||||
if ui.button("or Mail.app default folder").clicked() {
|
||||
self.email_folder = self.format.default_path().map(|e| e.to_path_buf())
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
if let Some(n) = self.email_folder.as_ref() {
|
||||
ui.label(format!("{}", n.display()))
|
||||
.on_hover_text(format!("{}", self.email_folder.as_ref().unwrap().display()));
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(
|
||||
egui::widgets::Label::new("Your Email Address:").text_color(Color32::WHITE),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
let response = ui.text_edit_singleline(&mut txt);
|
||||
if response.changed() {
|
||||
self.email_address = Some(txt);
|
||||
}
|
||||
|
||||
ui.small_button("?")
|
||||
.on_hover_text("Multiple addresses can be\nseparated by comma (,)");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(
|
||||
egui::widgets::Label::new("Used to filter send mails")
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.checkbox(&mut self.save_to_disk, "Save Imported Output Database?");
|
||||
ui.small_button("?").on_hover_text(
|
||||
"Save the database generated\nduring import. It can be opened\nwith the \"Open Database\" \nbutton below",
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
if self.save_to_disk {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Output Location").clicked() {
|
||||
self.open_database_dialog()
|
||||
}
|
||||
if let Some(Some(Some(name))) = self.database_path.as_ref().map(|e| e.file_name().map(|e| e.to_str().map(|e| e.to_string()))) {
|
||||
ui.add(egui::widgets::Label::new(name));
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
// FIXME: Only true if all data is set
|
||||
if true {
|
||||
let button_size1: Vec2 = ((center.width() / 2.0) - 25.0, 25.0).into();
|
||||
let button_size2: Vec2 = ((center.width() / 2.0) - 25.0, 25.0).into();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(
|
||||
button_size1,
|
||||
egui::Button::new("Start").text_color(Color32::WHITE),
|
||||
);
|
||||
ui.add_sized(button_size2, egui::Button::new("Or Open Database"));
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
|
||||
response.response
|
||||
}
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
fn format_selection(&mut self, ui: &mut egui::Ui, width: f32) {
|
||||
let mut selected = self.format;
|
||||
let response = egui::ComboBox::from_id_source("mailbox_type_comboox")
|
||||
.width(width)
|
||||
.selected_text(format!("{:?}", selected.name()))
|
||||
.show_ui(ui, |ui| {
|
||||
for format in FormatType::all_cases() {
|
||||
ui.selectable_value(&mut selected, format, format.name());
|
||||
}
|
||||
});
|
||||
if response.response.changed() {
|
||||
self.format = selected;
|
||||
}
|
||||
}
|
||||
|
||||
fn open_email_folder_dialog(&mut self) {
|
||||
let default_path = self
|
||||
.format
|
||||
.default_path()
|
||||
.unwrap_or(std::path::Path::new("~/"));
|
||||
|
||||
let folder = rfd::FileDialog::new()
|
||||
.set_directory(default_path)
|
||||
.pick_folder();
|
||||
|
||||
let path = match folder {
|
||||
Some(path) => path,
|
||||
None => return,
|
||||
};
|
||||
self.email_folder = Some(path);
|
||||
}
|
||||
|
||||
fn open_database_dialog(&mut self) {
|
||||
let default_path = "~/Desktop/";
|
||||
|
||||
// FIXME: Not sure if this works
|
||||
#[cfg(target_os = "windows")]
|
||||
let default_path = "C:\\Users";
|
||||
|
||||
let filename = rfd::FileDialog::new()
|
||||
.add_filter("sqlite", &["sqlite"])
|
||||
.set_directory(default_path)
|
||||
.save_file();
|
||||
|
||||
let path = match filename {
|
||||
Some(path) => path,
|
||||
None => return,
|
||||
};
|
||||
self.database_path = Some(path);
|
||||
}
|
||||
|
||||
fn draw_background(&mut self, ui: &mut egui::Ui, size: Vec2) {
|
||||
let painter = ui.painter();
|
||||
|
||||
let division = 6.0;
|
||||
|
||||
// paint stuff
|
||||
let rect_size = vec2(size.x / division, size.y / division);
|
||||
|
||||
let offset = self.timer * 42.5;
|
||||
|
||||
if offset > rect_size.x as f64 {
|
||||
self.timer = 0.0;
|
||||
self.offset_counter += 1;
|
||||
}
|
||||
|
||||
// Reset the offset counter as we're going out of the size
|
||||
if (self.offset_counter as f32 * rect_size.x) > (size.x * 1.1) {
|
||||
self.offset_counter = 0;
|
||||
}
|
||||
|
||||
// figure out the offset addition
|
||||
let add = self.offset_counter as i8; //(offset as f32 / rect_size.x) as i8;
|
||||
|
||||
Self::draw_rectangles(
|
||||
painter,
|
||||
offset,
|
||||
division,
|
||||
rect_size,
|
||||
&[
|
||||
(4 + add, 4, 3),
|
||||
(3 + add, 3, 2),
|
||||
(1 + add, 1, 5),
|
||||
(5 + add, 2, 5),
|
||||
(2 + add, 1, 6),
|
||||
(3 + add, 3, 7),
|
||||
(4 + add, 5, 1),
|
||||
(3 + add, 3, 7),
|
||||
(6 + add, 1, 3),
|
||||
(1 + add, 5, 4),
|
||||
(3 + add, 6, 5),
|
||||
],
|
||||
division as usize,
|
||||
);
|
||||
|
||||
let diff = ui.input().unstable_dt as f64;
|
||||
self.timer += diff;
|
||||
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
fn draw_rectangles(
|
||||
painter: &Painter,
|
||||
offset: f64,
|
||||
division: f32,
|
||||
size: Vec2,
|
||||
recurse: &[(i8, i8, i8)],
|
||||
total: usize,
|
||||
) {
|
||||
for y in 0..=(division + 2.0) as i8 {
|
||||
for x in 0..=(division + 2.0) as i8 {
|
||||
let fx = ((x - 1) as f32 * size.x) + (offset as f32);
|
||||
let fy = (y - 1) as f32 * size.y;
|
||||
let pos = Pos2::new(fx, fy);
|
||||
let rect = Rect::from_min_size(pos, size);
|
||||
painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Color32::from_gray(70)));
|
||||
for (rx, ry, rd) in recurse {
|
||||
// on the x axis take the offset into account
|
||||
let rx = (*rx).rem((total as i8) + 1);
|
||||
if rx == x && ry == &y {
|
||||
Self::draw_segmentation(painter, rect, *rd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_segmentation(painter: &Painter, into: Rect, divisions: i8) {
|
||||
let mut rect = into;
|
||||
for d in 0..=divisions {
|
||||
// division back and forth in direction
|
||||
let next = if d % 2 == 0 {
|
||||
Rect::from_min_size(
|
||||
Pos2 {
|
||||
x: rect.center().x,
|
||||
y: rect.top(),
|
||||
},
|
||||
Vec2 {
|
||||
x: rect.width() / 2.0,
|
||||
y: rect.height(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Rect::from_min_size(
|
||||
Pos2 {
|
||||
x: rect.left(),
|
||||
y: rect.center().y,
|
||||
},
|
||||
Vec2 {
|
||||
x: rect.width(),
|
||||
y: rect.height() / 2.0,
|
||||
},
|
||||
)
|
||||
};
|
||||
painter.rect_stroke(next, 0.0, Stroke::new(1.0, Color32::from_gray(70)));
|
||||
rect = next;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
use eframe::egui::{self, Response, Stroke, Widget};
|
||||
use eyre::Report;
|
||||
|
||||
use super::super::widgets::{self, FilterState, Spinner};
|
||||
|
||||
use crate::model::Engine;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UIState {
|
||||
pub show_emails: bool,
|
||||
pub show_filters: bool,
|
||||
pub show_export: bool,
|
||||
pub action_close: bool,
|
||||
}
|
||||
|
||||
pub struct Visualize {
|
||||
engine: Engine,
|
||||
error: Option<Report>,
|
||||
state: UIState,
|
||||
filter_state: FilterState,
|
||||
platform_custom_setup: bool,
|
||||
}
|
||||
|
||||
impl Widget for &mut Visualize {
|
||||
fn ui(self, ui: &mut egui::Ui) -> Response {
|
||||
// Avoid any processing if there is an unhandled error.
|
||||
if self.error.is_none() {
|
||||
self.error = self.engine.process().err();
|
||||
}
|
||||
|
||||
if !self.platform_custom_setup {
|
||||
self.platform_custom_setup = true;
|
||||
self.error = super::super::platform::initial_update(&ui.ctx()).err();
|
||||
|
||||
// Make the UI a bit bigger
|
||||
let pixels = ui.ctx().pixels_per_point();
|
||||
ui.ctx().set_pixels_per_point(pixels * 1.2)
|
||||
}
|
||||
|
||||
let platform_colors = super::super::platform::platform_colors();
|
||||
|
||||
let response = if let Some(error) = self.error.as_ref() {
|
||||
dbg!(&error);
|
||||
egui::CentralPanel::default()
|
||||
.show(ui.ctx(), |ui| ui.add(widgets::ErrorBox(error)))
|
||||
.response
|
||||
} else {
|
||||
let frame = egui::containers::Frame::none()
|
||||
.fill(platform_colors.window_background_dark)
|
||||
.stroke(Stroke::none());
|
||||
|
||||
egui::TopBottomPanel::top("my_panel")
|
||||
.frame(frame)
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.add(super::super::navigation_bar::NavigationBar::new(
|
||||
&mut self.engine,
|
||||
&mut self.error,
|
||||
&mut self.state,
|
||||
&mut self.filter_state,
|
||||
));
|
||||
});
|
||||
|
||||
if self.state.show_emails {
|
||||
egui::SidePanel::right("my_left_panel")
|
||||
.default_width(500.0)
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.add(super::super::mail_panel::MailPanel::new(
|
||||
&mut self.engine,
|
||||
&mut self.error,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::containers::Frame::none())
|
||||
.show(ui.ctx(), |ui| {
|
||||
if self.engine.segmentations().is_empty() {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.add(Spinner::new(egui::vec2(50.0, 50.0)));
|
||||
});
|
||||
} else {
|
||||
let stroke = Stroke::none();
|
||||
let fill = platform_colors.content_background_dark;
|
||||
super::super::widgets::background::background_color(
|
||||
ui,
|
||||
15.0,
|
||||
stroke,
|
||||
fill,
|
||||
|ui| {
|
||||
ui.vertical(|ui: &mut egui::Ui| {
|
||||
ui.add(super::super::segmentation_bar::SegmentationBar::new(
|
||||
&mut self.engine,
|
||||
&mut self.error,
|
||||
));
|
||||
ui.add(super::super::widgets::Rectangles::new(
|
||||
&mut self.engine,
|
||||
&mut self.error,
|
||||
));
|
||||
})
|
||||
.response
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.response
|
||||
};
|
||||
|
||||
// If we're waiting for a computation to succeed, we re-render again.
|
||||
if self.engine.is_busy() {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
mod config;
|
||||
|
||||
pub use config::{Config, ImporterFormat};
|
||||
pub use config::{Config, FormatType};
|
||||
|
Loading…
Reference in New Issue