heic2jpg

git clone git://archive.git.mtrnord.blog/MTRNord/heic2jpg.git
Log | Files | Refs | README | LICENSE

app.rs (16009B)


      1 use crate::config::{APP_ID, PROFILE};
      2 use crate::modals::about::AboutDialog;
      3 use crate::pages::finished_page::{self, FinishedPage};
      4 use crate::pages::progressing_page::{ProgressingPage, ProgressingPageMsg};
      5 use crate::pages::select_folder::{InOut, SelectFolder, SelectFolderOut};
      6 use crate::workers::conversion_worker::{
      7     ConversionWorker, ConversionWorkerInputMsg, ConversionWorkerMsg,
      8 };
      9 use gettextrs::gettext;
     10 use gtk::prelude::*;
     11 use gtk::{gio, glib};
     12 use relm4::SimpleComponent;
     13 use relm4::{
     14     actions::{RelmAction, RelmActionGroup},
     15     adw, gtk, main_application, Component, ComponentController, ComponentParts, ComponentSender,
     16     Controller, WorkerController,
     17 };
     18 use std::path::PathBuf;
     19 use tracing::info;
     20 
     21 enum Mode {
     22     InputSelection,
     23     OutputSelection,
     24     ConversionSelection,
     25     Progressing,
     26     Finished,
     27     Failed,
     28 }
     29 
     30 pub(super) struct App {
     31     about_dialog: Controller<AboutDialog>,
     32     input_folder_selector: Controller<SelectFolder>,
     33     output_folder_selector: Controller<SelectFolder>,
     34     progressing_page: Controller<ProgressingPage>,
     35     finished_page: Controller<FinishedPage>,
     36     input_folder: Option<PathBuf>,
     37     output_folder: Option<PathBuf>,
     38     conversion_worker: WorkerController<ConversionWorker>,
     39     mode: Mode,
     40     failure: Option<String>,
     41 }
     42 
     43 #[derive(Debug)]
     44 pub(super) enum AppMsg {
     45     InputFolderSelected(PathBuf),
     46     OutputFolderSelected(PathBuf),
     47     DeselectInputFolder,
     48     DeselectOutputFolder,
     49     Convert,
     50     ConversionStarted(usize),
     51     ProgressUpdate(f64),
     52     ConversionComplete,
     53     ConversionFailed(String),
     54     StartOver,
     55     Quit,
     56     Noop,
     57 }
     58 
     59 relm4::new_action_group!(pub(super) WindowActionGroup, "win");
     60 relm4::new_stateless_action!(PreferencesAction, WindowActionGroup, "preferences");
     61 relm4::new_stateless_action!(pub(super) ShortcutsAction, WindowActionGroup, "show-help-overlay");
     62 relm4::new_stateless_action!(AboutAction, WindowActionGroup, "about");
     63 
     64 #[relm4::component(pub)]
     65 impl SimpleComponent for App {
     66     type Init = ();
     67     type Input = AppMsg;
     68     type Output = ();
     69     type Widgets = AppWidgets;
     70 
     71     menu! {
     72         primary_menu: {
     73             section! {
     74                 "_Preferences" => PreferencesAction,
     75                 "_Keyboard" => ShortcutsAction,
     76                 "_About Heic2JPG" => AboutAction,
     77             }
     78         }
     79     }
     80 
     81     view! {
     82         #[root]
     83         main_window = adw::ApplicationWindow::new(&main_application()) {
     84             set_visible: true,
     85             set_default_width: 650,
     86             set_default_height: 600,
     87             set_width_request: 360,
     88             set_height_request: 294,
     89 
     90             connect_close_request[sender] => move |_| {
     91                 sender.input(AppMsg::Quit);
     92                 glib::Propagation::Stop
     93             },
     94 
     95             #[wrap(Some)]
     96             set_help_overlay: shortcuts = &gtk::Builder::from_resource(
     97                     "/dev/nordgedanken/heic2jpg/gtk/help-overlay.ui"
     98                 )
     99                 .object::<gtk::ShortcutsWindow>("help_overlay")
    100                 .unwrap() -> gtk::ShortcutsWindow {
    101                     set_transient_for: Some(&main_window),
    102                     set_application: Some(&main_application()),
    103             },
    104 
    105             add_css_class?: if PROFILE == "Devel" {
    106                     Some("devel")
    107                 } else {
    108                     None
    109                 },
    110 
    111             adw::ToolbarView {
    112                add_top_bar = &adw::HeaderBar {
    113                     #[wrap(Some)]
    114                     set_title_widget = &adw::WindowTitle {
    115                         set_title: &gettext("Convert Heic to JPG"),
    116                     },
    117                     pack_end = &gtk::MenuButton {
    118                         set_icon_name: "open-menu-symbolic",
    119                         set_menu_model: Some(&primary_menu),
    120                     }
    121                 },
    122 
    123                 #[transition = "SlideRight"]
    124                 match model.mode {
    125                     Mode::Progressing => {
    126                         gtk::Box {
    127                             set_vexpand: true,
    128                             set_hexpand: true,
    129                             append = model.progressing_page.widget(),
    130                         }
    131                     }
    132                     Mode::Finished => {
    133                         gtk::Box {
    134                             set_vexpand: true,
    135                             set_hexpand: true,
    136                             append = model.finished_page.widget(),
    137                         }
    138                     }
    139                     Mode::Failed => {
    140                         adw::StatusPage {
    141                             set_hexpand: true,
    142                             set_vexpand: true,
    143                             set_title: &gettext("Conversion Failed"),
    144                             #[watch]
    145                             set_description: model.failure.as_deref(),
    146                             set_icon_name: Some("error-outline"),
    147 
    148                             gtk::Box {
    149                                 set_halign: gtk::Align::Center,
    150                                 set_orientation: gtk::Orientation::Horizontal,
    151                                 set_spacing: 24,
    152                                 gtk::Button {
    153                                     set_label: &gettext("Close"),
    154                                     connect_clicked[sender] => move |_| {
    155                                         sender.input(AppMsg::Quit);
    156                                     }
    157                                 },
    158                                 gtk::Button {
    159                                     set_label: "Restart",
    160                                     connect_clicked[sender] => move |_| {
    161                                         sender.input(AppMsg::StartOver);
    162                                     }
    163                                 }
    164                             }
    165                         }
    166                     }
    167                     Mode::InputSelection => {
    168                         gtk::Box {
    169                             set_vexpand: true,
    170                             set_hexpand: true,
    171                             append = model.input_folder_selector.widget(),
    172                         }
    173                     }
    174                     Mode::OutputSelection => {
    175                         gtk::Box {
    176                             set_vexpand: true,
    177                             set_hexpand: true,
    178                             append = model.output_folder_selector.widget(),
    179                         }
    180                     }
    181                     Mode::ConversionSelection => {
    182                         adw::StatusPage {
    183                             set_hexpand: true,
    184                             set_vexpand: true,
    185                             set_title: &gettext("Start Conversion"),
    186                             set_description: Some(&gettext("Click the button below to start the conversion")),
    187                             set_icon_name: Some("blend-tool"),
    188 
    189                             gtk::Box {
    190                                 set_halign: gtk::Align::Center,
    191                                 set_orientation: gtk::Orientation::Horizontal,
    192                                 set_spacing: 24,
    193                                 gtk::Button {
    194                                     set_label: &gettext("Convert"),
    195                                     add_css_class: "suggested-action",
    196                                     add_css_class: "pill",
    197                                     connect_clicked[sender] => move |_| {
    198                                         sender.input(AppMsg::Convert);
    199                                     }
    200                                 },
    201                                 gtk::Button {
    202                                     set_label: "Restart",
    203                                     connect_clicked[sender] => move |_| {
    204                                         sender.input(AppMsg::StartOver);
    205                                     }
    206                                 }
    207                             }
    208                         }
    209                     }
    210                 }
    211             }
    212 
    213         }
    214     }
    215 
    216     fn init(
    217         _init: Self::Init,
    218         root: Self::Root,
    219         sender: ComponentSender<Self>,
    220     ) -> ComponentParts<Self> {
    221         let about_dialog = AboutDialog::builder()
    222             .transient_for(&root)
    223             .launch(())
    224             .detach();
    225         let input_folder_selector =
    226             SelectFolder::builder()
    227                 .launch(InOut::Input)
    228                 .forward(sender.input_sender(), |msg| match msg {
    229                     SelectFolderOut::FolderSelected(path) => AppMsg::InputFolderSelected(path),
    230                     SelectFolderOut::AbortLast => AppMsg::Noop,
    231                 });
    232         let output_folder_selector =
    233             SelectFolder::builder()
    234                 .launch(InOut::Output)
    235                 .forward(sender.input_sender(), |msg| match msg {
    236                     SelectFolderOut::FolderSelected(path) => AppMsg::OutputFolderSelected(path),
    237                     SelectFolderOut::AbortLast => AppMsg::DeselectInputFolder,
    238                 });
    239         let conversion_worker =
    240             ConversionWorker::builder()
    241                 .detach_worker(())
    242                 .forward(sender.input_sender(), |msg| match msg {
    243                     ConversionWorkerMsg::ConversionStarted(number) => {
    244                         AppMsg::ConversionStarted(number)
    245                     }
    246                     ConversionWorkerMsg::ProgressUpdate(number) => AppMsg::ProgressUpdate(number),
    247                     ConversionWorkerMsg::ConversionComplete => AppMsg::ConversionComplete,
    248                     ConversionWorkerMsg::ConversionFailed(e) => AppMsg::ConversionFailed(e),
    249                 });
    250 
    251         let progressing_page = ProgressingPage::builder().launch(()).detach();
    252         let finished_page =
    253             FinishedPage::builder()
    254                 .launch(())
    255                 .forward(sender.input_sender(), |msg| match msg {
    256                     finished_page::FinishedPageMsg::StartOver => AppMsg::StartOver,
    257                     finished_page::FinishedPageMsg::Quit => AppMsg::Quit,
    258                 });
    259 
    260         let model = Self {
    261             about_dialog,
    262             input_folder_selector,
    263             output_folder_selector,
    264             progressing_page,
    265             finished_page,
    266             conversion_worker,
    267             input_folder: None,
    268             output_folder: None,
    269             mode: Mode::InputSelection,
    270             failure: None,
    271         };
    272 
    273         let widgets = view_output!();
    274 
    275         let mut actions = RelmActionGroup::<WindowActionGroup>::new();
    276 
    277         let shortcuts_action = {
    278             let shortcuts = widgets.shortcuts.clone();
    279             RelmAction::<ShortcutsAction>::new_stateless(move |_| {
    280                 shortcuts.present();
    281             })
    282         };
    283 
    284         let about_action = {
    285             let sender = model.about_dialog.sender().clone();
    286             RelmAction::<AboutAction>::new_stateless(move |_| {
    287                 sender.send(()).unwrap();
    288             })
    289         };
    290 
    291         actions.add_action(shortcuts_action);
    292         actions.add_action(about_action);
    293         actions.register_for_widget(&widgets.main_window);
    294 
    295         widgets.load_window_size();
    296 
    297         ComponentParts { model, widgets }
    298     }
    299 
    300     fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
    301         match message {
    302             AppMsg::ConversionStarted(number) => {
    303                 self.mode = Mode::Progressing;
    304                 let _ = self
    305                     .progressing_page
    306                     .sender()
    307                     .send(ProgressingPageMsg::SetFileCount(number));
    308             }
    309             AppMsg::InputFolderSelected(path) => {
    310                 self.input_folder = Some(path);
    311                 self.mode = Mode::OutputSelection
    312             }
    313             AppMsg::DeselectInputFolder => {
    314                 self.input_folder = None;
    315                 self.mode = Mode::InputSelection
    316             }
    317             AppMsg::OutputFolderSelected(path) => {
    318                 self.output_folder = Some(path);
    319                 self.mode = Mode::ConversionSelection
    320             }
    321             AppMsg::DeselectOutputFolder => {
    322                 self.output_folder = None;
    323                 self.mode = Mode::OutputSelection
    324             }
    325             AppMsg::Quit => main_application().quit(),
    326             AppMsg::Convert => {
    327                 if let (Some(input_folder), Some(output_folder)) =
    328                     (&self.input_folder, &self.output_folder)
    329                 {
    330                     info!(
    331                         "Starting conversion from {:?} to {:?}",
    332                         input_folder, output_folder
    333                     );
    334                     let _ = self.conversion_worker.sender().send(
    335                         ConversionWorkerInputMsg::ConvertFolder(
    336                             input_folder.clone(),
    337                             output_folder.clone(),
    338                         ),
    339                     );
    340                 } else {
    341                     self.mode = Mode::Failed;
    342                     self.failure =
    343                         Some(gettext("Please select both input and output folders").to_string());
    344                 }
    345             }
    346             AppMsg::ProgressUpdate(progress) => {
    347                 if let Mode::Progressing = self.mode {
    348                     let _ = self
    349                         .progressing_page
    350                         .sender()
    351                         .send(ProgressingPageMsg::SetProgress(progress));
    352                 }
    353             }
    354             AppMsg::ConversionComplete => {
    355                 self.mode = Mode::Finished;
    356                 let notification = gio::Notification::new(&gettext("Conversion Complete"));
    357                 notification.set_body(Some(&gettext("The conversion was successful")));
    358                 notification.set_category(Some("transfer.complete"));
    359                 notification.set_priority(gio::NotificationPriority::Normal);
    360                 let pass_icon = gio::Icon::for_string("test-pass");
    361                 if let Ok(pass_icon) = pass_icon {
    362                     // Ends up as an empty icon on my system. Something is wrong with the icon loading?.
    363                     //notification.set_icon(&pass_icon);
    364                 }
    365 
    366                 let app = relm4::main_application();
    367                 app.send_notification(None, &notification);
    368             }
    369             AppMsg::ConversionFailed(e) => {
    370                 self.mode = Mode::Failed;
    371                 self.failure = Some(e);
    372             }
    373             AppMsg::StartOver => {
    374                 self.input_folder = None;
    375                 self.output_folder = None;
    376                 let _ = self
    377                     .progressing_page
    378                     .sender()
    379                     .send(ProgressingPageMsg::SetProgress(0.0));
    380                 self.failure = None;
    381                 let _ = self
    382                     .progressing_page
    383                     .sender()
    384                     .send(ProgressingPageMsg::SetFileCount(0));
    385                 self.mode = Mode::InputSelection;
    386             }
    387             AppMsg::Noop => {}
    388         }
    389     }
    390 
    391     fn shutdown(&mut self, widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
    392         widgets.save_window_size().unwrap();
    393     }
    394 }
    395 
    396 impl AppWidgets {
    397     fn save_window_size(&self) -> Result<(), glib::BoolError> {
    398         let settings = gio::Settings::new(APP_ID);
    399         let (width, height) = self.main_window.default_size();
    400 
    401         settings.set_int("window-width", width)?;
    402         settings.set_int("window-height", height)?;
    403 
    404         settings.set_boolean("is-maximized", self.main_window.is_maximized())?;
    405 
    406         Ok(())
    407     }
    408 
    409     fn load_window_size(&self) {
    410         let settings = gio::Settings::new(APP_ID);
    411 
    412         let width = settings.int("window-width");
    413         let height = settings.int("window-height");
    414         let is_maximized = settings.boolean("is-maximized");
    415 
    416         self.main_window.set_default_size(width, height);
    417 
    418         if is_maximized {
    419             self.main_window.maximize();
    420         }
    421     }
    422 }