commit c4089b5f0db330968b8f07ecc688b7375dba128f
parent afb93e5a856d96e46f090ba1fd0a099d270a8522
Author: MTRNord <mtrnord1@gmail.com>
Date: Tue, 12 Nov 2024 15:30:56 +0100
Cleanup code and add icons
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
19 files changed, 496 insertions(+), 329 deletions(-)
diff --git a/build-aux/dev.nordgedanken.heic2jpg.Devel.json b/build-aux/dev.nordgedanken.heic2jpg.Devel.json
@@ -10,6 +10,7 @@
"command": "heic2jpg",
"finish-args": [
"--share=ipc",
+ "--filesystem=host",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
@@ -51,4 +52,4 @@
]
}
]
-}
+}
+\ No newline at end of file
diff --git a/data/dev.nordgedanken.heic2jpg.gschema.xml.in b/data/dev.nordgedanken.heic2jpg.gschema.xml.in
@@ -2,11 +2,11 @@
<schemalist>
<schema path="/dev/nordgedanken/heic2jpg/" id="@app-id@" gettext-domain="@gettext-package@">
<key name="window-width" type="i">
- <default>600</default>
+ <default>1</default>
<summary>Window width</summary>
</key>
<key name="window-height" type="i">
- <default>400</default>
+ <default>-1</default>
<summary>Window height</summary>
</key>
<key name="is-maximized" type="b">
diff --git a/data/resources/icons/scalable/actions/blend-tool-symbolic.svg b/data/resources/icons/scalable/actions/blend-tool-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 6 11 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -4 -1 h 3 v 10 h -3 z m 0 0"/><path d="m 8 3 h 1 v 10 h -1 z m 2 9 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m -1 7 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 0" fill-opacity="0.35"/><path d="m 3.5 1 c -1.375 0 -2.5 1.125 -2.5 2.5 v 9 c 0 1.375 1.125 2.5 2.5 2.5 h 9 c 1.375 0 2.5 -1.125 2.5 -2.5 v -9 c 0 -1.375 -1.125 -2.5 -2.5 -2.5 z m 0 1 h 9 c 0.839844 0 1.5 0.660156 1.5 1.5 v 9 c 0 0.839844 -0.660156 1.5 -1.5 1.5 h -9 c -0.839844 0 -1.5 -0.660156 -1.5 -1.5 v -9 c 0 -0.839844 0.660156 -1.5 1.5 -1.5 z m 0 0"/></g></svg>
diff --git a/data/resources/icons/scalable/actions/error-outline-symbolic.svg b/data/resources/icons/scalable/actions/error-outline-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 4 7.011719 h 8 v 2 h -8 z m 0 0"/><path d="m 8 0 c -4.40625 0 -8 3.59375 -8 8 s 3.59375 8 8 8 s 8 -3.59375 8 -8 s -3.59375 -8 -8 -8 z m 0 2 c 3.324219 0 6 2.675781 6 6 s -2.675781 6 -6 6 s -6 -2.675781 -6 -6 s 2.675781 -6 6 -6 z m 0 0"/></g></svg>
diff --git a/data/resources/icons/scalable/actions/mouse-click-symbolic.svg b/data/resources/icons/scalable/actions/mouse-click-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 4.199219 0.105469 c -0.160157 0.007812 -0.3125 0.050781 -0.449219 0.132812 c -0.230469 0.128907 -0.398438 0.347657 -0.464844 0.605469 c -0.070312 0.257812 -0.035156 0.53125 0.101563 0.761719 l 0.5 0.863281 c 0.273437 0.476562 0.886719 0.640625 1.363281 0.363281 c 0.476562 -0.273437 0.640625 -0.886719 0.363281 -1.363281 l -0.5 -0.863281 c -0.1875 -0.328125 -0.539062 -0.5195315 -0.914062 -0.5 z m 4.601562 0 c -0.375 -0.0195315 -0.726562 0.171875 -0.914062 0.5 l -0.5 0.863281 c -0.277344 0.476562 -0.113281 1.089844 0.363281 1.363281 c 0.476562 0.277344 1.089844 0.113281 1.363281 -0.363281 l 0.5 -0.863281 c 0.136719 -0.230469 0.171875 -0.503907 0.101563 -0.761719 c -0.066406 -0.257812 -0.234375 -0.476562 -0.464844 -0.605469 c -0.136719 -0.082031 -0.289062 -0.125 -0.449219 -0.132812 z m -6.800781 3.894531 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 1 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 8 0 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 1 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m -2.136719 2.011719 c -0.09375 0.011719 -0.1875 0.035156 -0.273437 0.074219 c -0.359375 0.164062 -0.589844 0.519531 -0.589844 0.914062 v 6 c 0 0.40625 0.242188 0.769531 0.617188 0.921875 c 0.375 0.15625 0.804687 0.070313 1.089843 -0.214844 l 0.875 -0.875 l 1.074219 1.699219 c 0.292969 0.46875 0.910156 0.609375 1.375 0.3125 c 0.46875 -0.292969 0.609375 -0.910156 0.3125 -1.375 l -0.917969 -1.46875 h 1.074219 c 0.414062 0 0.789062 -0.257812 0.933594 -0.644531 c 0.148437 -0.390625 0.042968 -0.828125 -0.269532 -1.105469 l -4.5 -4 c -0.21875 -0.191406 -0.511718 -0.28125 -0.800781 -0.238281 z m -3.113281 1.019531 c -0.355469 0 -0.6875 0.191406 -0.863281 0.5 l -0.5 0.863281 c -0.136719 0.230469 -0.171875 0.503907 -0.101563 0.761719 c 0.066406 0.257812 0.234375 0.476562 0.464844 0.605469 c 0.476562 0.277343 1.089844 0.113281 1.363281 -0.367188 l 0.5 -0.863281 c 0.277344 -0.476562 0.113281 -1.089844 -0.363281 -1.363281 c -0.152344 -0.089844 -0.324219 -0.136719 -0.5 -0.136719 z m 0 0" fill="#222222"/></svg>
diff --git a/data/resources/icons/scalable/actions/pocket-knife-symbolic.svg b/data/resources/icons/scalable/actions/pocket-knife-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 2 -0.0195312 c -0.550781 0 -1 0.4492192 -1 1.0000002 v 0.695312 c 0 0.535157 0.4375 1.058594 0.753906 1.230469 c 0.101563 0.058594 0.140625 0.054688 0.222656 0.085938 c -0.121093 0.050781 -0.195312 0.050781 -0.339843 0.15625 c -0.335938 0.25 -0.636719 0.796874 -0.636719 1.296874 s 0.257812 1.027344 0.574219 1.304688 c 0.167969 0.148438 0.285156 0.171875 0.433593 0.242188 c -0.132812 0.054687 -0.226562 0.0625 -0.386718 0.1875 c -0.335938 0.257812 -0.621094 0.800781 -0.621094 1.300781 s 0.316406 1.089843 0.710938 1.332031 c 0.394531 0.246094 0.714843 0.257812 0.9375 0.277344 c 0.082031 0.011718 0.054687 0.007812 0.109374 0.011718 c 0.179688 0.230469 0.453126 0.367188 0.742188 0.378907 c 0.550781 0 1 -0.445313 1 -1 c 0 -0.265625 -0.097656 -0.601563 -0.292969 -0.835938 c -0.144531 -0.171875 -0.296875 -0.230469 -0.445312 -0.300781 c 0.046875 -0.03125 0.070312 -0.019531 0.117187 -0.054688 c 0.335938 -0.261718 0.621094 -0.808593 0.621094 -1.308593 s -0.320312 -1.03125 -0.636719 -1.269531 c -0.183593 -0.132813 -0.289062 -0.152344 -0.429687 -0.210938 c 0.148437 -0.058594 0.261718 -0.070312 0.441406 -0.210938 c 0.339844 -0.261718 0.625 -0.808593 0.625 -1.308593 s -0.269531 -1.03125 -0.601562 -1.300781 c -0.328126 -0.269532 -0.636719 -0.355469 -0.855469 -0.421876 c -0.027344 -0.007812 -0.015625 -0.003906 -0.042969 -0.015624 v -0.261719 c 0 -0.550781 -0.449219 -1.0000002 -1 -1.0000002 z m 0.5 8.5000002 c 0.023438 0.167969 0.089844 0.324219 0.1875 0.457031 c -0.007812 -0.007812 -0.011719 -0.003906 -0.019531 -0.015625 c -0.117188 -0.136719 -0.167969 -0.34375 -0.167969 -0.441406 z m 0 0"/><path d="m 12.351562 -0.0117188 c -0.972656 -0.0585937 -2.023437 0.3164068 -2.855468 1.1484378 l -8.339844 8.339843 c -0.832031 0.835938 -1.2070312 1.886719 -1.1484375 2.855469 c 0.0546875 0.972657 0.4999995 1.84375 1.1484375 2.492188 c 0.652344 0.652343 1.523438 1.09375 2.492188 1.152343 c 0.972656 0.058594 2.023437 -0.316406 2.859374 -1.152343 l 8.335938 -8.335938 c 0.832031 -0.835937 1.210938 -1.886719 1.152344 -2.855469 c -0.058594 -0.972656 -0.503906 -1.84375 -1.152344 -2.496093 c -0.648438 -0.648438 -1.519531 -1.089844 -2.492188 -1.1484378 z m -0.117187 1.9960938 c 0.421875 0.023437 0.859375 0.234375 1.195313 0.566406 c 0.335937 0.335938 0.546874 0.777344 0.570312 1.195313 c 0.023438 0.421875 -0.089844 0.847656 -0.570312 1.324218 l -8.335938 8.339844 c -0.480469 0.480469 -0.90625 0.59375 -1.324219 0.570313 c -0.421875 -0.023438 -0.863281 -0.234375 -1.195312 -0.570313 c -0.335938 -0.335937 -0.546875 -0.773437 -0.574219 -1.195312 c -0.023438 -0.417969 0.09375 -0.84375 0.574219 -1.320313 l 8.335937 -8.34375 c 0.480469 -0.476562 0.902344 -0.589843 1.324219 -0.566406 z m 0 0"/><path d="m 14 3.976562 l -0.050781 8.09375 c -0.003907 0.921876 -1.019531 1.910157 -1.949219 1.910157 v -5 h -2 v 7 h 2 c 2.128906 0 3.9375 -1.757813 3.949219 -3.898438 l 0.050781 -8.09375 z m 0 0"/><path d="m 5 11.980469 c 0 0.554687 -0.449219 1 -1 1 s -1 -0.445313 -1 -1 c 0 -0.550781 0.449219 -1 1 -1 s 1 0.449219 1 1 z m 0 0"/><path d="m 13 3.980469 c 0 0.554687 -0.449219 1 -1 1 s -1 -0.445313 -1 -1 c 0 -0.550781 0.449219 -1 1 -1 s 1 0.449219 1 1 z m 0 0"/></g></svg>
diff --git a/data/resources/icons/scalable/actions/test-pass-symbolic.svg b/data/resources/icons/scalable/actions/test-pass-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 8.234375 1.003906 c -2.15625 -0.070312 -4.277344 0.859375 -5.683594 2.601563 c -1.871093 2.316406 -2.066406 5.582031 -0.484375 8.105469 c 1.582032 2.527343 4.601563 3.777343 7.503906 3.109374 c 2.90625 -0.667968 5.074219 -3.117187 5.390626 -6.082031 c 0.046874 -0.359375 -0.101563 -0.71875 -0.394532 -0.933593 c -0.292968 -0.21875 -0.679687 -0.257813 -1.011718 -0.109376 c -0.332032 0.152344 -0.554688 0.472657 -0.582032 0.835938 c -0.226562 2.121094 -1.769531 3.863281 -3.851562 4.34375 c -2.078125 0.476562 -4.226563 -0.414062 -5.359375 -2.222656 c -1.132813 -1.808594 -0.992188 -4.128906 0.347656 -5.792969 s 3.578125 -2.289063 5.585937 -1.5625 c 0.339844 0.128906 0.71875 0.066406 0.996094 -0.167969 c 0.28125 -0.230468 0.410156 -0.59375 0.34375 -0.949218 c -0.0625 -0.355469 -0.316406 -0.648438 -0.660156 -0.761719 c -0.699219 -0.253907 -1.421875 -0.390625 -2.140625 -0.414063 z m 0 0"/><path d="m 13.167969 2.542969 l -5.292969 5.292969 l -2.417969 -2.417969 l -1.414062 1.414062 l 3.832031 3.832031 l 6.707031 -6.707031 z m 0 0"/></g></svg>
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
@@ -4,5 +4,10 @@
<!-- see https://gtk-rs.org/gtk4-rs/git/docs/gtk4/struct.Application.html#automatic-resources -->
<file compressed="true" preprocess="xml-stripblanks" alias="gtk/help-overlay.ui">ui/shortcuts.ui</file>
<file compressed="true">style.css</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/mouse-click-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/blend-tool-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/error-outline-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/pocket-knife-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/test-pass-symbolic.svg</file>
</gresource>
</gresources>
diff --git a/po/POTFILES.in b/po/POTFILES.in
@@ -4,7 +4,10 @@ data/dev.nordgedanken.heic2jpg.metainfo.xml.in.in
data/resources/ui/shortcuts.ui
src/app.rs
src/main.rs
-src/select_folder.rs
-src/conversion_worker.rs
src/modals/about.rs
src/modals/mod.rs
+src/pages/finished_page.rs
+src/pages/mod.rs
+src/pages/select_folder.rs
+src/workers/conversion_worker.rs
+src/workers/mod.rs
diff --git a/src/app.rs b/src/app.rs
@@ -1,7 +1,11 @@
use crate::config::{APP_ID, PROFILE};
-use crate::conversion_worker::{ConversionWorker, ConversionWorkerInputMsg, ConversionWorkerMsg};
use crate::modals::about::AboutDialog;
-use crate::select_folder::{InOut, SelectFolder, SelectFolderOut};
+use crate::pages::finished_page::{self, FinishedPage};
+use crate::pages::progressing_page::{ProgressingPage, ProgressingPageMsg};
+use crate::pages::select_folder::{InOut, SelectFolder, SelectFolderOut};
+use crate::workers::conversion_worker::{
+ ConversionWorker, ConversionWorkerInputMsg, ConversionWorkerMsg,
+};
use gettextrs::gettext;
use gtk::prelude::*;
use gtk::{gio, glib};
@@ -12,6 +16,7 @@ use relm4::{
Controller, WorkerController,
};
use std::path::PathBuf;
+use tracing::info;
enum Mode {
InputSelection,
@@ -26,13 +31,13 @@ pub(super) struct App {
about_dialog: Controller<AboutDialog>,
input_folder_selector: Controller<SelectFolder>,
output_folder_selector: Controller<SelectFolder>,
+ progressing_page: Controller<ProgressingPage>,
+ finished_page: Controller<FinishedPage>,
input_folder: Option<PathBuf>,
output_folder: Option<PathBuf>,
conversion_worker: WorkerController<ConversionWorker>,
mode: Mode,
- progress: f64,
failure: Option<String>,
- file_count: usize,
}
#[derive(Debug)]
@@ -77,7 +82,10 @@ impl SimpleComponent for App {
#[root]
main_window = adw::ApplicationWindow::new(&main_application()) {
set_visible: true,
- set_resizable: true,
+ set_default_width: 650,
+ set_default_height: 600,
+ set_width_request: 360,
+ set_height_request: 294,
connect_close_request[sender] => move |_| {
sender.input(AppMsg::Quit);
@@ -101,7 +109,6 @@ impl SimpleComponent for App {
},
adw::ToolbarView {
-
add_top_bar = &adw::HeaderBar {
#[wrap(Some)]
set_title_widget = &adw::WindowTitle {
@@ -116,61 +123,17 @@ impl SimpleComponent for App {
#[transition = "SlideRight"]
match model.mode {
Mode::Progressing => {
- adw::StatusPage {
- set_hexpand: true,
+ gtk::Box {
set_vexpand: true,
- set_title: &gettext("Converting"),
- set_description: Some(&gettext("Please wait while the conversion is in progress")),
- gtk::Box {
- set_halign: gtk::Align::Center,
- set_orientation: gtk::Orientation::Vertical,
- set_spacing: 8,
-
- gtk::Label {
- set_xalign: 0.5,
- #[watch]
- set_visible: model.file_count > 0,
- #[watch]
- set_label: &format!(
- "{} / {}",
- model.file_count as u32 * model.progress as u32,
- model.file_count
- ),
- },
- gtk::ProgressBar {
- set_hexpand: true,
- #[watch]
- set_fraction: model.progress,
- }
- }
+ set_hexpand: true,
+ append = model.progressing_page.widget(),
}
}
Mode::Finished => {
- adw::StatusPage {
- set_hexpand: true,
+ gtk::Box {
set_vexpand: true,
- set_title: &gettext("Conversion Complete"),
- set_description: Some(&gettext("The conversion was successful")),
- gtk::Box {
- set_halign: gtk::Align::Center,
- set_orientation: gtk::Orientation::Horizontal,
- set_spacing: 24,
-
- gtk::Button {
- set_label: "Close",
- add_css_class: "suggested-action",
- add_css_class: "pill",
- connect_clicked[sender] => move |_| {
- sender.input(AppMsg::Quit);
- }
- },
- gtk::Button {
- set_label: "Restart",
- connect_clicked[sender] => move |_| {
- sender.input(AppMsg::StartOver);
- }
- }
- }
+ set_hexpand: true,
+ append = model.finished_page.widget(),
}
}
Mode::Failed => {
@@ -180,6 +143,7 @@ impl SimpleComponent for App {
set_title: &gettext("Conversion Failed"),
#[watch]
set_description: model.failure.as_deref(),
+ set_icon_name: Some("error-outline"),
gtk::Box {
set_halign: gtk::Align::Center,
@@ -220,6 +184,7 @@ impl SimpleComponent for App {
set_vexpand: true,
set_title: &gettext("Start Conversion"),
set_description: Some(&gettext("Click the button below to start the conversion")),
+ set_icon_name: Some("blend-tool"),
gtk::Box {
set_halign: gtk::Align::Center,
@@ -283,17 +248,26 @@ impl SimpleComponent for App {
ConversionWorkerMsg::ConversionFailed(e) => AppMsg::ConversionFailed(e),
});
+ let progressing_page = ProgressingPage::builder().launch(()).detach();
+ let finished_page =
+ FinishedPage::builder()
+ .launch(())
+ .forward(sender.input_sender(), |msg| match msg {
+ finished_page::FinishedPageMsg::StartOver => AppMsg::StartOver,
+ finished_page::FinishedPageMsg::Quit => AppMsg::Quit,
+ });
+
let model = Self {
about_dialog,
input_folder_selector,
output_folder_selector,
+ progressing_page,
+ finished_page,
conversion_worker,
input_folder: None,
output_folder: None,
mode: Mode::InputSelection,
- progress: 0.0,
failure: None,
- file_count: 0,
};
let widgets = view_output!();
@@ -326,8 +300,11 @@ impl SimpleComponent for App {
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
AppMsg::ConversionStarted(number) => {
- self.file_count = number;
self.mode = Mode::Progressing;
+ let _ = self
+ .progressing_page
+ .sender()
+ .send(ProgressingPageMsg::SetFileCount(number));
}
AppMsg::InputFolderSelected(path) => {
self.input_folder = Some(path);
@@ -350,13 +327,16 @@ impl SimpleComponent for App {
if let (Some(input_folder), Some(output_folder)) =
(&self.input_folder, &self.output_folder)
{
+ info!(
+ "Starting conversion from {:?} to {:?}",
+ input_folder, output_folder
+ );
let _ = self.conversion_worker.sender().send(
ConversionWorkerInputMsg::ConvertFolder(
input_folder.clone(),
output_folder.clone(),
),
);
- self.mode = Mode::Progressing;
} else {
self.mode = Mode::Failed;
self.failure =
@@ -365,7 +345,10 @@ impl SimpleComponent for App {
}
AppMsg::ProgressUpdate(progress) => {
if let Mode::Progressing = self.mode {
- self.progress = progress;
+ let _ = self
+ .progressing_page
+ .sender()
+ .send(ProgressingPageMsg::SetProgress(progress));
}
}
AppMsg::ConversionComplete => {
@@ -374,6 +357,11 @@ impl SimpleComponent for App {
notification.set_body(Some(&gettext("The conversion was successful")));
notification.set_category(Some("transfer.complete"));
notification.set_priority(gio::NotificationPriority::Normal);
+ let pass_icon = gio::Icon::for_string("test-pass");
+ if let Ok(pass_icon) = pass_icon {
+ // Ends up as an empty icon on my system. Something is wrong with the icon loading?.
+ //notification.set_icon(&pass_icon);
+ }
let app = relm4::main_application();
app.send_notification(None, ¬ification);
@@ -385,9 +373,15 @@ impl SimpleComponent for App {
AppMsg::StartOver => {
self.input_folder = None;
self.output_folder = None;
- self.progress = 0.0;
+ let _ = self
+ .progressing_page
+ .sender()
+ .send(ProgressingPageMsg::SetProgress(0.0));
self.failure = None;
- self.file_count = 0;
+ let _ = self
+ .progressing_page
+ .sender()
+ .send(ProgressingPageMsg::SetFileCount(0));
self.mode = Mode::InputSelection;
}
AppMsg::Noop => {}
diff --git a/src/conversion_worker.rs b/src/conversion_worker.rs
@@ -1,128 +0,0 @@
-use magick_rust::{MagickError, MagickWand};
-use std::path::PathBuf;
-use std::result::Result;
-use tracing::info;
-use walkdir::WalkDir;
-
-use relm4::{ComponentSender, Worker};
-
-#[derive(Debug)]
-pub enum ConversionWorkerInputMsg {
- ConvertFolder(PathBuf, PathBuf),
-}
-
-#[derive(Debug)]
-pub enum ConversionWorkerMsg {
- ConversionStarted(usize),
- ProgressUpdate(f64),
- ConversionComplete,
- ConversionFailed(String),
-}
-
-pub struct ConversionWorker;
-
-impl Worker for ConversionWorker {
- type Init = ();
- type Input = ConversionWorkerInputMsg;
- type Output = ConversionWorkerMsg;
-
- fn init(_init: Self::Init, _sender: ComponentSender<Self>) -> Self {
- Self
- }
-
- fn update(&mut self, msg: ConversionWorkerInputMsg, sender: ComponentSender<Self>) {
- match msg {
- ConversionWorkerInputMsg::ConvertFolder(input_path, output_path) => {
- // Walk directory, find all heic files, convert them to jpg and update progress
- info!("Converting folder {:?}", input_path);
- let result = self.convert_folder(input_path, output_path, &sender);
-
- // Send the result of the conversion back
- match result {
- Ok(_) => sender
- .output(ConversionWorkerMsg::ConversionComplete)
- .unwrap(),
- Err(e) => sender
- .output(ConversionWorkerMsg::ConversionFailed(e.to_string()))
- .unwrap(),
- }
- }
- }
- }
-}
-
-impl ConversionWorker {
- fn convert_folder(
- &self,
- input_path: PathBuf,
- output_path: PathBuf,
- sender: &ComponentSender<Self>,
- ) -> Result<(), MagickError> {
- // Start the conversion
- info!("Converting folder {:?} to {:?}", input_path, output_path);
-
- // Use walkdir to find all heic files in the input directory
- let heic_files: Vec<PathBuf> = WalkDir::new(input_path)
- .follow_links(true)
- .same_file_system(false)
- .into_iter()
- .filter_map(|entry| {
- let entry = entry.ok()?;
- let path = entry.path();
- if path.is_file()
- && path
- .extension()
- .map_or(false, |ext| ext.eq_ignore_ascii_case("heic"))
- {
- Some(path.to_path_buf())
- } else {
- None
- }
- })
- .collect();
- info!("Found {} heic files", heic_files.len());
- sender
- .output(ConversionWorkerMsg::ConversionStarted(heic_files.len()))
- .unwrap();
-
- // Convert each heic file to jpg
- for (index, heic_file) in heic_files.iter().enumerate() {
- info!("Converting file {:?}", heic_file);
- let output_file = output_path
- .join(heic_file.file_stem().unwrap())
- .with_extension("jpg");
-
- // Convert the file
- self.convert_heic_to_jpg(heic_file.to_path_buf(), output_file)?;
-
- // Update the progress
- sender
- .output(ConversionWorkerMsg::ProgressUpdate(
- (index + 1) as f64 / heic_files.len() as f64,
- ))
- .unwrap();
- }
- info!("Conversion complete");
- Ok(())
- }
-
- fn convert_heic_to_jpg(
- &self,
- input_file: PathBuf,
- output_file: PathBuf,
- ) -> Result<(), MagickError> {
- // Create a MagickWand
- let mut wand = MagickWand::new();
-
- // Read the input file
- info!("Reading file {:?}", input_file);
- wand.read_image(input_file.to_str().unwrap())?;
-
- // Convert the image to jpg
- wand.set_image_format("jpg")?;
- info!("Converting to jpg");
- wand.write_image(output_file.to_str().unwrap())?;
-
- Ok(())
- }
-}
diff --git a/src/main.rs b/src/main.rs
@@ -1,9 +1,9 @@
#[rustfmt::skip]
mod config;
mod app;
-mod conversion_worker;
mod modals;
-mod select_folder;
+mod pages;
+mod workers;
use config::{APP_ID, GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE};
use gettextrs::LocaleCategory;
diff --git a/src/pages/finished_page.rs b/src/pages/finished_page.rs
@@ -0,0 +1,65 @@
+use gettextrs::gettext;
+use relm4::{
+ adw,
+ gtk::{
+ self,
+ prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt},
+ },
+ ComponentParts, ComponentSender, SimpleComponent,
+};
+
+pub struct FinishedPage;
+
+#[derive(Debug)]
+pub enum FinishedPageMsg {
+ StartOver,
+ Quit,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for FinishedPage {
+ type Init = ();
+ type Input = ();
+ type Output = FinishedPageMsg;
+
+ view! {
+ adw::StatusPage {
+ set_hexpand: true,
+ set_vexpand: true,
+ set_title: &gettext("Conversion Complete"),
+ set_description: Some(&gettext("The conversion was successful")),
+ set_icon_name: Some("test-pass"),
+ gtk::Box {
+ set_halign: gtk::Align::Center,
+ set_orientation: gtk::Orientation::Horizontal,
+ set_spacing: 24,
+
+ gtk::Button {
+ set_label: "Close",
+ add_css_class: "suggested-action",
+ add_css_class: "pill",
+ connect_clicked[sender] => move |_| {
+ let _ = sender.output(FinishedPageMsg::Quit);
+ }
+ },
+ gtk::Button {
+ set_label: "Restart",
+ connect_clicked[sender] => move |_| {
+ let _ = sender.output(FinishedPageMsg::StartOver);
+ }
+ }
+ }
+ }
+ }
+
+ fn init(
+ _init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let model = Self {};
+
+ let widgets = view_output!();
+ ComponentParts { model, widgets }
+ }
+}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
@@ -0,0 +1,3 @@
+pub mod finished_page;
+pub mod progressing_page;
+pub mod select_folder;
diff --git a/src/pages/progressing_page.rs b/src/pages/progressing_page.rs
@@ -0,0 +1,84 @@
+use gettextrs::gettext;
+use relm4::{
+ adw,
+ gtk::{
+ self,
+ prelude::{BoxExt, OrientableExt, WidgetExt},
+ },
+ ComponentParts, ComponentSender, SimpleComponent,
+};
+
+pub struct ProgressingPage {
+ file_count: usize,
+ progress: f64,
+}
+
+#[derive(Debug)]
+pub enum ProgressingPageMsg {
+ SetFileCount(usize),
+ SetProgress(f64),
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for ProgressingPage {
+ type Init = ();
+ type Input = ProgressingPageMsg;
+ type Output = ();
+
+ view! {
+ adw::StatusPage {
+ set_hexpand: true,
+ set_vexpand: true,
+ set_title: &gettext("Converting"),
+ set_description: Some(&gettext("Please wait while the conversion is in progress")),
+ set_icon_name: Some("pocket-knife"),
+ gtk::Box {
+ set_halign: gtk::Align::Center,
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 8,
+
+ gtk::Label {
+ set_xalign: 0.5,
+ #[watch]
+ set_visible: model.file_count > 0,
+ #[watch]
+ set_label: &format!(
+ "{} / {}",
+ model.file_count as u32 * model.progress as u32,
+ model.file_count
+ ),
+ },
+ gtk::ProgressBar {
+ set_hexpand: true,
+ #[watch]
+ set_fraction: model.progress,
+ }
+ }
+ }
+ }
+
+ fn init(
+ _init: Self::Init,
+ root: Self::Root,
+ _sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let model = Self {
+ file_count: 0,
+ progress: 0.0,
+ };
+
+ let widgets = view_output!();
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
+ match message {
+ ProgressingPageMsg::SetFileCount(file_count) => {
+ self.file_count = file_count;
+ }
+ ProgressingPageMsg::SetProgress(progress) => {
+ self.progress = progress;
+ }
+ }
+ }
+}
diff --git a/src/pages/select_folder.rs b/src/pages/select_folder.rs
@@ -0,0 +1,132 @@
+use gettextrs::gettext;
+use gtk::prelude::*;
+use relm4::{
+ adw, gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller,
+ SimpleComponent,
+};
+use relm4_components::open_dialog::*;
+use std::path::PathBuf;
+
+#[derive(Debug, PartialEq)]
+pub(crate) enum InOut {
+ Input,
+ Output,
+}
+
+#[derive(Debug)]
+pub(crate) struct SelectFolder {
+ open_dialog: Controller<OpenDialog>,
+ folder: Option<PathBuf>,
+ description: String,
+ button_label: String,
+ direction: InOut,
+}
+
+#[derive(Debug)]
+pub(crate) enum SelectFolderMsg {
+ OpenRequest,
+ OpenResponse(PathBuf),
+ Ignore,
+}
+
+#[derive(Debug)]
+pub(crate) enum SelectFolderOut {
+ FolderSelected(PathBuf),
+ AbortLast,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for SelectFolder {
+ type Init = InOut;
+ type Input = SelectFolderMsg;
+ type Output = SelectFolderOut;
+
+ view! {
+ adw::StatusPage {
+ set_hexpand: true,
+ set_vexpand: true,
+ set_title: &gettext("Select Folder"),
+ set_description: Some(&model.description),
+ set_icon_name: Some("mouse-click"),
+
+ gtk::Box {
+ set_halign: gtk::Align::Center,
+ set_orientation: gtk::Orientation::Horizontal,
+ set_spacing: 24,
+
+ gtk::Button {
+ set_halign: gtk::Align::Center,
+ set_label: &model.button_label,
+ add_css_class: "suggested-action",
+ add_css_class: "pill",
+ connect_clicked[sender] => move |_| {
+ sender.input(SelectFolderMsg::OpenRequest);
+ }
+ },
+
+ // If this is the output folder selector, add an Restart button
+ gtk::Button {
+ set_visible: model.direction == InOut::Output,
+ set_halign: gtk::Align::Center,
+ set_label: &gettext("Restart"),
+ add_css_class: "destructive-action",
+ connect_clicked[sender] => move |_| {
+ let _ = sender.output(SelectFolderOut::AbortLast);
+ }
+ }
+ }
+ }
+ }
+
+ fn init(
+ init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let (description, button_label) = match init {
+ InOut::Input => (
+ gettext("Select the folder where the Heic files can be found"),
+ gettext("Select input directory"),
+ ),
+ InOut::Output => (
+ gettext("Select the folder where the JPG files are meant to be saved"),
+ gettext("Select output directory"),
+ ),
+ };
+
+ let dialog_settings = OpenDialogSettings {
+ folder_mode: true,
+ ..Default::default()
+ };
+
+ let open_dialog = OpenDialog::builder()
+ .transient_for_native(&root)
+ .launch(dialog_settings)
+ .forward(sender.input_sender(), |response| match response {
+ OpenDialogResponse::Accept(path) => SelectFolderMsg::OpenResponse(path),
+ OpenDialogResponse::Cancel => SelectFolderMsg::Ignore,
+ });
+
+ let model = Self {
+ open_dialog,
+ folder: None,
+ description: description.to_string(),
+ button_label: button_label.to_string(),
+ direction: init,
+ };
+
+ let widgets = view_output!();
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
+ match message {
+ SelectFolderMsg::OpenRequest => self.open_dialog.emit(OpenDialogMsg::Open),
+ SelectFolderMsg::OpenResponse(path) => {
+ self.folder = Some(path.clone());
+ let _ = sender.output(SelectFolderOut::FolderSelected(path.clone()));
+ }
+ SelectFolderMsg::Ignore => {}
+ }
+ }
+}
diff --git a/src/select_folder.rs b/src/select_folder.rs
@@ -1,131 +0,0 @@
-use gettextrs::gettext;
-use gtk::prelude::*;
-use relm4::{
- adw, gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller,
- SimpleComponent,
-};
-use relm4_components::open_dialog::*;
-use std::path::PathBuf;
-
-#[derive(Debug, PartialEq)]
-pub(super) enum InOut {
- Input,
- Output,
-}
-
-#[derive(Debug)]
-pub(super) struct SelectFolder {
- open_dialog: Controller<OpenDialog>,
- folder: Option<PathBuf>,
- description: String,
- button_label: String,
- direction: InOut,
-}
-
-#[derive(Debug)]
-pub(super) enum SelectFolderMsg {
- OpenRequest,
- OpenResponse(PathBuf),
- Ignore,
-}
-
-#[derive(Debug)]
-pub(super) enum SelectFolderOut {
- FolderSelected(PathBuf),
- AbortLast,
-}
-
-#[relm4::component(pub)]
-impl SimpleComponent for SelectFolder {
- type Init = InOut;
- type Input = SelectFolderMsg;
- type Output = SelectFolderOut;
-
- view! {
- adw::StatusPage {
- set_hexpand: true,
- set_vexpand: true,
- set_title: &gettext("Select Folder"),
- set_description: Some(&model.description),
-
- gtk::Box {
- set_halign: gtk::Align::Center,
- set_orientation: gtk::Orientation::Horizontal,
- set_spacing: 24,
-
- gtk::Button {
- set_halign: gtk::Align::Center,
- set_label: &model.button_label,
- add_css_class: "suggested-action",
- add_css_class: "pill",
- connect_clicked[sender] => move |_| {
- sender.input(SelectFolderMsg::OpenRequest);
- }
- },
-
- // If this is the output folder selector, add an Restart button
- gtk::Button {
- set_visible: model.direction == InOut::Output,
- set_halign: gtk::Align::Center,
- set_label: &gettext("Restart"),
- add_css_class: "destructive-action",
- connect_clicked[sender] => move |_| {
- let _ = sender.output(SelectFolderOut::AbortLast);
- }
- }
- }
- }
- }
-
- fn init(
- init: Self::Init,
- root: Self::Root,
- sender: ComponentSender<Self>,
- ) -> ComponentParts<Self> {
- let (description, button_label) = match init {
- InOut::Input => (
- gettext("Select the folder where the Heic files can be found"),
- gettext("Select input directory"),
- ),
- InOut::Output => (
- gettext("Select the folder where the JPG files are meant to be saved"),
- gettext("Select output directory"),
- ),
- };
-
- let dialog_settings = OpenDialogSettings {
- folder_mode: true,
- ..Default::default()
- };
-
- let open_dialog = OpenDialog::builder()
- .transient_for_native(&root)
- .launch(dialog_settings)
- .forward(sender.input_sender(), |response| match response {
- OpenDialogResponse::Accept(path) => SelectFolderMsg::OpenResponse(path),
- OpenDialogResponse::Cancel => SelectFolderMsg::Ignore,
- });
-
- let model = Self {
- open_dialog,
- folder: None,
- description: description.to_string(),
- button_label: button_label.to_string(),
- direction: init,
- };
-
- let widgets = view_output!();
- ComponentParts { model, widgets }
- }
-
- fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
- match message {
- SelectFolderMsg::OpenRequest => self.open_dialog.emit(OpenDialogMsg::Open),
- SelectFolderMsg::OpenResponse(path) => {
- self.folder = Some(path.clone());
- let _ = sender.output(SelectFolderOut::FolderSelected(path.clone()));
- }
- SelectFolderMsg::Ignore => {}
- }
- }
-}
diff --git a/src/workers/conversion_worker.rs b/src/workers/conversion_worker.rs
@@ -0,0 +1,127 @@
+use magick_rust::{MagickError, MagickWand};
+use std::path::PathBuf;
+use std::result::Result;
+use tracing::info;
+use walkdir::WalkDir;
+
+use relm4::{ComponentSender, Worker};
+
+#[derive(Debug)]
+pub enum ConversionWorkerInputMsg {
+ ConvertFolder(PathBuf, PathBuf),
+}
+
+#[derive(Debug)]
+pub enum ConversionWorkerMsg {
+ ConversionStarted(usize),
+ ProgressUpdate(f64),
+ ConversionComplete,
+ ConversionFailed(String),
+}
+
+pub struct ConversionWorker;
+
+impl Worker for ConversionWorker {
+ type Init = ();
+ type Input = ConversionWorkerInputMsg;
+ type Output = ConversionWorkerMsg;
+
+ fn init(_init: Self::Init, _sender: ComponentSender<Self>) -> Self {
+ Self
+ }
+
+ fn update(&mut self, msg: ConversionWorkerInputMsg, sender: ComponentSender<Self>) {
+ match msg {
+ ConversionWorkerInputMsg::ConvertFolder(input_path, output_path) => {
+ // Walk directory, find all heic files, convert them to jpg and update progress
+ info!("Converting folder {:?}", input_path);
+ let result = self.convert_folder(input_path, output_path, &sender);
+
+ // Send the result of the conversion back
+ match result {
+ Ok(_) => sender
+ .output(ConversionWorkerMsg::ConversionComplete)
+ .unwrap(),
+ Err(e) => sender
+ .output(ConversionWorkerMsg::ConversionFailed(e.to_string()))
+ .unwrap(),
+ }
+ }
+ }
+ }
+}
+
+impl ConversionWorker {
+ fn convert_folder(
+ &self,
+ input_path: PathBuf,
+ output_path: PathBuf,
+ sender: &ComponentSender<Self>,
+ ) -> Result<(), MagickError> {
+ // Start the conversion
+ info!("Converting folder {:?} to {:?}", input_path, output_path);
+
+ // Use walkdir to find all heic files in the input directory
+ let heic_files: Vec<PathBuf> = WalkDir::new(input_path)
+ .follow_links(true)
+ .same_file_system(false)
+ .into_iter()
+ .filter_map(|entry| {
+ let entry = entry.ok()?;
+ let path = entry.path();
+ let extension = path.extension();
+ //info!("Found file {:?}", path);
+ if path.is_file() && extension.map_or(false, |ext| ext.eq_ignore_ascii_case("heic"))
+ {
+ Some(path.to_path_buf())
+ } else {
+ None
+ }
+ })
+ .collect();
+ info!("Found {} heic files", heic_files.len());
+ sender
+ .output(ConversionWorkerMsg::ConversionStarted(heic_files.len()))
+ .unwrap();
+
+ // Convert each heic file to jpg
+ for (index, heic_file) in heic_files.iter().enumerate() {
+ info!("Converting file {:?}", heic_file);
+ let output_file = output_path
+ .join(heic_file.file_stem().unwrap())
+ .with_extension("jpg");
+
+ // Convert the file
+ self.convert_heic_to_jpg(heic_file.to_path_buf(), output_file)?;
+
+ // Update the progress
+ sender
+ .output(ConversionWorkerMsg::ProgressUpdate(
+ (index + 1) as f64 / heic_files.len() as f64,
+ ))
+ .unwrap();
+ }
+ info!("Conversion complete");
+ Ok(())
+ }
+
+ fn convert_heic_to_jpg(
+ &self,
+ input_file: PathBuf,
+ output_file: PathBuf,
+ ) -> Result<(), MagickError> {
+ // Create a MagickWand
+ let mut wand = MagickWand::new();
+
+ // Read the input file
+ info!("Reading file {:?}", input_file);
+ wand.read_image(input_file.to_str().unwrap())?;
+
+ // Convert the image to jpg
+ wand.set_image_format("jpg")?;
+ info!("Converting to jpg");
+ wand.write_image(output_file.to_str().unwrap())?;
+
+ Ok(())
+ }
+}
diff --git a/src/workers/mod.rs b/src/workers/mod.rs
@@ -0,0 +1 @@
+pub mod conversion_worker;