feat(export): add save/select/delete export presets

This commit is contained in:
2026-08-23 19:31:33 +05:30
parent 2e3af826f7
commit 02ce06d364
10 changed files with 1399 additions and 152 deletions
+22 -16
View File
@@ -352,22 +352,28 @@
</div>
<div class="modal-body">
<!-- Simple/Advanced Export Toggle -->
<!-- Export Profiles -->
<div class="export-section export-profiles">
<label>Export Profile:</label>
<div class="profile-controls">
<select id="export-profile-select">
<option value="">Custom Settings</option>
</select>
<button
id="save-profile-btn"
type="button"
title="Save current settings as profile"
>
💾 Save
</button>
<button id="delete-profile-btn" type="button" title="Delete selected profile">
🗑️ Delete
<!-- Export Presets (main-process persisted; see src/renderer/export-presets.js) -->
<div class="export-section export-presets">
<label>Export Preset:</label>
<div class="preset-controls">
<div class="preset-dropdown">
<button
type="button"
id="preset-dropdown-toggle"
aria-haspopup="listbox"
aria-expanded="false"
>
Custom Settings
</button>
<div
id="preset-dropdown-list"
class="preset-dropdown-list hidden"
role="listbox"
aria-label="Export presets"
></div>
</div>
<button id="save-preset-btn" type="button" title="Save current settings as preset">
💾 Save as preset
</button>
</div>
<small class="export-help">Save and reuse your favorite export configurations</small>
+12
View File
@@ -13,6 +13,7 @@ const GitOperations = require('./main/GitOperations');
const PdfFontHeader = require('./main/PdfFontHeader');
const MonospaceFontConfig = require('./main/MonospaceFontConfig');
const ExportCss = require('./main/ExportCss');
const ExportPresets = require('./main/ExportPresets');
const EpubFontEmbedder = require('./main/EpubFontEmbedder');
const DocxFontEmbedder = require('./main/DocxFontEmbedder');
@@ -2021,6 +2022,17 @@ ipcMain.on('save-header-footer-settings', (event, settings) => {
});
});
// Export Presets IPC Handlers (invoke-style). Presets live in settings.json
// under the `exportPresets` key — the same store used for header/footer and
// page settings; list logic is in src/main/ExportPresets.js.
ipcMain.handle('get-export-presets', async () => ExportPresets.loadPresets(store));
ipcMain.handle('save-export-preset', async (event, preset) =>
ExportPresets.savePreset(store, preset)
);
ipcMain.handle('delete-export-preset', async (event, presetId) =>
ExportPresets.deletePreset(store, presetId)
);
// Get current page settings
ipcMain.on('get-page-settings', (event) => {
event.reply('page-settings-data', pageSettings);
+92
View File
@@ -0,0 +1,92 @@
'use strict';
/**
* Export preset persistence (Task 21 — export presets/profiles).
*
* Pure list operations over the `exportPresets` array kept in the app's
* settings.json store (the `store.get`/`store.set` helpers defined in
* src/main.js — the same store that holds headerFooterSettings and
* pageSettings). The store is injected so the logic is unit-testable with a
* fake store (see tests/main/ExportPresets.test.js); src/main.js wires these
* functions to the get-export-presets / save-export-preset /
* delete-export-preset invoke channels.
*
* Preset shape: { id: string, name: string, format: string|null, options: object }
* — `options` is the export-options snapshot captured from the renderer's
* export dialog, so selecting a preset can pre-fill that dialog exactly.
*/
const PRESET_KEY = 'exportPresets';
const MAX_PRESETS = 50;
const MAX_NAME_LENGTH = 100;
/**
* Read the stored presets, defensively skipping corrupt data.
* @param {{get: Function, set: Function}} store settings store
* @returns {Array<{id: string, name: string, format: string|null, options: Object}>}
*/
function loadPresets(store) {
const stored = store.get(PRESET_KEY, []);
if (!Array.isArray(stored)) return [];
return stored.filter(
(preset) =>
preset &&
typeof preset === 'object' &&
typeof preset.id === 'string' &&
typeof preset.name === 'string'
);
}
/**
* Validate, normalize, and upsert a preset by id. A missing id gets a newly
* generated one (insert); an id that already exists is replaced (update).
* @param {{get: Function, set: Function}} store settings store
* @param {{id?: string, name?: string, format?: string, options?: Object}} preset
* @returns {Array} the updated preset list (also persisted)
* @throws when the preset is not an object, the name is empty, or the cap is hit
*/
function savePreset(store, preset) {
if (!preset || typeof preset !== 'object') {
throw new Error('Preset must be an object');
}
const name = typeof preset.name === 'string' ? preset.name.trim().slice(0, MAX_NAME_LENGTH) : '';
if (!name) {
throw new Error('Preset name is required');
}
const options = preset.options && typeof preset.options === 'object' ? preset.options : {};
const format = typeof preset.format === 'string' ? preset.format : null;
const presets = loadPresets(store);
const id = typeof preset.id === 'string' && preset.id ? preset.id : createPresetId();
const entry = { id, name, format, options };
const index = presets.findIndex((existing) => existing.id === id);
if (index >= 0) {
presets[index] = entry;
} else {
if (presets.length >= MAX_PRESETS) {
throw new Error(`Cannot store more than ${MAX_PRESETS} export presets`);
}
presets.push(entry);
}
store.set(PRESET_KEY, presets);
return presets;
}
/**
* Remove the preset with the given id (idempotent).
* @param {{get: Function, set: Function}} store settings store
* @param {string} presetId
* @returns {Array} the updated preset list (also persisted)
*/
function deletePreset(store, presetId) {
const presets = loadPresets(store).filter((preset) => preset.id !== presetId);
store.set(PRESET_KEY, presets);
return presets;
}
function createPresetId() {
return `preset-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
module.exports = { loadPresets, savePreset, deletePreset, PRESET_KEY, MAX_PRESETS };
+5
View File
@@ -74,6 +74,11 @@ const ALLOWED_SEND_CHANNELS = [
'browse-word-template',
'clear-word-template',
// Export presets (invoke channels — gated by this same array)
'get-export-presets',
'save-export-preset',
'delete-export-preset',
// Page settings
'get-page-settings',
'update-page-settings',
+11 -126
View File
@@ -13,6 +13,7 @@ const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands');
const { showMediaOperationsDialog } = require('./renderer/media-operations-dialog');
const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog');
const { initExportPresets, refreshExportPresets } = require('./renderer/export-presets');
const { csvToMarkdownTable } = require('./utils/csv-to-markdown-table');
/**
@@ -2425,6 +2426,9 @@ function showExportDialog(format) {
// Initialize form values
initializeExportForm(format);
// Populate the preset dropdown from the main-process store
refreshExportPresets();
}
function hideExportDialog() {
window.modals.exportModal.close();
@@ -2599,129 +2603,14 @@ function collectExportOptions() {
return options;
}
// Export Profiles Management
let exportProfiles = {};
function loadExportProfiles() {
const saved = localStorage.getItem('exportProfiles');
if (saved) {
try {
exportProfiles = JSON.parse(saved);
populateProfileDropdown();
} catch (e) {
console.error('Failed to load export profiles:', e);
exportProfiles = {};
}
}
}
function saveExportProfiles() {
localStorage.setItem('exportProfiles', JSON.stringify(exportProfiles));
}
function populateProfileDropdown() {
const select = document.getElementById('export-profile-select');
if (!select) return;
// Clear existing options except the first one
while (select.options.length > 1) {
select.remove(1);
}
// Add saved profiles
Object.keys(exportProfiles).forEach((name) => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});
}
function saveCurrentProfile() {
const name = prompt('Enter a name for this export profile:', 'My Profile');
if (!name || name.trim() === '') return;
const profileName = name.trim();
// Collect current settings
const profile = {
format: currentExportFormat,
advancedMode: document.getElementById('advanced-export-toggle').checked,
pageSize: document.getElementById('page-size').value,
pageOrientation: document.getElementById('page-orientation').value,
basicToc: document.getElementById('basic-toc').checked,
basicNumberSections: document.getElementById('basic-number-sections').checked,
};
// Add advanced options if enabled
if (profile.advancedMode) {
profile.template = document.getElementById('export-template').value;
profile.toc = document.getElementById('export-toc').checked;
profile.tocDepth = document.getElementById('export-toc-depth').value;
profile.numberSections = document.getElementById('export-number-sections').checked;
profile.citeproc = document.getElementById('export-citeproc').checked;
if (currentExportFormat === 'pdf') {
profile.pdfEngine = document.getElementById('pdf-engine').value;
profile.pdfGeometry = document.getElementById('pdf-geometry').value;
}
}
exportProfiles[profileName] = profile;
saveExportProfiles();
populateProfileDropdown();
// Select the newly created profile
document.getElementById('export-profile-select').value = profileName;
notifyUser(`Profile "${profileName}" saved successfully.`, 'success');
}
function loadProfile(profileName) {
if (!profileName || !exportProfiles[profileName]) return;
const profile = exportProfiles[profileName];
// Apply settings
if (profile.advancedMode !== undefined) {
document.getElementById('advanced-export-toggle').checked = profile.advancedMode;
const advancedOptions = document.getElementById('advanced-export-options');
if (profile.advancedMode) {
advancedOptions.classList.remove('hidden');
} else {
advancedOptions.classList.add('hidden');
}
}
if (profile.pageSize) document.getElementById('page-size').value = profile.pageSize;
if (profile.pageOrientation)
document.getElementById('page-orientation').value = profile.pageOrientation;
if (profile.basicToc !== undefined)
document.getElementById('basic-toc').checked = profile.basicToc;
if (profile.basicNumberSections !== undefined)
document.getElementById('basic-number-sections').checked = profile.basicNumberSections;
// Advanced options
if (profile.advancedMode && profile.template)
document.getElementById('export-template').value = profile.template;
if (profile.toc !== undefined) document.getElementById('export-toc').checked = profile.toc;
if (profile.tocDepth) document.getElementById('export-toc-depth').value = profile.tocDepth;
if (profile.numberSections !== undefined)
document.getElementById('export-number-sections').checked = profile.numberSections;
if (profile.citeproc !== undefined)
document.getElementById('export-citeproc').checked = profile.citeproc;
if (profile.pdfEngine) document.getElementById('pdf-engine').value = profile.pdfEngine;
if (profile.pdfGeometry) document.getElementById('pdf-geometry').value = profile.pdfGeometry;
}
function deleteSelectedProfile() {
const select = document.getElementById('export-profile-select');
const profileName = select.value;
if (!profileName) {
notifyUser('Select a profile to delete.', 'warning');
return;
}
if (confirm(`Are you sure you want to delete the profile "${profileName}"?`)) {
delete exportProfiles[profileName];
saveExportProfiles();
populateProfileDropdown();
select.value = '';
notifyUser(`Profile "${profileName}" deleted successfully.`, 'success');
}
}
// Export Presets — the preset dropdown / "Save as preset" logic lives in
// src/renderer/export-presets.js; presets are persisted by the main process
// in settings.json (get-export-presets / save-export-preset /
// delete-export-preset invoke channels), replacing the earlier
// localStorage-only export profiles.
// Event listeners for export dialog
document.addEventListener('DOMContentLoaded', () => {
// Load export profiles on startup
loadExportProfiles();
// Template selection
document.getElementById('export-template').addEventListener('change', (e) => {
const customPath = document.getElementById('custom-template-path');
@@ -2812,12 +2701,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Export Profile buttons
document.getElementById('save-profile-btn').addEventListener('click', saveCurrentProfile);
document.getElementById('delete-profile-btn').addEventListener('click', deleteSelectedProfile);
document.getElementById('export-profile-select').addEventListener('change', (e) => {
loadProfile(e.target.value);
});
// Export Preset controls (dropdown, per-row delete, save-as-preset)
initExportPresets({ notify: notifyUser });
// Add metadata field
document.getElementById('add-metadata-field').addEventListener('click', () => {
+430
View File
@@ -0,0 +1,430 @@
/**
* Export Presets UI
*
* Preset dropdown + "Save as preset" for the export-options dialog
* (#export-dialog in src/index.html). Replaces the earlier localStorage-only
* "export profiles": presets are now owned by the main process and persisted
* in settings.json (`exportPresets` key) through three invoke channels —
* get-export-presets / save-export-preset / delete-export-preset — so they
* behave like every other app setting instead of living and dying with the
* renderer's localStorage.
*
* Construction mirrors src/renderer/document-compare-dialog.js: a dialog
* module in src/renderer/ using the raw `ipcRenderer` invoke surface
* (nodeIntegration is enabled for this renderer). DOM is hand-rolled — a
* button + row list instead of a native <select> so each preset row can carry
* its own delete icon.
*
* The dialog markup lives in src/index.html; renderer.js calls
* initExportPresets() once at startup and refreshExportPresets() whenever the
* export dialog opens.
*/
const { ipcRenderer } = require('electron');
let currentPresets = [];
let selectedPresetId = null;
let notify = (message, type) => console.warn(`Export presets (${type}): ${message}`);
// ============================================
// DOM helpers (all element access is guarded —
// this module may outlive a dialog re-render)
// ============================================
function elementById(id) {
return document.getElementById(id);
}
function valueOf(id) {
const el = elementById(id);
return el ? el.value : '';
}
function setValue(id, value) {
const el = elementById(id);
if (el) el.value = value;
}
function isChecked(id) {
const el = elementById(id);
return !!(el && el.checked);
}
function setChecked(id, checked) {
const el = elementById(id);
if (el) el.checked = checked;
}
function setVisible(id, visible) {
const el = elementById(id);
if (el) el.style.display = visible ? 'block' : 'none';
}
function getDialogFormat() {
const dialogEl = elementById('export-dialog');
return dialogEl ? dialogEl.getAttribute('data-format') : null;
}
// ============================================
// Capture / restore of the dialog's option values
// ============================================
/**
* Snapshot every option field of the export dialog into a plain object.
* Side-effect free (unlike collectExportOptions in renderer.js, which also
* pushes page settings to the main process) — the snapshot is stored as the
* preset's `options` and replayed by applyPresetToDialog().
* @returns {Object} options snapshot
*/
function captureDialogOptions() {
const format = getDialogFormat();
const advancedMode = isChecked('advanced-export-toggle');
const options = {
advancedMode,
pageSize: valueOf('page-size'),
pageOrientation: valueOf('page-orientation'),
customWidth: valueOf('custom-width').trim() || null,
customHeight: valueOf('custom-height').trim() || null,
};
if (!advancedMode) {
options.toc = isChecked('basic-toc');
options.numberSections = isChecked('basic-number-sections');
return options;
}
const template = valueOf('export-template');
options.template = template === 'custom' ? valueOf('custom-template-path').trim() : template;
options.metadata = {};
document.querySelectorAll('.metadata-field').forEach((field) => {
const key = field.querySelector('.metadata-key').value.trim();
const value = field.querySelector('.metadata-value').value.trim();
if (key && value) options.metadata[key] = value;
});
options.toc = isChecked('export-toc');
options.tocDepth = valueOf('export-toc-depth') || '3';
options.numberSections = isChecked('export-number-sections');
options.citeproc = isChecked('export-citeproc');
if (format === 'pdf') {
options.pdfEngine = valueOf('pdf-engine');
const geometrySelect = valueOf('pdf-geometry');
options.geometry =
geometrySelect === 'custom'
? valueOf('custom-geometry').trim() || 'margin=1in'
: geometrySelect;
}
if (format === 'revealjs') {
options.revealTheme = valueOf('reveal-theme');
options.revealTransition = valueOf('reveal-transition');
options.revealTransitionSpeed = valueOf('reveal-speed');
options.revealSlideNumber = isChecked('reveal-slide-number');
options.revealControls = isChecked('reveal-controls');
options.revealProgress = isChecked('reveal-progress');
options.revealHistory = isChecked('reveal-history');
options.revealCenter = isChecked('reveal-center');
}
const bibliography = valueOf('bibliography-file').trim();
const csl = valueOf('csl-file').trim();
if (bibliography) options.bibliography = bibliography;
if (csl) options.csl = csl;
return options;
}
/**
* Restore a preset's option snapshot onto the dialog. Every field is written
* explicitly (preset value or the dialog default), so switching from a rich
* preset to a plain one clears whatever the rich one had set.
* @param {{options?: Object}} preset preset to apply
*/
function applyPresetToDialog(preset) {
const options = (preset && preset.options) || {};
const advancedMode = options.advancedMode === true;
setChecked('advanced-export-toggle', advancedMode);
const advancedSection = elementById('advanced-export-options');
if (advancedSection) advancedSection.classList.toggle('hidden', !advancedMode);
// Page setup (applies to both modes)
setValue('page-size', options.pageSize || 'a4');
setValue('page-orientation', options.pageOrientation || 'portrait');
setValue('custom-width', options.customWidth || '');
setValue('custom-height', options.customHeight || '');
setVisible('custom-page-size', valueOf('page-size') === 'custom');
setChecked('basic-toc', !advancedMode && options.toc === true);
setChecked('basic-number-sections', !advancedMode && options.numberSections === true);
// In basic mode the advanced-only fields are reset to their defaults so no
// stale values from a previously applied rich preset survive the switch.
const advancedOptions = advancedMode ? options : {};
// Template: anything other than the literal default is a custom path
const template =
advancedOptions.template && advancedOptions.template !== 'default'
? advancedOptions.template
: null;
if (template) {
setValue('export-template', 'custom');
setValue('custom-template-path', template);
setVisible('custom-template-path', true);
setVisible('template-file-input', true);
} else {
setValue('export-template', 'default');
setValue('custom-template-path', '');
setVisible('custom-template-path', false);
setVisible('template-file-input', false);
}
rebuildMetadataRows(advancedOptions.metadata || {});
setChecked('export-toc', advancedOptions.toc === true);
setValue('export-toc-depth', advancedOptions.tocDepth || '3');
setChecked('export-number-sections', advancedOptions.numberSections === true);
setChecked('export-citeproc', advancedOptions.citeproc === true);
// PDF options
setValue('pdf-engine', advancedOptions.pdfEngine || 'xelatex');
const geometry = advancedOptions.geometry || 'margin=1in';
const geometrySelect = elementById('pdf-geometry');
const hasGeometryOption =
geometrySelect && Array.from(geometrySelect.options).some((opt) => opt.value === geometry);
if (hasGeometryOption) {
setValue('pdf-geometry', geometry);
setValue('custom-geometry', '');
setVisible('custom-geometry', false);
} else {
setValue('pdf-geometry', 'custom');
setValue('custom-geometry', geometry);
setVisible('custom-geometry', true);
}
// Reveal.js options
setValue('reveal-theme', advancedOptions.revealTheme || 'black');
setValue('reveal-transition', advancedOptions.revealTransition || 'slide');
setValue('reveal-speed', advancedOptions.revealTransitionSpeed || 'default');
setChecked('reveal-slide-number', advancedOptions.revealSlideNumber === true);
setChecked('reveal-controls', advancedOptions.revealControls !== false);
setChecked('reveal-progress', advancedOptions.revealProgress !== false);
setChecked('reveal-history', advancedOptions.revealHistory !== false);
setChecked('reveal-center', advancedOptions.revealCenter !== false);
setValue('bibliography-file', advancedOptions.bibliography || '');
setValue('csl-file', advancedOptions.csl || '');
}
function rebuildMetadataRows(metadata) {
const container = document.querySelector('.metadata-container');
if (!container) return;
const entries = Object.keys(metadata).map((key) => [key, metadata[key]]);
if (entries.length === 0) {
['title', 'author', 'date', 'subject'].forEach((key) => entries.push([key, '']));
}
container.innerHTML = '';
entries.forEach(([key, value]) => {
const field = document.createElement('div');
field.className = 'metadata-field';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.className = 'metadata-key';
keyInput.value = key;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'metadata-value';
valueInput.value = value;
field.append(keyInput, valueInput);
container.appendChild(field);
});
}
// ============================================
// Preset dropdown rendering + interaction
// ============================================
function createPresetRow(preset) {
const selected = preset.id === selectedPresetId;
const row = document.createElement('div');
row.className = selected ? 'preset-row selected' : 'preset-row';
row.dataset.id = preset.id;
row.setAttribute('role', 'option');
row.setAttribute('aria-selected', selected ? 'true' : 'false');
const selectButton = document.createElement('button');
selectButton.type = 'button';
selectButton.className = 'preset-row-select';
selectButton.textContent = preset.name;
row.appendChild(selectButton);
if (preset.format) {
const badge = document.createElement('span');
badge.className = 'preset-format';
badge.textContent = preset.format;
row.appendChild(badge);
}
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'preset-delete';
deleteButton.textContent = '×';
deleteButton.title = `Delete preset ${preset.name}`;
deleteButton.setAttribute('aria-label', `Delete preset ${preset.name}`);
row.appendChild(deleteButton);
return row;
}
function renderPresets() {
const list = elementById('preset-dropdown-list');
const toggle = elementById('preset-dropdown-toggle');
if (!list || !toggle) return;
list.innerHTML = '';
if (currentPresets.length === 0) {
const empty = document.createElement('div');
empty.className = 'preset-empty';
empty.textContent = 'No saved presets';
list.appendChild(empty);
} else {
currentPresets.forEach((preset) => list.appendChild(createPresetRow(preset)));
}
const selected = currentPresets.find((preset) => preset.id === selectedPresetId);
toggle.textContent = selected ? selected.name : 'Custom Settings';
closeDropdown();
}
function toggleDropdown() {
const list = elementById('preset-dropdown-list');
if (!list) return;
const opened = !list.classList.contains('hidden');
list.classList.toggle('hidden');
const toggle = elementById('preset-dropdown-toggle');
if (toggle) toggle.setAttribute('aria-expanded', String(!opened));
}
function closeDropdown() {
const list = elementById('preset-dropdown-list');
if (list) list.classList.add('hidden');
const toggle = elementById('preset-dropdown-toggle');
if (toggle) toggle.setAttribute('aria-expanded', 'false');
}
function handleListClick(event) {
const deleteButton = event.target.closest('.preset-delete');
const row = event.target.closest('.preset-row');
if (deleteButton && row) {
deleteExportPreset(row.dataset.id);
return;
}
if (row) selectPreset(row.dataset.id);
}
function selectPreset(presetId) {
const preset = currentPresets.find((candidate) => candidate.id === presetId);
if (!preset) return;
selectedPresetId = presetId;
applyPresetToDialog(preset);
renderPresets();
}
// ============================================
// IPC-backed preset operations
// ============================================
/**
* Fetch presets from the main process and re-render the dropdown.
* Called whenever the export dialog opens.
*/
async function refreshExportPresets() {
try {
const presets = await ipcRenderer.invoke('get-export-presets');
currentPresets = Array.isArray(presets) ? presets : [];
} catch (error) {
console.error('Failed to load export presets:', error);
currentPresets = [];
}
if (!currentPresets.some((preset) => preset.id === selectedPresetId)) {
selectedPresetId = null;
}
renderPresets();
}
async function saveCurrentAsPreset() {
const selected = currentPresets.find((preset) => preset.id === selectedPresetId);
const defaultName = selected ? selected.name : 'My Preset';
const answer = window.prompt('Enter a name for this export preset:', defaultName);
if (answer === null) return; // user cancelled the prompt
const name = answer.trim();
if (!name) {
notify('Preset name cannot be empty.', 'warning');
return;
}
// A selected preset is overwritten (same id); otherwise a new id is minted.
// The main process re-validates and remains the source of truth.
const preset = {
id: selected ? selected.id : createPresetId(),
name,
format: getDialogFormat(),
options: captureDialogOptions(),
};
try {
const presets = await ipcRenderer.invoke('save-export-preset', preset);
currentPresets = Array.isArray(presets) ? presets : currentPresets;
// Normally the saved preset keeps the id we sent; fall back to the last
// entry with the same name should the main process have normalized it.
selectedPresetId = preset.id;
if (!currentPresets.some((candidate) => candidate.id === selectedPresetId)) {
const byName = currentPresets.filter((candidate) => candidate.name === name).pop();
selectedPresetId = byName ? byName.id : null;
}
renderPresets();
notify(`Preset "${name}" saved.`, 'success');
} catch (error) {
console.error('Failed to save export preset:', error);
notify('Failed to save preset. Please try again.', 'warning');
}
}
async function deleteExportPreset(presetId) {
const preset = currentPresets.find((candidate) => candidate.id === presetId);
const label = preset ? preset.name : 'this preset';
if (!window.confirm(`Are you sure you want to delete the preset "${label}"?`)) return;
try {
const presets = await ipcRenderer.invoke('delete-export-preset', presetId);
currentPresets = Array.isArray(presets) ? presets : currentPresets;
if (selectedPresetId === presetId) selectedPresetId = null;
renderPresets();
notify(`Preset "${label}" deleted.`, 'success');
} catch (error) {
console.error('Failed to delete export preset:', error);
notify('Failed to delete preset. Please try again.', 'warning');
}
}
function createPresetId() {
return `preset-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
/**
* Wire the preset section of the export dialog. Call once after DOM ready.
* @param {{notify?: Function}} options hooks from renderer.js
*/
function initExportPresets(options = {}) {
if (typeof options.notify === 'function') notify = options.notify;
selectedPresetId = null;
const saveButton = elementById('save-preset-btn');
if (saveButton) saveButton.addEventListener('click', saveCurrentAsPreset);
const toggle = elementById('preset-dropdown-toggle');
if (toggle) toggle.addEventListener('click', toggleDropdown);
const list = elementById('preset-dropdown-list');
if (list) list.addEventListener('click', handleListClick);
}
module.exports = {
initExportPresets,
refreshExportPresets,
captureDialogOptions,
applyPresetToDialog,
};
+98 -10
View File
@@ -1491,29 +1491,26 @@ body.theme-github .line-numbers {
color: #586069;
}
/* Export Profiles Styles */
.export-profiles {
/* Export Presets Styles */
.export-presets {
border-bottom: 1px solid #ddd;
padding-bottom: 15px;
margin-bottom: 15px;
}
.profile-controls {
.preset-controls {
display: flex;
gap: 8px;
align-items: center;
margin-top: 8px;
}
.profile-controls select {
.preset-dropdown {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
position: relative;
}
.profile-controls button {
.preset-dropdown button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
@@ -1523,7 +1520,98 @@ body.theme-github .line-numbers {
transition: all 0.2s;
}
.profile-controls button:hover {
.preset-dropdown button:hover {
background: #e8e8e8;
}
#preset-dropdown-toggle {
width: 100%;
text-align: left;
}
.preset-dropdown-list {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 220px;
overflow-y: auto;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
z-index: 10;
}
.preset-dropdown-list.hidden {
display: none;
}
.preset-row {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
}
.preset-row.selected {
background: #eef4fb;
}
.preset-row:hover {
background: #f0f0f0;
}
.preset-row-select {
flex: 1;
border: none;
background: none;
text-align: left;
padding: 6px 4px;
cursor: pointer;
font-size: 13px;
}
.preset-format {
font-size: 11px;
text-transform: uppercase;
color: #586069;
border: 1px solid #ddd;
border-radius: 3px;
padding: 1px 4px;
}
.preset-delete {
border: none;
background: none;
color: #586069;
cursor: pointer;
font-size: 15px;
line-height: 1;
padding: 4px 6px;
}
.preset-delete:hover {
color: #cb2431;
}
.preset-empty {
padding: 8px;
font-size: 13px;
color: #586069;
}
#save-preset-btn {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
#save-preset-btn:hover {
background: #e8e8e8;
}