heic2jpg

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

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:
Mbuild-aux/dev.nordgedanken.heic2jpg.Devel.json | 4+++-
Mdata/dev.nordgedanken.heic2jpg.gschema.xml.in | 4++--
Adata/resources/icons/scalable/actions/blend-tool-symbolic.svg | 2++
Adata/resources/icons/scalable/actions/error-outline-symbolic.svg | 2++
Adata/resources/icons/scalable/actions/mouse-click-symbolic.svg | 2++
Adata/resources/icons/scalable/actions/pocket-knife-symbolic.svg | 2++
Adata/resources/icons/scalable/actions/test-pass-symbolic.svg | 2++
Mdata/resources/resources.gresource.xml | 5+++++
Mpo/POTFILES.in | 7+++++--
Msrc/app.rs | 120++++++++++++++++++++++++++++++++++++++-----------------------------------------
Dsrc/conversion_worker.rs | 128-------------------------------------------------------------------------------
Msrc/main.rs | 4++--
Asrc/pages/finished_page.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/pages/mod.rs | 3+++
Asrc/pages/progressing_page.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/pages/select_folder.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/select_folder.rs | 131-------------------------------------------------------------------------------
Asrc/workers/conversion_worker.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/workers/mod.rs | 1+
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, &notification); @@ -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;