From 3a416532c64b98f7cd8dba1643a5316409dd6ea7 Mon Sep 17 00:00:00 2001 From: gronod Date: Sat, 29 Aug 2026 17:49:17 +0100 Subject: [PATCH] feat(stage3): support opening existing .ti2 target files to jump to Stage 3 (resolves #140) --- src-tauri/src/commands.rs | 95 +++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 + src/index.html | 14 +++++- src/js/chartread.js | 28 +++++++++++- src/js/state.js | 27 ++++++++++- src/js/targen.js | 57 ++++++++++++++++++++++- 6 files changed, 218 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index bc9a9c3..0ac0b81 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -166,6 +166,101 @@ pub fn open_log_dir(app: AppHandle) -> Result<(), String> { Ok(()) } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Ti2Metadata { + pub basename: String, + pub cwd: String, + pub instrument: Option, + pub patch_count: Option, + pub colorant_count: Option, + pub pages: Option, + pub has_ti1: bool, + pub has_ti2: bool, +} + +#[tauri::command] +pub fn parse_ti2_header(app: AppHandle, file_path: String) -> Result { + let path = std::path::PathBuf::from(&file_path); + if !path.exists() { + return Err(format!("File does not exist: {}", file_path)); + } + + let content = std::fs::read_to_string(&path) + .map_err(|e| format!("Failed to read target file: {}", e))?; + + let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); + let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); + let cwd = parent.to_string_lossy().to_string(); + let basename = file_stem.to_string(); + + let ti1_path = parent.join(format!("{}.ti1", basename)); + let ti2_path = parent.join(format!("{}.ti2", basename)); + + let mut instrument: Option = None; + let mut patch_count: Option = None; + let mut colorant_count: Option = None; + let mut pages: Option = None; + + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("TARGET_INSTRUMENT") { + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if parts.len() >= 2 { + instrument = Some(parts[1].trim_matches('"').to_string()); + } + } else if trimmed.starts_with("NUMBER_OF_FIELDS") { + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if parts.len() >= 2 { + colorant_count = parts[1].parse::().ok(); + } + } else if trimmed.starts_with("NUMBER_OF_SETS") { + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if parts.len() >= 2 { + patch_count = parts[1].parse::().ok(); + } + } else if trimmed.starts_with("NUMBER_OF_PAGES") || trimmed.starts_with("PAGES") { + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if parts.len() >= 2 { + pages = parts[1].parse::().ok(); + } + } + } + + Ok(Ti2Metadata { + basename, + cwd, + instrument, + patch_count, + colorant_count, + pages, + has_ti1: ti1_path.exists(), + has_ti2: ti2_path.exists(), + }) +} + +#[tauri::command] +pub async fn select_existing_target( + app: AppHandle, + default_dir: Option, +) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let mut builder = app.dialog().file().add_filter("ArgyllCMS Target Files (.ti1, .ti2)", &["ti1", "ti2"]); + 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_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_target_file( app: AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 037fd7b..b3d248c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,8 @@ pub fn run() { commands::get_default_working_dir, commands::get_log_path, commands::open_log_dir, + commands::parse_ti2_header, + commands::select_existing_target, commands::select_target_file, commands::select_directory, commands::send_stdin, diff --git a/src/index.html b/src/index.html index 53f018f..aca845f 100644 --- a/src/index.html +++ b/src/index.html @@ -97,7 +97,10 @@
- +
+ + +
@@ -305,6 +308,15 @@

Measure Target

Read the printed target patches with your spectrophotometer.

+ + +
diff --git a/src/js/chartread.js b/src/js/chartread.js index 04c5123..2a22c66 100644 --- a/src/js/chartread.js +++ b/src/js/chartread.js @@ -9,12 +9,38 @@ let stage2Basename = ""; let stage2Cwd = ""; /** - * Called by printtarg.js after Stage 2 completes. + * Called by printtarg.js after Stage 2 completes, or when resuming from .ti2. */ export function setStage2Result(basename, cwd) { stage2Basename = basename || wizardState.basename; stage2Cwd = cwd || wizardState.cwd; wizardState.setTarget(stage2Basename, stage2Cwd); + populateStage3TargetContext(); +} + +export function populateStage3TargetContext(metadata) { + const banner = document.getElementById("stage3LoadedTargetBanner"); + const basenameEl = document.getElementById("stage3TargetBasename"); + const metaEl = document.getElementById("stage3TargetMeta"); + const badgeEl = document.getElementById("stage3TargetBadge"); + + const basename = (metadata && metadata.basename) || stage2Basename || wizardState.basename; + const cwd = (metadata && metadata.cwd) || stage2Cwd || wizardState.cwd; + + if (basename && banner) { + banner.classList.remove("hidden"); + if (basenameEl) basenameEl.textContent = `${basename}.ti2`; + if (metaEl) { + const parts = []; + if (metadata && metadata.patch_count) parts.push(`${metadata.patch_count} patches`); + if (metadata && metadata.instrument) parts.push(`Instrument: ${metadata.instrument}`); + if (cwd) parts.push(`Dir: ${cwd}`); + metaEl.textContent = parts.join(" • "); + } + if (badgeEl) { + badgeEl.textContent = metadata ? "Resumed from .ti2" : "Generated"; + } + } } // State machine states diff --git a/src/js/state.js b/src/js/state.js index 57c7b51..5ef8618 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -8,7 +8,32 @@ export const wizardState = { setTarget(basename, cwd) { if (basename) this.basename = basename; if (cwd) this.cwd = cwd; - this.updateGating(); + return this.updateGating(); + }, + + navigateToStage(stageNumber) { + this.currentStage = stageNumber; + const steps = document.querySelectorAll('.step'); + const stages = document.querySelectorAll('.stage'); + + steps.forEach(s => { + if (s.getAttribute('data-step') === String(stageNumber)) { + s.classList.remove('disabled'); + s.classList.add('active'); + } else { + s.classList.remove('active'); + } + }); + + stages.forEach(s => { + if (s.id === `stage-${stageNumber}`) { + s.classList.remove('hidden'); + s.classList.add('active'); + } else { + s.classList.remove('active'); + s.classList.add('hidden'); + } + }); }, async updateGating() { diff --git a/src/js/targen.js b/src/js/targen.js index 4a5112c..1884a7f 100644 --- a/src/js/targen.js +++ b/src/js/targen.js @@ -1,6 +1,7 @@ const { invoke } = window.__TAURI__.core; const { listen } = window.__TAURI__.event; import { setStage1Result } from './printtarg.js'; +import { populateStage3TargetContext } from './chartread.js'; import { wizardState } from './state.js'; export function initTargen() { @@ -12,6 +13,7 @@ export function initTargen() { const targetBasename = document.getElementById("targetBasename"); const selectedPathDisplay = document.getElementById("selectedPathDisplay"); const btnBrowse = document.getElementById("btnBrowse"); + const btnOpenExisting = document.getElementById("btnOpenExisting"); const btnGenerate = document.getElementById("btnGenerate"); const logContainer = document.getElementById("targenLogContainer"); const logPre = document.getElementById("targenLog"); @@ -100,13 +102,64 @@ export function initTargen() { } updateGenerateButton(); } - } catch (err) - { + } catch (err) { console.error("Failed to open file dialog:", err); } }); } + // Open existing target (.ti1 or .ti2) + if (btnOpenExisting) { + btnOpenExisting.addEventListener("click", async () => { + try { + const filePath = await invoke("select_existing_target", { + defaultDir: currentWorkingDir || null, + }); + + if (filePath) { + const isWindows = filePath.includes('\\'); + const sep = isWindows ? '\\' : '/'; + const parts = filePath.split(sep); + const fileName = parts.pop(); + const isTi2 = fileName.toLowerCase().endsWith('.ti2'); + + currentWorkingDir = parts.join(sep); + const basename = fileName.replace(/\.(ti1|ti2)$/i, ''); + + if (targetBasename) { + targetBasename.value = basename; + } + if (selectedPathDisplay) { + selectedPathDisplay.textContent = `Directory: ${currentWorkingDir}`; + } + updateGenerateButton(); + + if (isTi2) { + // Parse .ti2 metadata and jump straight to Stage 3 + try { + const meta = await invoke("parse_ti2_header", { filePath }); + await wizardState.setTarget(meta.basename, meta.cwd); + populateStage3TargetContext(meta); + wizardState.navigateToStage(3); + } catch (parseErr) { + console.warn("Could not parse .ti2 header, navigating with basic target:", parseErr); + await wizardState.setTarget(basename, currentWorkingDir); + populateStage3TargetContext({ basename, cwd: currentWorkingDir }); + wizardState.navigateToStage(3); + } + } else { + // .ti1 selected -> advance to Stage 2 (Print Layout) + await wizardState.setTarget(basename, currentWorkingDir); + setStage1Result(basename, currentWorkingDir); + wizardState.navigateToStage(2); + } + } + } catch (err) { + console.error("Failed to open existing target:", err); + } + }); + } + // Generate button clicks if (btnGenerate) { btnGenerate.addEventListener("click", async () => { -- 2.39.5