Feat: Server Props Search

Feat: Add custom prop adds
Feat: Add Removing Props
This commit is contained in:
MCHost
2025-06-26 02:39:42 -04:00
parent 728ea10e80
commit 375d1400d6
2 changed files with 416 additions and 245 deletions

View File

@ -42,6 +42,8 @@
--spacing: 0.25rem; --spacing: 0.25rem;
--container-md: 28rem; --container-md: 28rem;
--container-xl: 36rem; --container-xl: 36rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem; --text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875); --text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem; --text-lg: 1.125rem;
@ -224,6 +226,9 @@
.inset-0 { .inset-0 {
inset: calc(var(--spacing) * 0); inset: calc(var(--spacing) * 0);
} }
.z-50 {
z-index: 50;
}
.container { .container {
width: 100%; width: 100%;
@media (width >= 40rem) { @media (width >= 40rem) {
@ -251,6 +256,9 @@
.mt-4 { .mt-4 {
margin-top: calc(var(--spacing) * 4); margin-top: calc(var(--spacing) * 4);
} }
.mr-2 {
margin-right: calc(var(--spacing) * 2);
}
.mb-1 { .mb-1 {
margin-bottom: calc(var(--spacing) * 1); margin-bottom: calc(var(--spacing) * 1);
} }
@ -278,6 +286,9 @@
.inline-block { .inline-block {
display: inline-block; display: inline-block;
} }
.table {
display: table;
}
.h-24 { .h-24 {
height: calc(var(--spacing) * 24); height: calc(var(--spacing) * 24);
} }
@ -296,9 +307,15 @@
.min-h-full { .min-h-full {
min-height: 100%; min-height: 100%;
} }
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/3 { .w-1\/3 {
width: calc(1/3 * 100%); width: calc(1/3 * 100%);
} }
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-2\/3 { .w-2\/3 {
width: calc(2/3 * 100%); width: calc(2/3 * 100%);
} }
@ -320,9 +337,15 @@
.flex-1 { .flex-1 {
flex: 1; flex: 1;
} }
.flex-shrink {
flex-shrink: 1;
}
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
.border-collapse {
border-collapse: collapse;
}
.transform { .transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
} }
@ -518,6 +541,10 @@
font-size: var(--text-xl); font-size: var(--text-xl);
line-height: var(--tw-leading, var(--text-xl--line-height)); line-height: var(--tw-leading, var(--text-xl--line-height));
} }
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.leading-relaxed { .leading-relaxed {
--tw-leading: var(--leading-relaxed); --tw-leading: var(--leading-relaxed);
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
@ -552,6 +579,9 @@
.text-white { .text-white {
color: var(--color-white); color: var(--color-white);
} }
.underline {
text-decoration-line: underline;
}
.opacity-50 { .opacity-50 {
opacity: 50%; opacity: 50%;
} }
@ -559,6 +589,10 @@
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter { .filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
} }
@ -634,6 +668,13 @@
} }
} }
} }
.hover\:text-red-700 {
&:hover {
@media (hover: hover) {
color: var(--color-red-700);
}
}
}
.sm\:w-\[90\%\] { .sm\:w-\[90\%\] {
@media (width >= 40rem) { @media (width >= 40rem) {
width: 90%; width: 90%;
@ -1188,6 +1229,11 @@
inherits: false; inherits: false;
initial-value: 0 0 #0000; initial-value: 0 0 #0000;
} }
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur { @property --tw-blur {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;
@ -1296,6 +1342,7 @@
--tw-ring-offset-width: 0px; --tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff; --tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000; --tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-blur: initial; --tw-blur: initial;
--tw-brightness: initial; --tw-brightness: initial;
--tw-contrast: initial; --tw-contrast: initial;

View File

@ -1412,260 +1412,384 @@ document.addEventListener('DOMContentLoaded', () => {
let displayProperties = {}; // Store original properties for filtering let displayProperties = {}; // Store original properties for filtering
function parseServerProperties(content) { function parseServerProperties(content) {
const properties = {}; const properties = {};
const lines = content.split('\n'); const lines = content.split('\n');
lines.forEach(line => { lines.forEach(line => {
if (line.trim() && !line.trim().startsWith('#')) { if (line.trim() && !line.trim().startsWith('#')) {
const [key, value] = line.split('=', 2).map(part => part.trim()); const [key, value] = line.split('=', 2).map(part => part.trim());
if (key && value !== undefined) { if (key && value !== undefined) {
properties[key] = value; properties[key] = value;
}
} }
}
});
return properties;
}
function renderPropertiesFields(properties, filter = '') {
const fieldsContainer = elements.propertiesFields;
// Create search box only if not already present
let searchContainer = fieldsContainer.querySelector('#searchContainer');
if (!searchContainer) {
searchContainer = document.createElement('div');
searchContainer.id = 'searchContainer';
searchContainer.className = 'mb-4';
searchContainer.style.display = 'block';
const searchLabel = document.createElement('label');
searchLabel.textContent = 'Search Properties';
searchLabel.className = 'block text-sm font-medium mb-1 text-white';
searchLabel.setAttribute('for', 'propertiesSearch');
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'propertiesSearch';
searchInput.placeholder = 'Search properties...';
searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full';
searchInput.setAttribute('aria-label', 'Search server properties');
searchContainer.appendChild(searchLabel);
searchContainer.appendChild(searchInput);
// Add custom property button (mini style)
const addButton = document.createElement('button');
addButton.textContent = 'Add Property';
addButton.type = 'button';
addButton.className = 'mt-2 bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs';
addButton.addEventListener('click', showCustomPropertyForm);
searchContainer.appendChild(addButton);
fieldsContainer.appendChild(searchContainer);
// Add input event listener
searchInput.addEventListener('input', (e) => {
renderPropertiesList(displayProperties, e.target.value);
}); });
return properties;
// Store reference to search input
elements.searchInput = searchInput;
} }
function renderPropertiesFields(properties, filter = '') { // Create or update properties list container
const fieldsContainer = elements.propertiesFields; let propertiesList = fieldsContainer.querySelector('#propertiesList');
if (!propertiesList) {
// Create search box only if not already present propertiesList = document.createElement('div');
let searchContainer = fieldsContainer.querySelector('#searchContainer'); propertiesList.id = 'propertiesList';
if (!searchContainer) { propertiesList.className = 'space-y-2';
searchContainer = document.createElement('div'); fieldsContainer.appendChild(propertiesList);
searchContainer.id = 'searchContainer';
searchContainer.className = 'mb-4';
searchContainer.style.display = 'block';
const searchLabel = document.createElement('label');
searchLabel.textContent = 'Search Properties';
searchLabel.className = 'block text-sm font-medium mb-1 text-white';
searchLabel.setAttribute('for', 'propertiesSearch');
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'propertiesSearch';
searchInput.placeholder = 'Search properties...';
searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full';
searchInput.setAttribute('aria-label', 'Search server properties');
searchContainer.appendChild(searchLabel);
searchContainer.appendChild(searchInput);
fieldsContainer.appendChild(searchContainer);
// Add input event listener
searchInput.addEventListener('input', (e) => {
renderPropertiesList(displayProperties, e.target.value);
});
// Store reference to search input
elements.searchInput = searchInput;
}
// Create or update properties list container
let propertiesList = fieldsContainer.querySelector('#propertiesList');
if (!propertiesList) {
propertiesList = document.createElement('div');
propertiesList.id = 'propertiesList';
propertiesList.className = 'space-y-2';
fieldsContainer.appendChild(propertiesList);
}
renderPropertiesList(properties, filter);
// Add modal close handler
const closeButton = elements.editPropertiesModal.querySelector('.close-button');
if (closeButton && !closeButton.dataset.closeHandlerAdded) {
closeButton.addEventListener('click', () => {
elements.searchInput.value = '';
renderPropertiesList(displayProperties, '');
elements.editPropertiesModal.classList.add('hidden');
});
closeButton.dataset.closeHandlerAdded = 'true';
}
} }
function renderPropertiesList(properties, filter = '') { renderPropertiesList(properties, filter);
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
propertiesList.innerHTML = '';
// Filter properties based on search input // Add modal close handler
const filteredProperties = Object.entries(properties).filter(([key]) => const closeButton = elements.editPropertiesModal.querySelector('.close-button');
key.toLowerCase().includes(filter.toLowerCase()) if (closeButton && !closeButton.dataset.closeHandlerAdded) {
); closeButton.addEventListener('click', () => {
elements.searchInput.value = '';
// Render filtered properties renderPropertiesList(displayProperties, '');
filteredProperties.forEach(([key, value]) => {
if (filteredSettings.includes(key)) {
return;
}
console.log(`Rendering field for ${key}: ${value}`); // Debug log
const fieldDiv = document.createElement('div');
fieldDiv.className = 'flex items-center space-x-2';
fieldDiv.style.display = 'flex';
let inputType = 'text';
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
if (isBoolean) {
inputType = 'switch';
} else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) {
inputType = 'number';
}
const label = document.createElement('label');
label.textContent = key;
label.className = 'w-1/3 text-sm font-medium text-white';
label.setAttribute('for', `prop-${key}`);
if (inputType === 'switch') {
// Hidden text input for accessibility and form association
const hiddenInput = document.createElement('input');
hiddenInput.type = 'text';
hiddenInput.id = `prop-${key}`;
hiddenInput.name = key;
hiddenInput.value = value.toLowerCase();
hiddenInput.style.display = 'none';
const switchContainer = document.createElement('div');
switchContainer.className = 'relative inline-block';
switchContainer.setAttribute('role', 'switch');
switchContainer.setAttribute('aria-checked', value.toLowerCase());
switchContainer.setAttribute('tabindex', '0');
switchContainer.dataset.name = key;
switchContainer.style.width = '40px';
switchContainer.style.height = '24px';
switchContainer.style.display = 'inline-block';
switchContainer.style.position = 'relative';
switchContainer.style.cursor = 'pointer';
switchContainer.style.zIndex = '10';
const switchTrack = document.createElement('div');
switchTrack.className = 'block w-full h-full rounded-full';
switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563';
switchTrack.style.transition = 'background-color 0.2s ease-in-out';
const switchHandle = document.createElement('div');
switchHandle.className = 'absolute rounded-full bg-white';
switchHandle.style.width = '16px';
switchHandle.style.height = '16px';
switchHandle.style.top = '4px';
switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px';
switchHandle.style.transition = 'left 0.2s ease-in-out';
switchHandle.style.position = 'absolute';
switchHandle.style.zIndex = '11';
// Handle click and keyboard events
const toggleSwitch = () => {
const currentValue = hiddenInput.value === 'true';
const newValue = !currentValue;
hiddenInput.value = newValue.toString();
switchContainer.setAttribute('aria-checked', newValue.toString());
switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563';
switchHandle.style.left = newValue ? '20px' : '4px';
};
switchContainer.addEventListener('click', toggleSwitch);
switchContainer.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSwitch();
}
});
switchContainer.appendChild(switchTrack);
switchContainer.appendChild(switchHandle);
fieldDiv.appendChild(label);
fieldDiv.appendChild(hiddenInput);
fieldDiv.appendChild(switchContainer);
} else {
const input = document.createElement('input');
input.id = `prop-${key}`;
input.name = key;
input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3';
input.type = inputType;
input.value = value;
if (inputType === 'number') {
input.min = '0';
}
fieldDiv.appendChild(label);
fieldDiv.appendChild(input);
}
propertiesList.appendChild(fieldDiv);
});
}
function propertiesToString(properties) {
let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
return header + Object.entries(properties)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
}
async function fetchServerProperties() {
try {
const key = `action-fetch-properties`;
const notification = showNotification('Loading server properties...', 'loading', key);
const response = await wsRequest('/server-properties', 'GET');
if (response.error) {
updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key);
return;
}
if (response.content && response.content.length > 4000) {
updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key);
return;
}
allProperties = parseServerProperties(response.content || '');
displayProperties = Object.fromEntries(
Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key))
);
renderPropertiesFields(displayProperties);
elements.editPropertiesModal.classList.remove('hidden');
updateNotification(notification, 'Server properties loaded successfully', 'success', key);
} catch (error) {
console.error('Fetch server properties error:', error);
showNotification(`Failed to load server properties: ${error.message}`, 'error', 'fetch-properties-error');
}
}
async function saveServerProperties() {
try {
const key = `action-save-properties`;
const notification = showNotification('Saving server properties...', 'loading', key);
const properties = {};
const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch)');
// Collect modified properties
inputs.forEach(input => {
const key = input.name;
let value = input.value.trim();
if (value !== '') {
properties[key] = value;
}
});
// Merge with allProperties to include all properties, even if not displayed
const fullProperties = { ...allProperties, ...properties };
const content = propertiesToString(fullProperties);
const response = await wsRequest('/server-properties', 'POST', { content });
if (response.error) {
updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key);
return;
}
// Clear search input and reset properties list
if (elements.searchInput) {
elements.searchInput.value = '';
renderPropertiesList(displayProperties, '');
}
elements.editPropertiesModal.classList.add('hidden'); elements.editPropertiesModal.classList.add('hidden');
updateNotification(notification, 'Server properties saved successfully', 'success', key); hideCustomPropertyForm();
} catch (error) { });
console.error('Save server properties error:', error); closeButton.dataset.closeHandlerAdded = 'true';
showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error');
}
} }
}
function showCustomPropertyForm() {
let formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
if (formContainer) {
formContainer.remove();
}
formContainer = document.createElement('div');
formContainer.id = 'customPropertyForm';
formContainer.className = 'mb-4 p-4 bg-gray-800 rounded';
const form = document.createElement('form');
form.addEventListener('submit', (e) => e.preventDefault());
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Property name';
keyInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Property value';
valueInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2';
const addButton = document.createElement('button');
addButton.type = 'button';
addButton.textContent = 'Add';
addButton.className = 'bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs mr-2';
addButton.addEventListener('click', () => {
const key = keyInput.value.trim();
const value = valueInput.value.trim();
if (key && value) {
displayProperties[key] = value;
allProperties[key] = value;
renderPropertiesList(displayProperties, elements.searchInput.value);
formContainer.remove();
} else {
showNotification('Please enter both property name and value', 'error', 'custom-property-error');
}
});
const cancelButton = document.createElement('button');
cancelButton.type = 'button';
cancelButton.textContent = 'Cancel';
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-2 py-1 rounded text-xs';
cancelButton.addEventListener('click', hideCustomPropertyForm);
form.appendChild(keyInput);
form.appendChild(valueInput);
form.appendChild(addButton);
form.appendChild(cancelButton);
formContainer.appendChild(form);
elements.propertiesFields.insertBefore(formContainer, elements.propertiesFields.querySelector('#propertiesList'));
}
function hideCustomPropertyForm() {
const formContainer = elements.propertiesFields.querySelector('#customPropertyForm');
if (formContainer) {
formContainer.remove();
}
}
function showDeleteConfirmationModal(key) {
const modal = document.createElement('div');
modal.id = 'deleteConfirmationModal';
modal.className = 'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
const modalContent = document.createElement('div');
modalContent.className = 'bg-gray-800 p-6 rounded-lg max-w-md w-full';
const header = document.createElement('h3');
header.className = 'text-lg font-medium text-white mb-4';
header.textContent = 'Confirm Deletion';
const message = document.createElement('p');
message.className = 'text-white mb-6';
message.textContent = `Are you sure you want to delete the property "${key}"? This action cannot be undone.`;
const buttonContainer = document.createElement('div');
buttonContainer.className = 'flex justify-end space-x-2';
const cancelButton = document.createElement('button');
cancelButton.type = 'button';
cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded';
cancelButton.textContent = 'Cancel';
cancelButton.addEventListener('click', () => modal.remove());
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => {
delete displayProperties[key];
delete allProperties[key];
renderPropertiesList(displayProperties, elements.searchInput.value);
modal.remove();
showNotification(`Property "${key}" deleted`, 'success', 'delete-property-success');
});
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(deleteButton);
modalContent.appendChild(header);
modalContent.appendChild(message);
modalContent.appendChild(buttonContainer);
modal.appendChild(modalContent);
elements.editPropertiesModal.appendChild(modal);
}
function renderPropertiesList(properties, filter = '') {
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
propertiesList.innerHTML = '';
const filteredProperties = Object.entries(properties).filter(([key]) =>
key.toLowerCase().includes(filter.toLowerCase())
);
filteredProperties.forEach(([key, value]) => {
if (filteredSettings.includes(key)) {
return;
}
console.log(`Rendering field for ${key}: ${value}`);
const fieldDiv = document.createElement('div');
fieldDiv.className = 'flex items-center space-x-2';
fieldDiv.style.display = 'flex';
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'text-red-500 hover:text-red-700';
deleteButton.innerHTML = '✕';
deleteButton.title = `Delete ${key}`;
deleteButton.addEventListener('click', () => showDeleteConfirmationModal(key));
let inputType = 'text';
let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false';
if (isBoolean) {
inputType = 'switch';
} else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) {
inputType = 'number';
}
const label = document.createElement('label');
label.textContent = key;
label.className = 'w-1/3 text-sm font-medium text-white';
label.setAttribute('for', `prop-${key}`);
if (inputType === 'switch') {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'text';
hiddenInput.id = `prop-${key}`;
hiddenInput.name = key;
hiddenInput.value = value.toLowerCase();
hiddenInput.style.display = 'none';
const switchContainer = document.createElement('div');
switchContainer.className = 'relative inline-block';
switchContainer.setAttribute('role', 'switch');
switchContainer.setAttribute('aria-checked', value.toLowerCase());
switchContainer.setAttribute('tabindex', '0');
switchContainer.dataset.name = key;
switchContainer.style.width = '40px';
switchContainer.style.height = '24px';
switchContainer.style.display = 'inline-block';
switchContainer.style.position = 'relative';
switchContainer.style.cursor = 'pointer';
switchContainer.style.zIndex = '10';
const switchTrack = document.createElement('div');
switchTrack.className = 'block w-full h-full rounded-full';
switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563';
switchTrack.style.transition = 'background-color 0.2s ease-in-out';
const switchHandle = document.createElement('div');
switchHandle.className = 'absolute rounded-full bg-white';
switchHandle.style.width = '16px';
switchHandle.style.height = '16px';
switchHandle.style.top = '4px';
switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px';
switchHandle.style.transition = 'left 0.2s ease-in-out';
switchHandle.style.position = 'absolute';
switchHandle.style.zIndex = '11';
const toggleSwitch = () => {
const currentValue = hiddenInput.value === 'true';
const newValue = !currentValue;
hiddenInput.value = newValue.toString();
switchContainer.setAttribute('aria-checked', newValue.toString());
switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563';
switchHandle.style.left = newValue ? '20px' : '4px';
};
switchContainer.addEventListener('click', toggleSwitch);
switchContainer.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSwitch();
}
});
switchContainer.appendChild(switchTrack);
switchContainer.appendChild(switchHandle);
fieldDiv.appendChild(deleteButton);
fieldDiv.appendChild(label);
fieldDiv.appendChild(hiddenInput);
fieldDiv.appendChild(switchContainer);
} else {
const input = document.createElement('input');
input.id = `prop-${key}`;
input.name = key;
input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3';
input.type = 'number';
input.value = value;
if (inputType === 'number') {
input.min = '0';
}
fieldDiv.appendChild(deleteButton);
fieldDiv.appendChild(label);
fieldDiv.appendChild(input);
}
propertiesList.appendChild(fieldDiv);
});
}
function propertiesToString(properties) {
let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
return header + Object.entries(properties)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
}
async function fetchServerProperties() {
try {
const key = `action-fetch-properties`;
const notification = showNotification('Loading server properties...', 'loading', key);
const response = await wsRequest('/server-properties', 'GET');
if (response.error) {
updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key);
return;
}
if (response.content && response.content.length > 4000) {
updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key);
return;
}
allProperties = parseServerProperties(response.content || '');
displayProperties = Object.fromEntries(
Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key))
);
renderPropertiesFields(displayProperties);
elements.editPropertiesModal.classList.remove('hidden');
updateNotification(notification, 'Server properties loaded successfully', 'success', key);
} catch (error) {
console.error('Fetch server properties error:', error);
showNotification(`Failed to load server properties: ${error.message}`, 'error', 'fetch-properties-error');
}
}
async function saveServerProperties() {
try {
const key = `action-save-properties`;
const notification = showNotification('Saving server properties...', 'loading', key);
const properties = {};
const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch):not([id="customPropertyKey"]):not([id="customPropertyValue"])');
// Collect modified properties
inputs.forEach(input => {
const key = input.name;
let value = input.value.trim();
if (value !== '') {
properties[key] = value;
}
});
// Merge with allProperties to include all properties, even if not displayed
const fullProperties = { ...allProperties, ...properties };
const content = propertiesToString(fullProperties);
const response = await wsRequest('/server-properties', 'POST', { content });
if (response.error) {
updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key);
return;
}
// Clear search input and reset properties list
if (elements.searchInput) {
elements.searchInput.value = '';
renderPropertiesList(displayProperties, '');
}
elements.editPropertiesModal.classList.add('hidden');
updateNotification(notification, 'Server properties saved successfully', 'success', key);
} catch (error) {
console.error('Save server properties error:', error);
showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error');
}
}
async function updateMods() { async function updateMods() {
try { try {