From 78647a59e5a42b99be11147ca45cc8d1bd177c58 Mon Sep 17 00:00:00 2001 From: Quentin Boyer Date: Sun, 12 Nov 2023 23:01:19 +0100 Subject: [PATCH] Allow to handle archive --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 292 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 255 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47e7c43..09e309e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c51860ce7be5be6f6104c4e13b14e56662ebbd7c96c50e10069d59f8c3d892" dependencies = [ "iced_widget", + "num-traits", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 67c508c..ecdd742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ iced = { version = "0.10.0", features = ["lazy"] } iced_aw = { version = "0.7.0", default-features = false, features = [ "modal", "card", + "number_input", ] } itertools = "0.11.0" peg = "0.8.2" diff --git a/src/main.rs b/src/main.rs index 1188919..b11c051 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use iced::{ }, window, Alignment, Application, Command, Event, Length, Renderer, Settings, Theme, }; -use iced_aw::{card, modal}; +use iced_aw::{card, modal, number_input}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -523,6 +523,106 @@ where } } +struct DatePicker<'a, F> { + label: &'a str, + title: &'a str, + confirm: &'a str, + on_pick: F, +} +struct DatePickerState { + modal: bool, + year: u64, + month: u8, +} + +impl Default for DatePickerState { + fn default() -> Self { + Self { + modal: Default::default(), + year: Default::default(), + month: 1, + } + } +} + +#[derive(Clone, Copy, Debug)] +enum DatePickerMsg { + Open, + Close, + SetYear(u64), + SetMonth(u8), + Submit, +} + +impl<'a, M, F> iced::widget::Component for DatePicker<'a, F> +where + F: FnMut(ReportDate) -> M, +{ + type State = DatePickerState; + type Event = DatePickerMsg; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + DatePickerMsg::SetYear(y) => state.year = y, + DatePickerMsg::SetMonth(m) => { + if m > 0 { + state.month = m + } + } + DatePickerMsg::Submit => { + let msg = Some((self.on_pick)(ReportDate { + year: state.year, + month: state.month, + })); + state.year = 0; + state.month = 1; + state.modal = false; + return msg; + } + DatePickerMsg::Open => { + state.modal = true; + } + DatePickerMsg::Close => { + state.modal = false; + } + } + + None + } + + fn view(&self, state: &Self::State) -> iced_aw::Element<'_, Self::Event, Renderer> { + let underlay = button(text(self.label)).on_press(DatePickerMsg::Open); + let overlay = match state.modal { + true => Some( + card( + self.title, + column![ + row![ + text("Year"), + number_input(state.year, u64::MAX, DatePickerMsg::SetYear) + ] + .align_items(Alignment::Center), + row![ + text("Month"), + number_input(state.month, 12, DatePickerMsg::SetMonth) + ] + .align_items(Alignment::Center), + button(text(self.confirm)).on_press(DatePickerMsg::Submit), + ], + ) + .on_close(DatePickerMsg::Close) + .max_width(300.0), + ), + false => None, + }; + + modal(underlay, overlay) + .backdrop(DatePickerMsg::Close) + .on_esc(DatePickerMsg::Close) + .into() + } +} + #[derive(Clone, Debug)] enum Message { Event(Event), @@ -533,6 +633,8 @@ enum Message { EditVariable(String, String), EditEarings1(f64), EditEarings2(f64), + SetDate(ReportDate), + Load(ReportDate), } struct Glaurung { @@ -543,9 +645,12 @@ struct Glaurung { save_file: PathBuf, earnings_1: f64, earnings_2: f64, + + archive: BTreeMap, + date: Option, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Clone)] struct Report { #[serde(default)] recurring: BTreeMap, @@ -559,10 +664,71 @@ struct Report { earnings_2: f64, } +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)] +struct ReportDate { + pub year: u64, + pub month: u8, +} + +impl std::fmt::Display for ReportDate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}", self.year, self.month) + } +} + +impl Serialize for ReportDate { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for ReportDate { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = ReportDate; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string of the form '{year}-{month}") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let Some((year, month)) = v.split_once('-') else { + return Err(E::custom("missing '-'")); + }; + + let report = ReportDate { + year: year.parse().map_err(E::custom)?, + month: month.parse().map_err(E::custom)?, + }; + + if report.month == 0 || report.month > 12 { + return Err(E::custom("month must be between 1 and 12")); + } + + Ok(report) + } + } + + deserializer.deserialize_str(Visitor) + } +} + #[derive(Serialize, Deserialize, Default)] struct SaveFile { #[serde(flatten)] current: Report, + #[serde(default)] + archive: BTreeMap, } #[derive(Default)] @@ -573,6 +739,21 @@ struct AppConfig { } impl Glaurung { + fn load(&mut self, report: Report) { + self.recurring = report.recurring; + self.savings = report.savings; + self.variable = report + .variable + .into_iter() + .map(|(k, e)| { + let f = calc::calc_parser::calc(&e); + (k, (e, f.ok())) + }) + .collect(); + self.earnings_1 = report.earnings_1; + self.earnings_2 = report.earnings_2; + } + fn report(&self) -> Report { Report { savings: self.savings.clone(), @@ -588,10 +769,29 @@ impl Glaurung { } } - fn save(&self) -> SaveFile { - SaveFile { - current: self.report(), + fn save(&self) { + let mut save = SaveFile { + current: Default::default(), + archive: self.archive.clone(), + }; + + match self.date { + Some(d) => { + save.archive.insert(d, self.report()); + } + None => save.current = self.report(), } + + let tmp_path = format!(".glaurung-{}", std::process::id()); + let save_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&tmp_path) + .expect("Can't open temp save file"); + serde_json::to_writer(save_file, &save).expect("could not write save file"); + + std::fs::rename(tmp_path, &self.save_file).expect("could not save data"); } } @@ -602,25 +802,22 @@ impl Application for Glaurung { type Flags = AppConfig; fn new(config: Self::Flags) -> (Self, Command) { + let mut this = Self { + config: config.config, + recurring: Default::default(), + savings: Default::default(), + archive: config.save.archive, + variable: Default::default(), + earnings_1: Default::default(), + earnings_2: Default::default(), + save_file: config.save_file, + date: None, + }; + + this.load(config.save.current); + ( - Self { - config: config.config, - recurring: config.save.current.recurring, - savings: config.save.current.savings, - variable: config - .save - .current - .variable - .into_iter() - .map(|(k, e)| { - let f = calc::calc_parser::calc(&e); - (k, (e, f.ok())) - }) - .collect(), - earnings_1: config.save.current.earnings_1, - earnings_2: config.save.current.earnings_2, - save_file: config.save_file, - }, + this, Command::batch(vec![ font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(Message::FontLoaded) ]), @@ -628,7 +825,11 @@ impl Application for Glaurung { } fn title(&self) -> String { - "Glaurung - Account Manager".into() + let mut title = "Glaurung - Account Manager".to_string(); + if let Some(d) = &self.date { + title += &format!(" ({d})"); + } + title } fn subscription(&self) -> iced::Subscription { @@ -646,21 +847,7 @@ impl Application for Glaurung { Message::FontLoaded(r) => r.expect("could not load font"), Message::Event(ev) => { if let Event::Window(window::Event::CloseRequested) = ev { - let tmp_path = format!(".glaurung-{}", std::process::id()); - let save_file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&tmp_path) - .expect("Can't open temp save file"); - serde_json::to_writer( - save_file, - &self.save() - ) - .expect("could not write save file"); - - std::fs::rename(tmp_path, &self.save_file).expect("could not save data"); - + self.save(); return window::close(); } } @@ -679,6 +866,12 @@ impl Application for Glaurung { Message::EditEarings2(v) => { self.earnings_2 = v; } + Message::SetDate(d) => self.date = Some(d), + Message::Load(d) => { + self.save(); + self.date = Some(d); + self.load(self.archive.get(&d).cloned().unwrap_or_default()); + } } Command::none() @@ -788,8 +981,14 @@ impl Application for Glaurung { ] .align_items(Alignment::Start); + let date = match self.date { + Some(d) => d.to_string(), + None => "No month set".to_string(), + }; + scrollable( column![ + text(date).size(TEXT_H1), text("Spendings").size(TEXT_H1), component(FixedAmounts { items: &self.recurring, @@ -840,6 +1039,21 @@ impl Application for Glaurung { on_add: Message::AddSaving, }), per_person, + row![ + component(DatePicker { + label: "Archive", + confirm: "Archive", + title: "Set report date", + on_pick: Message::SetDate + }), + component(DatePicker { + label: "Load", + confirm: "Load", + title: "Load archived report", + on_pick: Message::Load + }) + ] + .spacing(5) ] .align_items(Alignment::Center) .padding(15),