mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-28 17:21:47 -03:00
- Add password set/change/remove with loading states and double-click prevention - Add protected identity deletion flow (server-side password removal first) - Add restore flow for password-protected UUIDs (verify password before saving) - Add UUID duplicate checks in setUuidForUser (prevent accidental overwrites) - Add name-locked error handling in launch flow (server enforces registered name) - Sync shield icon across all identity mutation paths - Refresh identity dropdown after all password/identity operations - Propagate force flag through IPC for legitimate overwrites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2257 lines
82 KiB
JavaScript
2257 lines
82 KiB
JavaScript
|
|
let customJavaCheck;
|
|
let customJavaOptions;
|
|
let customJavaPath;
|
|
let browseJavaBtn;
|
|
let settingsPlayerName;
|
|
let discordRPCCheck;
|
|
let closeLauncherCheck;
|
|
let allowMultiInstanceCheck;
|
|
let launcherHwAccelCheck;
|
|
let gpuPreferenceRadios;
|
|
let gameBranchRadios;
|
|
|
|
|
|
// UUID Management elements
|
|
let currentUuidDisplay;
|
|
let copyUuidBtn;
|
|
let regenerateUuidBtn;
|
|
let manageUuidsBtn;
|
|
let uuidModal;
|
|
let uuidModalClose;
|
|
let addIdentityBtn;
|
|
let uuidAddForm;
|
|
let addIdentityUsername;
|
|
let addIdentityUuid;
|
|
let addIdentityRegenerateBtn;
|
|
let addIdentityConfirmBtn;
|
|
let addIdentityCancelBtn;
|
|
let uuidAdvancedToggle;
|
|
let uuidAdvancedContent;
|
|
let uuidList;
|
|
let customUuidInput;
|
|
let setCustomUuidBtn;
|
|
|
|
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
|
|
// Apply defaults with i18n support
|
|
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
|
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
|
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
|
|
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'custom-confirm-modal';
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 20000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
`;
|
|
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'custom-confirm-dialog';
|
|
dialog.style.cssText = `
|
|
background: #1f2937;
|
|
border-radius: 12px;
|
|
padding: 0;
|
|
min-width: 400px;
|
|
max-width: 500px;
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
|
border: 1px solid rgba(147, 51, 234, 0.3);
|
|
transform: scale(0.9);
|
|
transition: transform 0.3s ease;
|
|
`;
|
|
|
|
dialog.innerHTML = `
|
|
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
|
|
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
|
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
|
</div>
|
|
</div>
|
|
<div style="padding: 24px; color: #e5e7eb;">
|
|
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
|
|
</div>
|
|
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
|
<button class="custom-confirm-cancel" style="
|
|
background: transparent;
|
|
color: #9ca3af;
|
|
border: 1px solid rgba(156, 163, 175, 0.3);
|
|
padding: 10px 20px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
">${cancelText}</button>
|
|
<button class="custom-confirm-action" style="
|
|
background: linear-gradient(135deg, #9333ea, #3b82f6);
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
">${confirmText}</button>
|
|
</div>
|
|
`;
|
|
|
|
modal.appendChild(dialog);
|
|
document.body.appendChild(modal);
|
|
|
|
// Animate in
|
|
setTimeout(() => {
|
|
modal.style.opacity = '1';
|
|
dialog.style.transform = 'scale(1)';
|
|
}, 10);
|
|
|
|
// Event handlers
|
|
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
|
|
const actionBtn = dialog.querySelector('.custom-confirm-action');
|
|
|
|
const closeModal = () => {
|
|
modal.style.opacity = '0';
|
|
dialog.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
modal.remove();
|
|
}, 300);
|
|
};
|
|
|
|
cancelBtn.onclick = () => {
|
|
closeModal();
|
|
if (onCancel) onCancel();
|
|
};
|
|
|
|
actionBtn.onclick = () => {
|
|
closeModal();
|
|
onConfirm();
|
|
};
|
|
|
|
modal.onclick = (e) => {
|
|
if (e.target === modal) {
|
|
closeModal();
|
|
if (onCancel) onCancel();
|
|
}
|
|
};
|
|
|
|
// Escape key
|
|
const handleEscape = (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeModal();
|
|
if (onCancel) onCancel();
|
|
document.removeEventListener('keydown', handleEscape);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEscape);
|
|
}
|
|
|
|
|
|
export async function initSettings() {
|
|
setupSettingsElements();
|
|
await loadAllSettings();
|
|
}
|
|
|
|
function setupSettingsElements() {
|
|
customJavaCheck = document.getElementById('customJavaCheck');
|
|
customJavaOptions = document.getElementById('customJavaOptions');
|
|
customJavaPath = document.getElementById('customJavaPath');
|
|
browseJavaBtn = document.getElementById('browseJavaBtn');
|
|
settingsPlayerName = document.getElementById('settingsPlayerName');
|
|
discordRPCCheck = document.getElementById('discordRPCCheck');
|
|
closeLauncherCheck = document.getElementById('closeLauncherCheck');
|
|
allowMultiInstanceCheck = document.getElementById('allowMultiInstanceCheck');
|
|
launcherHwAccelCheck = document.getElementById('launcherHwAccelCheck');
|
|
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
|
gameBranchRadios = document.querySelectorAll('input[name="gameBranch"]');
|
|
|
|
console.log('[Settings] gameBranchRadios found:', gameBranchRadios.length);
|
|
|
|
|
|
// UUID Management elements
|
|
currentUuidDisplay = document.getElementById('currentUuid');
|
|
copyUuidBtn = document.getElementById('copyUuidBtn');
|
|
regenerateUuidBtn = document.getElementById('regenerateUuidBtn');
|
|
manageUuidsBtn = document.getElementById('manageUuidsBtn');
|
|
uuidModal = document.getElementById('uuidModal');
|
|
uuidModalClose = document.getElementById('uuidModalClose');
|
|
addIdentityBtn = document.getElementById('addIdentityBtn');
|
|
uuidAddForm = document.getElementById('uuidAddForm');
|
|
addIdentityUsername = document.getElementById('addIdentityUsername');
|
|
addIdentityUuid = document.getElementById('addIdentityUuid');
|
|
addIdentityRegenerateBtn = document.getElementById('addIdentityRegenerateBtn');
|
|
addIdentityConfirmBtn = document.getElementById('addIdentityConfirmBtn');
|
|
addIdentityCancelBtn = document.getElementById('addIdentityCancelBtn');
|
|
uuidAdvancedToggle = document.getElementById('uuidAdvancedToggle');
|
|
uuidAdvancedContent = document.getElementById('uuidAdvancedContent');
|
|
uuidList = document.getElementById('uuidList');
|
|
customUuidInput = document.getElementById('customUuidInput');
|
|
setCustomUuidBtn = document.getElementById('setCustomUuidBtn');
|
|
|
|
if (customJavaCheck) {
|
|
customJavaCheck.addEventListener('change', toggleCustomJava);
|
|
}
|
|
|
|
if (browseJavaBtn) {
|
|
browseJavaBtn.addEventListener('click', browseJavaPath);
|
|
}
|
|
|
|
if (settingsPlayerName) {
|
|
settingsPlayerName.addEventListener('change', savePlayerName);
|
|
}
|
|
|
|
if (discordRPCCheck) {
|
|
discordRPCCheck.addEventListener('change', saveDiscordRPC);
|
|
}
|
|
|
|
if (closeLauncherCheck) {
|
|
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
|
|
}
|
|
|
|
if (allowMultiInstanceCheck) {
|
|
allowMultiInstanceCheck.addEventListener('change', saveAllowMultiInstance);
|
|
}
|
|
|
|
if (launcherHwAccelCheck) {
|
|
launcherHwAccelCheck.addEventListener('change', saveLauncherHwAccel);
|
|
}
|
|
|
|
|
|
// UUID event listeners
|
|
if (copyUuidBtn) {
|
|
copyUuidBtn.addEventListener('click', copyCurrentUuid);
|
|
}
|
|
|
|
if (regenerateUuidBtn) {
|
|
regenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
|
|
}
|
|
|
|
if (manageUuidsBtn) {
|
|
manageUuidsBtn.addEventListener('click', openUuidModal);
|
|
}
|
|
|
|
if (uuidModalClose) {
|
|
uuidModalClose.addEventListener('click', closeUuidModal);
|
|
}
|
|
|
|
if (addIdentityBtn) {
|
|
addIdentityBtn.addEventListener('click', showAddIdentityForm);
|
|
}
|
|
|
|
if (addIdentityRegenerateBtn) {
|
|
addIdentityRegenerateBtn.addEventListener('click', regenerateAddIdentityUuid);
|
|
}
|
|
|
|
if (addIdentityConfirmBtn) {
|
|
addIdentityConfirmBtn.addEventListener('click', confirmAddIdentity);
|
|
}
|
|
|
|
if (addIdentityCancelBtn) {
|
|
addIdentityCancelBtn.addEventListener('click', hideAddIdentityForm);
|
|
}
|
|
|
|
if (uuidAdvancedToggle) {
|
|
uuidAdvancedToggle.addEventListener('click', toggleAdvancedSection);
|
|
}
|
|
|
|
if (setCustomUuidBtn) {
|
|
setCustomUuidBtn.addEventListener('click', setCustomUuid);
|
|
}
|
|
|
|
if (uuidModal) {
|
|
uuidModal.addEventListener('click', (e) => {
|
|
if (e.target === uuidModal) {
|
|
closeUuidModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (gpuPreferenceRadios) {
|
|
gpuPreferenceRadios.forEach(radio => {
|
|
radio.addEventListener('change', async () => {
|
|
await saveGpuPreference();
|
|
await updateGpuLabel();
|
|
});
|
|
});
|
|
}
|
|
|
|
if (gameBranchRadios) {
|
|
gameBranchRadios.forEach(radio => {
|
|
radio.addEventListener('change', handleBranchChange);
|
|
});
|
|
}
|
|
}
|
|
|
|
function toggleCustomJava() {
|
|
if (!customJavaOptions) return;
|
|
|
|
if (customJavaCheck && customJavaCheck.checked) {
|
|
customJavaOptions.style.display = 'block';
|
|
} else {
|
|
customJavaOptions.style.display = 'none';
|
|
if (customJavaPath) customJavaPath.value = '';
|
|
saveCustomJavaPath('');
|
|
}
|
|
}
|
|
|
|
async function browseJavaPath() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.browseJavaPath) {
|
|
const result = await window.electronAPI.browseJavaPath();
|
|
if (result && result.filePaths && result.filePaths.length > 0) {
|
|
const selectedPath = result.filePaths[0];
|
|
if (customJavaPath) {
|
|
customJavaPath.value = selectedPath;
|
|
}
|
|
await saveCustomJavaPath(selectedPath);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error browsing Java path:', error);
|
|
}
|
|
}
|
|
|
|
async function saveCustomJavaPath(path) {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveJavaPath) {
|
|
await window.electronAPI.saveJavaPath(path);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving custom Java path:', error);
|
|
}
|
|
}
|
|
|
|
async function loadCustomJavaPath() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadJavaPath) {
|
|
const savedPath = await window.electronAPI.loadJavaPath();
|
|
if (savedPath && savedPath.trim()) {
|
|
if (customJavaPath) {
|
|
customJavaPath.value = savedPath;
|
|
}
|
|
if (customJavaCheck) {
|
|
customJavaCheck.checked = true;
|
|
}
|
|
if (customJavaOptions) {
|
|
customJavaOptions.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading custom Java path:', error);
|
|
}
|
|
}
|
|
|
|
async function saveDiscordRPC() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
|
|
const enabled = discordRPCCheck.checked;
|
|
console.log('Saving Discord RPC setting:', enabled);
|
|
|
|
const result = await window.electronAPI.saveDiscordRPC(enabled);
|
|
|
|
if (result && result.success) {
|
|
console.log('Discord RPC setting saved successfully:', enabled);
|
|
|
|
// Feedback visuel pour l'utilisateur
|
|
if (enabled) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
|
|
showNotification(msg, 'success');
|
|
} else {
|
|
const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
|
|
showNotification(msg, 'success');
|
|
}
|
|
} else {
|
|
throw new Error('Failed to save Discord RPC setting');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving Discord RPC setting:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.discordSaveFailed') : 'Failed to save Discord setting';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function loadDiscordRPC() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
|
|
const enabled = await window.electronAPI.loadDiscordRPC();
|
|
if (discordRPCCheck) {
|
|
discordRPCCheck.checked = enabled;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading Discord RPC setting:', error);
|
|
}
|
|
}
|
|
|
|
async function saveCloseLauncher() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
|
|
const enabled = closeLauncherCheck.checked;
|
|
await window.electronAPI.saveCloseLauncher(enabled);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving close launcher setting:', error);
|
|
}
|
|
}
|
|
|
|
async function loadCloseLauncher() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
|
|
const enabled = await window.electronAPI.loadCloseLauncher();
|
|
if (closeLauncherCheck) {
|
|
closeLauncherCheck.checked = enabled;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading close launcher setting:', error);
|
|
}
|
|
}
|
|
|
|
async function saveAllowMultiInstance() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveAllowMultiInstance && allowMultiInstanceCheck) {
|
|
const enabled = allowMultiInstanceCheck.checked;
|
|
await window.electronAPI.saveAllowMultiInstance(enabled);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving multi-instance setting:', error);
|
|
}
|
|
}
|
|
|
|
async function loadAllowMultiInstance() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadAllowMultiInstance) {
|
|
const enabled = await window.electronAPI.loadAllowMultiInstance();
|
|
if (allowMultiInstanceCheck) {
|
|
allowMultiInstanceCheck.checked = enabled;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading multi-instance setting:', error);
|
|
}
|
|
}
|
|
|
|
async function saveLauncherHwAccel() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveLauncherHardwareAcceleration && launcherHwAccelCheck) {
|
|
const enabled = launcherHwAccelCheck.checked;
|
|
await window.electronAPI.saveLauncherHardwareAcceleration(enabled);
|
|
|
|
const msg = window.i18n ? window.i18n.t('notifications.hwAccelSaved') : 'Setting saved. Please restart the launcher to apply changes.';
|
|
showNotification(msg, 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving hardware acceleration setting:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.hwAccelSaveFailed') : 'Failed to save setting';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function loadLauncherHwAccel() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadLauncherHardwareAcceleration) {
|
|
const enabled = await window.electronAPI.loadLauncherHardwareAcceleration();
|
|
if (launcherHwAccelCheck) {
|
|
launcherHwAccelCheck.checked = enabled;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading hardware acceleration setting:', error);
|
|
}
|
|
}
|
|
|
|
|
|
async function savePlayerName() {
|
|
try {
|
|
if (!window.electronAPI || !settingsPlayerName) return;
|
|
|
|
const playerName = settingsPlayerName.value.trim();
|
|
|
|
if (!playerName) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
if (playerName.length > 16) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less';
|
|
showNotification(msg, 'error');
|
|
settingsPlayerName.value = playerName.substring(0, 16);
|
|
return;
|
|
}
|
|
|
|
const result = await window.electronAPI.saveUsername(playerName);
|
|
|
|
// Check if save was successful
|
|
if (result && result.success === false) {
|
|
console.error('[Settings] Failed to save username:', result.error);
|
|
const errorMsg = window.i18n
|
|
? window.i18n.t('notifications.playerNameSaveFailed')
|
|
: `Failed to save player name: ${result.error || 'Unknown error'}`;
|
|
showNotification(errorMsg, 'error');
|
|
return;
|
|
}
|
|
|
|
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
|
showNotification(successMsg, 'success');
|
|
|
|
// Refresh UUID display since it may have changed for the new username
|
|
await loadCurrentUuid();
|
|
|
|
// Also refresh the UUID list to update which entry is marked as current
|
|
await loadAllUuids();
|
|
|
|
// Refresh header identity dropdown + shield icon
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
|
|
} catch (error) {
|
|
console.error('Error saving player name:', error);
|
|
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
|
showNotification(errorMsg, 'error');
|
|
}
|
|
}
|
|
|
|
async function loadPlayerName() {
|
|
try {
|
|
if (!window.electronAPI || !settingsPlayerName) return;
|
|
|
|
const savedName = await window.electronAPI.loadUsername();
|
|
if (savedName) {
|
|
settingsPlayerName.value = savedName;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading player name:', error);
|
|
}
|
|
}
|
|
|
|
async function saveGpuPreference() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.saveGpuPreference && gpuPreferenceRadios) {
|
|
const gpuPreference = Array.from(gpuPreferenceRadios).find(radio => radio.checked)?.value || 'auto';
|
|
await window.electronAPI.saveGpuPreference(gpuPreference);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving GPU preference:', error);
|
|
}
|
|
}
|
|
|
|
async function updateGpuLabel() {
|
|
const detectionInfo = document.getElementById('gpu-detection-info');
|
|
if (!detectionInfo) return;
|
|
|
|
if (gpuPreferenceRadios) {
|
|
const checked = Array.from(gpuPreferenceRadios).find(radio => radio.checked);
|
|
if (checked) {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.getDetectedGpu) {
|
|
const detected = await window.electronAPI.getDetectedGpu();
|
|
if (checked.value === 'auto') {
|
|
if (detected.dedicatedName) {
|
|
detectionInfo.textContent = `dGPU detected, using ${detected.dedicatedName}`;
|
|
} else {
|
|
detectionInfo.textContent = `dGPU not detected, using iGPU (${detected.integratedName}) instead`;
|
|
}
|
|
detectionInfo.style.display = 'block';
|
|
} else if (checked.value === 'integrated') {
|
|
detectionInfo.textContent = `Detected: ${detected.integratedName}`;
|
|
detectionInfo.style.display = 'block';
|
|
} else if (checked.value === 'dedicated') {
|
|
if (detected.dedicatedName) {
|
|
detectionInfo.textContent = `Detected: ${detected.dedicatedName}`;
|
|
} else {
|
|
detectionInfo.textContent = `No dedicated GPU detected`;
|
|
}
|
|
detectionInfo.style.display = 'block';
|
|
} else {
|
|
detectionInfo.style.display = 'none';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting detected GPU:', error);
|
|
detectionInfo.style.display = 'none';
|
|
}
|
|
} else {
|
|
detectionInfo.style.display = 'none';
|
|
}
|
|
} else {
|
|
detectionInfo.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function loadGpuPreference() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadGpuPreference && gpuPreferenceRadios) {
|
|
const savedPreference = await window.electronAPI.loadGpuPreference();
|
|
if (savedPreference) {
|
|
for (const radio of gpuPreferenceRadios) {
|
|
if (radio.value === savedPreference) {
|
|
radio.checked = true;
|
|
break;
|
|
}
|
|
}
|
|
await updateGpuLabel();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading GPU preference:', error);
|
|
}
|
|
}
|
|
|
|
async function loadAllSettings() {
|
|
await loadCustomJavaPath();
|
|
await loadPlayerName();
|
|
await loadCurrentUuid();
|
|
await loadDiscordRPC();
|
|
await loadCloseLauncher();
|
|
await loadAllowMultiInstance();
|
|
await loadLauncherHwAccel();
|
|
await loadGpuPreference();
|
|
await loadVersionBranch();
|
|
await loadWrapperConfigUI();
|
|
}
|
|
|
|
|
|
async function openGameLocation() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.openGameLocation) {
|
|
await window.electronAPI.openGameLocation();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error opening game location:', error);
|
|
}
|
|
}
|
|
|
|
export function getCurrentJavaPath() {
|
|
if (customJavaCheck && customJavaCheck.checked && customJavaPath) {
|
|
return customJavaPath.value.trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Get current player name from UI input
|
|
* Returns null if no name is set (caller must handle this)
|
|
* NOTE: launcher.js now loads username directly from backend config
|
|
* This function is used for display purposes only
|
|
*/
|
|
export function getCurrentPlayerName() {
|
|
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
|
return settingsPlayerName.value.trim();
|
|
}
|
|
// Return null instead of 'Player' - caller must handle missing username
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get current player name with fallback for display purposes only
|
|
* DO NOT use this for launching game - use backend loadUsername() instead
|
|
*/
|
|
export function getCurrentPlayerNameForDisplay() {
|
|
return getCurrentPlayerName() || 'Player';
|
|
}
|
|
|
|
window.openGameLocation = openGameLocation;
|
|
|
|
document.addEventListener('DOMContentLoaded', initSettings);
|
|
|
|
window.SettingsAPI = {
|
|
getCurrentJavaPath,
|
|
getCurrentPlayerName,
|
|
getCurrentPlayerNameForDisplay,
|
|
reloadBranch: loadVersionBranch
|
|
};
|
|
|
|
async function loadCurrentUuid() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.getCurrentUuid) {
|
|
const uuid = await window.electronAPI.getCurrentUuid();
|
|
if (uuid) {
|
|
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading current UUID:', error);
|
|
}
|
|
}
|
|
|
|
async function copyCurrentUuid() {
|
|
try {
|
|
const uuid = currentUuidDisplay ? currentUuidDisplay.value : null;
|
|
if (uuid && navigator.clipboard) {
|
|
await navigator.clipboard.writeText(uuid);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
|
showNotification(msg, 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error copying UUID:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function regenerateCurrentUuid() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
|
|
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
|
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
|
|
|
showCustomConfirm(
|
|
message,
|
|
title,
|
|
async () => {
|
|
await performRegenerateUuid();
|
|
},
|
|
null,
|
|
confirmBtn,
|
|
cancelBtn
|
|
);
|
|
} else {
|
|
console.error('electronAPI.resetCurrentUserUuid not available');
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenNotAvailable') : 'UUID regeneration not available';
|
|
showNotification(msg, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in regenerateCurrentUuid:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed') : 'Failed to regenerate UUID';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function performRegenerateUuid() {
|
|
try {
|
|
const result = await window.electronAPI.resetCurrentUserUuid();
|
|
if (result.success && result.uuid) {
|
|
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
|
showNotification(msg, 'success');
|
|
|
|
if (uuidModal && uuidModal.style.display !== 'none') {
|
|
await loadAllUuids();
|
|
}
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to generate new UUID');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error regenerating UUID:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed').replace('{error}', error.message) : `Failed to regenerate UUID: ${error.message}`;
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function openUuidModal() {
|
|
try {
|
|
if (uuidModal) {
|
|
uuidModal.style.display = 'flex';
|
|
uuidModal.classList.add('active');
|
|
await loadAllUuids();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error opening UUID modal:', error);
|
|
}
|
|
}
|
|
|
|
// Expose globally so identity dropdown and Escape handler can use them
|
|
window.openUuidModal = openUuidModal;
|
|
window.loadCurrentUuid = loadCurrentUuid;
|
|
|
|
function closeUuidModal() {
|
|
if (uuidModal) {
|
|
uuidModal.classList.remove('active');
|
|
setTimeout(() => {
|
|
uuidModal.style.display = 'none';
|
|
}, 300);
|
|
}
|
|
}
|
|
window.closeUuidModal = closeUuidModal;
|
|
|
|
async function loadAllUuids() {
|
|
try {
|
|
if (!uuidList) return;
|
|
|
|
uuidList.innerHTML = `
|
|
<div class="uuid-loading">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
Loading UUIDs...
|
|
</div>
|
|
`;
|
|
|
|
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
|
const mappings = await window.electronAPI.getAllUuidMappings();
|
|
|
|
if (mappings.length === 0) {
|
|
uuidList.innerHTML = `
|
|
<div class="uuid-loading">
|
|
<i class="fas fa-info-circle"></i>
|
|
No UUIDs found
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
uuidList.innerHTML = '';
|
|
|
|
for (const mapping of mappings) {
|
|
const item = document.createElement('div');
|
|
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
|
|
|
|
item.innerHTML = `
|
|
<div class="uuid-item-info">
|
|
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
|
|
<div class="uuid-item-uuid">${mapping.uuid}</div>
|
|
</div>
|
|
<div class="uuid-item-actions">
|
|
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
|
${!mapping.isCurrent ? `<button class="uuid-item-btn switch" onclick="switchToUsername('${escapeHtml(mapping.username)}')" title="Switch to this identity">
|
|
<i class="fas fa-user-check"></i>
|
|
</button>` : ''}
|
|
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
${mapping.isCurrent ? `<button class="uuid-item-btn regenerate" onclick="regenerateUuidForUser('${escapeHtml(mapping.username)}')" title="Regenerate UUID">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>` : ''}
|
|
${!mapping.isCurrent ? `<button class="uuid-item-btn delete" onclick="deleteUuid('${escapeHtml(mapping.username)}')" title="Delete UUID">
|
|
<i class="fas fa-trash"></i>
|
|
</button>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
uuidList.appendChild(item);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading UUIDs:', error);
|
|
if (uuidList) {
|
|
uuidList.innerHTML = `
|
|
<div class="uuid-loading">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
Error loading UUIDs
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function showAddIdentityForm() {
|
|
if (!uuidAddForm) return;
|
|
uuidAddForm.style.display = 'block';
|
|
if (addIdentityUsername) {
|
|
addIdentityUsername.value = '';
|
|
addIdentityUsername.focus();
|
|
}
|
|
if (addIdentityUuid) {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.generateNewUuid) {
|
|
const newUuid = await window.electronAPI.generateNewUuid();
|
|
if (newUuid) addIdentityUuid.value = newUuid;
|
|
}
|
|
} catch (e) {
|
|
console.error('Error pre-generating UUID:', e);
|
|
}
|
|
}
|
|
if (addIdentityBtn) addIdentityBtn.style.display = 'none';
|
|
}
|
|
|
|
function hideAddIdentityForm() {
|
|
if (uuidAddForm) uuidAddForm.style.display = 'none';
|
|
if (addIdentityBtn) addIdentityBtn.style.display = '';
|
|
}
|
|
|
|
async function regenerateAddIdentityUuid() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.generateNewUuid) {
|
|
const newUuid = await window.electronAPI.generateNewUuid();
|
|
if (newUuid && addIdentityUuid) {
|
|
addIdentityUuid.value = newUuid;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error generating UUID:', error);
|
|
}
|
|
}
|
|
|
|
async function confirmAddIdentity() {
|
|
try {
|
|
const username = addIdentityUsername ? addIdentityUsername.value.trim() : '';
|
|
const uuid = addIdentityUuid ? addIdentityUuid.value.trim() : '';
|
|
|
|
if (!username) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a username';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
if (username.length > 16) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Username must be 16 characters or less';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
if (!uuid || !uuidRegex.test(uuid)) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
// Check if name already exists locally
|
|
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
|
const mappings = await window.electronAPI.getAllUuidMappings();
|
|
const existing = mappings.find(m => m.username.toLowerCase() === username.toLowerCase());
|
|
if (existing) {
|
|
showNotification(`Identity "${existing.username}" already exists (UUID: ${existing.uuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
|
|
return;
|
|
}
|
|
// Check if UUID already used by another identity
|
|
const uuidMatch = mappings.find(m => m.uuid.toLowerCase() === uuid.toLowerCase());
|
|
if (uuidMatch) {
|
|
showNotification(`This UUID is already used by identity "${uuidMatch.username}". Each identity must have a unique UUID.`, 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check username reservation on auth server
|
|
try {
|
|
const cfg = await window.electronAPI.loadConfig();
|
|
const authDomain = cfg.authDomain || 'auth.sanasol.ws';
|
|
const checkResp = await fetch(`https://${authDomain}/player/username/status/${encodeURIComponent(username)}`);
|
|
if (checkResp.ok) {
|
|
const status = await checkResp.json();
|
|
if (status.reserved) {
|
|
showNotification(`Username "${username}" is reserved by another player who set a password. Choose a different name.`, 'error');
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Server check failed — allow creation (fail-open)
|
|
console.log('[Identity] Server username check skipped:', e.message);
|
|
}
|
|
|
|
// Check if UUID is password-protected on server (restore access flow)
|
|
let uuidIsProtected = false;
|
|
let registeredName = null;
|
|
try {
|
|
if (window.electronAPI.checkPasswordStatus) {
|
|
const pwStatus = await window.electronAPI.checkPasswordStatus(uuid);
|
|
if (pwStatus && pwStatus.hasPassword) {
|
|
uuidIsProtected = true;
|
|
registeredName = pwStatus.registeredName || null;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('[Identity] UUID password check skipped:', e.message);
|
|
}
|
|
|
|
if (uuidIsProtected) {
|
|
// UUID is password-protected — need password to restore it
|
|
showRestoreProtectedIdentityDialog(username, uuid, registeredName);
|
|
return;
|
|
}
|
|
|
|
await saveNewIdentity(username, uuid);
|
|
} catch (error) {
|
|
console.error('Error adding identity:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.identityAddFailed') : 'Failed to add identity';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function saveNewIdentity(username, uuid) {
|
|
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
|
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
|
if (result.success) {
|
|
showNotification('Identity added successfully!', 'success');
|
|
hideAddIdentityForm();
|
|
await loadAllUuids();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
} else if (result.error === 'duplicate') {
|
|
showNotification(`Identity "${username}" already exists (UUID: ${result.existingUuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
|
|
} else if (result.error === 'uuid_in_use') {
|
|
showNotification(`This UUID is already used by identity "${result.existingUsername}". Each identity must have a unique UUID.`, 'error');
|
|
} else {
|
|
throw new Error(result.error || 'Failed to add identity');
|
|
}
|
|
}
|
|
}
|
|
|
|
function showRestoreProtectedIdentityDialog(username, uuid, registeredName) {
|
|
const existing = document.querySelector('.custom-confirm-modal');
|
|
if (existing) existing.remove();
|
|
|
|
const nameWarning = registeredName && registeredName.toLowerCase() !== username.toLowerCase()
|
|
? `<p style="color: #f59e0b; margin: 0 0 12px; font-size: 0.9rem;">
|
|
<i class="fas fa-exclamation-triangle"></i> This UUID is locked to name "<strong>${escapeHtml(registeredName)}</strong>".
|
|
Your entered name "${escapeHtml(username)}" will be replaced.
|
|
</p>`
|
|
: '';
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'custom-confirm-modal';
|
|
overlay.style.cssText = `
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.8); backdrop-filter: blur(4px);
|
|
z-index: 20000; display: flex; align-items: center; justify-content: center;
|
|
opacity: 0; transition: opacity 0.3s ease;
|
|
`;
|
|
|
|
const dialog = document.createElement('div');
|
|
dialog.style.cssText = `
|
|
background: #1f2937; border-radius: 12px; padding: 0;
|
|
min-width: 420px; max-width: 520px;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
|
border: 1px solid rgba(147, 51, 234, 0.4);
|
|
transform: scale(0.9); transition: transform 0.3s ease;
|
|
`;
|
|
|
|
dialog.innerHTML = `
|
|
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
|
|
<i class="fas fa-shield-alt" style="font-size: 24px;"></i>
|
|
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Restore Protected Identity</h3>
|
|
</div>
|
|
</div>
|
|
<div style="padding: 24px;">
|
|
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
|
|
This UUID is <strong style="color: #22c55e;">password-protected</strong>. Enter the password to restore access.
|
|
</p>
|
|
${nameWarning}
|
|
<div id="restoreError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
|
|
<input type="password" id="restorePasswordInput" style="
|
|
width: 100%; box-sizing: border-box; padding: 10px 14px;
|
|
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
|
|
" placeholder="Password" autofocus />
|
|
</div>
|
|
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
|
<button id="restoreCancelBtn" style="
|
|
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
|
|
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
|
|
">Cancel</button>
|
|
<button id="restoreConfirmBtn" style="
|
|
padding: 8px 20px; border-radius: 8px; border: none;
|
|
background: linear-gradient(135deg, #9333ea, #3b82f6); color: white;
|
|
cursor: pointer; font-weight: 600; font-size: 0.9rem;
|
|
">Verify & Restore</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(dialog);
|
|
document.body.appendChild(overlay);
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '1';
|
|
dialog.style.transform = 'scale(1)';
|
|
});
|
|
|
|
const input = overlay.querySelector('#restorePasswordInput');
|
|
const errorMsg = overlay.querySelector('#restoreError');
|
|
const confirmBtn = overlay.querySelector('#restoreConfirmBtn');
|
|
const cancelBtn = overlay.querySelector('#restoreCancelBtn');
|
|
let busy = false;
|
|
|
|
const close = () => { overlay.remove(); };
|
|
cancelBtn.onclick = close;
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
|
|
const doRestore = async () => {
|
|
if (busy) return;
|
|
const password = input.value.trim();
|
|
if (!password) {
|
|
errorMsg.textContent = 'Password is required';
|
|
errorMsg.style.display = 'block';
|
|
input.focus();
|
|
return;
|
|
}
|
|
|
|
busy = true;
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.textContent = 'Verifying...';
|
|
errorMsg.style.display = 'none';
|
|
|
|
try {
|
|
// Use the registered name if UUID is name-locked
|
|
const finalName = registeredName || username;
|
|
|
|
// Verify password by attempting to get tokens
|
|
const cfg = await window.electronAPI.loadConfig();
|
|
const authDomain = cfg.authDomain || 'auth.sanasol.ws';
|
|
const resp = await fetch(`https://${authDomain}/game-session/new`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ uuid, name: finalName, password, scopes: 'hytale:server hytale:client' })
|
|
});
|
|
|
|
if (resp.status === 401 || resp.status === 429) {
|
|
const err = await resp.json();
|
|
errorMsg.textContent = err.error || 'Incorrect password';
|
|
errorMsg.style.display = 'block';
|
|
input.value = '';
|
|
input.focus();
|
|
busy = false;
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Verify & Restore';
|
|
return;
|
|
}
|
|
|
|
if (!resp.ok) {
|
|
throw new Error(`Server returned ${resp.status}`);
|
|
}
|
|
|
|
// Password verified — save identity locally (force to allow the name)
|
|
close();
|
|
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
|
const result = await window.electronAPI.setUuidForUser(finalName, uuid, true);
|
|
if (result.success || (result && result.uuid)) {
|
|
showNotification(`Identity "${finalName}" restored successfully!`, 'success');
|
|
hideAddIdentityForm();
|
|
await loadAllUuids();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
} else {
|
|
showNotification(result.error || 'Failed to save identity', 'error');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
errorMsg.textContent = 'Error: ' + e.message;
|
|
errorMsg.style.display = 'block';
|
|
busy = false;
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Verify & Restore';
|
|
}
|
|
};
|
|
|
|
confirmBtn.onclick = doRestore;
|
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doRestore(); });
|
|
}
|
|
|
|
function toggleAdvancedSection() {
|
|
if (!uuidAdvancedContent || !uuidAdvancedToggle) return;
|
|
const isOpen = uuidAdvancedContent.style.display !== 'none';
|
|
uuidAdvancedContent.style.display = isOpen ? 'none' : 'block';
|
|
const chevron = uuidAdvancedToggle.querySelector('.uuid-advanced-chevron');
|
|
if (chevron) {
|
|
chevron.classList.toggle('open', !isOpen);
|
|
}
|
|
}
|
|
|
|
// Password section toggle
|
|
function togglePasswordSection() {
|
|
const content = document.getElementById('passwordSectionContent');
|
|
const toggle = document.getElementById('passwordSectionToggle');
|
|
if (!content || !toggle) return;
|
|
const isOpen = content.style.display !== 'none';
|
|
content.style.display = isOpen ? 'none' : 'block';
|
|
const chevron = toggle.querySelector('.uuid-advanced-chevron');
|
|
if (chevron) chevron.classList.toggle('open', !isOpen);
|
|
if (!isOpen) refreshPasswordStatus();
|
|
}
|
|
|
|
async function refreshPasswordStatus() {
|
|
const statusMsg = document.getElementById('passwordStatusMsg');
|
|
const currentPwInput = document.getElementById('currentPasswordInput');
|
|
const removeBtn = document.getElementById('removePasswordBtn');
|
|
const setBtn = document.getElementById('setPasswordBtn');
|
|
try {
|
|
const uuid = await window.electronAPI?.getCurrentUuid();
|
|
if (!uuid) { if (statusMsg) statusMsg.textContent = 'No UUID available'; return; }
|
|
const result = await window.electronAPI.checkPasswordStatus(uuid);
|
|
if (result && result.hasPassword) {
|
|
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-lock" style="color:#22c55e"></i> Password is set for this UUID';
|
|
if (currentPwInput) currentPwInput.style.display = '';
|
|
if (removeBtn) removeBtn.style.display = '';
|
|
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Change Password'; }
|
|
} else {
|
|
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-unlock" style="color:#f59e0b"></i> No password set — anyone can use this UUID';
|
|
if (currentPwInput) currentPwInput.style.display = 'none';
|
|
if (removeBtn) removeBtn.style.display = 'none';
|
|
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Set Password'; }
|
|
}
|
|
} catch (e) {
|
|
if (statusMsg) statusMsg.textContent = 'Could not check password status';
|
|
}
|
|
}
|
|
|
|
window.handleSetPassword = async function () {
|
|
const newPw = document.getElementById('newPasswordInput');
|
|
const currentPw = document.getElementById('currentPasswordInput');
|
|
const setBtn = document.getElementById('setPasswordBtn');
|
|
if (!newPw || !newPw.value || newPw.value.length < 6) {
|
|
showNotification('Password must be at least 6 characters', 'error');
|
|
return;
|
|
}
|
|
if (setBtn) { setBtn.disabled = true; setBtn.textContent = 'Setting...'; }
|
|
try {
|
|
const uuid = await window.electronAPI.getCurrentUuid();
|
|
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, currentPw?.value || null);
|
|
if (result.success) {
|
|
showNotification('Password set successfully', 'success');
|
|
newPw.value = '';
|
|
if (currentPw) currentPw.value = '';
|
|
refreshPasswordStatus();
|
|
updatePasswordShieldIcon();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
} else {
|
|
showNotification(result.error || 'Failed to set password', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Error: ' + e.message, 'error');
|
|
} finally {
|
|
if (setBtn) { setBtn.disabled = false; setBtn.textContent = 'Set Password'; }
|
|
}
|
|
};
|
|
|
|
window.handleRemovePassword = async function () {
|
|
const currentPw = document.getElementById('currentPasswordInput');
|
|
const removeBtn = document.getElementById('removePasswordBtn');
|
|
if (!currentPw || !currentPw.value) {
|
|
showNotification('Enter your current password to remove it', 'error');
|
|
return;
|
|
}
|
|
if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; }
|
|
try {
|
|
const uuid = await window.electronAPI.getCurrentUuid();
|
|
const result = await window.electronAPI.removePlayerPassword(uuid, currentPw.value);
|
|
if (result.success) {
|
|
showNotification('Password removed', 'success');
|
|
currentPw.value = '';
|
|
refreshPasswordStatus();
|
|
updatePasswordShieldIcon();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
} else {
|
|
showNotification(result.error || 'Failed to remove password', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Error: ' + e.message, 'error');
|
|
} finally {
|
|
if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; }
|
|
}
|
|
};
|
|
|
|
// ─── Password Shield Icon ───
|
|
|
|
window.updatePasswordShieldIcon = updatePasswordShieldIcon;
|
|
async function updatePasswordShieldIcon() {
|
|
const icon = document.getElementById('passwordShieldIcon');
|
|
if (!icon) return;
|
|
try {
|
|
const uuid = await window.electronAPI?.getCurrentUuid();
|
|
if (!uuid) {
|
|
icon.className = 'fas fa-unlock password-shield unprotected';
|
|
icon.setAttribute('data-tooltip', 'No identity loaded');
|
|
return;
|
|
}
|
|
const result = await window.electronAPI.checkPasswordStatus(uuid);
|
|
if (result && result.hasPassword) {
|
|
icon.className = 'fas fa-lock password-shield protected';
|
|
icon.setAttribute('data-tooltip', 'Protected — click to manage');
|
|
} else {
|
|
icon.className = 'fas fa-unlock password-shield unprotected';
|
|
icon.setAttribute('data-tooltip', 'Click to protect identity');
|
|
}
|
|
} catch (e) {
|
|
icon.className = 'fas fa-unlock password-shield unprotected';
|
|
icon.setAttribute('data-tooltip', 'Click to protect identity');
|
|
}
|
|
}
|
|
|
|
// ─── Password Modal ───
|
|
|
|
window.openPasswordModal = async function () {
|
|
const modal = document.getElementById('passwordModal');
|
|
if (!modal) return;
|
|
modal.style.display = 'flex';
|
|
|
|
// Clear inputs
|
|
const newPw = document.getElementById('pwModalNewPassword');
|
|
const curPw = document.getElementById('pwModalCurrentPassword');
|
|
if (newPw) newPw.value = '';
|
|
if (curPw) curPw.value = '';
|
|
|
|
// Load current identity info
|
|
try {
|
|
const username = await window.electronAPI?.loadUsername() || 'Player';
|
|
const uuid = await window.electronAPI?.getCurrentUuid() || '';
|
|
document.getElementById('pwModalName').textContent = username;
|
|
document.getElementById('pwModalUuid').textContent = uuid || 'No UUID';
|
|
|
|
// Check password status
|
|
const result = uuid ? await window.electronAPI.checkPasswordStatus(uuid) : null;
|
|
const badge = document.getElementById('pwModalStatusBadge');
|
|
const statusText = document.getElementById('pwModalStatusText');
|
|
const curPwInput = document.getElementById('pwModalCurrentPassword');
|
|
const removeBtn = document.getElementById('pwModalRemoveBtn');
|
|
const setBtn = document.getElementById('pwModalSetBtn');
|
|
const usernameInfo = document.getElementById('pwModalUsernameInfo');
|
|
|
|
if (result && result.hasPassword) {
|
|
badge.innerHTML = '<span style="color:#22c55e;font-size:0.8em;padding:3px 8px;background:rgba(34,197,94,0.15);border-radius:6px;"><i class="fas fa-lock"></i> Protected</span>';
|
|
statusText.innerHTML = '<i class="fas fa-check-circle" style="color:#22c55e"></i> Password set — your UUID and username are protected';
|
|
if (curPwInput) curPwInput.style.display = '';
|
|
if (removeBtn) removeBtn.style.display = '';
|
|
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Change Password'; }
|
|
if (usernameInfo) usernameInfo.style.display = 'none';
|
|
} else {
|
|
badge.innerHTML = '<span style="color:#f59e0b;font-size:0.8em;padding:3px 8px;background:rgba(245,158,11,0.1);border-radius:6px;"><i class="fas fa-unlock"></i> Open</span>';
|
|
statusText.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i> Anyone can use this UUID and username';
|
|
if (curPwInput) curPwInput.style.display = 'none';
|
|
if (removeBtn) removeBtn.style.display = 'none';
|
|
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; }
|
|
if (usernameInfo) usernameInfo.style.display = '';
|
|
}
|
|
} catch (e) {
|
|
document.getElementById('pwModalStatusText').textContent = 'Could not check password status';
|
|
}
|
|
};
|
|
|
|
window.closePasswordModal = function () {
|
|
const modal = document.getElementById('passwordModal');
|
|
if (modal) modal.style.display = 'none';
|
|
};
|
|
|
|
window.handlePasswordModalSet = async function () {
|
|
const newPw = document.getElementById('pwModalNewPassword');
|
|
const curPw = document.getElementById('pwModalCurrentPassword');
|
|
const setBtn = document.getElementById('pwModalSetBtn');
|
|
if (!newPw || !newPw.value || newPw.value.length < 6) {
|
|
showNotification('Password must be at least 6 characters', 'error');
|
|
return;
|
|
}
|
|
if (setBtn) { setBtn.disabled = true; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Saving...'; }
|
|
try {
|
|
const uuid = await window.electronAPI.getCurrentUuid();
|
|
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, curPw?.value || null);
|
|
if (result.success) {
|
|
const msg = result.username_reserved ? 'Password set! Username "' + (result.reserved_username || '') + '" reserved.' : 'Password set!';
|
|
showNotification(msg, 'success');
|
|
newPw.value = '';
|
|
if (curPw) curPw.value = '';
|
|
updatePasswordShieldIcon();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
openPasswordModal(); // refresh modal state
|
|
} else {
|
|
showNotification(result.error || 'Failed to set password', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Error: ' + e.message, 'error');
|
|
} finally {
|
|
if (setBtn) { setBtn.disabled = false; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; }
|
|
}
|
|
};
|
|
|
|
window.handlePasswordModalRemove = async function () {
|
|
const curPw = document.getElementById('pwModalCurrentPassword');
|
|
const removeBtn = document.getElementById('pwModalRemoveBtn');
|
|
if (!curPw || !curPw.value) {
|
|
showNotification('Enter your current password to remove it', 'error');
|
|
return;
|
|
}
|
|
if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; }
|
|
try {
|
|
const uuid = await window.electronAPI.getCurrentUuid();
|
|
const result = await window.electronAPI.removePlayerPassword(uuid, curPw.value);
|
|
if (result.success) {
|
|
showNotification('Password removed', 'success');
|
|
curPw.value = '';
|
|
updatePasswordShieldIcon();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
openPasswordModal(); // refresh modal state
|
|
} else {
|
|
showNotification(result.error || 'Failed to remove password', 'error');
|
|
}
|
|
} catch (e) {
|
|
showNotification('Error: ' + e.message, 'error');
|
|
} finally {
|
|
if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; }
|
|
}
|
|
};
|
|
|
|
// Close modal on backdrop click
|
|
document.addEventListener('click', (e) => {
|
|
const modal = document.getElementById('passwordModal');
|
|
if (modal && e.target === modal) closePasswordModal();
|
|
});
|
|
|
|
// Bind password section toggle (for legacy UUID modal section)
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const pwToggle = document.getElementById('passwordSectionToggle');
|
|
if (pwToggle) pwToggle.addEventListener('click', togglePasswordSection);
|
|
updatePasswordShieldIcon();
|
|
});
|
|
|
|
window.regenerateUuidForUser = async function (username) {
|
|
try {
|
|
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
|
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
|
|
|
showCustomConfirm(
|
|
message,
|
|
title,
|
|
async () => {
|
|
await performRegenerateUuid();
|
|
},
|
|
null,
|
|
confirmBtn,
|
|
cancelBtn
|
|
);
|
|
} catch (error) {
|
|
console.error('Error regenerating UUID:', error);
|
|
}
|
|
};
|
|
|
|
async function setCustomUuid() {
|
|
try {
|
|
if (!customUuidInput || !customUuidInput.value.trim()) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidRequired') : 'Please enter a UUID';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
const uuid = customUuidInput.value.trim();
|
|
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
if (!uuidRegex.test(uuid)) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
const message = window.i18n ? window.i18n.t('confirm.setCustomUuidMessage') : 'Are you sure you want to set this custom UUID? This will change your player identity.';
|
|
const title = window.i18n ? window.i18n.t('confirm.setCustomUuidTitle') : 'Set Custom UUID';
|
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.setCustomUuidButton') : 'Set UUID';
|
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
|
|
|
showCustomConfirm(
|
|
message,
|
|
title,
|
|
async () => {
|
|
await performSetCustomUuid(uuid);
|
|
},
|
|
null,
|
|
confirmBtn,
|
|
cancelBtn
|
|
);
|
|
} catch (error) {
|
|
console.error('Error in setCustomUuid:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed') : 'Failed to set custom UUID';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
async function performSetCustomUuid(uuid) {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
|
// IMPORTANT: Use saved username from config, not unsaved DOM input
|
|
// This prevents setting UUID for wrong user if username field was edited but not saved
|
|
let username = null;
|
|
if (window.electronAPI.loadUsername) {
|
|
username = await window.electronAPI.loadUsername();
|
|
}
|
|
if (!username) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.noUsername') : 'No username configured. Please save your username first.';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
const result = await window.electronAPI.setUuidForUser(username, uuid, true); // force: true — explicit UUID change
|
|
|
|
if (result.success) {
|
|
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
|
if (customUuidInput) customUuidInput.value = '';
|
|
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
|
|
showNotification(msg, 'success');
|
|
|
|
await loadAllUuids();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to set custom UUID');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error setting custom UUID:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
window.copyUuid = async function (uuid) {
|
|
try {
|
|
if (navigator.clipboard) {
|
|
await navigator.clipboard.writeText(uuid);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
|
showNotification(msg, 'success');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error copying UUID:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
|
|
showNotification(msg, 'error');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Switch to a different username/UUID identity
|
|
* This changes the active username to use that username's UUID
|
|
*/
|
|
window.switchToUsername = async function (username) {
|
|
try {
|
|
const message = window.i18n
|
|
? window.i18n.t('confirm.switchUsernameMessage').replace('{username}', username)
|
|
: `Switch to username "${username}"? This will change your active player identity.`;
|
|
const title = window.i18n ? window.i18n.t('confirm.switchUsernameTitle') : 'Switch Identity';
|
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.switchUsernameButton') : 'Switch';
|
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
|
|
|
showCustomConfirm(
|
|
message,
|
|
title,
|
|
async () => {
|
|
await performSwitchToUsername(username);
|
|
},
|
|
null,
|
|
confirmBtn,
|
|
cancelBtn
|
|
);
|
|
} catch (error) {
|
|
console.error('Error in switchToUsername:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.switchUsernameFailed') : 'Failed to switch username';
|
|
showNotification(msg, 'error');
|
|
}
|
|
};
|
|
|
|
async function performSwitchToUsername(username) {
|
|
try {
|
|
if (!window.electronAPI || !window.electronAPI.saveUsername) {
|
|
throw new Error('API not available');
|
|
}
|
|
|
|
const result = await window.electronAPI.saveUsername(username);
|
|
|
|
if (result && result.success === false) {
|
|
throw new Error(result.error || 'Failed to save username');
|
|
}
|
|
|
|
// Update the username input field
|
|
if (settingsPlayerName) {
|
|
settingsPlayerName.value = username;
|
|
}
|
|
|
|
// Refresh the current UUID display
|
|
await loadCurrentUuid();
|
|
|
|
// Refresh the UUID list to show new "Current" badge
|
|
await loadAllUuids();
|
|
|
|
// Refresh header identity dropdown + shield icon
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
|
|
const msg = window.i18n
|
|
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
|
|
: `Switched to "${username}" successfully!`;
|
|
showNotification(msg, 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Error switching username:', error);
|
|
const msg = window.i18n
|
|
? window.i18n.t('notifications.switchUsernameFailed')
|
|
: `Failed to switch username: ${error.message}`;
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
window.deleteUuid = async function (username) {
|
|
try {
|
|
// Look up UUID for this username
|
|
let uuid = null;
|
|
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
|
const mappings = await window.electronAPI.getAllUuidMappings();
|
|
const entry = mappings.find(m => m.username.toLowerCase() === username.toLowerCase());
|
|
if (entry) uuid = entry.uuid;
|
|
}
|
|
|
|
// Check if password-protected
|
|
let isProtected = false;
|
|
if (uuid && window.electronAPI && window.electronAPI.checkPasswordStatus) {
|
|
try {
|
|
const pwStatus = await window.electronAPI.checkPasswordStatus(uuid);
|
|
isProtected = pwStatus && pwStatus.hasPassword;
|
|
} catch (e) {
|
|
console.log('[Identity] Password status check failed:', e.message);
|
|
}
|
|
}
|
|
|
|
if (isProtected) {
|
|
// Password-protected identity — show warning with password input
|
|
showPasswordProtectedDeleteDialog(username, uuid);
|
|
} else {
|
|
// Normal identity — simple confirm
|
|
const message = `Are you sure you want to delete the identity "${username}"? This action cannot be undone.`;
|
|
showCustomConfirm(
|
|
message,
|
|
'Delete Identity',
|
|
async () => { await performDeleteUuid(username); },
|
|
null,
|
|
'Delete',
|
|
'Cancel'
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in deleteUuid:', error);
|
|
showNotification('Failed to delete identity', 'error');
|
|
}
|
|
};
|
|
|
|
function showPasswordProtectedDeleteDialog(username, uuid) {
|
|
const existing = document.querySelector('.custom-confirm-modal');
|
|
if (existing) existing.remove();
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'custom-confirm-modal';
|
|
overlay.style.cssText = `
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.8); backdrop-filter: blur(4px);
|
|
z-index: 20000; display: flex; align-items: center; justify-content: center;
|
|
opacity: 0; transition: opacity 0.3s ease;
|
|
`;
|
|
|
|
const dialog = document.createElement('div');
|
|
dialog.style.cssText = `
|
|
background: #1f2937; border-radius: 12px; padding: 0;
|
|
min-width: 420px; max-width: 520px;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
transform: scale(0.9); transition: transform 0.3s ease;
|
|
`;
|
|
|
|
dialog.innerHTML = `
|
|
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
|
<i class="fas fa-shield-alt" style="font-size: 24px;"></i>
|
|
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Delete Protected Identity</h3>
|
|
</div>
|
|
</div>
|
|
<div style="padding: 24px;">
|
|
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
|
|
<strong>"${escapeHtml(username)}"</strong> is password-protected. Deleting it will:
|
|
</p>
|
|
<ul style="color: #f87171; margin: 0 0 16px; padding-left: 20px; line-height: 1.8;">
|
|
<li>Remove the password protection from this UUID</li>
|
|
<li>Release the reserved username "${escapeHtml(username)}"</li>
|
|
<li>Allow anyone to use this UUID and name</li>
|
|
</ul>
|
|
<p style="color: #9ca3af; margin: 0 0 16px; font-size: 0.9rem;">
|
|
Enter your current password to confirm deletion:
|
|
</p>
|
|
<div id="pwDeleteError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
|
|
<input type="password" id="pwDeleteInput" style="
|
|
width: 100%; box-sizing: border-box; padding: 10px 14px;
|
|
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
|
|
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
|
|
" placeholder="Current password" autofocus />
|
|
</div>
|
|
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
|
<button id="pwDeleteCancelBtn" style="
|
|
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
|
|
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
|
|
">Cancel</button>
|
|
<button id="pwDeleteConfirmBtn" style="
|
|
padding: 8px 20px; border-radius: 8px; border: none;
|
|
background: #ef4444; color: white; cursor: pointer; font-weight: 600; font-size: 0.9rem;
|
|
">Delete & Remove Password</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(dialog);
|
|
document.body.appendChild(overlay);
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '1';
|
|
dialog.style.transform = 'scale(1)';
|
|
});
|
|
|
|
const input = overlay.querySelector('#pwDeleteInput');
|
|
const errorMsg = overlay.querySelector('#pwDeleteError');
|
|
const confirmBtn = overlay.querySelector('#pwDeleteConfirmBtn');
|
|
const cancelBtn = overlay.querySelector('#pwDeleteCancelBtn');
|
|
let busy = false;
|
|
|
|
const close = () => { overlay.remove(); };
|
|
|
|
cancelBtn.onclick = close;
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
|
|
const doDelete = async () => {
|
|
if (busy) return;
|
|
const password = input.value.trim();
|
|
if (!password) {
|
|
errorMsg.textContent = 'Password is required';
|
|
errorMsg.style.display = 'block';
|
|
input.focus();
|
|
return;
|
|
}
|
|
|
|
busy = true;
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.textContent = 'Removing...';
|
|
errorMsg.style.display = 'none';
|
|
|
|
try {
|
|
// Step 1: Remove password on server (validates current password)
|
|
const removeResult = await window.electronAPI.removePlayerPassword(uuid, password);
|
|
if (!removeResult.success) {
|
|
errorMsg.textContent = removeResult.error || 'Incorrect password';
|
|
errorMsg.style.display = 'block';
|
|
input.value = '';
|
|
input.focus();
|
|
busy = false;
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Delete & Remove Password';
|
|
return;
|
|
}
|
|
|
|
// Step 2: Also clear saved password if any
|
|
try {
|
|
const cfg = await window.electronAPI.loadConfig();
|
|
if (cfg.savedPasswords && cfg.savedPasswords[uuid]) {
|
|
delete cfg.savedPasswords[uuid];
|
|
await window.electronAPI.saveConfig({ savedPasswords: cfg.savedPasswords });
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
// Step 3: Delete identity locally
|
|
close();
|
|
await performDeleteUuid(username);
|
|
} catch (e) {
|
|
errorMsg.textContent = 'Error: ' + e.message;
|
|
errorMsg.style.display = 'block';
|
|
busy = false;
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.textContent = 'Delete & Remove Password';
|
|
}
|
|
};
|
|
|
|
confirmBtn.onclick = doDelete;
|
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doDelete(); });
|
|
}
|
|
|
|
async function performDeleteUuid(username) {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
|
|
const result = await window.electronAPI.deleteUuidForUser(username);
|
|
|
|
if (result.success) {
|
|
showNotification('Identity deleted successfully!', 'success');
|
|
await loadAllUuids();
|
|
if (window.loadIdentities) window.loadIdentities();
|
|
updatePasswordShieldIcon();
|
|
} else {
|
|
throw new Error(result.error || 'Failed to delete identity');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting UUID:', error);
|
|
showNotification(`Failed to delete identity: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 1rem 1.5rem;
|
|
border-radius: 8px;
|
|
color: white;
|
|
font-weight: 600;
|
|
z-index: 10000;
|
|
opacity: 0;
|
|
transform: translateX(100%);
|
|
transition: all 0.3s ease;
|
|
`;
|
|
|
|
if (type === 'success') {
|
|
notification.style.background = 'linear-gradient(135deg, #22c55e, #16a34a)';
|
|
} else if (type === 'error') {
|
|
notification.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)';
|
|
} else {
|
|
notification.style.background = 'linear-gradient(135deg, #3b82f6, #2563eb)';
|
|
}
|
|
|
|
notification.innerHTML = `
|
|
<i class="fas fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation-triangle' : 'info-circle'}"></i>
|
|
${message}
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.opacity = '1';
|
|
notification.style.transform = 'translateX(0)';
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
notification.style.opacity = '0';
|
|
notification.style.transform = 'translateX(100%)';
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.parentNode.removeChild(notification);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}// Append this to settings.js for branch management
|
|
|
|
// === Game Branch Management ===
|
|
async function handleBranchChange(event) {
|
|
const newBranch = event.target.value;
|
|
const currentBranch = await loadVersionBranch();
|
|
|
|
if (newBranch === currentBranch) {
|
|
return; // No change
|
|
}
|
|
|
|
// Confirm branch change
|
|
const branchName = window.i18n ?
|
|
window.i18n.t(`settings.branch${newBranch === 'pre-release' ? 'PreRelease' : 'Release'}`) :
|
|
newBranch;
|
|
|
|
const message = window.i18n ?
|
|
window.i18n.t('settings.branchWarning') :
|
|
'Changing branch will download and install a different game version';
|
|
|
|
showCustomConfirm(
|
|
message,
|
|
window.i18n ? window.i18n.t('settings.gameBranch') : 'Game Branch',
|
|
async () => {
|
|
await switchBranch(newBranch);
|
|
},
|
|
() => {
|
|
// Cancel: revert radio selection
|
|
loadVersionBranch().then(branch => {
|
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`);
|
|
if (radioToCheck) {
|
|
radioToCheck.checked = true;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async function switchBranch(newBranch) {
|
|
try {
|
|
const switchingMsg = window.i18n ?
|
|
window.i18n.t('settings.branchSwitching').replace('{branch}', newBranch) :
|
|
`Switching to ${newBranch}...`;
|
|
|
|
showNotification(switchingMsg, 'info');
|
|
|
|
// Lock play button
|
|
const playButton = document.getElementById('playButton');
|
|
if (playButton) {
|
|
playButton.disabled = true;
|
|
playButton.classList.add('disabled');
|
|
}
|
|
|
|
// DON'T save branch yet - wait for installation confirmation
|
|
|
|
// Suggest reinstalling
|
|
setTimeout(() => {
|
|
const branchLabel = newBranch === 'release' ?
|
|
(window.i18n ? window.i18n.t('install.releaseVersion') : 'Release') :
|
|
(window.i18n ? window.i18n.t('install.preReleaseVersion') : 'Pre-Release');
|
|
|
|
const confirmMsg = window.i18n ?
|
|
window.i18n.t('settings.branchInstallConfirm').replace('{branch}', branchLabel) :
|
|
`The game will be installed for the ${branchLabel} branch. Continue?`;
|
|
|
|
showCustomConfirm(
|
|
confirmMsg,
|
|
window.i18n ? window.i18n.t('settings.installRequired') : 'Installation Required',
|
|
async () => {
|
|
// Show progress and trigger game installation
|
|
if (window.LauncherUI) {
|
|
window.LauncherUI.showProgress();
|
|
}
|
|
|
|
try {
|
|
const playerName = await window.electronAPI.loadUsername();
|
|
const result = await window.electronAPI.installGame(playerName || 'Player', '', '', newBranch);
|
|
|
|
if (result.success) {
|
|
// Save branch ONLY after successful installation
|
|
await window.electronAPI.saveVersionBranch(newBranch);
|
|
|
|
const switchedMsg = window.i18n ?
|
|
window.i18n.t('settings.branchSwitched').replace('{branch}', newBranch) :
|
|
`Switched to ${newBranch} successfully!`;
|
|
|
|
const successMsg = window.i18n ?
|
|
window.i18n.t('progress.installationComplete') :
|
|
'Installation completed successfully!';
|
|
|
|
showNotification(switchedMsg, 'success');
|
|
showNotification(successMsg, 'success');
|
|
|
|
// Refresh radio buttons to reflect the new branch
|
|
await loadVersionBranch();
|
|
console.log('[Settings] Radio buttons updated after branch switch');
|
|
|
|
setTimeout(() => {
|
|
if (window.LauncherUI) {
|
|
window.LauncherUI.hideProgress();
|
|
}
|
|
|
|
// Unlock play button
|
|
const playButton = document.getElementById('playButton');
|
|
if (playButton) {
|
|
playButton.disabled = false;
|
|
playButton.classList.remove('disabled');
|
|
}
|
|
}, 2000);
|
|
} else {
|
|
throw new Error(result.error || 'Installation failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Installation error:', error);
|
|
const errorMsg = window.i18n ?
|
|
window.i18n.t('progress.installationFailed').replace('{error}', error.message) :
|
|
`Installation failed: ${error.message}`;
|
|
|
|
showNotification(errorMsg, 'error');
|
|
|
|
if (window.LauncherUI) {
|
|
window.LauncherUI.hideProgress();
|
|
}
|
|
|
|
// Revert radio selection to old branch
|
|
loadVersionBranch().then(oldBranch => {
|
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${oldBranch}"]`);
|
|
if (radioToCheck) {
|
|
radioToCheck.checked = true;
|
|
}
|
|
});
|
|
|
|
// Unlock play button
|
|
const playButton = document.getElementById('playButton');
|
|
if (playButton) {
|
|
playButton.disabled = false;
|
|
playButton.classList.remove('disabled');
|
|
}
|
|
}
|
|
},
|
|
() => {
|
|
// Cancel - unlock play button
|
|
const playButton = document.getElementById('playButton');
|
|
if (playButton) {
|
|
playButton.disabled = false;
|
|
playButton.classList.remove('disabled');
|
|
}
|
|
},
|
|
window.i18n ? window.i18n.t('common.install') : 'Install',
|
|
window.i18n ? window.i18n.t('common.cancel') : 'Cancel'
|
|
);
|
|
}, 500);
|
|
|
|
} catch (error) {
|
|
console.error('Error switching branch:', error);
|
|
showNotification(`Failed to switch branch: ${error.message}`, 'error');
|
|
|
|
// Revert radio selection
|
|
loadVersionBranch().then(branch => {
|
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`);
|
|
if (radioToCheck) {
|
|
radioToCheck.checked = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadVersionBranch() {
|
|
try {
|
|
if (window.electronAPI && window.electronAPI.loadVersionBranch) {
|
|
const branch = await window.electronAPI.loadVersionBranch();
|
|
console.log('[Settings] Loaded version_branch from config:', branch);
|
|
|
|
// Use default if branch is null/undefined
|
|
const selectedBranch = branch || 'release';
|
|
console.log('[Settings] Selected branch:', selectedBranch);
|
|
|
|
// Update radio buttons
|
|
if (gameBranchRadios && gameBranchRadios.length > 0) {
|
|
gameBranchRadios.forEach(radio => {
|
|
radio.checked = radio.value === selectedBranch;
|
|
console.log(`[Settings] Radio ${radio.value}: ${radio.checked ? 'checked' : 'unchecked'}`);
|
|
});
|
|
} else {
|
|
console.warn('[Settings] gameBranchRadios not found or empty');
|
|
}
|
|
|
|
return selectedBranch;
|
|
}
|
|
return 'release'; // Default
|
|
} catch (error) {
|
|
console.error('Error loading version branch:', error);
|
|
return 'release';
|
|
}
|
|
}
|
|
|
|
// === Java Wrapper Configuration UI ===
|
|
|
|
let _wrapperConfig = null;
|
|
let _wrapperPreviewOpen = false;
|
|
|
|
async function loadWrapperConfigUI() {
|
|
try {
|
|
if (!window.electronAPI || !window.electronAPI.loadWrapperConfig) return;
|
|
|
|
_wrapperConfig = await window.electronAPI.loadWrapperConfig();
|
|
renderStripFlagsList();
|
|
renderInjectArgsList();
|
|
setupWrapperEventListeners();
|
|
} catch (error) {
|
|
console.error('Error loading wrapper config UI:', error);
|
|
}
|
|
}
|
|
|
|
function renderStripFlagsList() {
|
|
const container = document.getElementById('wrapperStripFlagsList');
|
|
if (!container || !_wrapperConfig) return;
|
|
|
|
if (_wrapperConfig.stripFlags.length === 0) {
|
|
container.innerHTML = '<div class="wrapper-items-empty">No flags configured</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
_wrapperConfig.stripFlags.forEach((flag, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'wrapper-item';
|
|
item.innerHTML = `
|
|
<span class="wrapper-item-text">${escapeHtml(flag)}</span>
|
|
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
|
<i class="fas fa-trash-alt"></i>
|
|
</button>
|
|
`;
|
|
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeStripFlag(index));
|
|
container.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function renderInjectArgsList() {
|
|
const container = document.getElementById('wrapperInjectArgsList');
|
|
if (!container || !_wrapperConfig) return;
|
|
|
|
if (_wrapperConfig.injectArgs.length === 0) {
|
|
container.innerHTML = '<div class="wrapper-items-empty">No arguments configured</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
_wrapperConfig.injectArgs.forEach((entry, index) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'wrapper-item';
|
|
|
|
const serverLabel = window.i18n ? window.i18n.t('settings.wrapperConditionServer') : 'Server Only';
|
|
const alwaysLabel = window.i18n ? window.i18n.t('settings.wrapperConditionAlways') : 'Always';
|
|
|
|
item.innerHTML = `
|
|
<span class="wrapper-item-text">${escapeHtml(entry.arg)}</span>
|
|
<div class="wrapper-item-condition">
|
|
<select data-index="${index}">
|
|
<option value="server"${entry.condition === 'server' ? ' selected' : ''}>${serverLabel}</option>
|
|
<option value="always"${entry.condition === 'always' ? ' selected' : ''}>${alwaysLabel}</option>
|
|
</select>
|
|
</div>
|
|
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
|
<i class="fas fa-trash-alt"></i>
|
|
</button>
|
|
`;
|
|
item.querySelector('select').addEventListener('change', (e) => updateArgCondition(index, e.target.value));
|
|
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeInjectArg(index));
|
|
container.appendChild(item);
|
|
});
|
|
}
|
|
|
|
async function addStripFlag() {
|
|
const input = document.getElementById('wrapperAddFlagInput');
|
|
if (!input || !_wrapperConfig) return;
|
|
|
|
const flag = input.value.trim();
|
|
if (!flag) return;
|
|
|
|
if (_wrapperConfig.stripFlags.includes(flag)) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.wrapperFlagExists') : 'This flag is already in the list';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
_wrapperConfig.stripFlags.push(flag);
|
|
input.value = '';
|
|
renderStripFlagsList();
|
|
await saveWrapperConfigToBackend();
|
|
await updateWrapperPreview();
|
|
}
|
|
|
|
async function removeStripFlag(index) {
|
|
if (!_wrapperConfig) return;
|
|
_wrapperConfig.stripFlags.splice(index, 1);
|
|
renderStripFlagsList();
|
|
await saveWrapperConfigToBackend();
|
|
await updateWrapperPreview();
|
|
}
|
|
|
|
async function addInjectArg() {
|
|
const input = document.getElementById('wrapperAddArgInput');
|
|
const condSelect = document.getElementById('wrapperAddArgCondition');
|
|
if (!input || !condSelect || !_wrapperConfig) return;
|
|
|
|
const arg = input.value.trim();
|
|
if (!arg) return;
|
|
|
|
const exists = _wrapperConfig.injectArgs.some(e => e.arg === arg);
|
|
if (exists) {
|
|
const msg = window.i18n ? window.i18n.t('notifications.wrapperArgExists') : 'This argument is already in the list';
|
|
showNotification(msg, 'error');
|
|
return;
|
|
}
|
|
|
|
_wrapperConfig.injectArgs.push({ arg, condition: condSelect.value });
|
|
input.value = '';
|
|
renderInjectArgsList();
|
|
await saveWrapperConfigToBackend();
|
|
await updateWrapperPreview();
|
|
}
|
|
|
|
async function removeInjectArg(index) {
|
|
if (!_wrapperConfig) return;
|
|
_wrapperConfig.injectArgs.splice(index, 1);
|
|
renderInjectArgsList();
|
|
await saveWrapperConfigToBackend();
|
|
await updateWrapperPreview();
|
|
}
|
|
|
|
async function updateArgCondition(index, condition) {
|
|
if (!_wrapperConfig || !_wrapperConfig.injectArgs[index]) return;
|
|
_wrapperConfig.injectArgs[index].condition = condition;
|
|
await saveWrapperConfigToBackend();
|
|
await updateWrapperPreview();
|
|
}
|
|
|
|
async function saveWrapperConfigToBackend() {
|
|
try {
|
|
const result = await window.electronAPI.saveWrapperConfig(_wrapperConfig);
|
|
if (!result || !result.success) {
|
|
throw new Error(result?.error || 'Save failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving wrapper config:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigSaveFailed') : 'Failed to save wrapper configuration';
|
|
showNotification(msg, 'error');
|
|
}
|
|
}
|
|
|
|
function setupWrapperEventListeners() {
|
|
const addFlagBtn = document.getElementById('wrapperAddFlagBtn');
|
|
const addFlagInput = document.getElementById('wrapperAddFlagInput');
|
|
const addArgBtn = document.getElementById('wrapperAddArgBtn');
|
|
const addArgInput = document.getElementById('wrapperAddArgInput');
|
|
const restoreBtn = document.getElementById('wrapperRestoreDefaultsBtn');
|
|
const previewToggle = document.getElementById('wrapperPreviewToggle');
|
|
|
|
if (addFlagBtn) addFlagBtn.addEventListener('click', addStripFlag);
|
|
if (addFlagInput) addFlagInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addStripFlag(); });
|
|
if (addArgBtn) addArgBtn.addEventListener('click', addInjectArg);
|
|
if (addArgInput) addArgInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addInjectArg(); });
|
|
|
|
if (restoreBtn) {
|
|
restoreBtn.addEventListener('click', () => {
|
|
const message = window.i18n ? window.i18n.t('confirm.resetWrapperMessage') : 'Are you sure you want to restore defaults? Your custom changes will be lost.';
|
|
const title = window.i18n ? window.i18n.t('confirm.resetWrapperTitle') : 'Restore Defaults';
|
|
|
|
showCustomConfirm(message, title, async () => {
|
|
try {
|
|
const result = await window.electronAPI.resetWrapperConfig();
|
|
if (result && result.success) {
|
|
_wrapperConfig = result.config;
|
|
renderStripFlagsList();
|
|
renderInjectArgsList();
|
|
await updateWrapperPreview();
|
|
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigReset') : 'Wrapper configuration restored to defaults';
|
|
showNotification(msg, 'success');
|
|
} else {
|
|
throw new Error(result?.error || 'Reset failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error resetting wrapper config:', error);
|
|
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigResetFailed') : 'Failed to restore wrapper configuration';
|
|
showNotification(msg, 'error');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (previewToggle) {
|
|
previewToggle.addEventListener('click', toggleWrapperPreview);
|
|
}
|
|
}
|
|
|
|
async function toggleWrapperPreview() {
|
|
const container = document.getElementById('wrapperPreviewContainer');
|
|
const chevron = document.getElementById('wrapperPreviewChevron');
|
|
if (!container) return;
|
|
|
|
_wrapperPreviewOpen = !_wrapperPreviewOpen;
|
|
|
|
if (_wrapperPreviewOpen) {
|
|
container.style.display = 'block';
|
|
if (chevron) chevron.classList.add('expanded');
|
|
await updateWrapperPreview();
|
|
} else {
|
|
container.style.display = 'none';
|
|
if (chevron) chevron.classList.remove('expanded');
|
|
}
|
|
}
|
|
|
|
async function updateWrapperPreview() {
|
|
if (!_wrapperPreviewOpen || !_wrapperConfig) return;
|
|
|
|
const previewEl = document.getElementById('wrapperPreviewContent');
|
|
if (!previewEl) return;
|
|
|
|
try {
|
|
const platform = await window.electronAPI.getCurrentPlatform();
|
|
const script = await window.electronAPI.previewWrapperScript(_wrapperConfig, platform);
|
|
previewEl.textContent = script;
|
|
} catch (error) {
|
|
previewEl.textContent = 'Error generating preview: ' + error.message;
|
|
}
|
|
}
|