fb2-clean 0.3.1

A simple GUI/CLI utility to clean fb2 books.
Documentation
mod logger;

use crate::{Config, Input, InputFile, Lang, Msg, Output, Result, Tags, msg};
use eframe::egui;
use log::{error, info};
use logger::{GuiLog, GuiLogger};
use std::path::Path;

#[derive(Debug)]
pub struct App {
    cfg: Config,
    log: GuiLog,
    input_buf: String,
    output_buf: String,
    is_output_set: bool,
    tags_buf: String,
    lang: Lang,
}

impl Default for App {
    fn default() -> App {
        let cfg = Config::default();
        App {
            log: Default::default(),
            input_buf: cfg.input.display().to_string(),
            output_buf: cfg.output.dir.display().to_string(),
            is_output_set: false,
            tags_buf: cfg.tags.to_string(),
            cfg,
            lang: Default::default(),
        }
    }
}

impl App {
    pub fn init_logger(&self, ctx: &egui::Context) {
        let logger = GuiLogger {
            buf: self.log.clone(),
            ctx: ctx.clone(),
        };
        if log::set_boxed_logger(Box::new(logger)).is_ok() {
            log::set_max_level(log::LevelFilter::Info);
        }
    }
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);

            ui.horizontal(|ui| {
                if ui
                    .add_sized(
                        [328.0, 32.0],
                        egui::Button::new(egui::RichText::new(msg!(GuiStart)).size(18.0)),
                    )
                    .clicked()
                {
                    let mut cfg = self.cfg.clone();
                    std::thread::spawn(move || {
                        if let Err(e) = start(&mut cfg) {
                            error!("{}", e);
                        }
                    });
                }
            });
            ui.separator();

            let old_lang = self.lang;
            egui::ComboBox::from_label(msg!(GuiLanguage))
                .selected_text(match self.lang {
                    Lang::Eng => "English",
                    Lang::Rus => "Русский",
                })
                .show_ui(ui, |ui| {
                    ui.selectable_value(&mut self.lang, Lang::Eng, "English");
                    ui.selectable_value(&mut self.lang, Lang::Rus, "Русский");
                });
            if self.lang != old_lang {
                let _ = Msg::set_lang(self.lang);
            }

            ui.add_space(10.0);
            ui.horizontal(|ui| {
                if ui.button(msg!(GuiSelectInputDirectory)).clicked() {
                    if let Some(path) = rfd::FileDialog::new().pick_folder() {
                        self.set_input(Input::Dir(path.into()));
                    }
                }
                if ui.button(msg!(GuiSelectInputFile)).clicked() {
                    if let Some(path) = rfd::FileDialog::new().pick_file() {
                        self.set_input_from_buf(path);
                    }
                }
            });
            if ui
                .text_edit_singleline(&mut self.input_buf)
                .on_hover_text(msg!(HelpInput))
                .lost_focus()
            {
                self.set_input_from_buf(self.input_buf.clone())
            }
            ui.add_space(10.0);

            ui.add_enabled_ui(!self.cfg.force, |ui| {
                if ui.button(msg!(GuiSelectOutputDirectory)).clicked() {
                    if let Some(path) = rfd::FileDialog::new().pick_folder() {
                        self.set_output(path);
                    }
                }
                if ui
                    .text_edit_singleline(&mut self.output_buf)
                    .on_hover_text(msg!(GuiSaveDirectory))
                    .lost_focus()
                {
                    match Output::new(&self.output_buf) {
                        Ok(output) => self.set_output(output.dir),
                        Err(e) => error!("{}: {}", Msg::GuiErrorSetOutput, e),
                    };
                }
            });
            ui.add_space(10.0);

            ui.label(msg!(GuiRemoveTags)).on_hover_text(msg!(HelpTags));
            if ui.text_edit_singleline(&mut self.tags_buf).lost_focus() {
                let new_tags = Tags::new(&self.tags_buf);
                self.tags_buf = new_tags.to_string();
                if new_tags != self.cfg.tags {
                    self.cfg.tags = new_tags;
                    info!("{}: '{}'", Msg::GuiTagsSet, self.tags_buf);
                }
            }
            ui.add_space(10.0);

            let input_is_dir = matches!(self.cfg.input, Input::Dir(_));

            ui.add_enabled_ui(input_is_dir, |ui| {
                ui.horizontal(|ui| {
                    ui.label(msg!(GuiMultithreading));
                    ui.add(egui::DragValue::new(&mut self.cfg.jobs).range(1..=255));
                });

                ui.horizontal(|ui| {
                    ui.label(msg!(GuiRecursiveSearch))
                        .on_hover_text(msg!(HelpRecursive));
                    ui.add(egui::DragValue::new(&mut self.cfg.recursive).range(0..=255));
                });
            });
            ui.add_space(10.0);

            ui.horizontal(|ui| {
                ui.add_enabled_ui(!self.cfg.unzip, |ui| {
                    ui.checkbox(&mut self.cfg.zip, "zip")
                        .on_hover_text(msg!(HelpZip));
                });
                ui.add_enabled_ui(!self.cfg.zip, |ui| {
                    ui.checkbox(&mut self.cfg.unzip, "unzip")
                        .on_hover_text(msg!(HelpUnzip));
                });
            });

            ui.checkbox(&mut self.cfg.force, msg!(GuiOverwrite))
                .on_hover_text(msg!(HelpForce));

            ui.add_enabled_ui(input_is_dir, |ui| {
                ui.checkbox(&mut self.cfg.exit_on_err, msg!(GuiStopOnError))
                    .on_hover_text(msg!(HelpExitOnError));
            });

            ui.separator();
            ui.label(msg!(GuiLog));
            egui::ScrollArea::vertical()
                .stick_to_bottom(true)
                .max_height(150.0)
                .show(ui, |ui| {
                    if let Ok(log) = self.log.try_lock() {
                        for l in log.iter() {
                            ui.label(l);
                        }
                    }
                });
        });
    }
}

impl App {
    fn set_input_from_buf(&mut self, buf: impl AsRef<Path>) {
        match Input::new(buf) {
            Ok(input) => self.set_input(input),
            Err(e) => error!("{}: '{}'", Msg::GuiErrorSetInput, e),
        }
    }

    fn set_input(&mut self, input: Input) {
        self.input_buf = input.display().to_string();
        if input != self.cfg.input {
            self.cfg.input = input;
            info!("{}: '{}'", Msg::GuiInputSet, self.input_buf);
        }

        if !self.is_output_set {
            if let Ok(output) = Output::try_from_input(&self.cfg.input) {
                self.set_output(output.dir);
                self.is_output_set = false;
            }
        }
    }

    fn set_output(&mut self, dir: impl Into<Box<Path>>) {
        let new = Output {
            dir: dir.into(),
            ..Default::default()
        };
        self.output_buf = new.dir.display().to_string();
        if new != self.cfg.output {
            self.cfg.output = new;
            self.is_output_set = true;
            info!("{}: '{}'", Msg::GuiOutputSet, self.output_buf);
        }
    }
}

fn start(cfg: &mut Config) -> Result<()> {
    cfg.output.create_dirs()?;
    let res = cfg.run();
    cfg.output.remove_created_dirs();
    res
}

impl Input {
    fn display(&self) -> std::path::Display<'_> {
        let path = match self {
            Self::Dir(path) => path,
            Self::File(InputFile { path, .. }) => path,
        };
        path.display()
    }
}