v2.4.4: Rename Profiles to Configurations, add Identity Switcher

- Rename "Profiles" to "Configurations" in all UI text and 11 locale files
- Add identity switcher dropdown in header (green accent, fa-id-badge icon)
- Quick-switch player identity without opening Settings
- "Manage" action opens UUID Management modal
- Header tooltips explaining what each dropdown does
- Config dropdown icon changed from fa-user-circle to fa-sliders-h
- Global Escape key handler for closing modals and dropdowns
- Fix identity selector not clickable (missing -webkit-app-region: no-drag)
- Sync header identity name after all identity-changing operations
- XSS protection in identity list rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-02-26 20:01:40 +01:00
parent 66493d35ca
commit b83b728d21
17 changed files with 842 additions and 181 deletions

View File

@@ -35,13 +35,21 @@ export function setupLauncher() {
// Initial Profile Load
loadProfiles();
// Close dropdown on outside click
// Initial Identity Load
loadIdentities();
// Close dropdowns on outside click
document.addEventListener('click', (e) => {
const selector = document.getElementById('profileSelector');
if (selector && !selector.contains(e.target)) {
const profileSelector = document.getElementById('profileSelector');
if (profileSelector && !profileSelector.contains(e.target)) {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
}
const identitySelector = document.getElementById('identitySelector');
if (identitySelector && !identitySelector.contains(e.target)) {
const dropdown = document.getElementById('identityDropdown');
if (dropdown) dropdown.classList.remove('show');
}
});
}
@@ -83,7 +91,7 @@ function renderProfileList(profiles, activeProfile) {
managerList.innerHTML = profiles.map(p => `
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
<div class="flex items-center gap-3">
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<i class="fas fa-sliders-h text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
@@ -106,13 +114,6 @@ function updateCurrentProfileUI(profile) {
}
}
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
}
};
window.openProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) {
@@ -146,7 +147,7 @@ window.createNewProfile = async () => {
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
if (!confirm('Are you sure you want to delete this configuration? Mod settings will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
@@ -160,7 +161,7 @@ window.deleteProfile = async (id) => {
window.switchProfile = async (id) => {
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching configuration...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
await window.electronAPI.profile.activate(id);
@@ -179,7 +180,7 @@ window.switchProfile = async (id) => {
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) {
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Configuration switched!';
window.LauncherUI.updateProgress({ message: switchedMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
}
@@ -676,6 +677,121 @@ async function loadCustomJavaPath() {
}
}
// ==========================================
// IDENTITY SWITCHER
// ==========================================
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function loadIdentities() {
try {
if (!window.electronAPI) return;
const nameEl = document.getElementById('currentIdentityName');
// Load current username
let currentUsername = 'Player';
if (window.electronAPI.loadUsername) {
const name = await window.electronAPI.loadUsername();
if (name) currentUsername = name;
}
if (nameEl) nameEl.textContent = currentUsername;
// Load all identities for dropdown
const list = document.getElementById('identityList');
if (!list || !window.electronAPI.getAllUuidMappings) return;
const mappings = await window.electronAPI.getAllUuidMappings();
renderIdentityList(mappings, currentUsername);
} catch (error) {
console.error('Failed to load identities:', error);
}
}
function renderIdentityList(mappings, currentUsername) {
const list = document.getElementById('identityList');
if (!list) return;
if (!mappings || mappings.length === 0) {
list.innerHTML = '<div class="identity-empty">No identities</div>';
return;
}
list.innerHTML = mappings.map(m => {
const safe = escapeHtml(m.username);
return `
<div class="identity-item ${m.username === currentUsername ? 'active' : ''}"
onclick="switchIdentity('${safe.replace(/'/g, "&#39;")}')">
<span>${safe}</span>
${m.username === currentUsername ? '<i class="fas fa-check ml-auto"></i>' : ''}
</div>
`;
}).join('');
}
window.toggleIdentityDropdown = () => {
const dropdown = document.getElementById('identityDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
// Close profile dropdown
const profileDropdown = document.getElementById('profileDropdown');
if (profileDropdown) profileDropdown.classList.remove('show');
}
};
window.openIdentityManager = () => {
// Close dropdown
const dropdown = document.getElementById('identityDropdown');
if (dropdown) dropdown.classList.remove('show');
// Open UUID modal from settings
if (window.openUuidModal) {
window.openUuidModal();
}
};
window.switchIdentity = async (username) => {
try {
if (!window.electronAPI || !window.electronAPI.saveUsername) return;
const result = await window.electronAPI.saveUsername(username);
if (result && result.success === false) {
throw new Error(result.error || 'Failed to switch identity');
}
// Refresh identity dropdown
await loadIdentities();
// Close dropdown
const dropdown = document.getElementById('identityDropdown');
if (dropdown) dropdown.classList.remove('show');
// Update settings page username field and UUID display
const settingsInput = document.getElementById('settingsPlayerName');
if (settingsInput) settingsInput.value = username;
if (window.loadCurrentUuid) window.loadCurrentUuid();
} catch (error) {
console.error('Failed to switch identity:', error);
}
};
// Make loadIdentities available globally for settings.js to call
window.loadIdentities = loadIdentities;
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
// Close identity dropdown
const identityDropdown = document.getElementById('identityDropdown');
if (identityDropdown) identityDropdown.classList.remove('show');
}
};
window.launch = launch;
window.uninstallGame = uninstallGame;
window.repairGame = repairGame;

View File

@@ -18,10 +18,15 @@ let regenerateUuidBtn;
let manageUuidsBtn;
let uuidModal;
let uuidModalClose;
let modalCurrentUuid;
let modalCopyUuidBtn;
let modalRegenerateUuidBtn;
let generateNewUuidBtn;
let addIdentityBtn;
let uuidAddForm;
let addIdentityUsername;
let addIdentityUuid;
let addIdentityRegenerateBtn;
let addIdentityConfirmBtn;
let addIdentityCancelBtn;
let uuidAdvancedToggle;
let uuidAdvancedContent;
let uuidList;
let customUuidInput;
let setCustomUuidBtn;
@@ -180,10 +185,15 @@ function setupSettingsElements() {
manageUuidsBtn = document.getElementById('manageUuidsBtn');
uuidModal = document.getElementById('uuidModal');
uuidModalClose = document.getElementById('uuidModalClose');
modalCurrentUuid = document.getElementById('modalCurrentUuid');
modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn');
modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn');
generateNewUuidBtn = document.getElementById('generateNewUuidBtn');
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');
@@ -230,16 +240,24 @@ function setupSettingsElements() {
uuidModalClose.addEventListener('click', closeUuidModal);
}
if (modalCopyUuidBtn) {
modalCopyUuidBtn.addEventListener('click', copyCurrentUuid);
if (addIdentityBtn) {
addIdentityBtn.addEventListener('click', showAddIdentityForm);
}
if (modalRegenerateUuidBtn) {
modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
if (addIdentityRegenerateBtn) {
addIdentityRegenerateBtn.addEventListener('click', regenerateAddIdentityUuid);
}
if (generateNewUuidBtn) {
generateNewUuidBtn.addEventListener('click', generateNewUuid);
if (addIdentityConfirmBtn) {
addIdentityConfirmBtn.addEventListener('click', confirmAddIdentity);
}
if (addIdentityCancelBtn) {
addIdentityCancelBtn.addEventListener('click', hideAddIdentityForm);
}
if (uuidAdvancedToggle) {
uuidAdvancedToggle.addEventListener('click', toggleAdvancedSection);
}
if (setCustomUuidBtn) {
@@ -467,6 +485,9 @@ async function savePlayerName() {
// Also refresh the UUID list to update which entry is marked as current
await loadAllUuids();
// Refresh header identity dropdown
if (window.loadIdentities) window.loadIdentities();
} catch (error) {
console.error('Error saving player name:', error);
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
@@ -630,7 +651,6 @@ async function loadCurrentUuid() {
const uuid = await window.electronAPI.getCurrentUuid();
if (uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
}
}
} catch (error) {
@@ -640,7 +660,7 @@ async function loadCurrentUuid() {
async function copyCurrentUuid() {
try {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
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!';
@@ -688,13 +708,13 @@ async function performRegenerateUuid() {
const result = await window.electronAPI.resetCurrentUserUuid();
if (result.success && result.uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
if (modalCurrentUuid) modalCurrentUuid.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();
} else {
throw new Error(result.error || 'Failed to generate new UUID');
}
@@ -717,6 +737,10 @@ async function openUuidModal() {
}
}
// 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');
@@ -725,6 +749,7 @@ function closeUuidModal() {
}, 300);
}
}
window.closeUuidModal = closeUuidModal;
async function loadAllUuids() {
try {
@@ -769,6 +794,9 @@ async function loadAllUuids() {
<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>` : ''}
@@ -791,23 +819,119 @@ async function loadAllUuids() {
}
}
async function generateNewUuid() {
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) {
if (customUuidInput) customUuidInput.value = newUuid;
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
showNotification(msg, 'success');
if (newUuid && addIdentityUuid) {
addIdentityUuid.value = newUuid;
}
}
} catch (error) {
console.error('Error generating new UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
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;
}
if (window.electronAPI && window.electronAPI.setUuidForUser) {
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
const msg = window.i18n ? window.i18n.t('notifications.identityAdded') : 'Identity added successfully!';
showNotification(msg, 'success');
hideAddIdentityForm();
await loadAllUuids();
if (window.loadIdentities) window.loadIdentities();
} else {
throw new Error(result.error || 'Failed to add identity');
}
}
} catch (error) {
console.error('Error adding identity:', error);
const msg = window.i18n ? window.i18n.t('notifications.identityAddFailed') : 'Failed to add identity';
showNotification(msg, 'error');
}
}
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);
}
}
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()) {
@@ -865,13 +989,13 @@ async function performSetCustomUuid(uuid) {
if (result.success) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.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();
} else {
throw new Error(result.error || 'Failed to set custom UUID');
}
@@ -950,6 +1074,9 @@ async function performSwitchToUsername(username) {
// Refresh the UUID list to show new "Current" badge
await loadAllUuids();
// Refresh header identity dropdown
if (window.loadIdentities) window.loadIdentities();
const msg = window.i18n
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
: `Switched to "${username}" successfully!`;
@@ -997,6 +1124,7 @@ async function performDeleteUuid(username) {
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
showNotification(msg, 'success');
await loadAllUuids();
if (window.loadIdentities) window.loadIdentities();
} else {
throw new Error(result.error || 'Failed to delete UUID');
}

View File

@@ -79,12 +79,18 @@ function setupWindowControls() {
const header = document.querySelector('.header');
const profileSelector = document.querySelector('.profile-selector');
const identitySelector = document.querySelector('.identity-selector');
if (profileSelector) {
profileSelector.style.pointerEvents = 'auto';
profileSelector.style.zIndex = '10000';
}
if (identitySelector) {
identitySelector.style.pointerEvents = 'auto';
identitySelector.style.zIndex = '10000';
}
if (windowControls) {
windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000';
@@ -98,6 +104,9 @@ function setupWindowControls() {
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
if (identitySelector) {
identitySelector.style.webkitAppRegion = 'no-drag';
}
}
if (window.electronAPI) {
@@ -1109,4 +1118,50 @@ window.openDiscordExternal = function() {
window.toggleMaximize = toggleMaximize;
// Global Escape key handler for closing popups/modals/dropdowns
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
// Custom confirm dialogs handle their own Escape — skip if one is open
if (document.querySelector('.custom-confirm-modal')) return;
// Close modals (highest priority)
const profileModal = document.getElementById('profileManagerModal');
if (profileModal && profileModal.style.display !== 'none') {
if (window.closeProfileManager) window.closeProfileManager();
return;
}
const uuidModal = document.getElementById('uuidModal');
if (uuidModal && uuidModal.style.display !== 'none') {
if (window.closeUuidModal) window.closeUuidModal();
return;
}
const discordModal = document.getElementById('discordPopupModal');
if (discordModal && discordModal.style.display !== 'none') {
discordModal.style.display = 'none';
return;
}
const versionModal = document.getElementById('versionSelectModal');
if (versionModal && versionModal.style.display !== 'none') {
versionModal.style.display = 'none';
return;
}
// Close dropdowns (lower priority)
const identityDropdown = document.getElementById('identityDropdown');
if (identityDropdown && identityDropdown.classList.contains('show')) {
identityDropdown.classList.remove('show');
return;
}
const profileDropdown = document.getElementById('profileDropdown');
if (profileDropdown && profileDropdown.classList.contains('show')) {
profileDropdown.classList.remove('show');
return;
}
});
document.addEventListener('DOMContentLoaded', setupUI);