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

@ -1412,260 +1412,384 @@ document.addEventListener('DOMContentLoaded', () => {
let displayProperties = {}; // Store original properties for filtering
function parseServerProperties(content) {
const properties = {};
const lines = content.split('\n');
lines.forEach(line => {
if (line.trim() && !line.trim().startsWith('#')) {
const [key, value] = line.split('=', 2).map(part => part.trim());
if (key && value !== undefined) {
properties[key] = value;
}
function parseServerProperties(content) {
const properties = {};
const lines = content.split('\n');
lines.forEach(line => {
if (line.trim() && !line.trim().startsWith('#')) {
const [key, value] = line.split('=', 2).map(part => part.trim());
if (key && value !== undefined) {
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 = '') {
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);
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';
}
// 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);
}
function renderPropertiesList(properties, filter = '') {
const propertiesList = elements.propertiesFields.querySelector('#propertiesList');
propertiesList.innerHTML = '';
renderPropertiesList(properties, filter);
// Filter properties based on search input
const filteredProperties = Object.entries(properties).filter(([key]) =>
key.toLowerCase().includes(filter.toLowerCase())
);
// Render filtered properties
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, '');
}
// 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');
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');
}
hideCustomPropertyForm();
});
closeButton.dataset.closeHandlerAdded = 'true';
}
}
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() {
try {