v2.4.1: Replace raw wrapper script editor with structured config UI

Replace the raw textarea script editor with a structured form for Java
wrapper configuration. Users now manage two lists (JVM flags to strip,
args to inject with server/always condition) instead of editing bash/batch
scripts directly. Scripts are generated at launch time from the structured
config. Includes collapsible script preview for power users.
This commit is contained in:
sanasol
2026-02-24 16:53:19 +01:00
parent 19c8991a44
commit 0d15659dc0
10 changed files with 770 additions and 20 deletions

View File

@@ -430,8 +430,70 @@
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-scroll"></i>
<span data-i18n="settings.wrapperConfig">Java Wrapper Configuration</span>
</h3>
<p class="settings-hint" style="margin-bottom: 12px;">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.wrapperConfigHint">Configure how the Java wrapper handles JVM flags and arguments at launch time.</span>
</p>
<!-- Strip Flags -->
<label class="settings-label" style="margin-bottom: 6px;">
<span data-i18n="settings.wrapperStripFlags">JVM Flags to Remove</span>
</label>
<div id="wrapperStripFlagsList" class="wrapper-items-list"></div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<input type="text" id="wrapperAddFlagInput" class="settings-input" style="flex:1;"
data-i18n-placeholder="settings.wrapperAddFlagPlaceholder" placeholder="e.g. -XX:+SomeFlag" spellcheck="false">
<button id="wrapperAddFlagBtn" class="settings-browse-btn">
<i class="fas fa-plus"></i>
<span data-i18n="settings.wrapperAdd">Add</span>
</button>
</div>
<!-- Inject Args -->
<label class="settings-label" style="margin-top: 16px; margin-bottom: 6px;">
<span data-i18n="settings.wrapperInjectArgs">Arguments to Inject</span>
</label>
<div id="wrapperInjectArgsList" class="wrapper-items-list"></div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<input type="text" id="wrapperAddArgInput" class="settings-input" style="flex:1;"
data-i18n-placeholder="settings.wrapperAddArgPlaceholder" placeholder="e.g. --some-flag" spellcheck="false">
<select id="wrapperAddArgCondition" class="wrapper-condition-select">
<option value="server" data-i18n="settings.wrapperConditionServer">Server Only</option>
<option value="always" data-i18n="settings.wrapperConditionAlways">Always</option>
</select>
<button id="wrapperAddArgBtn" class="settings-browse-btn">
<i class="fas fa-plus"></i>
<span data-i18n="settings.wrapperAdd">Add</span>
</button>
</div>
<!-- Restore Defaults -->
<div style="margin-top: 12px;">
<button id="wrapperRestoreDefaultsBtn" class="settings-browse-btn" style="background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3);">
<i class="fas fa-undo"></i>
<span data-i18n="settings.wrapperRestoreDefaults">Restore Defaults</span>
</button>
</div>
<!-- Script Preview (collapsible) -->
<div style="margin-top: 12px;">
<button id="wrapperPreviewToggle" class="wrapper-preview-toggle">
<i class="fas fa-chevron-right" id="wrapperPreviewChevron"></i>
<span data-i18n="settings.wrapperAdvancedPreview">Advanced: Script Preview</span>
</button>
<div id="wrapperPreviewContainer" style="display: none; margin-top: 8px;">
<pre id="wrapperPreviewContent" class="wrapper-preview-content"></pre>
</div>
</div>
</div>
</div>
<div class="settings-column">
<div class="settings-section">
<h3 class="settings-section-title">

View File

@@ -569,6 +569,7 @@ async function loadAllSettings() {
await loadLauncherHwAccel();
await loadGpuPreference();
await loadVersionBranch();
await loadWrapperConfigUI();
}
@@ -1254,3 +1255,235 @@ async function loadVersionBranch() {
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;
}
}

View File

@@ -147,7 +147,18 @@
"branchSwitching": "Switching to {branch}...",
"branchSwitched": "Switched to {branch} successfully!",
"installRequired": "Installation Required",
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?",
"wrapperConfig": "Java Wrapper Configuration",
"wrapperConfigHint": "Configure how the Java wrapper handles JVM flags and arguments at launch time.",
"wrapperStripFlags": "JVM Flags to Remove",
"wrapperInjectArgs": "Arguments to Inject",
"wrapperAddFlagPlaceholder": "e.g. -XX:+SomeFlag",
"wrapperAddArgPlaceholder": "e.g. --some-flag",
"wrapperAdd": "Add",
"wrapperConditionServer": "Server Only",
"wrapperConditionAlways": "Always",
"wrapperRestoreDefaults": "Restore Defaults",
"wrapperAdvancedPreview": "Advanced: Script Preview"
},
"uuid": {
"modalTitle": "UUID Management",
@@ -221,7 +232,13 @@
"noUsername": "No username configured. Please save your username first.",
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
"switchUsernameFailed": "Failed to switch username",
"playerNameTooLong": "Player name must be 16 characters or less"
"playerNameTooLong": "Player name must be 16 characters or less",
"wrapperConfigSaved": "Wrapper configuration saved",
"wrapperConfigSaveFailed": "Failed to save wrapper configuration",
"wrapperConfigReset": "Wrapper configuration restored to defaults",
"wrapperConfigResetFailed": "Failed to restore wrapper configuration",
"wrapperFlagExists": "This flag is already in the list",
"wrapperArgExists": "This argument is already in the list"
},
"confirm": {
"defaultTitle": "Confirm action",
@@ -239,7 +256,9 @@
"uninstallGameButton": "Uninstall",
"switchUsernameTitle": "Switch Identity",
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
"switchUsernameButton": "Switch"
"switchUsernameButton": "Switch",
"resetWrapperTitle": "Restore Defaults",
"resetWrapperMessage": "Are you sure you want to restore the default wrapper configuration? Your custom changes will be lost."
},
"progress": {
"initializing": "Initializing...",

View File

@@ -4829,6 +4829,140 @@ select.settings-input option {
transform: translateY(0);
}
.wrapper-items-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.wrapper-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
transition: all 0.3s ease;
}
.wrapper-item:hover {
border-color: rgba(147, 51, 234, 0.3);
background: rgba(147, 51, 234, 0.05);
}
.wrapper-item-text {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.82rem;
color: #e5e7eb;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrapper-item-condition select {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #e5e7eb;
font-size: 0.75rem;
padding: 4px 8px;
cursor: pointer;
margin: 0 8px;
}
.wrapper-item-condition select:focus {
outline: none;
border-color: rgba(147, 51, 234, 0.5);
}
.wrapper-item-delete {
padding: 4px 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
}
.wrapper-item-delete:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #ef4444;
}
.wrapper-items-empty {
font-style: italic;
text-align: center;
color: rgba(255, 255, 255, 0.4);
padding: 12px;
font-size: 0.82rem;
}
.wrapper-condition-select {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #e5e7eb;
font-size: 0.82rem;
padding: 6px 10px;
cursor: pointer;
}
.wrapper-condition-select:focus {
outline: none;
border-color: rgba(147, 51, 234, 0.5);
}
.wrapper-preview-toggle {
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
font-size: 0.82rem;
padding: 4px 0;
display: flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.wrapper-preview-toggle:hover {
color: rgba(255, 255, 255, 0.9);
}
.wrapper-preview-toggle i {
transition: transform 0.2s;
font-size: 0.7rem;
}
.wrapper-preview-toggle i.expanded {
transform: rotate(90deg);
}
.wrapper-preview-content {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: rgba(255, 255, 255, 0.7);
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.72rem;
line-height: 1.5;
padding: 12px;
max-height: 300px;
overflow: auto;
white-space: pre;
tab-size: 4;
}
.settings-hint {
margin-top: 0.5rem;
font-size: 0.8rem;