let API_KEY = null; const CURSEFORGE_API = 'https://api.curseforge.com/v1'; const HYTALE_GAME_ID = 70216; let installedMods = []; let browseMods = []; let searchQuery = ''; let modsPage = 0; let modsPageSize = 20; let modsTotalPages = 1; export async function initModsManager() { try { if (window.electronAPI && window.electronAPI.getEnvVar) { API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY'); console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No'); } } catch (err) { console.error('Failed to load API Key:', err); } setupModsEventListeners(); await loadInstalledMods(); await loadBrowseMods(); } function setupModsEventListeners() { const searchInput = document.getElementById('modsSearch'); if (searchInput) { let searchTimeout; searchInput.addEventListener('input', (e) => { searchQuery = e.target.value.toLowerCase().trim(); clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { modsPage = 0; loadBrowseMods(); }, 500); }); } const myModsBtn = document.getElementById('myModsBtn'); if (myModsBtn) { myModsBtn.addEventListener('click', openMyModsModal); } const closeModalBtn = document.getElementById('closeMyModsModal'); if (closeModalBtn) { closeModalBtn.addEventListener('click', closeMyModsModal); } const modal = document.getElementById('myModsModal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { closeMyModsModal(); } }); } const prevPageBtn = document.getElementById('prevPage'); const nextPageBtn = document.getElementById('nextPage'); if (prevPageBtn) { prevPageBtn.addEventListener('click', () => { if (modsPage > 0) { modsPage--; loadBrowseMods(); } }); } if (nextPageBtn) { nextPageBtn.addEventListener('click', () => { if (modsPage < modsTotalPages - 1) { modsPage++; loadBrowseMods(); } }); } } function openMyModsModal() { const modal = document.getElementById('myModsModal'); if (modal) { modal.classList.add('active'); loadInstalledMods(); } } function closeMyModsModal() { const modal = document.getElementById('myModsModal'); if (modal) { modal.classList.remove('active'); } } async function loadInstalledMods() { try { const modsPath = await window.electronAPI?.getModsPath(); if (!modsPath) { showInstalledModsError('Could not get mods directory'); return; } const mods = await window.electronAPI?.loadInstalledMods(modsPath); installedMods = mods || []; displayInstalledMods(installedMods); } catch (error) { console.error('Error loading installed mods:', error); showInstalledModsError('Failed to load installed mods'); } } function displayInstalledMods(mods) { const modsContainer = document.getElementById('installedModsList'); if (!modsContainer) return; if (mods.length === 0) { modsContainer.innerHTML = `

No Mods Installed

Add mods from CurseForge or import local files

`; if (window.i18n) { const container = modsContainer.querySelector('.empty-installed-mods'); container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled'); container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc'); } return; } modsContainer.innerHTML = mods.map(mod => createInstalledModCard(mod)).join(''); mods.forEach(mod => { const toggleBtn = document.getElementById(`toggle-installed-${mod.id}`); const deleteBtn = document.getElementById(`delete-installed-${mod.id}`); if (toggleBtn) { toggleBtn.addEventListener('click', () => toggleMod(mod.id)); } if (deleteBtn) { deleteBtn.addEventListener('click', () => deleteMod(mod.id)); } }); } function createInstalledModCard(mod) { const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500'; const statusText = mod.enabled ? (window.i18n ? window.i18n.t('mods.active') : 'ACTIVE') : (window.i18n ? window.i18n.t('mods.disabled') : 'DISABLED'); const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable'; const toggleBtnText = mod.enabled ? (window.i18n ? window.i18n.t('mods.disable') : 'DISABLE') : (window.i18n ? window.i18n.t('mods.enable') : 'ENABLE'); const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play'; return `

${mod.name}

v${mod.version}

${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}

${statusText}
`; } async function loadBrowseMods() { const browseContainer = document.getElementById('browseModsList'); if (!browseContainer) return; browseContainer.innerHTML = `
${window.i18n ? window.i18n.t('mods.loadingMods') : 'Loading mods from CurseForge...'}
`; try { if (!API_KEY || API_KEY.length < 10) { browseContainer.innerHTML = `

API Key Required

CurseForge API key is needed to browse mods

`; return; } const offset = modsPage * modsPageSize; let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`; if (searchQuery && searchQuery.length > 0) { url += `&searchFilter=${encodeURIComponent(searchQuery)}`; } console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url); const response = await fetch(url, { headers: { 'x-api-key': API_KEY, 'Accept': 'application/json' } }); console.log('Response status:', response.status); if (!response.ok) { const errorText = await response.text(); console.error('API Error Response:', errorText); throw new Error(`CurseForge API error: ${response.status} - ${errorText}`); } const data = await response.json(); console.log('API Response data:', data); console.log('Total mods found:', data.data?.length || 0); browseMods = (data.data || []).map(mod => ({ id: mod.id.toString(), name: mod.name, slug: mod.slug, summary: mod.summary || 'No description available', downloadCount: mod.downloadCount || 0, author: mod.authors?.[0]?.name || 'Unknown', version: mod.latestFiles?.[0]?.displayName || 'Unknown', thumbnailUrl: mod.logo?.thumbnailUrl || null, websiteUrl: mod.links?.websiteUrl || null, modId: mod.id, fileId: mod.latestFiles?.[0]?.id, fileName: mod.latestFiles?.[0]?.fileName, downloadUrl: mod.latestFiles?.[0]?.downloadUrl })); console.log('Processed mods:', browseMods.length); modsTotalPages = Math.ceil((data.pagination?.totalCount || 1) / modsPageSize); displayBrowseMods(browseMods); updatePagination(); } catch (error) { console.error('Error loading browse mods:', error); browseContainer.innerHTML = `

API Error

Failed to load mods from CurseForge

${error.message}
`; } } function displayBrowseMods(mods) { const browseContainer = document.getElementById('browseModsList'); if (!browseContainer) return; if (mods.length === 0) { browseContainer.innerHTML = `

No Mods Found

Try adjusting your search

`; if (window.i18n) { const container = browseContainer.querySelector('.empty-browse-mods'); container.querySelector('h4').textContent = window.i18n.t('mods.noModsFound'); container.querySelector('p').textContent = window.i18n.t('mods.noModsFoundDesc'); } return; } browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join(''); mods.forEach(mod => { const installBtn = document.getElementById(`install-${mod.id}`); if (installBtn) { installBtn.addEventListener('click', () => downloadAndInstallMod(mod)); } }); } function createBrowseModCard(mod) { const isInstalled = installedMods.some(installed => { // Check by CurseForge ID (most reliable) if (installed.curseForgeId && installed.curseForgeId.toString() === mod.id.toString()) { return true; } // Check by exact name match for manually installed mods if (installed.name.toLowerCase() === mod.name.toLowerCase()) { return true; } return false; }); return `
${mod.thumbnailUrl ? `\"${mod.name}\"'\">` : `` }

${mod.name}

${mod.version}

${mod.summary}

${mod.author} ${formatNumber(mod.downloadCount)}
${!isInstalled ? `` : `` }
`; } async function downloadAndInstallMod(modInfo) { try { const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`; window.LauncherUI?.showProgress(downloadMsg); const result = await window.electronAPI?.downloadMod(modInfo); if (result?.success) { const newMod = { id: result.modInfo.id, name: modInfo.name, version: modInfo.version, description: modInfo.summary, author: modInfo.author, enabled: true, fileName: result.fileName, fileSize: result.modInfo.fileSize, dateInstalled: new Date().toISOString(), curseForgeId: modInfo.modId, curseForgeFileId: modInfo.fileId }; installedMods.push(newMod); await loadInstalledMods(); await loadBrowseMods(); window.LauncherUI?.hideProgress(); const successMsg = window.i18n ? window.i18n.t('notifications.modsInstalledSuccess').replace('{name}', modInfo.name) : `${modInfo.name} installed successfully! 🎉`; showNotification(successMsg, 'success'); } else { throw new Error(result?.error || 'Failed to download mod'); } } catch (error) { console.error('Error downloading mod:', error); window.LauncherUI?.hideProgress(); const errorMsg = window.i18n ? window.i18n.t('notifications.modsDownloadFailed').replace('{error}', error.message) : 'Failed to download mod: ' + error.message; showNotification(errorMsg, 'error'); } } async function toggleMod(modId) { try { const toggleMsg = window.i18n ? window.i18n.t('notifications.modsTogglingMod') : 'Toggling mod...'; window.LauncherUI?.showProgress(toggleMsg); const modsPath = await window.electronAPI?.getModsPath(); const result = await window.electronAPI?.toggleMod(modId, modsPath); if (result?.success) { await loadInstalledMods(); window.LauncherUI?.hideProgress(); } else { throw new Error(result?.error || 'Failed to toggle mod'); } } catch (error) { console.error('Error toggling mod:', error); window.LauncherUI?.hideProgress(); const errorMsg = window.i18n ? window.i18n.t('notifications.modsToggleFailed').replace('{error}', error.message) : 'Failed to toggle mod: ' + error.message; showNotification(errorMsg, 'error'); } } async function deleteMod(modId) { const mod = installedMods.find(m => m.id === modId); if (!mod) return; const confirmMsg = window.i18n ? window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') : `Are you sure you want to delete "${mod.name}"? This action cannot be undone.`; showConfirmModal( confirmMsg, async () => { try { const deleteMsg = window.i18n ? window.i18n.t('notifications.modsDeletingMod') : 'Deleting mod...'; window.LauncherUI?.showProgress(deleteMsg); const modsPath = await window.electronAPI?.getModsPath(); const result = await window.electronAPI?.uninstallMod(modId, modsPath); if (result?.success) { await loadInstalledMods(); await loadBrowseMods(); window.LauncherUI?.hideProgress(); const successMsg = window.i18n ? window.i18n.t('notifications.modsDeletedSuccess').replace('{name}', mod.name) : `"${mod.name}" deleted successfully`; showNotification(successMsg, 'success'); } else { throw new Error(result?.error || 'Failed to delete mod'); } } catch (error) { console.error('Error deleting mod:', error); window.LauncherUI?.hideProgress(); const errorMsg = window.i18n ? window.i18n.t('notifications.modsDeleteFailed').replace('{error}', error.message) : 'Failed to delete mod: ' + error.message; showNotification(errorMsg, 'error'); } } ); } function formatNumber(num) { if (!num) return '0'; if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num.toString(); } function showNotification(message, type = 'info', duration = 4000) { const existing = document.querySelector(`.mod-notification.${type}`); if (existing) { existing.remove(); } const notification = document.createElement('div'); notification.className = `mod-notification ${type}`; const icons = { success: 'fa-check-circle', error: 'fa-exclamation-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' }; const colors = { success: '#10b981', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b' }; notification.innerHTML = `
${message}
`; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${colors[type]}; color: white; padding: 16px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 10000; min-width: 300px; max-width: 400px; transform: translateX(100%); transition: transform 0.3s ease; display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 500; `; const contentStyle = ` display: flex; align-items: center; gap: 10px; flex: 1; `; const closeStyle = ` background: none; border: none; color: white; cursor: pointer; padding: 4px; border-radius: 4px; opacity: 0.8; transition: opacity 0.2s; margin-left: 10px; `; notification.querySelector('.notification-content').style.cssText = contentStyle; notification.querySelector('.notification-close').style.cssText = closeStyle; document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 10); // Auto remove setTimeout(() => { if (notification.parentElement) { notification.style.transform = 'translateX(100%)'; setTimeout(() => { notification.remove(); }, 300); } }, duration); } function showConfirmModal(message, onConfirm, onCancel = null) { const existingModal = document.querySelector('.mod-confirm-modal'); if (existingModal) { existingModal.remove(); } const modal = document.createElement('div'); modal.className = 'mod-confirm-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(4px); z-index: 20000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; `; const dialog = document.createElement('div'); dialog.className = 'mod-confirm-dialog'; dialog.style.cssText = ` background: #1f2937; border-radius: 12px; padding: 0; min-width: 400px; max-width: 500px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6); border: 1px solid rgba(239, 68, 68, 0.3); transform: scale(0.9); transition: transform 0.3s ease; `; dialog.innerHTML = `

${window.i18n ? window.i18n.t('mods.confirmDeletion') : 'Confirm Deletion'}

${message}

`; modal.appendChild(dialog); document.body.appendChild(modal); // Animate in setTimeout(() => { modal.style.opacity = '1'; dialog.style.transform = 'scale(1)'; }, 10); // Event handlers const cancelBtn = dialog.querySelector('.mod-confirm-cancel'); const deleteBtn = dialog.querySelector('.mod-confirm-delete'); const closeModal = () => { modal.style.opacity = '0'; dialog.style.transform = 'scale(0.9)'; setTimeout(() => { modal.remove(); }, 300); }; cancelBtn.onclick = () => { closeModal(); if (onCancel) onCancel(); }; deleteBtn.onclick = () => { closeModal(); onConfirm(); }; modal.onclick = (e) => { if (e.target === modal) { closeModal(); if (onCancel) onCancel(); } }; // Escape key const handleEscape = (e) => { if (e.key === 'Escape') { closeModal(); if (onCancel) onCancel(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); } function updatePagination() { const currentPageEl = document.getElementById('currentPage'); const totalPagesEl = document.getElementById('totalPages'); const prevBtn = document.getElementById('prevPage'); const nextBtn = document.getElementById('nextPage'); if (currentPageEl) currentPageEl.textContent = modsPage + 1; if (totalPagesEl) totalPagesEl.textContent = modsTotalPages; if (prevBtn) { prevBtn.disabled = modsPage === 0; prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1'; prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer'; } if (nextBtn) { nextBtn.disabled = modsPage >= modsTotalPages - 1; nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1'; nextBtn.style.cursor = modsPage >= modsTotalPages - 1 ? 'not-allowed' : 'pointer'; } } function showInstalledModsError(message) { const modsContainer = document.getElementById('installedModsList'); if (!modsContainer) return; modsContainer.innerHTML = `

Error

${message}

`; } function viewModPage(modId) { console.log('Looking for mod with ID:', modId, 'Type:', typeof modId); console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id }))); const mod = browseMods.find(m => m.id.toString() === modId.toString()); if (mod) { console.log('Found mod:', mod.name); let modUrl; if (mod.websiteUrl && mod.websiteUrl.includes('curseforge.com')) { modUrl = mod.websiteUrl; } else if (mod.slug) { modUrl = `https://www.curseforge.com/hytale/mods/${mod.slug}`; } else { const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`; } console.log('Opening URL:', modUrl); if (window.electronAPI && window.electronAPI.openExternalLink) { window.electronAPI.openExternalLink(modUrl); } else { if (window.electronAPI && window.electronAPI.shell) { window.electronAPI.shell.openExternal(modUrl); } else { window.open(modUrl, '_blank'); } } } else { console.error('Mod not found with ID:', modId); const errorMsg = window.i18n ? window.i18n.t('notifications.modsModNotFound') : 'Mod information not found'; showNotification(errorMsg, 'error'); } } window.modsManager = { toggleMod, deleteMod, openMyModsModal, closeMyModsModal, viewModPage, loadInstalledMods, loadBrowseMods }; document.addEventListener('DOMContentLoaded', initModsManager);