From 90691382553e0f82a978895b8989da1f5959fe7b Mon Sep 17 00:00:00 2001
From: Julian Mutter <julian.mutter@comumail.de>
Date: Fri, 2 Feb 2024 21:11:21 +0100
Subject: [PATCH] Implement sheet listings with relm4 factory

---
 src/database.rs         |   4 +-
 src/main.rs             |   6 +--
 src/sheet_listing.rs    |   1 +
 src/ui/app.rs           |  73 +++++++++++++++++++-------
 src/ui/mod.rs           |   1 +
 src/ui/sheet_listing.rs | 110 ++++++----------------------------------
 src/ui/sheet_model.rs   |  48 ++++++++++++++++++
 7 files changed, 126 insertions(+), 117 deletions(-)
 create mode 100644 src/sheet_listing.rs
 create mode 100644 src/ui/sheet_model.rs

diff --git a/src/database.rs b/src/database.rs
index 3f6eebc..d73b93f 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -49,9 +49,9 @@ impl Database {
         .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")
-            .bind(sheet.path)
+            .bind(sheet.path.clone())
             .bind(sheet.id)
             .execute(&self.connection)
             .await
diff --git a/src/main.rs b/src/main.rs
index 328e440..3f8f19d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -39,16 +39,16 @@ async fn main() {
     debug!("Validating sheets from database...");
     let validation_result = validate_sheet_files(sheets, &cli.directory);
     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();
     }
 
     let app = RelmApp::new("de.frajul.sheet-organizer");
     // 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>,
     invalidated_sheets: Vec<Sheet>,
     updated_sheets: Vec<Sheet>,
diff --git a/src/sheet_listing.rs b/src/sheet_listing.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/sheet_listing.rs
@@ -0,0 +1 @@
+
diff --git a/src/ui/app.rs b/src/ui/app.rs
index 7ac072d..a137148 100644
--- a/src/ui/app.rs
+++ b/src/ui/app.rs
@@ -1,9 +1,12 @@
 use std::path::PathBuf;
 
 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::{
     mcdu::McduModel,
@@ -12,7 +15,8 @@ use super::{
 
 pub struct AppModel {
     mcdu: Controller<McduModel>,
-    sheet_listing: Controller<SheetListingModel>,
+    sheets_and_files_listing: Connector<SheetListingModel>,
+    new_files_listing: Connector<SheetListingModel>,
 }
 
 #[derive(Debug)]
@@ -25,7 +29,7 @@ pub enum AppInput {
 impl SimpleComponent for AppModel {
     type Input = AppInput;
     type Output = ();
-    type Init = PathBuf;
+    type Init = FileValidationResult;
 
     view! {
         #[root]
@@ -39,11 +43,23 @@ impl SimpleComponent for AppModel {
                 gtk::Box {
                     set_orientation: gtk::Orientation::Vertical,
                     set_hexpand: true,
-                    gtk::ScrolledWindow {
-                        model.sheet_listing.widget(),
-                        set_vexpand: true,
-                        set_hexpand: true,
-                        // set_hscrollbar_policy: PolicyType::Never,
+                    #[name = "stack_switcher"]
+                    gtk::StackSwitcher {
+                        set_stack: Some(&stack),
+                        set_margin_all: 10,
+                    },
+                    #[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(),
@@ -52,7 +68,7 @@ impl SimpleComponent for AppModel {
     }
 
     fn init(
-        path: Self::Init,
+        file_validation_result: Self::Init,
         window: &Self::Root,
         sender: ComponentSender<Self>,
     ) -> relm4::ComponentParts<Self> {
@@ -64,16 +80,37 @@ impl SimpleComponent for AppModel {
                 McduOutput::SearchStarted(query) => AppInput::SearchStarted(query),
             });
 
-        let sheet_listing =
-            SheetListingModel::builder()
-                .launch(path)
-                .forward(sender.input_sender(), |response| match response {
-                    SheetPressedMessage::SheetPressed(path) => AppInput::SheetPressed(path),
-                });
+        let new_files: Vec<SheetModelType> = file_validation_result
+            .unassigned_files
+            .iter()
+            .map(|file| SheetModelType::Pdf {
+                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 {
             mcdu,
-            sheet_listing,
+            sheets_and_files_listing,
+            new_files_listing,
         };
 
         let widgets = view_output!();
@@ -84,7 +121,7 @@ impl SimpleComponent for AppModel {
     fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
         match message {
             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 }),
         }
     }
 }
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 3bb5055..5f3a11e 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -1,3 +1,4 @@
 pub mod app;
 pub mod mcdu;
 pub mod sheet_listing;
+pub mod sheet_model;
diff --git a/src/ui/sheet_listing.rs b/src/ui/sheet_listing.rs
index 47724b0..d1ec6a6 100644
--- a/src/ui/sheet_listing.rs
+++ b/src/ui/sheet_listing.rs
@@ -1,83 +1,18 @@
 use std::path::PathBuf;
 
 use gtk::prelude::*;
+use relm4::factory::FactoryVecDeque;
 use relm4::prelude::*;
 use relm4::{
     gtk, Component, ComponentController, ComponentParts, ComponentSender, SimpleComponent,
 };
 use walkdir::WalkDir;
 
-pub struct SheetModel {
-    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());
-            }
-        }
-    }
-}
+use super::sheet_model::{SheetModel, SheetModelType};
 
 pub struct SheetListingModel {
     query: String,
-    sheet_models: Vec<Controller<SheetModel>>,
+    sheets: FactoryVecDeque<SheetModel>,
 }
 
 #[derive(Debug)]
@@ -87,45 +22,35 @@ pub struct SheetListingInput {
 
 #[relm4::component(pub)]
 impl SimpleComponent for SheetListingModel {
-    type Init = PathBuf;
+    type Init = Vec<SheetModelType>;
     type Input = SheetListingInput;
-    type Output = SheetPressedMessage;
+    type Output = ();
 
     view! {
         #[root]
         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(
-        dir: Self::Init,
+        init: Self::Init,
         root: &Self::Root,
         sender: ComponentSender<Self>,
     ) -> ComponentParts<Self> {
-        let mut sheet_models = Vec::new();
-
-        for entry in WalkDir::new(dir)
-            .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 mut sheets = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
+        for sheet_model_type in init {
+            sheets.guard().push_back(sheet_model_type);
         }
 
         let model = SheetListingModel {
             query: String::new(),
-            sheet_models,
+            sheets,
         };
         let widgets = view_output!();
         ComponentParts { model, widgets }
@@ -133,8 +58,5 @@ impl SimpleComponent for SheetListingModel {
 
     fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
         self.query = message.query;
-        for model in self.sheet_models.iter() {
-            model.emit(SheetModelInput::SearchChanged(self.query.clone()));
-        }
     }
 }
diff --git a/src/ui/sheet_model.rs b/src/ui/sheet_model.rs
new file mode 100644
index 0000000..7c868a2
--- /dev/null
+++ b/src/ui/sheet_model.rs
@@ -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 }
+    }
+}