paginate mods area and add a search mods feature
This commit is contained in:
161
public/app.js
161
public/app.js
@ -10,6 +10,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalResults = 0;
|
let totalResults = 0;
|
||||||
const resultsPerPage = 10;
|
const resultsPerPage = 10;
|
||||||
|
let modListCurrentPage = 1;
|
||||||
|
let modListSearchQuery = '';
|
||||||
let allProperties = {};
|
let allProperties = {};
|
||||||
const filteredSettings = [
|
const filteredSettings = [
|
||||||
'bug-report-link',
|
'bug-report-link',
|
||||||
@ -79,7 +81,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
connectionStatus: document.getElementById('connectionStatus'),
|
connectionStatus: document.getElementById('connectionStatus'),
|
||||||
geyserStatus: document.getElementById('geyserStatus'),
|
geyserStatus: document.getElementById('geyserStatus'),
|
||||||
sftpStatus: document.getElementById('sftpStatus'),
|
sftpStatus: document.getElementById('sftpStatus'),
|
||||||
backupBtn: document.getElementById('backupBtn')
|
backupBtn: document.getElementById('backupBtn'),
|
||||||
|
modListSearch: document.getElementById('modListSearch'),
|
||||||
|
clearModListSearch: document.getElementById('clearModListSearch'),
|
||||||
|
modListPagination: document.getElementById('modListPagination')
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadouts = {
|
const loadouts = {
|
||||||
@ -162,11 +167,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
connectionStatus: '',
|
connectionStatus: '',
|
||||||
geyserStatus: '',
|
geyserStatus: '',
|
||||||
sftpStatus: '',
|
sftpStatus: '',
|
||||||
activeNotifications: new Map() // Track active notifications to prevent duplicates
|
activeNotifications: new Map(),
|
||||||
|
allMods: []
|
||||||
};
|
};
|
||||||
|
|
||||||
function showNotification(message, type = 'loading', key = null) {
|
function showNotification(message, type = 'loading', key = null) {
|
||||||
// If a notification for this key exists, remove it
|
|
||||||
if (key && state.activeNotifications.has(key)) {
|
if (key && state.activeNotifications.has(key)) {
|
||||||
const existing = state.activeNotifications.get(key);
|
const existing = state.activeNotifications.get(key);
|
||||||
existing.notification.style.opacity = '0';
|
existing.notification.style.opacity = '0';
|
||||||
@ -200,7 +205,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateNotification(notification, message, type, key = null) {
|
function updateNotification(notification, message, type, key = null) {
|
||||||
// Ensure only one notification per key
|
|
||||||
if (key && state.activeNotifications.has(key) && state.activeNotifications.get(key).notification !== notification) {
|
if (key && state.activeNotifications.has(key) && state.activeNotifications.get(key).notification !== notification) {
|
||||||
const existing = state.activeNotifications.get(key);
|
const existing = state.activeNotifications.get(key);
|
||||||
existing.notification.style.opacity = '0';
|
existing.notification.style.opacity = '0';
|
||||||
@ -407,7 +411,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
const requestId = crypto.randomUUID();
|
const requestId = crypto.randomUUID();
|
||||||
const action = endpoint.replace('/', '').replace('-', ' ');
|
const action = endpoint.replace('/', '').replace('-', ' ');
|
||||||
const key = `action-${action}`; // Unique key per action type
|
const key = `action-${action}`;
|
||||||
const notification = showNotification(`Processing ${action}...`, 'loading', key);
|
const notification = showNotification(`Processing ${action}...`, 'loading', key);
|
||||||
pendingRequests.set(requestId, { resolve, reject, notification, key });
|
pendingRequests.set(requestId, { resolve, reject, notification, key });
|
||||||
ws.send(JSON.stringify({ type: 'request', requestId, endpoint, method, body }));
|
ws.send(JSON.stringify({ type: 'request', requestId, endpoint, method, body }));
|
||||||
@ -819,32 +823,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'mod-list' && message.data?.mods) {
|
if (message.type === 'mod-list' && message.data?.mods) {
|
||||||
const modListHtml = message.data.mods.map(mod => `
|
state.allMods = message.data.mods || [];
|
||||||
<div class="bg-gray-700 p-4 rounded">
|
renderModList();
|
||||||
<p><strong>${mod.name}</strong> (${mod.version})</p>
|
|
||||||
<p>ID: ${mod.id}</p>
|
|
||||||
<button class="uninstall-mod bg-red-600 hover:bg-red-700 px-2 py-1 rounded mt-2" data-mod-id="${mod.id}">Uninstall</button>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
if (state.modListHtml !== modListHtml && elements.modList) {
|
|
||||||
elements.modList.innerHTML = modListHtml;
|
|
||||||
state.modListHtml = modListHtml;
|
|
||||||
document.querySelectorAll('.uninstall-mod').forEach(button => {
|
|
||||||
button.addEventListener('click', async () => {
|
|
||||||
const modId = button.getAttribute('data-mod-id');
|
|
||||||
const key = `action-uninstall-mod-${modId}`;
|
|
||||||
try {
|
|
||||||
await wsRequest('/uninstall', 'POST', { mod: modId });
|
|
||||||
const response = await wsRequest('/mod-list');
|
|
||||||
updateNonDockerUI({ type: 'mod-list', data: response });
|
|
||||||
showNotification(`Mod ${modId} uninstalled successfully`, 'success', key);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Uninstall mod error:', error);
|
|
||||||
showNotification(`Failed to uninstall mod ${modId}: ${error.message}`, 'error', key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'log' && message.data?.message) {
|
if (message.type === 'log' && message.data?.message) {
|
||||||
@ -1020,6 +1000,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateModListPagination() {
|
||||||
|
const filteredMods = state.allMods.filter(mod =>
|
||||||
|
mod.name.toLowerCase().includes(modListSearchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
const totalModPages = Math.max(1, Math.ceil(filteredMods.length / resultsPerPage));
|
||||||
|
elements.modListPagination.innerHTML = '';
|
||||||
|
|
||||||
|
const createPageButton = (page, text, disabled = false) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.textContent = text;
|
||||||
|
button.className = `px-3 py-1 rounded ${disabled || page === modListCurrentPage
|
||||||
|
? 'bg-gray-600 cursor-not-allowed'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
}`;
|
||||||
|
if (!disabled && page !== modListCurrentPage) {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
modListCurrentPage = page;
|
||||||
|
renderModList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
elements.modListPagination.appendChild(button);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filteredMods.length > 0) {
|
||||||
|
createPageButton(modListCurrentPage - 1, 'Previous', modListCurrentPage === 1);
|
||||||
|
const maxButtons = 5;
|
||||||
|
const startPage = Math.max(1, modListCurrentPage - Math.floor(maxButtons / 2));
|
||||||
|
const endPage = Math.min(totalModPages, startPage + maxButtons - 1);
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
createPageButton(i, i.toString());
|
||||||
|
}
|
||||||
|
createPageButton(modListCurrentPage + 1, 'Next', modListCurrentPage === totalModPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeSearch() {
|
function closeSearch() {
|
||||||
elements.modSearch.value = '';
|
elements.modSearch.value = '';
|
||||||
elements.modResults.innerHTML = '';
|
elements.modResults.innerHTML = '';
|
||||||
@ -1029,6 +1044,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
totalResults = 0;
|
totalResults = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearModListSearch() {
|
||||||
|
elements.modListSearch.value = '';
|
||||||
|
modListSearchQuery = '';
|
||||||
|
modListCurrentPage = 1;
|
||||||
|
elements.clearModListSearch.classList.add('hidden');
|
||||||
|
renderModList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce function to limit the rate of search execution
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function searchMods(page = currentPage) {
|
async function searchMods(page = currentPage) {
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
const mod = elements.modSearch.value.trim();
|
const mod = elements.modSearch.value.trim();
|
||||||
@ -1076,6 +1112,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderModList() {
|
||||||
|
const filteredMods = state.allMods.filter(mod =>
|
||||||
|
mod.name.toLowerCase().includes(modListSearchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
const startIndex = (modListCurrentPage - 1) * resultsPerPage;
|
||||||
|
const endIndex = startIndex + resultsPerPage;
|
||||||
|
const paginatedMods = filteredMods.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const modListHtml = paginatedMods.length > 0
|
||||||
|
? paginatedMods.map(mod => `
|
||||||
|
<div class="bg-gray-700 p-4 rounded">
|
||||||
|
<p><strong>${mod.name}</strong> (${mod.version})</p>
|
||||||
|
<p>ID: ${mod.id}</p>
|
||||||
|
<button class="uninstall-mod bg-red-600 hover:bg-red-700 px-2 py-1 rounded mt-2" data-mod-id="${mod.id}">Uninstall</button>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
: '<p class="text-gray-400">No mods found.</p>';
|
||||||
|
|
||||||
|
if (state.modListHtml !== modListHtml && elements.modList) {
|
||||||
|
elements.modList.innerHTML = modListHtml;
|
||||||
|
state.modListHtml = modListHtml;
|
||||||
|
document.querySelectorAll('.uninstall-mod').forEach(button => {
|
||||||
|
button.addEventListener('click', async () => {
|
||||||
|
const modId = button.getAttribute('data-mod-id');
|
||||||
|
const key = `action-uninstall-mod-${modId}`;
|
||||||
|
try {
|
||||||
|
await wsRequest('/uninstall', 'POST', { mod: modId });
|
||||||
|
const response = await wsRequest('/mod-list');
|
||||||
|
updateNonDockerUI({ type: 'mod-list', data: response });
|
||||||
|
showNotification(`Mod ${modId} uninstalled successfully`, 'success', key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Uninstall mod error:', error);
|
||||||
|
showNotification(`Failed to uninstall mod ${modId}: ${error.message}`, 'error', key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModListPagination();
|
||||||
|
}
|
||||||
|
|
||||||
async function sendConsoleCommand() {
|
async function sendConsoleCommand() {
|
||||||
const command = elements.consoleInput.value.trim();
|
const command = elements.consoleInput.value.trim();
|
||||||
if (command) {
|
if (command) {
|
||||||
@ -1330,18 +1407,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const key = `action-update-mods`;
|
const key = `action-update-mods`;
|
||||||
const response = await wsRequest('/update-mods', 'POST');
|
const response = await wsRequest('/update-mods', 'POST');
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
updateNotification(notification, `Failed to update mods: ${response.error}`, 'error', key);
|
|
||||||
elements.updateModsOutput.textContent = `Error: ${response.error}`;
|
elements.updateModsOutput.textContent = `Error: ${response.error}`;
|
||||||
|
showNotification(`Failed to update mods: ${response.error}`, 'error', key);
|
||||||
} else {
|
} else {
|
||||||
const output = response.output || 'No output from mod update.';
|
const output = response.output || 'No output from mod update.';
|
||||||
elements.updateModsOutput.textContent = output;
|
elements.updateModsOutput.textContent = output;
|
||||||
|
showNotification('Mods updated successfully', 'success', key);
|
||||||
}
|
}
|
||||||
elements.updateModsModal.classList.remove('hidden');
|
elements.updateModsModal.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update mods error:', error);
|
console.error('Update mods error:', error);
|
||||||
showNotification(`Failed to update mods: ${error.message}`, 'error', 'update-mods-error');
|
|
||||||
elements.updateModsOutput.textContent = `Error: ${error.message}`;
|
elements.updateModsOutput.textContent = `Error: ${error.message}`;
|
||||||
elements.updateModsModal.classList.remove('hidden');
|
elements.updateModsModal.classList.remove('hidden');
|
||||||
|
showNotification(`Failed to update mods: ${error.message}`, 'error', 'update-mods-error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1390,6 +1468,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const key = `action-my-link`;
|
const key = `action-my-link`;
|
||||||
await wsRequest('/my-link');
|
await wsRequest('/my-link');
|
||||||
|
showNotification('Connection link generated successfully', 'success', key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Generate connection link error:', error);
|
console.error('Generate connection link error:', error);
|
||||||
showNotification(`Failed to generate connection link: ${error.message}`, 'error', 'my-link-error');
|
showNotification(`Failed to generate connection link: ${error.message}`, 'error', 'my-link-error');
|
||||||
@ -1400,6 +1479,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const key = `action-my-geyser-link`;
|
const key = `action-my-geyser-link`;
|
||||||
await wsRequest('/my-geyser-link');
|
await wsRequest('/my-geyser-link');
|
||||||
|
showNotification('Geyser link generated successfully', 'success', key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Generate geyser link error:', error);
|
console.error('Generate geyser link error:', error);
|
||||||
showNotification(`Failed to generate Geyser link: ${error.message}`, 'error', 'geyser-link-error');
|
showNotification(`Failed to generate Geyser link: ${error.message}`, 'error', 'geyser-link-error');
|
||||||
@ -1410,6 +1490,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const key = `action-my-sftp`;
|
const key = `action-my-sftp`;
|
||||||
await wsRequest('/my-sftp');
|
await wsRequest('/my-sftp');
|
||||||
|
showNotification('SFTP link generated successfully', 'success', key);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Generate SFTP link error:', error);
|
console.error('Generate SFTP link error:', error);
|
||||||
showNotification(`Failed to generate SFTP link: ${error.message}`, 'error', 'sftp-link-error');
|
showNotification(`Failed to generate SFTP link: ${error.message}`, 'error', 'sftp-link-error');
|
||||||
@ -1601,6 +1682,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
saveServerProperties();
|
saveServerProperties();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.clearModListSearch.addEventListener('click', clearModListSearch);
|
||||||
|
|
||||||
|
// Debounced search for installed mods
|
||||||
|
const debouncedModListSearch = debounce((query) => {
|
||||||
|
modListSearchQuery = query.trim();
|
||||||
|
modListCurrentPage = 1;
|
||||||
|
elements.clearModListSearch.classList.toggle('hidden', !modListSearchQuery);
|
||||||
|
renderModList();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
elements.modListSearch.addEventListener('input', (e) => {
|
||||||
|
debouncedModListSearch(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
} else {
|
} else {
|
||||||
|
@ -187,7 +187,14 @@
|
|||||||
<div id="modResults" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
|
<div id="modResults" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
|
||||||
<div id="pagination" class="mt-4 flex justify-center space-x-2"></div>
|
<div id="pagination" class="mt-4 flex justify-center space-x-2"></div>
|
||||||
<h3 class="text-lg font-semibold mt-4">Installed Mods</h3>
|
<h3 class="text-lg font-semibold mt-4">Installed Mods</h3>
|
||||||
<div id="modList" class="mt-2"></div>
|
<div class="mb-4 flex space-x-2">
|
||||||
|
<input id="modListSearch" type="text" placeholder="Search Installed Mods"
|
||||||
|
class="bg-gray-700 px-4 py-2 rounded text-white flex-grow">
|
||||||
|
<button id="clearModListSearch" type="button"
|
||||||
|
class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded hidden">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="modList" class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4"></div>
|
||||||
|
<div id="modListPagination" class="mt-4 flex justify-center space-x-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
|
<div class="bg-gray-800 p-6 rounded-lg shadow-lg mb-6">
|
||||||
|
Reference in New Issue
Block a user