feat(stage3): support opening existing .ti2 target files to jump to Stage 3 (resolves #140) #143
@@ -166,6 +166,101 @@ pub fn open_log_dir(app: AppHandle) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Ti2Metadata {
|
||||||
|
pub basename: String,
|
||||||
|
pub cwd: String,
|
||||||
|
pub instrument: Option<String>,
|
||||||
|
pub patch_count: Option<u32>,
|
||||||
|
pub colorant_count: Option<u32>,
|
||||||
|
pub pages: Option<u32>,
|
||||||
|
pub has_ti1: bool,
|
||||||
|
pub has_ti2: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn parse_ti2_header(app: AppHandle, file_path: String) -> Result<Ti2Metadata, String> {
|
||||||
|
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<String> = None;
|
||||||
|
let mut patch_count: Option<u32> = None;
|
||||||
|
let mut colorant_count: Option<u32> = None;
|
||||||
|
let mut pages: Option<u32> = 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::<u32>().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::<u32>().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::<u32>().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<String>,
|
||||||
|
) -> Result<Option<String>, 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]
|
#[tauri::command]
|
||||||
pub async fn select_target_file(
|
pub async fn select_target_file(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub fn run() {
|
|||||||
commands::get_default_working_dir,
|
commands::get_default_working_dir,
|
||||||
commands::get_log_path,
|
commands::get_log_path,
|
||||||
commands::open_log_dir,
|
commands::open_log_dir,
|
||||||
|
commands::parse_ti2_header,
|
||||||
|
commands::select_existing_target,
|
||||||
commands::select_target_file,
|
commands::select_target_file,
|
||||||
commands::select_directory,
|
commands::select_directory,
|
||||||
commands::send_stdin,
|
commands::send_stdin,
|
||||||
|
|||||||
+13
-1
@@ -97,7 +97,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Project Directory & Filename</label>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||||
|
<label style="margin: 0;">Project Directory & Filename</label>
|
||||||
|
<button type="button" id="btnOpenExisting" class="secondary" style="font-size: 0.75rem; padding: 2px 8px;" title="Open existing .ti1 or .ti2 target file">📂 Open Target...</button>
|
||||||
|
</div>
|
||||||
<div class="input-row has-btn">
|
<div class="input-row has-btn">
|
||||||
<input type="text" id="targetBasename" placeholder="e.g. my_paper_profile">
|
<input type="text" id="targetBasename" placeholder="e.g. my_paper_profile">
|
||||||
<button type="button" id="btnBrowse" class="secondary">Browse...</button>
|
<button type="button" id="btnBrowse" class="secondary">Browse...</button>
|
||||||
@@ -305,6 +308,15 @@
|
|||||||
<h2>Measure Target</h2>
|
<h2>Measure Target</h2>
|
||||||
<p>Read the printed target patches with your spectrophotometer.</p>
|
<p>Read the printed target patches with your spectrophotometer.</p>
|
||||||
|
|
||||||
|
<!-- Loaded Target Context Banner -->
|
||||||
|
<div id="stage3LoadedTargetBanner" class="info-banner hidden" style="margin-bottom: 16px; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); padding: 10px 14px; border-radius: 6px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<strong>Active Target:</strong> <span id="stage3TargetBasename" style="font-family: monospace; font-weight: 600;"></span>
|
||||||
|
<div id="stage3TargetMeta" style="font-size: 0.8rem; opacity: 0.8; margin-top: 2px;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="badge" id="stage3TargetBadge" style="background: var(--accent-color, #3b82f6); color: white; padding: 3px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 500;">Ready</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Instrument & Hardware Port Selector -->
|
<!-- Instrument & Hardware Port Selector -->
|
||||||
<div class="form-container" style="margin-bottom: 16px;">
|
<div class="form-container" style="margin-bottom: 16px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
+27
-1
@@ -9,12 +9,38 @@ let stage2Basename = "";
|
|||||||
let stage2Cwd = "";
|
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) {
|
export function setStage2Result(basename, cwd) {
|
||||||
stage2Basename = basename || wizardState.basename;
|
stage2Basename = basename || wizardState.basename;
|
||||||
stage2Cwd = cwd || wizardState.cwd;
|
stage2Cwd = cwd || wizardState.cwd;
|
||||||
wizardState.setTarget(stage2Basename, stage2Cwd);
|
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
|
// State machine states
|
||||||
|
|||||||
+26
-1
@@ -8,7 +8,32 @@ export const wizardState = {
|
|||||||
setTarget(basename, cwd) {
|
setTarget(basename, cwd) {
|
||||||
if (basename) this.basename = basename;
|
if (basename) this.basename = basename;
|
||||||
if (cwd) this.cwd = cwd;
|
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() {
|
async updateGating() {
|
||||||
|
|||||||
+55
-2
@@ -1,6 +1,7 @@
|
|||||||
const { invoke } = window.__TAURI__.core;
|
const { invoke } = window.__TAURI__.core;
|
||||||
const { listen } = window.__TAURI__.event;
|
const { listen } = window.__TAURI__.event;
|
||||||
import { setStage1Result } from './printtarg.js';
|
import { setStage1Result } from './printtarg.js';
|
||||||
|
import { populateStage3TargetContext } from './chartread.js';
|
||||||
import { wizardState } from './state.js';
|
import { wizardState } from './state.js';
|
||||||
|
|
||||||
export function initTargen() {
|
export function initTargen() {
|
||||||
@@ -12,6 +13,7 @@ export function initTargen() {
|
|||||||
const targetBasename = document.getElementById("targetBasename");
|
const targetBasename = document.getElementById("targetBasename");
|
||||||
const selectedPathDisplay = document.getElementById("selectedPathDisplay");
|
const selectedPathDisplay = document.getElementById("selectedPathDisplay");
|
||||||
const btnBrowse = document.getElementById("btnBrowse");
|
const btnBrowse = document.getElementById("btnBrowse");
|
||||||
|
const btnOpenExisting = document.getElementById("btnOpenExisting");
|
||||||
const btnGenerate = document.getElementById("btnGenerate");
|
const btnGenerate = document.getElementById("btnGenerate");
|
||||||
const logContainer = document.getElementById("targenLogContainer");
|
const logContainer = document.getElementById("targenLogContainer");
|
||||||
const logPre = document.getElementById("targenLog");
|
const logPre = document.getElementById("targenLog");
|
||||||
@@ -100,13 +102,64 @@ export function initTargen() {
|
|||||||
}
|
}
|
||||||
updateGenerateButton();
|
updateGenerateButton();
|
||||||
}
|
}
|
||||||
} catch (err)
|
} catch (err) {
|
||||||
{
|
|
||||||
console.error("Failed to open file dialog:", 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
|
// Generate button clicks
|
||||||
if (btnGenerate) {
|
if (btnGenerate) {
|
||||||
btnGenerate.addEventListener("click", async () => {
|
btnGenerate.addEventListener("click", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user