From 975eda71bd019cc6a8465fa4f3015c3eb40a9452 Mon Sep 17 00:00:00 2001 From: Benedikt Terhechte Date: Sun, 17 Oct 2021 21:46:50 +0200 Subject: [PATCH] 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 handling --- Cargo.lock | 188 ++++++++++++++ Cargo.toml | 19 ++ src/bin/cli.rs | 8 +- src/bin/gui.rs | 3 +- src/database/query.rs | 2 +- src/gui/app.rs | 129 +++------ src/gui/app_state/import.rs | 0 src/gui/app_state/mod.rs | 15 ++ src/gui/app_state/startup.rs | 332 ++++++++++++++++++++++++ src/gui/app_state/visualize.rs | 115 ++++++++ src/gui/mod.rs | 6 +- src/gui/navigation_bar.rs | 2 +- src/gui/widgets/background.rs | 8 +- src/importer/formats/apple_mail/mod.rs | 5 + src/importer/formats/gmailbackup/mod.rs | 5 + src/importer/formats/mbox/mod.rs | 5 + src/importer/formats/mod.rs | 6 + src/importer/mod.rs | 6 +- src/lib.rs | 23 +- src/types/config.rs | 81 +++--- src/types/mod.rs | 2 +- 21 files changed, 808 insertions(+), 152 deletions(-) create mode 100644 src/gui/app_state/import.rs create mode 100644 src/gui/app_state/mod.rs create mode 100644 src/gui/app_state/startup.rs create mode 100644 src/gui/app_state/visualize.rs diff --git a/Cargo.lock b/Cargo.lock index 6c0eba6..5c0172a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + [[package]] name = "arrayvec" version = "0.4.12" @@ -90,6 +96,18 @@ dependencies = [ "nodrop", ] +[[package]] +name = "atk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic_refcell" version = "0.1.8" @@ -141,6 +159,16 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +[[package]] +name = "cairo-sys-rs" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b448b876970834fda82ba3aeaccadbd760206b75388fc5c1b02f1e343b697570" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.6.5" @@ -157,6 +185,15 @@ version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +[[package]] +name = "cfg-expr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -637,6 +674,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "gdk-pixbuf-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -654,6 +721,19 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" +[[package]] +name = "gio-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -665,6 +745,16 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glib-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glium" version = "0.30.2" @@ -772,6 +862,7 @@ dependencies = [ "objc", "rayon", "regex", + "rfd", "rsql_builder", "rusqlite", "serde", @@ -785,6 +876,35 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gobject-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -852,6 +972,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -1319,6 +1448,18 @@ dependencies = [ "ttf-parser 0.12.3", ] +[[package]] +name = "pango-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1578,6 +1719,29 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "rfd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acac5884e3a23b02ebd6ce50fd2729732cdbdb16ea944fbbfbfa638a67992aa" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winapi", +] + [[package]] name = "rsql_builder" version = "0.1.2" @@ -1797,6 +1961,24 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "system-deps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +dependencies = [ + "anyhow", + "cfg-expr", + "heck", + "itertools", + "pkg-config", + "strum", + "strum_macros", + "thiserror", + "toml", + "version-compare", +] + [[package]] name = "takeable-option" version = "0.5.0" @@ -1978,6 +2160,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + [[package]] name = "version_check" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index 042f9e1..af63a48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,27 @@ name = "gmaildb" version = "0.1.0" edition = "2018" +description = "Bam" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.bundle.bin.gui] +name = "ExampleApplication" +identifier = "com.doe.exampleapplication" +#icon = ["128x128@2x.png"] +version = "1.0.0" +#resources = ["assets", "images/**/*.png", "secrets/public_key.txt"] +copyright = "Copyright (c) Jane Doe 2016. All rights reserved." +category = "Developer Tool" +short_description = "An example application." +long_description = """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do +eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut +enim ad minim veniam, quis nostrud exercitation ullamco laboris +nisi ut aliquip ex ea commodo consequat. +""" + [dependencies] eyre = "0.6.5" thiserror = "1.0.29" @@ -30,6 +48,7 @@ lru = { version = "0.7.0", optional = true } emlx = { git = "https://github.com/terhechte/emlx", features = []} walkdir = "*" mbox-reader = "0.2.0" +rfd = "0.5.1" [features] default = ["gui"] diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 90c2603..1239466 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -9,7 +9,7 @@ use std::{ use gmaildb::{ self, importer::{Adapter, State}, - types::ImporterFormat, + types::FormatType, }; fn main() -> Result<()> { @@ -23,15 +23,15 @@ fn main() -> Result<()> { // with dynamic dispatch. (to abstract away the match) // Will try again when I'm online. let handle = match config.format { - ImporterFormat::AppleMail => { + FormatType::AppleMail => { let importer = gmaildb::importer::applemail_importer(config); adapter.process(importer)? } - ImporterFormat::GmailVault => { + FormatType::GmailVault => { let importer = gmaildb::importer::gmail_importer(config); adapter.process(importer)? } - ImporterFormat::MboxVault => { + FormatType::Mbox => { let importer = gmaildb::importer::mbox_importer(config); adapter.process(importer)? } diff --git a/src/bin/gui.rs b/src/bin/gui.rs index da81fd0..a889447 100644 --- a/src/bin/gui.rs +++ b/src/bin/gui.rs @@ -1,7 +1,6 @@ #[cfg(feature = "gui")] fn main() { - let config = gmaildb::make_config(); - gmaildb::gui::run_gui(config); + gmaildb::gui::run_gui(); } #[cfg(not(feature = "gui"))] diff --git a/src/database/query.rs b/src/database/query.rs index 4be5767..c82fdef 100644 --- a/src/database/query.rs +++ b/src/database/query.rs @@ -4,7 +4,7 @@ pub use serde_json::Value; use strum::{self, IntoEnumIterator}; use strum_macros::{EnumIter, IntoStaticStr}; -use std::ops::{Range, Sub}; +use std::ops::Range; pub const AMOUNT_FIELD_NAME: &str = "amount"; diff --git a/src/gui/app.rs b/src/gui/app.rs index 45000b7..fefbf41 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1,40 +1,22 @@ use eframe::{ - egui::{self, Stroke}, + egui::{self}, epi::{self, Frame, Storage}, }; -use eyre::{Report, Result}; +use eyre::Result; -use super::widgets::{self, FilterState, Spinner}; -use crate::model::Engine; -use crate::types::Config; +use super::app_state::{self, Startup, Visualize}; -#[derive(Default)] -pub struct UIState { - pub show_emails: bool, - pub show_filters: bool, - pub show_export: bool, - pub action_close: bool, -} - -pub struct GmailDBApp { - _config: Config, - engine: Engine, - error: Option, - state: UIState, - filter_state: FilterState, - platform_custom_setup: bool, +pub enum GmailDBApp { + Startup { panel: Startup }, + Visualize { panel: Visualize }, } impl GmailDBApp { - pub fn new(config: &Config) -> Result { - let engine = Engine::new(config)?; - Ok(Self { - _config: config.clone(), - engine, - error: None, - state: UIState::default(), - filter_state: FilterState::new(), - platform_custom_setup: false, + pub fn new() -> Result { + // Temporarily create config without state machine + let config = app_state::make_temporary_ui_config(); + Ok(GmailDBApp::Startup { + panel: Startup::default(), }) } } @@ -50,7 +32,8 @@ impl epi::App for GmailDBApp { _frame: &mut Frame<'_>, _storage: Option<&dyn Storage>, ) { - self.error = self.engine.start().err(); + // FIXME: Bring back + //self.error = self.engine.start().err(); super::platform::setup(ctx); // Adapt to the platform colors @@ -58,83 +41,29 @@ impl epi::App for GmailDBApp { let mut visuals = egui::Visuals::dark(); visuals.widgets.noninteractive.bg_fill = platform_colors.window_background_dark; ctx.set_visuals(visuals); + + // Make the UI a bit bigger + let pixels = ctx.pixels_per_point(); + ctx.set_pixels_per_point(pixels * 1.2) } fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { - // 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::platform::initial_update(&ctx).err(); - } - - let Self { - engine, - error, - state, - filter_state, - .. - } = self; - - let platform_colors = super::platform::platform_colors(); - - if let Some(error) = error { - dbg!(&error); - egui::CentralPanel::default().show(ctx, |ui| ui.add(widgets::ErrorBox(error))); - } else { - let frame = egui::containers::Frame::none() - .fill(platform_colors.window_background_dark) - .stroke(Stroke::none()); - egui::TopBottomPanel::top("my_panel") - .frame(frame) - .show(ctx, |ui| { - ui.add(super::navigation_bar::NavigationBar::new( - engine, - error, - state, - filter_state, - )); - }); - - if state.show_emails { - egui::SidePanel::right("my_left_panel") - .default_width(500.0) - .show(ctx, |ui| { - ui.add(super::mail_panel::MailPanel::new(engine, error)); - }); - } - - egui::CentralPanel::default() - .frame(egui::containers::Frame::none()) - .show(ctx, |ui| { - if 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::widgets::background::background_color(ui, 15.0, stroke, fill, |ui| { - ui.vertical(|ui: &mut egui::Ui| { - ui.add(super::segmentation_bar::SegmentationBar::new( - engine, error, - )); - ui.add(super::widgets::Rectangles::new(engine, error)); - }); - }) - } - }); + match self { + GmailDBApp::Startup { panel } => Self::update_panel(panel, ctx, frame), + _ => panic!(), } // Resize the native window to be just the size we need it to be: frame.set_window_size(ctx.used_size()); + } +} - // If we're waiting for a computation to succeed, we re-render again. - if engine.is_busy() { - ctx.request_repaint(); - } +impl GmailDBApp { + fn update_panel(panel: &mut Startup, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + egui::CentralPanel::default() + .frame(egui::containers::Frame::none()) + .show(ctx, |ui| { + ui.add(panel); + }); } } diff --git a/src/gui/app_state/import.rs b/src/gui/app_state/import.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/gui/app_state/mod.rs b/src/gui/app_state/mod.rs new file mode 100644 index 0000000..7828a34 --- /dev/null +++ b/src/gui/app_state/mod.rs @@ -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, + ) +} diff --git a/src/gui/app_state/startup.rs b/src/gui/app_state/startup.rs new file mode 100644 index 0000000..707b3f9 --- /dev/null +++ b/src/gui/app_state/startup.rs @@ -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, + /// Should we keep them in memory, + /// or save them to disk, to this location + database_path: Option, + /// Should we save to disk as a flag + save_to_disk: bool, + /// The email address of the user + email_address: Option, + /// 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; + } + } +} diff --git a/src/gui/app_state/visualize.rs b/src/gui/app_state/visualize.rs new file mode 100644 index 0000000..363d1ab --- /dev/null +++ b/src/gui/app_state/visualize.rs @@ -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, + 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 + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index c35ad84..abc8455 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,16 +1,16 @@ -use crate::types::Config; use eframe::{self, egui, epi}; mod app; +mod app_state; mod mail_panel; mod navigation_bar; mod platform; mod segmentation_bar; pub(crate) mod widgets; -pub fn run_gui(config: Config) { +pub fn run_gui() { let options = eframe::NativeOptions::default(); - let app: Box = match app::GmailDBApp::new(&config) { + let app: Box = match app::GmailDBApp::new() { Ok(n) => Box::new(n), Err(e) => Box::new(ErrorApp(e)), }; diff --git a/src/gui/navigation_bar.rs b/src/gui/navigation_bar.rs index 307437d..3668982 100644 --- a/src/gui/navigation_bar.rs +++ b/src/gui/navigation_bar.rs @@ -2,7 +2,7 @@ use crate::model::Engine; use eframe::egui::{self, Widget}; use eyre::Report; -use super::app::UIState; +use super::app_state::UIState; use super::platform::navigation_button; use super::widgets::{FilterPanel, FilterState}; diff --git a/src/gui/widgets/background.rs b/src/gui/widgets/background.rs index ea4e5f0..63dc219 100644 --- a/src/gui/widgets/background.rs +++ b/src/gui/widgets/background.rs @@ -1,15 +1,15 @@ /// This will draw Ui with a background color and margins. /// This can be used for calls that don't provide a `Frame`, /// such as `horizontal` or `vertical` -use eframe::egui::{self, Color32, Rect, Stroke, Ui}; +use eframe::egui::{self, Color32, Rect, Response, Stroke, Ui}; -pub fn background_color( +pub fn background_color( ui: &mut Ui, padding: f32, stroke: Stroke, fill: Color32, - show: impl FnOnce(&mut Ui) -> R, -) -> R { + show: impl FnOnce(&mut Ui) -> Response, +) -> Response { let outer_rect_bounds = ui.available_rect_before_wrap(); let where_to_put_background = ui.painter().add(egui::Shape::Noop); let margin = egui::Vec2::splat(padding); diff --git a/src/importer/formats/apple_mail/mod.rs b/src/importer/formats/apple_mail/mod.rs index eef327d..3ad9c60 100644 --- a/src/importer/formats/apple_mail/mod.rs +++ b/src/importer/formats/apple_mail/mod.rs @@ -8,6 +8,11 @@ pub struct AppleMail {} impl ImporterFormat for AppleMail { type Item = mail::Mail; + + fn default_path() -> Option<&'static std::path::Path> { + Some(std::path::Path::new("~/Library/Mail")) + } + fn emails(&self, config: &Config, sender: MessageSender) -> Result> { filesystem::read_emails(config, sender) } diff --git a/src/importer/formats/gmailbackup/mod.rs b/src/importer/formats/gmailbackup/mod.rs index c924339..2398e1c 100644 --- a/src/importer/formats/gmailbackup/mod.rs +++ b/src/importer/formats/gmailbackup/mod.rs @@ -10,6 +10,11 @@ pub struct Gmail {} impl ImporterFormat for Gmail { type Item = raw_email::RawEmailEntry; + + fn default_path() -> Option<&'static std::path::Path> { + None + } + fn emails(&self, config: &Config, sender: MessageSender) -> Result> { folders_in(&config.emails_folder_path, sender, |path, sender| { emails_in(path, sender, RawEmailEntry::new) diff --git a/src/importer/formats/mbox/mod.rs b/src/importer/formats/mbox/mod.rs index dade2b2..ffb03b0 100644 --- a/src/importer/formats/mbox/mod.rs +++ b/src/importer/formats/mbox/mod.rs @@ -64,6 +64,11 @@ fn inner_emails(config: &Config) -> Result> { impl ImporterFormat for Mbox { type Item = Mail; + + fn default_path() -> Option<&'static Path> { + None + } + fn emails(&self, config: &Config, _sender: MessageSender) -> Result> { inner_emails(config) } diff --git a/src/importer/formats/mod.rs b/src/importer/formats/mod.rs index 933faeb..e23df53 100644 --- a/src/importer/formats/mod.rs +++ b/src/importer/formats/mod.rs @@ -1,3 +1,5 @@ +use std::path::Path; + pub use eyre::Result; mod apple_mail; @@ -19,6 +21,10 @@ pub use super::{Message, MessageReceiver, MessageSender}; pub trait ImporterFormat: Send + Sync { type Item: ParseableEmail; + /// The default location path where the data for this format resides + /// on system. If there is none (such as for mbox) return `None` + fn default_path() -> Option<&'static Path>; + /// Return all the emails in this format. /// Use the sneder to give progress updates via the `ReadProgress` case. fn emails(&self, config: &Config, sender: MessageSender) -> Result>; diff --git a/src/importer/mod.rs b/src/importer/mod.rs index 6d4df2d..64c4375 100644 --- a/src/importer/mod.rs +++ b/src/importer/mod.rs @@ -1,6 +1,6 @@ use crossbeam_channel; -mod formats; +pub(crate) mod formats; mod importer; mod message_adapter; @@ -37,11 +37,11 @@ pub type MessageSender = crossbeam_channel::Sender; pub type MessageReceiver = crossbeam_channel::Receiver; pub fn importer(config: &Config) -> Box { - use crate::types::ImporterFormat::*; + use crate::types::FormatType::*; match config.format { AppleMail => Box::new(applemail_importer(config.clone())), GmailVault => Box::new(gmail_importer(config.clone())), - MboxVault => Box::new(gmail_importer(config.clone())), + Mbox => Box::new(gmail_importer(config.clone())), } } diff --git a/src/lib.rs b/src/lib.rs index d18d972..9d0964c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,10 @@ pub fn setup_tracing() { .init(); } +/// Create a config for the `cli` and validate the input pub fn make_config() -> types::Config { - use types::ImporterFormat; + use std::path::Path; + use types::FormatType; let arguments: Vec = std::env::args().collect(); let folder = arguments .get(1) @@ -32,10 +34,27 @@ pub fn make_config() -> types::Config { let sender = arguments .get(3) .unwrap_or_else(|| usage("Missing sender email address argument")); - let format: ImporterFormat = arguments + let format: FormatType = arguments .get(4) .unwrap_or_else(|| usage("Missing sender email address argument")) .into(); + + let database_path = Path::new(database); + if database_path.is_dir() { + panic!( + "Database Path can't be a directory: {}", + &database_path.display() + ); + } + let emails_folder_path = Path::new(folder); + // For non-mbox files, we make sure we have a directory + if format != FormatType::Mbox && !emails_folder_path.is_dir() { + panic!( + "Emails Folder Path is not a directory: {}", + &emails_folder_path.display() + ); + } + crate::types::Config::new(database, folder, sender.to_string(), format) } diff --git a/src/types/config.rs b/src/types/config.rs index 9260365..3b4866a 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -1,18 +1,57 @@ +use strum::{self, IntoEnumIterator}; +use strum_macros::{EnumIter, IntoStaticStr}; + use std::path::{Path, PathBuf}; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ImporterFormat { +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr, EnumIter)] +pub enum FormatType { AppleMail, GmailVault, - MboxVault, + Mbox, +} + +impl FormatType { + pub fn all_cases() -> impl Iterator { + FormatType::iter() + } + + pub fn name(&self) -> &'static str { + match self { + FormatType::AppleMail => "Apple Mail", + FormatType::GmailVault => "Gmail Vault Download", + FormatType::Mbox => "Mbox", + } + } + + /// Forward the importer format location + pub fn default_path(&self) -> Option<&'static Path> { + use crate::importer::formats::{self, ImporterFormat}; + match self { + FormatType::AppleMail => formats::AppleMail::default_path(), + FormatType::GmailVault => formats::Gmail::default_path(), + FormatType::Mbox => formats::Mbox::default_path(), + } + } +} + +impl Default for FormatType { + /// We return a different default, based on the platform we're on + /// FIXME: We don't have support for Outlook yet, so on windows we go with Mbox as well + fn default() -> Self { + #[cfg(target_os = "macos")] + return FormatType::AppleMail; + + #[cfg(not(target_os = "macos"))] + return FormatType::MboxVault; + } } -impl From<&String> for ImporterFormat { +impl From<&String> for FormatType { fn from(format: &String) -> Self { match format.as_str() { - "apple" => ImporterFormat::AppleMail, - "gmailvault" => ImporterFormat::GmailVault, - "mbox" => ImporterFormat::MboxVault, + "apple" => FormatType::AppleMail, + "gmailvault" => FormatType::GmailVault, + "mbox" => FormatType::Mbox, _ => panic!("Unknown format: {}", &format), } } @@ -27,34 +66,14 @@ pub struct Config { /// The address used to send emails pub sender_email: String, /// The importer format we're using - pub format: ImporterFormat, + pub format: FormatType, } impl Config { - pub fn new>( - db: A, - mails: A, - sender_email: String, - format: ImporterFormat, - ) -> Self { - let database_path = db.as_ref().to_path_buf(); - if database_path.is_dir() { - panic!( - "Database Path can't be a directory: {}", - &database_path.display() - ); - } - let emails_folder_path = mails.as_ref().to_path_buf(); - // For non-mbox files, we make sure we have a directory - if format != ImporterFormat::MboxVault && !emails_folder_path.is_dir() { - panic!( - "Emails Folder Path is not a directory: {}", - &emails_folder_path.display() - ); - } + pub fn new>(db: A, mails: A, sender_email: String, format: FormatType) -> Self { Config { - database_path, - emails_folder_path, + database_path: db.as_ref().to_path_buf(), + emails_folder_path: mails.as_ref().to_path_buf(), sender_email, format, } diff --git a/src/types/mod.rs b/src/types/mod.rs index 69b1a6e..3c149b7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,3 +1,3 @@ mod config; -pub use config::{Config, ImporterFormat}; +pub use config::{Config, FormatType};