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
pull/1/head
Benedikt Terhechte 3 years ago
parent 9092cae220
commit 975eda71bd

188
Cargo.lock generated

@ -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"

@ -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"]

@ -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)?
}

@ -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"))]

@ -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";

@ -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<Report>,
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<Self> {
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<Self> {
// 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);
});
}
}

@ -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,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<dyn epi::App> = match app::GmailDBApp::new(&config) {
let app: Box<dyn epi::App> = match app::GmailDBApp::new() {
Ok(n) => Box::new(n),
Err(e) => Box::new(ErrorApp(e)),
};

@ -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};

@ -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<R>(
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);

@ -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<Vec<Self::Item>> {
filesystem::read_emails(config, sender)
}

@ -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<Vec<Self::Item>> {
folders_in(&config.emails_folder_path, sender, |path, sender| {
emails_in(path, sender, RawEmailEntry::new)

@ -64,6 +64,11 @@ fn inner_emails(config: &Config) -> Result<Vec<Mail>> {
impl ImporterFormat for Mbox {
type Item = Mail;
fn default_path() -> Option<&'static Path> {
None
}
fn emails(&self, config: &Config, _sender: MessageSender) -> Result<Vec<Self::Item>> {
inner_emails(config)
}

@ -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<Vec<Self::Item>>;

@ -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<Message>;
pub type MessageReceiver = crossbeam_channel::Receiver<Message>;
pub fn importer(config: &Config) -> Box<dyn importer::Importerlike> {
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())),
}
}

@ -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<String> = 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)
}

@ -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<Item = FormatType> {
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<A: AsRef<Path>>(
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<A: AsRef<Path>>(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,
}

@ -1,3 +1,3 @@
mod config;
pub use config::{Config, ImporterFormat};
pub use config::{Config, FormatType};

Loading…
Cancel
Save