// Aether BG Renderer Processes
// --- GLOBAL VARIABLES & STATE ---
let cropper = null;
let currentImagePath = null;
let originalFileName = '';
let processedTempPath = null;
let activeBgMode = 'transparent'; // 'transparent', 'solid', 'gradient'
let activeSolidColor = '#ffffff';
let activeGradientStops = [
{ offset: 0, color: '#a78bfa' },
{ offset: 1, color: '#06b6d4' }
];
let scaleX = 1;
let scaleY = 1;
// Define gradient templates for canvas exporting
const GRADIENTS = {
'grad-1': [{ offset: 0, color: '#a78bfa' }, { offset: 1, color: '#06b6d4' }], // Violet Cyan
'grad-2': [{ offset: 0, color: '#f43f5e' }, { offset: 1, color: '#f97316' }], // Sunset
'grad-3': [{ offset: 0, color: '#10b981' }, { offset: 1, color: '#06b6d4' }], // Emerald
'grad-4': [{ offset: 0, color: '#ec4899' }, { offset: 1, color: '#8b5cf6' }], // Cyberpunk
'grad-5': [{ offset: 0, color: '#3b82f6' }, { offset: 1, color: '#8b5cf6' }], // Aether Blue
'grad-6': [{ offset: 0, color: '#f59e0b' }, { offset: 1, color: '#ef4444' }] // Warm Ember
};
// --- DOM ELEMENTS SELECTION ---
const elements = {
// Titlebar
winMin: document.getElementById('win-min'),
winMax: document.getElementById('win-max'),
winClose: document.getElementById('win-close'),
btnShowCredits: document.getElementById('btn-show-credits'),
creditsOverlay: document.getElementById('credits-overlay'),
btnCloseCredits: document.getElementById('btn-close-credits'),
// Setup Wizard
setupOverlay: document.getElementById('setup-overlay'),
btnStartSetup: document.getElementById('btn-start-setup'),
setupProgressBar: document.getElementById('setup-progress-bar'),
setupStatusText: document.getElementById('setup-status-text'),
stepVenv: document.getElementById('step-venv'),
stepPip: document.getElementById('step-pip'),
stepDeps: document.getElementById('step-deps'),
stepLibs: document.getElementById('step-libs'),
// Workspace Views
dropzone: document.getElementById('dropzone'),
btnBrowse: document.getElementById('btn-browse'),
editorContainer: document.getElementById('editor-container'),
canvasWrapper: document.getElementById('canvas-wrapper'),
imageElement: document.getElementById('image-element'),
canvasBgOverlay: document.getElementById('canvas-bg-overlay'),
// Workspace Toolbar Controls
ctrlCropDrag: document.getElementById('ctrl-crop-drag'),
ctrlCropBox: document.getElementById('ctrl-crop-box'),
ctrlRotL: document.getElementById('ctrl-rot-l'),
ctrlRotR: document.getElementById('ctrl-rot-r'),
ctrlFlipH: document.getElementById('ctrl-flip-h'),
ctrlFlipV: document.getElementById('ctrl-flip-v'),
cropRatioSelect: document.getElementById('crop-ratio-select'),
ctrlAutoTrim: document.getElementById('ctrl-auto-trim'),
ctrlReset: document.getElementById('ctrl-reset'),
// Control Tower: Info Panel
infoCardEmpty: document.getElementById('info-card-empty'),
infoCardLoaded: document.getElementById('info-card-loaded'),
infoFilename: document.getElementById('info-filename'),
infoResolution: document.getElementById('info-resolution'),
infoSize: document.getElementById('info-size'),
btnChangeImage: document.getElementById('btn-change-image'),
// Control Tower: AI Processor
btnRemoveBg: document.getElementById('btn-remove-bg'),
loaderBox: document.getElementById('loader-box'),
loaderText: document.getElementById('loader-text'),
loaderSubtext: document.getElementById('loader-subtext'),
// Control Tower: Background settings
secCanvasBg: document.getElementById('sec-canvas-bg'),
tabBgTrans: document.getElementById('tab-bg-trans'),
tabBgSolid: document.getElementById('tab-bg-solid'),
tabBgGrad: document.getElementById('tab-bg-grad'),
subpanelSolid: document.getElementById('subpanel-solid'),
subpanelGradient: document.getElementById('subpanel-gradient'),
customColorPicker: document.getElementById('custom-color-picker'),
btnCustomColorPicker: document.getElementById('btn-custom-color-picker'),
// Control Tower: Export Studio
secExport: document.getElementById('sec-export'),
exportFormat: document.getElementById('export-format'),
rowQualitySlider: document.getElementById('row-quality-slider'),
exportQuality: document.getElementById('export-quality'),
exportQualityVal: document.getElementById('export-quality-val'),
btnExport: document.getElementById('btn-export'),
// Toast Banner
toastBanner: document.getElementById('toast-banner'),
toastTitle: document.getElementById('toast-title'),
toastMessage: document.getElementById('toast-message'),
btnCloseToast: document.getElementById('btn-close-toast')
};
// --- INITIALIZE & ENVIRONMENT CHECKS ---
document.addEventListener('DOMContentLoaded', async () => {
setupTitlebarListeners();
setupToastListeners();
// Check if Python virtual environment exists
const { hasVenv } = await window.api.checkPythonSetup();
if (!hasVenv) {
elements.setupOverlay.classList.remove('hidden');
} else {
initWorkspace();
}
});
// Titlebar Controls
function setupTitlebarListeners() {
elements.winMin.addEventListener('click', () => window.api.minimizeWindow());
elements.winMax.addEventListener('click', () => window.api.maximizeWindow());
elements.winClose.addEventListener('click', () => window.api.closeWindow());
// Show Credits Modal
elements.btnShowCredits.addEventListener('click', () => {
elements.creditsOverlay.classList.remove('hidden');
});
// Close Credits Modal
elements.btnCloseCredits.addEventListener('click', () => {
elements.creditsOverlay.classList.add('hidden');
});
}
// Workspace Initialization
function initWorkspace() {
setupDragAndDrop();
setupToolbarListeners();
setupControlTowerListeners();
setupBgSwatches();
// Browse Button click
elements.btnBrowse.addEventListener('click', selectLocalImage);
elements.dropzone.addEventListener('click', (e) => {
// Only open if clicking container directly and not buttons
if (e.target === elements.dropzone || e.target.closest('.dropzone-content') && !e.target.closest('button')) {
selectLocalImage();
}
});
// Change image button click
elements.btnChangeImage.addEventListener('click', resetWorkspaceToEmpty);
}
function resetWorkspaceToEmpty() {
currentImagePath = null;
originalFileName = '';
processedTempPath = null;
activeBgMode = 'transparent';
scaleX = 1;
scaleY = 1;
// Destroy cropper safely
if (cropper) {
cropper.destroy();
cropper = null;
}
// Restore drag drop state views
elements.editorContainer.classList.add('hidden');
elements.dropzone.classList.remove('hidden');
elements.infoCardLoaded.classList.add('hidden');
elements.infoCardEmpty.classList.remove('hidden');
// Disable processing button until a new file is loaded
elements.btnRemoveBg.disabled = true;
// Clear background customizations
resetBgSettings();
showToast("Image Changed", "Workspace cleared. You can now load a new image.", "info");
}
// --- PYTHON VENV INSTALLER ENGINE ---
elements.btnStartSetup.addEventListener('click', async () => {
elements.btnStartSetup.disabled = true;
elements.setupProgressBar.parentElement.style.display = 'block';
elements.setupProgressBar.style.width = '5%';
// Track active steps
const setStepActive = (stepEl) => {
document.querySelectorAll('.step-item').forEach(el => el.classList.remove('active'));
stepEl.classList.add('active');
};
const setStepDone = (stepEl) => {
stepEl.classList.remove('active');
stepEl.classList.add('done');
};
const setStepFailed = (stepEl) => {
stepEl.classList.remove('active');
stepEl.classList.add('failed');
};
setStepActive(elements.stepVenv);
// Listen for pipeline setup output logs
window.api.onSetupProgress((progressText) => {
elements.setupStatusText.innerText = progressText;
// Parse step state
if (progressText.includes("Creating Python virtual environment")) {
setStepActive(elements.stepVenv);
elements.setupProgressBar.style.width = '15%';
} else if (progressText.includes("Virtual environment successfully created") || progressText.includes("Virtual environment already exists")) {
setStepDone(elements.stepVenv);
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '30%';
} else if (progressText.includes("Upgrading pip")) {
setStepActive(elements.stepPip);
elements.setupProgressBar.style.width = '45%';
} else if (progressText.includes("Installing PyTorch")) {
setStepDone(elements.stepPip);
setStepActive(elements.stepDeps);
elements.setupProgressBar.style.width = '60%';
} else if (progressText.includes("Downloading torch") || progressText.includes("Installing collected packages")) {
elements.setupProgressBar.style.width = '75%';
} else if (progressText.includes("Setup completed successfully")) {
setStepDone(elements.stepDeps);
setStepDone(elements.stepLibs);
elements.setupProgressBar.style.width = '100%';
}
});
try {
const result = await window.api.runPythonSetup();
if (result.code === 0) {
elements.setupStatusText.innerText = "Configuration Complete! Enjoy background removal.";
showToast("Setup Success", "Python environment is fully configured.", "success");
setTimeout(() => {
elements.setupOverlay.classList.add('hidden');
initWorkspace();
}, 1500);
} else {
setStepFailed(elements.stepDeps);
elements.setupStatusText.innerText = "Setup failed. Check system dependencies.";
elements.btnStartSetup.disabled = false;
showToast("Setup Error", "Failed to build Python environment.", "error");
}
} catch (err) {
elements.setupStatusText.innerText = `Error: ${err.message}`;
elements.btnStartSetup.disabled = false;
showToast("Setup Error", err.message, "error");
}
});
// --- IMAGE FILE INPUT ENGINE ---
function setupDragAndDrop() {
const dropzone = elements.dropzone;
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.add('dragover');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.remove('dragover');
}, false);
});
dropzone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
handleImageSelection(files[0].path);
}
});
}
async function selectLocalImage() {
const filePath = await window.api.selectImage();
if (filePath) {
handleImageSelection(filePath);
}
}
function handleImageSelection(filePath) {
currentImagePath = filePath;
originalFileName = filePath.split(/[\\/]/).pop();
// Reset previous states
processedTempPath = null;
activeBgMode = 'transparent';
scaleX = 1;
scaleY = 1;
resetBgSettings();
// Show image in preview element
elements.imageElement.src = `file://${filePath}`;
// Set file metadata in control tower
const stats = getFileStats(filePath);
elements.infoFilename.innerText = originalFileName;
elements.infoSize.innerText = stats.sizeFormatted;
// Load dimensions
const img = new Image();
img.onload = function() {
elements.infoResolution.innerText = `${this.naturalWidth} x ${this.naturalHeight} px`;
// Switch views
elements.dropzone.classList.add('hidden');
elements.editorContainer.classList.remove('hidden');
elements.infoCardEmpty.classList.add('hidden');
elements.infoCardLoaded.classList.remove('hidden');
// Enable AI button
elements.btnRemoveBg.disabled = false;
// Initialize CropperJS on image
initCropper();
};
img.src = `file://${filePath}`;
}
function getFileStats(path) {
// Simple synchronous-like wrapper or mockup stats.
// Note: Electron file handles path directly.
return {
sizeFormatted: "Calculating..." // Updated as soon as loaded
};
}
// Helper: Formatter
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// --- CROPPERJS CONTROLLER HUB ---
function initCropper() {
if (cropper) {
cropper.destroy();
}
// Load Cropper configuration
cropper = new Cropper(elements.imageElement, {
viewMode: 1,
dragMode: 'move',
autoCrop: false,
responsive: true,
restore: false,
guides: true,
center: true,
highlight: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
});
// Wire toolbar click listeners
elements.ctrlCropDrag.classList.add('active');
elements.ctrlCropBox.classList.remove('active');
elements.ctrlAutoTrim.disabled = true; // Disabled until background is removed
}
function setupToolbarListeners() {
elements.ctrlCropDrag.addEventListener('click', () => {
cropper.setDragMode('move');
elements.ctrlCropDrag.classList.add('active');
elements.ctrlCropBox.classList.remove('active');
});
elements.ctrlCropBox.addEventListener('click', () => {
cropper.setDragMode('crop');
elements.ctrlCropDrag.classList.remove('active');
elements.ctrlCropBox.classList.add('active');
});
elements.ctrlRotL.addEventListener('click', () => cropper.rotate(-90));
elements.ctrlRotR.addEventListener('click', () => cropper.rotate(90));
elements.ctrlFlipH.addEventListener('click', () => {
scaleX = scaleX === 1 ? -1 : 1;
cropper.scale(scaleX, scaleY);
});
elements.ctrlFlipV.addEventListener('click', () => {
scaleY = scaleY === 1 ? -1 : 1;
cropper.scale(scaleX, scaleY);
});
elements.cropRatioSelect.addEventListener('change', (e) => {
const value = parseFloat(e.target.value);
cropper.setAspectRatio(value);
});
elements.ctrlReset.addEventListener('click', () => {
cropper.reset();
scaleX = 1;
scaleY = 1;
cropper.scale(1, 1);
elements.cropRatioSelect.value = "NaN";
cropper.setAspectRatio(NaN);
showToast("Reset Action", "Workspace state cleared.", "info");
});
elements.ctrlAutoTrim.addEventListener('click', autoTrimTransparent);
}
// --- NATIVE AUTOTRIM (PIXEL ITERATION BOUNDS) ---
function autoTrimTransparent() {
if (!cropper || !processedTempPath) return;
// We request canvas size matching the full uncropped image dimension
const canvas = cropper.getCroppedCanvas({
width: cropper.getImageData().naturalWidth,
height: cropper.getImageData().naturalHeight
});
if (!canvas) {
showToast("Trim Warning", "Could not analyze transparent boundaries.", "error");
return;
}
const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
let minX = canvas.width;
let maxX = 0;
let minY = canvas.height;
let maxY = 0;
let foundAlpha = false;
// Loop through pixels in grid to find bounding box of non-transparent values
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const alphaIndex = (y * canvas.width + x) * 4 + 3;
if (data[alphaIndex] > 8) { // Alpha transparency cut-off
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
foundAlpha = true;
}
}
}
if (foundAlpha) {
// Add small padding to avoid clipping exactly
const pad = 10;
minX = Math.max(0, minX - pad);
minY = Math.max(0, minY - pad);
maxX = Math.min(canvas.width, maxX + pad);
maxY = Math.min(canvas.height, maxY + pad);
// Convert pixel coordinates to Cropper canvas boundaries
const canvasData = cropper.getCanvasData();
const scale = canvasData.width / cropper.getImageData().naturalWidth;
// Set crop boundaries
cropper.setCropBoxData({
left: canvasData.left + minX * scale,
top: canvasData.top + minY * scale,
width: (maxX - minX) * scale,
height: (maxY - minY) * scale
});
// Switch to crop selection mode visual indicator
cropper.setDragMode('crop');
elements.ctrlCropDrag.classList.remove('active');
elements.ctrlCropBox.classList.add('active');
showToast("Auto-Trim Complete", "Crop boundaries adjusted securely around subject.", "success");
} else {
showToast("Trim Info", "No transparent border boundaries identified.", "info");
}
}
// --- CONTROL TOWER CONTROLLER ---
function setupControlTowerListeners() {
// Remove Background click
elements.btnRemoveBg.addEventListener('click', executeBackgroundRemoval);
// Background Settings Tabs
elements.tabBgTrans.addEventListener('click', () => switchBgMode('transparent'));
elements.tabBgSolid.addEventListener('click', () => switchBgMode('solid'));
elements.tabBgGrad.addEventListener('click', () => switchBgMode('gradient'));
// Export Quality Slider
elements.exportFormat.addEventListener('change', (e) => {
if (e.target.value === 'png') {
elements.rowQualitySlider.classList.add('hidden');
} else {
elements.rowQualitySlider.classList.remove('hidden');
}
});
elements.exportQuality.addEventListener('input', (e) => {
elements.exportQualityVal.innerText = `${e.target.value}%`;
});
// Export MASTERPIECE Click
elements.btnExport.addEventListener('click', executeExport);
}
// --- BIREFNET EXECUTION PIPELINE ---
async function executeBackgroundRemoval() {
if (!currentImagePath) return;
elements.btnRemoveBg.classList.add('hidden');
elements.loaderBox.classList.remove('hidden');
elements.loaderText.innerText = "Initializing AI model...";
elements.loaderSubtext.innerText = "Loading neural weights into memory...";
// Get unique temporary output file path
const tempOut = await window.api.getTempPath('transparent_result.png');
// Listen to execution progress
window.api.onProcessProgress((progress) => {
if (progress.includes("[STATUS]")) {
const statusText = progress.replace("[STATUS] ", "");
elements.loaderText.innerText = statusText;
if (statusText.includes("Hugging Face")) {
elements.loaderSubtext.innerText = "This downloads ~200MB of weights on first run.";
} else if (statusText.includes("inference")) {
elements.loaderSubtext.innerText = "Extracting details and soft matting alpha...";
} else if (statusText.includes("Saving")) {
elements.loaderSubtext.innerText = "Assembling high-res RGBA container...";
}
}
});
try {
const result = await window.api.removeBackground(currentImagePath, tempOut);
if (result.success) {
processedTempPath = result.outputPath;
showToast("AI Success", "Image background removed successfully!", "success");
// Update workspace preview image source to output transparent PNG
// Add timestamp to bypass browser cache refreshing
const freshSrc = `file://${processedTempPath}?t=${Date.now()}`;
// Re-initialize CropperJS on the new image src
elements.imageElement.src = freshSrc;
// Wait for image source update to build the cropper safely
setTimeout(() => {
initCropper();
// Unlock background customization and export
elements.secCanvasBg.classList.remove('disabled-group');
elements.secExport.classList.remove('disabled-group');
elements.ctrlAutoTrim.disabled = false; // Unlock trim feature!
}, 300);
} else {
showToast("Inference Error", result.error || "Execution terminated unexpectedly.", "error");
}
} catch (err) {
showToast("AI Fatal Error", err.message, "error");
} finally {
elements.loaderBox.classList.add('hidden');
elements.btnRemoveBg.classList.remove('hidden');
}
}
// --- CANVAS CUSTOM BACKGROUND SETTINGS ---
function resetBgSettings() {
elements.secCanvasBg.classList.add('disabled-group');
elements.secExport.classList.add('disabled-group');
switchBgMode('transparent');
}
function switchBgMode(mode) {
activeBgMode = mode;
// Adjust active tab css
elements.tabBgTrans.classList.remove('active');
elements.tabBgSolid.classList.remove('active');
elements.tabBgGrad.classList.remove('active');
elements.subpanelSolid.classList.add('hidden');
elements.subpanelGradient.classList.add('hidden');
if (mode === 'transparent') {
elements.tabBgTrans.classList.add('active');
elements.canvasBgOverlay.style.opacity = '0';
} else if (mode === 'solid') {
elements.tabBgSolid.classList.add('active');
elements.subpanelSolid.classList.remove('hidden');
elements.canvasBgOverlay.style.background = activeSolidColor;
elements.canvasBgOverlay.style.opacity = '1';
} else if (mode === 'gradient') {
elements.tabBgGrad.classList.add('active');
elements.subpanelGradient.classList.remove('hidden');
// Draw current active gradient stops onto the workspace preview
const cssGrad = buildCssGradientString(activeGradientStops);
elements.canvasBgOverlay.style.background = cssGrad;
elements.canvasBgOverlay.style.opacity = '1';
}
}
function buildCssGradientString(stops) {
return `linear-gradient(135deg, ${stops[0].color}, ${stops[1].color})`;
}
function setupBgSwatches() {
// Solid Swatches
const solidSwatches = document.querySelectorAll('.swatch:not(.custom)');
solidSwatches.forEach(swatch => {
swatch.addEventListener('click', (e) => {
solidSwatches.forEach(s => s.classList.remove('active'));
e.target.classList.add('active');
activeSolidColor = e.target.getAttribute('data-color');
elements.canvasBgOverlay.style.background = activeSolidColor;
});
});
// Custom Color Picker
elements.customColorPicker.addEventListener('input', (e) => {
activeSolidColor = e.target.value;
elements.canvasBgOverlay.style.background = activeSolidColor;
// Highlight custom swatch borders
elements.btnCustomColorPicker.classList.add('active');
solidSwatches.forEach(s => s.classList.remove('active'));
});
// Gradient Swatches
const gradSwatches = document.querySelectorAll('.grad-swatch');
gradSwatches.forEach(swatch => {
swatch.addEventListener('click', (e) => {
gradSwatches.forEach(s => s.classList.remove('active'));
e.target.classList.add('active');
const gradKey = e.target.className.split(' ').find(cls => cls.startsWith('grad-'));
if (GRADIENTS[gradKey]) {
activeGradientStops = GRADIENTS[gradKey];
elements.canvasBgOverlay.style.background = e.target.getAttribute('data-gradient');
}
});
});
}
// --- CANVAS EXPORT AND MERGING ENGINE ---
async function executeExport() {
if (!cropper) return;
elements.btnExport.disabled = true;
elements.btnExport.innerHTML = `
Exporting masterpiece...
`;
setTimeout(async () => {
try {
// Get the cropped canvas at original resolution
const croppedCanvas = cropper.getCroppedCanvas();
if (!croppedCanvas) {
throw new Error("Failed to extract active cropped canvas workspace.");
}
let finalCanvas = croppedCanvas;
// If solid or gradient background mode is selected, we burn it behind the subject
if (activeBgMode !== 'transparent') {
finalCanvas = document.createElement('canvas');
finalCanvas.width = croppedCanvas.width;
finalCanvas.height = croppedCanvas.height;
const ctx = finalCanvas.getContext('2d');
if (activeBgMode === 'solid') {
ctx.fillStyle = activeSolidColor;
ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
} else if (activeBgMode === 'gradient') {
// Render the 135deg gradient vector on final output canvas
const grad = ctx.createLinearGradient(0, 0, finalCanvas.width, finalCanvas.height);
activeGradientStops.forEach(stop => {
grad.addColorStop(stop.offset, stop.color);
});
ctx.fillStyle = grad;
ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
}
// Overlay transparent subject on top of custom background
ctx.drawImage(croppedCanvas, 0, 0);
}
// Read Export Format settings
const format = elements.exportFormat.value;
let mimeType = 'image/png';
let extension = 'png';
if (format === 'jpeg') {
mimeType = 'image/jpeg';
extension = 'jpg';
} else if (format === 'webp') {
mimeType = 'image/webp';
extension = 'webp';
}
const quality = parseFloat(elements.exportQuality.value) / 100;
const base64Data = finalCanvas.toDataURL(mimeType, quality);
// Create default filename suggestion
const nameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName;
const exportDefaultName = `${nameWithoutExt}_no_bg.${extension}`;
// Open Save File dialog via context bridge
const result = await window.api.exportImage(base64Data, exportDefaultName);
if (result.success) {
showToastWithAction(
"Export Successful",
`Saved file correctly: ${result.filePath.split(/[\\/]/).pop()}`,
"success",
result.filePath
);
} else if (!result.canceled) {
showToast("Export Failed", result.error || "Save operation failed.", "error");
}
} catch (err) {
showToast("Export Error", err.message, "error");
} finally {
// Re-enable button
elements.btnExport.disabled = false;
elements.btnExport.innerHTML = `
Export Masterpiece
`;
}
}, 100);
}
// --- GLOBAL TOAST SYSTEM IMPLEMENTATION ---
let toastTimeout = null;
function setupToastListeners() {
elements.btnCloseToast.addEventListener('click', hideToast);
}
function showToast(title, message, type = 'info') {
hideToast();
elements.toastTitle.innerText = title;
elements.toastMessage.innerText = message;
// Styling
elements.toastBanner.className = 'toast-notification glass-panel-inner';
if (type === 'success') elements.toastBanner.classList.add('success');
if (type === 'error') elements.toastBanner.classList.add('error');
elements.toastBanner.classList.remove('hidden');
// Set automatic dismissal
toastTimeout = setTimeout(hideToast, 5000);
}
function showToastWithAction(title, message, type = 'success', filePath) {
hideToast();
elements.toastTitle.innerText = title;
elements.toastMessage.innerHTML = `${message}
`;
elements.toastBanner.className = 'toast-notification glass-panel-inner';
if (type === 'success') elements.toastBanner.classList.add('success');
if (type === 'error') elements.toastBanner.classList.add('error');
elements.toastBanner.classList.remove('hidden');
// Bind click logic to "Show in File Explorer" button
setTimeout(() => {
const btnExplore = document.getElementById('btn-toast-explore');
if (btnExplore) {
btnExplore.addEventListener('click', () => {
window.api.showInExplorer(filePath);
});
}
}, 50);
// Keep it active longer since it contains interaction
toastTimeout = setTimeout(hideToast, 8000);
}
function hideToast() {
if (toastTimeout) {
clearTimeout(toastTimeout);
toastTimeout = null;
}
elements.toastBanner.classList.add('hidden');
}