Allow to handle archive

This commit is contained in:
Quentin Boyer 2023-11-12 23:01:19 +01:00
parent c5ff34cd4f
commit 78647a59e5
3 changed files with 255 additions and 39 deletions

1
Cargo.lock generated
View file

@ -883,6 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c51860ce7be5be6f6104c4e13b14e56662ebbd7c96c50e10069d59f8c3d892" checksum = "46c51860ce7be5be6f6104c4e13b14e56662ebbd7c96c50e10069d59f8c3d892"
dependencies = [ dependencies = [
"iced_widget", "iced_widget",
"num-traits",
] ]
[[package]] [[package]]

View file

@ -11,6 +11,7 @@ iced = { version = "0.10.0", features = ["lazy"] }
iced_aw = { version = "0.7.0", default-features = false, features = [ iced_aw = { version = "0.7.0", default-features = false, features = [
"modal", "modal",
"card", "card",
"number_input",
] } ] }
itertools = "0.11.0" itertools = "0.11.0"
peg = "0.8.2" peg = "0.8.2"

View file

@ -15,7 +15,7 @@ use iced::{
}, },
window, Alignment, Application, Command, Event, Length, Renderer, Settings, Theme, 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 itertools::Itertools;
use serde::{Deserialize, Serialize}; 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<M, Renderer> 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<M> {
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)] #[derive(Clone, Debug)]
enum Message { enum Message {
Event(Event), Event(Event),
@ -533,6 +633,8 @@ enum Message {
EditVariable(String, String), EditVariable(String, String),
EditEarings1(f64), EditEarings1(f64),
EditEarings2(f64), EditEarings2(f64),
SetDate(ReportDate),
Load(ReportDate),
} }
struct Glaurung { struct Glaurung {
@ -543,9 +645,12 @@ struct Glaurung {
save_file: PathBuf, save_file: PathBuf,
earnings_1: f64, earnings_1: f64,
earnings_2: f64, earnings_2: f64,
archive: BTreeMap<ReportDate, Report>,
date: Option<ReportDate>,
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default, Clone)]
struct Report { struct Report {
#[serde(default)] #[serde(default)]
recurring: BTreeMap<String, f64>, recurring: BTreeMap<String, f64>,
@ -559,10 +664,71 @@ struct Report {
earnings_2: f64, 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ReportDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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)] #[derive(Serialize, Deserialize, Default)]
struct SaveFile { struct SaveFile {
#[serde(flatten)] #[serde(flatten)]
current: Report, current: Report,
#[serde(default)]
archive: BTreeMap<ReportDate, Report>,
} }
#[derive(Default)] #[derive(Default)]
@ -573,6 +739,21 @@ struct AppConfig {
} }
impl Glaurung { 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 { fn report(&self) -> Report {
Report { Report {
savings: self.savings.clone(), savings: self.savings.clone(),
@ -588,10 +769,29 @@ impl Glaurung {
} }
} }
fn save(&self) -> SaveFile { fn save(&self) {
SaveFile { let mut save = SaveFile {
current: self.report(), 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; type Flags = AppConfig;
fn new(config: Self::Flags) -> (Self, Command<Message>) { fn new(config: Self::Flags) -> (Self, Command<Message>) {
( let mut this = Self {
Self {
config: config.config, config: config.config,
recurring: config.save.current.recurring, recurring: Default::default(),
savings: config.save.current.savings, savings: Default::default(),
variable: config archive: config.save.archive,
.save variable: Default::default(),
.current earnings_1: Default::default(),
.variable earnings_2: Default::default(),
.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, save_file: config.save_file,
}, date: None,
};
this.load(config.save.current);
(
this,
Command::batch(vec![ Command::batch(vec![
font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(Message::FontLoaded) font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(Message::FontLoaded)
]), ]),
@ -628,7 +825,11 @@ impl Application for Glaurung {
} }
fn title(&self) -> String { 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<Self::Message> { fn subscription(&self) -> iced::Subscription<Self::Message> {
@ -646,21 +847,7 @@ impl Application for Glaurung {
Message::FontLoaded(r) => r.expect("could not load font"), Message::FontLoaded(r) => r.expect("could not load font"),
Message::Event(ev) => { Message::Event(ev) => {
if let Event::Window(window::Event::CloseRequested) = ev { if let Event::Window(window::Event::CloseRequested) = ev {
let tmp_path = format!(".glaurung-{}", std::process::id()); self.save();
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");
return window::close(); return window::close();
} }
} }
@ -679,6 +866,12 @@ impl Application for Glaurung {
Message::EditEarings2(v) => { Message::EditEarings2(v) => {
self.earnings_2 = 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() Command::none()
@ -788,8 +981,14 @@ impl Application for Glaurung {
] ]
.align_items(Alignment::Start); .align_items(Alignment::Start);
let date = match self.date {
Some(d) => d.to_string(),
None => "No month set".to_string(),
};
scrollable( scrollable(
column![ column![
text(date).size(TEXT_H1),
text("Spendings").size(TEXT_H1), text("Spendings").size(TEXT_H1),
component(FixedAmounts { component(FixedAmounts {
items: &self.recurring, items: &self.recurring,
@ -840,6 +1039,21 @@ impl Application for Glaurung {
on_add: Message::AddSaving, on_add: Message::AddSaving,
}), }),
per_person, 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) .align_items(Alignment::Center)
.padding(15), .padding(15),