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]]
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"

View File

@ -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"

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
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, Button};
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<Roll>,
#[serde(default)]
pub films: Stash<Film, FilmKey>,
}
struct MyApp {
state: AppState,
roll_views: Vec<RollViewWindow>,
new_roll_window: Option<NewRollWindow>,
edit_films_window: Option<EditFilmsWindow>,
}
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,18 +65,31 @@ 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.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| {
ui.heading("Film Manager");
ui.label("Rolls:");
for (i, roll) in self.state.rolls.iter().enumerate() {
if ui.button(roll.short_str()).clicked()
&& self.roll_views.iter().find(|v| v.roll() == i).is_none()
let has_view = self.roll_views.iter().any(|view| view.roll() == i);
if ui.add_enabled(!has_view, Button::new(roll.short_str())).clicked()
&& !has_view
{
self.roll_views.push(RollViewWindow::new(i));
}
}
if ui.button("Add").clicked() && self.new_roll_window.is_none() {
self.new_roll_window = Some(NewRollWindow::default());
if ui
.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
.retain_mut(|win| !win.draw(ctx, &mut self.state));
@ -70,6 +98,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;
}
}
});
}
}

View File

@ -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,
self.exps,
));
close_window = true;

View File

@ -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<Exposure>,
}
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 {
@ -31,13 +40,14 @@ impl Roll {
printed: false,
damaged: false,
paper: None,
})
});
}
Self {
id,
name,
desc,
date,
film,
exposures,
}
}

View File

@ -1,12 +1,12 @@
use chrono::NaiveDate;
use eframe::{
egui::{self, RichText, Window},
egui::{self, Button, RichText, Window},
epaint::Color32,
};
use egui_datepicker::DatePicker;
use itertools::Itertools;
use crate::AppState;
use crate::{film::FilmKey, AppState};
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| {
@ -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;
}
if self.confirm_delete {
@ -176,13 +210,15 @@ impl RollViewWindow {
"Really delete roll {}?",
app_state.rolls[self.roll].id
));
if ui.button("No").clicked() {
self.confirm_delete = false;
}
if ui.button("Yes").clicked() {
app_state.rolls.remove(self.roll);
close_window = true;
}
ui.horizontal(|ui| {
if ui.button("No").clicked() {
self.confirm_delete = false;
}
if ui.button("Yes").clicked() {
app_state.rolls.remove(self.roll);
close_window = true;
}
});
});
}
});