1150 lines
44 KiB
JavaScript
1150 lines
44 KiB
JavaScript
// 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'),
|
||
langSelect: document.getElementById('lang-select'),
|
||
|
||
// 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();
|
||
initLanguage(); // Initialize translations immediately on load
|
||
|
||
// 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;
|
||
|
||
// Create a temporary hidden canvas to read pixels directly from the original image element
|
||
// This bypasses any cropper canvas restrictions or CORS issues when webSecurity is false
|
||
const canvas = document.createElement('canvas');
|
||
const img = elements.imageElement;
|
||
|
||
canvas.width = img.naturalWidth;
|
||
canvas.height = img.naturalHeight;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
try {
|
||
ctx.drawImage(img, 0, 0);
|
||
} catch (err) {
|
||
const title = getTrans('toastTrimFailed');
|
||
const msg = getTrans('toastTrimFailedMsg') + ": " + err.message;
|
||
showToast(title, msg, "error");
|
||
return;
|
||
}
|
||
|
||
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;
|
||
|
||
// Scan alpha channel (index + 3) to locate non-empty boundaries
|
||
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) { // transparency threshold
|
||
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 edge pixels 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);
|
||
|
||
// Turn on Crop selection mode visual indicators
|
||
cropper.setDragMode('crop');
|
||
elements.ctrlCropDrag.classList.remove('active');
|
||
elements.ctrlCropBox.classList.add('active');
|
||
|
||
// Convert pixel coordinates to Cropper canvas boundaries
|
||
const canvasData = cropper.getCanvasData();
|
||
const scale = canvasData.width / img.naturalWidth;
|
||
|
||
// Set crop boundaries
|
||
cropper.setCropBoxData({
|
||
left: canvasData.left + minX * scale,
|
||
top: canvasData.top + minY * scale,
|
||
width: (maxX - minX) * scale,
|
||
height: (maxY - minY) * scale
|
||
});
|
||
|
||
showToast(getTrans('toastTrimComplete'), getTrans('toastTrimCompleteMsg'), "success");
|
||
} else {
|
||
showToast(getTrans('toastTitleInfo'), getTrans('toastTrimFailedMsg'), "info");
|
||
}
|
||
}
|
||
|
||
// --- LOCALIZATION ENGINE (i18n) ---
|
||
const TRANSLATIONS = {
|
||
en: {
|
||
title: "Dekupai - Trunçgil AI Decoupage",
|
||
sourceFile: "Source File",
|
||
noImage: "No image loaded",
|
||
fileName: "Name",
|
||
fileResolution: "Resolution",
|
||
fileSize: "Size",
|
||
newImage: "Import New Image",
|
||
bgRemoval: "Background Removal",
|
||
extractSubject: "Extract Subject",
|
||
initAI: "Initializing AI Model...",
|
||
loadingWeights: "First launch downloads model (~200MB)",
|
||
bgSettings: "Background Settings",
|
||
transparent: "Transparent",
|
||
solidColor: "Solid Color",
|
||
gradient: "Gradient",
|
||
selectSolid: "Select Solid Fill",
|
||
selectGrad: "Select High-End Gradient",
|
||
exportStudio: "Export Studio",
|
||
format: "Format",
|
||
quality: "Quality",
|
||
exportMasterpiece: "Export Masterpiece",
|
||
toastTitleInfo: "Info",
|
||
toastTitleSuccess: "Success",
|
||
toastTitleError: "Error",
|
||
toastImageChanged: "Image Changed",
|
||
toastImageChangedMsg: "Workspace cleared. Ready for a new image.",
|
||
toastTrimComplete: "Auto-Trim Complete",
|
||
toastTrimCompleteMsg: "Crop boundaries adjusted securely.",
|
||
toastTrimFailed: "Trim Warning",
|
||
toastTrimFailedMsg: "Could not analyze transparent boundaries.",
|
||
toastSetupSuccess: "Setup Success",
|
||
toastSetupSuccessMsg: "Python environment is fully configured.",
|
||
toastSetupError: "Setup Error",
|
||
toastSetupErrorMsg: "Failed to configure Python environment.",
|
||
initWizardTitle: "Initializing AI Engine",
|
||
initWizardSubtitle: "Dekupai requires a local Python environment to run the BiRefNet neural network models.",
|
||
configureEngine: "Configure Engine (Automatic)",
|
||
stepVenv: "Creating isolated virtual environment (.venv)",
|
||
stepPip: "Upgrading Python package installer (pip)",
|
||
stepDeps: "Installing PyTorch AI Engine (CPU version)",
|
||
stepLibs: "Installing HuggingFace Transformers & PIL library",
|
||
creditsVision: "Developed under the vision of",
|
||
creditsExpertise: "by the engineering expertise of",
|
||
creditsDesc: "Dekupai is an advanced deep-learning desktop workspace that provides local, high-precision background removal using BiRefNet neural segmentation networks.",
|
||
closeCredits: "Close Credits",
|
||
dragDropText: "Drag & Drop Image Here",
|
||
dragDropSub: "Supports PNG, JPG, JPEG, or WebP formats",
|
||
orText: "OR",
|
||
browseFiles: "Browse Local Files",
|
||
autoTrim: "Auto-Trim"
|
||
},
|
||
tr: {
|
||
title: "Dekupai - Trunçgil AI Dekupaj",
|
||
sourceFile: "Kaynak Dosya",
|
||
noImage: "Görsel yüklenmedi",
|
||
fileName: "Dosya Adı",
|
||
fileResolution: "Çözünürlük",
|
||
fileSize: "Boyut",
|
||
newImage: "Yeni Görsel Yükle",
|
||
bgRemoval: "Arka Plan Temizleme",
|
||
extractSubject: "Özneyi Dekupe Et",
|
||
initAI: "AI Modeli Başlatılıyor...",
|
||
loadingWeights: "İlk çalıştırmada model indirilir (~200MB)",
|
||
bgSettings: "Arka Plan Ayarları",
|
||
transparent: "Şeffaf",
|
||
solidColor: "Tek Renk",
|
||
gradient: "Degrade",
|
||
selectSolid: "Düz Renk Seçin",
|
||
selectGrad: "Premium Degrade Seçin",
|
||
exportStudio: "Dışa Aktarma Stüdyosu",
|
||
format: "Format",
|
||
quality: "Kalite",
|
||
exportMasterpiece: "Çalışmayı Dışa Aktar",
|
||
toastTitleInfo: "Bilgi",
|
||
toastTitleSuccess: "Başarılı",
|
||
toastTitleError: "Hata",
|
||
toastImageChanged: "Görsel Değiştirildi",
|
||
toastImageChangedMsg: "Çalışma alanı temizlendi. Yeni görsel için hazır.",
|
||
toastTrimComplete: "Otomatik Kırpma Tamamlandı",
|
||
toastTrimCompleteMsg: "Kırpma sınırları güvenli bir şekilde ayarlandı.",
|
||
toastTrimFailed: "Kırpma Uyarısı",
|
||
toastTrimFailedMsg: "Şeffaf sınırlar analiz edilemedi.",
|
||
toastSetupSuccess: "Kurulum Başarılı",
|
||
toastSetupSuccessMsg: "Python ortamı tamamen yapılandırıldı.",
|
||
toastSetupError: "Kurulum Hatası",
|
||
toastSetupErrorMsg: "Python ortamı yapılandırılamadı.",
|
||
initWizardTitle: "AI Motoru Başlatılıyor",
|
||
initWizardSubtitle: "Dekupai, BiRefNet yapay zeka modelini çalıştırmak için yerel bir Python ortamına ihtiyaç duyar.",
|
||
configureEngine: "Motoru Yapılandır (Otomatik)",
|
||
stepVenv: "İzole sanal ortam oluşturuluyor (.venv)",
|
||
stepPip: "Python paket yöneticisi güncelleniyor (pip)",
|
||
stepDeps: "PyTorch AI motoru kuruluyor (CPU versiyonu)",
|
||
stepLibs: "HuggingFace Transformers ve PIL kuruluyor",
|
||
creditsVision: "Vizyon ve liderlik",
|
||
creditsExpertise: "yazılım geliştirme ve mühendislik",
|
||
creditsDesc: "Dekupai, BiRefNet yapay sinir ağı segmentasyon modellerini kullanarak yerel ve yüksek hassasiyetli arka plan temizleme sağlayan gelişmiş bir derin öğrenme masaüstü uygulamasıdır.",
|
||
closeCredits: "Kapat",
|
||
dragDropText: "Görseli Buraya Sürükleyip Bırakın",
|
||
dragDropSub: "PNG, JPG, JPEG veya WebP formatlarını destekler",
|
||
orText: "VEYA",
|
||
browseFiles: "Yerel Dosyalara Göz Atın",
|
||
autoTrim: "Oto-Kırp"
|
||
},
|
||
de: {
|
||
title: "Dekupai - Trunçgil AI Dekupage",
|
||
sourceFile: "Quelldatei",
|
||
noImage: "Kein Bild geladen",
|
||
fileName: "Dateiname",
|
||
fileResolution: "Auflösung",
|
||
fileSize: "Größe",
|
||
newImage: "Neues Bild Laden",
|
||
bgRemoval: "Hintergrundentfernung",
|
||
extractSubject: "Motiv Freistellen",
|
||
initAI: "AI-Modell wird initialisiert...",
|
||
loadingWeights: "Erster Start lädt Modell herunter (~200MB)",
|
||
bgSettings: "Hintergrundeinstellungen",
|
||
transparent: "Transparent",
|
||
solidColor: "Vollfarbe",
|
||
gradient: "Verlauf",
|
||
selectSolid: "Volltonfarbe wählen",
|
||
selectGrad: "Premium-Verlauf wählen",
|
||
exportStudio: "Export Studio",
|
||
format: "Format",
|
||
quality: "Qualität",
|
||
exportMasterpiece: "Meisterwerk Exportieren",
|
||
toastTitleInfo: "Info",
|
||
toastTitleSuccess: "Erfolg",
|
||
toastTitleError: "Fehler",
|
||
toastImageChanged: "Bild Geändert",
|
||
toastImageChangedMsg: "Arbeitsbereich geleert. Bereit für neues Bild.",
|
||
toastTrimComplete: "Auto-Zuschneiden Fertig",
|
||
toastTrimCompleteMsg: "Zuschnittsgrenzen sicher angepasst.",
|
||
toastTrimFailed: "Zuschnitt Warnung",
|
||
toastTrimFailedMsg: "Transparente Grenzen konnten nicht analysiert werden.",
|
||
toastSetupSuccess: "Setup erfolgreich",
|
||
toastSetupSuccessMsg: "Python-Umgebung ist vollständig konfiguriert.",
|
||
toastSetupError: "Setup-Fehler",
|
||
toastSetupErrorMsg: "Konfiguration der Python-Umgebung fehlgeschlagen.",
|
||
initWizardTitle: "AI-Engine wird initialisiert",
|
||
initWizardSubtitle: "Dekupai erfordert eine lokale Python-Umgebung, um die BiRefNet-Modelle auszuführen.",
|
||
configureEngine: "Engine konfigurieren (Automatisch)",
|
||
stepVenv: "Erstellen einer isolierten virtuellen Umgebung (.venv)",
|
||
stepPip: "Upgrade des Python-Paketinstallers (pip)",
|
||
stepDeps: "Installation der PyTorch AI-Engine (CPU-Version)",
|
||
stepLibs: "Installation der HuggingFace Transformers & PIL Bibliothek",
|
||
creditsVision: "Entwickelt unter der Vision von",
|
||
creditsExpertise: "durch die Software-Expertise von",
|
||
creditsDesc: "Dekupai ist ein fortschrittlicher Deep-Learning-Desktop-Arbeitsbereich, der eine lokale, hochpräzise Hintergrundentfernung mithilfe von BiRefNet-Segmentierungsnetzwerken bietet.",
|
||
closeCredits: "Schließen",
|
||
dragDropText: "Bild hierher ziehen und ablegen",
|
||
dragDropSub: "Unterstützt PNG-, JPG-, JPEG- oder WebP-Formate",
|
||
orText: "ODER",
|
||
browseFiles: "Lokale Dateien durchsuchen",
|
||
autoTrim: "Auto-Trim"
|
||
},
|
||
ru: {
|
||
title: "Dekupai - Trunçgil AI Декупаж",
|
||
sourceFile: "Исходный файл",
|
||
noImage: "Изображение не загружено",
|
||
fileName: "Имя",
|
||
fileResolution: "Разрешение",
|
||
fileSize: "Размер",
|
||
newImage: "Загрузить новое изображение",
|
||
bgRemoval: "Удаление фона",
|
||
extractSubject: "Вырезать объект",
|
||
initAI: "Инициализация модели ИИ...",
|
||
loadingWeights: "Первый запуск загружает модель (~200 МБ)",
|
||
bgSettings: "Настройки фона",
|
||
transparent: "Прозрачный",
|
||
solidColor: "Сплошной цвет",
|
||
gradient: "Градиент",
|
||
selectSolid: "Выберите сплошной цвет",
|
||
selectGrad: "Выберите премиум градиент",
|
||
exportStudio: "Студия экспорта",
|
||
format: "Формат",
|
||
quality: "Качество",
|
||
exportMasterpiece: "Экспортировать шедевр",
|
||
toastTitleInfo: "Информация",
|
||
toastTitleSuccess: "Успех",
|
||
toastTitleError: "Ошибка",
|
||
toastImageChanged: "Изображение изменено",
|
||
toastImageChangedMsg: "Рабочая область очищена. Готово к новому изображению.",
|
||
toastTrimComplete: "Автообрезка завершена",
|
||
toastTrimCompleteMsg: "Границы обрезки успешно скорректированы.",
|
||
toastTrimFailed: "Предупреждение об обрезке",
|
||
toastTrimFailedMsg: "Не удалось проанализировать прозрачные границы.",
|
||
toastSetupSuccess: "Установка успешна",
|
||
toastSetupSuccessMsg: "Среда Python полностью настроена.",
|
||
toastSetupError: "Ошибка установки",
|
||
toastSetupErrorMsg: "Не удалось настроить среду Python.",
|
||
initWizardTitle: "Инициализация движка ИИ",
|
||
initWizardSubtitle: "Для работы Dekupai требуется локальная среда Python для запуска нейросетевых моделей BiRefNet.",
|
||
configureEngine: "Настроить движок (Автоматически)",
|
||
stepVenv: "Создание изолированной виртуальной среды (.venv)",
|
||
stepPip: "Обновление установщика пакетов Python (pip)",
|
||
stepDeps: "Установка движка ИИ PyTorch (версия для CPU)",
|
||
stepLibs: "Установка библиотек HuggingFace Transformers и PIL",
|
||
creditsVision: "Разработано под руководством",
|
||
creditsExpertise: "программная инженерия и разработка",
|
||
creditsDesc: "Dekupai — это продвинутое настольное приложение для глубокого обучения, обеспечивающее локальное высокоточное удаление фона с использованием моделей сегментации BiRefNet.",
|
||
closeCredits: "Закрыть",
|
||
dragDropText: "Перетащите изображение сюда",
|
||
dragDropSub: "Поддерживает форматы PNG, JPG, JPEG или WebP",
|
||
orText: "ИЛИ",
|
||
browseFiles: "Обзор локальных файлов",
|
||
autoTrim: "Автообрезка"
|
||
},
|
||
ar: {
|
||
title: "Dekupai - Trunçgil AI إزالة الخلفية",
|
||
sourceFile: "الملف المصدر",
|
||
noImage: "لم يتم تحميل أي صورة",
|
||
fileName: "الاسم",
|
||
fileResolution: "الدقة",
|
||
fileSize: "الحجم",
|
||
newImage: "تحميل صورة جديدة",
|
||
bgRemoval: "إزالة الخلفية",
|
||
extractSubject: "استخراج العنصر",
|
||
initAI: "تهيئة نموذج الذكاء الاصطناعي...",
|
||
loadingWeights: "التشغيل الأول يقوم بتحميل النموذج (~200 ميجابايت)",
|
||
bgSettings: "إعدادات الخلفية",
|
||
transparent: "شفاف",
|
||
solidColor: "لون موحد",
|
||
gradient: "تدرج لوني",
|
||
selectSolid: "اختر لوناً موحداً",
|
||
selectGrad: "اختر تدرجاً لونيًا فاخرًا",
|
||
exportStudio: "استوديو التصدير",
|
||
format: "الصيغة",
|
||
quality: "الجودة",
|
||
exportMasterpiece: "تصدير العمل الفني",
|
||
toastTitleInfo: "معلومات",
|
||
toastTitleSuccess: "نجاح",
|
||
toastTitleError: "خطأ",
|
||
toastImageChanged: "تم تغيير الصورة",
|
||
toastImageChangedMsg: "تم مسح مساحة العمل. جاهز لصورة جديدة.",
|
||
toastTrimComplete: "اكتمل القص التلقائي",
|
||
toastTrimCompleteMsg: "تم ضبط حدود القص بأمان حول العنصر.",
|
||
toastTrimFailed: "تحذير القص",
|
||
toastTrimFailedMsg: "تعذر تحليل الحدود الشفافة.",
|
||
toastSetupSuccess: "نجاح التهيئة",
|
||
toastSetupSuccessMsg: "تم تكوين بيئة بايثون بنجاح.",
|
||
toastSetupError: "خطأ في التهيئة",
|
||
toastSetupErrorMsg: "فشل تكوين بيئة بايثون.",
|
||
initWizardTitle: "تهيئة محرك الذكاء الاصطناعي",
|
||
initWizardSubtitle: "يتطلب Dekupai بيئة بايثون محلية لتشغيل نماذج الشبكة العصبية BiRefNet.",
|
||
configureEngine: "تكوين المحرك (تلقائي)",
|
||
stepVenv: "إنشاء بيئة افتراضية معزولة (.venv)",
|
||
stepPip: "ترقية مثبت حزم بايثون (pip)",
|
||
stepDeps: "تثبيت محرك الذكاء الاصطناعي PyTorch (نسخة المعالج CPU)",
|
||
stepLibs: "تثبيت مكتبة HuggingFace Transformers و PIL",
|
||
creditsVision: "تم التطوير تحت رؤية",
|
||
creditsExpertise: "بواسطة الخبرة البرمجية والهندسية لـ",
|
||
creditsDesc: "Dekupai هو مساحة عمل متقدمة للتعلم العميق توفر إزالة محلية ودقيقة للغاية للخلفيات باستخدام نماذج BiRefNet.",
|
||
closeCredits: "إغلاق",
|
||
dragDropText: "اسحب وأسقط الصورة هنا",
|
||
dragDropSub: "يدعم صيغ PNG أو JPG أو JPEG أو WebP",
|
||
orText: "أو",
|
||
browseFiles: "تصفح الملفات المحلية",
|
||
autoTrim: "قص تلقائي"
|
||
}
|
||
};
|
||
|
||
let currentLang = 'en'; // default English base language
|
||
|
||
function changeLanguage(lang) {
|
||
currentLang = lang;
|
||
localStorage.setItem('dekupai_lang', lang);
|
||
|
||
if (elements.langSelect) {
|
||
elements.langSelect.value = lang;
|
||
}
|
||
|
||
// Update HTML elements marked with data-i18n
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
if (TRANSLATIONS[lang] && TRANSLATIONS[lang][key]) {
|
||
// If element contains an inner span for icon text
|
||
const span = el.querySelector('span');
|
||
if (span) {
|
||
span.innerText = TRANSLATIONS[lang][key];
|
||
} else if (el.tagName === 'OPTION') {
|
||
el.innerText = TRANSLATIONS[lang][key];
|
||
} else {
|
||
// Preserving SVG structure inside tags
|
||
const svg = el.querySelector('svg');
|
||
if (svg) {
|
||
// If it has SVG, clear other text and append text node safely
|
||
const textNodes = Array.from(el.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
|
||
if (textNodes.length > 0) {
|
||
textNodes[textNodes.length - 1].nodeValue = " " + TRANSLATIONS[lang][key];
|
||
} else {
|
||
el.appendChild(document.createTextNode(" " + TRANSLATIONS[lang][key]));
|
||
}
|
||
} else {
|
||
el.innerText = TRANSLATIONS[lang][key];
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Handle RTL for Arabic
|
||
if (lang === 'ar') {
|
||
document.body.dir = 'rtl';
|
||
} else {
|
||
document.body.dir = 'ltr';
|
||
}
|
||
|
||
// Update original title
|
||
document.title = TRANSLATIONS[lang].title;
|
||
}
|
||
|
||
function getTrans(key) {
|
||
return (TRANSLATIONS[currentLang] && TRANSLATIONS[currentLang][key]) || TRANSLATIONS['en'][key] || key;
|
||
}
|
||
|
||
function initLanguage() {
|
||
const savedLang = localStorage.getItem('dekupai_lang') || 'en';
|
||
|
||
if (elements.langSelect) {
|
||
elements.langSelect.value = savedLang;
|
||
elements.langSelect.addEventListener('change', (e) => {
|
||
changeLanguage(e.target.value);
|
||
});
|
||
}
|
||
|
||
changeLanguage(savedLang);
|
||
}
|
||
|
||
// --- 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 = `
|
||
<svg class="loader-ring" viewBox="0 0 50 50" style="width:16px;height:16px;margin:0;"><circle cx="25" cy="25" r="20" fill="none" stroke-width="4" stroke="#fff"></circle></svg>
|
||
<span>Exporting masterpiece...</span>
|
||
`;
|
||
|
||
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 = `
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||
<span>Export Masterpiece</span>
|
||
`;
|
||
}
|
||
}, 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} <br><button id="btn-toast-explore" class="btn btn-secondary btn-sm" style="margin-top:6px;width:100%;">Show in File Explorer</button>`;
|
||
|
||
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');
|
||
}
|