Compare commits

..

3 Commits

8 changed files with 454 additions and 38 deletions

43
Cargo.lock generated
View File

@ -453,9 +453,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdbc37d37da9e5bce8173f3a41b71d9bf3c674deebbaceacd0ebdabde76efb03" checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
@ -949,6 +949,8 @@ dependencies = [
"env_logger", "env_logger",
"itertools", "itertools",
"serde", "serde",
"smart-default",
"stash",
"toml", "toml",
] ]
@ -1997,6 +1999,17 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 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]] [[package]]
name = "smithay-client-toolkit" name = "smithay-client-toolkit"
version = "0.16.0" version = "0.16.0"
@ -2036,6 +2049,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -2278,6 +2302,15 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
dependencies = [
"void",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"
@ -2301,6 +2334,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]] [[package]]
name = "waker-fn" name = "waker-fn"
version = "1.1.0" version = "1.1.0"

View File

@ -6,11 +6,13 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = { version = "0.4.25", features = ["serde"] } chrono = { version = "0.4.26", features = ["serde"] }
eframe = { version = "0.22.0", features = ["ron", "persistence"] } eframe = { version = "0.22.0", features = ["ron", "persistence"] }
egui-datepicker = { version = "0.3.0", path = "egui-datepicker" } egui-datepicker = { version = "0.3.0", path = "egui-datepicker" }
egui_extras = { version = "0.22.0", features = ["datepicker"] } egui_extras = { version = "0.22.0", features = ["datepicker"] }
env_logger = "0.10.0" env_logger = "0.10.0"
itertools = "0.10.5" itertools = "0.10.5"
serde = { version = "1.0.163", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
smart-default = "0.7.1"
stash = { version = "0.1.5", features = ["serialization"] }
toml = "0.7.4" toml = "0.7.4"

224
src/edit_films.rs Normal file
View File

@ -0,0 +1,224 @@
use eframe::{
egui::{self, DragValue, Window},
epaint::Color32,
};
use stash::Stash;
use crate::{
film::{Film, FilmKey},
AppState,
};
pub struct EditFilmsWindow {
add_film_window: Option<AddFilmWindow>,
edit_film_window: Option<EditFilmWindow>,
delete_film_window: Option<DeleteFilmWindow>,
}
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("Edit Films")
.open(&mut close_unclicked)
.show(ctx, |ui| {
for (i, film) in app_state.films.iter() {
ui.horizontal(|ui| {
ui.label(film.to_string());
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 film_used = app_state.rolls.iter().any(|roll| roll.film == i);
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("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<Film, FilmKey>) -> 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("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
}
}

43
src/film.rs Normal file
View File

@ -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<usize> for FilmKey {
fn from(value: usize) -> Self {
Self(value)
}
}
impl From<FilmKey> for usize {
fn from(value: FilmKey) -> Self {
value.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<T: Into<String>>(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))
}
}

View File

@ -1,14 +1,20 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod edit_films;
mod film;
mod new_roll; mod new_roll;
mod roll; mod roll;
mod roll_view; mod roll_view;
use eframe::egui; use edit_films::EditFilmsWindow;
use eframe::egui::{self, menu, Button};
use film::{Film, FilmKey};
use new_roll::NewRollWindow; use new_roll::NewRollWindow;
use roll::Roll; use roll::Roll;
use roll_view::RollViewWindow; use roll_view::RollViewWindow;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
use stash::Stash;
fn main() -> Result<(), eframe::Error> { fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). 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 struct AppState {
pub rolls: Vec<Roll>, pub rolls: Vec<Roll>,
#[serde(default)]
pub films: Stash<Film, FilmKey>,
} }
struct MyApp { struct MyApp {
state: AppState, state: AppState,
roll_views: Vec<RollViewWindow>, roll_views: Vec<RollViewWindow>,
new_roll_window: Option<NewRollWindow>, new_roll_window: Option<NewRollWindow>,
edit_films_window: Option<EditFilmsWindow>,
} }
impl MyApp { impl MyApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self { 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 { Self {
state: eframe::get_value(cc.storage.unwrap(), "state").unwrap_or_default(), state,
roll_views: Vec::new(), roll_views: Vec::new(),
new_roll_window: None, new_roll_window: None,
edit_films_window: None,
} }
} }
} }
@ -50,18 +65,31 @@ impl eframe::App for MyApp {
} }
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 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.add_enabled(self.edit_films_window.is_none(), Button::new("Edit films")).clicked() {
self.edit_films_window = Some(EditFilmsWindow::new());
}
})
})
});
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Film Manager"); ui.heading("Film Manager");
ui.label("Rolls:"); ui.label("Rolls:");
for (i, roll) in self.state.rolls.iter().enumerate() { for (i, roll) in self.state.rolls.iter().enumerate() {
if ui.button(roll.short_str()).clicked() let has_view = self.roll_views.iter().any(|view| view.roll() == i);
&& self.roll_views.iter().find(|v| v.roll() == i).is_none() if ui.add_enabled(!has_view, Button::new(roll.short_str())).clicked()
&& !has_view
{ {
self.roll_views.push(RollViewWindow::new(i)); self.roll_views.push(RollViewWindow::new(i));
} }
} }
if ui.button("Add").clicked() && self.new_roll_window.is_none() { if ui
self.new_roll_window = Some(NewRollWindow::default()); .add_enabled(self.new_roll_window.is_none(), egui::Button::new("Add"))
.clicked()
{
self.new_roll_window = Some(NewRollWindow::new(&self.state));
} }
self.roll_views self.roll_views
.retain_mut(|win| !win.draw(ctx, &mut self.state)); .retain_mut(|win| !win.draw(ctx, &mut self.state));
@ -70,6 +98,11 @@ impl eframe::App for MyApp {
self.new_roll_window = None; 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;
}
}
}); });
} }
} }

View File

@ -1,28 +1,35 @@
use chrono::{Local, NaiveDate}; use chrono::{Local, NaiveDate};
use eframe::{ use eframe::{
egui::{self, Window, DragValue}, egui::{self, DragValue, Window},
epaint::Color32, epaint::Color32,
}; };
use egui_datepicker::DatePicker; use egui_datepicker::DatePicker;
use crate::{roll::Roll, AppState}; use crate::{film::FilmKey, roll::Roll, AppState};
pub struct NewRollWindow { pub struct NewRollWindow {
id: String, id: String,
name: String, name: String,
desc: String, desc: String,
date: NaiveDate, date: NaiveDate,
film: FilmKey,
exps: u8, exps: u8,
show_id_req_err: bool, show_id_req_err: bool,
} }
impl Default for NewRollWindow { impl NewRollWindow {
fn default() -> Self { 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 { Self {
id: String::new(), id: String::new(),
name: String::new(), name: String::new(),
desc: String::new(), desc: String::new(),
date: Local::now().date_naive(), date: Local::now().date_naive(),
film,
exps: 24, exps: 24,
show_id_req_err: false, show_id_req_err: false,
} }
@ -33,6 +40,17 @@ impl NewRollWindow {
pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool { pub fn draw(&mut self, ctx: &egui::Context, app_state: &mut AppState) -> bool {
let mut close_unclicked = true; let mut close_unclicked = true;
let mut close_window = false; 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") Window::new("New Roll")
.open(&mut close_unclicked) .open(&mut close_unclicked)
.show(ctx, |ui| { .show(ctx, |ui| {
@ -62,6 +80,16 @@ impl NewRollWindow {
ui.add(DragValue::new(&mut self.exps).clamp_range(1..=50)) ui.add(DragValue::new(&mut self.exps).clamp_range(1..=50))
.labelled_by(label.id); .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 ui.button("Submit").clicked() {
if self.id.is_empty() { if self.id.is_empty() {
self.show_id_req_err = true; self.show_id_req_err = true;
@ -71,6 +99,7 @@ impl NewRollWindow {
self.name.clone(), self.name.clone(),
self.desc.clone(), self.desc.clone(),
self.date, self.date,
self.film,
self.exps, self.exps,
)); ));
close_window = true; close_window = true;

View File

@ -1,6 +1,8 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::film::FilmKey;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Exposure { pub struct Exposure {
pub num: u8, pub num: u8,
@ -16,14 +18,21 @@ pub struct Exposure {
pub struct Roll { pub struct Roll {
pub id: String, pub id: String,
pub name: String, pub name: String,
#[serde(default)]
pub desc: String, pub desc: String,
pub date: NaiveDate, pub date: NaiveDate,
pub film: FilmKey,
pub exposures: Vec<Exposure>, pub exposures: Vec<Exposure>,
} }
impl Roll { 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(); let mut exposures = Vec::new();
for num in 1..=num_exposures { for num in 1..=num_exposures {
exposures.push(Exposure { exposures.push(Exposure {
@ -31,13 +40,14 @@ impl Roll {
printed: false, printed: false,
damaged: false, damaged: false,
paper: None, paper: None,
}) });
} }
Self { Self {
id, id,
name, name,
desc, desc,
date, date,
film,
exposures, exposures,
} }
} }

View File

@ -1,12 +1,12 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use eframe::{ use eframe::{
egui::{self, RichText, Window}, egui::{self, Button, RichText, Window},
epaint::Color32, epaint::Color32,
}; };
use egui_datepicker::DatePicker; use egui_datepicker::DatePicker;
use itertools::Itertools; use itertools::Itertools;
use crate::AppState; use crate::{film::FilmKey, AppState};
pub struct RollViewWindow { pub struct RollViewWindow {
roll: usize, roll: usize,
@ -20,6 +20,8 @@ pub struct RollViewWindow {
edit_desc: String, edit_desc: String,
editing_date: bool, editing_date: bool,
edit_date: NaiveDate, edit_date: NaiveDate,
editing_film: bool,
edit_film: FilmKey,
} }
impl RollViewWindow { impl RollViewWindow {
@ -36,6 +38,8 @@ impl RollViewWindow {
edit_desc: String::new(), edit_desc: String::new(),
editing_date: false, editing_date: false,
edit_date: NaiveDate::default(), edit_date: NaiveDate::default(),
editing_film: false,
edit_film: FilmKey::from(0),
} }
} }
@ -87,7 +91,8 @@ impl RollViewWindow {
} }
}); });
if self.editing_desc { if self.editing_desc {
let label = ui.horizontal(|ui| { let label = ui
.horizontal(|ui| {
let label = ui.label("Description: "); let label = ui.label("Description: ");
if ui.button("Done").clicked() { if ui.button("Done").clicked() {
roll.desc = self.edit_desc.clone(); roll.desc = self.edit_desc.clone();
@ -97,7 +102,8 @@ impl RollViewWindow {
self.editing_desc = false; self.editing_desc = false;
} }
label label
}).inner; })
.inner;
ui.text_edit_multiline(&mut self.edit_desc) ui.text_edit_multiline(&mut self.edit_desc)
.labelled_by(label.id); .labelled_by(label.id);
} else { } 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.label("Exposures:");
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
@ -167,7 +198,10 @@ impl RollViewWindow {
} }
}); });
}); });
if ui.button("Delete").clicked() { if ui
.add_enabled(!self.confirm_delete, Button::new("Delete"))
.clicked()
{
self.confirm_delete = true; self.confirm_delete = true;
} }
if self.confirm_delete { if self.confirm_delete {
@ -176,6 +210,7 @@ impl RollViewWindow {
"Really delete roll {}?", "Really delete roll {}?",
app_state.rolls[self.roll].id app_state.rolls[self.roll].id
)); ));
ui.horizontal(|ui| {
if ui.button("No").clicked() { if ui.button("No").clicked() {
self.confirm_delete = false; self.confirm_delete = false;
} }
@ -184,6 +219,7 @@ impl RollViewWindow {
close_window = true; close_window = true;
} }
}); });
});
} }
}); });
!close_unclicked || close_window !close_unclicked || close_window