feat(presets): Reusable Profiling Presets with import/export and multi-stage loading (fixes #88) #100
@@ -35,6 +35,11 @@ pub fn run() {
|
||||
commands::print_target_native,
|
||||
settings::load_settings,
|
||||
settings::save_settings,
|
||||
settings::get_all_presets,
|
||||
settings::save_preset,
|
||||
settings::delete_preset,
|
||||
settings::export_preset_json,
|
||||
settings::import_preset_json,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -2,10 +2,99 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct ProfilingPreset {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub colour_space: String,
|
||||
pub patch_count: u32,
|
||||
pub white_patches: Option<u32>,
|
||||
pub black_patches: Option<u32>,
|
||||
pub instrument: String,
|
||||
pub page_size: String,
|
||||
pub bit_depth: u8,
|
||||
pub dpi: u32,
|
||||
pub colprof_algorithm: String,
|
||||
pub colprof_quality: String,
|
||||
pub colprof_intent: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||
pub struct AppSettings {
|
||||
pub argyll_binary_dir: Option<String>,
|
||||
pub default_instrument: Option<String>,
|
||||
#[serde(default)]
|
||||
pub custom_presets: Vec<ProfilingPreset>,
|
||||
}
|
||||
|
||||
pub fn get_default_presets() -> Vec<ProfilingPreset> {
|
||||
vec![
|
||||
ProfilingPreset {
|
||||
id: "preset-std-rgb".to_string(),
|
||||
name: "Standard RGB Photo (800 patches)".to_string(),
|
||||
description: Some("Recommended for standard fine-art and photographic RGB printing on inkjet printers.".to_string()),
|
||||
colour_space: "rgb".to_string(),
|
||||
patch_count: 800,
|
||||
white_patches: Some(4),
|
||||
black_patches: None,
|
||||
instrument: "i1".to_string(),
|
||||
page_size: "A4".to_string(),
|
||||
bit_depth: 8,
|
||||
dpi: 300,
|
||||
colprof_algorithm: "l".to_string(),
|
||||
colprof_quality: "m".to_string(),
|
||||
colprof_intent: None,
|
||||
},
|
||||
ProfilingPreset {
|
||||
id: "preset-hq-cmyk".to_string(),
|
||||
name: "High-Gamut CMYK Proofing (1500 patches)".to_string(),
|
||||
description: Some("High-precision CMYK press and RIP proofing profiling with deep shadow neutral boost.".to_string()),
|
||||
colour_space: "cmyk".to_string(),
|
||||
patch_count: 1500,
|
||||
white_patches: None,
|
||||
black_patches: Some(8),
|
||||
instrument: "i1".to_string(),
|
||||
page_size: "A3".to_string(),
|
||||
bit_depth: 16,
|
||||
dpi: 300,
|
||||
colprof_algorithm: "l".to_string(),
|
||||
colprof_quality: "h".to_string(),
|
||||
colprof_intent: None,
|
||||
},
|
||||
ProfilingPreset {
|
||||
id: "preset-draft-rgb".to_string(),
|
||||
name: "Fast RGB Draft (400 patches)".to_string(),
|
||||
description: Some("Quick turn-around draft profiling for testing new media.".to_string()),
|
||||
colour_space: "rgb".to_string(),
|
||||
patch_count: 400,
|
||||
white_patches: None,
|
||||
black_patches: None,
|
||||
instrument: "i1".to_string(),
|
||||
page_size: "A4".to_string(),
|
||||
bit_depth: 8,
|
||||
dpi: 150,
|
||||
colprof_algorithm: "l".to_string(),
|
||||
colprof_quality: "l".to_string(),
|
||||
colprof_intent: None,
|
||||
},
|
||||
ProfilingPreset {
|
||||
id: "preset-ultra-rgb".to_string(),
|
||||
name: "Ultra Precision RGB (2500 patches)".to_string(),
|
||||
description: Some("Maximum precision lookup table profile for exhibition and master printmaking.".to_string()),
|
||||
colour_space: "rgb".to_string(),
|
||||
patch_count: 2500,
|
||||
white_patches: Some(6),
|
||||
black_patches: Some(6),
|
||||
instrument: "i1".to_string(),
|
||||
page_size: "A3".to_string(),
|
||||
bit_depth: 16,
|
||||
dpi: 300,
|
||||
colprof_algorithm: "l".to_string(),
|
||||
colprof_quality: "u".to_string(),
|
||||
colprof_intent: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -25,3 +114,81 @@ pub fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String
|
||||
let json = serde_json::to_string_pretty(&settings).unwrap();
|
||||
fs::write(path.join("settings.json"), json).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_presets(app: AppHandle) -> Vec<ProfilingPreset> {
|
||||
let mut presets = get_default_presets();
|
||||
let settings = load_settings(app).unwrap_or_default();
|
||||
presets.extend(settings.custom_presets);
|
||||
presets
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_preset(app: AppHandle, preset: ProfilingPreset) -> Result<Vec<ProfilingPreset>, String> {
|
||||
let mut settings = load_settings(app.clone()).unwrap_or_default();
|
||||
if let Some(pos) = settings.custom_presets.iter().position(|p| p.id == preset.id) {
|
||||
settings.custom_presets[pos] = preset;
|
||||
} else {
|
||||
settings.custom_presets.push(preset);
|
||||
}
|
||||
save_settings(app.clone(), settings)?;
|
||||
Ok(get_all_presets(app))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_preset(app: AppHandle, id: String) -> Result<Vec<ProfilingPreset>, String> {
|
||||
let mut settings = load_settings(app.clone()).unwrap_or_default();
|
||||
settings.custom_presets.retain(|p| p.id != id);
|
||||
save_settings(app.clone(), settings)?;
|
||||
Ok(get_all_presets(app))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn export_preset_json(preset: ProfilingPreset) -> Result<String, String> {
|
||||
serde_json::to_string_pretty(&preset).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn import_preset_json(json: String) -> Result<ProfilingPreset, String> {
|
||||
serde_json::from_str::<ProfilingPreset>(&json).map_err(|e| format!("Invalid preset format: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_default_presets() {
|
||||
let defaults = get_default_presets();
|
||||
assert_eq!(defaults.len(), 4);
|
||||
assert_eq!(defaults[0].id, "preset-std-rgb");
|
||||
assert_eq!(defaults[0].colour_space, "rgb");
|
||||
assert_eq!(defaults[0].patch_count, 800);
|
||||
assert_eq!(defaults[1].colour_space, "cmyk");
|
||||
assert_eq!(defaults[1].patch_count, 1500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preset_json_export_and_import() {
|
||||
let preset = ProfilingPreset {
|
||||
id: "custom-test".to_string(),
|
||||
name: "Custom Test Preset".to_string(),
|
||||
description: Some("Test description".to_string()),
|
||||
colour_space: "rgb".to_string(),
|
||||
patch_count: 1200,
|
||||
white_patches: Some(2),
|
||||
black_patches: Some(4),
|
||||
instrument: "i1".to_string(),
|
||||
page_size: "A4".to_string(),
|
||||
bit_depth: 16,
|
||||
dpi: 300,
|
||||
colprof_algorithm: "l".to_string(),
|
||||
colprof_quality: "h".to_string(),
|
||||
colprof_intent: None,
|
||||
};
|
||||
|
||||
let json = export_preset_json(preset.clone()).expect("Export failed");
|
||||
let imported = import_preset_json(json).expect("Import failed");
|
||||
assert_eq!(preset, imported);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,21 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profiling Preset Selector -->
|
||||
<div class="preset-selector-container" style="padding: 0 16px 12px; border-bottom: 1px solid var(--border-color, #2a2a30);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||
<label for="presetSelect" style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; font-weight: 600;">Profiling Preset</label>
|
||||
<div style="display: flex; gap: 4px;">
|
||||
<button type="button" id="btnSavePresetModal" class="icon-btn" title="Save current settings as Preset" style="font-size: 0.85rem; padding: 2px 4px; border: 1px solid var(--border-color, #333); border-radius: 4px; background: transparent; cursor: pointer;">💾</button>
|
||||
<button type="button" id="btnOpenPresetsDialog" class="icon-btn" title="Manage Presets (Import/Export/Delete)" style="font-size: 0.85rem; padding: 2px 4px; border: 1px solid var(--border-color, #333); border-radius: 4px; background: transparent; cursor: pointer;">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
<select id="presetSelect" style="width: 100%; font-size: 0.85rem; padding: 6px 8px; border-radius: 6px; background: var(--bg-card, #1a1a22); color: var(--text-color, #eee); border: 1px solid var(--border-color, #333); cursor: pointer;">
|
||||
<option value="" disabled selected>Loading presets...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<nav class="stepper">
|
||||
<button class="step active" data-step="1">1. Generate Target</button>
|
||||
<button class="step" data-step="2">2. Print Layout</button>
|
||||
@@ -511,5 +526,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Save Preset Dialog -->
|
||||
<dialog id="savePresetDialog" class="settings-modal">
|
||||
<div class="modal-content">
|
||||
<h2>Save Profiling Preset</h2>
|
||||
<p style="font-size: 0.9rem; opacity: 0.8; margin-bottom: 16px;">Save the current settings from Stage 1, 2, and 4 as a reusable profiling preset.</p>
|
||||
<div class="form-group">
|
||||
<label for="savePresetName">Preset Name</label>
|
||||
<input type="text" id="savePresetName" placeholder="e.g. My Custom FineArt Preset">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="savePresetDesc">Description (Optional)</label>
|
||||
<input type="text" id="savePresetDesc" placeholder="e.g. Optimized for 300gsm Cotton Rag with 1500 patches">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button id="btnConfirmSavePreset" class="primary">Save Preset</button>
|
||||
<button id="btnCloseSavePresetDialog" class="secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Manage Presets Dialog -->
|
||||
<dialog id="managePresetsDialog" class="settings-modal" style="max-width: 600px;">
|
||||
<div class="modal-content">
|
||||
<h2>Manage Profiling Presets</h2>
|
||||
<p style="font-size: 0.9rem; opacity: 0.8; margin-bottom: 14px;">View built-in and custom presets, or import and export preset configurations in JSON format.</p>
|
||||
|
||||
<div id="managePresetsList" style="display: flex; flex-direction: column; gap: 8px; max-height: 240px; overflow-y: auto; margin-bottom: 16px; padding: 4px;"></div>
|
||||
|
||||
<div style="display: flex; gap: 8px; justify-content: space-between; border-top: 1px solid var(--border-color, #333); padding-top: 14px;">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" id="btnExportActivePreset" class="secondary" title="Export currently selected preset to JSON file">📤 Export Active</button>
|
||||
<button type="button" id="btnImportPreset" class="secondary" title="Import preset from JSON file">📥 Import Preset</button>
|
||||
</div>
|
||||
<button id="btnCloseManagePresetsDialog" class="primary">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 { initPresets } from './presets.js';
|
||||
import { wizardState } from './state.js';
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
@@ -89,4 +90,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
safeInit('Stage 5 (Profcheck)', initProfcheck);
|
||||
safeInit('Settings', initSettings);
|
||||
safeInit('Gamut Viewer', initGamutViewer);
|
||||
safeInit('Presets', initPresets);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
let currentPresets = [];
|
||||
let activePresetId = "preset-std-rgb";
|
||||
|
||||
export async function initPresets() {
|
||||
const presetSelect = document.getElementById("presetSelect");
|
||||
const btnSavePresetModal = document.getElementById("btnSavePresetModal");
|
||||
const btnOpenPresetsDialog = document.getElementById("btnOpenPresetsDialog");
|
||||
const savePresetDialog = document.getElementById("savePresetDialog");
|
||||
const btnCloseSavePresetDialog = document.getElementById("btnCloseSavePresetDialog");
|
||||
const btnConfirmSavePreset = document.getElementById("btnConfirmSavePreset");
|
||||
const savePresetName = document.getElementById("savePresetName");
|
||||
const savePresetDesc = document.getElementById("savePresetDesc");
|
||||
const managePresetsDialog = document.getElementById("managePresetsDialog");
|
||||
const btnCloseManagePresetsDialog = document.getElementById("btnCloseManagePresetsDialog");
|
||||
const managePresetsList = document.getElementById("managePresetsList");
|
||||
const btnExportActivePreset = document.getElementById("btnExportActivePreset");
|
||||
const btnImportPreset = document.getElementById("btnImportPreset");
|
||||
|
||||
async function loadPresets() {
|
||||
try {
|
||||
currentPresets = await invoke("get_all_presets");
|
||||
renderPresetDropdown();
|
||||
renderManagePresetsList();
|
||||
} catch (err) {
|
||||
console.error("Failed to load presets:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPresetDropdown() {
|
||||
if (!presetSelect) return;
|
||||
presetSelect.innerHTML = "";
|
||||
|
||||
currentPresets.forEach((p) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
if (p.id === activePresetId) opt.selected = true;
|
||||
presetSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function renderManagePresetsList() {
|
||||
if (!managePresetsList) return;
|
||||
managePresetsList.innerHTML = "";
|
||||
|
||||
currentPresets.forEach((p) => {
|
||||
const isBuiltin = p.id.startsWith("preset-");
|
||||
const item = document.createElement("div");
|
||||
item.style.cssText = "display:flex; justify-content:space-between; align-items:center; background:rgba(255,255,255,0.05); padding:8px 12px; border-radius:6px;";
|
||||
|
||||
item.innerHTML = `
|
||||
<div style="flex:1; margin-right:12px;">
|
||||
<div style="font-weight:600; font-size:0.9rem;">${p.name} ${isBuiltin ? '<span style="font-size:0.7rem; opacity:0.6; border:1px solid #555; padding:1px 4px; border-radius:3px; margin-left:4px;">Built-in</span>' : ''}</div>
|
||||
<div style="font-size:0.75rem; opacity:0.7; margin-top:2px;">${p.description || "No description"}</div>
|
||||
<div style="font-size:0.7rem; opacity:0.5; margin-top:2px;">${p.colour_space.toUpperCase()} • ${p.patch_count} patches • ${p.page_size} • ${p.bit_depth}-bit • Quality ${p.colprof_quality.toUpperCase()}</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px;">
|
||||
<button type="button" class="secondary btn-export-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Export</button>
|
||||
${!isBuiltin ? `<button type="button" class="danger btn-delete-one" data-id="${p.id}" style="font-size:0.75rem; padding:4px 8px;">Delete</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
managePresetsList.appendChild(item);
|
||||
});
|
||||
|
||||
// Attach listeners for export / delete
|
||||
managePresetsList.querySelectorAll(".btn-export-one").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const id = btn.getAttribute("data-id");
|
||||
const preset = currentPresets.find(p => p.id === id);
|
||||
if (preset) exportPreset(preset);
|
||||
});
|
||||
});
|
||||
|
||||
managePresetsList.querySelectorAll(".btn-delete-one").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const id = btn.getAttribute("data-id");
|
||||
if (confirm("Delete this custom preset?")) {
|
||||
try {
|
||||
currentPresets = await invoke("delete_preset", { id });
|
||||
if (activePresetId === id) activePresetId = currentPresets[0]?.id || "preset-std-rgb";
|
||||
renderPresetDropdown();
|
||||
renderManagePresetsList();
|
||||
} catch (err) {
|
||||
alert("Delete failed: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyPreset(preset) {
|
||||
if (!preset) return;
|
||||
activePresetId = preset.id;
|
||||
|
||||
// Stage 1 controls
|
||||
const csRadios = document.querySelectorAll('input[name="colourSpace"]');
|
||||
csRadios.forEach((r) => {
|
||||
if (r.value.toLowerCase() === preset.colour_space.toLowerCase()) r.checked = true;
|
||||
});
|
||||
|
||||
const patchCountPreset = document.getElementById("patchCountPreset");
|
||||
const patchCountCustom = document.getElementById("patchCountCustom");
|
||||
if (patchCountPreset && patchCountCustom) {
|
||||
const match = Array.from(patchCountPreset.options).find(o => o.value === String(preset.patch_count));
|
||||
if (match) {
|
||||
patchCountPreset.value = String(preset.patch_count);
|
||||
patchCountCustom.classList.add("hidden");
|
||||
} else {
|
||||
patchCountPreset.value = "custom";
|
||||
patchCountCustom.value = preset.patch_count;
|
||||
patchCountCustom.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
const whitePatches = document.getElementById("whitePatches");
|
||||
if (whitePatches) whitePatches.value = preset.white_patches !== null && preset.white_patches !== undefined ? preset.white_patches : "";
|
||||
|
||||
const blackPatches = document.getElementById("blackPatches");
|
||||
if (blackPatches) blackPatches.value = preset.black_patches !== null && preset.black_patches !== undefined ? preset.black_patches : "";
|
||||
|
||||
// Stage 2 controls
|
||||
const instrumentSelect = document.getElementById("instrumentSelect");
|
||||
if (instrumentSelect && preset.instrument) instrumentSelect.value = preset.instrument;
|
||||
|
||||
const pageSizeSelect = document.getElementById("pageSizeSelect");
|
||||
if (pageSizeSelect && preset.page_size) pageSizeSelect.value = preset.page_size;
|
||||
|
||||
const bitDepthRadios = document.querySelectorAll('input[name="bitDepth"]');
|
||||
bitDepthRadios.forEach((r) => {
|
||||
if (Number(r.value) === preset.bit_depth) r.checked = true;
|
||||
});
|
||||
|
||||
// Stage 4 controls
|
||||
const colprofQuality = document.getElementById("colprofQuality");
|
||||
if (colprofQuality && preset.colprof_quality) colprofQuality.value = preset.colprof_quality;
|
||||
|
||||
const colprofAlgorithm = document.getElementById("colprofAlgorithm");
|
||||
if (colprofAlgorithm && preset.colprof_algorithm) colprofAlgorithm.value = preset.colprof_algorithm;
|
||||
}
|
||||
|
||||
function collectCurrentSettingsAsPreset(name, description) {
|
||||
const csRadio = document.querySelector('input[name="colourSpace"]:checked');
|
||||
const colour_space = csRadio ? csRadio.value : "rgb";
|
||||
|
||||
const patchCountPreset = document.getElementById("patchCountPreset");
|
||||
const patchCountCustom = document.getElementById("patchCountCustom");
|
||||
let patch_count = 800;
|
||||
if (patchCountPreset) {
|
||||
if (patchCountPreset.value === "custom" && patchCountCustom) {
|
||||
patch_count = parseInt(patchCountCustom.value, 10) || 800;
|
||||
} else {
|
||||
patch_count = parseInt(patchCountPreset.value, 10) || 800;
|
||||
}
|
||||
}
|
||||
|
||||
const whiteInput = document.getElementById("whitePatches");
|
||||
const white_patches = whiteInput && whiteInput.value ? parseInt(whiteInput.value, 10) : null;
|
||||
|
||||
const blackInput = document.getElementById("blackPatches");
|
||||
const black_patches = blackInput && blackInput.value ? parseInt(blackInput.value, 10) : null;
|
||||
|
||||
const instrumentSelect = document.getElementById("instrumentSelect");
|
||||
const instrument = instrumentSelect ? instrumentSelect.value : "i1";
|
||||
|
||||
const pageSizeSelect = document.getElementById("pageSizeSelect");
|
||||
const page_size = pageSizeSelect ? pageSizeSelect.value : "A4";
|
||||
|
||||
const bitDepthRadio = document.querySelector('input[name="bitDepth"]:checked');
|
||||
const bit_depth = bitDepthRadio ? parseInt(bitDepthRadio.value, 10) : 8;
|
||||
|
||||
const colprofQuality = document.getElementById("colprofQuality");
|
||||
const colprof_quality = colprofQuality ? colprofQuality.value : "m";
|
||||
|
||||
const colprofAlgorithm = document.getElementById("colprofAlgorithm");
|
||||
const colprof_algorithm = colprofAlgorithm ? colprofAlgorithm.value : "l";
|
||||
|
||||
return {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: name || "Custom Preset",
|
||||
description: description || null,
|
||||
colour_space,
|
||||
patch_count,
|
||||
white_patches,
|
||||
black_patches,
|
||||
instrument,
|
||||
page_size,
|
||||
bit_depth,
|
||||
dpi: 300,
|
||||
colprof_algorithm,
|
||||
colprof_quality,
|
||||
colprof_intent: null,
|
||||
};
|
||||
}
|
||||
|
||||
function exportPreset(preset) {
|
||||
try {
|
||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(preset, null, 2));
|
||||
const downloadAnchor = document.createElement("a");
|
||||
downloadAnchor.setAttribute("href", dataStr);
|
||||
downloadAnchor.setAttribute("download", `${preset.name.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}.preset.json`);
|
||||
document.body.appendChild(downloadAnchor);
|
||||
downloadAnchor.click();
|
||||
downloadAnchor.remove();
|
||||
} catch (err) {
|
||||
alert("Export failed: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown change listener
|
||||
if (presetSelect) {
|
||||
presetSelect.addEventListener("change", () => {
|
||||
const selected = currentPresets.find(p => p.id === presetSelect.value);
|
||||
if (selected) applyPreset(selected);
|
||||
});
|
||||
}
|
||||
|
||||
// Save preset modal triggers
|
||||
if (btnSavePresetModal && savePresetDialog) {
|
||||
btnSavePresetModal.addEventListener("click", () => {
|
||||
if (savePresetName) savePresetName.value = "";
|
||||
if (savePresetDesc) savePresetDesc.value = "";
|
||||
savePresetDialog.showModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCloseSavePresetDialog && savePresetDialog) {
|
||||
btnCloseSavePresetDialog.addEventListener("click", () => {
|
||||
savePresetDialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnConfirmSavePreset && savePresetDialog) {
|
||||
btnConfirmSavePreset.addEventListener("click", async () => {
|
||||
const name = savePresetName ? savePresetName.value.trim() : "";
|
||||
if (!name) {
|
||||
alert("Please enter a name for your preset.");
|
||||
return;
|
||||
}
|
||||
const desc = savePresetDesc ? savePresetDesc.value.trim() : "";
|
||||
const preset = collectCurrentSettingsAsPreset(name, desc);
|
||||
|
||||
try {
|
||||
currentPresets = await invoke("save_preset", { preset });
|
||||
activePresetId = preset.id;
|
||||
renderPresetDropdown();
|
||||
renderManagePresetsList();
|
||||
savePresetDialog.close();
|
||||
} catch (err) {
|
||||
alert("Save preset failed: " + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Manage presets dialog triggers
|
||||
if (btnOpenPresetsDialog && managePresetsDialog) {
|
||||
btnOpenPresetsDialog.addEventListener("click", () => {
|
||||
renderManagePresetsList();
|
||||
managePresetsDialog.showModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCloseManagePresetsDialog && managePresetsDialog) {
|
||||
btnCloseManagePresetsDialog.addEventListener("click", () => {
|
||||
managePresetsDialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnExportActivePreset) {
|
||||
btnExportActivePreset.addEventListener("click", () => {
|
||||
const preset = currentPresets.find(p => p.id === activePresetId);
|
||||
if (preset) exportPreset(preset);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnImportPreset) {
|
||||
btnImportPreset.addEventListener("click", () => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = ".json";
|
||||
fileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (evt) => {
|
||||
try {
|
||||
const jsonText = evt.target.result;
|
||||
const imported = await invoke("import_preset_json", { json: jsonText });
|
||||
imported.id = `custom-${Date.now()}`;
|
||||
currentPresets = await invoke("save_preset", { preset: imported });
|
||||
activePresetId = imported.id;
|
||||
applyPreset(imported);
|
||||
renderPresetDropdown();
|
||||
renderManagePresetsList();
|
||||
alert(`Preset "${imported.name}" imported successfully!`);
|
||||
} catch (err) {
|
||||
alert("Import failed: " + err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
await loadPresets();
|
||||
const defaultP = currentPresets.find(p => p.id === "preset-std-rgb");
|
||||
if (defaultP) applyPreset(defaultP);
|
||||
}
|
||||
Reference in New Issue
Block a user