fix(stage1): Replace broken window.__TAURI__.dialog with backend select_target_file command (fixes #103) #104

Merged
gronod merged 1 commits from fix/103-stage1-file-browse into development 2026-08-25 19:31:17 +01:00
7 changed files with 226 additions and 141 deletions
+1 -1
View File
@@ -1387,7 +1387,7 @@ dependencies = [
[[package]] [[package]]
name = "iccery" name = "iccery"
version = "0.2.1" version = "0.3.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image", "image",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "iccery" name = "iccery"
version = "0.3.0" version = "0.3.1"
description = "Modern Printer Profiling UI frontend for ArgyllCMS" description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"] authors = ["Gordon"]
edition = "2021" edition = "2021"
+57
View File
@@ -121,6 +121,63 @@ pub fn get_default_working_dir(app: AppHandle) -> Result<String, String> {
resolve_safe_cwd(&app, "") resolve_safe_cwd(&app, "")
} }
#[tauri::command]
pub async fn select_target_file(
app: AppHandle,
default_dir: Option<String>,
default_name: Option<String>,
) -> Result<Option<String>, 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<String>,
) -> Result<Option<String>, 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] #[tauri::command]
pub async fn detect_instruments(app: AppHandle, state: State<'_, ProcessManager>) -> Result<(), String> { pub async fn detect_instruments(app: AppHandle, state: State<'_, ProcessManager>) -> Result<(), String> {
let binary = resolve_binary(app.clone(), "instlist".to_string()).await?; let binary = resolve_binary(app.clone(), "instlist".to_string()).await?;
+2
View File
@@ -14,6 +14,8 @@ pub fn run() {
commands::spawn_process, commands::spawn_process,
commands::get_app_info, commands::get_app_info,
commands::get_default_working_dir, commands::get_default_working_dir,
commands::select_target_file,
commands::select_directory,
commands::send_stdin, commands::send_stdin,
commands::kill_process, commands::kill_process,
commands::resolve_binary, commands::resolve_binary,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery", "productName": "ICCery",
"version": "0.3.0", "version": "0.3.1",
"identifier": "com.gronod.iccery", "identifier": "com.gronod.iccery",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
+1 -1
View File
@@ -460,7 +460,7 @@
<img src="./assets/ICCery-logo.svg" alt="ICCery Logo" class="about-logo" /> <img src="./assets/ICCery-logo.svg" alt="ICCery Logo" class="about-logo" />
</div> </div>
<h2>About ICCery</h2> <h2>About ICCery</h2>
<p><strong>Version:</strong> <span id="aboutVersion">v0.1.0</span> &bull; <strong>Build Date:</strong> <span id="aboutBuildDate">August 2026</span></p> <p><strong>Version:</strong> <span id="aboutVersion">v0.3.1</span> &bull; <strong>Build Date:</strong> <span id="aboutBuildDate">August 2026</span></p>
<p><strong>Copyright &copy; 2026 Gordon Bolton. All rights reserved.</strong></p> <p><strong>Copyright &copy; 2026 Gordon Bolton. All rights reserved.</strong></p>
<h3>Licences & EULA</h3> <h3>Licences & EULA</h3>
<div class="license-text-container"> <div class="license-text-container">
+159 -133
View File
@@ -1,6 +1,5 @@
const { invoke } = window.__TAURI__.core; const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event; const { listen } = window.__TAURI__.event;
const { save } = window.__TAURI__.dialog;
import { setStage1Result } from './printtarg.js'; import { setStage1Result } from './printtarg.js';
import { wizardState } from './state.js'; import { wizardState } from './state.js';
@@ -18,20 +17,23 @@ export function initTargen() {
const logPre = document.getElementById("targenLog"); const logPre = document.getElementById("targenLog");
let currentWorkingDir = ""; let currentWorkingDir = "";
let currentBasename = "";
function updateGenerateButton() { function updateGenerateButton() {
const hasBasename = targetBasename.value.trim().length > 0; const hasBasename = targetBasename && targetBasename.value.trim().length > 0;
const hasCwd = currentWorkingDir.trim().length > 0; const hasCwd = currentWorkingDir && currentWorkingDir.trim().length > 0;
btnGenerate.disabled = !(hasBasename && hasCwd); if (btnGenerate) {
btnGenerate.disabled = !(hasBasename && hasCwd);
}
} }
// Load sensible default working directory on startup // Load sensible default working directory on startup
invoke("get_default_working_dir") invoke("get_default_working_dir")
.then((defaultDir) => { .then((defaultDir) => {
if (defaultDir && !currentWorkingDir) { if (defaultDir) {
currentWorkingDir = defaultDir; currentWorkingDir = defaultDir;
selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; if (selectedPathDisplay) {
selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`;
}
updateGenerateButton(); updateGenerateButton();
} }
}) })
@@ -40,143 +42,167 @@ export function initTargen() {
}); });
// Handle patch count preset changes // Handle patch count preset changes
patchCountPreset.addEventListener("change", (e) => { if (patchCountPreset && patchCountCustom) {
if (e.target.value === "custom") { patchCountPreset.addEventListener("change", (e) => {
patchCountCustom.classList.remove("hidden"); if (e.target.value === "custom") {
} else { patchCountCustom.classList.remove("hidden");
patchCountCustom.classList.add("hidden"); } else {
} patchCountCustom.classList.add("hidden");
}); }
});
}
// Enable generate button only when both basename and directory are valid // Enable generate button when basename is input
targetBasename.addEventListener("input", () => { if (targetBasename) {
updateGenerateButton(); targetBasename.addEventListener("input", () => {
}); if (!currentWorkingDir) {
invoke("get_default_working_dir")
// Browse button opens save dialog .then((defaultDir) => {
btnBrowse.addEventListener("click", async () => { if (defaultDir) {
try { currentWorkingDir = defaultDir;
let filePath = await save({ if (selectedPathDisplay) {
title: "Save Target File As...", selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`;
filters: [{ }
name: "ArgyllCMS Target", }
extensions: ["ti1"] updateGenerateButton();
}] })
}); .catch(() => updateGenerateButton());
} else {
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}`;
updateGenerateButton(); 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 // Generate button clicks
btnGenerate.addEventListener("click", async () => { if (btnGenerate) {
const basename = targetBasename.value.trim(); btnGenerate.addEventListener("click", async () => {
if (!currentWorkingDir || !basename) { const basename = targetBasename ? targetBasename.value.trim() : "";
logPre.textContent = "[ERROR] Please specify both a target basename and a working directory.\n"; if (!currentWorkingDir || !basename) {
logContainer.open = true; if (logPre) {
logContainer.classList.remove("hidden"); logPre.textContent = "[ERROR] Please specify both a target basename and a working directory.\n";
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 (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 unlistenStderr = await listen("process:stderr", (event) => { const config = {
if (event.payload.id === processId && event.payload.line) { colour_space: colourSpace,
logPre.textContent += "ERR: " + event.payload.line + "\n"; patch_count: patchCount,
logPre.scrollTop = logPre.scrollHeight; 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,
};
const unlistenExit = await listen("process:exit", (event) => { try {
if (event.payload.id === processId) { const processId = `targen_${basename}`;
unlistenStdout();
unlistenStderr();
unlistenExit();
if (event.payload.code === 0) { const unlistenStdout = await listen("process:stdout", (event) => {
logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n"; if (event.payload.id === processId && event.payload.line && logPre) {
btnGenerate.disabled = false; logPre.textContent += event.payload.line + "\n";
wizardState.setTarget(basename, currentWorkingDir); logPre.scrollTop = logPre.scrollHeight;
setStage1Result(basename, currentWorkingDir);
advanceToStage2();
} else {
logPre.textContent += `\n[ERROR] Targen exited with code ${event.payload.code}.\n`;
btnGenerate.disabled = false;
} }
} });
});
logPre.textContent = "Starting targen...\n"; const unlistenStderr = await listen("process:stderr", (event) => {
await invoke("run_targen", { config }); if (event.payload.id === processId && event.payload.line && logPre) {
logPre.textContent += "ERR: " + event.payload.line + "\n";
logPre.scrollTop = logPre.scrollHeight;
}
});
} catch (err) { const unlistenExit = await listen("process:exit", (event) => {
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`; if (event.payload.id === processId) {
btnGenerate.disabled = false; 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() { function advanceToStage2() {
@@ -184,11 +210,11 @@ function advanceToStage2() {
const stages = document.querySelectorAll('.stage'); const stages = document.querySelectorAll('.stage');
// Update stepper // Update stepper
steps.forEach(s => s.classList.remove('active')); steps.forEach((s) => s.classList.remove('active'));
if (steps[1]) steps[1].classList.add('active'); if (steps[1]) steps[1].classList.add('active');
// Update sections // Update sections
stages.forEach(s => { stages.forEach((s) => {
s.classList.remove('active'); s.classList.remove('active');
s.classList.add('hidden'); s.classList.add('hidden');
}); });