Allow to handle archive
This commit is contained in:
parent
c5ff34cd4f
commit
78647a59e5
3 changed files with 255 additions and 39 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -883,6 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "46c51860ce7be5be6f6104c4e13b14e56662ebbd7c96c50e10069d59f8c3d892"
|
||||
dependencies = [
|
||||
"iced_widget",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
292
src/main.rs
292
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<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)]
|
||||
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<ReportDate, Report>,
|
||||
date: Option<ReportDate>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
struct Report {
|
||||
#[serde(default)]
|
||||
recurring: BTreeMap<String, f64>,
|
||||
|
|
@ -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<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)]
|
||||
struct SaveFile {
|
||||
#[serde(flatten)]
|
||||
current: Report,
|
||||
#[serde(default)]
|
||||
archive: BTreeMap<ReportDate, Report>,
|
||||
}
|
||||
|
||||
#[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<Message>) {
|
||||
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<Self::Message> {
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue