(() => { const CatalogManager = require('./assets/js/catalogmanager') const ConfigManager = require('./assets/js/configmanager') const ProfileAssetManager = require('./assets/js/profileassetmanager') const ServerRuntime = require('./assets/js/serverruntime') const { DistroAPI } = require('./assets/js/distromanager') const libraryList = document.getElementById('libraryList') const libraryEmptyState = document.getElementById('libraryEmptyState') function renderLibraryEmptyState(isEmpty){ libraryEmptyState.style.display = isEmpty ? 'flex' : 'none' } function createBadge(text){ const badge = document.createElement('span') badge.className = 'launcherBadge' badge.textContent = text return badge } function describeProfileFeatures(profile){ const features = ['맵'] if(profile.modsEnabled){ features.push('모드') } if(profile.pluginsEnabled){ features.push('플러그인') } if(profile.serverEnabled){ features.push('서버') } return features } function createParagraph(className, text){ const element = document.createElement('p') element.className = className element.textContent = text return element } function showLibraryMessage(title, message){ if(typeof setOverlayContent === 'function'){ setOverlayContent(title, message, '확인') setOverlayHandler(() => toggleOverlay(false)) toggleOverlay(true) } } function isProfileInstalled(profile){ const state = ConfigManager.getLibraryProfileAssetState(profile.id) const mapReady = state.worldInstalledAt != null || state.prefetchedAt != null || profile.worldArchiveUrl == null const serverReady = profile.serverEnabled !== true || profile.hostReady !== true || state.serverInstalledAt != null || state.prefetchedAt != null || profile.serverJarUrl == null return mapReady && serverReady } async function prepareProfileAssets(profile){ try { await ProfileAssetManager.prefetchProfileAssets(profile) if(profile.serverEnabled && profile.hostReady){ await ProfileAssetManager.ensureServerJarInstalled(profile) } await renderLibraryView() showLibraryMessage('자료 준비 완료', `${profile.name} 자료를 준비했습니다.`) } catch (error) { console.error(error) showLibraryMessage('자료 준비 실패', '프로필 자료를 내려받는 중 오류가 발생했습니다.') } } async function applyProfileSelection(profile){ CatalogManager.selectProfile(profile.id) CatalogManager.applyConfiguredProfile() const distro = await DistroAPI.refreshDistributionOrFallback() if(distro == null){ throw new Error('Distribution refresh returned null.') } const currentServer = distro.getServerById(ConfigManager.getSelectedServer()) if(currentServer == null && typeof distro.getMainServer === 'function'){ const mainServer = distro.getMainServer() if(mainServer != null){ ConfigManager.setSelectedServer(mainServer.rawServer.id) ConfigManager.save() } } onDistroRefresh(distro) } function appendAddressOverrideField(profile, container){ if(profile.serverEnabled !== true){ return } const fieldGroup = document.createElement('div') fieldGroup.className = 'launcherFieldGroup' const label = document.createElement('label') label.className = 'launcherFieldLabel' label.textContent = '접속 주소' const input = document.createElement('input') input.className = 'launcherFieldInput' input.type = 'text' input.placeholder = '비워두면 로컬 서버 실행' input.value = ConfigManager.getLibraryServerAddressOverride(profile.id) ?? '' input.addEventListener('change', () => { CatalogManager.setServerAddressOverride(profile.id, input.value) if(typeof refreshServerStatus === 'function'){ refreshServerStatus(true) } }) const help = document.createElement('div') help.className = 'launcherFieldHint' help.textContent = profile.hostReady ? '주소를 비워두면 PLAY 시 로컬 서버를 실행하고, 값을 넣으면 해당 주소로 바로 접속합니다.' : '주소를 비워두면 로컬 서버 실행을 시도합니다. 지금은 버킷 JAR이 없어 직접 실행 준비가 부족합니다.' fieldGroup.appendChild(label) fieldGroup.appendChild(input) fieldGroup.appendChild(help) container.appendChild(fieldGroup) } async function renderLibraryView(){ libraryList.innerHTML = '' try { const installedProfiles = await CatalogManager.getInstalledProfiles() const selectedProfileId = CatalogManager.getSelectedProfileId() renderLibraryEmptyState(installedProfiles.length === 0) for(const profile of installedProfiles){ const hostState = ServerRuntime.getHostedProfileState(profile.id) const card = document.createElement('article') card.className = 'launcherCard' if(profile.id === selectedProfileId){ card.setAttribute('selected', 'true') } const header = document.createElement('div') header.className = 'launcherCardHeader' const titleGroup = document.createElement('div') titleGroup.className = 'launcherCardTitleGroup' const title = document.createElement('h3') title.className = 'launcherCardTitle' title.textContent = profile.name const meta = document.createElement('div') meta.className = 'launcherCardMeta' describeProfileFeatures(profile).forEach((label) => { meta.appendChild(createBadge(label)) }) if(profile.id === selectedProfileId){ meta.appendChild(createBadge('선택됨')) } if(!profile.launchReady){ meta.appendChild(createBadge('실행 준비 필요')) } if(profile.serverEnabled && profile.hostReady){ meta.appendChild(createBadge('로컬 서버 가능')) } if(hostState.running){ meta.appendChild(createBadge(hostState.ready ? '서버 실행 중' : '서버 시작 중')) } titleGroup.appendChild(title) titleGroup.appendChild(meta) header.appendChild(titleGroup) const description = createParagraph('launcherCardDescription', profile.description || '설명이 없습니다.') const extra = document.createElement('div') extra.className = 'launcherCardContent' appendAddressOverrideField(profile, extra) const actions = document.createElement('div') actions.className = 'launcherCardActions' const installButton = document.createElement('button') installButton.className = 'launcherSecondaryButton' installButton.textContent = isProfileInstalled(profile) ? '설치됨' : '설치' installButton.disabled = isProfileInstalled(profile) || !profile.launchReady installButton.addEventListener('click', async () => { await prepareProfileAssets(profile) }) const selectButton = document.createElement('button') selectButton.className = 'launcherSecondaryButton' selectButton.textContent = profile.id === selectedProfileId ? '선택됨' : '선택' selectButton.disabled = profile.id === selectedProfileId selectButton.addEventListener('click', async () => { try { await applyProfileSelection(profile) if(typeof refreshSelectedProfileButton === 'function'){ refreshSelectedProfileButton() } if(typeof refreshServerStatus === 'function'){ refreshServerStatus(true) } await renderLibraryView() } catch (error) { console.error(error) showLibraryMessage('프로필 선택 실패', '선택한 프로필의 배포 정보 또는 서버 정보를 불러오지 못했습니다.') } }) const removeButton = document.createElement('button') removeButton.className = 'launcherGhostButton' removeButton.textContent = '제거' removeButton.addEventListener('click', async () => { await ServerRuntime.stopHostedProfile(profile.id) CatalogManager.removeProfile(profile.id) if(typeof refreshSelectedProfileButton === 'function'){ refreshSelectedProfileButton() } if(typeof refreshServerStatus === 'function'){ refreshServerStatus(true) } await renderLibraryView() if(typeof refreshInstallView === 'function'){ await refreshInstallView() } }) actions.appendChild(installButton) actions.appendChild(selectButton) actions.appendChild(removeButton) card.appendChild(header) card.appendChild(description) if(profile.serverEnabled){ card.appendChild(extra) } card.appendChild(actions) libraryList.appendChild(card) } } catch (error) { console.error(error) renderLibraryEmptyState(false) const errorCard = document.createElement('article') errorCard.className = 'launcherCard' errorCard.innerHTML = '
선택한 카탈로그를 읽지 못했습니다. 관리자 사이트에서 카탈로그 경로를 다시 확인하세요.
' libraryList.appendChild(errorCard) } } document.getElementById('libraryBackButton').addEventListener('click', () => { switchView(getCurrentView(), VIEWS.landing) }) setInterval(() => { if(getCurrentView() === VIEWS.library && ServerRuntime.hasRunningProfiles()){ renderLibraryView() } }, 3000) window.refreshLibraryView = renderLibraryView renderLibraryView() })()