diff --git a/public/app.js b/public/app.js index c23a093..068c2aa 100644 --- a/public/app.js +++ b/public/app.js @@ -10,6 +10,8 @@ document.addEventListener('DOMContentLoaded', () => { let currentPage = 1; let totalResults = 0; const resultsPerPage = 10; + let modListCurrentPage = 1; + let modListSearchQuery = ''; let allProperties = {}; const filteredSettings = [ 'bug-report-link', @@ -79,7 +81,10 @@ document.addEventListener('DOMContentLoaded', () => { connectionStatus: document.getElementById('connectionStatus'), geyserStatus: document.getElementById('geyserStatus'), 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 = { @@ -162,11 +167,11 @@ document.addEventListener('DOMContentLoaded', () => { connectionStatus: '', geyserStatus: '', sftpStatus: '', - activeNotifications: new Map() // Track active notifications to prevent duplicates + activeNotifications: new Map(), + allMods: [] }; function showNotification(message, type = 'loading', key = null) { - // If a notification for this key exists, remove it if (key && state.activeNotifications.has(key)) { const existing = state.activeNotifications.get(key); existing.notification.style.opacity = '0'; @@ -200,7 +205,6 @@ document.addEventListener('DOMContentLoaded', () => { } 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) { const existing = state.activeNotifications.get(key); existing.notification.style.opacity = '0'; @@ -407,7 +411,7 @@ document.addEventListener('DOMContentLoaded', () => { } const requestId = crypto.randomUUID(); 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); pendingRequests.set(requestId, { resolve, reject, notification, key }); 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) { - const modListHtml = message.data.mods.map(mod => ` -
-

${mod.name} (${mod.version})

-

ID: ${mod.id}

- -
- `).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); - } - }); - }); - } + state.allMods = message.data.mods || []; + renderModList(); } 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() { elements.modSearch.value = ''; elements.modResults.innerHTML = ''; @@ -1029,6 +1044,27 @@ document.addEventListener('DOMContentLoaded', () => { 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) { currentPage = page; 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 => ` +
+

${mod.name} (${mod.version})

+

ID: ${mod.id}

+ +
+ `).join('') + : '

No mods found.

'; + + 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() { const command = elements.consoleInput.value.trim(); if (command) { @@ -1330,18 +1407,19 @@ document.addEventListener('DOMContentLoaded', () => { const key = `action-update-mods`; const response = await wsRequest('/update-mods', 'POST'); if (response.error) { - updateNotification(notification, `Failed to update mods: ${response.error}`, 'error', key); elements.updateModsOutput.textContent = `Error: ${response.error}`; + showNotification(`Failed to update mods: ${response.error}`, 'error', key); } else { const output = response.output || 'No output from mod update.'; elements.updateModsOutput.textContent = output; + showNotification('Mods updated successfully', 'success', key); } elements.updateModsModal.classList.remove('hidden'); } catch (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.updateModsModal.classList.remove('hidden'); + showNotification(`Failed to update mods: ${error.message}`, 'error', 'update-mods-error'); } } @@ -1390,6 +1468,7 @@ document.addEventListener('DOMContentLoaded', () => { try { const key = `action-my-link`; await wsRequest('/my-link'); + showNotification('Connection link generated successfully', 'success', key); } catch (error) { console.error('Generate connection link error:', error); showNotification(`Failed to generate connection link: ${error.message}`, 'error', 'my-link-error'); @@ -1400,6 +1479,7 @@ document.addEventListener('DOMContentLoaded', () => { try { const key = `action-my-geyser-link`; await wsRequest('/my-geyser-link'); + showNotification('Geyser link generated successfully', 'success', key); } catch (error) { console.error('Generate geyser link error:', error); showNotification(`Failed to generate Geyser link: ${error.message}`, 'error', 'geyser-link-error'); @@ -1410,6 +1490,7 @@ document.addEventListener('DOMContentLoaded', () => { try { const key = `action-my-sftp`; await wsRequest('/my-sftp'); + showNotification('SFTP link generated successfully', 'success', key); } catch (error) { console.error('Generate SFTP link error:', error); showNotification(`Failed to generate SFTP link: ${error.message}`, 'error', 'sftp-link-error'); @@ -1601,6 +1682,20 @@ document.addEventListener('DOMContentLoaded', () => { 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) { connectWebSocket(); } else { diff --git a/public/index.html b/public/index.html index 3a06dc0..d7c9bae 100644 --- a/public/index.html +++ b/public/index.html @@ -187,7 +187,14 @@

Installed Mods

-
+
+ + +
+
+