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:
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 = >k::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 => {}
+ }
+ }
+}