Large Scale UI Integration

- Split UI up into multiple states
- Changes throughout the codebase to accomodate that
- All still messy but opening a folder and creating a database works now.
pull/1/head
Benedikt Terhechte 3 years ago
parent 17c3326c36
commit b8cce5deba

41
Cargo.lock generated

@ -488,6 +488,27 @@ dependencies = [
"syn",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@ -868,6 +889,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"shellexpand",
"strum",
"strum_macros",
"thiserror",
@ -1694,6 +1716,16 @@ dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "regex"
version = "1.5.4"
@ -1869,6 +1901,15 @@ dependencies = [
"libc",
]
[[package]]
name = "shellexpand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829"
dependencies = [
"dirs-next",
]
[[package]]
name = "siphasher"
version = "0.3.7"

@ -50,6 +50,7 @@ walkdir = "*"
mbox-reader = "0.2.0"
rfd = "0.5.1"
rand = "0.8.4"
shellexpand = "*"
[features]
default = ["gui"]

@ -15,3 +15,11 @@ Update: It currently parses 632115 emails in ~56 seconds, so roughly `11.000` em
- [ ] try `iced` or `druid` as well
- [ ] maybe add blocking versions of the calls, too (in model)
- [ ] abstract over `Fields` and backend
- [ ] open database support
- [ ] add the read, send, filtres
- [ ] mbox is multiple files
- [ ] rename
- [ ] store last opened sqlite file
- [ ] save config into sqlite
- [ ] split up into multiple crates

BIN
src/.DS_Store vendored

Binary file not shown.

@ -17,6 +17,12 @@ pub struct Database {
impl Database {
/// Open database at path `Path`.
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
// FIXME: if the file exists, we're re-opening it.
// this means we need to query the `meta` table
// to retrieve the contents of the config...
dbg!(path.as_ref());
#[allow(unused_mut)]
let mut connection = Connection::open(path.as_ref())?;
@ -140,6 +146,7 @@ impl Database {
fn create_tables(connection: &Connection) -> Result<()> {
connection.execute(TBL_EMAILS, params![])?;
connection.execute(TBL_ERRORS, params![])?;
connection.execute(TBL_META, params![])?;
Ok(())
}
}

@ -19,11 +19,6 @@ CREATE TABLE IF NOT EXISTS emails (
meta_is_seen BOOL NULL
);"#;
pub const TBL_ERRORS: &str = r#"
CREATE TABLE IF NOT EXISTS errors (
message TEXT NOT NULL
);"#;
pub const QUERY_EMAILS: &str = r#"
INSERT INTO emails
(
@ -43,9 +38,30 @@ VALUES
)
"#;
pub const TBL_ERRORS: &str = r#"
CREATE TABLE IF NOT EXISTS errors (
message TEXT NOT NULL
);"#;
pub const QUERY_ERRORS: &str = r#"
INSERT INTO errors
(message)
VALUES
(?)
"#;
pub const TBL_META: &str = r#"
CREATE TABLE IF NOT EXISTS meta (
key TEXT NOT NULL,
value TEXT NOT NULL
);"#;
pub const QUERY_INSERT_META: &str = r#"
INSERT INTO meta
(key, value)
VALUES
(?, ?)
"#;
pub const QUERY_SELECT_META: &str = r#"
SELECT key, value FROM meta"#;

BIN
src/gui/.DS_Store vendored

Binary file not shown.

@ -1,28 +1,24 @@
use eframe::{
egui::{self},
epi::{self, Frame, Storage},
epi::{self, App, Frame, Storage},
};
use eyre::Result;
use eyre::{Report, Result};
use super::app_state::{self, Import, Startup, Visualize};
use super::app_state::StateUI;
pub enum GmailDBApp {
Startup { panel: Startup },
Import { panel: Import },
Visualize { panel: Visualize },
}
pub struct GmailDBApp(StateUI);
impl GmailDBApp {
pub fn new() -> Result<Self> {
// Temporarily create config without state machine
let config = app_state::make_temporary_ui_config();
Ok(GmailDBApp::Import {
panel: Import::new(config),
})
//let config = app_state::make_temporary_ui_config();
// let config = crate::make_config();
let state = StateUI::new();
Ok(GmailDBApp(state))
}
}
impl epi::App for GmailDBApp {
impl App for GmailDBApp {
fn name(&self) -> &str {
"Gmail DB"
}
@ -49,23 +45,25 @@ impl epi::App for GmailDBApp {
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
match self {
GmailDBApp::Startup { panel } => Self::update_panel(panel, ctx, frame),
GmailDBApp::Import { panel } => Self::update_panel(panel, ctx, frame),
_ => panic!(),
}
self.0.update(ctx);
// match self {
// GmailDBApp::Startup { panel } => Self::update_panel(panel, ctx, frame),
// GmailDBApp::Import { 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());
}
}
impl GmailDBApp {
fn update_panel(panel: impl egui::Widget, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
ui.add(panel);
});
}
}
// impl GmailDBApp {
// fn update_panel(panel: impl egui::Widget, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
// egui::CentralPanel::default()
// .frame(egui::containers::Frame::none())
// .show(ctx, |ui| {
// ui.add(panel);
// });
// }
// }

@ -0,0 +1,43 @@
use eframe::{
egui,
egui::{Response, Widget},
};
use super::super::widgets;
use super::{StateUI, StateUIAction, StateUIVariant};
use crate::types::Config;
pub struct ErrorUI {
/// The error to display
report: eyre::Report,
/// The config that led to this error, in order
/// to let the user `go back`.
/// As we might not have a config *yet* this is optional
config: Option<Config>,
}
impl ErrorUI {
pub fn new(report: eyre::Report, config: Option<Config>) -> Self {
Self { report, config }
}
}
impl StateUIVariant for ErrorUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
ui.add(|ui: &mut egui::Ui| self.ui(ui));
});
// If the user tapped the back button, go back to startup
StateUIAction::Nothing
}
}
impl ErrorUI {
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
// FIXME: Try again button
// that goes back to the `Startup` screen.
// somehow it should also fill it out again?
ui.add(widgets::ErrorBox(&self.report))
}
}

@ -1,15 +1,28 @@
//! The startup form to configure what and how to import
use std::thread::JoinHandle;
use eframe::egui::epaint::Shadow;
use eframe::egui::{self, vec2, Color32, Pos2, Rect, Response, Stroke, TextStyle, Vec2, Widget};
use eyre::Result;
use rand::seq::SliceRandom;
use super::super::platform::platform_colors;
use super::super::widgets::background::{shadow_background, AnimatedBackground};
use super::{StateUI, StateUIAction, StateUIVariant};
use crate::types::Config;
use crate::types::FormatType;
use crate::{
importer::{self, Adapter, State},
types::FormatType,
};
pub struct Import {
pub struct ImporterUI {
/// The config for this configuration
config: Config,
/// The adapter handling the import
adapter: Adapter,
/// The handle to the adapter thread
/// As handle.join takes `self` it has to be optional
handle: Option<JoinHandle<Result<()>>>,
/// The animation divisions
animation_divisions: usize,
/// time counter
@ -24,10 +37,21 @@ pub struct Import {
progress_blocks: Vec<usize>,
/// The progress divisions
progress_divisions: usize,
/// we're done importing
pub done_importing: bool,
/// Any errors during importing
pub importer_error: Option<eyre::Report>,
}
impl Import {
pub fn new(config: Config) -> Self {
// impl super::StateUI for ImporterUI {
// fn next(&self) -> Option<super::MainApp> {
// self.importer_error.map(|e| super::MainApp::E)
// }
// }
impl ImporterUI {
pub fn new(config: Config) -> Result<Self> {
let cloned_config = config.clone();
// Build a random distribution of elements
// to animate the import process
let mut rng = rand::thread_rng();
@ -38,34 +62,101 @@ impl Import {
let progress_block_count =
(animation_divisions * progress_divisions) * (animation_divisions * progress_divisions);
let mut progress_blocks: Vec<usize> = (0..progress_block_count).collect();
dbg!(progress_block_count);
progress_blocks.shuffle(&mut rng);
Self {
// The adapter that controls the syncing
let adapter = Adapter::new();
// Could not figure out how to build this properly
// with dynamic dispatch. (to abstract away the match)
// Will try again when I'm online.
let handle = match config.format {
FormatType::AppleMail => {
let importer = importer::applemail_importer(config);
adapter.process(importer)?
}
FormatType::GmailVault => {
let importer = importer::gmail_importer(config);
adapter.process(importer)?
}
FormatType::Mbox => {
let importer = importer::mbox_importer(config);
adapter.process(importer)?
}
};
Ok(Self {
config: cloned_config,
adapter,
handle: Some(handle),
animation_divisions,
config,
timer: 0.0,
offset_counter: 0,
intro_timer: 0.0,
progress_blocks,
progress_divisions,
done_importing: false,
importer_error: None,
})
}
}
impl StateUIVariant for ImporterUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
ui.add(|ui: &mut egui::Ui| self.ui(ui));
});
// If we generated an action above, return it
//self.action.take().unwrap_or(StateUIAction::Nothing)
match (self.importer_error.take(), self.done_importing) {
(Some(report), _) => StateUIAction::Error {
report,
config: self.config.clone(),
},
(_, true) => StateUIAction::ImportDone {
config: self.config.clone(),
},
(_, false) => StateUIAction::Nothing,
}
}
}
impl Widget for &mut Import {
fn ui(self, ui: &mut egui::Ui) -> Response {
impl ImporterUI {
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
self.intro_timer += ui.input().unstable_dt as f64;
let growth = self.intro_timer.clamp(0.0, 1.0);
let available = ui.available_size();
// We take the progress as a value fromt the blocks
// FIXME: temporary using intro timer
let p = ((self.intro_timer * 5.0) / 100.0);
let n = (self.progress_blocks.len() as f64 * p) as usize;
//println!("{} / {}", n, self.progress_blocks.len());
let slice = &self.progress_blocks[0..=n];
let (label, progress, writing, done) = match self.handle_adapter() {
Ok(n) => n,
Err(e) => {
self.importer_error = Some(e);
todo!();
// return e;
}
};
if let Ok(Some(error)) = self.adapter.error() {
println!("Has error");
self.importer_error = Some(error);
}
if done {
// if we're done, the join handle should not lock
println!("Done!");
if let Some(handle) = self.handle.take() {
println!("Wait Join Handle!");
self.importer_error = handle.join().ok().map(|e| e.err()).flatten();
}
println!("Done Importing");
self.done_importing = true;
}
let n = (self.progress_blocks.len() as f32 * progress) as usize;
let n = n.min(self.progress_blocks.len());
let slice = &self.progress_blocks[0..n];
AnimatedBackground {
divisions: self.animation_divisions,
@ -105,12 +196,50 @@ impl Widget for &mut Import {
ui.centered_and_justified(|ui| {
ui.vertical_centered_justified(|ui| {
ui.heading("Import in Progress");
let bar = egui::widgets::ProgressBar::new(0.5).animate(true);
ui.add(bar);
ui.small("133 / 1000");
ui.add_space(10.0);
if writing {
let bar = egui::widgets::ProgressBar::new(1.0).animate(false);
ui.add(bar);
let bar = egui::widgets::ProgressBar::new(progress).animate(true);
ui.add(bar);
} else {
let bar = egui::widgets::ProgressBar::new(progress).animate(true);
ui.add(bar);
ui.add_space(20.0);
}
ui.small(label);
});
})
})
.response
}
}
impl ImporterUI {
/// Returns the current label, the progress (0-1), writing? (true), and done? (true)
fn handle_adapter(&mut self) -> Result<(String, f32, bool, bool)> {
let (mut label, progress, writing) = {
let write = self.adapter.write_count()?;
if write.count > 0 {
(
format!("\rParsing emails {}/{}...", write.count, write.total),
(write.count as f32 / write.total as f32),
true,
)
} else {
let read = self.adapter.read_count()?;
(
format!("Reading emails {}/{}...", read.count, read.total),
(read.count as f32 / read.total as f32),
false,
)
}
};
let State { done, finishing } = self.adapter.finished()?;
if finishing {
label = format!("Finishing Up");
}
Ok((label, progress, writing, done))
}
}

@ -0,0 +1,134 @@
use eframe::egui::{self, Response, Stroke, Widget};
use eyre::{Report, Result};
use super::super::widgets::{self, FilterState, Spinner};
use super::{StateUI, StateUIAction, StateUIVariant};
use crate::types::Config;
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 MainUI {
config: Config,
engine: Engine,
error: Option<Report>,
state: UIState,
filter_state: FilterState,
platform_custom_setup: bool,
}
impl MainUI {
pub fn new(config: Config) -> Result<Self> {
let mut engine = Engine::new(&config)?;
engine.start()?;
Ok(Self {
config,
engine,
error: None,
state: UIState::default(),
filter_state: FilterState::new(),
platform_custom_setup: false,
})
}
}
impl StateUIVariant for MainUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction {
// 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(&ctx).err();
// Make the UI a bit bigger
let pixels = ctx.pixels_per_point();
ctx.set_pixels_per_point(pixels * 1.2)
}
let platform_colors = super::super::platform::platform_colors();
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::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(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(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::color_background(
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
},
);
}
});
// If we're waiting for a computation to succeed, we re-render again.
if self.engine.is_busy() {
ctx.request_repaint();
}
match (self.state.action_close, self.error.take()) {
(_, Some(error)) => StateUIAction::Error {
report: error,
config: self.config.clone(),
},
(true, _) => StateUIAction::Close {
config: self.config.clone(),
},
_ => StateUIAction::Nothing,
}
}
}

@ -1,16 +1,156 @@
mod error;
mod import;
mod main;
mod startup;
mod visualize;
pub use import::Import;
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,
)
use std::path::PathBuf;
use eframe::egui::{self, Widget};
pub use error::ErrorUI;
use eyre::{Report, Result};
pub use import::ImporterUI;
pub use main::{MainUI, UIState};
pub use startup::StartupUI;
use crate::types::{Config, FormatType};
pub enum StateUIAction {
CreateDatabase {
database_path: Option<PathBuf>,
emails_folder_path: PathBuf,
sender_emails: Vec<String>,
format: FormatType,
},
OpenDatabase {
database_path: PathBuf,
},
ImportDone {
config: Config,
},
Close {
config: Config,
},
Error {
report: Report,
config: Config,
},
Nothing,
}
// FIXME: Removve
// pub fn make_temporary_ui_config() -> crate::types::Config {
// crate::types::Config::new(
// "./db6.sql",
// "",
// vec!["terhechte@me.com".to_string()],
// crate::types::FormatType::AppleMail,
// )
// }
// pub enum MainApp {
// Startup { panel: StartupUI },
// Import { panel: ImporterUI },
// Main { panel: MainUI },
// Error { panel: ErrorUI },
// }
/// This defines the state machine switches between the `MainApp`
/// states.
/// FIXME: I'm not particularly happy with this abstraction right now.
// impl MainApp {
// /// An Error state can always happen
// pub fn error(report: eyre::Report) -> MainApp {
// MainApp::Error {
// panel: ErrorUI(report),
// }
// }
// // pub fn import(startup: &) -> MainApp {
// // }
// }
pub enum StateUI {
Startup(startup::StartupUI),
Import(import::ImporterUI),
Main(main::MainUI),
Error(error::ErrorUI),
}
pub trait StateUIVariant {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction;
}
impl StateUI {
/// This proxies the `update` call to the individual calls in
/// the `app_state` types
pub fn update(&mut self, ctx: &egui::CtxRef) {
let response = match self {
StateUI::Startup(panel) => panel.update_panel(ctx),
StateUI::Import(panel) => panel.update_panel(ctx),
StateUI::Main(panel) => panel.update_panel(ctx),
StateUI::Error(panel) => panel.update_panel(ctx),
};
match response {
StateUIAction::CreateDatabase {
database_path,
emails_folder_path,
sender_emails,
format,
} => {
*self =
self.create_database(database_path, emails_folder_path, sender_emails, format)
}
StateUIAction::OpenDatabase { database_path } => {
*self = self.open_database(database_path)
}
StateUIAction::ImportDone { config } => {
*self = match main::MainUI::new(config.clone()) {
Ok(n) => StateUI::Main(n),
Err(e) => StateUI::Error(ErrorUI::new(e, Some(config.clone()))),
};
}
StateUIAction::Close { config } => {
*self = StateUI::Startup(StartupUI::from_config(config));
}
StateUIAction::Error { report, config } => {
*self = StateUI::Error(error::ErrorUI::new(report, Some(config)))
}
StateUIAction::Nothing => (),
}
}
}
impl StateUI {
pub fn new() -> StateUI {
StateUI::Startup(startup::StartupUI::default())
}
pub fn create_database(
&self,
database_path: Option<PathBuf>,
emails_folder_path: PathBuf,
sender_emails: Vec<String>,
format: FormatType,
) -> StateUI {
let config = match Config::new(database_path, emails_folder_path, sender_emails, format) {
Ok(n) => n,
Err(e) => {
return StateUI::Error(error::ErrorUI::new(e, None));
}
};
let importer = match import::ImporterUI::new(config.clone()) {
Ok(n) => n,
Err(e) => {
return StateUI::Error(error::ErrorUI::new(e, Some(config.clone())));
}
};
return StateUI::Import(importer);
}
pub fn open_database(&mut self, database_path: PathBuf) -> StateUI {
// FIXME: the database needs to be opened in order to figure
// out whether it is a correct DB, before we can head on
todo!()
}
}

@ -7,10 +7,12 @@ use std::path::PathBuf;
use super::super::platform::platform_colors;
use super::super::widgets::background::{shadow_background, AnimatedBackground};
use crate::types::FormatType;
use super::{StateUI, StateUIAction, StateUIVariant};
use crate::database;
use crate::types::{Config, FormatType};
#[derive(Default)]
pub struct Startup {
pub struct StartupUI {
/// Which importer format are we using
format: FormatType,
/// Where are the emails located
@ -26,10 +28,46 @@ pub struct Startup {
timer: f64,
/// recursive offset counter
offset_counter: usize,
/// Potential error message to display to the user
error_message: Option<String>,
/// The result of the actions
action: Option<StateUIAction>,
}
impl Widget for &mut Startup {
fn ui(self, ui: &mut egui::Ui) -> Response {
impl StartupUI {
pub fn from_config(config: Config) -> Self {
let emails = if !config.sender_emails.is_empty() {
let mails: Vec<String> = config.sender_emails.iter().map(|e| e.to_owned()).collect();
Some(mails.join(", "))
} else {
None
};
Self {
format: config.format,
email_folder: Some(config.emails_folder_path),
database_path: Some(config.database_path),
save_to_disk: true,
email_address: emails,
..Default::default()
}
}
}
impl StateUIVariant for StartupUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
ui.add(|ui: &mut egui::Ui| self.ui(ui));
});
// If we generated an action above, return it
self.action.take().unwrap_or(StateUIAction::Nothing)
}
}
impl StartupUI {
/// Separated to have a less stuff happening
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
let available = ui.available_size();
AnimatedBackground {
@ -75,7 +113,10 @@ impl Widget for &mut Startup {
let hyperlink_color = visuals.hyperlink_color;
// placeholder text
let mut txt = "john@example.org".to_string();
let mut txt = self
.email_address
.clone()
.unwrap_or("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
@ -105,7 +146,7 @@ impl Widget for &mut Startup {
}
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())
self.email_folder = self.format.default_path();
}
}
});
@ -162,15 +203,31 @@ impl Widget for &mut Startup {
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(
let response = ui.add_sized(
button_size1,
egui::Button::new("Start")
.enabled(self.email_folder.is_some())
.enabled(
// if we have an email folder,
// and - if we want to save to disk -
// if we have a database path
self.email_folder.is_some() &&
(self.save_to_disk == self.database_path.is_some())
)
.text_color(Color32::WHITE),
);
ui.add_sized(button_size2, egui::Button::new("Or Open Database"));
if response.clicked() {
self.action_start();
}
let response = ui.add_sized(button_size2, egui::Button::new("Or Open Database"));
if response.clicked() {
self.action_open_database();
}
});
ui.end_row();
if let Some(ref e) = self.error_message {
let r = Color32::from_rgb(255, 0, 0);
ui.colored_label(r, e);
}
});
});
@ -178,7 +235,44 @@ impl Widget for &mut Startup {
}
}
impl Startup {
impl StartupUI {
fn action_start(&mut self) {
let email = match &self.email_folder {
Some(n) => n.clone(),
_ => return,
};
// Split by comma, remove whitespace
let emails: Vec<String> = self
.email_address
.iter()
.map(|e| e.split(",").map(|e| e.trim().to_string()).collect())
.collect();
//.unwrap_or_default();
if !email.exists() {
self.error_message = Some(format!("Email folder doesn't exist"));
return;
}
if self.save_to_disk && !self.database_path.is_some() {
self.error_message = Some(format!("Please select a database folder"));
return;
}
self.action = Some(StateUIAction::CreateDatabase {
database_path: self.database_path.clone(),
emails_folder_path: email,
sender_emails: emails,
format: self.format,
});
}
fn action_open_database(&mut self) {
// somehow ask the database to open and return a config...
// this should rather lie in a in-between model-layer...
}
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")
@ -198,7 +292,7 @@ impl Startup {
let default_path = self
.format
.default_path()
.unwrap_or(std::path::Path::new("~/"));
.unwrap_or(std::path::Path::new("~/").to_path_buf());
let folder = rfd::FileDialog::new()
.set_directory(default_path)
@ -229,4 +323,12 @@ impl Startup {
};
self.database_path = Some(path);
}
// fn set_default_folder(&self) -> PathBuf {
// let path = self.format.default_path() {
// Some(n) => n,
// }
// self.email_folder = self.format.default_path().map(|e| e.to_path_buf())
// }
}

@ -1,115 +0,0 @@
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::color_background(
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
}
}

@ -7,7 +7,7 @@ use rayon::prelude::*;
use walkdir::WalkDir;
use super::super::shared::filesystem::emails_in;
use super::super::MessageSender;
use super::super::{Message, MessageSender};
use crate::types::Config;
use super::mail::Mail;
@ -32,7 +32,8 @@ pub fn read_emails(config: &Config, sender: MessageSender) -> Result<Vec<Mail>>
_ => None,
})
.collect();
let mails = folders
sender.send(Message::ReadTotal(folders.len()))?;
let mails: Vec<Mail> = folders
.into_par_iter()
.filter_map(
|path| match emails_in(path.clone(), sender.clone(), Mail::new) {

@ -1,6 +1,9 @@
mod filesystem;
mod mail;
use shellexpand;
use std::{path::PathBuf, str::FromStr};
use super::{Config, ImporterFormat, MessageSender, Result};
#[derive(Default)]
@ -9,8 +12,9 @@ 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 default_path() -> Option<PathBuf> {
let path = shellexpand::tilde("~/Library/Mail");
Some(PathBuf::from_str(&path.to_string()).unwrap())
}
fn emails(&self, config: &Config, sender: MessageSender) -> Result<Vec<Self::Item>> {

@ -11,7 +11,7 @@ pub struct Gmail {}
impl ImporterFormat for Gmail {
type Item = raw_email::RawEmailEntry;
fn default_path() -> Option<&'static std::path::Path> {
fn default_path() -> Option<std::path::PathBuf> {
None
}

@ -65,7 +65,7 @@ fn inner_emails(config: &Config) -> Result<Vec<Mail>> {
impl ImporterFormat for Mbox {
type Item = Mail;
fn default_path() -> Option<&'static Path> {
fn default_path() -> Option<std::path::PathBuf> {
None
}

@ -1,4 +1,4 @@
use std::path::Path;
use std::path::PathBuf;
pub use eyre::Result;
@ -23,7 +23,7 @@ pub trait ImporterFormat: Send + Sync {
/// 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>;
fn default_path() -> Option<PathBuf>;
/// Return all the emails in this format.
/// Use the sneder to give progress updates via the `ReadProgress` case.

@ -20,8 +20,8 @@ pub fn into_database<Mail: ParseableEmail + 'static>(
bail!("Channel Failure {:?}", &e);
}
// Create a new database connection
let database = Database::new(config.database_path.clone())?;
// Create a new database connection, just for writing
let database = Database::new(config.database_path.clone()).unwrap();
// Consume the connection to begin the import. It will return the `handle` to use for
// waiting for the database to finish importing, and the `sender` to submit work.
@ -32,7 +32,7 @@ pub fn into_database<Mail: ParseableEmail + 'static>(
// in paralell..
.par_iter_mut()
// parsing them
.map(|raw_mail| parse_email(raw_mail, config.sender_email.as_str()))
.map(|raw_mail| parse_email(raw_mail, &config.sender_emails))
// and inserting them into SQLite
.for_each(|entry| {
// Try to write the message into the database

@ -3,6 +3,7 @@ use email_parser::address::{Address, EmailAddress, Mailbox};
use eyre::{eyre, Result};
use std::borrow::Cow;
use std::collections::HashSet;
use std::path::Path;
use super::email::{EmailEntry, EmailMeta};
@ -24,7 +25,7 @@ pub trait ParseableEmail: Send + Sized + Sync {
pub fn parse_email<Entry: ParseableEmail>(
entry: &mut Entry,
config_sender_email: &str,
sender_emails: &HashSet<String>,
) -> Result<EmailEntry> {
if let Err(e) = entry.prepare() {
tracing::error!("Prepare Error: {:?}", e);
@ -58,8 +59,10 @@ pub fn parse_email<Entry: ParseableEmail>(
// In order to determine the sender, we have to
// build up the address again :-(
let is_send =
format!("{}@{}", sender_local_part, sender_domain).as_str() == config_sender_email;
let is_send = {
let email = format!("{}@{}", sender_local_part, sender_domain);
sender_emails.contains(&email)
};
Ok(EmailEntry {
path: path.to_path_buf(),

@ -1,14 +1,14 @@
use super::formats::shared;
use super::{Config, ImporterFormat};
use super::MessageReceiver;
use super::{Message, MessageReceiver};
use crossbeam_channel::{self, unbounded};
use eyre::Result;
use std::thread::JoinHandle;
pub trait Importerlike {
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<usize>>)>;
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<()>>)>;
}
pub struct Importer<Format: ImporterFormat> {
@ -23,23 +23,35 @@ impl<Format: ImporterFormat + 'static> Importer<Format> {
}
impl<Format: ImporterFormat + 'static> Importerlike for Importer<Format> {
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<usize>>)> {
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<()>>)> {
let Importer { format, .. } = self;
let (sender, receiver) = unbounded();
let config = self.config;
let handle: JoinHandle<Result<usize>> = std::thread::spawn(move || {
let emails = format.emails(&config, sender.clone())?;
let processed = shared::database::into_database(&config, emails, sender.clone())?;
Ok(processed)
let handle: JoinHandle<Result<()>> = std::thread::spawn(move || {
let outer_sender = sender.clone();
let processed = move || {
let emails = format.emails(&config, sender.clone())?;
let processed = shared::database::into_database(&config, emails, sender.clone())?;
Ok(processed)
};
let result = processed();
// Send the error away and map it to a crossbeam channel error
match result {
Ok(_) => Ok(()),
Err(e) => match outer_sender.send(Message::Error(e)) {
Ok(_) => Ok(()),
Err(e) => Err(eyre::Report::new(e)),
},
}
});
Ok((receiver, handle))
}
}
impl<T: Importerlike + Sized> Importerlike for Box<T> {
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<usize>>)> {
fn import(self) -> Result<(MessageReceiver, JoinHandle<Result<()>>)> {
(*self).import()
}
}

@ -1,4 +1,4 @@
use eyre::{bail, eyre, Result};
use eyre::{bail, eyre, Report, Result};
use std::sync::{Arc, RwLock};
use std::thread::JoinHandle;
@ -7,7 +7,7 @@ use super::formats::ImporterFormat;
use super::importer::Importerlike;
use super::Message;
#[derive(Clone, Debug, Copy, Default)]
#[derive(Debug, Default)]
struct Data {
total_read: usize,
read: usize,
@ -15,6 +15,7 @@ struct Data {
write: usize,
finishing: bool,
done: bool,
error: Option<Report>,
}
#[derive(Clone, Debug, Copy)]
@ -69,7 +70,15 @@ impl Adapter {
for entry in receiver.try_iter() {
match entry {
Message::ReadTotal(n) => write_guard.total_read = n,
Message::ReadOne => write_guard.read += 1,
Message::ReadOne => {
write_guard.read += 1;
// Depending on the implementation, we may receive read calls before
// the total size is known. We prevent division by zero by
// always setting the total to read + 1 in these cases
if write_guard.total_read <= write_guard.read {
write_guard.total_read = write_guard.read + 1;
}
}
Message::WriteTotal(n) => write_guard.total_write = n,
Message::WriteOne => write_guard.write += 1,
Message::FinishingUp => write_guard.finishing = true,
@ -77,6 +86,9 @@ impl Adapter {
write_guard.done = true;
break 'outer;
}
Message::Error(e) => {
write_guard.error = Some(e);
}
};
}
}
@ -111,4 +123,17 @@ impl Adapter {
done: item.done,
})
}
pub fn error(&self) -> Result<Option<Report>> {
// We take the error of out of the write lock only if there is an error.
let item = self.consumer_lock.read().map_err(|e| eyre!("{:?}", &e))?;
let is_error = item.error.is_some();
drop(item);
if is_error {
let mut item = self.producer_lock.write().map_err(|e| eyre!("{:?}", &e))?;
Ok(item.error.take())
} else {
Ok(None)
}
}
}

@ -11,6 +11,7 @@ pub use message_adapter::*;
use formats::ImporterFormat;
/// The message that informs of the importers progress
#[derive(Debug)]
pub enum Message {
/// How much progress are we making on reading the contents
/// of the emails.
@ -31,6 +32,8 @@ pub enum Message {
FinishingUp,
/// Finally, this indicates that we're done.
Done,
/// An error happened during processing
Error(eyre::Report),
}
pub type MessageSender = crossbeam_channel::Sender<Message>;

@ -55,7 +55,10 @@ pub fn make_config() -> types::Config {
);
}
crate::types::Config::new(database, folder, sender.to_string(), format)
match crate::types::Config::new(Some(database), folder, vec![sender.to_string()], format) {
Ok(n) => n,
Err(r) => panic!("Error: {:?}", &r),
}
}
fn usage(error: &'static str) -> ! {

@ -69,6 +69,7 @@ impl<Context: Send + Sync + 'static> Link<Context> {
}
pub(super) fn run<Context: Send + Sync + 'static>(config: &Config) -> Result<Link<Context>> {
// Create a new database connection, just for reading
let database = Database::new(&config.database_path)?;
let (input_sender, input_receiver) = unbounded();
let (output_sender, output_receiver) = unbounded();

@ -1,6 +1,9 @@
use rand::Rng;
use strum::{self, IntoEnumIterator};
use strum_macros::{EnumIter, IntoStaticStr};
use std::collections::HashSet;
use std::iter::FromIterator;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr, EnumIter)]
@ -24,7 +27,7 @@ impl FormatType {
}
/// Forward the importer format location
pub fn default_path(&self) -> Option<&'static Path> {
pub fn default_path(&self) -> Option<PathBuf> {
use crate::importer::formats::{self, ImporterFormat};
match self {
FormatType::AppleMail => formats::AppleMail::default_path(),
@ -63,19 +66,39 @@ pub struct Config {
pub database_path: PathBuf,
/// The path where the emails are
pub emails_folder_path: PathBuf,
/// The address used to send emails
pub sender_email: String,
/// The addresses used to send emails
pub sender_emails: HashSet<String>,
/// The importer format we're using
pub format: FormatType,
}
impl Config {
pub fn new<A: AsRef<Path>>(db: A, mails: A, sender_email: String, format: FormatType) -> Self {
Config {
database_path: db.as_ref().to_path_buf(),
pub fn new<A: AsRef<Path>>(
db: Option<A>,
mails: A,
sender_emails: Vec<String>,
format: FormatType,
) -> eyre::Result<Self> {
// If we don't have a database path, we use a temporary folder.
let database_path = match db {
Some(n) => n.as_ref().to_path_buf(),
None => {
let number: u32 = rand::thread_rng().gen();
let folder = "gmaildb";
let filename = format!("{}.sqlite", number);
let mut temp_dir = std::env::temp_dir();
temp_dir.push(folder);
// the folder has to be created
std::fs::create_dir_all(&temp_dir)?;
temp_dir.push(filename);
temp_dir
}
};
Ok(Config {
database_path,
emails_folder_path: mails.as_ref().to_path_buf(),
sender_email,
sender_emails: HashSet::from_iter(sender_emails.into_iter()),
format,
}
})
}
}

Loading…
Cancel
Save