heic2jpg

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

commit 8b71840cb89291cda89ef13defa8239e4eb2f2d3
parent 807a75c1b1aca3a7390c16db1dcce5e2561d4ae1
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat,  9 Nov 2024 23:39:33 +0100

Prepare basic UI layout and CI

Diffstat:
A.github/workflows/flatter.yml | 43+++++++++++++++++++++++++++++++++++++++++++
MCargo.lock | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 2++
Mdata/dev.nordgedanken.heic2jpg.desktop.in.in | 4++--
Msrc/app.rs | 102++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/main.rs | 11++++++++++-
Asrc/select_folder.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 297 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/flatter.yml b/.github/workflows/flatter.yml @@ -0,0 +1,43 @@ +name: Flatter (Deploy) + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + flatter: + name: Flatter + runs-on: ubuntu-latest + container: + image: ghcr.io/andyholmes/flatter/gnome:46 + options: --privileged + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + uses: andyholmes/actions/flatter@main + with: + files: | + build-aux/flatpak/dev.nordgedanken.heic2jpg.Devel.json + upload-pages-artifact: true + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: flatter + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/Cargo.lock b/Cargo.lock @@ -607,8 +607,10 @@ dependencies = [ "gettext-rs", "magick_rust", "relm4", + "relm4-components", "tracing", "tracing-subscriber", + "walkdir", ] [[package]] @@ -998,6 +1000,17 @@ dependencies = [ ] [[package]] +name = "relm4-components" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3d67f2982131c5e6047af4278d8fe750266767e57b58bc15f2e11e190eef36" +dependencies = [ + "once_cell", + "relm4", + "tracker", +] + +[[package]] name = "relm4-css" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1049,6 +1062,15 @@ dependencies = [ ] [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1276,6 +1298,26 @@ dependencies = [ ] [[package]] +name = "tracker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415" +dependencies = [ + "tracker-macros", +] + +[[package]] +name = "tracker-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1294,6 +1336,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1383,6 +1435,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -12,5 +12,7 @@ lto = true gettext-rs = { version = "0.7", features = ["gettext-system"] } magick_rust = "1.0.0" relm4 = { version = "0.9.1", features = ["libadwaita", "gnome_46"] } +relm4-components = { version = "0.9.1", features = ["libadwaita"] } tracing = "0.1.37" tracing-subscriber = "0.3" +walkdir = "2.5.0" diff --git a/data/dev.nordgedanken.heic2jpg.desktop.in.in b/data/dev.nordgedanken.heic2jpg.desktop.in.in @@ -1,10 +1,10 @@ [Desktop Entry] Name=Heic2JPG -Comment=Write a GTK + Rust application +Comment=An application to convert heic images to JPG images Type=Application Exec=heic2jpg Terminal=false -Categories=GNOME;GTK; +Categories=Graphics; # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! Keywords=Gnome;GTK; # Translators: Do NOT translate or transliterate this text (this is an icon file name)! diff --git a/src/app.rs b/src/app.rs @@ -1,23 +1,29 @@ +use crate::config::{APP_ID, PROFILE}; +use crate::modals::about::AboutDialog; +use crate::select_folder::{SelectFolder, SelectFolderOut}; +use gtk::prelude::*; +use gtk::{gio, glib}; use relm4::{ actions::{RelmAction, RelmActionGroup}, adw, gtk, main_application, Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent, }; - -use gtk::prelude::{ - ApplicationExt, ApplicationWindowExt, GtkWindowExt, OrientableExt, SettingsExt, WidgetExt, -}; -use gtk::{gio, glib}; - -use crate::config::{APP_ID, PROFILE}; -use crate::modals::about::AboutDialog; +use std::path::PathBuf; pub(super) struct App { about_dialog: Controller<AboutDialog>, + input_folder_selector: Controller<SelectFolder>, + output_folder_selector: Controller<SelectFolder>, + input_folder: Option<PathBuf>, + output_folder: Option<PathBuf>, } #[derive(Debug)] pub(super) enum AppMsg { + InputFolderSelected(PathBuf), + OutputFolderSelected(PathBuf), + DeselectInputFolder, + DeselectOutputFolder, Quit, } @@ -70,18 +76,63 @@ impl SimpleComponent for App { gtk::Box { set_orientation: gtk::Orientation::Vertical, + set_vexpand: true, adw::HeaderBar { + #[wrap(Some)] + set_title_widget = &adw::WindowTitle { + set_title: "Convert Heic to JPG", + }, pack_end = &gtk::MenuButton { set_icon_name: "open-menu-symbolic", set_menu_model: Some(&primary_menu), } }, - gtk::Label { - set_label: "Hello world!", - add_css_class: "title-header", - set_vexpand: true, + // If no folder is selected, show the folder selector + if model.input_folder.is_none() { + gtk::Box { + set_vexpand: true, + set_hexpand: true, + append = model.input_folder_selector.widget(), + } + } else { + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Select Output Folder", + set_description: Some("Select the folder where the converted JPG files should be saved"), + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 24, + gtk::Label { + set_halign: gtk::Align::Center, + #[watch] + set_text: &format!("Selected Input Folder: {}", model.input_folder.as_ref().map(|p| p.to_string_lossy()).unwrap_or_default()) + }, + gtk::Box { + set_halign: gtk::Align::Center, + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 24, + gtk::Button { + set_halign: gtk::Align::Center, + set_label: "Select output directory", + connect_clicked[sender] => move |_| { + + } + }, + + gtk::Button { + set_halign: gtk::Align::Center, + set_label: "Abort", + connect_clicked[sender] => move |_| { + sender.input(AppMsg::DeselectInputFolder); + } + }, + } + } + } } } @@ -97,8 +148,27 @@ impl SimpleComponent for App { .transient_for(&root) .launch(()) .detach(); - - let model = Self { about_dialog }; + let input_folder_selector = + SelectFolder::builder() + .launch(()) + .forward(sender.input_sender(), |msg| match msg { + SelectFolderOut::FolderSelected(path) => AppMsg::InputFolderSelected(path), + }); + let output_folder_selector = + SelectFolder::builder() + .launch(()) + .forward(sender.input_sender(), |msg| match msg { + SelectFolderOut::FolderSelected(path) => AppMsg::InputFolderSelected(path), + }); + + + let model = Self { + about_dialog, + input_folder_selector, + output_folder_selector, + input_folder: None, + output_folder: None, + }; let widgets = view_output!(); @@ -129,6 +199,10 @@ impl SimpleComponent for App { fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) { match message { + AppMsg::InputFolderSelected(path) => self.input_folder = Some(path), + AppMsg::DeselectInputFolder => self.input_folder = None, + AppMsg::OutputFolderSelected(path) => self.output_folder = Some(path), + AppMsg::DeselectOutputFolder => self.output_folder = None, AppMsg::Quit => main_application().quit(), } } diff --git a/src/main.rs b/src/main.rs @@ -2,22 +2,31 @@ mod config; mod app; mod modals; +mod select_folder; use config::{APP_ID, GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; use gettextrs::{gettext, LocaleCategory}; use gtk::prelude::ApplicationExt; use gtk::{gio, glib}; +use magick_rust::magick_wand_genesis; use relm4::{ actions::{AccelsPlus, RelmAction, RelmActionGroup}, gtk, main_application, RelmApp, }; +use std::sync::Once; use app::App; +use relm4::set_global_css; relm4::new_action_group!(AppActionGroup, "app"); relm4::new_stateless_action!(QuitAction, AppActionGroup, "quit"); +static START_IMAGICK: Once = Once::new(); + fn main() { + START_IMAGICK.call_once(|| { + magick_wand_genesis(); + }); gtk::init().unwrap(); // Enable logging @@ -62,6 +71,6 @@ fn main() { gio::ResourceLookupFlags::NONE, ) .unwrap(); - app.set_global_css(&glib::GString::from_utf8_checked(data.to_vec()).unwrap()); + set_global_css(&glib::GString::from_utf8_checked(data.to_vec()).unwrap()); app.visible_on_activate(false).run::<App>(()); } diff --git a/src/select_folder.rs b/src/select_folder.rs @@ -0,0 +1,91 @@ +use gtk::prelude::*; +use relm4::{ + adw, gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, + SimpleComponent, +}; +use relm4_components::{open_dialog::*, save_dialog::*}; +use std::path::PathBuf; + +pub(super) struct SelectFolder { + open_dialog: Controller<OpenDialog>, + folder: Option<PathBuf>, +} + +#[derive(Debug)] +pub(super) enum SelectFolderMsg { + OpenRequest, + OpenResponse(PathBuf), + Ignore, +} + +#[derive(Debug)] +pub(super) enum SelectFolderOut { + FolderSelected(PathBuf), +} + +#[relm4::component(pub)] +impl SimpleComponent for SelectFolder { + type Init = (); + type Input = SelectFolderMsg; + type Output = SelectFolderOut; + + view! { + adw::StatusPage { + set_hexpand: true, + set_vexpand: true, + set_title: "Select Folder", + set_description: Some("Select the folder containing the HEIC files you want to convert"), + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 24, + + gtk::Button { + set_halign: gtk::Align::Center, + set_label: "Select input directory", + connect_clicked[sender] => move |_| { + sender.input(SelectFolderMsg::OpenRequest); + } + }, + } + } + } + + fn init( + _init: Self::Init, + root: Self::Root, + sender: ComponentSender<Self>, + ) -> ComponentParts<Self> { + 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, + }; + + 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 => {} + } + } +}