From 5e52cb93a2226a9bdca0345146942c55f93bc2b3 Mon Sep 17 00:00:00 2001 From: gronod Date: Tue, 25 Aug 2026 10:37:42 +0100 Subject: [PATCH] feat(wizard): gate stage navigation on disk artefacts and remove test_target fallbacks (v0.1.15) --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands.rs | 37 ++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/js/app.js | 8 +++++ src/js/chartread.js | 23 +++++++++----- src/js/colprof.js | 22 ++++++++++--- src/js/printtarg.js | 7 +++-- src/js/profcheck.js | 21 ++++++++++--- src/js/state.js | 65 ++++++++++++++++++++++++++++++++------- src/js/targen.js | 4 +-- src/styles/main.css | 9 ++++++ 13 files changed, 170 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 18fb9bf..09cdd1c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "iccery", "private": true, - "version": "0.1.14", + "version": "0.1.15", "type": "module", "scripts": { "tauri": "tauri" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 44eab58..dc5eb5a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iccery" -version = "0.1.14" +version = "0.1.15" 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 37c8f59..7535eaf 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -122,6 +122,43 @@ pub fn resolve_profile_extension(cwd: &str, basename: &str) -> (String, std::pat } } +#[derive(Serialize)] +pub struct StageStatus { + pub stage1_complete: bool, + pub stage2_complete: bool, + pub stage3_complete: bool, + pub stage4_complete: bool, + pub profile_path: Option, +} + +#[tauri::command] +pub fn verify_stage_artefacts(cwd: String, basename: String) -> StageStatus { + if cwd.trim().is_empty() || basename.trim().is_empty() { + return StageStatus { + stage1_complete: false, + stage2_complete: false, + stage3_complete: false, + stage4_complete: false, + profile_path: None, + }; + } + + let base_path = std::path::Path::new(&cwd); + let ti1 = base_path.join(format!("{}.ti1", basename)).exists(); + let ti2 = base_path.join(format!("{}.ti2", basename)).exists(); + let ti3 = base_path.join(format!("{}.ti3", basename)).exists(); + let (_, prof_p) = resolve_profile_extension(&cwd, &basename); + let prof_exists = prof_p.exists(); + + StageStatus { + stage1_complete: ti1, + stage2_complete: ti2, + stage3_complete: ti3, + stage4_complete: prof_exists, + profile_path: if prof_exists { Some(prof_p.to_string_lossy().to_string()) } else { None }, + } +} + #[tauri::command] pub fn get_profile_path(cwd: String, basename: String) -> String { let (_, path) = resolve_profile_extension(&cwd, &basename); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5e3e6b6..dd951e9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,6 +19,7 @@ pub fn run() { commands::resolve_binary, commands::detect_instruments, commands::get_profile_path, + commands::verify_stage_artefacts, commands::extract_gamut, commands::run_targen, commands::run_printtarg, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bb059bd..8b18854 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.1.14", + "version": "0.1.15", "identifier": "com.gronod.iccery", "build": { "frontendDist": "../src" diff --git a/src/js/app.js b/src/js/app.js index 9645cd3..53c5304 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -5,6 +5,7 @@ import { initColprof } from './colprof.js'; import { initProfcheck } from './profcheck.js'; import { initSettings } from './settings.js'; import { initGamutViewer } from './gamut_viewer.js'; +import { wizardState } from './state.js'; const { invoke } = window.__TAURI__.core; @@ -13,8 +14,15 @@ document.addEventListener('DOMContentLoaded', () => { const steps = document.querySelectorAll('.step'); const stages = document.querySelectorAll('.stage'); + // Initialize gating on load + wizardState.updateGating(); + steps.forEach(step => { step.addEventListener('click', () => { + if (step.classList.contains('disabled')) { + return; + } + const targetStep = step.getAttribute('data-step'); // Update UI diff --git a/src/js/chartread.js b/src/js/chartread.js index a0fb3d0..9b78b92 100644 --- a/src/js/chartread.js +++ b/src/js/chartread.js @@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; import { startSwatchListener, stopSwatchListener } from './swatch_grid.js'; import { setStage3Result } from './colprof.js'; +import { wizardState } from './state.js'; // Module-level state: set by Stage 2 when it completes let stage2Basename = ""; @@ -11,8 +12,9 @@ let stage2Cwd = ""; * Called by printtarg.js after Stage 2 completes. */ export function setStage2Result(basename, cwd) { - stage2Basename = basename; - stage2Cwd = cwd; + stage2Basename = basename || wizardState.basename; + stage2Cwd = cwd || wizardState.cwd; + wizardState.setTarget(stage2Basename, stage2Cwd); } // State machine states @@ -84,11 +86,17 @@ export function initChartread() { // Start reading button if (btnStartRead) { btnStartRead.addEventListener("click", async () => { - if (!stage2Basename) { + const basename = stage2Basename || wizardState.basename; + const cwd = stage2Cwd || wizardState.cwd; + + if (!basename || !cwd) { setPrompt("Error: No .ti2 file available. Complete Stage 2 first."); return; } + stage2Basename = basename; + stage2Cwd = cwd; + logPre.textContent = ""; logContainer.open = false; logContainer.classList.remove("hidden"); @@ -96,11 +104,11 @@ export function initChartread() { setPrompt("Starting chartread... waiting for instrument calibration prompt."); const config = { - basename: stage2Basename, - cwd: stage2Cwd, + basename: basename, + cwd: cwd, }; - currentProcessId = `chartread_${stage2Basename}`; + currentProcessId = `chartread_${basename}`; // Start swatch grid listener await startSwatchListener(currentProcessId); @@ -152,7 +160,8 @@ export function initChartread() { setState(STATE.FINISHED); setPrompt("✅ Measurement complete! .ti3 file has been saved."); logPre.textContent += "\n[SUCCESS] chartread completed. .ti3 file written.\n"; - setStage3Result(stage2Basename, stage2Cwd); + wizardState.setTarget(basename, cwd); + setStage3Result(basename, cwd); advanceToStage4(); } else { setState(STATE.FINISHED); diff --git a/src/js/colprof.js b/src/js/colprof.js index 7fa57b6..4cf66be 100644 --- a/src/js/colprof.js +++ b/src/js/colprof.js @@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; import { setStage4Result } from './profcheck.js'; import { loadGamutMesh } from './gamut_viewer.js'; +import { wizardState } from './state.js'; let chartreadBasename = ""; let chartreadCwd = ""; @@ -10,8 +11,9 @@ let chartreadCwd = ""; * Called by chartread.js after Stage 3 completes. */ export function setStage3Result(basename, cwd) { - chartreadBasename = basename; - chartreadCwd = cwd; + chartreadBasename = basename || wizardState.basename; + chartreadCwd = cwd || wizardState.cwd; + wizardState.setTarget(chartreadBasename, chartreadCwd); } export function initColprof() { @@ -31,8 +33,19 @@ export function initColprof() { if (!btnCreateProfile) return; btnCreateProfile.addEventListener("click", async () => { - const basename = chartreadBasename || "test_target"; - const cwd = chartreadCwd || ""; + const basename = chartreadBasename || wizardState.basename; + const cwd = chartreadCwd || wizardState.cwd; + + if (!basename || !cwd) { + logPre.textContent = "[ERROR] No .ti3 measurement file available. Please complete Stage 3 first.\n"; + logContainer.open = true; + logContainer.classList.remove("hidden"); + btnCreateProfile.disabled = false; + return; + } + + chartreadBasename = basename; + chartreadCwd = cwd; const description = descInput.value.trim() || basename; @@ -104,6 +117,7 @@ export function initColprof() { successInfo.textContent = `Profile: ${displayFilename} (${description})`; successCard.classList.remove("hidden"); + wizardState.setTarget(basename, cwd); setStage4Result(basename, cwd); // Automatically extract gamut mesh for 3D visualization diff --git a/src/js/printtarg.js b/src/js/printtarg.js index d6bc09a..cf789d4 100644 --- a/src/js/printtarg.js +++ b/src/js/printtarg.js @@ -1,6 +1,7 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; import { setStage2Result } from './chartread.js'; +import { wizardState } from './state.js'; // Module-level state: set by Stage 1 when it completes let stage1Basename = ""; @@ -13,8 +14,9 @@ let discoveredPrinters = []; * Passes the basename and working directory forward. */ export function setStage1Result(basename, cwd) { - stage1Basename = basename; - stage1Cwd = cwd; + stage1Basename = basename || wizardState.basename; + stage1Cwd = cwd || wizardState.cwd; + wizardState.setTarget(stage1Basename, stage1Cwd); } export function initPrinttarg() { @@ -347,6 +349,7 @@ export function initPrinttarg() { showNotification("info", "Target pages generated. Select your destination printer below and print with color management strictly bypassed."); } + wizardState.setTarget(stage1Basename, stage1Cwd); setStage2Result(stage1Basename, stage1Cwd); } else { logPre.textContent += `\n[ERROR] printtarg exited with code ${event.payload.code}.\n`; diff --git a/src/js/profcheck.js b/src/js/profcheck.js index 0b79861..8136208 100644 --- a/src/js/profcheck.js +++ b/src/js/profcheck.js @@ -1,6 +1,7 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; import { loadGamutMesh } from './gamut_viewer.js'; +import { wizardState } from './state.js'; let profileBasename = ""; let profileCwd = ""; @@ -9,8 +10,9 @@ let profileCwd = ""; * Called by colprof.js after Stage 4 completes. */ export function setStage4Result(basename, cwd) { - profileBasename = basename; - profileCwd = cwd; + profileBasename = basename || wizardState.basename; + profileCwd = cwd || wizardState.cwd; + wizardState.setTarget(profileBasename, profileCwd); } export function initProfcheck() { @@ -26,8 +28,19 @@ export function initProfcheck() { if (!btnVerify) return; btnVerify.addEventListener("click", async () => { - const basename = profileBasename || "test_target"; - const cwd = profileCwd || ""; + const basename = profileBasename || wizardState.basename; + const cwd = profileCwd || wizardState.cwd; + + if (!basename || !cwd) { + logPre.textContent = "[ERROR] No profile available to verify. Please complete Stage 4 first.\n"; + logContainer.open = true; + logContainer.classList.remove("hidden"); + btnVerify.disabled = false; + return; + } + + profileBasename = basename; + profileCwd = cwd; const sep = cwd.includes('\\') ? '\\' : '/'; const ti3Path = cwd ? `${cwd}${sep}${basename}.ti3` : `${basename}.ti3`; diff --git a/src/js/state.js b/src/js/state.js index 8642ed3..57c7b51 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -1,16 +1,59 @@ -// Simple state machine for the UI -export const state = { +const { invoke } = window.__TAURI__.core; + +export const wizardState = { currentStage: 1, - profileName: 'default_profile', - instrument: null, + basename: "", + cwd: "", - update(newState) { - Object.assign(this, newState); - this.render(); + setTarget(basename, cwd) { + if (basename) this.basename = basename; + if (cwd) this.cwd = cwd; + this.updateGating(); }, - - render() { - // Reactive UI updates go here - console.log('State updated:', this); + + async updateGating() { + const steps = document.querySelectorAll('.step'); + if (!steps || steps.length === 0) return; + + if (!this.basename || !this.cwd) { + steps.forEach((step, idx) => { + if (idx === 0) { + step.classList.remove('disabled'); + } else { + step.classList.add('disabled'); + } + }); + return; + } + + try { + const status = await invoke('verify_stage_artefacts', { + cwd: this.cwd, + basename: this.basename, + }); + + // Step 1: always accessible + // Step 2: unlocked if .ti1 exists + // Step 3: unlocked if .ti2 exists + // Step 4: unlocked if .ti3 exists + // Step 5: unlocked if profile exists + const unlocked = [ + true, + status.stage1_complete, + status.stage2_complete, + status.stage3_complete, + status.stage4_complete, + ]; + + steps.forEach((step, idx) => { + if (unlocked[idx]) { + step.classList.remove('disabled'); + } else { + step.classList.add('disabled'); + } + }); + } catch (e) { + console.warn("Could not verify stage artefacts:", e); + } } }; diff --git a/src/js/targen.js b/src/js/targen.js index 91b3ed0..083eb42 100644 --- a/src/js/targen.js +++ b/src/js/targen.js @@ -2,6 +2,7 @@ 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'; export function initTargen() { const colourSpaceRadios = document.querySelectorAll('input[name="colourSpace"]'); @@ -157,9 +158,8 @@ export function initTargen() { if (event.payload.code === 0) { logPre.textContent += "\n[SUCCESS] Targen completed successfully.\n"; - // In a real app we'd dispatch an event to advance the stepper here. - // For now, we'll manually unlock stage 2 in the state. btnGenerate.disabled = false; + wizardState.setTarget(basename, currentWorkingDir); setStage1Result(basename, currentWorkingDir); advanceToStage2(); } else { diff --git a/src/styles/main.css b/src/styles/main.css index 9c04fa3..ff99b6f 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -93,6 +93,15 @@ body { border-left: 4px solid var(--accent-color); } +.step.disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.step.disabled:hover { + background-color: transparent; +} + .content { flex: 1; padding: 24px 32px; -- 2.39.5