From 3e16dbd3303048860db2f4be5aef6ed4fd4c7722 Mon Sep 17 00:00:00 2001 From: pjht Date: Wed, 31 May 2023 08:51:48 -0500 Subject: [PATCH] Add support for associating a film to a roll --- Cargo.lock | 43 ++++++++- Cargo.toml | 4 +- src/edit_films.rs | 229 ++++++++++++++++++++++++++++++++++++++++++++++ src/film.rs | 43 +++++++++ src/main.rs | 39 +++++++- src/new_roll.rs | 37 +++++++- src/roll.rs | 14 ++- src/roll_view.rs | 55 ++++++++--- 8 files changed, 439 insertions(+), 25 deletions(-) create mode 100644 src/edit_films.rs create mode 100644 src/film.rs diff --git a/Cargo.lock b/Cargo.lock index dad3237..b0f65d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,9 +453,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbc37d37da9e5bce8173f3a41b71d9bf3c674deebbaceacd0ebdabde76efb03" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", @@ -949,6 +949,8 @@ dependencies = [ "env_logger", "itertools", "serde", + "smart-default", + "stash", "toml", ] @@ -1997,6 +1999,17 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "smithay-client-toolkit" version = "0.16.0" @@ -2036,6 +2049,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "stash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79601e59056976819e06b2aad284be6edc9b5b27d26808330a1656d6b7dc91a" +dependencies = [ + "serde", + "serde_derive", + "unreachable", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2278,6 +2302,15 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "url" version = "2.3.1" @@ -2301,6 +2334,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index b1c1233..a0ad76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4.25", features = ["serde"] } +chrono = { version = "0.4.26", features = ["serde"] } eframe = { version = "0.22.0", features = ["ron", "persistence"] } egui-datepicker = { version = "0.3.0", path = "egui-datepicker" } egui_extras = { version = "0.22.0", features = ["datepicker"] } env_logger = "0.10.0" itertools = "0.10.5" serde = { version = "1.0.163", features = ["derive"] } +smart-default = "0.7.1" +stash = { version = "0.1.5", features = ["serialization"] } toml = "0.7.4" diff --git a/src/edit_films.rs b/src/edit_films.rs new file mode 100644 index 0000000..0efc92c --- /dev/null +++ b/src/edit_films.rs @@ -0,0 +1,229 @@ +use eframe::{ + egui::{self, DragValue, Window}, + epaint::Color32, +}; +use stash::Stash; + +use crate::{ + film::{Film, FilmKey}, + AppState, +}; + +pub struct EditFilmsWindow { + add_film_window: Option, + edit_film_window: Option, + delete_film_window: Option, +} + +impl EditFilmsWindow { + pub fn new() -> Self { + Self { + add_film_window: None, + edit_film_window: None, + delete_film_window: None, + } + } + + pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { + let mut close_unclicked = true; + Window::new(format!("Edit Films")) + .open(&mut close_unclicked) + .show(ctx, |ui| { + for (i, film) in app_state.films.iter() { + ui.horizontal(|ui| { + ui.label(format!("{}", film)); + if ui + .add_enabled( + self.edit_film_window.is_none() + && self + .delete_film_window + .as_ref() + .map_or(true, |win| win.film != i), + egui::Button::new("Edit"), + ) + .clicked() + { + self.edit_film_window = Some(EditFilmWindow::new(i, &app_state.films)); + } + let mut film_used = false; + for roll in &app_state.rolls { + if roll.film == i { + film_used = true; + } + } + if ui + .add_enabled( + self.delete_film_window.is_none() + && self + .edit_film_window + .as_ref() + .map_or(true, |win| win.film != i) + && !film_used, + egui::Button::new("Delete"), + ) + .clicked() + { + self.delete_film_window = Some(DeleteFilmWindow::new(i)); + } + if film_used { + ui.label("(Used by one or more rolls, cannot delete)"); + } + }); + } + if ui + .add_enabled(self.add_film_window.is_none(), egui::Button::new("Add")) + .clicked() + { + self.add_film_window = Some(AddFilmWindow::new()); + } + }); + if let Some(add_film_window) = self.add_film_window.as_mut() { + if add_film_window.draw(ctx, app_state) { + self.add_film_window = None; + } + } + if let Some(edit_film_window) = self.edit_film_window.as_mut() { + if edit_film_window.draw(ctx, app_state) { + self.edit_film_window = None; + } + } + if let Some(delete_film_window) = self.delete_film_window.as_mut() { + if delete_film_window.draw(ctx, app_state) { + self.delete_film_window = None; + } + } + !close_unclicked + } +} + +struct AddFilmWindow { + name: String, + iso: u32, + show_name_req_err: bool, +} + +impl AddFilmWindow { + fn new() -> Self { + Self { + name: String::new(), + iso: 400, + show_name_req_err: false, + } + } + + pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { + let mut close_unclicked = true; + let mut close_window = false; + Window::new(format!("Add Film")) + .open(&mut close_unclicked) + .show(ctx, |ui| { + ui.horizontal(|ui| { + let label = ui.label("Name:"); + ui.text_edit_singleline(&mut self.name) + .labelled_by(label.id); + }); + ui.horizontal(|ui| { + let label = ui.label("ISO:"); + ui.add(DragValue::new(&mut self.iso).clamp_range(1..=u32::MAX)) + .labelled_by(label.id); + }); + if ui.button("Submit").clicked() { + if self.name.is_empty() { + self.show_name_req_err = true; + return; + } + app_state.films.put(Film::new(&self.name, self.iso)); + close_window = true; + } + + if self.show_name_req_err { + ui.colored_label(Color32::DARK_RED, "Error: Name required"); + } + }); + !close_unclicked || close_window + } +} + +struct EditFilmWindow { + film: FilmKey, + name: String, + iso: u32, + show_name_req_err: bool, +} + +impl EditFilmWindow { + fn new(film: FilmKey, films: &Stash) -> Self { + let name = films[film].name.clone(); + let iso = films[film].iso; + Self { + film, + name, + iso, + show_name_req_err: false, + } + } + + pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { + let mut close_unclicked = true; + let mut close_window = false; + Window::new(format!("Edit Film")) + .open(&mut close_unclicked) + .show(ctx, |ui| { + ui.horizontal(|ui| { + let label = ui.label("Name:"); + ui.text_edit_singleline(&mut self.name) + .labelled_by(label.id); + }); + ui.horizontal(|ui| { + let label = ui.label("ISO:"); + ui.add(DragValue::new(&mut self.iso).clamp_range(1..=u32::MAX)) + .labelled_by(label.id); + }); + if ui.button("Submit").clicked() { + if self.name.is_empty() { + self.show_name_req_err = true; + return; + } + app_state.films[self.film] = Film::new(&self.name, self.iso); + close_window = true; + } + if self.show_name_req_err { + ui.colored_label(Color32::DARK_RED, "Error: Name required"); + } + }); + !close_unclicked || close_window + } +} + +struct DeleteFilmWindow { + film: FilmKey, +} + +impl DeleteFilmWindow { + fn new(film: FilmKey) -> Self { + Self { film } + } + + pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { + let mut close_unclicked = true; + let mut close_window = false; + Window::new("Confirm delete") + .open(&mut close_unclicked) + .show(ctx, |ui| { + ui.heading(format!( + "Really delete {}?", + &app_state.films[self.film].name + )); + ui.horizontal(|ui| { + if ui.button("Yes").clicked() { + app_state.films.take(self.film); + close_window = true; + } + if ui.button("No").clicked() { + close_window = true; + } + }); + }); + !close_unclicked || close_window + } +} diff --git a/src/film.rs b/src/film.rs new file mode 100644 index 0000000..98198c4 --- /dev/null +++ b/src/film.rs @@ -0,0 +1,43 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use smart_default::SmartDefault; + +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct FilmKey(usize); + +impl From for FilmKey { + fn from(value: usize) -> Self { + Self(value) + } +} + +impl Into for FilmKey { + fn into(self) -> usize { + self.0 + } +} + +#[derive(SmartDefault, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Film { + #[default = "Unknown"] + pub name: String, + #[default = 400] + pub iso: u32, +} + +impl Film { + pub fn new>(name: T, iso: u32) -> Self { + Self { + name: name.into(), + iso, + } + } +} + +impl Display for Film { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} - ISO {}", &self.name, self.iso)) + } +} diff --git a/src/main.rs b/src/main.rs index 1b71692..64a0ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,20 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +mod edit_films; +mod film; mod new_roll; mod roll; mod roll_view; -use eframe::egui; +use edit_films::EditFilmsWindow; +use eframe::egui::{self, menu}; +use film::{Film, FilmKey}; use new_roll::NewRollWindow; use roll::Roll; use roll_view::RollViewWindow; use serde::{Deserialize, Serialize}; +use smart_default::SmartDefault; +use stash::Stash; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -23,23 +29,32 @@ fn main() -> Result<(), eframe::Error> { ) } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, SmartDefault)] pub struct AppState { pub rolls: Vec, + #[serde(default)] + pub films: Stash, } struct MyApp { state: AppState, roll_views: Vec, new_roll_window: Option, + edit_films_window: Option, } impl MyApp { fn new(cc: &eframe::CreationContext<'_>) -> Self { + let state = if cc.storage.unwrap().get_string("state").is_none() { + AppState::default() + } else { + eframe::get_value(cc.storage.unwrap(), "state").unwrap() + }; Self { - state: eframe::get_value(cc.storage.unwrap(), "state").unwrap_or_default(), + state, roll_views: Vec::new(), new_roll_window: None, + edit_films_window: None, } } } @@ -50,6 +65,17 @@ impl eframe::App for MyApp { } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::TopBottomPanel::top("menu_bar_panel").show(ctx, |ui| { + menu::bar(ui, |ui| { + ui.menu_button("View", |ui| { + if ui.button("Edit films").clicked() { + if self.edit_films_window.is_none() { + self.edit_films_window = Some(EditFilmsWindow::new()); + } + } + }) + }) + }); egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Film Manager"); ui.label("Rolls:"); @@ -61,7 +87,7 @@ impl eframe::App for MyApp { } } if ui.button("Add").clicked() && self.new_roll_window.is_none() { - self.new_roll_window = Some(NewRollWindow::default()); + self.new_roll_window = Some(NewRollWindow::new(&self.state)); } self.roll_views .retain_mut(|win| !win.draw(ctx, &mut self.state)); @@ -70,6 +96,11 @@ impl eframe::App for MyApp { self.new_roll_window = None; } } + if let Some(edit_films_window) = self.edit_films_window.as_mut() { + if edit_films_window.draw(ctx, &mut self.state) { + self.edit_films_window = None; + } + } }); } } diff --git a/src/new_roll.rs b/src/new_roll.rs index a76f7ca..45c4d8c 100644 --- a/src/new_roll.rs +++ b/src/new_roll.rs @@ -1,28 +1,35 @@ use chrono::{Local, NaiveDate}; use eframe::{ - egui::{self, Window, DragValue}, + egui::{self, DragValue, Window}, epaint::Color32, }; use egui_datepicker::DatePicker; -use crate::{roll::Roll, AppState}; +use crate::{film::FilmKey, roll::Roll, AppState}; pub struct NewRollWindow { id: String, name: String, desc: String, date: NaiveDate, + film: FilmKey, exps: u8, show_id_req_err: bool, } -impl Default for NewRollWindow { - fn default() -> Self { +impl NewRollWindow { + pub fn new(app_state: &AppState) -> Self { + let film = if app_state.films.is_empty() { + FilmKey::from(0) // Create a dummy key so the error window can be shown + } else { + app_state.films.iter().next().unwrap().0 + }; Self { id: String::new(), name: String::new(), desc: String::new(), date: Local::now().date_naive(), + film, exps: 24, show_id_req_err: false, } @@ -33,6 +40,17 @@ impl NewRollWindow { pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { let mut close_unclicked = true; let mut close_window = false; + if app_state.films.is_empty() { + Window::new("Error") + .open(&mut close_unclicked) + .show(ctx, |ui| { + ui.heading("No films are available. Please add at least one film."); + if ui.button("OK").clicked() { + close_window = true; + } + }); + return !close_unclicked || close_window; + } Window::new("New Roll") .open(&mut close_unclicked) .show(ctx, |ui| { @@ -62,6 +80,16 @@ impl NewRollWindow { ui.add(DragValue::new(&mut self.exps).clamp_range(1..=50)) .labelled_by(label.id); }); + ui.horizontal(|ui| { + ui.label("Film: "); + egui::ComboBox::from_id_source("new_roll_film") + .selected_text(format!("{}", &app_state.films[self.film])) + .show_ui(ui, |ui| { + for (i, film) in &app_state.films { + ui.selectable_value(&mut self.film, i, film.to_string()); + } + }); + }); if ui.button("Submit").clicked() { if self.id.is_empty() { self.show_id_req_err = true; @@ -71,6 +99,7 @@ impl NewRollWindow { self.name.clone(), self.desc.clone(), self.date, + self.film.clone(), self.exps, )); close_window = true; diff --git a/src/roll.rs b/src/roll.rs index 2eebca3..ac79c49 100644 --- a/src/roll.rs +++ b/src/roll.rs @@ -1,6 +1,8 @@ use chrono::NaiveDate; use serde::{Deserialize, Serialize}; +use crate::film::FilmKey; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Exposure { pub num: u8, @@ -16,14 +18,21 @@ pub struct Exposure { pub struct Roll { pub id: String, pub name: String, - #[serde(default)] pub desc: String, pub date: NaiveDate, + pub film: FilmKey, pub exposures: Vec, } impl Roll { - pub fn new(id: String, name: String, desc: String, date: NaiveDate, num_exposures: u8) -> Self { + pub fn new( + id: String, + name: String, + desc: String, + date: NaiveDate, + film: FilmKey, + num_exposures: u8, + ) -> Self { let mut exposures = Vec::new(); for num in 1..=num_exposures { exposures.push(Exposure { @@ -38,6 +47,7 @@ impl Roll { name, desc, date, + film, exposures, } } diff --git a/src/roll_view.rs b/src/roll_view.rs index a32e655..a2ef0da 100644 --- a/src/roll_view.rs +++ b/src/roll_view.rs @@ -6,7 +6,7 @@ use eframe::{ use egui_datepicker::DatePicker; use itertools::Itertools; -use crate::AppState; +use crate::{AppState, film::FilmKey}; pub struct RollViewWindow { roll: usize, @@ -20,6 +20,8 @@ pub struct RollViewWindow { edit_desc: String, editing_date: bool, edit_date: NaiveDate, + editing_film: bool, + edit_film: FilmKey, } impl RollViewWindow { @@ -36,6 +38,8 @@ impl RollViewWindow { edit_desc: String::new(), editing_date: false, edit_date: NaiveDate::default(), + editing_film: false, + edit_film: FilmKey::from(0), } } @@ -87,17 +91,19 @@ impl RollViewWindow { } }); if self.editing_desc { - let label = ui.horizontal(|ui| { - let label = ui.label("Description: "); - if ui.button("Done").clicked() { - roll.desc = self.edit_desc.clone(); - self.editing_desc = false; - } - if ui.button("Cancel").clicked() { - self.editing_desc = false; - } - label - }).inner; + let label = ui + .horizontal(|ui| { + let label = ui.label("Description: "); + if ui.button("Done").clicked() { + roll.desc = self.edit_desc.clone(); + self.editing_desc = false; + } + if ui.button("Cancel").clicked() { + self.editing_desc = false; + } + label + }) + .inner; ui.text_edit_multiline(&mut self.edit_desc) .labelled_by(label.id); } else { @@ -136,6 +142,31 @@ impl RollViewWindow { } } }); + ui.horizontal(|ui| { + if self.editing_film { + ui.label("Film: "); + egui::ComboBox::from_id_source("new_roll_film") + .selected_text(format!("{}", app_state.films[self.edit_film])) + .show_ui(ui, |ui| { + for (i, film) in &app_state.films { + ui.selectable_value(&mut self.edit_film, i, film.to_string()); + } + }); + if ui.button("Done").clicked() { + roll.film = self.edit_film; + self.editing_film = false; + } + if ui.button("Cancel").clicked() { + self.editing_film = false; + } + } else { + ui.label(format!("Film: {}", &app_state.films[roll.film])); + if ui.button("Edit").clicked() { + self.editing_film = true; + self.edit_film = roll.film; + } + } + }); ui.label("Exposures:"); ui.horizontal_top(|ui| { ui.vertical(|ui| {