Implement sheet listings with relm4 factory

This commit is contained in:
Julian Mutter 2024-02-02 21:11:21 +01:00
parent 6a6e1d03df
commit 9069138255
7 changed files with 126 additions and 117 deletions

View File

@ -49,9 +49,9 @@ impl Database {
.map(|_| ()) .map(|_| ())
} }
pub async fn update_sheet_path(&self, sheet: Sheet) -> sqlx::Result<()> { pub async fn update_sheet_path(&self, sheet: &Sheet) -> sqlx::Result<()> {
sqlx::query("UPDATE sheets SET path = $1 WHERE id = $2") sqlx::query("UPDATE sheets SET path = $1 WHERE id = $2")
.bind(sheet.path) .bind(sheet.path.clone())
.bind(sheet.id) .bind(sheet.id)
.execute(&self.connection) .execute(&self.connection)
.await .await

View File

@ -39,16 +39,16 @@ async fn main() {
debug!("Validating sheets from database..."); debug!("Validating sheets from database...");
let validation_result = validate_sheet_files(sheets, &cli.directory); let validation_result = validate_sheet_files(sheets, &cli.directory);
debug!("{}", validation_result.get_stats()); debug!("{}", validation_result.get_stats());
for updated in validation_result.updated_sheets { for updated in validation_result.updated_sheets.iter() {
database.update_sheet_path(updated).await.unwrap(); database.update_sheet_path(updated).await.unwrap();
} }
let app = RelmApp::new("de.frajul.sheet-organizer"); let app = RelmApp::new("de.frajul.sheet-organizer");
// Pass empty command line args to allow my own parsing // Pass empty command line args to allow my own parsing
app.with_args(Vec::new()).run::<AppModel>(cli.directory); app.with_args(Vec::new()).run::<AppModel>(validation_result);
} }
struct FileValidationResult { pub struct FileValidationResult {
validated_sheets: Vec<Sheet>, validated_sheets: Vec<Sheet>,
invalidated_sheets: Vec<Sheet>, invalidated_sheets: Vec<Sheet>,
updated_sheets: Vec<Sheet>, updated_sheets: Vec<Sheet>,

1
src/sheet_listing.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -1,9 +1,12 @@
use std::path::PathBuf; use std::path::PathBuf;
use gtk::prelude::*; use gtk::prelude::*;
use relm4::prelude::*; use relm4::{component::Connector, prelude::*};
use crate::ui::{mcdu::McduOutput, sheet_listing::SheetPressedMessage}; use crate::{
ui::{mcdu::McduOutput, sheet_model::SheetModelType},
FileValidationResult,
};
use super::{ use super::{
mcdu::McduModel, mcdu::McduModel,
@ -12,7 +15,8 @@ use super::{
pub struct AppModel { pub struct AppModel {
mcdu: Controller<McduModel>, mcdu: Controller<McduModel>,
sheet_listing: Controller<SheetListingModel>, sheets_and_files_listing: Connector<SheetListingModel>,
new_files_listing: Connector<SheetListingModel>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -25,7 +29,7 @@ pub enum AppInput {
impl SimpleComponent for AppModel { impl SimpleComponent for AppModel {
type Input = AppInput; type Input = AppInput;
type Output = (); type Output = ();
type Init = PathBuf; type Init = FileValidationResult;
view! { view! {
#[root] #[root]
@ -39,11 +43,23 @@ impl SimpleComponent for AppModel {
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
set_hexpand: true, set_hexpand: true,
gtk::ScrolledWindow { #[name = "stack_switcher"]
model.sheet_listing.widget(), gtk::StackSwitcher {
set_vexpand: true, set_stack: Some(&stack),
set_hexpand: true, set_margin_all: 10,
// set_hscrollbar_policy: PolicyType::Never, },
#[name = "stack"]
gtk::Stack {
add_titled[None, "Sheets & Files"]= &gtk::ScrolledWindow {
model.sheets_and_files_listing.widget(),
set_vexpand: true,
set_hexpand: true,
},
add_titled[None, "New Files"]= &gtk::ScrolledWindow {
model.new_files_listing.widget(),
set_vexpand: true,
set_hexpand: true,
},
}, },
}, },
model.mcdu.widget(), model.mcdu.widget(),
@ -52,7 +68,7 @@ impl SimpleComponent for AppModel {
} }
fn init( fn init(
path: Self::Init, file_validation_result: Self::Init,
window: &Self::Root, window: &Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> relm4::ComponentParts<Self> { ) -> relm4::ComponentParts<Self> {
@ -64,16 +80,37 @@ impl SimpleComponent for AppModel {
McduOutput::SearchStarted(query) => AppInput::SearchStarted(query), McduOutput::SearchStarted(query) => AppInput::SearchStarted(query),
}); });
let sheet_listing = let new_files: Vec<SheetModelType> = file_validation_result
SheetListingModel::builder() .unassigned_files
.launch(path) .iter()
.forward(sender.input_sender(), |response| match response { .map(|file| SheetModelType::Pdf {
SheetPressedMessage::SheetPressed(path) => AppInput::SheetPressed(path), path: file.to_path_buf(),
}); })
.collect();
let new_files_clone_iter = file_validation_result
.unassigned_files
.into_iter()
.map(|file| SheetModelType::Pdf { path: file });
let sheets_and_files: Vec<SheetModelType> = file_validation_result
.validated_sheets
.into_iter()
.chain(file_validation_result.updated_sheets.into_iter())
.map(|sheet| SheetModelType::Sheet { sheet })
.chain(new_files_clone_iter)
.collect();
let sheets_and_files_listing = SheetListingModel::builder().launch(sheets_and_files);
let new_files_listing = SheetListingModel::builder().launch(new_files);
// .forward(sender.input_sender(), |_| {});
// .forward(sender.input_sender(), |response| match response {
// SheetPressedMessage::SheetPressed(path) => AppInput::SheetPressed(path),
// });
let model = AppModel { let model = AppModel {
mcdu, mcdu,
sheet_listing, sheets_and_files_listing,
new_files_listing,
}; };
let widgets = view_output!(); let widgets = view_output!();
@ -84,7 +121,7 @@ impl SimpleComponent for AppModel {
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) { fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message { match message {
AppInput::SheetPressed(sheet) => opener::open(sheet).unwrap(), AppInput::SheetPressed(sheet) => opener::open(sheet).unwrap(),
AppInput::SearchStarted(query) => self.sheet_listing.emit(SheetListingInput { query }), AppInput::SearchStarted(query) => {} //self.sheet_listing.emit(SheetListingInput { query }),
} }
} }
} }

View File

@ -1,3 +1,4 @@
pub mod app; pub mod app;
pub mod mcdu; pub mod mcdu;
pub mod sheet_listing; pub mod sheet_listing;
pub mod sheet_model;

View File

@ -1,83 +1,18 @@
use std::path::PathBuf; use std::path::PathBuf;
use gtk::prelude::*; use gtk::prelude::*;
use relm4::factory::FactoryVecDeque;
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{ use relm4::{
gtk, Component, ComponentController, ComponentParts, ComponentSender, SimpleComponent, gtk, Component, ComponentController, ComponentParts, ComponentSender, SimpleComponent,
}; };
use walkdir::WalkDir; use walkdir::WalkDir;
pub struct SheetModel { use super::sheet_model::{SheetModel, SheetModelType};
path: PathBuf,
visible: bool,
}
#[derive(Debug)]
pub enum SheetModelInput {
OnClicked,
SearchChanged(String),
}
#[derive(Debug)]
pub enum SheetPressedMessage {
SheetPressed(PathBuf),
}
#[relm4::component(pub)]
impl SimpleComponent for SheetModel {
type Init = PathBuf;
type Input = SheetModelInput;
type Output = SheetPressedMessage;
view! {
#[root]
gtk::Box {
#[watch]
set_visible: model.visible,
set_orientation: gtk::Orientation::Vertical,
append = &gtk::Button {
set_label: &format!("{}", model.path.file_name().unwrap().to_string_lossy()),
set_halign: gtk::Align::Start,
set_margin_all: 10,
connect_clicked[sender] => move |_| sender.input(SheetModelInput::OnClicked),
}
}
}
fn init(
path: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = SheetModel {
path,
visible: true,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
SheetModelInput::OnClicked => sender
.output(SheetPressedMessage::SheetPressed(self.path.clone()))
.unwrap(),
SheetModelInput::SearchChanged(query) => {
self.visible = self
.path
.file_name()
.unwrap()
.to_string_lossy()
.to_lowercase()
.contains(&query.to_lowercase());
}
}
}
}
pub struct SheetListingModel { pub struct SheetListingModel {
query: String, query: String,
sheet_models: Vec<Controller<SheetModel>>, sheets: FactoryVecDeque<SheetModel>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -87,45 +22,35 @@ pub struct SheetListingInput {
#[relm4::component(pub)] #[relm4::component(pub)]
impl SimpleComponent for SheetListingModel { impl SimpleComponent for SheetListingModel {
type Init = PathBuf; type Init = Vec<SheetModelType>;
type Input = SheetListingInput; type Input = SheetListingInput;
type Output = SheetPressedMessage; type Output = ();
view! { view! {
#[root] #[root]
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, // set_orientation: gtk::Orientation::Vertical,
model.sheets.widget() -> &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
},
} }
} }
fn init( fn init(
dir: Self::Init, init: Self::Init,
root: &Self::Root, root: &Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let mut sheet_models = Vec::new(); let mut sheets = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
for sheet_model_type in init {
for entry in WalkDir::new(dir) sheets.guard().push_back(sheet_model_type);
.into_iter()
.filter_map(|e| e.ok())
.filter(|file| file.file_type().is_file())
.map(|file| file.into_path())
.filter(|path| {
path.extension()
.map(|s| s.to_string_lossy().to_ascii_lowercase() == "pdf")
.unwrap_or(false)
})
{
let sheet_model = SheetModel::builder()
.launch(entry)
.forward(sender.output_sender(), |m| m);
root.append(sheet_model.widget());
sheet_models.push(sheet_model);
} }
let model = SheetListingModel { let model = SheetListingModel {
query: String::new(), query: String::new(),
sheet_models, sheets,
}; };
let widgets = view_output!(); let widgets = view_output!();
ComponentParts { model, widgets } ComponentParts { model, widgets }
@ -133,8 +58,5 @@ impl SimpleComponent for SheetListingModel {
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) { fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
self.query = message.query; self.query = message.query;
for model in self.sheet_models.iter() {
model.emit(SheetModelInput::SearchChanged(self.query.clone()));
}
} }
} }

48
src/ui/sheet_model.rs Normal file
View File

@ -0,0 +1,48 @@
use std::path::PathBuf;
use gtk::prelude::*;
use relm4::prelude::*;
use crate::sheet::Sheet;
use super::sheet_listing::SheetListingInput;
pub enum SheetModelType {
Sheet { sheet: Sheet },
Pdf { path: PathBuf },
}
pub struct SheetModel {
label: String,
}
#[relm4::factory(pub)]
impl FactoryComponent for SheetModel {
type Init = SheetModelType;
type ParentWidget = gtk::Box;
type CommandOutput = ();
type ParentInput = SheetListingInput;
type Input = ();
type Output = ();
view! {
#[root]
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
append = &gtk::Label {
set_label: &self.label,
set_halign: gtk::Align::Start,
set_margin_all: 10,
}
}
}
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let label = match value {
SheetModelType::Sheet { sheet } => sheet.name,
SheetModelType::Pdf { path } => path.file_name().unwrap().to_str().unwrap().to_string(),
};
SheetModel { label }
}
}