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]]
name = "iccery"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"base64 0.22.1",
"image",
+1 -1
View File
@@ -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"
+57
View File
@@ -121,6 +121,63 @@ pub fn get_default_working_dir(app: AppHandle) -> Result<String, String> {
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]
pub async fn detect_instruments(app: AppHandle, state: State<'_, ProcessManager>) -> Result<(), String> {
let binary = resolve_binary(app.clone(), "instlist".to_string()).await?;
+2
View File
@@ -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,
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -460,7 +460,7 @@
<img src="./assets/ICCery-logo.svg" alt="ICCery Logo" class="about-logo" />
</div>
<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>
<h3>Licences & EULA</h3>
<div class="license-text-container">
+67 -41
View File
@@ -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;
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;
if (selectedPathDisplay) {
selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`;
}
updateGenerateButton();
}
})
@@ -40,6 +42,7 @@ export function initTargen() {
});
// Handle patch count preset changes
if (patchCountPreset && patchCountCustom) {
patchCountPreset.addEventListener("change", (e) => {
if (e.target.value === "custom") {
patchCountCustom.classList.remove("hidden");
@@ -47,30 +50,40 @@ export function initTargen() {
patchCountCustom.classList.add("hidden");
}
});
}
// Enable generate button only when both basename and directory are valid
// 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();
}
});
}
// Browse button opens save dialog
// Browse button opens save dialog via backend command
if (btnBrowse) {
btnBrowse.addEventListener("click", async () => {
try {
let filePath = await save({
title: "Save Target File As...",
filters: [{
name: "ArgyllCMS Target",
extensions: ["ti1"]
}]
const filePath = await invoke("select_target_file", {
defaultDir: currentWorkingDir || null,
defaultName: targetBasename ? targetBasename.value.trim() || null : null,
});
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.
// Normalize separators to locate directory and filename
const isWindows = filePath.includes('\\');
const sep = isWindows ? '\\' : '/';
const parts = filePath.split(sep);
@@ -79,72 +92,85 @@ export function initTargen() {
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 save dialog:", err);
} catch (err)
{
console.error("Failed to open file dialog:", err);
}
});
}
// Generate button clicks
if (btnGenerate) {
btnGenerate.addEventListener("click", async () => {
const basename = targetBasename.value.trim();
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";
}
if (logContainer) {
logContainer.open = true;
logContainer.classList.remove("hidden");
}
btnGenerate.disabled = false;
return;
}
logPre.textContent = "";
if (logPre) logPre.textContent = "";
if (logContainer) {
logContainer.open = false;
logContainer.classList.remove("hidden");
}
btnGenerate.disabled = true;
// Determine patch count
let patchCount = parseInt(patchCountPreset.value, 10);
if (patchCountPreset.value === "custom") {
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 => {
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,
white_patches: (whitePatches && whitePatches.value) ? parseInt(whitePatches.value, 10) : null,
black_patches: (blackPatches && blackPatches.value) ? parseInt(blackPatches.value, 10) : null,
basename: basename,
cwd: currentWorkingDir
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) {
if (event.payload.id === processId && event.payload.line && logPre) {
logPre.textContent += event.payload.line + "\n";
logPre.scrollTop = logPre.scrollHeight;
}
});
const unlistenStderr = await listen("process:stderr", (event) => {
if (event.payload.id === processId && event.payload.line) {
if (event.payload.id === processId && event.payload.line && logPre) {
logPre.textContent += "ERR: " + event.payload.line + "\n";
logPre.scrollTop = logPre.scrollHeight;
}
@@ -157,26 +183,26 @@ export function initTargen() {
unlistenExit();
if (event.payload.code === 0) {
logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n";
if (logPre) 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`;
if (logPre) logPre.textContent += `\n[ERROR] Targen exited with code ${event.payload.code}.\n`;
btnGenerate.disabled = false;
}
}
});
logPre.textContent = "Starting targen...\n";
if (logPre) logPre.textContent = "Starting targen...\n";
await invoke("run_targen", { config });
} catch (err) {
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
if (logPre) logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
btnGenerate.disabled = false;
}
});
}
}
function advanceToStage2() {
@@ -184,11 +210,11 @@ function advanceToStage2() {
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');
});