diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b51587f..7f5655a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1387,7 +1387,7 @@ dependencies = [ [[package]] name = "iccery" -version = "0.2.1" +version = "0.3.0" dependencies = [ "base64 0.22.1", "image", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5c9b3c3..b36b116 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.3.0" +version = "0.3.1" description = "Modern Printer Profiling UI frontend for ArgyllCMS" authors = ["Gordon"] edition = "2021" diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ec7b5c9..6d8be3c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -121,6 +121,63 @@ pub fn get_default_working_dir(app: AppHandle) -> Result { resolve_safe_cwd(&app, "") } +#[tauri::command] +pub async fn select_target_file( + app: AppHandle, + default_dir: Option, + default_name: Option, +) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let mut builder = app.dialog().file().add_filter("ArgyllCMS Target", &["ti1"]); + if let Some(ref dir) = default_dir { + if !dir.trim().is_empty() { + builder = builder.set_directory(std::path::PathBuf::from(dir)); + } + } + if let Some(ref name) = default_name { + if !name.trim().is_empty() { + let filename = if name.to_lowercase().ends_with(".ti1") { + name.to_string() + } else { + format!("{}.ti1", name) + }; + builder = builder.set_file_name(filename); + } + } + + let (tx, rx) = tokio::sync::oneshot::channel(); + builder.save_file(move |file_path| { + let res = file_path.map(|p| p.to_string()); + let _ = tx.send(res); + }); + + rx.await.map_err(|e| format!("Dialog channel error: {}", e)) +} + +#[tauri::command] +pub async fn select_directory( + app: AppHandle, + default_dir: Option, +) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let mut builder = app.dialog().file(); + if let Some(ref dir) = default_dir { + if !dir.trim().is_empty() { + builder = builder.set_directory(std::path::PathBuf::from(dir)); + } + } + + let (tx, rx) = tokio::sync::oneshot::channel(); + builder.pick_folder(move |folder_path| { + let res = folder_path.map(|p| p.to_string()); + let _ = tx.send(res); + }); + + rx.await.map_err(|e| format!("Dialog channel error: {}", e)) +} + #[tauri::command] pub async fn detect_instruments(app: AppHandle, state: State<'_, ProcessManager>) -> Result<(), String> { let binary = resolve_binary(app.clone(), "instlist".to_string()).await?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8eb4dee..ebf33af 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,6 +14,8 @@ pub fn run() { commands::spawn_process, commands::get_app_info, commands::get_default_working_dir, + commands::select_target_file, + commands::select_directory, commands::send_stdin, commands::kill_process, commands::resolve_binary, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2db0bff..b9cf59f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ICCery", - "version": "0.3.0", + "version": "0.3.1", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" diff --git a/src/index.html b/src/index.html index 76be7ca..c7aacb3 100644 --- a/src/index.html +++ b/src/index.html @@ -460,7 +460,7 @@

About ICCery

-

Version: v0.1.0Build Date: August 2026

+

Version: v0.3.1Build Date: August 2026

Copyright © 2026 Gordon Bolton. All rights reserved.

Licences & EULA

diff --git a/src/js/targen.js b/src/js/targen.js index 083eb42..4a5112c 100644 --- a/src/js/targen.js +++ b/src/js/targen.js @@ -1,6 +1,5 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; -const { save } = window.__TAURI__.dialog; import { setStage1Result } from './printtarg.js'; import { wizardState } from './state.js'; @@ -18,20 +17,23 @@ export function initTargen() { const logPre = document.getElementById("targenLog"); let currentWorkingDir = ""; - let currentBasename = ""; function updateGenerateButton() { - const hasBasename = targetBasename.value.trim().length > 0; - const hasCwd = currentWorkingDir.trim().length > 0; - btnGenerate.disabled = !(hasBasename && hasCwd); + const hasBasename = targetBasename && targetBasename.value.trim().length > 0; + const hasCwd = currentWorkingDir && currentWorkingDir.trim().length > 0; + if (btnGenerate) { + btnGenerate.disabled = !(hasBasename && hasCwd); + } } // Load sensible default working directory on startup invoke("get_default_working_dir") .then((defaultDir) => { - if (defaultDir && !currentWorkingDir) { + if (defaultDir) { currentWorkingDir = defaultDir; - selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + if (selectedPathDisplay) { + selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + } updateGenerateButton(); } }) @@ -40,155 +42,179 @@ export function initTargen() { }); // Handle patch count preset changes - patchCountPreset.addEventListener("change", (e) => { - if (e.target.value === "custom") { - patchCountCustom.classList.remove("hidden"); - } else { - patchCountCustom.classList.add("hidden"); - } - }); + if (patchCountPreset && patchCountCustom) { + patchCountPreset.addEventListener("change", (e) => { + if (e.target.value === "custom") { + patchCountCustom.classList.remove("hidden"); + } else { + patchCountCustom.classList.add("hidden"); + } + }); + } - // Enable generate button only when both basename and directory are valid - targetBasename.addEventListener("input", () => { - updateGenerateButton(); - }); - - // Browse button opens save dialog - btnBrowse.addEventListener("click", async () => { - try { - let filePath = await save({ - title: "Save Target File As...", - filters: [{ - name: "ArgyllCMS Target", - extensions: ["ti1"] - }] - }); - - if (filePath) { - // Ensure .ti1 suffix is present in the display if the OS dialog didn't append it - if (!filePath.toLowerCase().endsWith('.ti1')) { - filePath += '.ti1'; - } - - // Simple extraction of directory and basename. - const isWindows = filePath.includes('\\'); - const sep = isWindows ? '\\' : '/'; - const parts = filePath.split(sep); - const fileName = parts.pop(); - - currentWorkingDir = parts.join(sep); - const basename = fileName.replace(/\.ti1$/i, ''); - - targetBasename.value = basename; - selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + // Enable generate button when basename is input + if (targetBasename) { + targetBasename.addEventListener("input", () => { + if (!currentWorkingDir) { + invoke("get_default_working_dir") + .then((defaultDir) => { + if (defaultDir) { + currentWorkingDir = defaultDir; + if (selectedPathDisplay) { + selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + } + } + updateGenerateButton(); + }) + .catch(() => updateGenerateButton()); + } else { updateGenerateButton(); } - } catch (err) { - console.error("Failed to open save dialog:", err); - } - }); + }); + } + + // Browse button opens save dialog via backend command + if (btnBrowse) { + btnBrowse.addEventListener("click", async () => { + try { + const filePath = await invoke("select_target_file", { + defaultDir: currentWorkingDir || null, + defaultName: targetBasename ? targetBasename.value.trim() || null : null, + }); + + if (filePath) { + // Normalize separators to locate directory and filename + const isWindows = filePath.includes('\\'); + const sep = isWindows ? '\\' : '/'; + const parts = filePath.split(sep); + const fileName = parts.pop(); + + currentWorkingDir = parts.join(sep); + const basename = fileName.replace(/\.ti1$/i, ''); + + if (targetBasename) { + targetBasename.value = basename; + } + if (selectedPathDisplay) { + selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + } + updateGenerateButton(); + } + } catch (err) + { + console.error("Failed to open file dialog:", err); + } + }); + } // Generate button clicks - btnGenerate.addEventListener("click", async () => { - const basename = targetBasename.value.trim(); - if (!currentWorkingDir || !basename) { - logPre.textContent = "[ERROR] Please specify both a target basename and a working directory.\n"; - logContainer.open = true; - logContainer.classList.remove("hidden"); - btnGenerate.disabled = false; - return; - } - - logPre.textContent = ""; - logContainer.open = false; - logContainer.classList.remove("hidden"); - btnGenerate.disabled = true; - - // Determine patch count - let patchCount = parseInt(patchCountPreset.value, 10); - if (patchCountPreset.value === "custom") { - const customVal = parseInt(patchCountCustom.value, 10); - patchCount = (!isNaN(customVal) && customVal > 0) ? customVal : 800; - } - if (isNaN(patchCount) || patchCount <= 0) { - patchCount = 800; - } - - // Determine colour space - let colourSpace = "rgb"; - colourSpaceRadios.forEach(radio => { - if (radio.checked) colourSpace = radio.value; - }); - - const basename = targetBasename.value.trim(); - - const config = { - colour_space: colourSpace, - patch_count: patchCount, - total_patches: patchCount, - white_patches: whitePatches.value ? parseInt(whitePatches.value, 10) : null, - black_patches: blackPatches.value ? parseInt(blackPatches.value, 10) : null, - basename: basename, - cwd: currentWorkingDir - }; - - try { - // Set up listeners just for this run - const processId = `targen_${basename}`; - - const unlistenStdout = await listen("process:stdout", (event) => { - if (event.payload.id === processId && event.payload.line) { - logPre.textContent += event.payload.line + "\n"; - logPre.scrollTop = logPre.scrollHeight; + if (btnGenerate) { + btnGenerate.addEventListener("click", async () => { + const basename = targetBasename ? targetBasename.value.trim() : ""; + if (!currentWorkingDir || !basename) { + if (logPre) { + logPre.textContent = "[ERROR] Please specify both a target basename and a working directory.\n"; } - }); - - const unlistenStderr = await listen("process:stderr", (event) => { - if (event.payload.id === processId && event.payload.line) { - logPre.textContent += "ERR: " + event.payload.line + "\n"; - logPre.scrollTop = logPre.scrollHeight; + if (logContainer) { + logContainer.open = true; + logContainer.classList.remove("hidden"); } + btnGenerate.disabled = false; + return; + } + + if (logPre) logPre.textContent = ""; + if (logContainer) { + logContainer.open = false; + logContainer.classList.remove("hidden"); + } + btnGenerate.disabled = true; + + // Determine patch count + let patchCount = 800; + if (patchCountPreset) { + patchCount = parseInt(patchCountPreset.value, 10); + if (patchCountPreset.value === "custom" && patchCountCustom) { + const customVal = parseInt(patchCountCustom.value, 10); + patchCount = (!isNaN(customVal) && customVal > 0) ? customVal : 800; + } + } + if (isNaN(patchCount) || patchCount <= 0) { + patchCount = 800; + } + + // Determine colour space + let colourSpace = "rgb"; + colourSpaceRadios.forEach((radio) => { + if (radio.checked) colourSpace = radio.value; }); - const unlistenExit = await listen("process:exit", (event) => { - if (event.payload.id === processId) { - unlistenStdout(); - unlistenStderr(); - unlistenExit(); - - if (event.payload.code === 0) { - logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n"; - btnGenerate.disabled = false; - wizardState.setTarget(basename, currentWorkingDir); - setStage1Result(basename, currentWorkingDir); - advanceToStage2(); - } else { - logPre.textContent += `\n[ERROR] Targen exited with code ${event.payload.code}.\n`; - btnGenerate.disabled = false; + const config = { + colour_space: colourSpace, + patch_count: patchCount, + total_patches: patchCount, + white_patches: (whitePatches && whitePatches.value) ? parseInt(whitePatches.value, 10) : null, + black_patches: (blackPatches && blackPatches.value) ? parseInt(blackPatches.value, 10) : null, + basename: basename, + cwd: currentWorkingDir, + }; + + try { + const processId = `targen_${basename}`; + + const unlistenStdout = await listen("process:stdout", (event) => { + if (event.payload.id === processId && event.payload.line && logPre) { + logPre.textContent += event.payload.line + "\n"; + logPre.scrollTop = logPre.scrollHeight; } - } - }); + }); - logPre.textContent = "Starting targen...\n"; - await invoke("run_targen", { config }); - - } catch (err) { - logPre.textContent += `\n[INVOKE ERROR] ${err}\n`; - btnGenerate.disabled = false; - } - }); + const unlistenStderr = await listen("process:stderr", (event) => { + if (event.payload.id === processId && event.payload.line && logPre) { + logPre.textContent += "ERR: " + event.payload.line + "\n"; + logPre.scrollTop = logPre.scrollHeight; + } + }); + + const unlistenExit = await listen("process:exit", (event) => { + if (event.payload.id === processId) { + unlistenStdout(); + unlistenStderr(); + unlistenExit(); + + if (event.payload.code === 0) { + if (logPre) logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n"; + btnGenerate.disabled = false; + wizardState.setTarget(basename, currentWorkingDir); + setStage1Result(basename, currentWorkingDir); + advanceToStage2(); + } else { + if (logPre) logPre.textContent += `\n[ERROR] Targen exited with code ${event.payload.code}.\n`; + btnGenerate.disabled = false; + } + } + }); + + if (logPre) logPre.textContent = "Starting targen...\n"; + await invoke("run_targen", { config }); + } catch (err) { + if (logPre) logPre.textContent += `\n[INVOKE ERROR] ${err}\n`; + btnGenerate.disabled = false; + } + }); + } } function advanceToStage2() { const steps = document.querySelectorAll('.step'); const stages = document.querySelectorAll('.stage'); - + // Update stepper - steps.forEach(s => s.classList.remove('active')); + steps.forEach((s) => s.classList.remove('active')); if (steps[1]) steps[1].classList.add('active'); - + // Update sections - stages.forEach(s => { + stages.forEach((s) => { s.classList.remove('active'); s.classList.add('hidden'); });