diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index e338313..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: dscalzi -patreon: dscalzi -custom: ['https://www.paypal.me/dscalzi'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 93be13c..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build - -on: push - -jobs: - release: - runs-on: ${{ matrix.os }} - - permissions: - contents: write - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - steps: - - name: Check out Git repository - uses: actions/checkout@v6 - - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: 22 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - name: Install Dependencies - run: npm ci - shell: bash - - - name: Build - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm run dist - shell: bash \ No newline at end of file diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml deleted file mode 100644 index 7bc9dd5..0000000 --- a/.github/workflows/windows-smoke.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Windows Smoke Test - -on: push - -jobs: - windows-smoke: - runs-on: windows-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v6 - - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: 22 - - - name: Install Dependencies - run: npm ci - - - name: Run Windows Smoke Test - run: npm run smoke:win diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ddd8d8e..0000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules/ -/.vs/ -/.vscode/ -/target/ -/logs/ -/dist/ -admin/data/uploads/* -!admin/data/uploads/.gitkeep diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 8fdd954..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 5b5f595..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2017-2026 Daniel D. Scalzi -Copyright (c) 2024 peunsu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 790b98e..45a08e7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,4 @@ # Minecraft Launcher - -Electron 기반 커스텀 마인크래프트 런처입니다. `MRSLauncher`를 최신 소스로 가져온 뒤, 설치 페이지 + 라이브러리 구조와 관리자 사이트를 붙였습니다. - -## 현재 구조 - -- 프로필은 `맵`을 기본으로 두고 `모드`, `플러그인`, `서버` 기능을 조합합니다. -- 설치 페이지는 관리자 등록 프로필을 읽기 전용으로 보여줍니다. -- 라이브러리에서는 프로필 설치, 선택, 제거만 하고, 서버 프로필은 접속 주소를 직접 입력할 수 있습니다. -- `PLAY`를 누르면: - - 서버 기능이 없는 프로필은 맵을 싱글플레이로 실행 - - 서버 기능이 있고 접속 주소가 있으면 해당 주소로 접속 - - 서버 기능이 있고 접속 주소가 없으면 로컬 서버를 먼저 실행한 뒤 `localhost`로 접속 -- 메인 화면 왼쪽 아래는 선택된 서버 프로필의 자동 포트 개방 상태를 표시합니다. - -## 자동 포트 개방 - -- 현재 구현은 `UPnP + Windows 방화벽` 기준입니다. -- 성공하면 자동 개방 상태를 표시합니다. -- 이미 열려 있으면 기존 포트를 그대로 사용합니다. - 실패하면 `직접 포트포워딩 해주세요` 안내를 표시합니다. - 접속 주소를 직접 입력한 경우에는 자동 포트 개방을 건너뜁니다. diff --git a/admin/data/catalog.json b/admin/data/catalog.json deleted file mode 100644 index e8a0f7e..0000000 --- a/admin/data/catalog.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": 1, - "profiles": [ - { - "id": "template-map-base", - "name": "Map Base Template", - "description": "맵만 사용하는 기본 프로필 예시입니다.", - "details": "맵 기반 기본 프로필입니다. 월드 ZIP과 distribution 파일만 있으면 싱글플레이 실행 흐름으로 사용할 수 있습니다.", - "modsEnabled": false, - "pluginsEnabled": false, - "serverEnabled": false, - "distributionUrl": "admin/data/distributions/template-map-base.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/map-base.zip", - "worldDirectoryName": "Map Base" - }, - { - "id": "template-map-mods", - "name": "Map + Mods Template", - "description": "맵과 모드를 함께 쓰는 프로필 예시입니다.", - "details": "맵 기반에 모드 구성이 포함된 프로필입니다. distribution 파일은 모드가 포함된 클라이언트용으로 준비하면 됩니다.", - "modsEnabled": true, - "pluginsEnabled": false, - "serverEnabled": false, - "distributionUrl": "admin/data/distributions/template-map-mods.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/map-mods.zip", - "worldDirectoryName": "Map Mods" - }, - { - "id": "template-map-plugin-server", - "name": "Map + Plugin Server Template", - "description": "맵, 플러그인, 서버를 함께 쓰는 프로필 예시입니다.", - "details": "플러그인을 켜면 서버도 같이 사용합니다. 주소를 비우면 로컬 서버를 띄우고, 주소를 입력하면 해당 서버로 바로 접속하는 흐름에 맞춘 예시입니다.", - "modsEnabled": false, - "pluginsEnabled": true, - "serverEnabled": true, - "distributionUrl": "admin/data/distributions/template-map-plugin-server.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/plugin-map.zip", - "worldDirectoryName": "Plugin Map", - "serverJarUrl": "https://example.com/server/paper.jar", - "serverPort": 25565, - "serverMemoryMb": 4096, - "serverMaxPlayers": 20, - "serverWhitelistEnabled": false - } - ] -} diff --git a/admin/data/distributions/.gitkeep b/admin/data/distributions/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/admin/data/distributions/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/admin/data/distributions/template-map-base.distribution.json b/admin/data/distributions/template-map-base.distribution.json deleted file mode 100644 index 74f60ad..0000000 --- a/admin/data/distributions/template-map-base.distribution.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "rss": "", - "servers": [ - { - "id": "template-map-base", - "name": "Map Base Template", - "description": "맵만 사용하는 기본 프로필용 샘플 distribution입니다.", - "version": "1.20.1", - "minecraftVersion": "1.20.1", - "address": "127.0.0.1:25565", - "mainServer": true, - "autoconnect": false, - "modules": [] - } - ] -} diff --git a/admin/data/distributions/template-map-mods.distribution.json b/admin/data/distributions/template-map-mods.distribution.json deleted file mode 100644 index d1a5e98..0000000 --- a/admin/data/distributions/template-map-mods.distribution.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "rss": "", - "servers": [ - { - "id": "template-map-mods", - "name": "Map + Mods Template", - "description": "맵과 모드를 함께 쓰는 프로필용 샘플 distribution입니다.", - "version": "1.20.1", - "minecraftVersion": "1.20.1", - "address": "127.0.0.1:25565", - "mainServer": true, - "autoconnect": false, - "modules": [] - } - ] -} diff --git a/admin/data/distributions/template-map-plugin-server.distribution.json b/admin/data/distributions/template-map-plugin-server.distribution.json deleted file mode 100644 index 02e227e..0000000 --- a/admin/data/distributions/template-map-plugin-server.distribution.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "rss": "", - "servers": [ - { - "id": "template-map-plugin-server", - "name": "Map + Plugin Server Template", - "description": "맵, 플러그인, 서버를 함께 쓰는 프로필용 샘플 distribution입니다.", - "version": "1.20.1", - "minecraftVersion": "1.20.1", - "address": "127.0.0.1:25565", - "mainServer": true, - "autoconnect": false, - "modules": [] - } - ] -} diff --git a/admin/data/uploads/.gitkeep b/admin/data/uploads/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/admin/data/uploads/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/admin/public/app.js b/admin/public/app.js deleted file mode 100644 index 41ab077..0000000 --- a/admin/public/app.js +++ /dev/null @@ -1,833 +0,0 @@ -const state = { - catalog: { - version: 1, - profiles: [] - }, - meta: null, - selectedProfileId: null, - dirty: false, - distributionEditor: { - document: null - } -} - -const profileList = document.getElementById('profileList') -const profileCount = document.getElementById('profileCount') -const localCatalogUrl = document.getElementById('localCatalogUrl') -const launcherCatalogPath = document.getElementById('launcherCatalogPath') -const emptyState = document.getElementById('emptyState') -const profileEditorForm = document.getElementById('profileEditorForm') -const statusBanner = document.getElementById('statusBanner') -const editorHint = document.getElementById('editorHint') -const saveCatalogButton = document.getElementById('saveCatalogButton') -const duplicateProfileButton = document.getElementById('duplicateProfileButton') -const deleteProfileButton = document.getElementById('deleteProfileButton') -const addProfileButton = document.getElementById('addProfileButton') -const serverSection = document.getElementById('serverSection') -const editDistributionButton = document.getElementById('editDistributionButton') -const createDistributionButton = document.getElementById('createDistributionButton') -const distributionEditorModal = document.getElementById('distributionEditorModal') -const distributionEditorHint = document.getElementById('distributionEditorHint') -const distributionEditorStatus = document.getElementById('distributionEditorStatus') -const distributionEditorForm = document.getElementById('distributionEditorForm') -const distributionModuleCount = document.getElementById('distributionModuleCount') -const distributionAdditionalServerCount = document.getElementById('distributionAdditionalServerCount') -const closeDistributionEditorButton = document.getElementById('closeDistributionEditorButton') -const loadDistributionTemplateButton = document.getElementById('loadDistributionTemplateButton') -const saveDistributionFileButton = document.getElementById('saveDistributionFileButton') - -const fieldElements = { - id: document.getElementById('field-id'), - name: document.getElementById('field-name'), - description: document.getElementById('field-description'), - details: document.getElementById('field-details'), - modsEnabled: document.getElementById('field-modsEnabled'), - pluginsEnabled: document.getElementById('field-pluginsEnabled'), - serverEnabled: document.getElementById('field-serverEnabled'), - distributionUrl: document.getElementById('field-distributionUrl'), - worldArchiveUrl: document.getElementById('field-worldArchiveUrl'), - worldDirectoryName: document.getElementById('field-worldDirectoryName'), - serverJarUrl: document.getElementById('field-serverJarUrl'), - serverPort: document.getElementById('field-serverPort'), - serverMemoryMb: document.getElementById('field-serverMemoryMb'), - serverMaxPlayers: document.getElementById('field-serverMaxPlayers'), - serverWhitelistEnabled: document.getElementById('field-serverWhitelistEnabled') -} - -const distributionFieldElements = { - version: document.getElementById('distribution-field-version'), - rss: document.getElementById('distribution-field-rss'), - serverId: document.getElementById('distribution-field-serverId'), - serverName: document.getElementById('distribution-field-serverName'), - serverDescription: document.getElementById('distribution-field-serverDescription'), - serverVersion: document.getElementById('distribution-field-serverVersion'), - minecraftVersion: document.getElementById('distribution-field-minecraftVersion'), - mainServer: document.getElementById('distribution-field-mainServer'), - autoconnect: document.getElementById('distribution-field-autoconnect') -} - -function slugify(value){ - return String(value ?? '') - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') -} - -function isRemoteUrl(value){ - return /^https?:\/\//i.test(String(value ?? '').trim()) -} - -function deepClone(value){ - return value == null ? value : JSON.parse(JSON.stringify(value)) -} - -function getDistributionTemplateProfileDefaults(profile){ - const safeId = slugify(profile?.id || profile?.name || 'example-profile') || 'example-profile' - const fallbackPort = Number.isFinite(Number(profile?.serverPort)) ? Number(profile.serverPort) : 25565 - return { - version: '1.0.0', - rss: '', - servers: [ - { - id: safeId, - name: profile?.name || '새 프로필', - description: profile?.description || '관리자 사이트에서 만든 distribution 템플릿입니다.', - version: '1.0.0', - minecraftVersion: '1.20.1', - address: `127.0.0.1:${fallbackPort}`, - mainServer: true, - autoconnect: false, - modules: [] - } - ] - } -} - -function normalizeDistributionDocument(rawDocument, profile){ - const baseDocument = deepClone(rawDocument) ?? {} - const defaultDocument = getDistributionTemplateProfileDefaults(profile) - const servers = Array.isArray(baseDocument.servers) && baseDocument.servers.length > 0 - ? baseDocument.servers - : defaultDocument.servers - const primaryServer = { - ...defaultDocument.servers[0], - ...(servers[0] ?? {}) - } - - baseDocument.version = typeof baseDocument.version === 'string' && baseDocument.version.trim().length > 0 - ? baseDocument.version.trim() - : defaultDocument.version - baseDocument.rss = typeof baseDocument.rss === 'string' ? baseDocument.rss.trim() : '' - baseDocument.servers = [primaryServer, ...servers.slice(1)] - - return baseDocument -} - -function populateDistributionEditorForm(documentData, profile){ - const normalized = normalizeDistributionDocument(documentData, profile) - const primaryServer = normalized.servers[0] - - state.distributionEditor.document = normalized - distributionFieldElements.version.value = normalized.version ?? '' - distributionFieldElements.rss.value = normalized.rss ?? '' - distributionFieldElements.serverId.value = primaryServer.id ?? '' - distributionFieldElements.serverName.value = primaryServer.name ?? '' - distributionFieldElements.serverDescription.value = primaryServer.description ?? '' - distributionFieldElements.serverVersion.value = primaryServer.version ?? '' - distributionFieldElements.minecraftVersion.value = primaryServer.minecraftVersion ?? '' - distributionFieldElements.mainServer.checked = primaryServer.mainServer !== false - distributionFieldElements.autoconnect.checked = primaryServer.autoconnect === true - - const moduleCount = Array.isArray(primaryServer.modules) ? primaryServer.modules.length : 0 - const additionalServerCount = Math.max(0, normalized.servers.length - 1) - distributionModuleCount.textContent = `${moduleCount}개` - distributionAdditionalServerCount.textContent = `${additionalServerCount}개` -} - -function buildDistributionDocumentFromForm(profile){ - const baseDocument = normalizeDistributionDocument(state.distributionEditor.document, profile) - const primaryServer = { - ...baseDocument.servers[0] - } - const fallbackPort = Number.isFinite(Number(profile?.serverPort)) ? Number(profile.serverPort) : 25565 - - baseDocument.version = distributionFieldElements.version.value.trim() || '1.0.0' - baseDocument.rss = distributionFieldElements.rss.value.trim() - - primaryServer.id = distributionFieldElements.serverId.value.trim() || slugify(profile?.id || profile?.name || 'example-profile') || 'example-profile' - primaryServer.name = distributionFieldElements.serverName.value.trim() || profile?.name || '새 프로필' - primaryServer.description = distributionFieldElements.serverDescription.value.trim() - primaryServer.version = distributionFieldElements.serverVersion.value.trim() || '1.0.0' - primaryServer.minecraftVersion = distributionFieldElements.minecraftVersion.value.trim() || '1.20.1' - primaryServer.address = typeof primaryServer.address === 'string' && primaryServer.address.trim().length > 0 - ? primaryServer.address.trim() - : `127.0.0.1:${fallbackPort}` - primaryServer.mainServer = distributionFieldElements.mainServer.checked - primaryServer.autoconnect = distributionFieldElements.autoconnect.checked - primaryServer.modules = Array.isArray(primaryServer.modules) ? primaryServer.modules : [] - - baseDocument.servers = [primaryServer, ...baseDocument.servers.slice(1)] - state.distributionEditor.document = baseDocument - return baseDocument -} - -function createProfile(){ - const timestamp = Date.now() - return { - id: `profile-${timestamp}`, - name: '새 프로필', - description: '', - details: '', - modsEnabled: false, - pluginsEnabled: false, - serverEnabled: false, - distributionUrl: '', - worldArchiveUrl: '', - worldDirectoryName: '', - serverJarUrl: '', - serverPort: 25565, - serverMemoryMb: 4096, - serverMaxPlayers: 20, - serverWhitelistEnabled: false - } -} - -function getSelectedProfile(){ - return state.catalog.profiles.find((profile) => profile.id === state.selectedProfileId) ?? null -} - -function markDirty(nextDirty = true){ - state.dirty = nextDirty - saveCatalogButton.textContent = nextDirty ? '카탈로그 저장 *' : '카탈로그 저장' -} - -function showStatus(message, tone = 'info'){ - statusBanner.hidden = false - statusBanner.dataset.tone = tone - statusBanner.textContent = message -} - -function clearStatus(){ - statusBanner.hidden = true - statusBanner.textContent = '' - delete statusBanner.dataset.tone -} - -function showDistributionEditorStatus(message, tone = 'info'){ - if(distributionEditorStatus == null){ - return - } - distributionEditorStatus.hidden = false - distributionEditorStatus.dataset.tone = tone - distributionEditorStatus.textContent = message -} - -function clearDistributionEditorStatus(){ - if(distributionEditorStatus == null){ - return - } - distributionEditorStatus.hidden = true - distributionEditorStatus.textContent = '' - delete distributionEditorStatus.dataset.tone -} - -function selectProfile(profileId){ - state.selectedProfileId = profileId - renderSidebar() - populateEditor() -} - -function describeProfileFeatures(profile){ - const badges = ['맵'] - if(profile.modsEnabled){ - badges.push('모드') - } - if(profile.pluginsEnabled){ - badges.push('플러그인') - } - if(profile.serverEnabled){ - badges.push('서버') - } - return badges -} - -function renderSidebar(){ - profileList.innerHTML = '' - profileCount.textContent = `${state.catalog.profiles.length}개` - - for(const profile of state.catalog.profiles){ - const button = document.createElement('button') - button.type = 'button' - button.className = 'profileListItem' - if(profile.id === state.selectedProfileId){ - button.setAttribute('selected', 'true') - } - - const title = document.createElement('div') - title.className = 'profileListName' - title.textContent = profile.name || profile.id - - const meta = document.createElement('div') - meta.className = 'profileListMeta' - meta.innerHTML = describeProfileFeatures(profile) - .map((label) => `${label}`) - .join('') - - const description = document.createElement('div') - description.className = 'profileListDescription' - description.textContent = profile.description || '설명이 없습니다.' - - button.appendChild(title) - button.appendChild(meta) - button.appendChild(description) - button.addEventListener('click', () => { - selectProfile(profile.id) - }) - - profileList.appendChild(button) - } -} - -function syncFeatureDependencies(profile, showMessage = false){ - if(profile.pluginsEnabled && !profile.serverEnabled){ - profile.serverEnabled = true - if(showMessage){ - showStatus('플러그인 사용을 켜면 서버 사용도 자동으로 같이 켜집니다.', 'info') - } - } -} - -function syncServerSection(profile){ - serverSection.hidden = !profile?.serverEnabled - fieldElements.serverEnabled.disabled = profile?.pluginsEnabled === true -} - -function updateDistributionEditorHint(profile, pathOverride){ - if(!profile){ - distributionEditorHint.textContent = '프로필에 연결할 distribution.json 내용을 사이트 안에서 직접 관리합니다.' - return - } - - const currentPath = String(pathOverride ?? profile.distributionUrl ?? '').trim() - - if(currentPath.length === 0){ - distributionEditorHint.textContent = '연결된 distribution 파일이 없습니다. 샘플을 불러오거나 새로 저장해서 이 프로필에 연결하세요.' - return - } - - if(isRemoteUrl(currentPath)){ - distributionEditorHint.textContent = `현재 값은 원격 URL입니다: ${currentPath} | 내용을 불러와 수정할 수 있고, 저장하면 이 프로젝트 안의 로컬 distribution 파일로 복사됩니다.` - return - } - - distributionEditorHint.textContent = `현재 연결된 distribution 파일: ${currentPath}` -} - -function populateEditor(){ - const profile = getSelectedProfile() - const hasSelection = profile != null - - emptyState.hidden = hasSelection - profileEditorForm.hidden = !hasSelection - duplicateProfileButton.disabled = !hasSelection - deleteProfileButton.disabled = !hasSelection - - if(!profile){ - editorHint.textContent = '왼쪽에서 프로필을 선택하거나 새 프로필을 추가하세요.' - syncServerSection(null) - updateDistributionEditorHint(null) - return - } - - syncFeatureDependencies(profile) - - editorHint.textContent = '맵은 기본이고, 모드/플러그인/서버를 체크해서 조합하는 프로필입니다. 저장하면 런처 카탈로그도 같이 갱신됩니다.' - - fieldElements.id.value = profile.id ?? '' - fieldElements.name.value = profile.name ?? '' - fieldElements.description.value = profile.description ?? '' - fieldElements.details.value = profile.details ?? '' - fieldElements.modsEnabled.checked = profile.modsEnabled === true - fieldElements.pluginsEnabled.checked = profile.pluginsEnabled === true - fieldElements.serverEnabled.checked = profile.serverEnabled === true - fieldElements.distributionUrl.value = profile.distributionUrl ?? '' - fieldElements.worldArchiveUrl.value = profile.worldArchiveUrl ?? '' - fieldElements.worldDirectoryName.value = profile.worldDirectoryName ?? '' - fieldElements.serverJarUrl.value = profile.serverJarUrl ?? '' - fieldElements.serverPort.value = profile.serverPort ?? 25565 - fieldElements.serverMemoryMb.value = profile.serverMemoryMb ?? 4096 - fieldElements.serverMaxPlayers.value = profile.serverMaxPlayers ?? 20 - fieldElements.serverWhitelistEnabled.checked = profile.serverWhitelistEnabled === true - - syncServerSection(profile) - updateDistributionEditorHint(profile) -} - -function updateSelectedProfile(patch){ - const profile = getSelectedProfile() - if(!profile){ - return - } - - Object.assign(profile, patch) - syncFeatureDependencies(profile) - markDirty(true) - renderSidebar() - syncServerSection(profile) - - if(Object.prototype.hasOwnProperty.call(patch, 'distributionUrl')){ - updateDistributionEditorHint(profile, patch.distributionUrl) - } -} - -function bindTextField(fieldName){ - fieldElements[fieldName].addEventListener('input', (event) => { - if(fieldName === 'id'){ - const profile = getSelectedProfile() - if(!profile){ - return - } - - const previousId = profile.id - profile.id = event.target.value - state.selectedProfileId = profile.id - markDirty(true) - renderSidebar() - - if(previousId !== profile.id){ - showStatus('프로필 ID를 바꿨습니다. 저장하면 이 ID로 반영됩니다.', 'info') - } - return - } - - updateSelectedProfile({ [fieldName]: event.target.value }) - }) -} - -function bindNumberField(fieldName, fallback){ - fieldElements[fieldName].addEventListener('input', (event) => { - const value = Number.parseInt(event.target.value || String(fallback), 10) - updateSelectedProfile({ - [fieldName]: Number.isFinite(value) ? value : fallback - }) - }) -} - -function bindCheckboxField(fieldName){ - fieldElements[fieldName].addEventListener('change', (event) => { - updateSelectedProfile({ - [fieldName]: event.target.checked - }) - - if(fieldName === 'pluginsEnabled' || fieldName === 'serverEnabled'){ - populateEditor() - } - }) -} - -function bindProfileForm(){ - profileEditorForm.addEventListener('submit', (event) => { - event.preventDefault() - }) - - bindTextField('id') - bindTextField('name') - bindTextField('description') - bindTextField('details') - bindTextField('distributionUrl') - bindTextField('worldArchiveUrl') - bindTextField('worldDirectoryName') - bindTextField('serverJarUrl') - bindNumberField('serverPort', 25565) - bindNumberField('serverMemoryMb', 4096) - bindNumberField('serverMaxPlayers', 20) - bindCheckboxField('modsEnabled') - bindCheckboxField('pluginsEnabled') - bindCheckboxField('serverEnabled') - bindCheckboxField('serverWhitelistEnabled') - - for(const button of document.querySelectorAll('.uploadButton')){ - button.addEventListener('click', async () => { - const targetField = button.dataset.uploadTarget - const accept = button.dataset.uploadAccept - await uploadIntoField(targetField, accept) - }) - } -} - -async function uploadIntoField(targetField, accept){ - const profile = getSelectedProfile() - if(!profile){ - return - } - - const picker = document.createElement('input') - picker.type = 'file' - picker.accept = accept || '' - picker.addEventListener('change', async () => { - const file = picker.files?.[0] - if(!file){ - return - } - - const formData = new FormData() - formData.append('file', file) - - try { - showStatus(`${file.name} 업로드 중...`, 'info') - - const response = await fetch('/api/upload', { - method: 'POST', - body: formData - }) - const result = await response.json() - if(!response.ok || result.ok !== true){ - throw new Error(result.message || '업로드에 실패했습니다.') - } - - profile[targetField] = result.file.path - fieldElements[targetField].value = result.file.path - markDirty(true) - renderSidebar() - - if(targetField === 'distributionUrl'){ - updateDistributionEditorHint(profile, result.file.path) - } - - showStatus(`업로드 완료: ${result.file.path}`, 'success') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : '업로드에 실패했습니다.', 'error') - } - }) - - picker.click() -} - -function openDistributionEditorModal(){ - distributionEditorModal.hidden = false - distributionEditorModal.style.display = 'flex' - document.body.style.overflow = 'hidden' - clearDistributionEditorStatus() -} - -function closeDistributionEditorModal(){ - distributionEditorModal.hidden = true - distributionEditorModal.style.display = 'none' - document.body.style.overflow = '' - clearDistributionEditorStatus() -} - -async function loadDistributionTemplate(){ - const response = await fetch('/api/distribution/template') - const result = await response.json() - if(!response.ok || result.ok !== true){ - throw new Error(result.message || 'distribution 샘플을 불러오지 못했습니다.') - } - - return JSON.parse(result.content) -} - -async function loadDistributionContent(requestedPath){ - const response = await fetch(`/api/distribution/content?path=${encodeURIComponent(requestedPath)}`) - const result = await response.json() - if(!response.ok || result.ok !== true){ - throw new Error(result.message || 'distribution 파일을 불러오지 못했습니다.') - } - - return JSON.parse(result.content) -} - -async function openDistributionEditor(mode){ - const profile = getSelectedProfile() - if(!profile){ - showStatus('먼저 프로필을 선택하세요.', 'error') - return - } - - openDistributionEditorModal() - distributionEditorForm.reset() - state.distributionEditor.document = null - updateDistributionEditorHint(profile) - showDistributionEditorStatus('distribution 내용을 준비하는 중...', 'info') - - try { - const currentPath = String(profile.distributionUrl ?? '').trim() - - if(mode === 'create' || currentPath.length === 0){ - const templateDocument = await loadDistributionTemplate() - populateDistributionEditorForm(templateDocument, profile) - updateDistributionEditorHint(profile, '') - showStatus('distribution 템플릿을 불러왔습니다.', 'success') - showDistributionEditorStatus('샘플을 불러왔습니다. 바로 수정한 뒤 저장하면 됩니다.', 'success') - return - } - - if(isRemoteUrl(currentPath)){ - const remoteDocument = await loadDistributionContent(currentPath) - populateDistributionEditorForm(remoteDocument, profile) - updateDistributionEditorHint(profile, currentPath) - showStatus('원격 distribution 내용을 불러왔습니다.', 'success') - showDistributionEditorStatus('원격 distribution 내용을 불러왔습니다. 저장하면 로컬 파일로 복사됩니다.', 'success') - return - } - - showStatus('distribution 파일을 불러오는 중...', 'info') - const localDocument = await loadDistributionContent(currentPath) - populateDistributionEditorForm(localDocument, profile) - updateDistributionEditorHint(profile, currentPath) - showStatus('distribution 파일을 불러왔습니다.', 'success') - showDistributionEditorStatus('현재 연결된 distribution 파일을 불러왔습니다.', 'success') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error') - showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일을 불러오지 못했습니다.', 'error') - } -} - -async function saveDistributionFile(){ - const profile = getSelectedProfile() - if(!profile){ - showStatus('먼저 프로필을 선택하세요.', 'error') - return - } - - try { - const distributionDocument = buildDistributionDocumentFromForm(profile) - saveDistributionFileButton.disabled = true - showStatus('distribution 파일 저장 중...', 'info') - showDistributionEditorStatus('distribution 파일 저장 중...', 'info') - - const response = await fetch('/api/distribution/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - profileId: profile.id, - content: JSON.stringify(distributionDocument, null, 2) - }) - }) - const result = await response.json() - if(!response.ok || result.ok !== true){ - throw new Error(result.message || 'distribution 파일 저장에 실패했습니다.') - } - - profile.distributionUrl = result.file.path - fieldElements.distributionUrl.value = result.file.path - markDirty(true) - renderSidebar() - populateEditor() - closeDistributionEditorModal() - showStatus('distribution 파일을 저장했습니다. 카탈로그 저장을 누르면 런처에 반영됩니다.', 'success') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error') - showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 파일 저장에 실패했습니다.', 'error') - } finally { - saveDistributionFileButton.disabled = false - } -} - -function bindDistributionEditor(){ - distributionEditorForm.addEventListener('submit', (event) => { - event.preventDefault() - }) - - distributionEditorModal.addEventListener('click', (event) => { - if(event.target === distributionEditorModal){ - closeDistributionEditorModal() - } - }) - - document.addEventListener('keydown', (event) => { - if(event.key === 'Escape' && distributionEditorModal.hidden === false){ - closeDistributionEditorModal() - } - }) -} - -window.__launcherAdminOpenDistributionEditor = async (mode) => { - await openDistributionEditor(mode) -} - -window.__launcherAdminCloseDistributionEditor = () => { - closeDistributionEditorModal() -} - -window.__launcherAdminLoadDistributionTemplate = async () => { - try { - const templateDocument = await loadDistributionTemplate() - populateDistributionEditorForm(templateDocument, getSelectedProfile()) - updateDistributionEditorHint(getSelectedProfile(), '') - showStatus('distribution 템플릿을 다시 불러왔습니다.', 'success') - showDistributionEditorStatus('샘플을 다시 불러왔습니다.', 'success') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error') - showDistributionEditorStatus(error instanceof Error ? error.message : 'distribution 템플릿을 불러오지 못했습니다.', 'error') - } -} - -window.__launcherAdminSaveDistributionFile = async () => { - await saveDistributionFile() -} - -async function loadMeta(){ - const response = await fetch('/api/meta') - const meta = await response.json() - state.meta = meta - launcherCatalogPath.textContent = meta.launcherCatalogPath - localCatalogUrl.textContent = meta.localCatalogUrl - localCatalogUrl.href = meta.localCatalogUrl -} - -function normalizeLoadedProfile(profile){ - return { - ...createProfile(), - ...profile, - modsEnabled: profile.modsEnabled === true, - pluginsEnabled: profile.pluginsEnabled === true, - serverEnabled: profile.serverEnabled === true || profile.pluginsEnabled === true, - serverWhitelistEnabled: profile.serverWhitelistEnabled === true - } -} - -async function loadCatalog(){ - const response = await fetch('/api/catalog') - const catalog = await response.json() - state.catalog = { - version: 1, - profiles: Array.isArray(catalog.profiles) ? catalog.profiles.map(normalizeLoadedProfile) : [] - } - - if(state.catalog.profiles.length > 0){ - state.selectedProfileId = state.catalog.profiles[0].id - } else { - state.selectedProfileId = null - } - - markDirty(false) - renderSidebar() - populateEditor() -} - -function addProfile(){ - const profile = createProfile() - state.catalog.profiles.push(profile) - markDirty(true) - selectProfile(profile.id) - showStatus(`${profile.name} 프로필을 추가했습니다.`, 'success') -} - -function duplicateSelectedProfile(){ - const profile = getSelectedProfile() - if(!profile){ - return - } - - const clonedProfile = structuredClone(profile) - clonedProfile.id = `${slugify(profile.id || profile.name) || 'profile'}-copy-${Date.now()}` - clonedProfile.name = `${profile.name} 복제본` - - state.catalog.profiles.push(clonedProfile) - markDirty(true) - selectProfile(clonedProfile.id) - showStatus('복제본을 만들었습니다.', 'success') -} - -function deleteSelectedProfile(){ - const profile = getSelectedProfile() - if(!profile){ - return - } - - const confirmed = window.confirm(`'${profile.name}' 프로필을 삭제할까요?`) - if(!confirmed){ - return - } - - state.catalog.profiles = state.catalog.profiles.filter((entry) => entry.id !== profile.id) - state.selectedProfileId = state.catalog.profiles[0]?.id ?? null - markDirty(true) - renderSidebar() - populateEditor() - showStatus('프로필을 삭제했습니다.', 'success') -} - -async function saveCatalog(){ - try { - clearStatus() - saveCatalogButton.disabled = true - showStatus('카탈로그 저장 중...', 'info') - - const response = await fetch('/api/catalog', { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(state.catalog) - }) - const result = await response.json() - if(!response.ok || result.ok !== true){ - throw new Error(result.message || '카탈로그 저장에 실패했습니다.') - } - - state.catalog = { - version: 1, - profiles: result.catalog.profiles.map(normalizeLoadedProfile) - } - if(!state.catalog.profiles.some((profile) => profile.id === state.selectedProfileId)){ - state.selectedProfileId = state.catalog.profiles[0]?.id ?? null - } - - markDirty(false) - renderSidebar() - populateEditor() - showStatus('카탈로그를 저장했고 런처 카탈로그에도 동기화했습니다.', 'success') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : '카탈로그 저장에 실패했습니다.', 'error') - } finally { - saveCatalogButton.disabled = false - } -} - -function bindTopLevelActions(){ - addProfileButton.addEventListener('click', () => { - addProfile() - }) - - saveCatalogButton.addEventListener('click', async () => { - await saveCatalog() - }) - - duplicateProfileButton.addEventListener('click', () => { - duplicateSelectedProfile() - }) - - deleteProfileButton.addEventListener('click', () => { - deleteSelectedProfile() - }) -} - -async function bootstrap(){ - closeDistributionEditorModal() - bindProfileForm() - bindDistributionEditor() - bindTopLevelActions() - - try { - await Promise.all([ - loadMeta(), - loadCatalog() - ]) - showStatus('관리자 사이트를 불러왔습니다. 저장하면 런처 카탈로그가 같이 갱신됩니다.', 'info') - } catch (error) { - console.error(error) - showStatus(error instanceof Error ? error.message : '관리자 사이트를 불러오지 못했습니다.', 'error') - } -} - -bootstrap() diff --git a/admin/public/index.html b/admin/public/index.html deleted file mode 100644 index 53d2aa2..0000000 --- a/admin/public/index.html +++ /dev/null @@ -1,289 +0,0 @@ - - - - - - Launcher Admin - - - -
- - -
-
-
- Catalog Editor -

선택한 프로필 편집

-

왼쪽에서 프로필을 선택하거나 새 프로필을 추가하세요.

-
-
- - - -
-
- - - -
-

프로필이 없습니다

-

왼쪽 버튼으로 새 프로필을 추가하세요. 맵은 기본이고, 모드/플러그인/서버는 체크해서 조합합니다.

-
- - -
-
- - - - - - diff --git a/admin/public/styles.css b/admin/public/styles.css deleted file mode 100644 index b9a436b..0000000 --- a/admin/public/styles.css +++ /dev/null @@ -1,452 +0,0 @@ -:root { - --bg: #0f1110; - --panel: #161a18; - --panel-strong: #1d231f; - --panel-soft: rgba(255, 255, 255, 0.04); - --line: rgba(255, 255, 255, 0.08); - --text: #f5f4ef; - --muted: rgba(245, 244, 239, 0.68); - --accent: #f0bf57; - --accent-strong: #ffd980; - --danger: #ff7b63; - --shadow: 0 24px 50px rgba(0, 0, 0, 0.28); - --radius: 22px; -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - min-height: 100%; - background: - radial-gradient(circle at top left, rgba(240, 191, 87, 0.12), transparent 30%), - linear-gradient(180deg, #0d0f0e 0%, #111512 100%); - color: var(--text); - font-family: "Pretendard Variable", "Noto Sans KR", sans-serif; -} - -button, -input, -textarea, -select { - font: inherit; -} - -a { - color: var(--accent-strong); -} - -code { - display: inline-block; - padding: 6px 8px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.06); - color: #fff4d8; - word-break: break-all; -} - -.adminShell { - display: grid; - grid-template-columns: 360px minmax(0, 1fr); - min-height: 100vh; -} - -.sidebar { - display: flex; - flex-direction: column; - gap: 20px; - padding: 28px 24px; - border-right: 1px solid var(--line); - background: rgba(10, 12, 11, 0.86); - backdrop-filter: blur(16px); -} - -.brandBlock h1, -.topBar h2, -.emptyState h3, -.sectionHeader h3 { - margin: 0; -} - -.brandBlock p, -.topBar p, -.emptyState p, -.helperSection p { - margin: 0; - line-height: 1.6; - color: var(--muted); -} - -.eyebrow { - display: inline-block; - margin-bottom: 10px; - color: var(--accent); - letter-spacing: 0.14em; - text-transform: uppercase; - font-size: 12px; - font-weight: 700; -} - -.metaPanel, -.emptyState, -.fieldSection, -.statusBanner { - padding: 18px 20px; - border: 1px solid var(--line); - border-radius: var(--radius); - background: var(--panel); - box-shadow: var(--shadow); -} - -.metaPanel { - display: flex; - flex-direction: column; - gap: 14px; -} - -.metaRow { - display: flex; - flex-direction: column; - gap: 8px; -} - -.metaLabel, -.profileListHeader, -.fieldBlock span, -.toggleBlock span { - font-size: 13px; - font-weight: 700; - letter-spacing: 0.04em; -} - -.profileListHeader { - display: flex; - justify-content: space-between; - color: var(--muted); -} - -.addButtons { - display: grid; - gap: 10px; -} - -.profileList { - display: flex; - flex-direction: column; - gap: 10px; - min-height: 120px; -} - -.profileListItem { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - padding: 16px; - border: 1px solid var(--line); - border-radius: 18px; - background: var(--panel-soft); - color: var(--text); - text-align: left; - cursor: pointer; - transition: border-color 0.2s ease, transform 0.2s ease, background 0.2s ease; -} - -.profileListItem:hover, -.profileListItem:focus { - border-color: rgba(240, 191, 87, 0.45); - transform: translateY(-1px); - outline: none; -} - -.profileListItem[selected="true"] { - border-color: rgba(240, 191, 87, 0.72); - background: rgba(240, 191, 87, 0.1); -} - -.profileListName { - font-size: 16px; - font-weight: 800; -} - -.profileListMeta { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.profileListDescription { - font-size: 14px; - color: var(--muted); - line-height: 1.45; -} - -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 4px 10px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - white-space: nowrap; -} - -.editorPane { - display: flex; - flex-direction: column; - gap: 20px; - padding: 28px; -} - -.topBar { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 24px; -} - -.topBarActions { - display: flex; - gap: 10px; - flex-wrap: wrap; -} - -.editorForm { - display: flex; - flex-direction: column; - gap: 18px; -} - -.sectionHeader { - margin-bottom: 16px; -} - -.fieldGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; -} - -.fieldBlock, -.toggleBlock { - display: flex; - flex-direction: column; - gap: 8px; -} - -.fieldBlockFull { - grid-column: 1 / -1; -} - -.toggleBlock { - justify-content: flex-end; -} - -.toggleBlock input { - width: 18px; - height: 18px; - margin: 0; -} - -.toggleBlock { - flex-direction: row; - align-items: center; -} - -input, -textarea, -select { - width: 100%; - padding: 14px 16px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 14px; - background: var(--panel-strong); - color: var(--text); -} - -textarea { - resize: vertical; - min-height: 150px; -} - -.uploadField { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto auto; - gap: 10px; - align-items: center; -} - -.fieldHelpText { - color: var(--muted); - font-size: 14px; - line-height: 1.5; -} - -.primaryAction, -.secondaryAction, -.dangerAction { - padding: 13px 18px; - border-radius: 14px; - border: 1px solid transparent; - color: var(--text); - cursor: pointer; - white-space: nowrap; - transition: transform 0.2s ease, opacity 0.2s ease, border-color 0.2s ease, background 0.2s ease; -} - -.primaryAction { - background: var(--accent); - color: #211808; - font-weight: 800; -} - -.secondaryAction { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.12); -} - -.dangerAction { - background: rgba(255, 123, 99, 0.1); - border-color: rgba(255, 123, 99, 0.3); - color: #ffb4a5; -} - -.primaryAction:hover, -.secondaryAction:hover, -.dangerAction:hover { - transform: translateY(-1px); -} - -.primaryAction:disabled, -.secondaryAction:disabled, -.dangerAction:disabled { - opacity: 0.45; - cursor: not-allowed; - transform: none; -} - -.statusBanner { - font-size: 14px; - line-height: 1.5; -} - -.statusBanner[data-tone="success"] { - border-color: rgba(115, 208, 144, 0.3); - background: rgba(115, 208, 144, 0.1); -} - -.statusBanner[data-tone="error"] { - border-color: rgba(255, 123, 99, 0.3); - background: rgba(255, 123, 99, 0.1); -} - -.statusBanner[data-tone="info"] { - border-color: rgba(240, 191, 87, 0.28); - background: rgba(240, 191, 87, 0.1); -} - -.modalBackdrop { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - background: rgba(5, 6, 6, 0.72); - backdrop-filter: blur(12px); - z-index: 20; -} - -.modalPanel { - display: flex; - flex-direction: column; - gap: 16px; - width: min(1100px, 100%); - max-height: calc(100vh - 48px); - padding: 24px; - border: 1px solid var(--line); - border-radius: 24px; - background: #111412; - box-shadow: var(--shadow); - pointer-events: auto; - overflow-y: auto; -} - -#distributionEditorStatus { - margin-top: -4px; -} - -.distributionSummary { - padding-bottom: 16px; -} - -.distributionSummaryValue { - width: 100%; - padding: 14px 16px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 14px; - background: var(--panel-strong); - color: var(--text); -} - -.distributionEditorForm { - gap: 16px; -} - -.modalHeader { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; -} - -.distributionTextarea { - min-height: 420px; - resize: vertical; - font-family: "JetBrains Mono", "Fira Code", monospace; - font-size: 13px; - line-height: 1.55; -} - -.modalActions { - display: flex; - justify-content: flex-end; - gap: 10px; - flex-wrap: wrap; -} - -@media (max-width: 1200px) { - .adminShell { - grid-template-columns: 1fr; - } - - .sidebar { - border-right: none; - border-bottom: 1px solid var(--line); - } -} - -@media (max-width: 760px) { - .editorPane, - .sidebar { - padding: 20px; - } - - .fieldGrid, - .uploadField, - .topBar { - grid-template-columns: 1fr; - } - - .topBar { - display: flex; - flex-direction: column; - } -} diff --git a/admin/server.js b/admin/server.js deleted file mode 100644 index ba7f959..0000000 --- a/admin/server.js +++ /dev/null @@ -1,454 +0,0 @@ -const express = require('express') -const fs = require('fs-extra') -const multer = require('multer') -const path = require('path') - -const HOST = process.env.LAUNCHER_ADMIN_HOST || '127.0.0.1' -const PORT = Number.parseInt(process.env.LAUNCHER_ADMIN_PORT || '8787', 10) -const PROJECT_ROOT = path.resolve(__dirname, '..') -const RUNTIME_DATA_DIR = path.join(__dirname, 'data') -const UPLOADS_DIR = path.join(RUNTIME_DATA_DIR, 'uploads') -const DISTRIBUTIONS_DIR = path.join(RUNTIME_DATA_DIR, 'distributions') -const RUNTIME_CATALOG_PATH = path.join(RUNTIME_DATA_DIR, 'catalog.json') -const LAUNCHER_CATALOG_PATH = path.join(PROJECT_ROOT, 'app', 'assets', 'launcher', 'catalog.json') -const PUBLIC_DIR = path.join(__dirname, 'public') -const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json') -const EXAMPLE_DISTRIBUTION_PREFIX = 'https://example.com/launcher/' -const PUBLIC_BASE_URL = normalizeText(process.env.LAUNCHER_PUBLIC_BASE_URL).replace(/\/+$/, '') - -function normalizeText(value){ - return typeof value === 'string' ? value.trim() : '' -} - -function normalizeMultilineText(value){ - return typeof value === 'string' - ? value.replace(/\r\n/g, '\n').trim() - : '' -} - -function normalizePort(value){ - const port = Number.parseInt(String(value ?? ''), 10) - if(Number.isFinite(port) && port >= 1 && port <= 65535){ - return port - } - return 25565 -} - -function normalizePositiveInteger(value, fallback, minimum = 1){ - const parsed = Number.parseInt(String(value ?? ''), 10) - if(Number.isFinite(parsed) && parsed >= minimum){ - return parsed - } - return fallback -} - -function normalizeBoolean(value){ - return value === true -} - -function getPublicBaseUrl(){ - if(PUBLIC_BASE_URL.length > 0){ - return PUBLIC_BASE_URL - } - return `http://${HOST}:${PORT}` -} - -function resolveSafeProjectPath(relativePath){ - const resolvedPath = path.resolve(PROJECT_ROOT, relativePath) - if(!resolvedPath.startsWith(PROJECT_ROOT + path.sep) && resolvedPath !== PROJECT_ROOT){ - throw new Error('허용되지 않은 경로입니다.') - } - return resolvedPath -} - -function createDistributionFileName(profileId){ - const safeId = String(profileId ?? 'distribution') - .trim() - .toLowerCase() - .replace(/[^a-z0-9._-]+/g, '-') - .replace(/-+/g, '-') - .replace(/^[-_.]+|[-_.]+$/g, '') - - return `${safeId.length > 0 ? safeId : 'distribution'}.distribution.json` -} - -function deriveFeatureFlags(rawProfile){ - const legacyKind = normalizeText(rawProfile?.kind) - - let modsEnabled = normalizeBoolean(rawProfile?.modsEnabled) - let pluginsEnabled = normalizeBoolean(rawProfile?.pluginsEnabled) - let serverEnabled = normalizeBoolean(rawProfile?.serverEnabled) - - if(legacyKind === 'modpack'){ - modsEnabled = true - } else if(legacyKind === 'server-pack'){ - pluginsEnabled = true - serverEnabled = true - } - - if(pluginsEnabled){ - serverEnabled = true - } - - return { - modsEnabled, - pluginsEnabled, - serverEnabled - } -} - -function normalizeServerJarSource(rawProfile){ - const directValue = normalizeText(rawProfile?.serverJarUrl) - if(directValue.length > 0){ - return directValue - } - - const legacyBundle = normalizeText(rawProfile?.serverBundleUrl) - if(legacyBundle.toLowerCase().endsWith('.jar')){ - return legacyBundle - } - - return '' -} - -function sanitizeProfile(rawProfile, index){ - const { - modsEnabled, - pluginsEnabled, - serverEnabled - } = deriveFeatureFlags(rawProfile) - - const sanitized = { - id: normalizeText(rawProfile?.id) || `profile-${index + 1}`, - name: normalizeText(rawProfile?.name) || `새 프로필 ${index + 1}`, - description: normalizeText(rawProfile?.description), - details: normalizeMultilineText(rawProfile?.details), - distributionUrl: normalizeText(rawProfile?.distributionUrl), - modsEnabled, - pluginsEnabled, - serverEnabled, - worldArchiveUrl: normalizeText(rawProfile?.worldArchiveUrl), - worldDirectoryName: normalizeText(rawProfile?.worldDirectoryName) - } - - if(serverEnabled){ - sanitized.serverJarUrl = normalizeServerJarSource(rawProfile) - sanitized.serverDirectoryName = normalizeText(rawProfile?.serverDirectoryName) || `${sanitized.id}-server` - sanitized.serverPort = normalizePort(rawProfile?.serverPort) - sanitized.serverMemoryMb = normalizePositiveInteger(rawProfile?.serverMemoryMb, 4096, 512) - sanitized.serverMaxPlayers = normalizePositiveInteger(rawProfile?.serverMaxPlayers, 20, 1) - sanitized.serverWhitelistEnabled = normalizeBoolean(rawProfile?.serverWhitelistEnabled) - } - - if(normalizeText(rawProfile?.artwork).length > 0){ - sanitized.artwork = normalizeText(rawProfile.artwork) - } - - return sanitized -} - -function sanitizeCatalog(payload){ - const profiles = Array.isArray(payload?.profiles) ? payload.profiles : [] - - return { - version: 1, - profiles: profiles.map((profile, index) => sanitizeProfile(profile, index)) - } -} - -function toProjectRelativePath(targetPath){ - return path.relative(PROJECT_ROOT, targetPath).split(path.sep).join('/') -} - -async function ensureRuntimeCatalog(){ - await fs.ensureDir(RUNTIME_DATA_DIR) - await fs.ensureDir(UPLOADS_DIR) - await fs.ensureDir(DISTRIBUTIONS_DIR) - - if(!(await fs.pathExists(RUNTIME_CATALOG_PATH))){ - if(await fs.pathExists(LAUNCHER_CATALOG_PATH)){ - await fs.copy(LAUNCHER_CATALOG_PATH, RUNTIME_CATALOG_PATH, { overwrite: true }) - } else { - await fs.writeJson(RUNTIME_CATALOG_PATH, { version: 1, profiles: [] }, { spaces: 2 }) - } - } - - await migrateExampleCatalog() -} - -async function ensureLocalSampleDistribution(profileId, profileName){ - const fileName = createDistributionFileName(profileId) - const targetPath = path.join(DISTRIBUTIONS_DIR, fileName) - - if(!(await fs.pathExists(targetPath))){ - const template = JSON.parse(await fs.readFile(SAMPLE_DISTRIBUTION_PATH, 'utf8')) - template.servers = [ - { - id: profileId, - name: profileName || profileId, - description: '관리자 사이트 로컬 샘플 distribution입니다.', - version: '1.20.1', - minecraftVersion: '1.20.1', - address: '127.0.0.1:25565', - mainServer: true, - autoconnect: false, - modules: [] - } - ] - await fs.writeFile(targetPath, JSON.stringify(template, null, 2) + '\n', 'utf8') - } - - return toProjectRelativePath(targetPath) -} - -async function migrateExampleCatalog(){ - if(!(await fs.pathExists(RUNTIME_CATALOG_PATH))){ - return - } - - const runtimeCatalog = sanitizeCatalog(await fs.readJson(RUNTIME_CATALOG_PATH)) - let changed = false - - for(const profile of runtimeCatalog.profiles){ - if(normalizeText(profile.distributionUrl).startsWith(EXAMPLE_DISTRIBUTION_PREFIX)){ - profile.distributionUrl = await ensureLocalSampleDistribution(profile.id, profile.name) - changed = true - } - } - - if(changed){ - await fs.writeJson(RUNTIME_CATALOG_PATH, runtimeCatalog, { spaces: 2 }) - await fs.writeJson(LAUNCHER_CATALOG_PATH, runtimeCatalog, { spaces: 2 }) - } -} - -async function readCatalog(){ - await ensureRuntimeCatalog() - return sanitizeCatalog(await fs.readJson(RUNTIME_CATALOG_PATH)) -} - -async function writeCatalog(catalog){ - const sanitizedCatalog = sanitizeCatalog(catalog) - await fs.ensureDir(path.dirname(LAUNCHER_CATALOG_PATH)) - await fs.writeJson(RUNTIME_CATALOG_PATH, sanitizedCatalog, { spaces: 2 }) - await fs.writeJson(LAUNCHER_CATALOG_PATH, sanitizedCatalog, { spaces: 2 }) - return sanitizedCatalog -} - -function createUploadStorage(){ - return multer.diskStorage({ - destination(_req, _file, callback){ - fs.ensureDir(UPLOADS_DIR) - .then(() => callback(null, UPLOADS_DIR)) - .catch((error) => callback(error)) - }, - filename(_req, file, callback){ - const extension = path.extname(file.originalname) - const baseName = path.basename(file.originalname, extension) - .replace(/[^a-zA-Z0-9._-]+/g, '-') - .replace(/-+/g, '-') - .replace(/^[-_.]+|[-_.]+$/g, '') - const timestamp = Date.now() - const safeBaseName = baseName.length > 0 ? baseName : 'file' - callback(null, `${timestamp}-${safeBaseName}${extension.toLowerCase()}`) - } - }) -} - -async function start(){ - await ensureRuntimeCatalog() - - const app = express() - const upload = multer({ - storage: createUploadStorage(), - limits: { - fileSize: 1024 * 1024 * 1024 - } - }) - - app.use((_req, res, next) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate') - res.setHeader('Pragma', 'no-cache') - res.setHeader('Expires', '0') - next() - }) - - app.use(express.json({ limit: '5mb' })) - app.use('/uploads', express.static(UPLOADS_DIR)) - app.use('/admin/data/uploads', express.static(UPLOADS_DIR)) - app.use('/admin/data/distributions', express.static(DISTRIBUTIONS_DIR)) - - app.get('/api/meta', async (_req, res) => { - res.json({ - host: HOST, - port: PORT, - runtimeCatalogPath: toProjectRelativePath(RUNTIME_CATALOG_PATH), - launcherCatalogPath: toProjectRelativePath(LAUNCHER_CATALOG_PATH), - localCatalogUrl: `${getPublicBaseUrl()}/catalog.json`, - distributionsPath: toProjectRelativePath(DISTRIBUTIONS_DIR) - }) - }) - - app.get('/api/distribution/template', async (_req, res, next) => { - try { - const content = await fs.readFile(SAMPLE_DISTRIBUTION_PATH, 'utf8') - res.json({ - ok: true, - content - }) - } catch (error) { - next(error) - } - }) - - app.get('/api/distribution/content', async (req, res, next) => { - try { - const requestedPath = normalizeText(req.query.path) - if(requestedPath.length === 0){ - res.status(400).json({ - ok: false, - message: '불러올 distribution 경로가 없습니다.' - }) - return - } - - if(/^https?:\/\//i.test(requestedPath)){ - const response = await fetch(requestedPath, { - signal: AbortSignal.timeout(10000) - }) - if(!response.ok){ - throw new Error(`원격 distribution을 불러오지 못했습니다. (${response.status})`) - } - - const content = await response.text() - JSON.parse(content) - res.json({ - ok: true, - content - }) - return - } - - const resolvedPath = resolveSafeProjectPath(requestedPath) - const content = await fs.readFile(resolvedPath, 'utf8') - res.json({ - ok: true, - content - }) - } catch (error) { - next(error) - } - }) - - app.get('/api/catalog', async (_req, res, next) => { - try { - res.json(await readCatalog()) - } catch (error) { - next(error) - } - }) - - app.put('/api/catalog', async (req, res, next) => { - try { - const savedCatalog = await writeCatalog(req.body) - res.json({ - ok: true, - catalog: savedCatalog - }) - } catch (error) { - next(error) - } - }) - - app.post('/api/upload', upload.single('file'), async (req, res, next) => { - try { - if(req.file == null){ - res.status(400).json({ - ok: false, - message: '업로드할 파일이 없습니다.' - }) - return - } - - const relativePath = toProjectRelativePath(req.file.path) - - res.json({ - ok: true, - file: { - name: req.file.originalname, - storedName: req.file.filename, - size: req.file.size, - path: relativePath, - localUrl: `${getPublicBaseUrl()}/uploads/${encodeURIComponent(req.file.filename)}` - } - }) - } catch (error) { - next(error) - } - }) - - app.post('/api/distribution/save', async (req, res, next) => { - try { - const profileId = normalizeText(req.body?.profileId) - const rawContent = typeof req.body?.content === 'string' ? req.body.content : '' - - if(profileId.length === 0){ - res.status(400).json({ - ok: false, - message: '프로필 ID가 필요합니다.' - }) - return - } - - const parsed = JSON.parse(rawContent) - const fileName = createDistributionFileName(profileId) - const targetPath = path.join(DISTRIBUTIONS_DIR, fileName) - const relativePath = toProjectRelativePath(targetPath) - - await fs.writeFile(targetPath, JSON.stringify(parsed, null, 2) + '\n', 'utf8') - - res.json({ - ok: true, - file: { - path: relativePath, - localUrl: `${getPublicBaseUrl()}/${relativePath}` - } - }) - } catch (error) { - next(error) - } - }) - - app.get('/catalog.json', async (_req, res, next) => { - try { - res.sendFile(RUNTIME_CATALOG_PATH) - } catch (error) { - next(error) - } - }) - - app.use(express.static(PUBLIC_DIR)) - - app.get(/.*/, (_req, res) => { - res.sendFile(path.join(PUBLIC_DIR, 'index.html')) - }) - - app.use((error, _req, res, _next) => { - console.error(error) - res.status(500).json({ - ok: false, - message: error instanceof Error ? error.message : '관리자 서버 처리 중 오류가 발생했습니다.' - }) - }) - - app.listen(PORT, HOST, () => { - console.log(`[launcher-admin] running on http://${HOST}:${PORT}`) - console.log(`[launcher-admin] runtime catalog: ${toProjectRelativePath(RUNTIME_CATALOG_PATH)}`) - console.log(`[launcher-admin] launcher catalog mirror: ${toProjectRelativePath(LAUNCHER_CATALOG_PATH)}`) - }) -} - -start().catch((error) => { - console.error(error) - process.exit(1) -}) diff --git a/app/app.ejs b/app/app.ejs deleted file mode 100644 index 7878c75..0000000 --- a/app/app.ejs +++ /dev/null @@ -1,50 +0,0 @@ - - - - <%= lang('app.title') %> - - - - - - - <%- include('frame') %> -
- <%- include('welcome') %> - <%- include('login') %> - <%- include('waiting') %> - <%- include('loginOptions') %> - <%- include('settings') %> - <%- include('library') %> - <%- include('install') %> - <%- include('landing') %> -
- <%- include('overlay') %> -
-
-
- -
-
-
- - diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css deleted file mode 100644 index beeada8..0000000 --- a/app/assets/css/launcher.css +++ /dev/null @@ -1,4645 +0,0 @@ -/* Github Code Highlighting. */ -@import "../../../node_modules/github-syntax-dark/lib/github-dark.css"; - -/******************************************************************************* - * * - * Fonts * - * * - ******************************************************************************/ - -@font-face { - font-family: 'Pretendard Regular'; - src: url('../fonts/Pretendard-Regular.ttf'); -} - -@font-face { - font-family: 'Pretendard Medium'; - src: url('../fonts/Pretendard-Medium.ttf'); -} - -@font-face { - font-family: 'Ringbearer'; - src: url('../fonts/Ringbearer.ttf'); -} - -/******************************************************************************* - * * - * Element Styles * - * * - ******************************************************************************/ - -/* Reset body, html, and div presets. */ -body, html, div { - margin: 0px; - padding: 0px; -} - -/* Reset p presets. */ -p { - -webkit-margin-before: 0em; - -webkit-margin-after: 0em; -} - -/* Set default font and color. */ -body, button { - font-family: 'Pretendard Regular'; - color: white; -} - -/*body { - background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; - background-size: cover; -}*/ - -/******************************************************************************* - * * - * Frame Styles (frame.ejs) * - * * - ******************************************************************************/ - -/* Frame Bar */ -#frameBar { - position: relative; - z-index: 100; - display: flex; - flex-direction: column; - transition: background-color 1s ease; - /*background-color: rgba(0, 0, 0, 0.5);*/ - -webkit-user-select: none; -} - -/* Undraggable region on the top of the frame. */ -#frameResizableTop { - height: 2px; - width: 100%; - -webkit-app-region: no-drag; -} - -/* Flexbox to wrap the main frame content. */ -#frameMain { - display: flex; - height: 20px -} - -/* Undraggable region on the left and right of the frame. */ -.frameResizableVert { - width: 2px; - -webkit-app-region: no-drag; -} - -/* Main frame content for windows. */ -#frameContentWin { - display: flex; - justify-content: space-between; - width: 100%; - -webkit-app-region: drag; -} - -/* Main frame content for darwin. */ -#frameContentDarwin { - display: flex; - justify-content: flex-start; - align-items: center; - width: 100%; - -webkit-app-region: drag; -} - -/* Frame logo (windows only). */ -#frameTitleDock { - padding: 0px 10px; -} -#frameTitleText { - font-size: 14px; - font-family: 'Pretendard Medium'; - letter-spacing: 0.5px; -} - -/* Windows frame button dock. */ -#frameButtonDockWin { - -webkit-app-region: no-drag !important; - position: relative; - top: -2px; - right: -2px; - height: 22px; -} -#frameButtonDockWin > .frameButton:not(:first-child) { - margin-left: -4px; -} - -/* Darwin frame button dock: NaN; */ -#frameButtonDockDarwin { - -webkit-app-region: no-drag !important; - position: relative; - top: -1px; - right: -1px; -} - -/* Windows Frame Button Styles. */ -.frameButton { - background: none; - border: none; - height: 22px; - width: 39px; - cursor: pointer; -} -.frameButton:hover, -.frameButton:focus { - background: rgba(189, 189, 189, 0.43); -} -.frameButton:active { - background: rgba(156, 156, 156, 0.43); -} -.frameButton:focus { - outline: 0px; -} - -/* Close button is red. */ -#frameButton_close:hover, -#frameButton_close:focus { - background: rgba(255, 53, 53, 0.61) !important; -} -#frameButton_close:active { - background: rgba(235, 0, 0, 0.61) !important; -} - -/* Darwin Frame Button Styles. */ -.frameButtonDarwin { - height: 12px; - width: 12px; - border-radius: 50%; - border: 0px; - margin-left: 5px; - -webkit-app-region: no-drag !important; - cursor: pointer; -} -.frameButtonDarwin:focus { - outline: 0px; -} - -#frameButtonDarwin_close { - background-color: #e74c32; -} -#frameButtonDarwin_close:hover, -#frameButtonDarwin_close:focus { - background-color: #FF9A8A; -} -#frameButtonDarwin_close:active { - background-color: #ff8d7b; -} - -#frameButtonDarwin_minimize { - background-color: #fed045; -} -#frameButtonDarwin_minimize:hover, -#frameButtonDarwin_minimize:focus { - background-color: #FFE9A9; -} -#frameButtonDarwin_minimize:active { - background-color: #ffde7b; -} - -#frameButtonDarwin_restoredown { - background-color: #96e734; -} -#frameButtonDarwin_restoredown:hover, -#frameButtonDarwin_restoredown:focus { - background-color: #D6FFA6; -} -#frameButtonDarwin_restoredown:active { - background-color: #bfff76; -} - -/******************************************************************************* - * * - * Welcome View (welcome.ejs) * - * * - ******************************************************************************/ - -#welcomeContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - background: rgba(0, 0, 0, 0.50); -} - -#welcomeContent { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 50%; - top: -10%; - position: relative; -} - -/* -.cloudDiv { - position: absolute; - height: 100%; - width: 100%; - display: flex; - flex-direction: column; -} - -.cloudTop { - height: 50%; - width: 100%; - background-image: url('../images/cloudTrans.png'); - animation: clouds1 80s linear infinite; - background-size: cover; -} - -.cloudBottom { - height: 50%; - width: 100%; - background-image: url('../images/cloudTrans2.png'); - animation: clouds2 70s linear infinite; - background-size: cover; -} - -@keyframes clouds1 { - to { - background-position: 200%; - } -} -@keyframes clouds2 { - to { - background-position: 230%; - } -} -*/ - -#welcomeImageSeal { - border-radius: 50%; - border: 2px solid #cad7e1; - background: rgba(1, 2, 1, 0.5); - height: 125px; - width: 125px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - margin-bottom: 5%; - margin-top: 10%; -} - -#welcomeHeader { - font-family: 'Pretendard Medium'; - text-align: center; - color: white; - margin-bottom: 25px; - letter-spacing: 1px; - font-size: 20px; - text-shadow: white 0px 0px 0px; -} - -#welcomeDescription { - text-align: justify; - font-size: 13px; - font-weight: 100; - text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px -} - -#welcomeDescCTA { - text-align: center; - font-size: 14px; - font-weight: 100; - text-shadow: rgba(255, 255, 255, 0.75) 0px 0px 20px -} - -/* Login button styles. */ -#welcomeButton { - background: none; - font-weight: bold; - letter-spacing: 2px; - border: none; - padding: 15px 5px; - margin: 10px 0px; - cursor: pointer; - position: relative; - right: -20px; - transition: 0.5s ease; - margin-top: 5%; - margin-bottom: -5%; -} -#welcomeButton:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} -#welcomeButton:hover, -#welcomeButton:focus { - text-shadow: 0px 0px 20px #fff; - outline: none; -} -#welcomeButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} -#welcomeSVG { - -webkit-transform: translate3d(0, 0, 0); - overflow: visible; - transform: rotate(90deg); - margin-left: 20px; - transition: 0.25s ease; - width: 20px; - height: 20px; -} -#welcomeButton:hover #welcomeSVG, -#welcomeButton:focus #welcomeSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#welcomeButton:active #welcomeSVG .arrowLine { - stroke: #c7c7c7; -} -#welcomeButton:active #welcomeSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#welcomeButton:disabled #welcomeSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -#welcomeButtonContent { - display: flex; - align-items: center; -} - -/******************************************************************************* - * * - * Login View (login.ejs) * - * * - ******************************************************************************/ - -/* Styles for dimmer login span. */ -.loginSpanDim { - font-size: 12px; - color: #848484; - font-weight: bold; -} - -/* Main login container. */ -#loginContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - transition: filter 0.25s ease; - background: rgba(0, 0, 0, 0.50); -} - -/* Login cancel button styles. */ -#loginCancelContainer { - position: absolute; - top: 5%; - right: 5%; -} - -/* Login cancel button styles. */ -#loginCancelButton { - background: none; - border: none; - outline: none; - cursor: pointer; - transition: 0.25s ease; -} -#loginCancelButton:hover #loginCancelIcon, -#loginCancelButton:hover #loginCancelText, -#loginCancelButton:focus #loginCancelIcon, -#loginCancelButton:focus #loginCancelText { - text-shadow: 0px 0px 20px white; -} -#loginCancelButton:hover #loginCancelIcon, -#loginCancelButton:focus #loginCancelIcon { - box-shadow: 0px 0px 20px white; -} -#loginCancelButton:active #loginCancelIcon, -#loginCancelButton:active #loginCancelText { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); - border-color: rgba(255, 255, 255, 0.75); -} -#loginCancelButton:active #loginCancelIcon { - box-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); -} -#loginCancelButton:disabled { - pointer-events: none; -} -#loginCancelButton:disabled #loginCancelIcon, -#loginCancelButton:disabled #loginCancelText { - color: rgba(255, 255, 255, 0.75); - border-color: rgba(255, 255, 255, 0.75); -} - -/* The X in a circle icon for the cancel button. */ -#loginCancelIcon { - border-radius: 50%; - border: 1px solid white; - box-sizing: border-box; - height: 30px; - width: 30px; - font-size: 19px; - line-height: 30px; - margin: 0 auto; - margin-bottom: 5px; - transition: 0.25s ease; -} -/* Text for the login cancel button. */ -#loginCancelText { - font-size: 15px; - transition: 0.25s ease; -} - -/* Login content wrapper. */ -#loginContent { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding: 0px 25px; -} - -/* Login form. */ -#loginForm { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* Login form anchor styles. */ -#loginForm a { - font-size: 12px; - color: #848484; - font-weight: bold; - text-decoration: none; - transition: 0.25s ease; -} -#loginForm a:hover, -#loginForm a:focus { - color: #a2a2a2; - outline: none; -} -#loginForm a:active { - color: #8b8b8b; -} - -/* Logo on login form. */ -#loginImageSeal { - border-radius: 50%; - border: 2px solid #cad7e1; - background: rgba(1, 2, 1, 0.5); - height: 125px; - width: 125px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - margin-bottom: 20px; -} - -/* Header on login view. */ -#loginSubheader { - font-family: 'Pretendard Medium'; - margin-bottom: 25px; - font-size: 12px; - letter-spacing: 1px; - font-weight: bold; -} - -/* Container to organize login field elements. */ -.loginFieldContainer { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* SVG icons on the login view. */ -.loginSVG { - fill: #fff; - height: 20px; - width: 20px; -} - -/* Span which displays errors related to login field content. */ -.loginErrorSpan { - font-family: 'Pretendard Medium'; - font-weight: bold; - font-size: 8px; - color: #ff1b0c; - width: 100%; - text-align: right; - position: absolute; - top: 7px; - opacity: 0; - transition: 0.25s ease; -} - -.shake { - animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; -} - -@keyframes shake { - 10%, 90% { - transform: translate3d(-1px, 0, 0); - } - - 20%, 80% { - transform: translate3d(2px, 0, 0); - } - - 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); - } - - 40%, 60% { - transform: translate3d(4px, 0, 0); - } -} - -/* Login text input styles. */ -.loginField { - font-family: 'Pretendard Regular'; - background: none; - border-width: 1.5px 0px 0px 0px; - border-style: solid; - width: 250px; - margin-bottom: 20px; - border-color: #fff; - color: rgba(255, 255, 255, 0.75); - font-weight: bold; - text-align: center; - box-sizing: border-box; - padding: 7.5px; - font-size: 10px; - letter-spacing: 1px; -} -.loginField:focus { - outline: none; -} -.loginField:disabled { - color: rgba(255, 255, 255, 0.50); -} -.loginField::-webkit-input-placeholder { - color: rgba(255, 255, 255, 0.75); - font-size: 10px; - letter-spacing: 1px; - text-align: center; - font-weight: bold; -} -.loginField:focus::-webkit-input-placeholder { - color: transparent; -} - -/* Add spacing between password field and options bar. */ -#labelPassword { - margin-bottom: 13px; -} - -/* Container which contains the forgot and remember options. */ -#loginOptions { - display: flex; - justify-content: space-between; - width: 100%; -} - -/* Remember option text. */ -#loginRememberText { - padding-right: 10px; - transition: 0.25s ease; -} - -/* Login button styles. */ -#loginButton { - background: none; - font-weight: bold; - letter-spacing: 2px; - border: none; - padding: 15px 5px; - margin: 10px 0px; - cursor: pointer; - position: relative; - right: -20px; - transition: 0.5s ease; -} -#loginButton:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} -#loginButton[loading] { - color: #fff; -} -#loginButton:hover, -#loginButton:focus { - text-shadow: 0px 0px 20px #fff; - outline: none; -} -#loginButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} -#loginSVG { - -webkit-transform: translate3d(0, 0, 0); - overflow: visible; - transform: rotate(90deg); - margin-left: 20px; - transition: 0.25s ease; - width: 20px; - height: 20px; -} -#loginButton:hover #loginSVG, -#loginButton:focus #loginSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#loginButton:active #loginSVG .arrowLine { - stroke: #c7c7c7; -} -#loginButton:active #loginSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#loginButton:disabled #loginSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -#loginButtonContent { - display: flex; - align-items: center; -} - -#loginButton .circle-loader, -#loginButton[loading] #loginSVG { - display: none; -} -#loginButton[loading] .circle-loader, -#loginButton #loginSVG { - display: initial; -} - - -.circle-loader { - margin-left: 20px; - border: 2px solid rgba(255, 255, 255, 0.5); - border-left-color: #ffffff; - animation-name: loader-spin; - animation-duration: 1s; - animation-iteration-count: infinite; - animation-timing-function: linear; - position: relative; - display: inline-block; - vertical-align: top; - border-radius: 50%; - width: 16px; - height: 16px; -} -.load-complete { - animation: none; - border-color: #ffffff; - transition: border 500ms ease-out; -} -.checkmark { - display: none; -} -.checkmark.draw:after { - animation-duration: 800ms; - animation-timing-function: ease; - animation-name: checkmark; - transform: scaleX(-1) rotate(135deg); -} -.checkmark:after { - opacity: 1; - height: 8px; - width: 4px; - transform-origin: left top; - border-right: 2px solid #ffffff; - border-top: 2px solid #ffffff; - content: ''; - left: 2px; - top: 8px; - position: absolute; -} -@keyframes loader-spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -@keyframes checkmark { - 0% { - height: 0; - width: 0; - opacity: 1; - } - 20% { - height: 0; - width: 4px; - opacity: 1; - } - 40% { - height: 8px; - width: 4px; - opacity: 1; - } - 100% { - height: 8px; - width: 4px; - opacity: 1; - } -} - - - -/*.spinningCircle { - margin-left: 20px; - height: 16px; - width: 16px; - border-radius: 50%; - border: 2px solid rgba(255,255,255,0); - border-top-color: #ffffff; - border-right-color: #ffffff; - border-left-color: rgba(255, 255, 255, 0.50); - border-bottom-color: rgba(255, 255, 255, 0.50); - animation: single2 4s infinite linear; -} - -@keyframes single2 { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(720deg); - } -}*/ - -/* Disclaimer container. */ -#loginDisclaimer { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -/* Add spacing between register anchor and disclaimer. */ -#loginRegisterSpan { - margin-bottom: 5px; -} - -/* Disclaimer text styles. */ -.loginDisclaimerText { - font-size: 7px; - color: #848484; - font-weight: bold; - text-align: center; -} - -/* * * -* Login View | Custom Checkbox -* * */ - -/* Checkbox container. */ -#checkmarkContainer { - display: flex; - justify-content: flex-end; - align-items: center; - position: relative; - cursor: pointer; - font-size: 22px; - -webkit-user-select: none; -} - -/* Hide the default checkbox. */ -#checkmarkContainer input { - opacity: 0; - cursor: pointer; - position: absolute; -} - -/* Create a custom checkbox. */ -.loginCheckmark { - position: relative; - height: 10px; - width: 10px; - border: 1px solid #848484; - border-radius: 1px; - background: none; - transition: 0.25s ease; -} -/* On hover and focus, add a grey border color. */ -#checkmarkContainer:hover input ~ *, -#checkmarkContainer input:focus ~ * { - color: #a2a2a2; - border-color: #a2a2a2; -} -/* On keydown, darken the checkbox a bit. */ -#checkmarkContainer input:active ~ *:not(#loginRememberText) { - color: #8d8d8d; - border-color: #8d8d8d; -} -#checkmarkContainer[disabled] { - pointer-events: none; -} -/* For checked -> #checkmarkContainer input:checked ~ * */ -/* Create the checkmark/indicator (hidden when not checked). */ -.loginCheckmark:after { - content: ""; - display: none; -} -/* Show the checkmark when checked. */ -#checkmarkContainer input:checked ~ .loginCheckmark:after { - display: block; -} -/* Style the checkmark/indicator. */ -#checkmarkContainer .loginCheckmark:after { - position: absolute; - left: 3.5px; - top: 0.5px; - width: 2px; - height: 6px; - border: solid #a2a2a2; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} - -/* -#login_filter { - height: calc(100% - 22px); - width: 100%; - z-index: 9000; - position: absolute; - filter: blur(8px) contrast(0.9) brightness(1.0); - background: url('./../images/backgrounds/0.jpg') no-repeat center center fixed; - transform: scale(1.2); - background-size: cover; -} -*/ - -/******************************************************************************* - * * - * Waiting View (waiting.ejs) * - * * - ******************************************************************************/ - -#waitingContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - transition: filter 0.25s ease; - background: rgba(0, 0, 0, 0.50); -} - -#waitingContent { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 50%; - top: -10%; - position: relative; -} - -.waitingSpinner:before { - transform: rotateX(60deg) rotateY(45deg) rotateZ(45deg); - animation: 750ms rotateBefore infinite linear reverse; -} -.waitingSpinner:after { - transform: rotateX(240deg) rotateY(45deg) rotateZ(45deg); - animation: 750ms rotateAfter infinite linear; -} -.waitingSpinner:before, -.waitingSpinner:after { - box-sizing: border-box; - content: ''; - display: block; - position: fixed; - top: calc(50% - 5em); - /* left: 50%; */ - margin-top: -5em; - margin-left: -5em; - width: 10em; - height: 10em; - transform-style: preserve-3d; - transform-origin: 50%; - transform: rotateY(50%); - perspective-origin: 50% 50%; - perspective: 340px; - background-size: 10em 10em; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2NnB4IiBoZWlnaHQ9IjI5N3B4IiB2aWV3Qm94PSIwIDAgMjY2IDI5NyIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8dGl0bGU+c3Bpbm5lcjwvdGl0bGU+CiAgICA8ZGVzY3JpcHRpb24+Q3JlYXRlZCB3aXRoIFNrZXRjaCAoaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoKTwvZGVzY3JpcHRpb24+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8cGF0aCBkPSJNMTcxLjUwNzgxMywzLjI1MDAwMDM4IEMyMjYuMjA4MTgzLDEyLjg1NzcxMTEgMjk3LjExMjcyMiw3MS40OTEyODIzIDI1MC44OTU1OTksMTA4LjQxMDE1NSBDMjE2LjU4MjAyNCwxMzUuODIwMzEgMTg2LjUyODQwNSw5Ny4wNjI0OTY0IDE1Ni44MDA3NzQsODUuNzczNDM0NiBDMTI3LjA3MzE0Myw3NC40ODQzNzIxIDc2Ljg4ODQ2MzIsODQuMjE2MTQ2MiA2MC4xMjg5MDY1LDEwOC40MTAxNTMgQy0xNS45ODA0Njg1LDIxOC4yODEyNDcgMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IDE0NS4yNzczNDQsMjk2LjY2Nzk2OCBDMTQ1LjI3NzM0NCwyOTYuNjY3OTY4IC0yNS40NDkyMTg3LDI1Ny4yNDIxOTggMy4zOTg0Mzc1LDEwOC40MTAxNTUgQzE2LjMwNzA2NjEsNDEuODExNDE3NCA4NC43Mjc1ODI5LC0xMS45OTIyOTg1IDE3MS41MDc4MTMsMy4yNTAwMDAzOCBaIiBpZD0iUGF0aC0xIiBmaWxsPSIjZmZmZmZmIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPg==); -} - -#waitingTextContainer { - position: fixed; - top: 50%; -} - -@keyframes rotateBefore { - from { - transform: rotateX(60deg) rotateY(45deg) rotateZ(0deg); - } - to { - transform: rotateX(60deg) rotateY(45deg) rotateZ(-360deg); - } -} - -@keyframes rotateAfter { - from { - transform: rotateX(240deg) rotateY(45deg) rotateZ(0deg); - } - to { - transform: rotateX(240deg) rotateY(45deg) rotateZ(360deg); - } -} - -/******************************************************************************* - * * - * Login Options View (loginOptions.ejs) * - * * - ******************************************************************************/ - -#loginOptionsContainer { - position: relative; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; - transition: filter 0.25s ease; - background: rgba(0, 0, 0, 0.50); -} - -#loginOptionsContent { - border-radius: 3px; - position: relative; - top: -5%; -} - -.loginOptionsMainContent { - display: flex; - flex-direction: column; - align-items: center; -} - -.loginOptionActions { - display: flex; - flex-direction: column; - row-gap: 10px; -} - -.loginOptionButtonContainer { - width: 16em; -} - -.loginOptionButton { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - height: 50px; - width: 100%; - text-align: left; - padding: 0px 25px; - cursor: pointer; - outline: none; - transition: 0.25s ease; - display: flex; - align-items: center; - column-gap: 5px; -} -.loginOptionButton:hover, -.loginOptionButton:focus { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} - -#loginOptionCancelContainer { - position: absolute; - bottom: -100px; -} - -#loginOptionCancelButton { - background: none; - border: none; - padding: 2px 0px; - font-size: 16px; - font-weight: bold; - color: lightgrey; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -#loginOptionCancelButton:hover, -#loginOptionCancelButton:focus { - text-shadow: 0px 0px 20px lightgrey; -} -#loginOptionCancelButton:active { - text-shadow: 0px 0px 20px rgba(211, 211, 211, 0.75); - color: rgba(211, 211, 211, 0.75); -} -#loginOptionCancelButton:disabled { - color: rgba(211, 211, 211, 0.75); - pointer-events: none; -} - - -/******************************************************************************* - * * - * Settings View (sttings.ejs) * - * * - ******************************************************************************/ - -/* Main settings container. */ -#settingsContainer { - position: relative; - height: 100%; - display: flex; - background-color: rgba(0, 0, 0, 0.50); - transition: background-color 0.25s cubic-bezier(.02, .01, .47, 1); -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#settingsContainer:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#settingsContainer[scrolled]:before { - opacity: 1; -} - -/* Left hand side of the settings UI, for navigation. */ -#settingsContainerLeft { - display: flex; - flex-direction: column; - padding-top: 4%; - height: 100%; - width: 25%; - box-sizing: border-box; -} - -.settingsTopActions { - padding: 0 0 18px 24px; -} - -/* Settings navigation container. */ -#settingsNavContainer { - height: 100%; - flex: 1; - display: flex; - flex-direction: column; -} - -/* Navigation header styles. */ -#settingsNavHeader { - height: 15%; - display: flex; - justify-content: center; -} -#settingsNavHeaderText { - font-size: 20px; -} - -/* Navigation items outer container. */ -#settingsNavItemsContainer { - height: 85%; - display: flex; - justify-content: center; - box-sizing: border-box; -} - -/* Navigation items content container. */ -#settingsNavItemsContent { - height: 100%; - display: flex; - flex-direction: column; - position: relative; -} - -/* Navigation item shared styles. */ -.settingsNavItem { - background: none; - border: none; - text-align: left; - margin: 5px 0px; - padding: 0px 20px; - color: grey; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -.settingsNavItem:hover, -.settingsNavItem:focus { - color: #c1c1c1; - text-shadow: 0px 0px 20px #c1c1c1; -} -.settingsNavItem[selected] { - cursor: default; - color: white; - text-shadow: none; -} - -/* Div to add some space between nav items. */ -.settingsNavSpacer { - height: 25px; -} - -/* Content container for the done button. */ -#settingsNavContentBottom { - position: absolute; - top: 65%; -} - -/* Settings navigational divider. */ -.settingsNavDivider { - width: 75%; - height: 1px; - background: rgba(126, 126, 126, 0.57); - margin-left: auto; - margin-bottom: 25px; -} - -/* Settings done button styles. */ -#settingsNavDone { - background: none; - border: none; - text-align: left; - margin: 5px 0px; - padding: 0px 20px; - color: white; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -#settingsNavDone:hover, -#settingsNavDone:focus { - text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; -} -#settingsNavDone:active { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} -#settingsNavDone:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} - -/* Right hand side of the settings container, for tabs. */ -#settingsContainerRight { - height: 100%; - width: 75%; - box-sizing: border-box; -} - -/* Settings tab shared styles. */ -.settingsTab { - width: 100%; - height: 100%; - overflow-y: auto; -} -.settingsTab::-webkit-scrollbar { - width: 2px; -} -.settingsTab::-webkit-scrollbar-track { - display: none; -} -.settingsTab::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Add spacing to the top of each settings tab. */ -.settingsTab > *:first-child { - margin-top: 5%; -} - -/* Add spacing to the bottom of each settings tab. */ -.settingsTab > *:last-child { - margin-bottom: 20%; -} - -/* Tab header shared styles. */ -.settingsTabHeader { - display: flex; - flex-direction: column; - margin-bottom: 20px; -} -.settingsTabHeaderText { - font-size: 20px; - font-family: 'Pretendard Medium'; -} -.settingsTabHeaderDesc { - font-size: 12px; -} - -/* Selected server content container */ -.settingsSelServContainer { - background: rgba(0, 0, 0, 0.25); - width: 75%; - border-radius: 3px; - display: flex; - justify-content: space-between; - margin: 15px 0px; -} - -/* Div which will be populated with the selected server's information. */ -.settingsSelServContent { - display: flex; - align-items: center; - justify-content: flex-start; - padding: 5px 0px; -} - -/* Wrapper container for the switch server button. */ -.settingsSwitchServerContainer { - display: flex; - align-items: center; - padding: 15px; -} - -/* Button to switch server configurations on the mods tab. */ -.settingsSwitchServerButton { - opacity: 0; - border: 1px solid rgb(255, 255, 255); - color: rgb(255, 255, 255); - background: none; - font-size: 12px; - border-radius: 3px; - font-family: 'Pretendard Medium'; - transition: 0.25s ease; - cursor: pointer; - outline: none; -} -.settingsSwitchServerButton:hover, -.settingsSwitchServerButton:focus { - box-shadow: 0px 0px 20px rgb(255, 255, 255); - background: rgba(255, 255, 255, 0.25); -} -.settingsSwitchServerButton:active { - box-shadow: 0px 0px 20px rgb(187, 187, 187); - background: rgba(187, 187, 187, 0.25); - border: 1px solid rgb(187, 187, 187); - color: rgb(187, 187, 187); -} -.settingsSelServContainer:hover .settingsSwitchServerButton { - opacity: 1; -} - -/* Remove spin button from number inputs. */ -#settingsContainer input[type=number]::-webkit-inner-spin-button { - -webkit-appearance: none; -} - -/* Default styles for text/number inputs. */ -#settingsContainer input[type=number], -#settingsContainer input[type=text] { - color: white; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - border: 1px solid rgba(126, 126, 126, 0.57); - font-family: 'Pretendard Regular'; - transition: 0.25s ease; -} -#settingsContainer input[type=number]:focus, -#settingsContainer input[type=text]:focus { - outline: none; - border-color: rgba(126, 126, 126, 0.87); -} -#settingsContainer input[type=number][error] { - border-color: rgb(255, 27, 12); - background: rgba(236, 0, 0, 0.25); - color: rgb(255, 27, 12); -} - -/* Styles for a generic settings entry. */ -.settingsFieldContainer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 20px 0px; - width: 75%; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); -} -.settingsFieldLeft { - display: flex; - flex-direction: column; -} -.settingsFieldTitle { - font-size: 14px; - font-family: 'Pretendard Medium'; - color: rgba(255, 255, 255, 0.95); -} -.settingsFieldDesc { - font-size: 12px; - color: rgba(255, 255, 255, .95); - margin-top: 5px; -} -.settingsDivider { - height: 1px; - width: 75%; - background: rgba(255, 255, 255, 0.25); -} - -/* Toggle Switch */ -.toggleSwitch { - position: relative; - display: inline-block; - width: 40px; - height: 20px; - border-radius: 50px; - box-sizing: border-box; -} -.toggleSwitch input { - display:none; -} -.toggleSwitchSlider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(255, 255, 255, 0.35); - transition: .4s; - border-radius: 50px; - border: 1px solid rgba(126, 126, 126, 0.57); -} -.toggleSwitchSlider:before { - position: absolute; - content: ""; - height: 13px; - width: 16px; - left: 3px; - bottom: 3px; - background-color: white; - box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.75); - border-radius: 50px; - transition: .4s; -} -input:checked + .toggleSwitchSlider { - background-color: rgb(31, 140, 11); - /* box-shadow: inset 2px 1px 20px black; */ - border: 1px solid rgb(31, 140, 11); -} -input:checked + .toggleSwitchSlider:before { - transform: translateX(15px); -} - -/* Range Slider styles. */ -.rangeSlider { - width: 35%; - height: 5px; - margin: 15px 0px; - background: grey; - border-radius: 3px; - position: relative; -} -.rangeSliderBar { - position: absolute; - background: #8be88b; - width: 50%; - height: 5px; - border-radius: 3px 0px 0px 3px; - transition: background 0.25s ease; -} -.rangeSliderTrack { - position: absolute; - top: -7.5px; - width: 7px; - height: 20px; - background: white; - border-radius: 3px; - left: 50%; - cursor: ew-resize; -} - -/* File selectors */ - -/* Main container for File selectors. */ -.settingsFileSelContainer { - display: flex; - flex-direction: column; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - margin-bottom: 20px; - margin-top: 20px; - width: 75%; -} - -/* File selector title. */ -.settingsFileSelTitle { - margin-bottom: 10px; -} - -/* Wrapper container for the actionable elements. */ -.settingsFileSelActions { - display: flex; - width: 90%; -} - -/* File selector icon settings. */ -.settingsFileSelIcon { - display: flex; - align-items: center; - background: rgba(126, 126, 126, 0.57); - border-radius: 3px 0px 0px 3px; - padding: 5px; - transition: 0.25s ease; -} -.settingsFileSelSVG { - width: 20px; - height: 20px; - fill: white; -} - -/* Disabled text field which stores the selected file path. */ -.settingsFileSelVal { - border-radius: 0px !important; - width: 100%; - padding: 5px 10px; - font-size: 12px; - height: 30px; -} - -/* File selection button. */ -.settingsFileSelButton { - border: 0px; - border-radius: 0px 3px 3px 0px; - font-size: 12px; - padding: 0px 5px; - cursor: pointer; - background: rgba(126, 126, 126, 0.57); - transition: 0.25s ease; - white-space: nowrap; - outline: none; -} -.settingsFileSelButton:hover, -.settingsFileSelButton:focus { - text-shadow: 0px 0px 20px white; -} -.settingsFileSelButton:active { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Description for the file selector. */ -.settingsFileSelDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - width: 89%; -} -.settingsFileSelDesc strong { - font-family: 'Pretendard Medium'; -} - -/* * * -* Settings View (Account Tab) -* * */ - -.settingsAuthAccountTypeContainer { - display: flex; - width: 75%; - flex-direction: column; -} - -.settingsAuthAccountTypeHeader { - display: flex; - align-items: center; - width: 100%; - justify-content: space-between; - padding: 10px 0px; - border-bottom: 1px solid #ffffff85; - margin-bottom: 30px; -} - -.settingsAuthAccountTypeHeaderLeft { - display: flex; - column-gap: 5px; -} - -/* Settings add account button styles. */ -.settingsAddAuthAccount { - background: none; - border: none; - text-align: left; - padding: 2px 0px; - color: white; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -.settingsAddAuthAccount:hover, -.settingsAddAuthAccount:focus { - text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; -} -.settingsAddAuthAccount:active { - text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75), 0px 0px 20px rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} -.settingsAddAuthAccount:disabled { - color: rgba(255, 255, 255, 0.75); - pointer-events: none; -} - -/* Auth account list container styles. */ -.settingsCurrentAccounts { - margin-bottom: 5%; -} -.settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) { - margin-bottom: 10px; -} -.settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) { - margin-top: 10px; -} - -/* Auth account shared styles. */ -.settingsAuthAccount { - display: flex; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - border: 1px solid rgba(126, 126, 126, 0.57); -} - -/* Left hand side of an auth account element, for the skin image. */ -.settingsAuthAccountLeft { - padding: 5px 5px 5px 20px; -} - -/* Image of the auth account's skin. */ -.settingsAuthAccountImage { - height: 115px; -} - -/* Right hand side of the auth account, for info + actions. */ -.settingsAuthAccountRight { - display: flex; - width: 100%; -} - -/* Account details container. */ -.settingsAuthAccountDetails { - display: flex; - flex-direction: column; - justify-content: center; - margin-left: 20px; - width: 100%; -} -.settingsAuthAccountDetails > *:not(:last-child) { - margin-bottom: 20px; -} - -/* Account detail element styles. */ -.settingsAuthAccountDetailPane { - display: flex; - flex-direction: column; -} -.settingsAuthAccountDetailTitle { - font-size: 12px; - color: grey; - font-weight: bold; - font-family: 'Pretendard Medium'; -} -.settingsAuthAccountDetailValue { - font-size: 14px; - -webkit-user-select: initial; -} - -/* Account actions container. */ -.settingsAuthAccountActions { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-end; - padding: 10px; -} - -/* Account select button shared styles. */ -.settingsAuthAccountSelect { - opacity: 0; - border: none; - white-space: nowrap; - background: none; - font-family: 'Pretendard Medium'; - outline: none; - transition: 0.25s ease; -} -.settingsAuthAccountSelect:hover:not([selected]), -.settingsAuthAccountSelect:focus:not([selected]) { - text-shadow: 0px 0px 20px white, 0px 0px 20px white; - cursor: pointer; -} -.settingsAuthAccount:hover .settingsAuthAccountSelect:not([selected]), -.settingsAuthAccountSelect[selected] { - opacity: 1; -} -.settingsAuthAccountSelect[selected] { - pointer-events: none; -} - -/* Account logout button shared styles. */ -.settingsAuthAccountLogOut { - opacity: 0; - border: 1px solid rgb(241, 55, 55); - color: rgb(241, 55, 55); - background: none; - font-size: 12px; - border-radius: 3px; - font-family: 'Pretendard Medium'; - transition: 0.25s ease; - cursor: pointer; - outline: none; -} -.settingsAuthAccountLogOut:hover, -.settingsAuthAccountLogOut:focus { - box-shadow: 0px 0px 20px rgb(241, 55, 55); - background: rgba(241, 55, 55, 0.25); -} -.settingsAuthAccountLogOut:active { - box-shadow: 0px 0px 20px rgb(185, 47, 47); - background: rgba(185, 47, 47, 0.25); - border: 1px solid rgb(185, 47, 47); - color: rgb(185, 47, 47); -} -.settingsAuthAccount:hover .settingsAuthAccountLogOut { - opacity: 1; -} - -/* * * -* Settings View (Minecraft Tab) -* * */ - -/* Game resolution UI elements. */ -#settingsGameResolutionContainer { - display: flex; - flex-direction: column; - padding-bottom: 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - width: 75%; -} -#settingsGameResolutionContent { - display: flex; - align-items: center; - padding-top: 10px; -} -#settingsGameResolutionCross { - color: grey; - padding: 0px 15px; -} -#settingsGameWidth, -#settingsGameHeight { - padding: 7.5px 5px; - width: 75px; -} - -/* * * -* Settings View (Mods Tab) -* * */ - -/* Main content container for the mod elements. */ -#settingsModsContainer { - width: 75%; -} - -/* Mod sub-container header text. */ -.settingsModsHeader { - padding-bottom: 10px; - border-bottom: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 10px; -} - -/* Mod elements sub-containers. */ -#settingsReqModsContainer, -#settingsOptModsContainer, -#settingsDropinModsContainer { - padding-bottom: 25px; -} - -/* Main content containers for mod elements. */ -#settingsReqModsContent, -#settingsOptModsContent, -#settingsDropinModsContent { - font-size: 12px; - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - color: white; -} - -/* Mod elements. */ -.settingsMod, -.settingsDropinMod { - padding: 10px; -} -.settingsSubMod { - padding: 10px 0px 10px 15px; - margin-left: 20px; - border-left: 1px solid rgba(255, 255, 255, 0.5); -} - -/* Main content container for mod element information. */ -.settingsModContent { - display: flex; - align-items: center; - justify-content: space-between; - transition: opacity 0.25s ease; -} - -/* Wrapper container for the left side of a mod element. */ -.settingsModMainWrapper { - display: flex; - align-items: center; -} - -/* Mod enabled/disabled status. */ -.settingsModStatus { - width: 7px; - height: 7px; - border-radius: 50%; - background-color: #c32625; - margin-right: 15px; - transition: 0.25s ease; -} - -/* Mod details container. */ -.settingsModDetails { - display: flex; - flex-direction: column; -} - -/* The version of the mod. */ -.settingsModVersion { - color: grey; - font-size: 10px; -} - -/* Disabled toggleswitch for required mods. */ -.toggleSwitch[reqmod] { - filter: grayscale(49%) brightness(60%); - pointer-events: none; -} - -/* Set the status color of an enabled mod. */ -.settingsBaseMod[enabled] > .settingsModContent > .settingsModMainWrapper > .settingsModStatus { - background-color: rgb(165, 195, 37); -} - -/* Add opacity to submods of a disabled mod. */ -.settingsBaseMod:not([enabled]) > .settingsSubModContainer .settingsModContent { - opacity: 0.5; -} - -/* Curve the left border for submods. */ -.settingsSubModContainer > .settingsSubMod:first-child { - border-top-left-radius: 10px; -} -.settingsSubModContainer > .settingsSubMod:last-child { - border-bottom-left-radius: 10px; -} -.settingsSubModContainer > .settingsSubMod:only-child { - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; -} - -/* Wrapper container for all submods. */ -.settingsSubModContainer { - margin-top: 10px; -} - -/* Button to open the mods folder for drop-in mods. */ -#settingsDropinFileSystemButton { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - height: 50px; - width: 100%; - text-align: left; - padding: 0px 50px; - cursor: pointer; - outline: none; - transition: 0.25s ease; - margin-bottom: 10px; -} -#settingsDropinFileSystemButton:hover, -#settingsDropinFileSystemButton:focus, -#settingsDropinFileSystemButton[drag] { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} -/* Refresh instructions on the file system button. */ -#settingsDropinRefreshNote { - font-size: 10px; - pointer-events: none; -} - -/* Button to remove drop-in mods. */ -.settingsDropinRemoveButton { - background: none; - border: none; - font-size: 10px; - text-align: left; - padding: 0px; - color: #c32625; - font-weight: bold; - cursor: pointer; - outline: none; - transition: 0.25s ease; -} -.settingsDropinRemoveButton:hover, -.settingsDropinRemoveButton:focus { - text-shadow: 0px 0px 20px #c32625, 0px 0px 20px #c32625, 0px 0px 20px #c32625; -} -.settingsDropinRemoveButton:active { - color: #9b1f1f; - text-shadow: 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f, 0px 0px 20px #9b1f1f; -} - -/* Shaderpack settings description. */ -#settingsShaderpackDesc { - font-size: 10px; - margin: 10px 0px; - color: lightgrey; - font-weight: bold; - width: 89%; -} - -/* Wrapper container. */ -#settingsShaderpackWrapper { - display: flex; -} - -/* Button to add shaderpacks. */ -#settingsShaderpackButton { - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - cursor: pointer; - outline: none; - transition: 0.25s ease; - font-size: 14px; - padding: 6px 11px; - margin-right: 5px; -} -#settingsShaderpackButton:hover, -#settingsShaderpackButton:focus, -#settingsShaderpackButton[drag] { - background: rgba(54, 54, 54, 0.25); - text-shadow: 0px 0px 20px white; -} - -/* Main select container. */ -.settingsSelectContainer { - position: relative; - width: 50%; -} - -/* Div which displays the selected option. */ -.settingsSelectSelected { - border-radius: 3px; - border-width: 1px; - font-size: 14px; - padding: 6px 16px; -} - -/* Style the arrow inside the select element. */ -.settingsSelectSelected:after { - position: absolute; - content: ""; - top: calc(50% - 3px); - right: 10px; - width: 0; - height: 0; - border: 6px solid transparent; - border-color: rgba(126, 126, 126, 0.57) transparent transparent transparent; -} - -/* Point the arrow upwards when the select box is open (active). */ -.settingsSelectSelected.select-arrow-active:after { - border-color: transparent transparent rgba(126, 126, 126, 0.57) transparent; - top: 7px; -} -.settingsSelectSelected.select-arrow-active { - border-radius: 3px 3px 0px 0px; -} - -/* Options content container. */ -.settingsSelectOptions { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 99; - max-height: 300%; - overflow-y: scroll; - border: 1px solid rgba(126, 126, 126, 0.57); - border-top: none; - border-radius: 0px 0px 3px 3px; -} -/* Hide the items when the select box is closed. */ -.settingsSelectOptions[hidden] { - display: none; -} -.settingsSelectOptions::-webkit-scrollbar { - width: 2px; -} -.settingsSelectOptions::-webkit-scrollbar-track { - display: none; -} -.settingsSelectOptions::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Shared styles between options and selection div. */ -.settingsSelectOptions div, -.settingsSelectSelected { - background: rgba(0, 0, 0, 0.25); - border-style: solid; - border-color: rgba(126, 126, 126, 0.57); - color: #ffffff; - cursor: pointer; -} -.settingsSelectOptions div { - border-width: 0px 0px 1px 0px; - font-size: 12px; - padding: 4px 16px; -} -.settingsSelectOptions div:last-child { - border-bottom: none; -} - -/* Hover + selected styles. */ -.settingsSelectOptions div:hover, .settingsSelectOptions div[selected] { - background-color: rgba(255, 255, 255, 0.25) !important; -} - -/* * * -* Settings View (Java Tab) -* * */ - -/* Style links on the Java tab. */ -#settingsTabJava a, -.settingsChangelogText a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#settingsTabJava a:hover, -#settingsTabJava a:focus, -.settingsChangelogText a:hover, -.settingsChangelogText a:focus { - color: rgba(255, 255, 255, 0.75); -} -#settingsTabJava a:active, -.settingsChangelogText a:active { - color: rgba(165, 165, 165, 0.75); -} - -/* Main container for memory management. */ -#settingsMemoryContainer { - width: 75%; - display: flex; - flex-direction: column; - border-bottom: 1px solid rgba(255, 255, 255, 0.50); - margin-bottom: 20px; -} - -/* Memory management title. */ -#settingsMemoryTitle { - margin-bottom: 10px; - padding-bottom: 5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.5); -} - -/* Memory management content. */ -#settingsMemoryContent { - display: flex; - justify-content: space-between; - width: 100%; -} -#settingsMemoryContentLeft { - width: 69%; -} -#settingsMemoryContentRight { - display: flex; - align-items: center; - margin-right: 10%; -} - -/* Header for memory sliders. */ -.settingsMemoryHeader { - font-size: 14px; -} - -/* Wrapper container for a memory slider and label. */ -.settingsMemoryActionContainer { - display: flex; - align-items: center; - justify-content: space-between; -} - -/* Label which displays a memory slider's value. */ -.settingsMemoryLabel { - font-size: 14px; - margin-right: 2%; -} - -/* Range sliders for min and max memory settings. */ -#settingsMaxRAMRange, -#settingsMinRAMRange { - width: 85%; -} - -/* Memory status elements. */ -#settingsMemoryStatus { - display: flex; - flex-direction: column; -} -#settingsMemoryStatus > .settingsMemoryStatusContainer:not(:last-child){ - margin-bottom: 50%; -} -.settingsMemoryStatusContainer { - display: flex; - flex-direction: column; - align-items: center; -} -.settingsMemoryStatusTitle { - font-size: 12px; - color: grey; - font-weight: bold; -} -.settingsMemoryStatusValue { - color: lightgrey; - font-size: 16px; -} - -/* Description for memory management. */ -#settingsMemoryDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - font-weight: bold; -} - -/* Status text which displays details on the selected executable. */ -#settingsJavaExecDetails { - font-weight: bold; - color: grey; - font-size: 12px; -} - -/* Main container for the JVM options setting. */ -#settingsJVMOptsContainer { - width: 75%; -} - -/* JVM options title. */ -#settingsJVMOptsTitle { - margin-bottom: 10px; -} - -/* Wrapper container for the actionable elements. */ -#settingsJVMOptsContent { - display: flex; - width: 90%; -} - -/* Text field to input the JVM options. */ -#settingsJVMOptsVal { - border-radius: 0px 3px 3px 0px !important; - width: 100%; - padding: 5px 10px; - font-size: 12px; -} -#settingsJVMOptsContent:focus-within > .settingsJavaIcon { - background: rgba(126, 126, 126, 0.87); -} - -/* Description for the JVM options setting. */ -#settingsJVMOptsDesc { - font-size: 10px; - margin: 20px 0px; - color: lightgrey; - font-weight: bold; - width: 89%; -} - -/* * * -* Settings View (Launcher Tab) -* * */ - -/* Tailored style for the data directory header. */ -#settingsDataDirTitle { - margin-bottom: 10px; -} - -/* * * -* Settings View (About Tab) -* * */ - -/* Main about content container. */ -#settingsAboutCurrentContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* About content. */ -#settingsAboutCurrentContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* About header elements. */ -#settingsAboutCurrentHeadline { - display: flex; - align-items: center; - padding-bottom: 5px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -#settingsAboutLogo { - width: 30px; - height: 30px; - padding: 5px; -} -#settingsAboutTitle { - font-size: 23px; - padding-left: 10px; -} - -/* Current version container. */ -#settingsAboutCurrentVersion { - display: flex; - align-items: center; - padding-top: 10px; -} - -/* Checkmark next to the version information. */ -#settingsAboutCurrentVersionCheck { - border-radius: 50%; - background: #23aa23; - text-align: center; - font-weight: bold; - margin: 11px 12px; - color: white; - height: 15px; - width: 15px; - font-size: 12px; - line-height: 17px; -} - -/* Current version details container. */ -#settingsAboutCurrentVersionDetails { - margin-left: 10px; -} - -/* Release type text. */ -#settingsAboutCurrentVersionTitle { - font-size: 12px; - font-family: 'Pretendard Medium'; - color: #23aa23; - font-weight: bold; -} - -/* Current version text. */ -#settingsAboutCurrentVersionLine { - font-size: 10px; - color: grey; - font-weight: bold; -} - -/* About information links. */ -#settingsAboutButtons { - display: flex; - padding: 0px 15px; - margin-bottom: 5px; -} -.settingsAboutButton { - background: none; - border: none; - font-size: 10px; - color: grey; - padding: 0px 5px; - transition: 0.25s ease; - outline: none; - text-decoration: none; -} -.settingsAboutButton:hover, -.settingsAboutButton:focus { - color: rgb(165, 165, 165); -} -.settingsAboutButton:active { - color: rgba(124, 124, 124, 0.75); -} - -/* Main changelog container. */ -.settingsChangelogContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* Changelog content container. */ -.settingsChangelogContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* Changelog header container. */ -.settingsChangelogHeadline { - padding-bottom: 10px; - margin-bottom: 10px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -/* Changelog header label. */ -.settingsChangelogLabel { - font-size: 12px; - color: grey; - font-weight: bold; -} - -/* Changelog text content container. */ -.settingsChangelogText { - font-size: 12px; -} - -/* Styles for the changelog elements. */ -.settingsChangelogText p { - margin-bottom: 16px; - line-height: 1.5; -} -.settingsChangelogText blockquote { - border-left: 0.25em solid rgba(126, 126, 126, 0.95); - margin: 0px; - padding: 0 0 0 1em; - color: rgba(255, 255, 255, 0.85); -} -.settingsChangelogText code { - padding: 0.1em 0.4em; - font-size: 85%; - background-color: rgba(255, 255, 255, 0.25); - color: white; - border-radius: 3px; - font-family: 'Pretendard Regular'; -} -.settingsChangelogText li+li { - margin-top: .25em; -} -.settingsChangelogText a.commit-link { - font-weight: 400; - color: #ffffff; - text-decoration: none; -} -.settingsChangelogText a.commit-link:hover { - text-decoration: underline !important; - text-decoration-color: black; -} -.settingsChangelogText tt { - padding: 0.1em 0.4em; - font-size: 86%; - background-color: white; - border-radius: 3px; - color: black; - font-weight: bold; -} -.settingsChangelogText a.commit-link:hover tt { - text-decoration: underline; - text-decoration-color: black; -} -.settingsChangelogText .highlight { - background: rgba(0, 0, 0, 0.30); - user-select: initial; - padding: 5px 10px; -} -.settingsChangelogText .highlight pre { - margin: 0px; -} - -/* Container for the changelog button. */ -.settingsChangelogActions { - padding: 0px 15px 5px 15px; -} - -/* Open changelog on GitHub. */ -.settingsChangelogButton { - padding: 0px; -} - -/* * * -* Settings View (Updates Tab) -* * */ - -/* Main about content container. */ -#settingsUpdateStatusContainer { - display: flex; - flex-direction: column; - background: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - width: 75%; - margin-bottom: 20px; -} - -/* Update content. */ -#settingsUpdateStatusContent { - display: flex; - flex-direction: column; - padding: 15px; -} - -/* Update header elements. */ -#settingsUpdateStatusHeadline { - display: flex; - align-items: center; - padding-bottom: 5px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} -#settingsUpdateTitle { - font-size: 16px; - padding-left: 10px; - font-weight: bold; -} - -/* Update version container. */ -#settingsUpdateVersion { - display: flex; - align-items: center; - padding: 10px 0px; - border-bottom: 1px solid rgba(126, 126, 126, 0.57); -} - -/* Checkmark next to the version information. */ -#settingsUpdateVersionCheck { - border-radius: 50%; - background: #23aa23; - text-align: center; - font-weight: bold; - margin: 11px 12px; - color: white; - height: 15px; - width: 15px; - font-size: 12px; - line-height: 17px; -} - -/* Update version details container. */ -#settingsUpdateVersionDetails { - margin-left: 10px; -} - -/* Release type text. */ -#settingsUpdateVersionTitle { - font-size: 12px; - font-family: 'Pretendard Medium'; - color: #23aa23; - font-weight: bold; -} - -/* Current version text. */ -#settingsUpdateVersionLine { - font-size: 10px; - color: grey; - font-weight: bold; -} - -/* Update action container. */ -#settingsUpdateActionContainer { - padding-top: 10px; - font-size: 14px; - font-weight: bold; -} - -/* Update action button styles. */ -#settingsUpdateActionButton { - display: flex; - flex-direction: column; - padding-left: 10px; - background: none; - border: none; - font-size: 14px; - font-weight: bold; - cursor: pointer; - outline: none; - text-align: left; - transition: 0.25s ease; -} -#settingsUpdateActionButton:hover, -#settingsUpdateActionButton:focus { - text-shadow: 0px 0px 20px white, 0px 0px 20px white, 0px 0px 20px white; -} -#settingsUpdateActionButton:active { - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; - color: #c7c7c7; -} -#settingsUpdateActionButton:disabled { - pointer-events: none; -} - -/******************************************************************************* - * * - * Landing View (Structural Styles) * - * * - ******************************************************************************/ - -/* Main content container. */ -#landingContainer { - height: 100%; - position: relative; - transition: background 2s ease; - overflow-y: hidden; -} - -/* Upper content container. */ -#landingContainer > #upper { - position: relative; - transition: top 2s ease; - top: 0px; - height: 82%; - display: flex; -} -#landingContainer > #upper > #left { - display: inline-flex; - width: 14%; - height: 100%; - justify-content: flex-end; -} -#landingContainer > #upper > #content { - display: inline-flex; - width: 58%; - height: 100%; -} -#landingContainer > #upper > #right { - display: inline-flex; - width: 28%; - height: 100%; - justify-content: flex-end; -} - -/* Lower content container. */ -#landingContainer > #lower { - height: 18%; - display: flex; - align-items: flex-end; - justify-content: space-between; - padding: 0 42px 58px; - background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)); -} -#landingContainer > #lower > #left { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - padding-bottom: 34px; - box-sizing: border-box; - flex: 1 1 0; - display: flex; - align-items: flex-end; - justify-content: flex-start; -} -#landingContainer > #lower > #left #content { - position: relative; - top: 0; - left: 0; - display: flex; - align-items: center; - line-height: 28px; -} -#landingContainer > #lower > #center { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - padding-bottom: 34px; - box-sizing: border-box; - flex: 0 0 auto; - display: flex; - align-items: flex-end; - justify-content: center; -} -#landingContainer > #lower > #center #content { - position: relative; - z-index: 500; - transition: top 2s ease; - top: 0; - display: flex; - align-items: center; -} -#landingContainer > #lower > #right { - position: relative; - transition: top 2s ease; - top: 0px; - height: 100%; - padding-bottom: 34px; - box-sizing: border-box; - flex: 1 1 0; - display: flex; - align-items: flex-end; - justify-content: flex-end; -} -#landingContainer > #lower > #right .bot_wrapper { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-end; -} - -/******************************************************************************* - * * - * Landing View (News Styles) * - * * - ******************************************************************************/ - -/* Main container. */ -#newsContainer { - position: absolute; - top: 100%; - height: 100%; - width: 100%; - transition: top 2s ease; - display: flex; - align-items: flex-end; - justify-content: center; -} - -/* News content container. */ -#newsContent { - height: 82vh; - width: 100%; - display: flex; - -webkit-user-select: initial; - position: relative; -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#newsContent:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#newsContent[scrolled]:before { - opacity: 1; -} - -/* News article status container (left). */ -#newsStatusContainer { - width: calc(30% - 60px); - height: calc(100% - 30px); - padding: 15px 15px 15px 45px; - display: flex; - flex-direction: column; - justify-content: space-between; - position: relative; -} - -/* News status content. */ -#newsStatusContent { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -/* News title wrapper. */ -#newsTitleContainer { - display: flex; - max-width: 90%; -} - -/* News article title styles. */ -#newsArticleTitle { - font-size: 18px; - font-weight: bold; - font-family: 'Pretendard Medium'; - color: white; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleTitle:hover, -#newsArticleTitle:focus { - text-shadow: 0px 0px 20px white; -} -#newsArticleTitle:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} - -/* News meta container. */ -#newsMetaContainer { - display: flex; - flex-direction: column; -} - -/* Date and author wrappers. */ -#newsArticleDateWrapper, -#newsArticleAuthorWrapper { - display: flex; - justify-content: flex-end; -} - -/* Date and author shared styles. */ -#newsArticleDate, -#newsArticleAuthor { - display: inline-block; - font-size: 10px; - padding: 0px 5px; - font-weight: bold; - border-radius: 2px; -} - -/* Date styles. */ -#newsArticleDate { - background: white; - color: black; - margin-top: 5px; -} - -/* Author styles. */ -#newsArticleAuthor { - background: #a02d2a; -} - -/* News article comments styles. */ -#newsArticleComments { - margin-top: 5px; - display: inline-block; - font-size: 10px; - color: #ffffff; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleComments:focus, -#newsArticleComments:hover { - color: #e0e0e0; -} -#newsArticleComments:active { - color: #c7c7c7; -} - -/* Article content container (right). */ -#newsArticleContainer { - width: calc(100% - 25px); - height: 100%; - margin: 0px 0px 0px 25px; -} - -/* Article content styles. */ -#newsArticleContentScrollable { - font-size: 12px; - overflow-y: scroll; - height: 100%; - padding: 0px 15px 0px 15px; -} -#newsArticleContentScrollable img, -#newsArticleContentScrollable iframe { - max-width: 95%; - height: auto; - display: block; - margin: 0 auto; -} -#newsArticleContentScrollable a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#newsArticleContentScrollable a:hover, -#newsArticleContentScrollable a:focus { - color: rgba(255, 255, 255, 0.75); -} -#newsArticleContentScrollable a:active { - color: rgba(165, 165, 165, 0.75); -} -#newsArticleContentScrollable::-webkit-scrollbar { - width: 2px; -} -#newsArticleContentScrollable::-webkit-scrollbar-track { - display: none; -} -#newsArticleContentScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} -.bbCodeSpoilerButton { - background: none; - border: none; - outline: none; - cursor: pointer; - font-size: 16px; - transition: 0.25s ease; - width: 100%; - border-bottom: 1px solid white; - padding-bottom: 15px; -} -.bbCodeSpoilerButton:hover, -.bbCodeSpoilerButton:focus { - text-shadow: 0px 0px 20px #ffffff, 0px 0px 20px #ffffff, 0px 0px 20px #ffffff; -} -.bbCodeSpoilerButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} -.bbCodeSpoilerText { - display: none; - padding: 15px 0px; - border-bottom: 1px solid white; -} - - -#newsArticleContentWrapper { - width: 80%; -} - -.newsArticleSpacerTop { - height: 15px; -} - -/* Div to add spacing at the end of a news article. */ -.newsArticleSpacerBot { - height: 30px; -} - -/* News navigation container. */ -#newsNavigationContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; - -webkit-user-select: none; - position: absolute; - bottom: 15px; - right: 0px; -} - -/* Navigation status span. */ -#newsNavigationStatus { - font-size: 12px; - margin: 0px 15px; -} - -/* Left and right navigation button styles. */ -#newsNavigateLeft, -#newsNavigateRight { - background: none; - border: none; - outline: none; - height: 20px; - cursor: pointer; -} -#newsNavigateLeft:hover #newsNavigationLeftSVG, -#newsNavigateLeft:focus #newsNavigationLeftSVG, -#newsNavigateRight:hover #newsNavigationRightSVG, -#newsNavigateRight:focus #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { - stroke: #c7c7c7; -} -#newsNavigateLeft:active #newsNavigationLeftSVG, -#newsNavigateRight:active #newsNavigationRightSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} -#newsNavigationLeftSVG { - transform: rotate(-90deg); - width: 15px; -} -#newsNavigationRightSVG { - transform: rotate(90deg); - width: 15px; -} - -/* News error (message) container. */ -#newsErrorContainer { - height: 100%; - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} -#newsErrorFailed { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} - -/* News error content (message). */ -.newsErrorContent { - font-size: 20px; -} -#newsErrorLoading { - display: flex; - width: 168.92px; -} -#nELoadSpan { - white-space: pre; -} -/* News error retry button styles. */ -#newsErrorRetry { - font-size: 12px; - font-weight: bold; - cursor: pointer; - background: none; - border: none; - outline: none; - transition: 0.25s ease; -} -#newsErrorRetry:focus, -#newsErrorRetry:hover { - text-shadow: 0px 0px 20px white; -} -#newsErrorRetry:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7; -} - -/******************************************************************************* - * * - * Landing View (Top Styles) * - * * - ******************************************************************************/ - -/* * * -* Landing View (Top Styles) | Left Content -* * */ - -/* Logo image. */ -#image_seal { - height: 70px; - width: auto; - position: relative; - /* border: 2px solid white; */ - box-sizing: border-box; - /* border-radius: 50%; */ -} - -/* Logo container styles. */ -#image_seal_container { - position: relative; - height: 70px; - width: 70px; - margin-top: 50px; -} - -/* Logo container styles w/ update. */ -#image_seal_container[update]{ - cursor: pointer -} -#image_seal_container[update]:before, -#image_seal_container[update]:after { - cursor: pointer; - position: absolute; - content: ''; - height: 100%; - width: 100%; - top: 0%; - left: 0%; - border-radius: 50%; - box-shadow: 0 0 15px #43c628; - animation: glow-grow 4s ease-out infinite; - background: rgba(0, 0, 0, 0.15); -} -#image_seal_container[update]:before { - animation-delay: 2s; -} - -/* Update available tooltip styles. */ -#updateAvailableTooltip { - cursor: pointer; - visibility: hidden; - opacity: 0; - width: 100px; - height: 15px; - background-color: rgb(0, 0, 0); - color: #fff; - text-align: center; - border-radius: 4px; - padding: 2px; - position: absolute; - z-index: 1; - top: 115%; - left: -17.5px; - font-family: 'Pretendard Medium'; - font-size: 12px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -#updateAvailableTooltip::after { - content: " "; - position: absolute; - left: 50%; - bottom: 100%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent rgb(0, 0, 0) transparent; -} -#image_seal_container[update]:hover #updateAvailableTooltip { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} - -/* Update available animation. */ -@keyframes glow-grow { - 0% { - opacity: 0; - transform: scale(1); - } - 80% { - opacity: 1; - } - 100% { - transform: scale(1.5); - opacity: 0; - } -} - -/* * * -* Landing View (Bottom Styles) | Right Content -* * */ - -/* Wrapper container for top, right content. */ -#rightContainer { - display: flex; - flex-direction: column; - position: relative; - top: 50px; - align-items: flex-end; - width: 100%; - padding-right: 26px; - box-sizing: border-box; - height: calc(100% - 50px); -} - -/* Right hand user content container. */ -#user_content { - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: center; - gap: 10px; - box-sizing: border-box; - position: relative; -} - -#accountTriggerGroup { - display: flex; - align-items: flex-end; - gap: 14px; -} - -#accountPreviewText { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 6px; - min-width: 210px; -} - -#accountPreviewLabel { - font-size: 13px; - letter-spacing: 0.12em; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.58); -} - -#accountPreviewName { - font-size: 18px; - font-weight: 800; - color: #ffffff; - text-align: right; - text-shadow: 0px 0px 18px rgba(0, 0, 0, 0.6); -} - -#avatarMenuButton { - padding: 0; - border: none; - background: none; - cursor: pointer; -} - -#avatarMenuButton:focus { - outline: none; -} - -#avatarMenuButton:disabled { - cursor: default; -} - -/* User profile avatar container. */ -#avatarContainer { - border-radius: 50%; - border: 3px solid #cad7e1; - box-sizing: border-box; - background: rgba(1, 2, 1, 0.5); - height: 88px; - width: 88px; - box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); - overflow: hidden; - position: relative; - background-position: center; - background-repeat: no-repeat, no-repeat; - background-size: cover, 60%; -} - -#avatarMenuButton:hover #avatarContainer, -#avatarMenuButton[aria-expanded="true"] #avatarContainer, -#avatarMenuButton:focus #avatarContainer { - border-color: #f1c15a; - box-shadow: 0px 0px 18px 0px rgba(241, 193, 90, 0.55); -} - -#accountMenu { - min-width: 220px; - padding: 16px; - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(10, 10, 10, 0.9); - backdrop-filter: blur(12px); - display: flex; - flex-direction: column; - align-items: stretch; - gap: 14px; - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28); -} - -#accountMenu[hidden] { - display: none; -} - -#accountMenuName { - font-size: 16px; - font-weight: 800; - letter-spacing: 0.04em; - color: #ffffff; - text-align: right; -} - -#accountMenuLogoutButton { - width: 100%; -} - -/* Social media icon content container. */ -#mediaContent { - position: relative; - display: flex; - flex-direction: column; - margin-top: 32px; - height: calc(100% - 120px); - width: 82px; - align-items: center; -} - -/* Social Media Icon division containers. */ -#internalMedia, #externalMedia { - display: flex; - flex-direction: column; -} - -/* Container object which wraps an icon to ensure fluid transitions. */ -.mediaContainer { - display: flex; - justify-content: center; - align-items: center; - height: 36px; -} - -/* Divider bar between the external and internal icons. */ -.mediaDivider { - height: 1px; - width: 18px; - background: rgb(255, 255, 255); - margin: 10px 0px; -} - -/* Social media icon shared styles. */ -.mediaSVG { - fill: #ffffff; - transition: 0.25s ease; - cursor: pointer; - height: 16px; - width: 32px; -} -.mediaSVG:hover, -.mediaURL:focus .mediaSVG, -.mediaSVG:active { - height: 24px; -} - -/* Social media URL shared styles. */ -.mediaURL { - outline: none; -} - -/* Internal media button shared styles. */ -.mediaButton { - background: none; - border: none; - padding: 0px; - display: flex; - align-items: center; - outline: none; -} - -#settingsMediaContainer { - position: relative; -} - -/* Settings icon colors. */ -#settingsSVG { - stroke: #ffffff; - height: 18px; -} -.mediaButton:hover #settingsSVG, -.mediaButton:focus #settingsSVG, -.mediaButton:active #settingsSVG { - height: 27px; -} - -/* Settings tooltip styles. */ -#settingsTooltip { - visibility: hidden; - opacity: 0; - width: 88px; - height: 24px; - background-color: rgba(0, 0, 0, 0.75); - text-align: center; - border-radius: 4px; - position: absolute; - z-index: 1; - right: 130%; - font-size: 13px; - line-height: 24px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -#settingsTooltip::after { - content: " "; - position: absolute; - top: 50%; - left: 100%; - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent transparent rgba(0, 0, 0, 0.75); -} -.mediaButton:hover #settingsTooltip, -.mediaButton:focus #settingsTooltip, -.mediaButton:active #settingsTooltip { - visibility: visible; - opacity: 1; - transition-delay:0s; -} - -/* Home icon colors. */ -#homeSVG:hover, -#homeURL:focus #homeSVG { - fill: #ff5c00; -} -#homeSVG:active { - fill: #e64b00; -} - -/* Map icon colors. */ -#mapSVG:hover, -#mapURL:focus #mapSVG { - fill: #00aaff; -} -#mapSVG:active { - fill: #008ecc; -} - -/* Github icon colors. */ -#githubSVG:hover, -#githubURL:focus #githubSVG { - fill: #6e5494; -} -#githubSVG:active { - fill: #5a437a; -} - -/* X icon colors. */ -#xSVG:hover, -#xURL:focus #xSVG { - fill: #000000; -} -#xSVG:active { - fill: #090909; -} - -/* Instagram icon colors. */ -#instagramSVG:hover, -#instagramURL:focus #instagramSVG { - fill: url('#instaFill') - /*fill: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); */ -} -#instagramSVG:active { - fill: url('#instaFill') -} - -/* Youtube icon colors. */ -#youtubeSVG:hover, -#youtubeURL:focus #youtubeSVG { - fill: #f00; -} -#youtubeSVG:active { - fill: #ea0202; -} - -/* Discord icon colors. */ -#discordSVG:hover, -#discordURL:focus #discordSVG { - fill: #7288d9; -} -#discordSVG:active { - fill: #657ac4; -} - -/******************************************************************************* - * * - * Landing View (Bottom Styles) * - * * - ******************************************************************************/ - -/* Style for a general label on the bottom of the landing view. */ -.bot_label { - font-size: 13px; - letter-spacing: 1.2px; - font-weight: bold; - text-shadow: 0px 0px 0px #bebcbb; - white-space: nowrap; -} - -/* Divider used on the bottom of the landing view. */ -.bot_divider { - height: 30px; - width: 2px; - background: rgba(107, 105, 105, 0.7); - margin-left: 16px; - margin-right: 16px; -} - -/* * * -* Landing View (Bottom Styles) | Left Content -* * */ - -/* Maintains maximum width on the status bar. */ -#server_status_wrapper { - position: relative; - display: inline-flex; - align-items: center; - gap: 10px; - width: auto; - flex: 0 0 auto; -} - -/* Span which displays the player count of the selected server. */ -#player_count { - color: #949494; - font-size: 14px; - font-weight: 900; - text-shadow: 0px 0px 20px #949494; - white-space: nowrap; -} - -#player_count[data-tone="success"] { - color: #8ce6a4; - text-shadow: 0px 0px 20px rgba(140, 230, 164, 0.6); -} - -#player_count[data-tone="error"] { - color: #ff8f8f; - text-shadow: 0px 0px 20px rgba(255, 143, 143, 0.6); -} - -#player_count[data-tone="info"] { - color: #f0d28a; - text-shadow: 0px 0px 20px rgba(240, 210, 138, 0.45); -} - -#portStatusTooltip { - position: absolute; - visibility: hidden; - opacity: 0; - width: 260px; - min-height: 56px; - background-color: rgba(0, 0, 0, 0.82); - color: #fff; - border-radius: 4px; - padding: 10px 12px; - z-index: 1; - font-family: 'Pretendard Medium'; - font-size: 13px; - line-height: 1.5; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; - bottom: calc(100% + 15px); - left: 0; - transform: none; - box-shadow: 0px 0px 20px rgb(0, 0, 0); - cursor: default; - white-space: normal; -} - -#portStatusTooltip:after { - content: " "; - position: absolute; - left: 24px; - top: 100%; - border-width: 5px; - border-style: solid; - border-color: rgba(0, 0, 0, 0.82) transparent transparent transparent; -} - -#server_status_wrapper:hover #portStatusTooltip { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} - -/* Wrapper container for the mojang status bar. */ -#mojangStatusWrapper { - position: relative; - display: flex; - align-items: center; - cursor: pointer; - white-space: nowrap; - flex-shrink: 0; -} - -/* Icon which displays the status of the mojang services. */ -#mojang_status_icon { - font-size: 34px; - color: #848484; - margin-left: 16px; - font-family: 'sans-serif'; -} - -/* Tooltip which displays more details about the mojang statuses. */ -#mojangStatusTooltip { - position: absolute; - visibility: hidden; - opacity: 0; - width: 175px; - min-height: 150px; - background-color: rgba(0, 0, 0, 0.75); - color: #fff; - border-radius: 4px; - padding: 5px 10px; - z-index: 1; - font-family: 'Pretendard Medium'; - font-size: 13px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; - bottom: calc(100% + 15px); - transform: translateX(-50%); - margin-left: 50%; - box-shadow: 0px 0px 20px rgb(0, 0, 0); - cursor: default; -} -#mojangStatusTooltip:after { - content: " "; - position: absolute; - left: 50%; - top: 100%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: rgba(0, 0, 0, 0.75) transparent transparent transparent; -} -#mojangStatusWrapper:hover #mojangStatusTooltip { - visibility: visible; - opacity: 1; - transition-delay: 0s; -} - -/* Tooltip title for the mojang statuses. */ -#mojangStatusTooltipTitle { - width: 100%; - text-align: center; - margin-bottom: 5px; - letter-spacing: 1px; -} - -/* Wrapper container for the non essential services title. */ -#mojangStatusNEContainer { - display: flex; - align-items: center; - margin: 10px 0px; -} - -/* White bar which surrounds the non essential service title. */ -.mojangStatusNEBar { - height: 1px; - width: 100%; - background: white; -} - -/* Non essential service title text. */ -#mojangStatusNETitle { - font-size: 11px; - padding: 0px 3px; - text-align: center; - letter-spacing: 1px; -} - -/* Wrapper container for mojang service information. */ -.mojangStatusContainer { - display: flex; -} - -/* Displays the name of the mojang service. */ -.mojangStatusName { - width: 100%; - font-size: 12px; - letter-spacing: 1px; - line-height: 14px; - padding: 7px 0px; -} - -/* Displays the status of the mojang service. */ -.mojangStatusIcon { - margin-right: 10px; - font-size: 20px; - color: #848484; -} - -/* * * -* Landing View (Bottom Styles) | Center Content -* * */ - -/* Button which opens the news view. */ -#newsButton { - background: none; - border: none; - cursor: pointer; - outline: none; - display: inline-flex; - align-items: center; - justify-content: center; -} -#newsButton:hover #newsButtonText, -#newsButton:focus #newsButtonText { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#newsButton:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} - -#newsButton:hover #newsButtonSVG, -#newsButton:focus #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #fff); -} -#newsButton:active #newsButtonSVG .arrowLine { - stroke: #c7c7c7; -} -#newsButton:active #newsButtonSVG { - -webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -} -#newsButton:disabled #newsButtonSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} - -/* Icon which indicates there is new news. */ -#newsButtonAlert { - width: 5px; - height: 5px; - position: absolute; - border-radius: 50%; - background: red; - right: -1px; - top: 50%; -} - -/* Arrow image which floats above the news button. */ -#newsButtonSVG { - height: 11px; - margin-left: -2px; - transition: 0.25s ease; -} - -/* Span which contains the news button text. */ -#newsButtonText { - color: white; - font-weight: 900; - letter-spacing: 2.5px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 16px; - line-height: 36px; - display: flex; - transition: 0.25s ease; - white-space: nowrap; -} - -/* * * -* Landing View (Bottom Styles) | Right Content -* * */ - -/* Main launch content container. */ -#landingContainer > #lower > #right #launch_content { - position: relative; - top: 0; - display: inline-flex; - align-items: center; - justify-content: flex-end; - max-width: 100%; -} - -/* The launch button. */ -#launch_button { - background: none; - border: none; - cursor: pointer; - font-weight: 900; - letter-spacing: 2px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 28px; - padding: 0px; - transition: 0.25s ease; - outline: none; - white-space: nowrap; -} -#launch_button:hover, -#launch_button:focus { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#launch_button:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} -#launch_button:disabled { - color: #c7c7c7; - cursor: default; - pointer-events: none; -} - -/* Launch details main container, hidden until launch processing begins. */ -#launch_details { - position: relative; - top: 12px; - display: none; - align-items: center; - justify-content: flex-end; - width: 100%; - max-width: 100%; - overflow: hidden; -} - -/* Left side of launch details container, displays percentage and a divider. */ -#launch_details_left { - display: flex; - align-items: center; - flex: 0 0 auto; -} - -/* Span which displays percentage complete. */ -#launch_progress_label { - font-weight: 900; - letter-spacing: 1px; - text-shadow: 0px 0px 0px #bebcbb; - font-size: 24px; - min-width: 64px; - max-width: 64px; - text-align: right; -} - -/* Right side of launch details container, displays progress bar and details. */ -#launch_details_right { - display: flex; - flex-direction: column; - justify-content: center; - min-width: 0; - overflow: hidden; -} - -/* Button which opens the server selection view. */ -#server_selection_button { - background: none; - border: none; - outline: none; - cursor: pointer; - font-size: 15px; - line-height: 30px; - padding: 0px; - transition: 0.25s ease; - white-space: nowrap; -} -#server_selection_button:hover, -#server_selection_button:focus { - text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff, 0px 0px 20px #fff; -} -#server_selection_button:active { - color: #c7c7c7; - text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; -} - -/* Progress bar styles. */ -#launch_progress[value] { - height: 4px; - width: 320px; - -webkit-appearance: none; -} -#launch_progress[value]::-webkit-progress-bar { - background-color: transparent; -} -#launch_progress[value]::-webkit-progress-value { - background-color: #fff; -} - -/* Span which displays information about the status of the launch process. */ -#launch_details_text { - display: block; - font-size: 13px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -/******************************************************************************* - * * - * Overlay View (overlay.ejs) * - * * - ******************************************************************************/ - -/* * * -* Overlay View (Main Content) -* * */ - -/* Overlay container, placed over the main div. */ -#overlayContainer { - position: absolute; - z-index: 500; - top: 22px; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: calc(100% - 22px); - background: rgba(0, 0, 0, 0.50); -} - -/* Main overlay content. */ -#overlayContent { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - /*justify-content: space-between;*/ - width: 300px; - /*height: 35%;*/ - box-sizing: border-box; - padding: 15px 0px; - /* background-color: #424242; */ - text-align: center; -} - -/* Main overlay content anchor styles. */ -#overlayContent a, -#overlayDismiss { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; -} -#overlayContent a:hover, -#overlayContent a:focus, -#overlayDismiss:focus { - color: rgba(255, 255, 255, 0.75); -} -#overlayContent a:active, -#overlayDismiss:active { - color: rgba(165, 165, 165, 0.75); -} - -/* Add spacing between overlay content elements. */ -#overlayContent > *:first-child { - margin-top: 0px !important; -} -#overlayContent > *:last-child { - margin-bottom: 0px !important; -} -#overlayContent > * { - margin: 8px 0px; -} - -/* Overlay title styles. */ -#overlayTitle { - font-family: 'Pretendard Medium'; - font-size: 20px; - font-weight: bold; - letter-spacing: 1px; - -webkit-user-select: initial; -} - -/* Overlay description styles. */ -#overlayDesc { - font-size: 12px; - font-weight: bold; - -webkit-user-select: initial; -} - -/* Div which contains action buttons. */ -#overlayActionContainer { - display: flex; - flex-direction: column; - justify-content: center; -} - -/* Overlay acknowledge button styles. */ -#overlayAcknowledge { - background: none; - border: 1px solid #ffffff; - color: white; - font-family: 'Pretendard Medium'; - font-weight: bold; - border-radius: 2px; - padding: 0px 8.1px; - cursor: pointer; - transition: 0.25s ease; -} -#overlayAcknowledge:hover, -#overlayAcknowledge:focus { - box-shadow: 0px 0px 10px 0px #fff; - outline: none; -} -#overlayAcknowledge:active { - border-color: rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Overlay dismiss option styles. */ -#overlayDismiss { - font-weight: bold; - font-size: 10px; - text-decoration: none; - padding-top: 2.5px; - background: none; - border: none; - outline: none; - cursor: pointer; -} -#overlayDismiss:hover { - color: rgba(255, 255, 255, 0.75); -} -#overlayDismiss:active { - color: rgba(165, 165, 165, 0.75); -} - -/* * * -* Overlay View (Server + Account Selection Content) -* * */ - -/* Server selection content container. */ -#serverSelectContent, -#accountSelectContent { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 75%; -} - -/* Server selection header. */ -#serverSelectHeader, -#accountSelectHeader { - font-family: 'Pretendard Medium'; - font-size: 20px; - font-weight: bold; - color: #fff; - margin-bottom: 25px; -} - -/* Wrapper div for the list of available servers. */ -#serverSelectList, -#accountSelectList { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - max-height: 65%; - min-height: 40%; -} - -/* Scrollable div which lists the available servers. */ -#serverSelectListScrollable, -#accountSelectListScrollable { - padding: 0px 5px; - overflow-y: scroll; -} -#serverSelectListScrollable::-webkit-scrollbar, -#accountSelectListScrollable::-webkit-scrollbar { - width: 2px; -} -#serverSelectListScrollable::-webkit-scrollbar-track, -#accountSelectListScrollable::-webkit-scrollbar-track { - display: none; -} -#serverSelectListScrollable::-webkit-scrollbar-thumb, -#accountSelectListScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} - -/* Content container for a server listing. */ -.serverListing { - border: none; - padding: 0px; - width: 375px; - min-height: 60px; - display: flex; - justify-content: flex-start; - align-items: center; - opacity: 0.6; - transition: 0.25s ease; - cursor: pointer; - position: relative; - background: rgba(131, 131, 131, 0.25); -} -.serverListing[selected] { - cursor: default; - opacity: 1.0; -} -.serverListing:hover, -.serverListing:focus { - outline: none; - opacity: 1.0; -} - -.accountListing { - color: white; - border: 1px solid rgba(126, 126, 126, 0.57); - border-radius: 3px; - padding: 5px 45px; - width: 250px; - display: flex; - justify-content: flex-start; - align-items: center; - opacity: 0.6; - transition: 0.25s ease; - cursor: pointer; - position: relative; - background: rgba(0, 0, 0, 0.25); -} -.accountListing[selected] { - cursor: default; - opacity: 1.0; -} -.accountListing:hover, -.accountListing:focus { - outline: none; - opacity: 1.0; -} - -.accountListingName { - display: flex; - height: 100%; - width: 100%; - padding-left: 10px; -} - -/* Add spacing between server listings. */ -#serverSelectListScrollable > .serverListing:not(:first-child):not(:last-child), -#accountSelectListScrollable > .accountListing:not(:first-child):not(:last-child) { - margin: 5px 0px; -} -#serverSelectListScrollable > .serverListing:first-child, -#accountSelectListScrollable > .accountListing:first-child { - margin-bottom: 5px; -} -#serverSelectListScrollable > .serverListing:last-child, -#accountSelectListScrollable > .accountListing:last-child { - margin-top: 5px; -} - -/* Server listing image. */ -.serverListingImg { - margin: 0px 10px 0px 5px; - border: 1px solid #fff; - height: 50px; - width: 50px; -} - -/* Content container for the server listing's details. */ -.serverListingDetails { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - height: 50px; -} - -/* The name of the server listing. */ -.serverListingName { - font-size: 14px; - font-weight: bold; -} - -/* Description for the server listing. */ -.serverListingDescription { - font-size: 10px; - line-height: 10px; - font-weight: bold; - text-align: left; -} - -/* Content container for the server listing's information. */ -.serverListingInfo { - width: 100%; - display: flex; - justify-content: flex-start; -} - -/* The minecraft version of the server listing. */ -.serverListingVersion { - font-size: 10px; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - line-height: 12px; - height: 12px; - border-radius: 2px; - background: rgba(31, 140, 11, 0.8); - padding: 0px 2px; -} - -/* The revision version of the server's manifest. */ -.serverListingRevision { - color: #969696; - font-size: 10px; - line-height: 12px; - padding: 0px 5px; -} - -/* Star which indicates the default (main) server. */ -.serverListingStarWrapper { - display: flex; - align-items: center; - cursor: pointer; - height: 12px; - position: relative; -} -/* Tooltip which displays when hovering over the star. */ -.serverListingStarTooltip { - visibility: hidden; - opacity: 0; - width: 65px; - background-color: rgba(0, 0, 0, 0.40); - text-align: center; - border-radius: 4px; - position: absolute; - z-index: 1; - left: 130%; - font-size: 10px; - transition: visibility 0s linear 0.25s, opacity 0.25s ease; -} -.serverListingStarTooltip::after { - content: " "; - position: absolute; - top: 50%; - right: 100%; /* To the left of the tooltip */ - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent rgba(0, 0, 0, 0.40) transparent transparent; -} -.serverListingStarWrapper:hover .serverListingStarTooltip { - visibility: visible; - opacity: 1; - transition-delay:0s; -} - -/* Content container which contains the server select actions. */ -#serverSelectActions, -#accountSelectActions { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-top: 25px; -} - -/* Server selection confirm button styles. */ -#serverSelectConfirm, -#accountSelectConfirm { - background: none; - border: 1px solid #ffffff; - color: white; - font-family: 'Pretendard Medium'; - font-weight: bold; - border-radius: 2px; - padding: 0px 8.1px; - cursor: pointer; - transition: 0.25s ease; - min-height: 20.67px; -} -#serverSelectConfirm:hover, -#serverSelectConfirm:focus, -#accountSelectConfirm:hover, -#accountSelectConfirm:focus { - box-shadow: 0px 0px 10px 0px #fff; - outline: none; -} -#serverSelectConfirm:active, -#accountSelectConfirm:active { - border-color: rgba(255, 255, 255, 0.75); - color: rgba(255, 255, 255, 0.75); -} - -/* Server selection cancel button styles. */ -#serverSelectCancel, -#accountSelectCancel { - font-weight: bold; - font-size: 10px; - text-decoration: none; - padding-top: 2.5px; - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - background: none; - border: none; - outline: none; - cursor: pointer; -} -#serverSelectCancel:hover, -#serverSelectCancel:focus, -#accountSelectCancel:hover, -#accountSelectCancel:focus { - color: rgba(255, 255, 255, 0.75); -} -#serverSelectCancel:active, -#accountSelectCancel:active { - color: rgba(165, 165, 165, 0.75); -} - -/******************************************************************************* - * * - * Loading Element (app.ejs) * - * * - ******************************************************************************/ - -/* Loading container, placed above everything. */ -#loadingContainer { - position: absolute; - z-index: 400; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: calc(100% - 22px); -} - -/* Loading content container. */ -#loadingContent { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -/* Spinner container. */ -#loadSpinnerContainer { - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - -/* Stationary image for the spinner. */ -#loadCenterImage { - position: absolute; - width: 277px; - height: auto; -} - -/* Rotating image for the spinner. */ -#loadSpinnerImage { - width: 280px; - height: auto; - z-index: 400; -} - -/* Rotating animation for the spinner. */ -@keyframes rotating { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* Class which is applied when the spinner image is spinning. */ -.rotating { - animation: rotating 10s linear infinite; -} - -#libraryContainer, -#installContainer { - width: 100%; - height: 100%; -} - -.launcherPageShell { - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: 18px; - width: 100%; - height: 100%; - padding: 42px 48px 32px; - overflow-y: auto; -} - -.launcherPageTopBar { - display: flex; - justify-content: flex-start; -} - -.launcherPageHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.launcherPageEyebrow { - display: inline-block; - margin-bottom: 10px; - font-size: 13px; - letter-spacing: 0.16em; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.65); -} - -.launcherPageTitle { - margin: 0; - font-size: 38px; - font-weight: 700; - color: #ffffff; -} - -.launcherPageSubtitle { - max-width: 760px; - margin: 12px 0 0; - font-size: 16px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.74); -} - -.launcherBackButton { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 0; - border: none; - background: none; - color: rgba(255, 255, 255, 0.88); - font-size: 16px; - font-weight: 700; - letter-spacing: 0.04em; - cursor: pointer; -} - -.launcherBackButton::before { - content: '←'; - font-size: 20px; -} - -.launcherBackButton:hover, -.launcherBackButton:focus { - color: #ffffff; - text-shadow: 0px 0px 16px rgba(255, 255, 255, 0.45); - outline: none; -} - -.launcherBackButton:active { - color: rgba(255, 255, 255, 0.72); -} - -.launcherPageActions, -.launcherCardActions, -#landingNavButtons { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -#landingNavButtons { - justify-content: center; - width: 100%; - padding-top: 18px; -} - -.launcherCardGrid { - display: flex; - flex-direction: column; - gap: 16px; -} - -.launcherListContainer { - display: flex; - flex-direction: column; - gap: 14px; -} - -.launcherListItem { - display: flex; - flex-direction: column; - align-items: stretch; - gap: 18px; - padding: 22px 24px; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 20px; - background: rgba(15, 15, 15, 0.56); - backdrop-filter: blur(10px); - box-shadow: 0 18px 50px rgba(0, 0, 0, 0.18); - cursor: pointer; -} - -.launcherListItem[selected="true"] { - border-color: rgba(253, 197, 78, 0.7); - box-shadow: 0 22px 60px rgba(253, 197, 78, 0.14); -} - -.launcherListItemMain { - min-width: 0; - flex: 1; - display: flex; - flex-direction: column; - gap: 8px; -} - -.launcherListItemTop { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 20px; -} - -.launcherListTitleRow { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; -} - -.launcherListTextGroup { - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.launcherListTitle { - margin: 0; - font-size: 24px; - color: #ffffff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.launcherListMeta { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; -} - -.launcherListDescription { - margin: 0; - font-size: 15px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.76); -} - -.launcherListActions { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 10px; - flex-wrap: wrap; - flex-shrink: 0; -} - -.launcherExpandableDetail { - display: flex; - flex-direction: column; - gap: 14px; - padding-top: 18px; - border-top: 1px solid rgba(255, 255, 255, 0.08); -} - -.launcherExpandableMeta { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.launcherCard { - display: flex; - flex-direction: column; - gap: 16px; - min-height: 240px; - padding: 22px; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 20px; - background: rgba(15, 15, 15, 0.56); - backdrop-filter: blur(10px); - box-shadow: 0 18px 50px rgba(0, 0, 0, 0.18); - width: 100%; -} - -.launcherCard[selected="true"] { - border-color: rgba(253, 197, 78, 0.7); - box-shadow: 0 22px 60px rgba(253, 197, 78, 0.14); -} - -.launcherCardHeader { - display: flex; - justify-content: space-between; - gap: 12px; -} - -.launcherCardTitleGroup { - display: flex; - flex-direction: column; - gap: 8px; -} - -.launcherCardTitle { - margin: 0; - font-size: 26px; - color: #ffffff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.launcherCardMeta { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.launcherBadge { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 5px 12px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.12); - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: rgba(255, 255, 255, 0.8); - white-space: nowrap; -} - -.launcherCardDescription, -.launcherNotice span, -.launcherEmptyDescription { - font-size: 15px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.76); -} - -.launcherEditorHeader { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.launcherEditorTitle { - display: block; - font-size: 22px; - font-weight: 700; - color: #ffffff; -} - -.launcherEditorDescription { - display: block; - margin-top: 6px; - font-size: 15px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.72); -} - -.launcherDetailBody { - box-sizing: border-box; - min-height: 180px; - padding: 16px 18px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.04); - color: rgba(255, 255, 255, 0.84); - font-size: 15px; - line-height: 1.6; - white-space: pre-wrap; -} - -.launcherInfoBlock { - display: flex; - flex-direction: column; - gap: 10px; - padding: 14px 16px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.04); -} - -.launcherInfoLine { - display: flex; - justify-content: space-between; - gap: 12px; -} - -.launcherInfoLabel { - font-size: 13px; - color: rgba(255, 255, 255, 0.58); - letter-spacing: 0.05em; - text-transform: uppercase; -} - -.launcherInfoValue { - text-align: right; - font-size: 15px; - color: rgba(255, 255, 255, 0.88); -} - -.launcherFieldGroup { - display: flex; - flex-direction: column; - gap: 8px; -} - -.launcherCardContent { - display: flex; - flex-direction: column; - gap: 12px; -} - -.launcherFormGrid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.launcherFormGridSpanFull { - grid-column: 1 / -1; -} - -.launcherInlineField { - display: flex; - align-items: center; - gap: 10px; -} - -.launcherInlineField .launcherFieldInput { - flex: 1; -} - -.launcherFieldLabel { - font-size: 13px; - color: rgba(255, 255, 255, 0.65); - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.launcherFieldInput { - box-sizing: border-box; - width: 100%; - padding: 12px 14px; - border: 1px solid rgba(255, 255, 255, 0.14); - border-radius: 12px; - background: rgba(10, 10, 10, 0.62); - color: #ffffff; - outline: none; -} - -.launcherFieldTextarea { - min-height: 96px; - resize: vertical; - font: inherit; -} - -.launcherFieldHint { - font-size: 13px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.6); -} - -.launcherCheckboxRow { - display: inline-flex; - align-items: center; - gap: 10px; - min-height: 44px; - color: rgba(255, 255, 255, 0.84); -} - -.launcherCheckboxRow input { - width: 16px; - height: 16px; -} - -.launcherPrimaryButton, -.launcherSecondaryButton, -.launcherGhostButton { - padding: 13px 18px; - border-radius: 14px; - border: 1px solid transparent; - font-size: 16px; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s ease, opacity 0.2s ease, border-color 0.2s ease, background 0.2s ease; - white-space: nowrap; -} - -.launcherPrimaryButton { - background: #f1c15a; - color: #1a1305; -} - -.launcherSecondaryButton { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.1); - color: #ffffff; -} - -.launcherGhostButton { - background: transparent; - border-color: rgba(255, 255, 255, 0.12); - color: rgba(255, 255, 255, 0.78); -} - -.launcherPrimaryButton:hover, -.launcherSecondaryButton:hover, -.launcherGhostButton:hover { - transform: translateY(-1px); -} - -.launcherPrimaryButton:disabled, -.launcherSecondaryButton:disabled, -.launcherGhostButton:disabled { - cursor: not-allowed; - opacity: 0.45; - transform: none; -} - -.launcherEmptyState, -.launcherNotice { - display: flex; - flex-direction: column; - gap: 8px; - padding: 20px; - border-radius: 20px; - background: rgba(8, 8, 8, 0.42); - border: 1px solid rgba(255, 255, 255, 0.08); -} - -.launcherIssueBlock { - display: flex; - flex-direction: column; - gap: 6px; -} - -.launcherIssueText { - color: rgba(255, 219, 154, 0.92); - line-height: 1.45; -} - -.launcherEmptyTitle { - font-size: 18px; - font-weight: 700; - color: #ffffff; -} - -@media (max-width: 900px) { - .launcherPageHeader, - .launcherEditorHeader { - flex-direction: column; - } - - .launcherFormGrid { - grid-template-columns: 1fr; - } -} diff --git a/app/assets/fonts/Pretendard-Black.ttf b/app/assets/fonts/Pretendard-Black.ttf deleted file mode 100644 index d0c1db8..0000000 Binary files a/app/assets/fonts/Pretendard-Black.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-Bold.ttf b/app/assets/fonts/Pretendard-Bold.ttf deleted file mode 100644 index fb07fc6..0000000 Binary files a/app/assets/fonts/Pretendard-Bold.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-ExtraBold.ttf b/app/assets/fonts/Pretendard-ExtraBold.ttf deleted file mode 100644 index 9d5fe07..0000000 Binary files a/app/assets/fonts/Pretendard-ExtraBold.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-ExtraLight.ttf b/app/assets/fonts/Pretendard-ExtraLight.ttf deleted file mode 100644 index 09e6542..0000000 Binary files a/app/assets/fonts/Pretendard-ExtraLight.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-Light.ttf b/app/assets/fonts/Pretendard-Light.ttf deleted file mode 100644 index 2e8541d..0000000 Binary files a/app/assets/fonts/Pretendard-Light.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-Medium.ttf b/app/assets/fonts/Pretendard-Medium.ttf deleted file mode 100644 index 1db67c6..0000000 Binary files a/app/assets/fonts/Pretendard-Medium.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-Regular.ttf b/app/assets/fonts/Pretendard-Regular.ttf deleted file mode 100644 index 01147e9..0000000 Binary files a/app/assets/fonts/Pretendard-Regular.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-SemiBold.ttf b/app/assets/fonts/Pretendard-SemiBold.ttf deleted file mode 100644 index 9f2690f..0000000 Binary files a/app/assets/fonts/Pretendard-SemiBold.ttf and /dev/null differ diff --git a/app/assets/fonts/Pretendard-Thin.ttf b/app/assets/fonts/Pretendard-Thin.ttf deleted file mode 100644 index fe9825f..0000000 Binary files a/app/assets/fonts/Pretendard-Thin.ttf and /dev/null differ diff --git a/app/assets/fonts/ringbearer.ttf b/app/assets/fonts/ringbearer.ttf deleted file mode 100644 index 5fb3a09..0000000 Binary files a/app/assets/fonts/ringbearer.ttf and /dev/null differ diff --git a/app/assets/images/Icon.ico b/app/assets/images/Icon.ico deleted file mode 100644 index 5660b65..0000000 Binary files a/app/assets/images/Icon.ico and /dev/null differ diff --git a/app/assets/images/Icon.png b/app/assets/images/Icon.png deleted file mode 100644 index 5ab9783..0000000 Binary files a/app/assets/images/Icon.png and /dev/null differ diff --git a/app/assets/images/backgrounds/0.png b/app/assets/images/backgrounds/0.png deleted file mode 100644 index 1e6c992..0000000 Binary files a/app/assets/images/backgrounds/0.png and /dev/null differ diff --git a/app/assets/images/backgrounds/1.png b/app/assets/images/backgrounds/1.png deleted file mode 100644 index a35b747..0000000 Binary files a/app/assets/images/backgrounds/1.png and /dev/null differ diff --git a/app/assets/images/backgrounds/2.png b/app/assets/images/backgrounds/2.png deleted file mode 100644 index 93a98f6..0000000 Binary files a/app/assets/images/backgrounds/2.png and /dev/null differ diff --git a/app/assets/images/backgrounds/3.png b/app/assets/images/backgrounds/3.png deleted file mode 100644 index afcd4a9..0000000 Binary files a/app/assets/images/backgrounds/3.png and /dev/null differ diff --git a/app/assets/images/backgrounds/4.png b/app/assets/images/backgrounds/4.png deleted file mode 100644 index bf80870..0000000 Binary files a/app/assets/images/backgrounds/4.png and /dev/null differ diff --git a/app/assets/images/backgrounds/5.png b/app/assets/images/backgrounds/5.png deleted file mode 100644 index e78562f..0000000 Binary files a/app/assets/images/backgrounds/5.png and /dev/null differ diff --git a/app/assets/images/backgrounds/6.png b/app/assets/images/backgrounds/6.png deleted file mode 100644 index 2bde975..0000000 Binary files a/app/assets/images/backgrounds/6.png and /dev/null differ diff --git a/app/assets/images/icons/arrow.svg b/app/assets/images/icons/arrow.svg deleted file mode 100644 index 9332605..0000000 --- a/app/assets/images/icons/arrow.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - arrow - - \ No newline at end of file diff --git a/app/assets/images/icons/discord.svg b/app/assets/images/icons/discord.svg deleted file mode 100644 index 8727128..0000000 --- a/app/assets/images/icons/discord.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - discord - - - - \ No newline at end of file diff --git a/app/assets/images/icons/github-mark-white.svg b/app/assets/images/icons/github-mark-white.svg deleted file mode 100644 index 69d901b..0000000 --- a/app/assets/images/icons/github-mark-white.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/home.svg b/app/assets/images/icons/home.svg deleted file mode 100644 index 316177f..0000000 --- a/app/assets/images/icons/home.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/instagram.svg b/app/assets/images/icons/instagram.svg deleted file mode 100644 index 00c70bd..0000000 --- a/app/assets/images/icons/instagram.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/link.svg b/app/assets/images/icons/link.svg deleted file mode 100644 index df151d4..0000000 --- a/app/assets/images/icons/link.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - link - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/lock.svg b/app/assets/images/icons/lock.svg deleted file mode 100644 index 47a5343..0000000 --- a/app/assets/images/icons/lock.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - Lock - - - - \ No newline at end of file diff --git a/app/assets/images/icons/map.svg b/app/assets/images/icons/map.svg deleted file mode 100644 index 0a158e4..0000000 --- a/app/assets/images/icons/map.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/microsoft.svg b/app/assets/images/icons/microsoft.svg deleted file mode 100644 index 78a4ed9..0000000 --- a/app/assets/images/icons/microsoft.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/mojang.svg b/app/assets/images/icons/mojang.svg deleted file mode 100644 index e1116b4..0000000 --- a/app/assets/images/icons/mojang.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/news.svg b/app/assets/images/icons/news.svg deleted file mode 100644 index 775578d..0000000 --- a/app/assets/images/icons/news.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - News - - - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/profile.svg b/app/assets/images/icons/profile.svg deleted file mode 100644 index 6526c65..0000000 --- a/app/assets/images/icons/profile.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - Profile - - - - \ No newline at end of file diff --git a/app/assets/images/icons/settings.svg b/app/assets/images/icons/settings.svg deleted file mode 100644 index 1a0ec76..0000000 --- a/app/assets/images/icons/settings.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - settings - - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar.svg b/app/assets/images/icons/sevenstar.svg deleted file mode 100644 index 4f55ef4..0000000 --- a/app/assets/images/icons/sevenstar.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Seven Pointed Star - - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle.svg b/app/assets/images/icons/sevenstar_circle.svg deleted file mode 100644 index 9e8c8a8..0000000 --- a/app/assets/images/icons/sevenstar_circle.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Seven Pointed Star with Circle - - - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_extended.svg b/app/assets/images/icons/sevenstar_circle_extended.svg deleted file mode 100644 index 8651baa..0000000 --- a/app/assets/images/icons/sevenstar_circle_extended.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - Seven Pointed Star Extended with Circle - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_hole.svg b/app/assets/images/icons/sevenstar_circle_hole.svg deleted file mode 100644 index 65250d4..0000000 --- a/app/assets/images/icons/sevenstar_circle_hole.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Seven Pointed Star with Circle and Hole - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_circle_hole_extended.svg b/app/assets/images/icons/sevenstar_circle_hole_extended.svg deleted file mode 100644 index e549b4d..0000000 --- a/app/assets/images/icons/sevenstar_circle_hole_extended.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Seven Pointed Star Extended with Circle and Hole - - - - \ No newline at end of file diff --git a/app/assets/images/icons/sevenstar_extended.svg b/app/assets/images/icons/sevenstar_extended.svg deleted file mode 100644 index b8e2824..0000000 --- a/app/assets/images/icons/sevenstar_extended.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Seven Pointed Star Extended - - \ No newline at end of file diff --git a/app/assets/images/icons/x.svg b/app/assets/images/icons/x.svg deleted file mode 100644 index c3b5162..0000000 --- a/app/assets/images/icons/x.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/assets/images/icons/youtube.svg b/app/assets/images/icons/youtube.svg deleted file mode 100644 index 38a2e8b..0000000 --- a/app/assets/images/icons/youtube.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - youtube - - - - \ No newline at end of file diff --git a/app/assets/images/minecraft.icns b/app/assets/images/minecraft.icns deleted file mode 100644 index 60ce69d..0000000 Binary files a/app/assets/images/minecraft.icns and /dev/null differ diff --git a/app/assets/js/authmanager.js b/app/assets/js/authmanager.js deleted file mode 100644 index 5d67bbb..0000000 --- a/app/assets/js/authmanager.js +++ /dev/null @@ -1,425 +0,0 @@ -/** - * AuthManager - * - * This module aims to abstract login procedures. Results from Mojang's REST api - * are retrieved through our Mojang module. These results are processed and stored, - * if applicable, in the config using the ConfigManager. All login procedures should - * be made through this module. - * - * @module authmanager - */ -// Requirements -const ConfigManager = require('./configmanager') -const { LoggerUtil } = require('helios-core') -const { RestResponseStatus } = require('helios-core/common') -const { MojangRestAPI, MojangErrorCode } = require('helios-core/mojang') -const { MicrosoftAuth, MicrosoftErrorCode } = require('helios-core/microsoft') -const { AZURE_CLIENT_ID } = require('./ipcconstants') -const Lang = require('./langloader') - -const log = LoggerUtil.getLogger('AuthManager') - -// Error messages - -function microsoftErrorDisplayable(errorCode) { - switch (errorCode) { - case MicrosoftErrorCode.NO_PROFILE: - return { - title: Lang.queryJS('auth.microsoft.error.noProfileTitle'), - desc: Lang.queryJS('auth.microsoft.error.noProfileDesc') - } - case MicrosoftErrorCode.NO_XBOX_ACCOUNT: - return { - title: Lang.queryJS('auth.microsoft.error.noXboxAccountTitle'), - desc: Lang.queryJS('auth.microsoft.error.noXboxAccountDesc') - } - case MicrosoftErrorCode.XBL_BANNED: - return { - title: Lang.queryJS('auth.microsoft.error.xblBannedTitle'), - desc: Lang.queryJS('auth.microsoft.error.xblBannedDesc') - } - case MicrosoftErrorCode.UNDER_18: - return { - title: Lang.queryJS('auth.microsoft.error.under18Title'), - desc: Lang.queryJS('auth.microsoft.error.under18Desc') - } - case MicrosoftErrorCode.UNKNOWN: - return { - title: Lang.queryJS('auth.microsoft.error.unknownTitle'), - desc: Lang.queryJS('auth.microsoft.error.unknownDesc') - } - } -} - -function mojangErrorDisplayable(errorCode) { - switch(errorCode) { - case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED: - return { - title: Lang.queryJS('auth.mojang.error.methodNotAllowedTitle'), - desc: Lang.queryJS('auth.mojang.error.methodNotAllowedDesc') - } - case MojangErrorCode.ERROR_NOT_FOUND: - return { - title: Lang.queryJS('auth.mojang.error.notFoundTitle'), - desc: Lang.queryJS('auth.mojang.error.notFoundDesc') - } - case MojangErrorCode.ERROR_USER_MIGRATED: - return { - title: Lang.queryJS('auth.mojang.error.accountMigratedTitle'), - desc: Lang.queryJS('auth.mojang.error.accountMigratedDesc') - } - case MojangErrorCode.ERROR_INVALID_CREDENTIALS: - return { - title: Lang.queryJS('auth.mojang.error.invalidCredentialsTitle'), - desc: Lang.queryJS('auth.mojang.error.invalidCredentialsDesc') - } - case MojangErrorCode.ERROR_RATELIMIT: - return { - title: Lang.queryJS('auth.mojang.error.tooManyAttemptsTitle'), - desc: Lang.queryJS('auth.mojang.error.tooManyAttemptsDesc') - } - case MojangErrorCode.ERROR_INVALID_TOKEN: - return { - title: Lang.queryJS('auth.mojang.error.invalidTokenTitle'), - desc: Lang.queryJS('auth.mojang.error.invalidTokenDesc') - } - case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: - return { - title: Lang.queryJS('auth.mojang.error.tokenHasProfileTitle'), - desc: Lang.queryJS('auth.mojang.error.tokenHasProfileDesc') - } - case MojangErrorCode.ERROR_CREDENTIALS_MISSING: - return { - title: Lang.queryJS('auth.mojang.error.credentialsMissingTitle'), - desc: Lang.queryJS('auth.mojang.error.credentialsMissingDesc') - } - case MojangErrorCode.ERROR_INVALID_SALT_VERSION: - return { - title: Lang.queryJS('auth.mojang.error.invalidSaltVersionTitle'), - desc: Lang.queryJS('auth.mojang.error.invalidSaltVersionDesc') - } - case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE: - return { - title: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeTitle'), - desc: Lang.queryJS('auth.mojang.error.unsupportedMediaTypeDesc') - } - case MojangErrorCode.ERROR_GONE: - return { - title: Lang.queryJS('auth.mojang.error.accountGoneTitle'), - desc: Lang.queryJS('auth.mojang.error.accountGoneDesc') - } - case MojangErrorCode.ERROR_UNREACHABLE: - return { - title: Lang.queryJS('auth.mojang.error.unreachableTitle'), - desc: Lang.queryJS('auth.mojang.error.unreachableDesc') - } - case MojangErrorCode.ERROR_NOT_PAID: - return { - title: Lang.queryJS('auth.mojang.error.gameNotPurchasedTitle'), - desc: Lang.queryJS('auth.mojang.error.gameNotPurchasedDesc') - } - case MojangErrorCode.UNKNOWN: - return { - title: Lang.queryJS('auth.mojang.error.unknownErrorTitle'), - desc: Lang.queryJS('auth.mojang.error.unknownErrorDesc') - } - default: - throw new Error(`Unknown error code: ${errorCode}`) - } -} - -// Functions - -/** - * Add a Mojang account. This will authenticate the given credentials with Mojang's - * authserver. The resultant data will be stored as an auth account in the - * configuration database. - * - * @param {string} username The account username (email if migrated). - * @param {string} password The account password. - * @returns {Promise.} Promise which resolves the resolved authenticated account object. - */ -exports.addMojangAccount = async function(username, password) { - try { - const response = await MojangRestAPI.authenticate(username, password, ConfigManager.getClientToken()) - console.log(response) - if(response.responseStatus === RestResponseStatus.SUCCESS) { - - const session = response.data - if(session.selectedProfile != null){ - const ret = ConfigManager.addMojangAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) - if(ConfigManager.getClientToken() == null){ - ConfigManager.setClientToken(session.clientToken) - } - ConfigManager.save() - return ret - } else { - return Promise.reject(mojangErrorDisplayable(MojangErrorCode.ERROR_NOT_PAID)) - } - - } else { - return Promise.reject(mojangErrorDisplayable(response.mojangErrorCode)) - } - - } catch (err){ - log.error(err) - return Promise.reject(mojangErrorDisplayable(MojangErrorCode.UNKNOWN)) - } -} - -const AUTH_MODE = { FULL: 0, MS_REFRESH: 1, MC_REFRESH: 2 } - -/** - * Perform the full MS Auth flow in a given mode. - * - * AUTH_MODE.FULL = Full authorization for a new account. - * AUTH_MODE.MS_REFRESH = Full refresh authorization. - * AUTH_MODE.MC_REFRESH = Refresh of the MC token, reusing the MS token. - * - * @param {string} entryCode FULL-AuthCode. MS_REFRESH=refreshToken, MC_REFRESH=accessToken - * @param {*} authMode The auth mode. - * @returns An object with all auth data. AccessToken object will be null when mode is MC_REFRESH. - */ -async function fullMicrosoftAuthFlow(entryCode, authMode) { - try { - - let accessTokenRaw - let accessToken - if(authMode !== AUTH_MODE.MC_REFRESH) { - const accessTokenResponse = await MicrosoftAuth.getAccessToken(entryCode, authMode === AUTH_MODE.MS_REFRESH, AZURE_CLIENT_ID) - if(accessTokenResponse.responseStatus === RestResponseStatus.ERROR) { - return Promise.reject(microsoftErrorDisplayable(accessTokenResponse.microsoftErrorCode)) - } - accessToken = accessTokenResponse.data - accessTokenRaw = accessToken.access_token - } else { - accessTokenRaw = entryCode - } - - const xblResponse = await MicrosoftAuth.getXBLToken(accessTokenRaw) - if(xblResponse.responseStatus === RestResponseStatus.ERROR) { - return Promise.reject(microsoftErrorDisplayable(xblResponse.microsoftErrorCode)) - } - const xstsResonse = await MicrosoftAuth.getXSTSToken(xblResponse.data) - if(xstsResonse.responseStatus === RestResponseStatus.ERROR) { - return Promise.reject(microsoftErrorDisplayable(xstsResonse.microsoftErrorCode)) - } - const mcTokenResponse = await MicrosoftAuth.getMCAccessToken(xstsResonse.data) - if(mcTokenResponse.responseStatus === RestResponseStatus.ERROR) { - return Promise.reject(microsoftErrorDisplayable(mcTokenResponse.microsoftErrorCode)) - } - const mcProfileResponse = await MicrosoftAuth.getMCProfile(mcTokenResponse.data.access_token) - if(mcProfileResponse.responseStatus === RestResponseStatus.ERROR) { - return Promise.reject(microsoftErrorDisplayable(mcProfileResponse.microsoftErrorCode)) - } - return { - accessToken, - accessTokenRaw, - xbl: xblResponse.data, - xsts: xstsResonse.data, - mcToken: mcTokenResponse.data, - mcProfile: mcProfileResponse.data - } - } catch(err) { - log.error(err) - return Promise.reject(microsoftErrorDisplayable(MicrosoftErrorCode.UNKNOWN)) - } -} - -/** - * Calculate the expiry date. Advance the expiry time by 10 seconds - * to reduce the liklihood of working with an expired token. - * - * @param {number} nowMs Current time milliseconds. - * @param {number} epiresInS Expires in (seconds) - * @returns - */ -function calculateExpiryDate(nowMs, epiresInS) { - return nowMs + ((epiresInS-10)*1000) -} - -/** - * Add a Microsoft account. This will pass the provided auth code to Mojang's OAuth2.0 flow. - * The resultant data will be stored as an auth account in the configuration database. - * - * @param {string} authCode The authCode obtained from microsoft. - * @returns {Promise.} Promise which resolves the resolved authenticated account object. - */ -exports.addMicrosoftAccount = async function(authCode) { - - const fullAuth = await fullMicrosoftAuthFlow(authCode, AUTH_MODE.FULL) - - // Advance expiry by 10 seconds to avoid close calls. - const now = new Date().getTime() - - const ret = ConfigManager.addMicrosoftAuthAccount( - fullAuth.mcProfile.id, - fullAuth.mcToken.access_token, - fullAuth.mcProfile.name, - calculateExpiryDate(now, fullAuth.mcToken.expires_in), - fullAuth.accessToken.access_token, - fullAuth.accessToken.refresh_token, - calculateExpiryDate(now, fullAuth.accessToken.expires_in) - ) - ConfigManager.save() - - return ret -} - -/** - * Remove a Mojang account. This will invalidate the access token associated - * with the account and then remove it from the database. - * - * @param {string} uuid The UUID of the account to be removed. - * @returns {Promise.} Promise which resolves to void when the action is complete. - */ -exports.removeMojangAccount = async function(uuid){ - try { - const authAcc = ConfigManager.getAuthAccount(uuid) - const response = await MojangRestAPI.invalidate(authAcc.accessToken, ConfigManager.getClientToken()) - if(response.responseStatus === RestResponseStatus.SUCCESS) { - ConfigManager.removeAuthAccount(uuid) - ConfigManager.save() - return Promise.resolve() - } else { - log.error('Error while removing account', response.error) - return Promise.reject(response.error) - } - } catch (err){ - log.error('Error while removing account', err) - return Promise.reject(err) - } -} - -/** - * Remove a Microsoft account. It is expected that the caller will invoke the OAuth logout - * through the ipc renderer. - * - * @param {string} uuid The UUID of the account to be removed. - * @returns {Promise.} Promise which resolves to void when the action is complete. - */ -exports.removeMicrosoftAccount = async function(uuid){ - try { - ConfigManager.removeAuthAccount(uuid) - ConfigManager.save() - return Promise.resolve() - } catch (err){ - log.error('Error while removing account', err) - return Promise.reject(err) - } -} - -/** - * Validate the selected account with Mojang's authserver. If the account is not valid, - * we will attempt to refresh the access token and update that value. If that fails, a - * new login will be required. - * - * @returns {Promise.} Promise which resolves to true if the access token is valid, - * otherwise false. - */ -async function validateSelectedMojangAccount(){ - const current = ConfigManager.getSelectedAccount() - const response = await MojangRestAPI.validate(current.accessToken, ConfigManager.getClientToken()) - - if(response.responseStatus === RestResponseStatus.SUCCESS) { - const isValid = response.data - if(!isValid){ - const refreshResponse = await MojangRestAPI.refresh(current.accessToken, ConfigManager.getClientToken()) - if(refreshResponse.responseStatus === RestResponseStatus.SUCCESS) { - const session = refreshResponse.data - ConfigManager.updateMojangAuthAccount(current.uuid, session.accessToken) - ConfigManager.save() - } else { - log.error('Error while validating selected profile:', refreshResponse.error) - log.info('Account access token is invalid.') - return false - } - log.info('Account access token validated.') - return true - } else { - log.info('Account access token validated.') - return true - } - } - -} - -/** - * Validate the selected account with Microsoft's authserver. If the account is not valid, - * we will attempt to refresh the access token and update that value. If that fails, a - * new login will be required. - * - * @returns {Promise.} Promise which resolves to true if the access token is valid, - * otherwise false. - */ -async function validateSelectedMicrosoftAccount(){ - const current = ConfigManager.getSelectedAccount() - const now = new Date().getTime() - const mcExpiresAt = current.expiresAt - const mcExpired = now >= mcExpiresAt - - if(!mcExpired) { - return true - } - - // MC token expired. Check MS token. - - const msExpiresAt = current.microsoft.expires_at - const msExpired = now >= msExpiresAt - - if(msExpired) { - // MS expired, do full refresh. - try { - const res = await fullMicrosoftAuthFlow(current.microsoft.refresh_token, AUTH_MODE.MS_REFRESH) - - ConfigManager.updateMicrosoftAuthAccount( - current.uuid, - res.mcToken.access_token, - res.accessToken.access_token, - res.accessToken.refresh_token, - calculateExpiryDate(now, res.accessToken.expires_in), - calculateExpiryDate(now, res.mcToken.expires_in) - ) - ConfigManager.save() - return true - } catch(_err) { - return false - } - } else { - // Only MC expired, use existing MS token. - try { - const res = await fullMicrosoftAuthFlow(current.microsoft.access_token, AUTH_MODE.MC_REFRESH) - - ConfigManager.updateMicrosoftAuthAccount( - current.uuid, - res.mcToken.access_token, - current.microsoft.access_token, - current.microsoft.refresh_token, - current.microsoft.expires_at, - calculateExpiryDate(now, res.mcToken.expires_in) - ) - ConfigManager.save() - return true - } - catch(_err) { - return false - } - } -} - -/** - * Validate the selected auth account. - * - * @returns {Promise.} Promise which resolves to true if the access token is valid, - * otherwise false. - */ -exports.validateSelected = async function(){ - const current = ConfigManager.getSelectedAccount() - - if(current.type === 'microsoft') { - return await validateSelectedMicrosoftAccount() - } else { - return await validateSelectedMojangAccount() - } - -} diff --git a/app/assets/js/catalogmanager.js b/app/assets/js/catalogmanager.js deleted file mode 100644 index 6fe9b3c..0000000 --- a/app/assets/js/catalogmanager.js +++ /dev/null @@ -1,343 +0,0 @@ -const fs = require('fs-extra') -const got = require('got') -const path = require('path') - -const ConfigManager = require('./configmanager') -const { DEFAULT_REMOTE_DISTRO_URL, setRemoteDistributionUrl } = require('./distromanager') - -const LOCAL_CATALOG_PATH = path.join(__dirname, '..', 'launcher', 'catalog.json') -const ROOT_ASSET_PATH = path.join(__dirname, '..', '..', '..') -const DEFAULT_REMOTE_CATALOG_URL = normalizeNullableText(process.env.LAUNCHER_CATALOG_URL) || 'http://127.0.0.1:8787/catalog.json' - -function normalizeText(value){ - return typeof value === 'string' ? value.trim() : '' -} - -function normalizeNullableText(value){ - const nextValue = normalizeText(value) - return nextValue.length > 0 ? nextValue : null -} - -function normalizeBoolean(value){ - return value === true -} - -function normalizePositiveInteger(value, fallback, minimum = 1){ - const parsed = Number.parseInt(String(value ?? ''), 10) - if(Number.isFinite(parsed) && parsed >= minimum){ - return parsed - } - return fallback -} - -function isRemoteSource(source){ - return /^https?:\/\//i.test(source) -} - -function isAbsoluteFileSystemPath(source){ - return path.isAbsolute(source) || /^[a-zA-Z]:[\\/]/.test(source) -} - -function deriveFeatureFlags(rawProfile){ - const legacyKind = normalizeText(rawProfile?.kind) - - let modsEnabled = normalizeBoolean(rawProfile?.modsEnabled) - let pluginsEnabled = normalizeBoolean(rawProfile?.pluginsEnabled) - let serverEnabled = normalizeBoolean(rawProfile?.serverEnabled) - - if(legacyKind === 'modpack'){ - modsEnabled = true - } else if(legacyKind === 'server-pack'){ - pluginsEnabled = true - serverEnabled = true - } - - if(pluginsEnabled){ - serverEnabled = true - } - - return { - modsEnabled, - pluginsEnabled, - serverEnabled - } -} - -function deriveLegacyKind(flags){ - if(flags.serverEnabled){ - return 'server-pack' - } - if(flags.modsEnabled){ - return 'modpack' - } - return 'map' -} - -function resolveLegacyServerJar(rawProfile){ - const directValue = normalizeNullableText(rawProfile?.serverJarUrl) - if(directValue != null){ - return directValue - } - - const legacyBundle = normalizeNullableText(rawProfile?.serverBundleUrl) - if(legacyBundle != null && legacyBundle.toLowerCase().endsWith('.jar')){ - return legacyBundle - } - - return null -} - -function resolveProfileSourceValue(value, catalogSource){ - const nextValue = normalizeNullableText(value) - if(nextValue == null){ - return null - } - - if(isRemoteSource(nextValue) || nextValue.startsWith('file://') || isAbsoluteFileSystemPath(nextValue)){ - return nextValue - } - - if(isRemoteSource(catalogSource)){ - return new URL(nextValue, catalogSource).toString() - } - - return path.resolve(ROOT_ASSET_PATH, nextValue) -} - -function toStoredProfile(rawProfile, catalogSource){ - const flags = deriveFeatureFlags(rawProfile) - const storedProfile = { - id: normalizeText(rawProfile.id), - name: normalizeText(rawProfile.name), - kind: deriveLegacyKind(flags), - description: normalizeText(rawProfile.description), - details: normalizeText(rawProfile.details), - distributionUrl: resolveProfileSourceValue(rawProfile.distributionUrl, catalogSource), - modsEnabled: flags.modsEnabled, - pluginsEnabled: flags.pluginsEnabled, - serverEnabled: flags.serverEnabled, - worldArchiveUrl: resolveProfileSourceValue(rawProfile.worldArchiveUrl, catalogSource), - worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName), - serverJarUrl: resolveProfileSourceValue(resolveLegacyServerJar(rawProfile), catalogSource), - serverDirectoryName: normalizeText(rawProfile.serverDirectoryName) || 'server', - serverPort: Number.isFinite(Number(rawProfile.serverPort)) ? Number(rawProfile.serverPort) : 25565, - serverMemoryMb: normalizePositiveInteger(rawProfile.serverMemoryMb, 4096, 512), - serverMaxPlayers: normalizePositiveInteger(rawProfile.serverMaxPlayers, 20, 1), - serverWhitelistEnabled: normalizeBoolean(rawProfile.serverWhitelistEnabled), - artwork: resolveProfileSourceValue(rawProfile.artwork, catalogSource) ?? '' - } - - if(!storedProfile.serverEnabled){ - storedProfile.serverJarUrl = null - storedProfile.serverDirectoryName = 'server' - storedProfile.serverPort = 25565 - storedProfile.serverMemoryMb = 4096 - storedProfile.serverMaxPlayers = 20 - storedProfile.serverWhitelistEnabled = false - } - - return storedProfile -} - -function normalizeProfile(rawProfile, sourceType = 'catalog', catalogSource = LOCAL_CATALOG_PATH){ - const storedProfile = toStoredProfile(rawProfile, catalogSource) - const launchIssues = [] - const hostIssues = [] - - if(storedProfile.distributionUrl == null){ - launchIssues.push('distribution URL이 필요합니다.') - } - - if(storedProfile.worldArchiveUrl == null){ - launchIssues.push('맵 ZIP 또는 로컬 월드 경로가 필요합니다.') - } - if(storedProfile.worldDirectoryName == null){ - launchIssues.push('월드 폴더 이름이 필요합니다.') - } - - if(storedProfile.serverEnabled && storedProfile.serverJarUrl == null){ - hostIssues.push('로컬 서버를 시작하려면 버킷 JAR 업로드가 필요합니다.') - } - - const launchReady = launchIssues.length === 0 - const hostReady = storedProfile.serverEnabled ? hostIssues.length === 0 : false - - return { - ...storedProfile, - sourceType, - isCustom: sourceType === 'custom', - configured: launchReady, - launchReady, - launchIssues, - hostReady, - hostIssues - } -} - -function resolveCatalogFileSource(source){ - if(source.startsWith('file://')){ - return decodeURIComponent(source.substring('file://'.length)) - } - return path.resolve(source) -} - -async function readCatalogSource(source){ - if(/^https?:\/\//i.test(source)){ - return got(source, { - responseType: 'json' - }).json() - } - - return fs.readJson(resolveCatalogFileSource(source)) -} - -exports.getLocalCatalogPath = function(){ - return LOCAL_CATALOG_PATH -} - -exports.getDefaultRemoteCatalogUrl = function(){ - return DEFAULT_REMOTE_CATALOG_URL -} - -function buildCatalogSourceCandidates(configuredSource){ - const trimmedSource = normalizeNullableText(configuredSource) - if(trimmedSource != null){ - return [trimmedSource] - } - - return [DEFAULT_REMOTE_CATALOG_URL, LOCAL_CATALOG_PATH] -} - -exports.loadCatalog = async function(){ - const configuredSource = ConfigManager.getLibraryCatalogSource() - const sourceCandidates = buildCatalogSourceCandidates(configuredSource) - let rawCatalog = { - version: 1, - profiles: [] - } - let sourceError = null - let source = sourceCandidates[sourceCandidates.length - 1] - - for(const candidate of sourceCandidates){ - try { - rawCatalog = await readCatalogSource(candidate) - source = candidate - sourceError = null - break - } catch (error) { - source = candidate - sourceError = error - } - } - - const rawProfiles = Array.isArray(rawCatalog.profiles) ? rawCatalog.profiles : [] - - return { - version: rawCatalog.version ?? 1, - source, - sourceError, - profiles: rawProfiles - .filter((profile) => profile != null && typeof profile.id === 'string' && typeof profile.name === 'string') - .map((profile) => normalizeProfile(profile, 'catalog', source)) - .sort((left, right) => left.name.localeCompare(right.name, 'ko')) - } -} - -exports.getInstalledProfiles = async function(){ - const catalog = await exports.loadCatalog() - const installedProfiles = ConfigManager.getInstalledLibraryProfiles() - - const mergedProfiles = installedProfiles.map((installedProfile) => { - const latestProfile = catalog.profiles.find((profile) => profile.id === installedProfile.id) - return latestProfile != null - ? { - ...installedProfile, - ...latestProfile, - installedAt: installedProfile.installedAt - } - : installedProfile - }) - - ConfigManager.setInstalledLibraryProfiles(mergedProfiles) - ConfigManager.save() - - return mergedProfiles -} - -exports.installProfile = async function(profileId){ - const catalog = await exports.loadCatalog() - const profile = catalog.profiles.find((entry) => entry.id === profileId) - - if(profile == null){ - throw new Error(`Unknown profile: ${profileId}`) - } - - const installedProfile = { - ...profile, - installedAt: new Date().toISOString() - } - - ConfigManager.upsertInstalledLibraryProfile(installedProfile) - - if(ConfigManager.getSelectedLibraryProfile() == null){ - ConfigManager.setSelectedLibraryProfile(profile.id) - } - - ConfigManager.save() - return installedProfile -} - -exports.removeProfile = function(profileId){ - ConfigManager.removeInstalledLibraryProfile(profileId) - ConfigManager.save() -} - -exports.selectProfile = function(profileId){ - ConfigManager.setSelectedLibraryProfile(profileId) - ConfigManager.save() -} - -exports.getSelectedProfileId = function(){ - return ConfigManager.getSelectedLibraryProfile() -} - -exports.getSelectedProfileSync = function(){ - const selectedProfileId = ConfigManager.getSelectedLibraryProfile() - if(selectedProfileId == null){ - return null - } - return ConfigManager.getInstalledLibraryProfile(selectedProfileId) -} - -exports.resolveServerAddress = function(profile){ - if(profile == null){ - return null - } - - const manualAddress = ConfigManager.getLibraryServerAddressOverride(profile.id) - if(manualAddress != null && manualAddress.length > 0){ - return manualAddress - } - - return null -} - -exports.setServerAddressOverride = function(profileId, address){ - ConfigManager.setLibraryServerAddressOverride(profileId, address) - ConfigManager.save() -} - -exports.shouldHostLocally = function(profile){ - if(profile == null || profile.serverEnabled !== true){ - return false - } - - return exports.resolveServerAddress(profile) == null -} - -exports.applyConfiguredProfile = function(){ - const selectedProfile = exports.getSelectedProfileSync() - const distributionUrl = selectedProfile?.distributionUrl ?? DEFAULT_REMOTE_DISTRO_URL - setRemoteDistributionUrl(distributionUrl) - return selectedProfile -} diff --git a/app/assets/js/configmanager.js b/app/assets/js/configmanager.js deleted file mode 100644 index e921024..0000000 --- a/app/assets/js/configmanager.js +++ /dev/null @@ -1,1023 +0,0 @@ -const fs = require('fs-extra') -const { LoggerUtil } = require('helios-core') -const os = require('os') -const path = require('path') - -const logger = LoggerUtil.getLogger('ConfigManager') - -const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME) - -const dataPath = path.join(sysRoot, '.mrslauncher') - -const launcherDir = require('@electron/remote').app.getPath('userData') - -/** - * Retrieve the absolute path of the launcher directory. - * - * @returns {string} The absolute path of the launcher directory. - */ -exports.getLauncherDirectory = function(){ - return launcherDir -} - -/** - * Get the launcher's data directory. This is where all files related - * to game launch are installed (common, instances, java, etc). - * - * @returns {string} The absolute path of the launcher's data directory. - */ -exports.getDataDirectory = function(def = false){ - return !def ? config.settings.launcher.dataDirectory : DEFAULT_CONFIG.settings.launcher.dataDirectory -} - -/** - * Set the new data directory. - * - * @param {string} dataDirectory The new data directory. - */ -exports.setDataDirectory = function(dataDirectory){ - config.settings.launcher.dataDirectory = dataDirectory -} - -const configPath = path.join(exports.getLauncherDirectory(), 'config.json') -const configPathLEGACY = path.join(dataPath, 'config.json') -const firstLaunch = !fs.existsSync(configPath) && !fs.existsSync(configPathLEGACY) - -exports.getAbsoluteMinRAM = function(ram){ - if(ram?.minimum != null) { - return ram.minimum/1024 - } else { - // Legacy behavior - const mem = os.totalmem() - return mem >= (6*1073741824) ? 3 : 2 - } -} - -exports.getAbsoluteMaxRAM = function(_ram){ - const mem = os.totalmem() - const gT16 = mem-(16*1073741824) - return Math.floor((mem-(gT16 > 0 ? (Number.parseInt(gT16/8) + (16*1073741824)/4) : mem/4))/1073741824) -} - -function resolveSelectedRAM(ram) { - if(ram?.recommended != null) { - return `${ram.recommended}M` - } else { - // Legacy behavior - const mem = os.totalmem() - return mem >= (8*1073741824) ? '4G' : (mem >= (6*1073741824) ? '3G' : '2G') - } -} - -/** - * Three types of values: - * Static = Explicitly declared. - * Dynamic = Calculated by a private function. - * Resolved = Resolved externally, defaults to null. - */ -const DEFAULT_CONFIG = { - settings: { - game: { - resWidth: 1280, - resHeight: 720, - fullscreen: false, - autoConnect: true, - launchDetached: true - }, - launcher: { - allowPrerelease: false, - dataDirectory: dataPath - } - }, - newsCache: { - date: null, - content: null, - dismissed: false - }, - clientToken: null, - selectedServer: null, // Resolved - selectedAccount: null, - library: { - catalogSource: null, - selectedProfile: null, - installedProfiles: [], - serverAddressOverrides: {}, - publishedServerAddresses: {}, - profileAssetState: {}, - quickPlayWorlds: {} - }, - authenticationDatabase: {}, - modConfigurations: [], - javaConfig: {} -} - -let config = null - -// Persistance Utility Functions - -/** - * Save the current configuration to a file. - */ -exports.save = function(){ - fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'UTF-8') -} - -/** - * Load the configuration into memory. If a configuration file exists, - * that will be read and saved. Otherwise, a default configuration will - * be generated. Note that "resolved" values default to null and will - * need to be externally assigned. - */ -exports.load = function(){ - let doLoad = true - - if(!fs.existsSync(configPath)){ - // Create all parent directories. - fs.ensureDirSync(path.join(configPath, '..')) - if(fs.existsSync(configPathLEGACY)){ - fs.moveSync(configPathLEGACY, configPath) - } else { - doLoad = false - config = DEFAULT_CONFIG - exports.save() - } - } - if(doLoad){ - let doValidate = false - try { - config = JSON.parse(fs.readFileSync(configPath, 'UTF-8')) - doValidate = true - } catch (err){ - logger.error(err) - logger.info('Configuration file contains malformed JSON or is corrupt.') - logger.info('Generating a new configuration file.') - fs.ensureDirSync(path.join(configPath, '..')) - config = DEFAULT_CONFIG - exports.save() - } - if(doValidate){ - config = validateKeySet(DEFAULT_CONFIG, config) - exports.save() - } - } - logger.info('Successfully Loaded') -} - -/** - * @returns {boolean} Whether or not the manager has been loaded. - */ -exports.isLoaded = function(){ - return config != null -} - -/** - * Validate that the destination object has at least every field - * present in the source object. Assign a default value otherwise. - * - * @param {Object} srcObj The source object to reference against. - * @param {Object} destObj The destination object. - * @returns {Object} A validated destination object. - */ -function validateKeySet(srcObj, destObj){ - if(srcObj == null){ - srcObj = {} - } - const validationBlacklist = ['authenticationDatabase', 'javaConfig'] - const keys = Object.keys(srcObj) - for(let i=0; i} An array of each stored authenticated account. - */ -exports.getAuthAccounts = function(){ - return config.authenticationDatabase -} - -/** - * Returns the authenticated account with the given uuid. Value may - * be null. - * - * @param {string} uuid The uuid of the authenticated account. - * @returns {Object} The authenticated account with the given uuid. - */ -exports.getAuthAccount = function(uuid){ - return config.authenticationDatabase[uuid] -} - -/** - * Update the access token of an authenticated mojang account. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The new Access Token. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.updateMojangAuthAccount = function(uuid, accessToken){ - config.authenticationDatabase[uuid].accessToken = accessToken - config.authenticationDatabase[uuid].type = 'mojang' // For gradual conversion. - return config.authenticationDatabase[uuid] -} - -/** - * Adds an authenticated mojang account to the database to be stored. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The accessToken of the authenticated account. - * @param {string} username The username (usually email) of the authenticated account. - * @param {string} displayName The in game name of the authenticated account. - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.addMojangAuthAccount = function(uuid, accessToken, username, displayName){ - config.selectedAccount = uuid - config.authenticationDatabase[uuid] = { - type: 'mojang', - accessToken, - username: username.trim(), - uuid: uuid.trim(), - displayName: displayName.trim() - } - return config.authenticationDatabase[uuid] -} - -/** - * Update the tokens of an authenticated microsoft account. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The new Access Token. - * @param {string} msAccessToken The new Microsoft Access Token - * @param {string} msRefreshToken The new Microsoft Refresh Token - * @param {date} msExpires The date when the microsoft access token expires - * @param {date} mcExpires The date when the mojang access token expires - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.updateMicrosoftAuthAccount = function(uuid, accessToken, msAccessToken, msRefreshToken, msExpires, mcExpires) { - config.authenticationDatabase[uuid].accessToken = accessToken - config.authenticationDatabase[uuid].expiresAt = mcExpires - config.authenticationDatabase[uuid].microsoft.access_token = msAccessToken - config.authenticationDatabase[uuid].microsoft.refresh_token = msRefreshToken - config.authenticationDatabase[uuid].microsoft.expires_at = msExpires - return config.authenticationDatabase[uuid] -} - -/** - * Adds an authenticated microsoft account to the database to be stored. - * - * @param {string} uuid The uuid of the authenticated account. - * @param {string} accessToken The accessToken of the authenticated account. - * @param {string} name The in game name of the authenticated account. - * @param {date} mcExpires The date when the mojang access token expires - * @param {string} msAccessToken The microsoft access token - * @param {string} msRefreshToken The microsoft refresh token - * @param {date} msExpires The date when the microsoft access token expires - * - * @returns {Object} The authenticated account object created by this action. - */ -exports.addMicrosoftAuthAccount = function(uuid, accessToken, name, mcExpires, msAccessToken, msRefreshToken, msExpires) { - config.selectedAccount = uuid - config.authenticationDatabase[uuid] = { - type: 'microsoft', - accessToken, - username: name.trim(), - uuid: uuid.trim(), - displayName: name.trim(), - expiresAt: mcExpires, - microsoft: { - access_token: msAccessToken, - refresh_token: msRefreshToken, - expires_at: msExpires - } - } - return config.authenticationDatabase[uuid] -} - -/** - * Remove an authenticated account from the database. If the account - * was also the selected account, a new one will be selected. If there - * are no accounts, the selected account will be null. - * - * @param {string} uuid The uuid of the authenticated account. - * - * @returns {boolean} True if the account was removed, false if it never existed. - */ -exports.removeAuthAccount = function(uuid){ - if(config.authenticationDatabase[uuid] != null){ - delete config.authenticationDatabase[uuid] - if(config.selectedAccount === uuid){ - const keys = Object.keys(config.authenticationDatabase) - if(keys.length > 0){ - config.selectedAccount = keys[0] - } else { - config.selectedAccount = null - config.clientToken = null - } - } - return true - } - return false -} - -/** - * Get the currently selected authenticated account. - * - * @returns {Object} The selected authenticated account. - */ -exports.getSelectedAccount = function(){ - return config.authenticationDatabase[config.selectedAccount] -} - -/** - * Set the selected authenticated account. - * - * @param {string} uuid The UUID of the account which is to be set - * as the selected account. - * - * @returns {Object} The selected authenticated account. - */ -exports.setSelectedAccount = function(uuid){ - const authAcc = config.authenticationDatabase[uuid] - if(authAcc != null) { - config.selectedAccount = uuid - } - return authAcc -} - -/** - * Retrieve the configured launcher catalog source. - * - * @returns {string|null} A remote URL or local path for the install catalog. - */ -exports.getLibraryCatalogSource = function(){ - return config.library.catalogSource -} - -/** - * Set the launcher catalog source. - * - * @param {string|null} source A remote URL or local path. - */ -exports.setLibraryCatalogSource = function(source){ - if(typeof source !== 'string' || source.trim().length === 0){ - config.library.catalogSource = null - } else { - config.library.catalogSource = source.trim() - } -} - -/** - * Retrieve the selected installable profile ID. - * - * @returns {string|null} The selected profile ID. - */ -exports.getSelectedLibraryProfile = function(){ - return config.library.selectedProfile -} - -/** - * Set the selected installable profile ID. - * - * @param {string|null} profileId The selected profile ID. - */ -exports.setSelectedLibraryProfile = function(profileId){ - config.library.selectedProfile = profileId -} - -/** - * Retrieve installed library profiles. - * - * @returns {Array.} Installed profile metadata. - */ -exports.getInstalledLibraryProfiles = function(){ - return config.library.installedProfiles -} - -/** - * Replace installed library profiles. - * - * @param {Array.} profiles Installed profile metadata. - */ -exports.setInstalledLibraryProfiles = function(profiles){ - config.library.installedProfiles = profiles -} - -/** - * Retrieve a single installed profile. - * - * @param {string} profileId The profile ID. - * @returns {Object|null} The installed profile entry. - */ -exports.getInstalledLibraryProfile = function(profileId){ - return config.library.installedProfiles.find((profile) => profile.id === profileId) ?? null -} - -/** - * Add or replace an installed library profile. - * - * @param {Object} profile The profile metadata snapshot. - */ -exports.upsertInstalledLibraryProfile = function(profile){ - const existingIndex = config.library.installedProfiles.findIndex((entry) => entry.id === profile.id) - if(existingIndex >= 0){ - config.library.installedProfiles[existingIndex] = profile - } else { - config.library.installedProfiles.push(profile) - } -} - -/** - * Remove an installed profile. - * - * @param {string} profileId The profile ID. - */ -exports.removeInstalledLibraryProfile = function(profileId){ - config.library.installedProfiles = config.library.installedProfiles.filter((profile) => profile.id !== profileId) - if(config.library.selectedProfile === profileId){ - config.library.selectedProfile = config.library.installedProfiles[0]?.id ?? null - } - delete config.library.serverAddressOverrides[profileId] - delete config.library.publishedServerAddresses[profileId] - delete config.library.profileAssetState[profileId] - delete config.library.quickPlayWorlds[profileId] -} - -/** - * Retrieve a manual server address override for a profile. - * - * @param {string} profileId The profile ID. - * @returns {string|null} The override address if present. - */ -exports.getLibraryServerAddressOverride = function(profileId){ - return config.library.serverAddressOverrides[profileId] ?? null -} - -/** - * Set a manual server address override for a profile. - * - * @param {string} profileId The profile ID. - * @param {string|null} address The address override. - */ -exports.setLibraryServerAddressOverride = function(profileId, address){ - if(address == null || address.trim().length === 0){ - delete config.library.serverAddressOverrides[profileId] - } else { - config.library.serverAddressOverrides[profileId] = address.trim() - } -} - -/** - * Retrieve a published server address for a hosted profile. - * - * @param {string} profileId The profile ID. - * @returns {string|null} A public address if available. - */ -exports.getPublishedLibraryServerAddress = function(profileId){ - return config.library.publishedServerAddresses[profileId] ?? null -} - -/** - * Store or clear a published server address. - * - * @param {string} profileId The profile ID. - * @param {string|null} address The published address. - */ -exports.setPublishedLibraryServerAddress = function(profileId, address){ - if(address == null || address.trim().length === 0){ - delete config.library.publishedServerAddresses[profileId] - } else { - config.library.publishedServerAddresses[profileId] = address.trim() - } -} - -/** - * Retrieve saved asset state for a profile. - * - * @param {string} profileId The profile ID. - * @returns {Object} Saved asset state. - */ -exports.getLibraryProfileAssetState = function(profileId){ - return config.library.profileAssetState[profileId] ?? {} -} - -/** - * Merge saved asset state for a profile. - * - * @param {string} profileId The profile ID. - * @param {Object} state Partial asset state. - */ -exports.setLibraryProfileAssetState = function(profileId, state){ - config.library.profileAssetState[profileId] = { - ...exports.getLibraryProfileAssetState(profileId), - ...state - } -} - -/** - * Clear saved asset state for a profile. - * - * @param {string} profileId The profile ID. - */ -exports.clearLibraryProfileAssetState = function(profileId){ - delete config.library.profileAssetState[profileId] -} - -/** - * Retrieve the quick play singleplayer world name for a profile. - * - * @param {string} profileId The profile ID. - * @returns {string|null} The saved world folder name. - */ -exports.getLibraryQuickPlayWorld = function(profileId){ - return config.library.quickPlayWorlds[profileId] ?? null -} - -/** - * Set or clear the quick play singleplayer world name for a profile. - * - * @param {string} profileId The profile ID. - * @param {string|null} worldName The world folder name. - */ -exports.setLibraryQuickPlayWorld = function(profileId, worldName){ - if(worldName == null || worldName.trim().length === 0){ - delete config.library.quickPlayWorlds[profileId] - } else { - config.library.quickPlayWorlds[profileId] = worldName.trim() - } -} - -/** - * Get an array of each mod configuration currently stored. - * - * @returns {Array.} An array of each stored mod configuration. - */ -exports.getModConfigurations = function(){ - return config.modConfigurations -} - -/** - * Set the array of stored mod configurations. - * - * @param {Array.} configurations An array of mod configurations. - */ -exports.setModConfigurations = function(configurations){ - config.modConfigurations = configurations -} - -/** - * Get the mod configuration for a specific server. - * - * @param {string} serverid The id of the server. - * @returns {Object} The mod configuration for the given server. - */ -exports.getModConfiguration = function(serverid){ - const cfgs = config.modConfigurations - for(let i=0; i 8) { - return defaultJavaConfig17(ram) - } else { - return defaultJavaConfig8(ram) - } -} - -function defaultJavaConfig8(ram) { - return { - minRAM: resolveSelectedRAM(ram), - maxRAM: resolveSelectedRAM(ram), - executable: null, - jvmOptions: [ - '-XX:+UseConcMarkSweepGC', - '-XX:+CMSIncrementalMode', - '-XX:-UseAdaptiveSizePolicy', - '-Xmn128M' - ], - } -} - -function defaultJavaConfig17(ram) { - return { - minRAM: resolveSelectedRAM(ram), - maxRAM: resolveSelectedRAM(ram), - executable: null, - jvmOptions: [ - '-XX:+UnlockExperimentalVMOptions', - '-XX:+UseG1GC', - '-XX:G1NewSizePercent=20', - '-XX:G1ReservePercent=20', - '-XX:MaxGCPauseMillis=50', - '-XX:G1HeapRegionSize=32M' - ], - } -} - -/** - * Ensure a java config property is set for the given server. - * - * @param {string} serverid The server id. - * @param {*} mcVersion The minecraft version of the server. - */ -exports.ensureJavaConfig = function(serverid, effectiveJavaOptions, ram) { - if(!Object.prototype.hasOwnProperty.call(config.javaConfig, serverid)) { - config.javaConfig[serverid] = defaultJavaConfig(effectiveJavaOptions, ram) - } -} - -/** - * Retrieve the minimum amount of memory for JVM initialization. This value - * contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = - * 1024 MegaBytes, etc. - * - * @param {string} serverid The server id. - * @returns {string} The minimum amount of memory for JVM initialization. - */ -exports.getMinRAM = function(serverid){ - return config.javaConfig[serverid].minRAM -} - -/** - * Set the minimum amount of memory for JVM initialization. This value should - * contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = - * 1024 MegaBytes, etc. - * - * @param {string} serverid The server id. - * @param {string} minRAM The new minimum amount of memory for JVM initialization. - */ -exports.setMinRAM = function(serverid, minRAM){ - config.javaConfig[serverid].minRAM = minRAM -} - -/** - * Retrieve the maximum amount of memory for JVM initialization. This value - * contains the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = - * 1024 MegaBytes, etc. - * - * @param {string} serverid The server id. - * @returns {string} The maximum amount of memory for JVM initialization. - */ -exports.getMaxRAM = function(serverid){ - return config.javaConfig[serverid].maxRAM -} - -/** - * Set the maximum amount of memory for JVM initialization. This value should - * contain the units of memory. For example, '5G' = 5 GigaBytes, '1024M' = - * 1024 MegaBytes, etc. - * - * @param {string} serverid The server id. - * @param {string} maxRAM The new maximum amount of memory for JVM initialization. - */ -exports.setMaxRAM = function(serverid, maxRAM){ - config.javaConfig[serverid].maxRAM = maxRAM -} - -/** - * Retrieve the path of the Java Executable. - * - * This is a resolved configuration value and defaults to null until externally assigned. - * - * @param {string} serverid The server id. - * @returns {string} The path of the Java Executable. - */ -exports.getJavaExecutable = function(serverid){ - return config.javaConfig[serverid].executable -} - -/** - * Set the path of the Java Executable. - * - * @param {string} serverid The server id. - * @param {string} executable The new path of the Java Executable. - */ -exports.setJavaExecutable = function(serverid, executable){ - config.javaConfig[serverid].executable = executable -} - -/** - * Retrieve the additional arguments for JVM initialization. Required arguments, - * such as memory allocation, will be dynamically resolved and will not be included - * in this value. - * - * @param {string} serverid The server id. - * @returns {Array.} An array of the additional arguments for JVM initialization. - */ -exports.getJVMOptions = function(serverid){ - return config.javaConfig[serverid].jvmOptions -} - -/** - * Set the additional arguments for JVM initialization. Required arguments, - * such as memory allocation, will be dynamically resolved and should not be - * included in this value. - * - * @param {string} serverid The server id. - * @param {Array.} jvmOptions An array of the new additional arguments for JVM - * initialization. - */ -exports.setJVMOptions = function(serverid, jvmOptions){ - config.javaConfig[serverid].jvmOptions = jvmOptions -} - -// Game Settings - -/** - * Retrieve the width of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The width of the game window. - */ -exports.getGameWidth = function(def = false){ - return !def ? config.settings.game.resWidth : DEFAULT_CONFIG.settings.game.resWidth -} - -/** - * Set the width of the game window. - * - * @param {number} resWidth The new width of the game window. - */ -exports.setGameWidth = function(resWidth){ - config.settings.game.resWidth = Number.parseInt(resWidth) -} - -/** - * Validate a potential new width value. - * - * @param {number} resWidth The width value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameWidth = function(resWidth){ - const nVal = Number.parseInt(resWidth) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Retrieve the height of the game window. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {number} The height of the game window. - */ -exports.getGameHeight = function(def = false){ - return !def ? config.settings.game.resHeight : DEFAULT_CONFIG.settings.game.resHeight -} - -/** - * Set the height of the game window. - * - * @param {number} resHeight The new height of the game window. - */ -exports.setGameHeight = function(resHeight){ - config.settings.game.resHeight = Number.parseInt(resHeight) -} - -/** - * Validate a potential new height value. - * - * @param {number} resHeight The height value to validate. - * @returns {boolean} Whether or not the value is valid. - */ -exports.validateGameHeight = function(resHeight){ - const nVal = Number.parseInt(resHeight) - return Number.isInteger(nVal) && nVal >= 0 -} - -/** - * Check if the game should be launched in fullscreen mode. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game is set to launch in fullscreen mode. - */ -exports.getFullscreen = function(def = false){ - return !def ? config.settings.game.fullscreen : DEFAULT_CONFIG.settings.game.fullscreen -} - -/** - * Change the status of if the game should be launched in fullscreen mode. - * - * @param {boolean} fullscreen Whether or not the game should launch in fullscreen mode. - */ -exports.setFullscreen = function(fullscreen){ - config.settings.game.fullscreen = fullscreen -} - -/** - * Check if the game should auto connect to servers. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game should auto connect to servers. - */ -exports.getAutoConnect = function(def = false){ - return !def ? config.settings.game.autoConnect : DEFAULT_CONFIG.settings.game.autoConnect -} - -/** - * Change the status of whether or not the game should auto connect to servers. - * - * @param {boolean} autoConnect Whether or not the game should auto connect to servers. - */ -exports.setAutoConnect = function(autoConnect){ - config.settings.game.autoConnect = autoConnect -} - -/** - * Check if the game should launch as a detached process. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the game will launch as a detached process. - */ -exports.getLaunchDetached = function(def = false){ - return !def ? config.settings.game.launchDetached : DEFAULT_CONFIG.settings.game.launchDetached -} - -/** - * Change the status of whether or not the game should launch as a detached process. - * - * @param {boolean} launchDetached Whether or not the game should launch as a detached process. - */ -exports.setLaunchDetached = function(launchDetached){ - config.settings.game.launchDetached = launchDetached -} - -// Launcher Settings - -/** - * Check if the launcher should download prerelease versions. - * - * @param {boolean} def Optional. If true, the default value will be returned. - * @returns {boolean} Whether or not the launcher should download prerelease versions. - */ -exports.getAllowPrerelease = function(def = false){ - return !def ? config.settings.launcher.allowPrerelease : DEFAULT_CONFIG.settings.launcher.allowPrerelease -} - -/** - * Change the status of Whether or not the launcher should download prerelease versions. - * - * @param {boolean} launchDetached Whether or not the launcher should download prerelease versions. - */ -exports.setAllowPrerelease = function(allowPrerelease){ - config.settings.launcher.allowPrerelease = allowPrerelease -} - -/** - * Validate a library catalog source. - * - * @param {string} source A remote URL or local path. - * @returns {boolean} Whether the value is allowed. - */ -exports.validateLibraryCatalogSource = function(source){ - const trimmedSource = typeof source === 'string' ? source.trim() : '' - if(trimmedSource.length === 0){ - return true - } - - if(trimmedSource.includes('://')){ - return /^https?:\/\/\S+$/i.test(trimmedSource) || trimmedSource.startsWith('file://') - } - - return true -} diff --git a/app/assets/js/discordwrapper.js b/app/assets/js/discordwrapper.js deleted file mode 100644 index 11ed053..0000000 --- a/app/assets/js/discordwrapper.js +++ /dev/null @@ -1,52 +0,0 @@ -// Work in progress -const { LoggerUtil } = require('helios-core') - -const logger = LoggerUtil.getLogger('DiscordWrapper') - -const { Client } = require('discord-rpc-patch') - -const Lang = require('./langloader') - -let client -let activity - -exports.initRPC = function(genSettings, servSettings, initialDetails = Lang.queryJS('discord.waiting')){ - client = new Client({ transport: 'ipc' }) - - activity = { - details: initialDetails, - state: Lang.queryJS('discord.state', {shortId: servSettings.shortId}), - largeImageKey: servSettings.largeImageKey, - largeImageText: servSettings.largeImageText, - smallImageKey: genSettings.smallImageKey, - smallImageText: genSettings.smallImageText, - startTimestamp: new Date().getTime(), - instance: false - } - - client.on('ready', () => { - logger.info('Discord RPC Connected') - client.setActivity(activity) - }) - - client.login({clientId: genSettings.clientId}).catch(error => { - if(error.message.includes('ENOENT')) { - logger.info('Unable to initialize Discord Rich Presence, no client detected.') - } else { - logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error) - } - }) -} - -exports.updateDetails = function(details){ - activity.details = details - client.setActivity(activity) -} - -exports.shutdownRPC = function(){ - if(!client) return - client.clearActivity() - client.destroy() - client = null - activity = null -} \ No newline at end of file diff --git a/app/assets/js/distromanager.js b/app/assets/js/distromanager.js deleted file mode 100644 index 3bd592c..0000000 --- a/app/assets/js/distromanager.js +++ /dev/null @@ -1,183 +0,0 @@ -const { DistributionAPI, HeliosDistribution } = require('helios-core/common') - -const ConfigManager = require('./configmanager') - -const DEFAULT_REMOTE_DISTRO_URL = 'https://cdn.mysticred.space/launcher/distribution.json' - -let remoteDistroUrl = DEFAULT_REMOTE_DISTRO_URL -let activeApi = createApi(remoteDistroUrl) - -function distributionsDiffer(left, right){ - return JSON.stringify(left) !== JSON.stringify(right) -} - -function sanitizeServer(rawServer){ - const nextServer = { - ...rawServer - } - - if(typeof nextServer.address !== 'string' || nextServer.address.trim().length === 0){ - nextServer.address = '127.0.0.1:25565' - } else { - nextServer.address = nextServer.address.trim() - } - - if(!Array.isArray(nextServer.modules)){ - nextServer.modules = [] - } - - if(typeof nextServer.minecraftVersion !== 'string' || nextServer.minecraftVersion.trim().length === 0){ - nextServer.minecraftVersion = typeof nextServer.version === 'string' && nextServer.version.trim().length > 0 - ? nextServer.version.trim() - : '1.20.1' - } - - return nextServer -} - -function sanitizeDistribution(rawDistribution){ - const nextDistribution = rawDistribution == null - ? { version: '1.0.0', rss: '', servers: [] } - : { ...rawDistribution } - - nextDistribution.servers = Array.isArray(nextDistribution.servers) - ? nextDistribution.servers.map((server) => sanitizeServer(server)) - : [] - - return nextDistribution -} - -function buildDistribution(api, rawDistribution){ - const sanitizedDistribution = sanitizeDistribution(rawDistribution) - api.rawDistribution = sanitizedDistribution - api.distribution = new HeliosDistribution(sanitizedDistribution, api.commonDir, api.instanceDir) - return api.distribution -} - -function patchDistributionApi(api){ - const originalPullRemote = api.pullRemote.bind(api) - const originalPullLocal = api.pullLocal.bind(api) - const originalWriteDistributionToDisk = api.writeDistributionToDisk.bind(api) - - api.writeDistributionToDisk = async function(distribution){ - const sanitizedDistribution = sanitizeDistribution(distribution) - await originalWriteDistributionToDisk(sanitizedDistribution) - } - - api.pullRemote = async function(){ - const response = await originalPullRemote() - if(response?.data != null){ - response.data = sanitizeDistribution(response.data) - } - return response - } - - api.pullLocal = async function(){ - const rawDistribution = await originalPullLocal() - if(rawDistribution == null){ - return rawDistribution - } - - const sanitizedDistribution = sanitizeDistribution(rawDistribution) - if(distributionsDiffer(rawDistribution, sanitizedDistribution)){ - await originalWriteDistributionToDisk(sanitizedDistribution) - } - - return sanitizedDistribution - } - - api.getDistribution = async function(){ - if(this.rawDistribution == null || this.distribution == null){ - const rawDistribution = await this.loadDistribution() - return buildDistribution(this, rawDistribution) - } - return this.distribution - } - - api.getDistributionLocalLoadOnly = async function(){ - if(this.rawDistribution == null || this.distribution == null){ - const rawDistribution = await this.pullLocal() - if(rawDistribution == null){ - throw new Error('FATAL: Unable to load distribution from local disk.') - } - return buildDistribution(this, rawDistribution) - } - return this.distribution - } - - api.refreshDistributionOrFallback = async function(){ - const rawDistribution = await this._loadDistributionNullable() - if(rawDistribution == null){ - return this.distribution - } - return buildDistribution(this, rawDistribution) - } - - return api -} - -function createApi(url) { - const api = new DistributionAPI( - ConfigManager.getLauncherDirectory(), - null, - null, - url, - false - ) - - return patchDistributionApi(api) -} - -function replaceApi(url) { - const nextApi = createApi(url) - - if(activeApi != null){ - if(typeof activeApi.commonDir !== 'undefined'){ - nextApi.commonDir = activeApi.commonDir - } - if(typeof activeApi.instanceDir !== 'undefined'){ - nextApi.instanceDir = activeApi.instanceDir - } - if(typeof activeApi.isDevMode === 'function' && activeApi.isDevMode()){ - nextApi.toggleDevMode(true) - } - } - - activeApi = nextApi - remoteDistroUrl = url -} - -exports.DEFAULT_REMOTE_DISTRO_URL = DEFAULT_REMOTE_DISTRO_URL - -exports.getRemoteDistributionUrl = function() { - return remoteDistroUrl -} - -exports.setRemoteDistributionUrl = function(url) { - if(url != null && url.trim().length > 0 && url !== remoteDistroUrl){ - replaceApi(url) - } - return remoteDistroUrl -} - -exports.resetRemoteDistributionUrl = function() { - replaceApi(DEFAULT_REMOTE_DISTRO_URL) - return remoteDistroUrl -} - -exports.DistroAPI = new Proxy({}, { - get(_target, prop) { - const value = activeApi[prop] - if(typeof value === 'function'){ - return value.bind(activeApi) - } - return value - }, - set(_target, prop, value) { - activeApi[prop] = value - return true - }, - has(_target, prop) { - return prop in activeApi - } -}) diff --git a/app/assets/js/dropinmodutil.js b/app/assets/js/dropinmodutil.js deleted file mode 100644 index f816a20..0000000 --- a/app/assets/js/dropinmodutil.js +++ /dev/null @@ -1,238 +0,0 @@ -const fs = require('fs-extra') -const path = require('path') -const { ipcRenderer, shell } = require('electron') -const { SHELL_OPCODE } = require('./ipcconstants') - -// Group #1: File Name (without .disabled, if any) -// Group #2: File Extension (jar, zip, or litemod) -// Group #3: If it is disabled (if string 'disabled' is present) -const MOD_REGEX = /^(.+(jar|zip|litemod))(?:\.(disabled))?$/ -const DISABLED_EXT = '.disabled' - -const SHADER_REGEX = /^(.+)\.zip$/ -const SHADER_OPTION = /shaderPack=(.+)/ -const SHADER_DIR = 'shaderpacks' -const SHADER_CONFIG = 'optionsshaders.txt' - -/** - * Validate that the given directory exists. If not, it is - * created. - * - * @param {string} modsDir The path to the mods directory. - */ -exports.validateDir = function(dir) { - fs.ensureDirSync(dir) -} - -/** - * Scan for drop-in mods in both the mods folder and version - * safe mods folder. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} version The minecraft version of the server configuration. - * - * @returns {{fullName: string, name: string, ext: string, disabled: boolean}[]} - * An array of objects storing metadata about each discovered mod. - */ -exports.scanForDropinMods = function(modsDir, version) { - const modsDiscovered = [] - if(fs.existsSync(modsDir)){ - let modCandidates = fs.readdirSync(modsDir) - let verCandidates = [] - const versionDir = path.join(modsDir, version) - if(fs.existsSync(versionDir)){ - verCandidates = fs.readdirSync(versionDir) - } - for(let file of modCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: match[0], - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - for(let file of verCandidates){ - const match = MOD_REGEX.exec(file) - if(match != null){ - modsDiscovered.push({ - fullName: path.join(version, match[0]), - name: match[1], - ext: match[2], - disabled: match[3] != null - }) - } - } - } - return modsDiscovered -} - -/** - * Add dropin mods. - * - * @param {FileList} files The files to add. - * @param {string} modsDir The path to the mods directory. - */ -exports.addDropinMods = function(files, modsdir) { - - exports.validateDir(modsdir) - - for(let f of files) { - if(MOD_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(modsdir, f.name)) - } - } - -} - -/** - * Delete a drop-in mod from the file system. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to delete. - * - * @returns {Promise.} True if the mod was deleted, otherwise false. - */ -exports.deleteDropinMod = async function(modsDir, fullName){ - - const res = await ipcRenderer.invoke(SHELL_OPCODE.TRASH_ITEM, path.join(modsDir, fullName)) - - if(!res.result) { - shell.beep() - console.error('Error deleting drop-in mod.', res.error) - return false - } - - return true -} - -/** - * Toggle a discovered mod on or off. This is achieved by either - * adding or disabling the .disabled extension to the local file. - * - * @param {string} modsDir The path to the mods directory. - * @param {string} fullName The fullName of the discovered mod to toggle. - * @param {boolean} enable Whether to toggle on or off the mod. - * - * @returns {Promise.} A promise which resolves when the mod has - * been toggled. If an IO error occurs the promise will be rejected. - */ -exports.toggleDropinMod = function(modsDir, fullName, enable){ - return new Promise((resolve, reject) => { - const oldPath = path.join(modsDir, fullName) - const newPath = path.join(modsDir, enable ? fullName.substring(0, fullName.indexOf(DISABLED_EXT)) : fullName + DISABLED_EXT) - - fs.rename(oldPath, newPath, (err) => { - if(err){ - reject(err) - } else { - resolve() - } - }) - }) -} - -/** - * Check if a drop-in mod is enabled. - * - * @param {string} fullName The fullName of the discovered mod to toggle. - * @returns {boolean} True if the mod is enabled, otherwise false. - */ -exports.isDropinModEnabled = function(fullName){ - return !fullName.endsWith(DISABLED_EXT) -} - -/** - * Scan for shaderpacks inside the shaderpacks folder. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {{fullName: string, name: string}[]} - * An array of objects storing metadata about each discovered shaderpack. - */ -exports.scanForShaderpacks = function(instanceDir){ - const shaderDir = path.join(instanceDir, SHADER_DIR) - const packsDiscovered = [{ - fullName: 'OFF', - name: 'Off (Default)' - }] - if(fs.existsSync(shaderDir)){ - let modCandidates = fs.readdirSync(shaderDir) - for(let file of modCandidates){ - const match = SHADER_REGEX.exec(file) - if(match != null){ - packsDiscovered.push({ - fullName: match[0], - name: match[1] - }) - } - } - } - return packsDiscovered -} - -/** - * Read the optionsshaders.txt file to locate the current - * enabled pack. If the file does not exist, OFF is returned. - * - * @param {string} instanceDir The path to the server instance directory. - * - * @returns {string} The file name of the enabled shaderpack. - */ -exports.getEnabledShaderpack = function(instanceDir){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - if(fs.existsSync(optionsShaders)){ - const buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - const match = SHADER_OPTION.exec(buf) - if(match != null){ - return match[1] - } else { - console.warn('WARNING: Shaderpack regex failed.') - } - } - return 'OFF' -} - -/** - * Set the enabled shaderpack. - * - * @param {string} instanceDir The path to the server instance directory. - * @param {string} pack the file name of the shaderpack. - */ -exports.setEnabledShaderpack = function(instanceDir, pack){ - exports.validateDir(instanceDir) - - const optionsShaders = path.join(instanceDir, SHADER_CONFIG) - let buf - if(fs.existsSync(optionsShaders)){ - buf = fs.readFileSync(optionsShaders, {encoding: 'utf-8'}) - buf = buf.replace(SHADER_OPTION, `shaderPack=${pack}`) - } else { - buf = `shaderPack=${pack}` - } - fs.writeFileSync(optionsShaders, buf, {encoding: 'utf-8'}) -} - -/** - * Add shaderpacks. - * - * @param {FileList} files The files to add. - * @param {string} instanceDir The path to the server instance directory. - */ -exports.addShaderpacks = function(files, instanceDir) { - - const p = path.join(instanceDir, SHADER_DIR) - - exports.validateDir(p) - - for(let f of files) { - if(SHADER_REGEX.exec(f.name) != null) { - fs.moveSync(f.path, path.join(p, f.name)) - } - } - -} \ No newline at end of file diff --git a/app/assets/js/ipcconstants.js b/app/assets/js/ipcconstants.js deleted file mode 100644 index 1e9638b..0000000 --- a/app/assets/js/ipcconstants.js +++ /dev/null @@ -1,28 +0,0 @@ -// NOTE FOR THIRD-PARTY -// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID. -// SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md -exports.AZURE_CLIENT_ID = '8f387cc5-3138-4699-89a9-f97948e3927e' -// SEE NOTE ABOVE. - - -// Opcodes -exports.MSFT_OPCODE = { - OPEN_LOGIN: 'MSFT_AUTH_OPEN_LOGIN', - OPEN_LOGOUT: 'MSFT_AUTH_OPEN_LOGOUT', - REPLY_LOGIN: 'MSFT_AUTH_REPLY_LOGIN', - REPLY_LOGOUT: 'MSFT_AUTH_REPLY_LOGOUT' -} -// Reply types for REPLY opcode. -exports.MSFT_REPLY_TYPE = { - SUCCESS: 'MSFT_AUTH_REPLY_SUCCESS', - ERROR: 'MSFT_AUTH_REPLY_ERROR' -} -// Error types for ERROR reply. -exports.MSFT_ERROR = { - ALREADY_OPEN: 'MSFT_AUTH_ERR_ALREADY_OPEN', - NOT_FINISHED: 'MSFT_AUTH_ERR_NOT_FINISHED' -} - -exports.SHELL_OPCODE = { - TRASH_ITEM: 'TRASH_ITEM' -} \ No newline at end of file diff --git a/app/assets/js/isdev.js b/app/assets/js/isdev.js deleted file mode 100644 index 1ed55e5..0000000 --- a/app/assets/js/isdev.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict' -const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1 -const isEnvSet = 'ELECTRON_IS_DEV' in process.env - -module.exports = isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath)) \ No newline at end of file diff --git a/app/assets/js/langloader.js b/app/assets/js/langloader.js deleted file mode 100644 index 1b5e40e..0000000 --- a/app/assets/js/langloader.js +++ /dev/null @@ -1,43 +0,0 @@ -const fs = require('fs-extra') -const path = require('path') -const toml = require('toml') -const merge = require('lodash.merge') - -let lang - -exports.loadLanguage = function(id){ - lang = merge(lang || {}, toml.parse(fs.readFileSync(path.join(__dirname, '..', 'lang', `${id}.toml`))) || {}) -} - -exports.query = function(id, placeHolders){ - let query = id.split('.') - let res = lang - for(let q of query){ - res = res[q] - } - let text = res === lang ? '' : res - if (placeHolders) { - Object.entries(placeHolders).forEach(([key, value]) => { - text = text.replace(`{${key}}`, value) - }) - } - return text -} - -exports.queryJS = function(id, placeHolders){ - return exports.query(`js.${id}`, placeHolders) -} - -exports.queryEJS = function(id, placeHolders){ - return exports.query(`ejs.${id}`, placeHolders) -} - -exports.setupLanguage = function(){ - // Load Language Files - exports.loadLanguage('en_US') - // Uncomment this when translations are ready - exports.loadLanguage('ko_KR') - - // Load Custom Language File for Launcher Customizer - exports.loadLanguage('_custom') -} \ No newline at end of file diff --git a/app/assets/js/portmanager.js b/app/assets/js/portmanager.js deleted file mode 100644 index e7183cf..0000000 --- a/app/assets/js/portmanager.js +++ /dev/null @@ -1,299 +0,0 @@ -const childProcess = require('child_process') -const natUpnp = require('nat-upnp') - -const ConfigManager = require('./configmanager') - -const RULE_PREFIX = 'MRS Launcher Auto Port' -const UPnP_DESCRIPTION_PREFIX = 'MRS Launcher' -const UPnP_TTL_SECONDS = 3600 - -const states = new Map() - -function buildState(profileId, port){ - return { - profileId, - port, - statusCode: 'checking', - summary: '포트 확인 중', - message: '포트 개방 가능 여부를 확인하고 있습니다.', - tone: 'info', - firewallCreated: false, - ruleName: null, - upnpCreated: false, - checkedAt: Date.now() - } -} - -function toPublicState(state){ - return { - port: state.port, - statusCode: state.statusCode, - summary: state.summary, - message: state.message, - tone: state.tone, - checkedAt: state.checkedAt - } -} - -function createClient(){ - return natUpnp.createClient() -} - -function clientGetMappings(client){ - return new Promise((resolve, reject) => { - client.getMappings((error, results) => { - if(error){ - reject(error) - return - } - resolve(Array.isArray(results) ? results : []) - }) - }) -} - -function clientPortMapping(client, options){ - return new Promise((resolve, reject) => { - client.portMapping(options, (error) => { - if(error){ - reject(error) - return - } - resolve() - }) - }) -} - -function clientPortUnmapping(client, options){ - return new Promise((resolve, reject) => { - client.portUnmapping(options, (error) => { - if(error){ - reject(error) - return - } - resolve() - }) - }) -} - -function extractPort(mapping){ - if(mapping == null){ - return null - } - - if(typeof mapping.public === 'number'){ - return mapping.public - } - if(typeof mapping.publicPort === 'number'){ - return mapping.publicPort - } - if(typeof mapping.port === 'number'){ - return mapping.port - } - if(mapping.public != null && typeof mapping.public === 'object' && typeof mapping.public.port === 'number'){ - return mapping.public.port - } - - return null -} - -function extractProtocol(mapping){ - if(mapping == null){ - return '' - } - - const protocol = mapping.protocol ?? mapping.type ?? mapping.public?.protocol ?? '' - return String(protocol).toUpperCase() -} - -async function findExistingTcpMapping(port){ - const client = createClient() - const mappings = await clientGetMappings(client) - return mappings.find((mapping) => extractPort(mapping) === port && (extractProtocol(mapping) === '' || extractProtocol(mapping) === 'TCP')) ?? null -} - -function getFirewallRuleName(profileId, port){ - return `${RULE_PREFIX} ${profileId} ${port}` -} - -function ensureWindowsFirewallRule(profileId, port){ - if(process.platform !== 'win32'){ - return { - created: false, - ruleName: null - } - } - - const ruleName = getFirewallRuleName(profileId, port) - const result = childProcess.spawnSync('netsh', [ - 'advfirewall', - 'firewall', - 'add', - 'rule', - `name=${ruleName}`, - 'dir=in', - 'action=allow', - 'protocol=TCP', - `localport=${port}` - ], { - encoding: 'utf8' - }) - - return { - created: result.status === 0, - ruleName - } -} - -function deleteWindowsFirewallRule(ruleName){ - if(process.platform !== 'win32' || !ruleName){ - return - } - - childProcess.spawnSync('netsh', [ - 'advfirewall', - 'firewall', - 'delete', - 'rule', - `name=${ruleName}` - ], { - encoding: 'utf8' - }) -} - -function buildPortFailureMessage(error){ - const baseMessage = '자동 포트 개방 실패. 직접 포트포워딩 해주세요.' - if(error == null){ - return baseMessage - } - - const errorMessage = typeof error === 'string' - ? error - : (error.message ?? String(error)) - - return `${baseMessage} ${errorMessage}` -} - -exports.ensurePortAvailability = async function(profile){ - if(profile?.serverEnabled !== true){ - return { - port: null, - statusCode: 'not-needed', - summary: '포트 개방 필요 없음', - message: '이 프로필은 서버를 사용하지 않습니다.', - tone: 'info', - checkedAt: Date.now() - } - } - - const manualAddress = ConfigManager.getLibraryServerAddressOverride(profile.id) - if(manualAddress != null && manualAddress.length > 0){ - await exports.releaseProfilePort(profile.id) - return { - port: profile.serverPort ?? 25565, - statusCode: 'manual-address', - summary: '접속 주소 사용 중', - message: `직접 입력한 접속 주소가 설정되어 있어 자동 포트 개방을 건너뜁니다. (${manualAddress})`, - tone: 'info', - checkedAt: Date.now() - } - } - - const port = profile.serverPort ?? 25565 - const currentState = states.get(profile.id) - if(currentState != null && currentState.port === port && currentState.statusCode !== 'failed'){ - currentState.checkedAt = Date.now() - return toPublicState(currentState) - } - - const state = buildState(profile.id, port) - states.set(profile.id, state) - - const firewallState = ensureWindowsFirewallRule(profile.id, port) - state.firewallCreated = firewallState.created - state.ruleName = firewallState.ruleName - - try { - const existingMapping = await findExistingTcpMapping(port) - if(existingMapping != null){ - state.statusCode = 'already-open' - state.summary = '이미 포트가 열려있습니다.' - state.message = `TCP ${port} 포트에 기존 포트포워딩이 있어 그대로 사용합니다.` - state.tone = 'success' - state.checkedAt = Date.now() - return toPublicState(state) - } - - const client = createClient() - await clientPortMapping(client, { - public: port, - private: port, - protocol: 'TCP', - ttl: UPnP_TTL_SECONDS, - description: `${UPnP_DESCRIPTION_PREFIX} ${profile.id}` - }) - - state.upnpCreated = true - state.statusCode = 'opened' - state.summary = '자동 포트 개방 성공' - state.message = `UPnP로 TCP ${port} 포트를 열었습니다.${state.firewallCreated ? ' 윈도우 방화벽 허용도 같이 적용했습니다.' : ''}` - state.tone = 'success' - } catch (error) { - const message = typeof error?.message === 'string' ? error.message : String(error) - if(/ConflictInMappingEntry|Already/i.test(message)){ - state.statusCode = 'already-open' - state.summary = '이미 포트가 열려있습니다.' - state.message = `TCP ${port} 포트에 기존 포트포워딩이 있어 그대로 사용합니다.` - state.tone = 'success' - } else { - state.statusCode = 'failed' - state.summary = '자동 포트 개방 실패' - state.message = buildPortFailureMessage(error) - state.tone = 'error' - } - } - - state.checkedAt = Date.now() - return toPublicState(state) -} - -exports.getPortAvailabilityState = function(profileId){ - const state = states.get(profileId) - return state != null ? toPublicState(state) : null -} - -exports.releaseProfilePort = async function(profileId){ - const state = states.get(profileId) - if(state == null){ - return - } - - if(state.upnpCreated){ - try { - const client = createClient() - await clientPortUnmapping(client, { - public: state.port, - protocol: 'TCP' - }) - } catch (error) { - void error - } - } - - if(state.firewallCreated && state.ruleName){ - deleteWindowsFirewallRule(state.ruleName) - } - - states.delete(profileId) -} - -exports.cleanupAll = async function(){ - await Promise.all(Array.from(states.keys()).map((profileId) => exports.releaseProfilePort(profileId))) -} - -process.once('exit', () => { - for(const state of states.values()){ - if(state.firewallCreated && state.ruleName){ - deleteWindowsFirewallRule(state.ruleName) - } - } -}) diff --git a/app/assets/js/preloader.js b/app/assets/js/preloader.js deleted file mode 100644 index 4601e40..0000000 --- a/app/assets/js/preloader.js +++ /dev/null @@ -1,80 +0,0 @@ -const {ipcRenderer} = require('electron') -const fs = require('fs-extra') -const os = require('os') -const path = require('path') - -const ConfigManager = require('./configmanager') -const CatalogManager = require('./catalogmanager') -const { DistroAPI } = require('./distromanager') -const LangLoader = require('./langloader') -const { LoggerUtil } = require('helios-core') -// eslint-disable-next-line no-unused-vars -const { HeliosDistribution } = require('helios-core/common') - -const logger = LoggerUtil.getLogger('Preloader') - -logger.info('Loading..') - -// Load ConfigManager -ConfigManager.load() - -// Yuck! -// TODO Fix this -DistroAPI['commonDir'] = ConfigManager.getCommonDirectory() -DistroAPI['instanceDir'] = ConfigManager.getInstanceDirectory() - -// Load Strings -LangLoader.setupLanguage() - -/** - * - * @param {HeliosDistribution} data - */ -function onDistroLoad(data){ - if(data != null){ - - // Resolve the selected server if its value has yet to be set. - if(ConfigManager.getSelectedServer() == null || data.getServerById(ConfigManager.getSelectedServer()) == null){ - logger.info('Determining default selected server..') - ConfigManager.setSelectedServer(data.getMainServer().rawServer.id) - ConfigManager.save() - } - } - ipcRenderer.send('distributionIndexDone', data != null) -} - -async function bootstrapDistribution(){ - try { - await CatalogManager.getInstalledProfiles() - } catch (err) { - logger.warn('Unable to refresh installed profiles from catalog at startup.', err) - } - - CatalogManager.applyConfiguredProfile() - - // Ensure Distribution is downloaded and cached. - DistroAPI.getDistribution() - .then(heliosDistro => { - logger.info('Loaded distribution index.') - - onDistroLoad(heliosDistro) - }) - .catch(err => { - logger.info('Failed to load an older version of the distribution index.') - logger.info('Application cannot run.') - logger.error(err) - - onDistroLoad(null) - }) -} - -bootstrapDistribution() - -// Clean up temp dir incase previous launches ended unexpectedly. -fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => { - if(err){ - logger.warn('Error while cleaning natives directory', err) - } else { - logger.info('Cleaned natives directory.') - } -}) diff --git a/app/assets/js/processbuilder.js b/app/assets/js/processbuilder.js deleted file mode 100644 index 4377d9a..0000000 --- a/app/assets/js/processbuilder.js +++ /dev/null @@ -1,982 +0,0 @@ -const AdmZip = require('adm-zip') -const child_process = require('child_process') -const crypto = require('crypto') -const fs = require('fs-extra') -const { LoggerUtil } = require('helios-core') -const { getMojangOS, isLibraryCompatible, mcVersionAtLeast } = require('helios-core/common') -const { Type } = require('helios-distribution-types') -const os = require('os') -const path = require('path') - -const ConfigManager = require('./configmanager') -const ServerRuntime = require('./serverruntime') - -const logger = LoggerUtil.getLogger('ProcessBuilder') - -function resolveServerAddressOverride(address, fallbackPort) { - if(address == null || address.trim().length === 0){ - return null - } - - const trimmed = address.trim() - - if(trimmed.startsWith('[')){ - const closingIndex = trimmed.indexOf(']') - if(closingIndex !== -1){ - const host = trimmed.substring(1, closingIndex) - const remainder = trimmed.substring(closingIndex + 1) - if(remainder.startsWith(':')){ - return { - hostname: host, - port: remainder.substring(1) || fallbackPort - } - } - return { - hostname: host, - port: fallbackPort - } - } - } - - const splitIndex = trimmed.lastIndexOf(':') - if(splitIndex > -1 && trimmed.indexOf(':') === splitIndex){ - return { - hostname: trimmed.substring(0, splitIndex), - port: trimmed.substring(splitIndex + 1) || fallbackPort - } - } - - return { - hostname: trimmed, - port: fallbackPort - } -} - - -/** - * Only forge and fabric are top level mod loaders. - * - * Forge 1.13+ launch logic is similar to fabrics, for now using usingFabricLoader flag to - * change minor details when needed. - * - * Rewrite of this module may be needed in the future. - */ -class ProcessBuilder { - - constructor(distroServer, vanillaManifest, modManifest, authUser, launcherVersion){ - this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.rawServer.id) - this.commonDir = ConfigManager.getCommonDirectory() - this.server = distroServer - this.vanillaManifest = vanillaManifest - this.usingModLoader = modManifest != null - this.modManifest = modManifest ?? { - id: vanillaManifest.id, - mainClass: vanillaManifest.mainClass, - arguments: { - jvm: [], - game: [] - }, - minecraftArguments: vanillaManifest.minecraftArguments ?? '' - } - this.authUser = authUser - this.launcherVersion = launcherVersion - this.forgeModListFile = path.join(this.gameDir, 'forgeMods.list') // 1.13+ - this.fmlDir = path.join(this.gameDir, 'forgeModList.json') - this.llDir = path.join(this.gameDir, 'liteloaderModList.json') - this.libPath = path.join(this.commonDir, 'libraries') - - this.usingLiteLoader = false - this.usingFabricLoader = false - this.llPath = null - } - - /** - * Convienence method to run the functions typically used to build a process. - */ - build(){ - fs.ensureDirSync(this.gameDir) - const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex')) - process.throwDeprecation = true - this.setupLiteLoader() - logger.info('Using liteloader:', this.usingLiteLoader) - this.usingFabricLoader = this.usingModLoader && this.server.modules.some(mdl => mdl.rawModule.type === Type.Fabric) - logger.info('Using fabric loader:', this.usingFabricLoader) - const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.rawServer.id).mods, this.server.modules) - - // Mod list below 1.13 - // Fabric only supports 1.14+ - if(this.usingModLoader && !mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ - this.constructJSONModList('forge', modObj.fMods, true) - if(this.usingLiteLoader){ - this.constructJSONModList('liteloader', modObj.lMods, true) - } - } - - const uberModArr = modObj.fMods.concat(modObj.lMods) - let args = this.constructJVMArguments(uberModArr, tempNativePath) - - if(this.usingModLoader && mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ - //args = args.concat(this.constructModArguments(modObj.fMods)) - args = args.concat(this.constructModList(modObj.fMods)) - } - - // Hide access token - const loggableArgs = [...args] - loggableArgs[loggableArgs.findIndex(x => x === this.authUser.accessToken)] = '**********' - - logger.info('Launch Arguments:', loggableArgs) - - const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, { - cwd: this.gameDir, - detached: ConfigManager.getLaunchDetached() - }) - - if(ConfigManager.getLaunchDetached()){ - child.unref() - } - - child.stdout.setEncoding('utf8') - child.stderr.setEncoding('utf8') - - child.stdout.on('data', (data) => { - data.trim().split('\n').forEach(x => console.log(`\x1b[32m[Minecraft]\x1b[0m ${x}`)) - - }) - child.stderr.on('data', (data) => { - data.trim().split('\n').forEach(x => console.log(`\x1b[31m[Minecraft]\x1b[0m ${x}`)) - }) - child.on('close', (code) => { - logger.info('Exited with code', code) - fs.remove(tempNativePath, (err) => { - if(err){ - logger.warn('Error while deleting temp dir', err) - } else { - logger.info('Temp dir deleted successfully.') - } - }) - }) - - return child - } - - /** - * Get the platform specific classpath separator. On windows, this is a semicolon. - * On Unix, this is a colon. - * - * @returns {string} The classpath separator for the current operating system. - */ - static getClasspathSeparator() { - return process.platform === 'win32' ? ';' : ':' - } - - /** - * Determine if an optional mod is enabled from its configuration value. If the - * configuration value is null, the required object will be used to - * determine if it is enabled. - * - * A mod is enabled if: - * * The configuration is not null and one of the following: - * * The configuration is a boolean and true. - * * The configuration is an object and its 'value' property is true. - * * The configuration is null and one of the following: - * * The required object is null. - * * The required object's 'def' property is null or true. - * - * @param {Object | boolean} modCfg The mod configuration object. - * @param {Object} required Optional. The required object from the mod's distro declaration. - * @returns {boolean} True if the mod is enabled, false otherwise. - */ - static isModEnabled(modCfg, required = null){ - return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && (typeof modCfg.value !== 'undefined' ? modCfg.value : true))) : required != null ? required.def : true - } - - /** - * Function which performs a preliminary scan of the top level - * mods. If liteloader is present here, we setup the special liteloader - * launch options. Note that liteloader is only allowed as a top level - * mod. It must not be declared as a submodule. - */ - setupLiteLoader(){ - for(let ll of this.server.modules){ - if(ll.rawModule.type === Type.LiteLoader){ - if(!ll.getRequired().value){ - const modCfg = ConfigManager.getModConfiguration(this.server.rawServer.id).mods - if(ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessMavenIdentifier()], ll.getRequired())){ - if(fs.existsSync(ll.getPath())){ - this.usingLiteLoader = true - this.llPath = ll.getPath() - } - } - } else { - if(fs.existsSync(ll.getPath())){ - this.usingLiteLoader = true - this.llPath = ll.getPath() - } - } - } - } - } - - /** - * Resolve an array of all enabled mods. These mods will be constructed into - * a mod list format and enabled at launch. - * - * @param {Object} modCfg The mod configuration object. - * @param {Array.} mdls An array of modules to parse. - * @returns {{fMods: Array., lMods: Array.}} An object which contains - * a list of enabled forge mods and litemods. - */ - resolveModConfiguration(modCfg, mdls){ - let fMods = [] - let lMods = [] - - for(let mdl of mdls){ - const type = mdl.rawModule.type - if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ - const o = !mdl.getRequired().value - const e = ProcessBuilder.isModEnabled(modCfg[mdl.getVersionlessMavenIdentifier()], mdl.getRequired()) - if(!o || (o && e)){ - if(mdl.subModules.length > 0){ - const v = this.resolveModConfiguration(modCfg[mdl.getVersionlessMavenIdentifier()].mods, mdl.subModules) - fMods = fMods.concat(v.fMods) - lMods = lMods.concat(v.lMods) - if(type === Type.LiteLoader){ - continue - } - } - if(type === Type.ForgeMod || type === Type.FabricMod){ - fMods.push(mdl) - } else { - lMods.push(mdl) - } - } - } - } - - return { - fMods, - lMods - } - } - - _lteMinorVersion(version) { - return Number(this.modManifest.id.split('-')[0].split('.')[1]) <= Number(version) - } - - /** - * Test to see if this version of forge requires the absolute: prefix - * on the modListFile repository field. - */ - _requiresAbsolute(){ - try { - if(this._lteMinorVersion(9)) { - return false - } - const ver = this.modManifest.id.split('-')[2] - const pts = ver.split('.') - const min = [14, 23, 3, 2655] - for(let i=0; i min[i]){ - return true - } - } - } catch (_err) { - // We know old forge versions follow this format. - // Error must be caused by newer version. - } - - // Equal or errored - return true - } - - /** - * Construct a mod list json object. - * - * @param {'forge' | 'liteloader'} type The mod list type to construct. - * @param {Array.} mods An array of mods to add to the mod list. - * @param {boolean} save Optional. Whether or not we should save the mod list file. - */ - constructJSONModList(type, mods, save = false){ - const modList = { - repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore') - } - - const ids = [] - if(type === 'forge'){ - for(let mod of mods){ - ids.push(mod.getExtensionlessMavenIdentifier()) - } - } else { - for(let mod of mods){ - ids.push(mod.getMavenIdentifier()) - } - } - modList.modRef = ids - - if(save){ - const json = JSON.stringify(modList, null, 4) - fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8') - } - - return modList - } - - // /** - // * Construct the mod argument list for forge 1.13 - // * - // * @param {Array.} mods An array of mods to add to the mod list. - // */ - // constructModArguments(mods){ - // const argStr = mods.map(mod => { - // return mod.getExtensionlessMavenIdentifier() - // }).join(',') - - // if(argStr){ - // return [ - // '--fml.mavenRoots', - // path.join('..', '..', 'common', 'modstore'), - // '--fml.mods', - // argStr - // ] - // } else { - // return [] - // } - - // } - - /** - * Construct the mod argument list for forge 1.13 and Fabric - * - * @param {Array.} mods An array of mods to add to the mod list. - */ - constructModList(mods) { - const writeBuffer = mods.map(mod => { - return this.usingFabricLoader ? mod.getPath() : mod.getExtensionlessMavenIdentifier() - }).join('\n') - - if(writeBuffer) { - fs.writeFileSync(this.forgeModListFile, writeBuffer, 'UTF-8') - return this.usingFabricLoader ? [ - '--fabric.addMods', - `@${this.forgeModListFile}` - ] : [ - '--fml.mavenRoots', - path.join('..', '..', 'common', 'modstore'), - '--fml.modLists', - this.forgeModListFile - ] - } else { - return [] - } - - } - - _processAutoConnectArg(args){ - const selectedProfileId = ConfigManager.getSelectedLibraryProfile() - const selectedProfile = selectedProfileId != null - ? ConfigManager.getInstalledLibraryProfile(selectedProfileId) - : null - const quickPlayWorld = selectedProfileId != null - ? ConfigManager.getLibraryQuickPlayWorld(selectedProfileId) - : null - - if(quickPlayWorld && selectedProfile?.serverEnabled !== true){ - if(mcVersionAtLeast('1.20', this.server.rawServer.minecraftVersion)){ - args.push('--quickPlaySingleplayer') - args.push(quickPlayWorld) - } - return - } - - if(ConfigManager.getAutoConnect() && this.server.rawServer.autoconnect){ - let serverAddressOverride = selectedProfileId != null - ? resolveServerAddressOverride( - ConfigManager.getLibraryServerAddressOverride(selectedProfileId), - this.server.port - ) - : null - - if(serverAddressOverride == null && selectedProfile?.serverEnabled === true){ - const hostState = ServerRuntime.getHostedProfileState(selectedProfileId) - if(hostState.running){ - serverAddressOverride = { - hostname: '127.0.0.1', - port: selectedProfile.serverPort ?? this.server.port - } - } - } - - const hostname = serverAddressOverride?.hostname ?? this.server.hostname - const port = serverAddressOverride?.port ?? (selectedProfile?.serverEnabled === true ? (selectedProfile.serverPort ?? this.server.port) : this.server.port) - if(mcVersionAtLeast('1.20', this.server.rawServer.minecraftVersion)){ - args.push('--quickPlayMultiplayer') - args.push(`${hostname}:${port}`) - } else { - args.push('--server') - args.push(hostname) - args.push('--port') - args.push(port) - } - } - } - - /** - * Construct the argument array that will be passed to the JVM process. - * - * @param {Array.} mods An array of enabled mods which will be launched with this process. - * @param {string} tempNativePath The path to store the native libraries. - * @returns {Array.} An array containing the full JVM arguments for this process. - */ - constructJVMArguments(mods, tempNativePath){ - if(mcVersionAtLeast('1.13', this.server.rawServer.minecraftVersion)){ - return this._constructJVMArguments113(mods, tempNativePath) - } else { - return this._constructJVMArguments112(mods, tempNativePath) - } - } - - /** - * Construct the argument array that will be passed to the JVM process. - * This function is for 1.12 and below. - * - * @param {Array.} mods An array of enabled mods which will be launched with this process. - * @param {string} tempNativePath The path to store the native libraries. - * @returns {Array.} An array containing the full JVM arguments for this process. - */ - _constructJVMArguments112(mods, tempNativePath){ - - let args = [] - - // Classpath Argument - args.push('-cp') - args.push(this.classpathArg(mods, tempNativePath).join(ProcessBuilder.getClasspathSeparator())) - - // Java Arguments - if(process.platform === 'darwin'){ - args.push('-Xdock:name=MRSLauncher') - args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) - } - args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id)) - args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id)) - args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id)) - args.push('-Djava.library.path=' + tempNativePath) - - // Main Java Class - args.push(this.modManifest.mainClass) - - // Forge Arguments - args = args.concat(this._resolveForgeArgs()) - - return args - } - - /** - * Construct the argument array that will be passed to the JVM process. - * This function is for 1.13+ - * - * Note: Required Libs https://github.com/MinecraftForge/MinecraftForge/blob/af98088d04186452cb364280340124dfd4766a5c/src/fmllauncher/java/net/minecraftforge/fml/loading/LibraryFinder.java#L82 - * - * @param {Array.} mods An array of enabled mods which will be launched with this process. - * @param {string} tempNativePath The path to store the native libraries. - * @returns {Array.} An array containing the full JVM arguments for this process. - */ - _constructJVMArguments113(mods, tempNativePath){ - - const argDiscovery = /\${*(.*)}/ - - // JVM Arguments First - let args = this.vanillaManifest.arguments.jvm - - // Debug securejarhandler - // args.push('-Dbsl.debug=true') - - if(this.usingModLoader && this.modManifest.arguments.jvm != null) { - for(const argStr of this.modManifest.arguments.jvm) { - args.push(argStr - .replaceAll('${library_directory}', this.libPath) - .replaceAll('${classpath_separator}', ProcessBuilder.getClasspathSeparator()) - .replaceAll('${version_name}', this.modManifest.id) - ) - } - } - - //args.push('-Dlog4j.configurationFile=D:\\WesterosCraft\\game\\common\\assets\\log_configs\\client-1.12.xml') - - // Java Arguments - if(process.platform === 'darwin'){ - args.push('-Xdock:name=MRSLauncher') - args.push('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns')) - } - args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.rawServer.id)) - args.push('-Xms' + ConfigManager.getMinRAM(this.server.rawServer.id)) - args = args.concat(ConfigManager.getJVMOptions(this.server.rawServer.id)) - - // Main Java Class - args.push(this.modManifest.mainClass) - - // Vanilla Arguments - args = args.concat(this.vanillaManifest.arguments.game) - - for(let i=0; i { - return arg != null - }) - - return args - } - - /** - * Resolve the arguments required by forge. - * - * @returns {Array.} An array containing the arguments required by forge. - */ - _resolveForgeArgs(){ - const baseMinecraftArguments = this.usingModLoader - ? this.modManifest.minecraftArguments - : (this.vanillaManifest.minecraftArguments ?? '') - const mcArgs = baseMinecraftArguments.split(' ') - const argDiscovery = /\${*(.*)}/ - - // Replace the declared variables with their proper values. - for(let i=0; i} list Array of classpath entries. - */ - _processClassPathList(list) { - - const ext = '.jar' - const extLen = ext.length - for(let i=0; i -1 && extIndex !== list[i].length - extLen) { - list[i] = list[i].substring(0, extIndex + extLen) - } - } - - } - - /** - * Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared - * libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries, - * this method requires all enabled mods as an input - * - * @param {Array.} mods An array of enabled mods which will be launched with this process. - * @param {string} tempNativePath The path to store the native libraries. - * @returns {Array.} An array containing the paths of each library required by this process. - */ - classpathArg(mods, tempNativePath){ - let cpArgs = [] - - if(!mcVersionAtLeast('1.17', this.server.rawServer.minecraftVersion) || this.usingFabricLoader || !this.usingModLoader) { - // Add the version.jar to the classpath. - // Must not be added to the classpath for Forge 1.17+. - const version = this.vanillaManifest.id - cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar')) - } - - - if(this.usingLiteLoader){ - cpArgs.push(this.llPath) - } - - // Resolve the Mojang declared libraries. - const mojangLibs = this._resolveMojangLibraries(tempNativePath) - - // Resolve the server declared libraries. - const servLibs = this._resolveServerLibraries(mods) - - // Merge libraries, server libs with the same - // maven identifier will override the mojang ones. - // Ex. 1.7.10 forge overrides mojang's guava with newer version. - const finalLibs = {...mojangLibs, ...servLibs} - cpArgs = cpArgs.concat(Object.values(finalLibs)) - - this._processClassPathList(cpArgs) - - return cpArgs - } - - /** - * Resolve the libraries defined by Mojang's version data. This method will also extract - * native libraries and point to the correct location for its classpath. - * - * TODO - clean up function - * - * @param {string} tempNativePath The path to store the native libraries. - * @returns {{[id: string]: string}} An object containing the paths of each library mojang declares. - */ - _resolveMojangLibraries(tempNativePath){ - const nativesRegex = /.+:natives-([^-]+)(?:-(.+))?/ - const libs = {} - - const libArr = this.vanillaManifest.libraries - fs.ensureDirSync(tempNativePath) - for(let i=0; i -1){ - shouldExclude = true - } - }) - - // Extract the file. - if(!shouldExclude){ - fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => { - if(err){ - logger.error('Error while extracting native library:', err) - } - }) - } - - } - } - // 1.19+ logic - else if(lib.name.includes('natives-')) { - - const regexTest = nativesRegex.exec(lib.name) - // const os = regexTest[1] - const arch = regexTest[2] ?? 'x64' - - if(arch != process.arch) { - continue - } - - // Extract the native library. - const exclusionArr = lib.extract != null ? lib.extract.exclude : ['META-INF/', '.git', '.sha1'] - const artifact = lib.downloads.artifact - - // Location of native zip. - const to = path.join(this.libPath, artifact.path) - - let zip = new AdmZip(to) - let zipEntries = zip.getEntries() - - // Unzip the native zip. - for(let i=0; i -1){ - shouldExclude = true - } - }) - - const extractName = fileName.includes('/') ? fileName.substring(fileName.lastIndexOf('/')) : fileName - - // Extract the file. - if(!shouldExclude){ - fs.writeFile(path.join(tempNativePath, extractName), zipEntries[i].getData(), (err) => { - if(err){ - logger.error('Error while extracting native library:', err) - } - }) - } - - } - } - // No natives - else { - const dlInfo = lib.downloads - const artifact = dlInfo.artifact - const to = path.join(this.libPath, artifact.path) - const versionIndependentId = lib.name.substring(0, lib.name.lastIndexOf(':')) - libs[versionIndependentId] = to - } - } - } - - return libs - } - - /** - * Resolve the libraries declared by this server in order to add them to the classpath. - * This method will also check each enabled mod for libraries, as mods are permitted to - * declare libraries. - * - * @param {Array.} mods An array of enabled mods which will be launched with this process. - * @returns {{[id: string]: string}} An object containing the paths of each library this server requires. - */ - _resolveServerLibraries(mods){ - const mdls = this.server.modules - let libs = {} - - // Locate Forge/Fabric/Libraries - for(let mdl of mdls){ - const type = mdl.rawModule.type - if(type === Type.ForgeHosted || type === Type.Fabric || type === Type.Library){ - libs[mdl.getVersionlessMavenIdentifier()] = mdl.getPath() - if(mdl.subModules.length > 0){ - const res = this._resolveModuleLibraries(mdl) - libs = {...libs, ...res} - } - } - } - - //Check for any libraries in our mod list. - for(let i=0; i 0){ - return {} - } - let libs = {} - for(let sm of mdl.subModules){ - if(sm.rawModule.type === Type.Library){ - - if(sm.rawModule.classpath ?? true) { - libs[sm.getVersionlessMavenIdentifier()] = sm.getPath() - } - } - // If this module has submodules, we need to resolve the libraries for those. - // To avoid unnecessary recursive calls, base case is checked here. - if(mdl.subModules.length > 0){ - const res = this._resolveModuleLibraries(sm) - libs = {...libs, ...res} - } - } - return libs - } - -} - -module.exports = ProcessBuilder diff --git a/app/assets/js/profileassetmanager.js b/app/assets/js/profileassetmanager.js deleted file mode 100644 index 37d7a91..0000000 --- a/app/assets/js/profileassetmanager.js +++ /dev/null @@ -1,188 +0,0 @@ -const AdmZip = require('adm-zip') -const fs = require('fs-extra') -const got = require('got') -const os = require('os') -const path = require('path') -const { pipeline } = require('stream/promises') - -const ConfigManager = require('./configmanager') - -function getProfileBaseDirectory(profileId){ - return path.join(ConfigManager.getDataDirectory(), 'profiles', profileId) -} - -function getProfileDownloadDirectory(profileId){ - return path.join(getProfileBaseDirectory(profileId), 'downloads') -} - -function getProfileCacheFile(profileId, fileName){ - return path.join(getProfileDownloadDirectory(profileId), fileName) -} - -function getServerBundleDirectory(profile){ - return path.join(getProfileBaseDirectory(profile.id), profile.serverDirectoryName || 'server') -} - -function isRemoteSource(source){ - return /^https?:\/\//i.test(source) -} - -function resolveLocalSource(source){ - if(source == null){ - return null - } - - if(source.startsWith('file://')){ - return decodeURIComponent(source.substring('file://'.length)) - } - - return path.resolve(source) -} - -async function downloadSourceToFile(source, destination){ - await fs.ensureDir(path.dirname(destination)) - - if(isRemoteSource(source)){ - await pipeline( - got.stream(source), - fs.createWriteStream(destination) - ) - return destination - } - - const localSource = resolveLocalSource(source) - const localStat = await fs.stat(localSource) - if(localStat.isDirectory()){ - throw new Error(`Directory source is not supported for file cache: ${localSource}`) - } - await fs.copy(localSource, destination, { overwrite: true }) - return destination -} - -async function extractZipToDirectory(zipPath, destination){ - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'launcher-assets-')) - try { - await fs.ensureDir(tempDir) - new AdmZip(zipPath).extractAllTo(tempDir, true) - - const rootEntries = (await fs.readdir(tempDir)).filter((entry) => entry !== '__MACOSX') - await fs.remove(destination) - - if(rootEntries.length === 1){ - const onlyEntry = path.join(tempDir, rootEntries[0]) - const entryStat = await fs.stat(onlyEntry) - if(entryStat.isDirectory()){ - await fs.copy(onlyEntry, destination, { overwrite: true }) - return - } - } - - await fs.copy(tempDir, destination, { overwrite: true }) - } finally { - await fs.remove(tempDir) - } -} - -async function ensureCachedFile(profileId, source, fileName){ - const cachePath = getProfileCacheFile(profileId, fileName) - await downloadSourceToFile(source, cachePath) - return cachePath -} - -async function ensureServerWorldInstalled(profile, serverDirectory){ - if(!profile.worldArchiveUrl){ - return null - } - - const targetWorldDirectory = path.join(serverDirectory, 'world') - if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){ - const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') - await extractZipToDirectory(cachePath, targetWorldDirectory) - } else { - await fs.remove(targetWorldDirectory) - await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true }) - } - - return targetWorldDirectory -} - -exports.getProfileBaseDirectory = getProfileBaseDirectory -exports.getServerBundleDirectory = getServerBundleDirectory - -exports.prefetchProfileAssets = async function(profile){ - if(profile.worldArchiveUrl){ - await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') - } - if(profile.serverJarUrl){ - await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar') - } - - const currentState = ConfigManager.getLibraryProfileAssetState(profile.id) - ConfigManager.setLibraryProfileAssetState(profile.id, { - ...currentState, - prefetchedAt: new Date().toISOString() - }) - ConfigManager.save() -} - -exports.ensureWorldInstalled = async function(profile, serverId){ - if(!profile.worldArchiveUrl || !profile.worldDirectoryName){ - return null - } - - const savesDirectory = path.join(ConfigManager.getInstanceDirectory(), serverId, 'saves') - const targetWorldDirectory = path.join(savesDirectory, profile.worldDirectoryName) - - await fs.ensureDir(savesDirectory) - if(isRemoteSource(profile.worldArchiveUrl) || profile.worldArchiveUrl.endsWith('.zip') || profile.worldArchiveUrl.startsWith('file://')){ - const cachePath = await ensureCachedFile(profile.id, profile.worldArchiveUrl, 'world.zip') - await extractZipToDirectory(cachePath, targetWorldDirectory) - } else { - await fs.remove(targetWorldDirectory) - await fs.copy(resolveLocalSource(profile.worldArchiveUrl), targetWorldDirectory, { overwrite: true }) - } - - ConfigManager.setLibraryQuickPlayWorld(profile.id, profile.worldDirectoryName) - ConfigManager.setLibraryProfileAssetState(profile.id, { - worldInstalledAt: new Date().toISOString(), - worldInstalledServerId: serverId, - worldDirectoryName: profile.worldDirectoryName - }) - ConfigManager.save() - - return targetWorldDirectory -} - -exports.ensureServerJarInstalled = async function(profile){ - if(!profile.serverJarUrl){ - return null - } - - const targetDirectory = getServerBundleDirectory(profile) - const targetJarPath = path.join(targetDirectory, 'server.jar') - await fs.ensureDir(targetDirectory) - - const cachePath = await ensureCachedFile(profile.id, profile.serverJarUrl, 'server.jar') - await fs.copy(cachePath, targetJarPath, { overwrite: true }) - await ensureServerWorldInstalled(profile, targetDirectory) - - ConfigManager.setLibraryProfileAssetState(profile.id, { - serverInstalledAt: new Date().toISOString(), - serverDirectory: targetDirectory, - serverJarPath: targetJarPath - }) - ConfigManager.save() - - return { - serverDirectory: targetDirectory, - serverJarPath: targetJarPath - } -} - -exports.ensureServerBundleInstalled = exports.ensureServerJarInstalled - -exports.prepareProfileForLaunch = async function(profile, serverId){ - return exports.ensureWorldInstalled(profile, serverId) -} - -exports.ensureServerWorldInstalled = ensureServerWorldInstalled diff --git a/app/assets/js/scripts/install.js b/app/assets/js/scripts/install.js deleted file mode 100644 index 638708d..0000000 --- a/app/assets/js/scripts/install.js +++ /dev/null @@ -1,308 +0,0 @@ -(() => { -const CatalogManager = require('./assets/js/catalogmanager') -const ConfigManager = require('./assets/js/configmanager') -const ProfileAssetManager = require('./assets/js/profileassetmanager') - -const installCatalogList = document.getElementById('installCatalogList') -const installPageShell = document.querySelector('#installContainer .launcherPageShell') - -let expandedProfileId = null - -function createInstallBadge(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 createInfoLine(label, value){ - const line = document.createElement('div') - line.className = 'launcherInfoLine' - - const labelElement = document.createElement('span') - labelElement.className = 'launcherInfoLabel' - labelElement.textContent = label - - const valueElement = document.createElement('span') - valueElement.className = 'launcherInfoValue' - valueElement.textContent = value - - line.appendChild(labelElement) - line.appendChild(valueElement) - return line -} - -function showInstallMessage(title, message){ - if(typeof setOverlayContent === 'function'){ - setOverlayContent(title, message, '확인') - setOverlayHandler(() => toggleOverlay(false)) - toggleOverlay(true) - } -} - -function buildDetailText(profile){ - if(typeof profile.details === 'string' && profile.details.trim().length > 0){ - return profile.details.trim() - } - - if(profile.serverEnabled){ - return '이 프로필은 맵을 기본으로 두고 서버 기능까지 함께 사용하는 항목입니다. 주소를 직접 넣으면 해당 서버로 접속하고, 주소를 비워두면 로컬 서버 실행 흐름을 사용할 수 있습니다.' - } - - if(profile.modsEnabled){ - return '이 프로필은 맵 기반 클라이언트에 모드 구성을 포함한 항목입니다. 관리자가 distribution과 월드 자료를 미리 등록해두고, 사용자는 라이브러리에 추가한 뒤 바로 실행합니다.' - } - - return '이 프로필은 맵 기반 기본 항목입니다. 관리자가 distribution과 월드 자료를 미리 등록해두고, 사용자는 라이브러리에 추가한 뒤 바로 실행합니다.' -} - -function toggleExpandedProfile(profileId){ - expandedProfileId = expandedProfileId === profileId ? null : profileId -} - -function isInstallable(profile){ - return profile.launchReady -} - -async function installProfile(profile){ - const installedProfile = await CatalogManager.installProfile(profile.id) - await ProfileAssetManager.prefetchProfileAssets(installedProfile) - if(installedProfile.serverEnabled && installedProfile.hostReady){ - await ProfileAssetManager.ensureServerJarInstalled(installedProfile) - } - - if(typeof refreshSelectedProfileButton === 'function'){ - refreshSelectedProfileButton() - } - if(typeof refreshServerStatus === 'function'){ - refreshServerStatus(true) - } - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } -} - -function createExpandedDetail(profile, installed){ - const detailSection = document.createElement('div') - detailSection.className = 'launcherExpandableDetail' - detailSection.addEventListener('click', (event) => { - event.stopPropagation() - }) - - const badgeRow = document.createElement('div') - badgeRow.className = 'launcherExpandableMeta' - describeProfileFeatures(profile).forEach((label) => { - badgeRow.appendChild(createInstallBadge(label)) - }) - if(installed){ - badgeRow.appendChild(createInstallBadge('설치됨')) - } - if(profile.serverEnabled && profile.hostReady){ - badgeRow.appendChild(createInstallBadge('로컬 서버 가능')) - } - if(!profile.launchReady){ - badgeRow.appendChild(createInstallBadge('설정 필요')) - } - - const infoBlock = document.createElement('div') - infoBlock.className = 'launcherInfoBlock' - infoBlock.appendChild(createInfoLine('프로필 ID', profile.id)) - infoBlock.appendChild(createInfoLine('구성', describeProfileFeatures(profile).join(' + '))) - infoBlock.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요')) - infoBlock.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName || '미설정')) - - if(profile.serverEnabled){ - infoBlock.appendChild(createInfoLine('서버 포트', String(profile.serverPort ?? 25565))) - infoBlock.appendChild(createInfoLine('서버 메모리', `${profile.serverMemoryMb ?? 4096}MB`)) - infoBlock.appendChild(createInfoLine('최대 인원수', String(profile.serverMaxPlayers ?? 20))) - infoBlock.appendChild(createInfoLine('화이트리스트', profile.serverWhitelistEnabled ? '사용' : '미사용')) - infoBlock.appendChild(createInfoLine('로컬 서버 준비', profile.hostReady ? '완료' : '버킷 JAR 필요')) - } - - if(profile.launchIssues.length > 0){ - infoBlock.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / '))) - } else if(profile.hostIssues.length > 0){ - infoBlock.appendChild(createInfoLine('서버 참고', profile.hostIssues.join(' / '))) - } - - const bodyGroup = document.createElement('div') - bodyGroup.className = 'launcherFieldGroup' - - const bodyLabel = document.createElement('label') - bodyLabel.className = 'launcherFieldLabel' - bodyLabel.textContent = '자세한 내용' - - const body = document.createElement('div') - body.className = 'launcherDetailBody' - body.textContent = buildDetailText(profile) - - bodyGroup.appendChild(bodyLabel) - bodyGroup.appendChild(body) - - const actions = document.createElement('div') - actions.className = 'launcherCardActions' - - const installButton = document.createElement('button') - installButton.className = 'launcherPrimaryButton' - installButton.textContent = installed ? '설치됨' : '라이브러리에 추가' - installButton.disabled = installed || !isInstallable(profile) - installButton.addEventListener('click', async (event) => { - event.stopPropagation() - try { - await installProfile(profile) - await renderInstallView() - showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`) - } catch (error) { - console.error(error) - const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.' - showInstallMessage('설치 실패', message) - } - }) - - const openLibraryButton = document.createElement('button') - openLibraryButton.className = 'launcherSecondaryButton' - openLibraryButton.textContent = '라이브러리 열기' - openLibraryButton.addEventListener('click', async (event) => { - event.stopPropagation() - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } - switchView(getCurrentView(), VIEWS.library) - }) - - actions.appendChild(installButton) - actions.appendChild(openLibraryButton) - - detailSection.appendChild(badgeRow) - detailSection.appendChild(infoBlock) - detailSection.appendChild(bodyGroup) - detailSection.appendChild(actions) - - return detailSection -} - -async function renderInstallView(){ - const previousScrollTop = installPageShell != null ? installPageShell.scrollTop : 0 - installCatalogList.innerHTML = '' - - try { - const catalog = await CatalogManager.loadCatalog() - const installedIds = new Set( - ConfigManager.getInstalledLibraryProfiles().map((profile) => profile.id) - ) - - if(expandedProfileId != null && !catalog.profiles.some((profile) => profile.id === expandedProfileId)){ - expandedProfileId = null - } - - if(catalog.sourceError != null){ - const warningCard = document.createElement('article') - warningCard.className = 'launcherCard' - warningCard.innerHTML = '

카탈로그 로드 경고

관리자가 등록한 카탈로그를 읽지 못했습니다. 현재 보이는 목록이 없다면 관리자 사이트에서 카탈로그 파일과 배포 경로를 다시 확인하세요.

' - installCatalogList.appendChild(warningCard) - } - - for(const profile of catalog.profiles){ - const installed = installedIds.has(profile.id) - const expanded = expandedProfileId === profile.id - - const row = document.createElement('article') - row.className = 'launcherListItem' - if(expanded){ - row.setAttribute('selected', 'true') - } - - const top = document.createElement('div') - top.className = 'launcherListItemTop' - - const main = document.createElement('div') - main.className = 'launcherListItemMain' - - const titleRow = document.createElement('div') - titleRow.className = 'launcherListTitleRow' - - const textGroup = document.createElement('div') - textGroup.className = 'launcherListTextGroup' - - const title = document.createElement('h3') - title.className = 'launcherListTitle' - title.textContent = profile.name - - const meta = document.createElement('div') - meta.className = 'launcherListMeta' - describeProfileFeatures(profile).forEach((label) => { - meta.appendChild(createInstallBadge(label)) - }) - if(installed){ - meta.appendChild(createInstallBadge('설치됨')) - } - if(profile.serverEnabled && profile.hostReady){ - meta.appendChild(createInstallBadge('로컬 서버 가능')) - } - - const description = document.createElement('p') - description.className = 'launcherListDescription' - description.textContent = profile.description || '설명이 없습니다.' - - const actions = document.createElement('div') - actions.className = 'launcherListActions' - - const detailButton = document.createElement('button') - detailButton.className = 'launcherSecondaryButton' - detailButton.textContent = expanded ? '간단히 보기' : '자세히 보기' - detailButton.addEventListener('click', async (event) => { - event.stopPropagation() - toggleExpandedProfile(profile.id) - await renderInstallView() - }) - - actions.appendChild(detailButton) - textGroup.appendChild(title) - textGroup.appendChild(meta) - titleRow.appendChild(textGroup) - top.appendChild(main) - top.appendChild(actions) - main.appendChild(titleRow) - main.appendChild(description) - row.appendChild(top) - - if(expanded){ - row.appendChild(createExpandedDetail(profile, installed)) - } - - installCatalogList.appendChild(row) - } - } catch (error) { - console.error(error) - const errorCard = document.createElement('article') - errorCard.className = 'launcherCard' - errorCard.innerHTML = '

설치 페이지 로드 실패

프로필 목록을 읽지 못했습니다. 관리자 사이트에서 catalog 설정을 확인하세요.

' - installCatalogList.appendChild(errorCard) - } finally { - if(installPageShell != null){ - installPageShell.scrollTop = previousScrollTop - } - } -} - -document.getElementById('installBackButton').addEventListener('click', () => { - switchView(getCurrentView(), VIEWS.landing) -}) - -window.refreshInstallView = renderInstallView -renderInstallView() -})() diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js deleted file mode 100644 index 4ca5a9c..0000000 --- a/app/assets/js/scripts/landing.js +++ /dev/null @@ -1,1255 +0,0 @@ -/** - * Script for landing.ejs - */ -(() => { -// Requirements -require('./assets/js/vanillapatch') - -const path = require('path') -const { URL } = require('url') -const { - MojangRestAPI -} = require('helios-core/mojang') -const { - RestResponseStatus, - isDisplayableError, - validateLocalFile -} = require('helios-core/common') -const { - FullRepair, - DistributionIndexProcessor, - MojangIndexProcessor, - downloadFile -} = require('helios-core/dl') -const { - validateSelectedJvm, - ensureJavaDirIsRoot, - javaExecFromRoot, - discoverBestJvmInstallation, - latestOpenJDK, - extractJdk -} = require('helios-core/java') - -// Internal Requirements -const AuthManager = require('./assets/js/authmanager') -const CatalogManager = require('./assets/js/catalogmanager') -const DiscordWrapper = require('./assets/js/discordwrapper') -const PortManager = require('./assets/js/portmanager') -const ProcessBuilder = require('./assets/js/processbuilder') -const ServerRuntime = require('./assets/js/serverruntime') - -// Launch Elements -const launch_content = document.getElementById('launch_content') -const launch_details = document.getElementById('launch_details') -const launch_progress = document.getElementById('launch_progress') -const launch_progress_label = document.getElementById('launch_progress_label') -const launch_details_text = document.getElementById('launch_details_text') -const server_selection_button = document.getElementById('server_selection_button') -const accountPreviewName = document.getElementById('accountPreviewName') -const avatarMenuButton = document.getElementById('avatarMenuButton') -const avatarContainer = document.getElementById('avatarContainer') -const accountMenu = document.getElementById('accountMenu') -const accountMenuName = document.getElementById('accountMenuName') -const accountMenuLogoutButton = document.getElementById('accountMenuLogoutButton') -const portStatusTooltip = document.getElementById('portStatusTooltip') - -const loggerLanding = LoggerUtil.getLogger('Landing') - -function setAccountMenuOpen(open){ - if(open && ConfigManager.getSelectedAccount() == null){ - return - } - - avatarMenuButton.setAttribute('aria-expanded', open ? 'true' : 'false') - if(open){ - accountMenu.removeAttribute('hidden') - } else { - accountMenu.setAttribute('hidden', '') - } -} - -function showAccountError(message){ - if(typeof setOverlayContent === 'function'){ - setOverlayContent( - Lang.queryJS('settings.msftLogout.errorTitle'), - message, - Lang.queryJS('landing.launch.okay') - ) - setOverlayHandler(() => toggleOverlay(false)) - toggleOverlay(true) - } -} - -async function logoutSelectedAccount(){ - const selectedAccount = ConfigManager.getSelectedAccount() - if(selectedAccount == null){ - return - } - - const accountCount = Object.keys(ConfigManager.getAuthAccounts()).length - accountMenuLogoutButton.disabled = true - - try { - if(selectedAccount.type === 'microsoft'){ - await AuthManager.removeMicrosoftAccount(selectedAccount.uuid) - } else { - await AuthManager.removeMojangAccount(selectedAccount.uuid) - } - - setAccountMenuOpen(false) - - const nextAccount = ConfigManager.getSelectedAccount() - if(nextAccount != null){ - updateSelectedAccount(nextAccount) - validateSelectedAccount() - } else if(accountCount === 1){ - updateSelectedAccount(null) - loginOptionsCancelEnabled(false) - loginOptionsViewOnLoginSuccess = VIEWS.landing - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - switchView(getCurrentView(), VIEWS.loginOptions) - } - } catch (error) { - console.error(error) - const message = selectedAccount.type === 'microsoft' - ? Lang.queryJS('settings.msftLogout.errorMessage') - : Lang.queryJS('landing.selectedAccount.logoutFailed') - showAccountError(message) - } finally { - accountMenuLogoutButton.disabled = false - } -} - -/* Launch Progress Wrapper Functions */ - -/** - * Show/hide the loading area. - * - * @param {boolean} loading True if the loading area should be shown, otherwise false. - */ -function toggleLaunchArea(loading){ - if(typeof syncLaunchDetailWidths === 'function'){ - syncLaunchDetailWidths() - } - - if(loading){ - launch_details.style.display = 'flex' - launch_content.style.display = 'none' - } else { - launch_details.style.display = 'none' - launch_content.style.display = 'inline-flex' - } - - window.requestAnimationFrame(() => { - if(typeof syncLaunchDetailWidths === 'function'){ - syncLaunchDetailWidths() - } - }) -} - -/** - * Set the details text of the loading area. - * - * @param {string} details The new text for the loading details. - */ -function setLaunchDetails(details){ - launch_details_text.innerHTML = details -} - -/** - * Set the value of the loading progress bar and display that value. - * - * @param {number} percent Percentage (0-100) - */ -function setLaunchPercentage(percent){ - launch_progress.setAttribute('max', 100) - launch_progress.setAttribute('value', percent) - launch_progress_label.innerHTML = percent + '%' -} - -/** - * Set the value of the OS progress bar and display that on the UI. - * - * @param {number} percent Percentage (0-100) - */ -function setDownloadPercentage(percent){ - remote.getCurrentWindow().setProgressBar(percent/100) - setLaunchPercentage(percent) -} - -/** - * Enable or disable the launch button. - * - * @param {boolean} val True to enable, false to disable. - */ -function setLaunchEnabled(val){ - document.getElementById('launch_button').disabled = !val -} - -function refreshSelectedProfileButton(){ - const selectedProfile = CatalogManager.getSelectedProfileSync() - if(selectedProfile == null){ - server_selection_button.innerHTML = '• ' + Lang.queryJS('landing.selectedProfile.noSelection') - setLaunchEnabled(false) - return - } - - const label = selectedProfile.name?.trim().length > 0 - ? selectedProfile.name.trim() - : selectedProfile.id - - server_selection_button.innerHTML = '• ' + label - setLaunchEnabled(selectedProfile.configured !== false) -} - -function isSelectedMapReady(profile){ - if(profile == null || profile.serverEnabled === true){ - return false - } - - const assetState = ConfigManager.getLibraryProfileAssetState(profile.id) - return profile.launchReady && ( - assetState.worldInstalledAt != null || - assetState.prefetchedAt != null || - profile.worldArchiveUrl == null - ) -} - -function updateLandingStatusDisplay(label, value, tone, tooltip, fade = false){ - const applyValues = () => { - document.getElementById('landingPlayerLabel').innerHTML = label - document.getElementById('player_count').innerHTML = value - document.getElementById('player_count').dataset.tone = tone - portStatusTooltip.textContent = tooltip - } - - if(fade){ - $('#server_status_wrapper').fadeOut(250, () => { - applyValues() - $('#server_status_wrapper').fadeIn(500) - }) - } else { - applyValues() - } -} - -// Bind launch button -document.getElementById('launch_button').addEventListener('click', async e => { - loggerLanding.info('Launching game..') - try { - const selectedProfile = CatalogManager.getSelectedProfileSync() - if(selectedProfile == null){ - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } - switchView(getCurrentView(), VIEWS.library) - return - } - - CatalogManager.applyConfiguredProfile() - const distro = await DistroAPI.refreshDistributionOrFallback() - if(distro == null){ - throw new Error('Distribution refresh returned null.') - } - - let server = distro.getServerById(ConfigManager.getSelectedServer()) - if(server == null && typeof distro.getMainServer === 'function'){ - const mainServer = distro.getMainServer() - if(mainServer != null){ - ConfigManager.setSelectedServer(mainServer.rawServer.id) - ConfigManager.save() - server = mainServer - } - } - - onDistroRefresh(distro) - refreshSelectedProfileButton() - - if(server == null){ - throw new Error('No server available for selected profile.') - } - - const jExe = ConfigManager.getJavaExecutable(ConfigManager.getSelectedServer()) - if(jExe == null){ - await asyncSystemScan(server.effectiveJavaOptions) - } else { - - setLaunchDetails(Lang.queryJS('landing.launch.pleaseWait')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const details = await validateSelectedJvm(ensureJavaDirIsRoot(jExe), server.effectiveJavaOptions.supported) - if(details != null){ - loggerLanding.info('Jvm Details', details) - await dlAsync() - - } else { - await asyncSystemScan(server.effectiveJavaOptions) - } - } - } catch(err) { - loggerLanding.error('Unhandled error in during launch process.', err) - showLaunchFailure(Lang.queryJS('landing.launch.failureTitle'), Lang.queryJS('landing.launch.failureText')) - } -}) - -// Bind settings button -document.getElementById('settingsMediaButton').onclick = async e => { - await prepareSettings() - switchView(getCurrentView(), VIEWS.settings) -} - -avatarMenuButton.addEventListener('click', (e) => { - e.stopPropagation() - setAccountMenuOpen(accountMenu.hasAttribute('hidden')) -}) - -accountMenu.addEventListener('click', (e) => { - e.stopPropagation() -}) - -accountMenuLogoutButton.addEventListener('click', async () => { - await logoutSelectedAccount() -}) - -document.addEventListener('click', () => { - if(!accountMenu.hasAttribute('hidden')){ - setAccountMenuOpen(false) - } -}) - -// Bind selected account -function updateSelectedAccount(authUser){ - let username = Lang.queryJS('landing.selectedAccount.noAccountSelected') - if(authUser != null){ - if(authUser.displayName != null){ - username = authUser.displayName - } - if(authUser.uuid != null){ - avatarContainer.style.backgroundImage = `url('https://mc-heads.net/avatar/${authUser.uuid}/64'), url('assets/images/Icon.png')` - } - } else { - avatarContainer.style.backgroundImage = `url('assets/images/Icon.png')` - } - accountPreviewName.textContent = username - accountMenuName.textContent = username - avatarMenuButton.disabled = authUser == null - if(authUser == null){ - setAccountMenuOpen(false) - } -} -updateSelectedAccount(ConfigManager.getSelectedAccount()) - -// Bind selected server -function updateSelectedServer(serv){ - if(getCurrentView() === VIEWS.settings){ - fullSettingsSave() - } - ConfigManager.setSelectedServer(serv != null ? serv.rawServer.id : null) - ConfigManager.save() - if(getCurrentView() === VIEWS.settings){ - animateSettingsTabRefresh() - } - refreshSelectedProfileButton() -} -// Real text is set based on the selected library profile. -server_selection_button.innerHTML = '• ' + Lang.queryJS('landing.selectedProfile.loading') -server_selection_button.onclick = async e => { - e.target.blur() - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } - switchView(getCurrentView(), VIEWS.library) -} - -// Update Mojang Status Color -const refreshMojangStatuses = async function(){ - loggerLanding.info('Refreshing Mojang Statuses..') - - let status = 'grey' - let tooltipEssentialHTML = '' - let tooltipNonEssentialHTML = '' - - const response = await MojangRestAPI.status() - let statuses - if(response.responseStatus === RestResponseStatus.SUCCESS) { - statuses = response.data - } else { - loggerLanding.warn('Unable to refresh Mojang service status.') - statuses = MojangRestAPI.getDefaultStatuses() - } - - greenCount = 0 - greyCount = 0 - - for(let i=0; i - - ${service.name} - ` - if(service.essential){ - tooltipEssentialHTML += tooltipHTML - } else { - tooltipNonEssentialHTML += tooltipHTML - } - - if(service.status === 'yellow' && status !== 'red'){ - status = 'yellow' - } else if(service.status === 'red'){ - status = 'red' - } else { - if(service.status === 'grey'){ - ++greyCount - } - ++greenCount - } - - } - - if(greenCount === statuses.length){ - if(greyCount === statuses.length){ - status = 'grey' - } else { - status = 'green' - } - } - - document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML - document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML - document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status) -} - -const refreshServerStatus = async (fade = false) => { - loggerLanding.info('Refreshing Server Status') - const selectedProfile = CatalogManager.getSelectedProfileSync() - - let pLabel = Lang.queryJS('landing.profileStatus.label') - let pVal = Lang.queryJS('landing.selectedProfile.noSelection') - let pTone = 'info' - let tooltip = '라이브러리에서 실행할 프로필을 먼저 선택하세요.' - - if(selectedProfile == null){ - updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade) - return - } - - if(selectedProfile.serverEnabled !== true){ - pLabel = Lang.queryJS('landing.mapStatus.label') - pVal = isSelectedMapReady(selectedProfile) - ? Lang.queryJS('landing.mapStatus.ready') - : Lang.queryJS('landing.mapStatus.notReady') - pTone = selectedProfile.launchReady ? 'success' : 'error' - tooltip = selectedProfile.launchReady - ? '이 프로필은 맵 실행 준비가 끝났습니다.' - : (selectedProfile.launchIssues?.join(' / ') || '맵 실행 준비가 아직 끝나지 않았습니다.') - updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade) - return - } - - try { - pLabel = Lang.queryJS('landing.portStatus.label') - const portState = await PortManager.ensurePortAvailability(selectedProfile) - pVal = portState.summary - pTone = portState.tone - tooltip = selectedProfile.hostIssues?.length > 0 - ? `${portState.message} / ${selectedProfile.hostIssues.join(' / ')}` - : portState.message - } catch (err) { - loggerLanding.warn('Unable to refresh port status.') - loggerLanding.debug(err) - pLabel = Lang.queryJS('landing.portStatus.label') - pVal = Lang.queryJS('landing.portStatus.failed') - pTone = 'error' - tooltip = err instanceof Error ? err.message : '자동 포트 개방 상태를 확인하지 못했습니다.' - } - - updateLandingStatusDisplay(pLabel, pVal, pTone, tooltip, fade) -} - -refreshMojangStatuses() -// Server Status is refreshed in uibinder.js on distributionIndexDone. - -// Refresh statuses every hour. The status page itself refreshes every day so... -let mojangStatusListener = setInterval(() => refreshMojangStatuses(true), 60*60*1000) -// Set refresh rate to once every 5 minutes. -let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000) - -window.addEventListener('beforeunload', () => { - PortManager.cleanupAll().catch(() => {}) -}) - -/** - * Shows an error overlay, toggles off the launch area. - * - * @param {string} title The overlay title. - * @param {string} desc The overlay description. - */ -function showLaunchFailure(title, desc){ - setOverlayContent( - title, - desc, - Lang.queryJS('landing.launch.okay') - ) - setOverlayHandler(null) - toggleOverlay(true) - toggleLaunchArea(false) -} - -/* System (Java) Scan */ - -/** - * Asynchronously scan the system for valid Java installations. - * - * @param {boolean} launchAfter Whether we should begin to launch after scanning. - */ -async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){ - - setLaunchDetails(Lang.queryJS('landing.systemScan.checking')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const jvmDetails = await discoverBestJvmInstallation( - ConfigManager.getDataDirectory(), - effectiveJavaOptions.supported - ) - - if(jvmDetails == null) { - // If the result is null, no valid Java installation was found. - // Show this information to the user. - setOverlayContent( - Lang.queryJS('landing.systemScan.noCompatibleJava'), - Lang.queryJS('landing.systemScan.installJavaMessage', { 'major': effectiveJavaOptions.suggestedMajor }), - Lang.queryJS('landing.systemScan.installJava'), - Lang.queryJS('landing.systemScan.installJavaManually') - ) - setOverlayHandler(() => { - setLaunchDetails(Lang.queryJS('landing.systemScan.javaDownloadPrepare')) - toggleOverlay(false) - - try { - downloadJava(effectiveJavaOptions, launchAfter) - } catch(err) { - loggerLanding.error('Unhandled error in Java Download', err) - showLaunchFailure(Lang.queryJS('landing.systemScan.javaDownloadFailureTitle'), Lang.queryJS('landing.systemScan.javaDownloadFailureText')) - } - }) - setDismissHandler(() => { - $('#overlayContent').fadeOut(250, () => { - //$('#overlayDismiss').toggle(false) - setOverlayContent( - Lang.queryJS('landing.systemScan.javaRequired', { 'major': effectiveJavaOptions.suggestedMajor }), - Lang.queryJS('landing.systemScan.javaRequiredMessage', { 'major': effectiveJavaOptions.suggestedMajor }), - Lang.queryJS('landing.systemScan.javaRequiredDismiss'), - Lang.queryJS('landing.systemScan.javaRequiredCancel') - ) - setOverlayHandler(() => { - toggleLaunchArea(false) - toggleOverlay(false) - }) - setDismissHandler(() => { - toggleOverlay(false, true) - - asyncSystemScan(effectiveJavaOptions, launchAfter) - }) - $('#overlayContent').fadeIn(250) - }) - }) - toggleOverlay(true, true) - } else { - // Java installation found, use this to launch the game. - const javaExec = javaExecFromRoot(jvmDetails.path) - ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), javaExec) - ConfigManager.save() - - // We need to make sure that the updated value is on the settings UI. - // Just incase the settings UI is already open. - settingsJavaExecVal.value = javaExec - await populateJavaExecDetails(settingsJavaExecVal.value) - - // TODO Callback hell, refactor - // TODO Move this out, separate concerns. - if(launchAfter){ - await dlAsync() - } - } - -} - -async function downloadJava(effectiveJavaOptions, launchAfter = true) { - - // TODO Error handling. - // asset can be null. - const asset = await latestOpenJDK( - effectiveJavaOptions.suggestedMajor, - ConfigManager.getDataDirectory(), - effectiveJavaOptions.distribution) - - if(asset == null) { - throw new Error(Lang.queryJS('landing.downloadJava.findJdkFailure')) - } - - let received = 0 - await downloadFile(asset.url, asset.path, ({ transferred }) => { - received = transferred - setDownloadPercentage(Math.trunc((transferred/asset.size)*100)) - }) - setDownloadPercentage(100) - - if(received != asset.size) { - loggerLanding.warn(`Java Download: Expected ${asset.size} bytes but received ${received}`) - if(!await validateLocalFile(asset.path, asset.algo, asset.hash)) { - log.error(`Hashes do not match, ${asset.id} may be corrupted.`) - // Don't know how this could happen, but report it. - throw new Error(Lang.queryJS('landing.downloadJava.javaDownloadCorruptedError')) - } - } - - // Extract - // Show installing progress bar. - remote.getCurrentWindow().setProgressBar(2) - - // Wait for extration to complete. - const eLStr = Lang.queryJS('landing.downloadJava.extractingJava') - let dotStr = '' - setLaunchDetails(eLStr) - const extractListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - setLaunchDetails(eLStr + dotStr) - }, 750) - - const newJavaExec = await extractJdk(asset.path) - - // Extraction complete, remove the loading from the OS progress bar. - remote.getCurrentWindow().setProgressBar(-1) - - // Extraction completed successfully. - ConfigManager.setJavaExecutable(ConfigManager.getSelectedServer(), newJavaExec) - ConfigManager.save() - - clearInterval(extractListener) - setLaunchDetails(Lang.queryJS('landing.downloadJava.javaInstalled')) - - // TODO Callback hell - // Refactor the launch functions - asyncSystemScan(effectiveJavaOptions, launchAfter) - -} - -// Keep reference to Minecraft Process -let proc -// Is DiscordRPC enabled -let hasRPC = false -// Joined server regex -// Change this if your server uses something different. -const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/ -const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+|Loading Minecraft .+ with Fabric Loader .+)$/ -const MIN_LINGER = 5000 - -async function dlAsync(login = true) { - - // Login parameter is temporary for debug purposes. Allows testing the validation/downloads without - // launching the game. - - const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite') - - setLaunchDetails(Lang.queryJS('landing.dlAsync.loadingServerInfo')) - - let distro - - try { - distro = await DistroAPI.refreshDistributionOrFallback() - onDistroRefresh(distro) - } catch(err) { - loggerLaunchSuite.error('Unable to refresh distribution index.', err) - showLaunchFailure(Lang.queryJS('landing.dlAsync.fatalError'), Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex')) - return - } - - const serv = distro.getServerById(ConfigManager.getSelectedServer()) - - if(login) { - if(ConfigManager.getSelectedAccount() == null){ - loggerLanding.error('You must be logged into an account.') - return - } - } - - setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait')) - toggleLaunchArea(true) - setLaunchPercentage(0, 100) - - const fullRepairModule = new FullRepair( - ConfigManager.getCommonDirectory(), - ConfigManager.getInstanceDirectory(), - ConfigManager.getLauncherDirectory(), - ConfigManager.getSelectedServer(), - DistroAPI.isDevMode() - ) - - const receiverPatchPath = path - .resolve(__dirname, '..', 'vanillapatch.js') - .replace(/\\/g, '/') - const existingNodeOptions = process.env.NODE_OPTIONS?.trim() - const receiverNodeOptions = `--require=${receiverPatchPath}` - - fullRepairModule.spawnReceiver({ - NODE_OPTIONS: existingNodeOptions != null && existingNodeOptions.length > 0 - ? `${existingNodeOptions} ${receiverNodeOptions}` - : receiverNodeOptions - }) - - fullRepairModule.childProcess.on('error', (err) => { - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText')) - }) - fullRepairModule.childProcess.on('close', (code, _signal) => { - if(code !== 0){ - loggerLaunchSuite.error(`Full Repair Module exited with code ${code}, assuming error.`) - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails')) - } - }) - - loggerLaunchSuite.info('Validating files.') - setLaunchDetails(Lang.queryJS('landing.dlAsync.validatingFileIntegrity')) - let invalidFileCount = 0 - try { - invalidFileCount = await fullRepairModule.verifyFiles(percent => { - setLaunchPercentage(percent) - }) - setLaunchPercentage(100) - } catch (err) { - loggerLaunchSuite.error('Error during file validation.') - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileVerificationTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails')) - return - } - - - if(invalidFileCount > 0) { - loggerLaunchSuite.info('Downloading files.') - setLaunchDetails(Lang.queryJS('landing.dlAsync.downloadingFiles')) - setLaunchPercentage(0) - try { - await fullRepairModule.download(percent => { - setDownloadPercentage(percent) - }) - setDownloadPercentage(100) - } catch(err) { - loggerLaunchSuite.error('Error during file download.') - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails')) - return - } - } else { - loggerLaunchSuite.info('No invalid files, skipping download.') - } - - // Remove download bar. - remote.getCurrentWindow().setProgressBar(-1) - - fullRepairModule.destroyReceiver() - - setLaunchDetails(Lang.queryJS('landing.dlAsync.preparingToLaunch')) - - const mojangIndexProcessor = new MojangIndexProcessor( - ConfigManager.getCommonDirectory(), - serv.rawServer.minecraftVersion) - const distributionIndexProcessor = new DistributionIndexProcessor( - ConfigManager.getCommonDirectory(), - distro, - serv.rawServer.id - ) - - const modLoaderData = await distributionIndexProcessor.loadModLoaderVersionJson() - const versionData = await mojangIndexProcessor.getVersionJson() - - if(login) { - const authUser = ConfigManager.getSelectedAccount() - const selectedProfile = CatalogManager.getSelectedProfileSync() - loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`) - let pb = new ProcessBuilder(serv, versionData, modLoaderData, authUser, remote.app.getVersion()) - setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame')) - - if(selectedProfile?.serverEnabled === true && CatalogManager.shouldHostLocally(selectedProfile)){ - if(!selectedProfile.hostReady){ - showLaunchFailure( - Lang.queryJS('landing.localServer.missingJarTitle'), - Lang.queryJS('landing.localServer.missingJarText') - ) - return - } - - setLaunchDetails(Lang.queryJS('landing.localServer.starting')) - try { - await ServerRuntime.startHostedProfile(selectedProfile) - refreshServerStatus(true) - } catch (error) { - loggerLaunchSuite.error('Failed to start local server.', error) - showLaunchFailure( - Lang.queryJS('landing.localServer.failureTitle'), - error instanceof Error ? error.message : Lang.queryJS('landing.localServer.failureText') - ) - return - } - } - - // const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/ - const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`) - - const onLoadComplete = () => { - toggleLaunchArea(false) - if(hasRPC){ - DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.loading')) - proc.stdout.on('data', gameStateChange) - } - proc.stdout.removeListener('data', tempListener) - proc.stderr.removeListener('data', gameErrorListener) - } - const start = Date.now() - - // Attach a temporary listener to the client output. - // Will wait for a certain bit of text meaning that - // the client application has started, and we can hide - // the progress bar stuff. - const tempListener = function(data){ - if(GAME_LAUNCH_REGEX.test(data.trim())){ - const diff = Date.now()-start - if(diff < MIN_LINGER) { - setTimeout(onLoadComplete, MIN_LINGER-diff) - } else { - onLoadComplete() - } - } - } - - // Listener for Discord RPC. - const gameStateChange = function(data){ - data = data.trim() - if(SERVER_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.joined')) - } else if(GAME_JOINED_REGEX.test(data)){ - DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.joining')) - } - } - - const gameErrorListener = function(data){ - data = data.trim() - if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){ - loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.') - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.launchWrapperNotDownloaded')) - } - } - - try { - // Build Minecraft process. - proc = pb.build() - - // Bind listeners to stdout. - proc.stdout.on('data', tempListener) - proc.stderr.on('data', gameErrorListener) - - setLaunchDetails(Lang.queryJS('landing.dlAsync.doneEnjoyServer')) - - // Init Discord Hook - if(distro.rawDistribution.discord != null && serv.rawServer.discord != null){ - DiscordWrapper.initRPC(distro.rawDistribution.discord, serv.rawServer.discord) - hasRPC = true - proc.on('close', (code, signal) => { - loggerLaunchSuite.info('Shutting down Discord Rich Presence..') - DiscordWrapper.shutdownRPC() - hasRPC = false - proc = null - }) - } - - } catch(err) { - - loggerLaunchSuite.error('Error during launch', err) - showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.checkConsoleForDetails')) - - } - } - -} - -/** - * News Loading Functions - */ - -// DOM Cache -const newsContent = document.getElementById('newsContent') -const newsArticleTitle = document.getElementById('newsArticleTitle') -const newsArticleDate = document.getElementById('newsArticleDate') -const newsArticleAuthor = document.getElementById('newsArticleAuthor') -const newsArticleComments = document.getElementById('newsArticleComments') -const newsNavigationStatus = document.getElementById('newsNavigationStatus') -const newsArticleContentScrollable = document.getElementById('newsArticleContentScrollable') -const nELoadSpan = document.getElementById('nELoadSpan') - -// News slide caches. -let newsActive = false -let newsGlideCount = 0 - -/** - * Show the news UI via a slide animation. - * - * @param {boolean} up True to slide up, otherwise false. - */ -function slide_(up){ - const lCUpper = document.querySelector('#landingContainer > #upper') - const lCLLeft = document.querySelector('#landingContainer > #lower > #left') - const lCLCenter = document.querySelector('#landingContainer > #lower > #center') - const lCLRight = document.querySelector('#landingContainer > #lower > #right') - const newsBtn = document.querySelector('#landingContainer > #lower > #center #content') - const landingContainer = document.getElementById('landingContainer') - const newsContainer = document.querySelector('#landingContainer > #newsContainer') - - newsGlideCount++ - - if(up){ - lCUpper.style.top = '-200vh' - lCLLeft.style.top = '-200vh' - lCLCenter.style.top = '-200vh' - lCLRight.style.top = '-200vh' - newsBtn.style.top = '130vh' - newsContainer.style.top = '0px' - //date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric'}) - //landingContainer.style.background = 'rgba(29, 29, 29, 0.55)' - landingContainer.style.background = 'rgba(0, 0, 0, 0.50)' - setTimeout(() => { - if(newsGlideCount === 1){ - lCLCenter.style.transition = 'none' - newsBtn.style.transition = 'none' - } - newsGlideCount-- - }, 2000) - } else { - setTimeout(() => { - newsGlideCount-- - }, 2000) - landingContainer.style.background = null - lCLCenter.style.transition = null - newsBtn.style.transition = null - newsContainer.style.top = '100%' - lCUpper.style.top = '0px' - lCLLeft.style.top = '0px' - lCLCenter.style.top = '0px' - lCLRight.style.top = '0px' - newsBtn.style.top = '10px' - } -} - -// Bind install button on the landing footer. -document.getElementById('newsButton').onclick = () => { - if(typeof refreshInstallView === 'function'){ - refreshInstallView() - } - switchView(getCurrentView(), VIEWS.install) -} - -// Array to store article meta. -let newsArr = null - -// News load animation listener. -let newsLoadingListener = null - -/** - * Set the news loading animation. - * - * @param {boolean} val True to set loading animation, otherwise false. - */ -function setNewsLoading(val){ - if(val){ - const nLStr = Lang.queryJS('landing.news.checking') - let dotStr = '..' - nELoadSpan.innerHTML = nLStr + dotStr - newsLoadingListener = setInterval(() => { - if(dotStr.length >= 3){ - dotStr = '' - } else { - dotStr += '.' - } - nELoadSpan.innerHTML = nLStr + dotStr - }, 750) - } else { - if(newsLoadingListener != null){ - clearInterval(newsLoadingListener) - newsLoadingListener = null - } - } -} - -// Bind retry button. -newsErrorRetry.onclick = () => { - $('#newsErrorFailed').fadeOut(250, () => { - initNews() - $('#newsErrorLoading').fadeIn(250) - }) -} - -newsArticleContentScrollable.onscroll = (e) => { - if(e.target.scrollTop > Number.parseFloat($('.newsArticleSpacerTop').css('height'))){ - newsContent.setAttribute('scrolled', '') - } else { - newsContent.removeAttribute('scrolled') - } -} - -/** - * Reload the news without restarting. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -function reloadNews(){ - return new Promise((resolve, reject) => { - $('#newsContent').fadeOut(250, () => { - $('#newsErrorLoading').fadeIn(250) - initNews().then(() => { - resolve() - }) - }) - }) -} - -let newsAlertShown = false - -/** - * Show the news alert indicating there is new news. - */ -function showNewsAlert(){ - newsAlertShown = true - $(newsButtonAlert).fadeIn(250) -} - -async function digestMessage(str) { - const msgUint8 = new TextEncoder().encode(str) - const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - return hashHex -} - -/** - * Initialize News UI. This will load the news and prepare - * the UI accordingly. - * - * @returns {Promise.} A promise which resolves when the news - * content has finished loading and transitioning. - */ -async function initNews(){ - - setNewsLoading(true) - - const news = await loadNews() - - newsArr = news?.articles || null - - if(newsArr == null){ - // News Loading Failed - setNewsLoading(false) - - await $('#newsErrorLoading').fadeOut(250).promise() - await $('#newsErrorFailed').fadeIn(250).promise() - - } else if(newsArr.length === 0) { - // No News Articles - setNewsLoading(false) - - ConfigManager.setNewsCache({ - date: null, - content: null, - dismissed: false - }) - ConfigManager.save() - - await $('#newsErrorLoading').fadeOut(250).promise() - await $('#newsErrorNone').fadeIn(250).promise() - } else { - // Success - setNewsLoading(false) - - const lN = newsArr[0] - const cached = ConfigManager.getNewsCache() - let newHash = await digestMessage(lN.content) - let newDate = new Date(lN.date) - let isNew = false - - if(cached.date != null && cached.content != null){ - - if(new Date(cached.date) >= newDate){ - - // Compare Content - if(cached.content !== newHash){ - isNew = true - showNewsAlert() - } else { - if(!cached.dismissed){ - isNew = true - showNewsAlert() - } - } - - } else { - isNew = true - showNewsAlert() - } - - } else { - isNew = true - showNewsAlert() - } - - if(isNew){ - ConfigManager.setNewsCache({ - date: newDate.getTime(), - content: newHash, - dismissed: false - }) - ConfigManager.save() - } - - const switchHandler = (forward) => { - let cArt = parseInt(newsContent.getAttribute('article')) - let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1) - - displayArticle(newsArr[nxtArt], nxtArt+1) - } - - document.getElementById('newsNavigateRight').onclick = () => { switchHandler(true) } - document.getElementById('newsNavigateLeft').onclick = () => { switchHandler(false) } - await $('#newsErrorContainer').fadeOut(250).promise() - displayArticle(newsArr[0], 1) - await $('#newsContent').fadeIn(250).promise() - } - - -} - -/** - * Add keyboard controls to the news UI. Left and right arrows toggle - * between articles. If you are on the landing page, the up arrow will - * open the news UI. - */ -document.addEventListener('keydown', (e) => { - if(newsActive){ - if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){ - document.getElementById(e.key === 'ArrowRight' ? 'newsNavigateRight' : 'newsNavigateLeft').click() - } - // Interferes with scrolling an article using the down arrow. - // Not sure of a straight forward solution at this point. - // if(e.key === 'ArrowDown'){ - // document.getElementById('newsButton').click() - // } - } -}) - -/** - * Display a news article on the UI. - * - * @param {Object} articleObject The article meta object. - * @param {number} index The article index. - */ -function displayArticle(articleObject, index){ - newsArticleTitle.innerHTML = articleObject.title - newsArticleTitle.href = articleObject.link - newsArticleAuthor.innerHTML = 'by ' + articleObject.author - newsArticleDate.innerHTML = articleObject.date - newsArticleComments.innerHTML = articleObject.comments - newsArticleComments.href = articleObject.commentsLink - newsArticleContentScrollable.innerHTML = '
' + articleObject.content + '
' - Array.from(newsArticleContentScrollable.getElementsByClassName('bbCodeSpoilerButton')).forEach(v => { - v.onclick = () => { - const text = v.parentElement.getElementsByClassName('bbCodeSpoilerText')[0] - text.style.display = text.style.display === 'block' ? 'none' : 'block' - } - }) - newsNavigationStatus.innerHTML = Lang.query('ejs.landing.newsNavigationStatus', {currentPage: index, totalPages: newsArr.length}) - newsContent.setAttribute('article', index-1) -} - -/** - * Load news information from the RSS feed specified in the - * distribution index. - */ -async function loadNews(){ - - const distroData = await DistroAPI.getDistribution() - if(!distroData.rawDistribution.rss) { - loggerLanding.debug('No RSS feed provided.') - return null - } - - const promise = new Promise((resolve, reject) => { - - const newsFeed = distroData.rawDistribution.rss - const newsHost = new URL(newsFeed).origin + '/' - $.ajax({ - url: newsFeed, - success: (data) => { - const items = $(data).find('item') - const articles = [] - - for(let i=0; i { - resolve({ - articles: null - }) - }) - }) - - return await promise -} - -window.updateSelectedAccount = updateSelectedAccount -window.updateSelectedServer = updateSelectedServer -window.refreshServerStatus = refreshServerStatus -window.initNews = initNews -window.refreshSelectedProfileButton = refreshSelectedProfileButton -})() diff --git a/app/assets/js/scripts/library.js b/app/assets/js/scripts/library.js deleted file mode 100644 index 0d52723..0000000 --- a/app/assets/js/scripts/library.js +++ /dev/null @@ -1,267 +0,0 @@ -(() => { -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() -})() diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js deleted file mode 100644 index 6eef5fa..0000000 --- a/app/assets/js/scripts/login.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Script for login.ejs - */ -// Validation Regexes. -const validUsername = /^[a-zA-Z0-9_]{1,16}$/ -const basicEmail = /^\S+@\S+\.\S+$/ -//const validEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i - -// Login Elements -const loginCancelContainer = document.getElementById('loginCancelContainer') -const loginCancelButton = document.getElementById('loginCancelButton') -const loginEmailError = document.getElementById('loginEmailError') -const loginUsername = document.getElementById('loginUsername') -const loginPasswordError = document.getElementById('loginPasswordError') -const loginPassword = document.getElementById('loginPassword') -const checkmarkContainer = document.getElementById('checkmarkContainer') -const loginRememberOption = document.getElementById('loginRememberOption') -const loginButton = document.getElementById('loginButton') -const loginForm = document.getElementById('loginForm') - -// Control variables. -let lu = false, lp = false - - -/** - * Show a login error. - * - * @param {HTMLElement} element The element on which to display the error. - * @param {string} value The error text. - */ -function showError(element, value){ - element.innerHTML = value - element.style.opacity = 1 -} - -/** - * Shake a login error to add emphasis. - * - * @param {HTMLElement} element The element to shake. - */ -function shakeError(element){ - if(element.style.opacity == 1){ - element.classList.remove('shake') - void element.offsetWidth - element.classList.add('shake') - } -} - -/** - * Validate that an email field is neither empty nor invalid. - * - * @param {string} value The email value. - */ -function validateEmail(value){ - if(value){ - if(!basicEmail.test(value) && !validUsername.test(value)){ - showError(loginEmailError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - lu = false - } else { - loginEmailError.style.opacity = 0 - lu = true - if(lp){ - loginDisabled(false) - } - } - } else { - lu = false - showError(loginEmailError, Lang.queryJS('login.error.requiredValue')) - loginDisabled(true) - } -} - -/** - * Validate that the password field is not empty. - * - * @param {string} value The password value. - */ -function validatePassword(value){ - if(value){ - loginPasswordError.style.opacity = 0 - lp = true - if(lu){ - loginDisabled(false) - } - } else { - lp = false - showError(loginPasswordError, Lang.queryJS('login.error.invalidValue')) - loginDisabled(true) - } -} - -// Emphasize errors with shake when focus is lost. -loginUsername.addEventListener('focusout', (e) => { - validateEmail(e.target.value) - shakeError(loginEmailError) -}) -loginPassword.addEventListener('focusout', (e) => { - validatePassword(e.target.value) - shakeError(loginPasswordError) -}) - -// Validate input for each field. -loginUsername.addEventListener('input', (e) => { - validateEmail(e.target.value) -}) -loginPassword.addEventListener('input', (e) => { - validatePassword(e.target.value) -}) - -/** - * Enable or disable the login button. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginDisabled(v){ - if(loginButton.disabled !== v){ - loginButton.disabled = v - } -} - -/** - * Enable or disable loading elements. - * - * @param {boolean} v True to enable, false to disable. - */ -function loginLoading(v){ - if(v){ - loginButton.setAttribute('loading', v) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.login'), Lang.queryJS('login.loggingIn')) - } else { - loginButton.removeAttribute('loading') - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.login')) - } -} - -/** - * Enable or disable login form. - * - * @param {boolean} v True to enable, false to disable. - */ -function formDisabled(v){ - loginDisabled(v) - loginCancelButton.disabled = v - loginUsername.disabled = v - loginPassword.disabled = v - if(v){ - checkmarkContainer.setAttribute('disabled', v) - } else { - checkmarkContainer.removeAttribute('disabled') - } - loginRememberOption.disabled = v -} - -let loginViewOnSuccess = VIEWS.landing -let loginViewOnCancel = VIEWS.settings -let loginViewCancelHandler - -function loginCancelEnabled(val){ - if(val){ - $(loginCancelContainer).show() - } else { - $(loginCancelContainer).hide() - } -} - -loginCancelButton.onclick = (e) => { - switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => { - loginUsername.value = '' - loginPassword.value = '' - loginCancelEnabled(false) - if(loginViewCancelHandler != null){ - loginViewCancelHandler() - loginViewCancelHandler = null - } - }) -} - -// Disable default form behavior. -loginForm.onsubmit = () => { return false } - -// Bind login button behavior. -loginButton.addEventListener('click', () => { - // Disable form. - formDisabled(true) - - // Show loading stuff. - loginLoading(true) - - AuthManager.addMojangAccount(loginUsername.value, loginPassword.value).then((value) => { - updateSelectedAccount(value) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.loggingIn'), Lang.queryJS('login.success')) - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - setTimeout(() => { - switchView(VIEWS.login, loginViewOnSuccess, 500, 500, async () => { - // Temporary workaround - if(loginViewOnSuccess === VIEWS.settings){ - await prepareSettings() - } - loginViewOnSuccess = VIEWS.landing // Reset this for good measure. - loginCancelEnabled(false) // Reset this for good measure. - loginViewCancelHandler = null // Reset this for good measure. - loginUsername.value = '' - loginPassword.value = '' - $('.circle-loader').toggleClass('load-complete') - $('.checkmark').toggle() - loginLoading(false) - loginButton.innerHTML = loginButton.innerHTML.replace(Lang.queryJS('login.success'), Lang.queryJS('login.login')) - formDisabled(false) - }) - }, 1000) - }).catch((displayableError) => { - loginLoading(false) - - let actualDisplayableError - if(isDisplayableError(displayableError)) { - msftLoginLogger.error('Error while logging in.', displayableError) - actualDisplayableError = displayableError - } else { - // Uh oh. - msftLoginLogger.error('Unhandled error during login.', displayableError) - actualDisplayableError = Lang.queryJS('login.error.unknown') - } - - setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) - setOverlayHandler(() => { - formDisabled(false) - toggleOverlay(false) - }) - toggleOverlay(true) - }) - -}) diff --git a/app/assets/js/scripts/loginOptions.js b/app/assets/js/scripts/loginOptions.js deleted file mode 100644 index cdb1bc8..0000000 --- a/app/assets/js/scripts/loginOptions.js +++ /dev/null @@ -1,50 +0,0 @@ -const loginOptionsCancelContainer = document.getElementById('loginOptionCancelContainer') -const loginOptionMicrosoft = document.getElementById('loginOptionMicrosoft') -const loginOptionMojang = document.getElementById('loginOptionMojang') -const loginOptionsCancelButton = document.getElementById('loginOptionCancelButton') - -let loginOptionsCancellable = false - -let loginOptionsViewOnLoginSuccess -let loginOptionsViewOnLoginCancel -let loginOptionsViewOnCancel -let loginOptionsViewCancelHandler - -function loginOptionsCancelEnabled(val){ - if(val){ - $(loginOptionsCancelContainer).show() - } else { - $(loginOptionsCancelContainer).hide() - } -} - -loginOptionMicrosoft.onclick = (e) => { - switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { - ipcRenderer.send( - MSFT_OPCODE.OPEN_LOGIN, - loginOptionsViewOnLoginSuccess, - loginOptionsViewOnLoginCancel - ) - }) -} - -loginOptionMojang.onclick = (e) => { - switchView(getCurrentView(), VIEWS.login, 500, 500, () => { - loginViewOnSuccess = loginOptionsViewOnLoginSuccess - loginViewOnCancel = loginOptionsViewOnLoginCancel - loginCancelEnabled(true) - }) -} - -loginOptionsCancelButton.onclick = (e) => { - switchView(getCurrentView(), loginOptionsViewOnCancel, 500, 500, () => { - // Clear login values (Mojang login) - // No cleanup needed for Microsoft. - loginUsername.value = '' - loginPassword.value = '' - if(loginOptionsViewCancelHandler != null){ - loginOptionsViewCancelHandler() - loginOptionsViewCancelHandler = null - } - }) -} \ No newline at end of file diff --git a/app/assets/js/scripts/overlay.js b/app/assets/js/scripts/overlay.js deleted file mode 100644 index 0d6ab2a..0000000 --- a/app/assets/js/scripts/overlay.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Script for overlay.ejs - */ - -/* Overlay Wrapper Functions */ - -/** - * Check to see if the overlay is visible. - * - * @returns {boolean} Whether or not the overlay is visible. - */ -function isOverlayVisible(){ - return document.getElementById('main').hasAttribute('overlay') -} - -let overlayHandlerContent - -/** - * Overlay keydown handler for a non-dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyHandler (e){ - if(e.key === 'Enter' || e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } -} -/** - * Overlay keydown handler for a dismissable overlay. - * - * @param {KeyboardEvent} e The keydown event. - */ -function overlayKeyDismissableHandler (e){ - if(e.key === 'Enter'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEnter')[0].click() - } else if(e.key === 'Escape'){ - document.getElementById(overlayHandlerContent).getElementsByClassName('overlayKeybindEsc')[0].click() - } -} - -/** - * Bind overlay keydown listeners for escape and exit. - * - * @param {boolean} state Whether or not to add new event listeners. - * @param {string} content The overlay content which will be shown. - * @param {boolean} dismissable Whether or not the overlay is dismissable - */ -function bindOverlayKeys(state, content, dismissable){ - overlayHandlerContent = content - document.removeEventListener('keydown', overlayKeyHandler) - document.removeEventListener('keydown', overlayKeyDismissableHandler) - if(state){ - if(dismissable){ - document.addEventListener('keydown', overlayKeyDismissableHandler) - } else { - document.addEventListener('keydown', overlayKeyHandler) - } - } -} - -/** - * Toggle the visibility of the overlay. - * - * @param {boolean} toggleState True to display, false to hide. - * @param {boolean} dismissable Optional. True to show the dismiss option, otherwise false. - * @param {string} content Optional. The content div to be shown. - */ -function toggleOverlay(toggleState, dismissable = false, content = 'overlayContent'){ - if(toggleState == null){ - toggleState = !document.getElementById('main').hasAttribute('overlay') - } - if(typeof dismissable === 'string'){ - content = dismissable - dismissable = false - } - bindOverlayKeys(toggleState, content, dismissable) - if(toggleState){ - document.getElementById('main').setAttribute('overlay', true) - // Make things untabbable. - $('#main *').attr('tabindex', '-1') - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - $('#overlayContainer').fadeIn({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'transparent' - } - } - }) - } else { - document.getElementById('main').removeAttribute('overlay') - // Make things tabbable. - $('#main *').removeAttr('tabindex') - $('#overlayContainer').fadeOut({ - duration: 250, - start: () => { - if(getCurrentView() === VIEWS.settings){ - document.getElementById('settingsContainer').style.backgroundColor = 'rgba(0, 0, 0, 0.50)' - } - }, - complete: () => { - $('#' + content).parent().children().hide() - $('#' + content).show() - if(dismissable){ - $('#overlayDismiss').show() - } else { - $('#overlayDismiss').hide() - } - } - }) - } -} - -async function toggleServerSelection(toggleState){ - await prepareServerSelectionList() - toggleOverlay(toggleState, true, 'serverSelectContent') -} - -/** - * Set the content of the overlay. - * - * @param {string} title Overlay title text. - * @param {string} description Overlay description text. - * @param {string} acknowledge Acknowledge button text. - * @param {string} dismiss Dismiss button text. - */ -function setOverlayContent(title, description, acknowledge, dismiss = Lang.queryJS('overlay.dismiss')){ - document.getElementById('overlayTitle').innerHTML = title - document.getElementById('overlayDesc').innerHTML = description - document.getElementById('overlayAcknowledge').innerHTML = acknowledge - document.getElementById('overlayDismiss').innerHTML = dismiss -} - -/** - * Set the onclick handler of the overlay acknowledge button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setOverlayHandler(handler){ - if(handler == null){ - document.getElementById('overlayAcknowledge').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayAcknowledge').onclick = handler - } -} - -/** - * Set the onclick handler of the overlay dismiss button. - * If the handler is null, a default handler will be added. - * - * @param {function} handler - */ -function setDismissHandler(handler){ - if(handler == null){ - document.getElementById('overlayDismiss').onclick = () => { - toggleOverlay(false) - } - } else { - document.getElementById('overlayDismiss').onclick = handler - } -} - -/* Server Select View */ - -document.getElementById('serverSelectConfirm').addEventListener('click', async () => { - const listings = document.getElementsByClassName('serverListing') - for(let i=0; i 0){ - const serv = (await DistroAPI.getDistribution()).getServerById(listings[i].getAttribute('servid')) - updateSelectedServer(serv) - toggleOverlay(false) - } -}) - -document.getElementById('accountSelectConfirm').addEventListener('click', async () => { - const listings = document.getElementsByClassName('accountListing') - for(let i=0; i 0){ - const authAcc = ConfigManager.setSelectedAccount(listings[0].getAttribute('uuid')) - ConfigManager.save() - updateSelectedAccount(authAcc) - if(getCurrentView() === VIEWS.settings) { - await prepareSettings() - } - toggleOverlay(false) - validateSelectedAccount() - } -}) - -// Bind server select cancel button. -document.getElementById('serverSelectCancel').addEventListener('click', () => { - toggleOverlay(false) -}) - -document.getElementById('accountSelectCancel').addEventListener('click', () => { - $('#accountSelectContent').fadeOut(250, () => { - $('#overlayContent').fadeIn(250) - }) -}) - -function setServerListingHandlers(){ - const listings = Array.from(document.getElementsByClassName('serverListing')) - listings.map((val) => { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('serverListing') - for(let i=0; i { - val.onclick = e => { - if(val.hasAttribute('selected')){ - return - } - const cListings = document.getElementsByClassName('accountListing') - for(let i=0; i - -
- ${serv.rawServer.name} - ${serv.rawServer.description} -
-
${serv.rawServer.minecraftVersion}
-
${serv.rawServer.version}
- ${serv.rawServer.mainServer ? `
- - - - - - - - ${Lang.queryJS('settings.serverListing.mainServer')} -
` : ''} -
-
- ` - } - document.getElementById('serverSelectListScrollable').innerHTML = htmlString - -} - -function populateAccountListings(){ - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v=>accountsObj[v]) - let htmlString = '' - for(let i=0; i - -
${accounts[i].displayName}
- ` - } - document.getElementById('accountSelectListScrollable').innerHTML = htmlString - -} - -async function prepareServerSelectionList(){ - await populateServerListings() - setServerListingHandlers() -} - -function prepareAccountSelectionList(){ - populateAccountListings() - setAccountListingHandlers() -} \ No newline at end of file diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js deleted file mode 100644 index ae2a03d..0000000 --- a/app/assets/js/scripts/settings.js +++ /dev/null @@ -1,1617 +0,0 @@ -// Requirements -const os = require('os') -const semver = require('semver') -const { - validateSelectedJvm, - ensureJavaDirIsRoot -} = require('helios-core/java') - -const CatalogManager = require('./assets/js/catalogmanager') -const DropinModUtil = require('./assets/js/dropinmodutil') -const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants') -const settingsBackButton = document.getElementById('settingsBackButton') - -const settingsState = { - invalid: new Set() -} - -function bindSettingsSelect(){ - for(let ele of document.getElementsByClassName('settingsSelectContainer')) { - const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] - - selectedDiv.onclick = (e) => { - e.stopPropagation() - closeSettingsSelect(e.target) - e.target.nextElementSibling.toggleAttribute('hidden') - e.target.classList.toggle('select-arrow-active') - } - } -} - -function closeSettingsSelect(el){ - for(let ele of document.getElementsByClassName('settingsSelectContainer')) { - const selectedDiv = ele.getElementsByClassName('settingsSelectSelected')[0] - const optionsDiv = ele.getElementsByClassName('settingsSelectOptions')[0] - - if(!(selectedDiv === el)) { - selectedDiv.classList.remove('select-arrow-active') - optionsDiv.setAttribute('hidden', '') - } - } -} - -/* If the user clicks anywhere outside the select box, -then close all select boxes: */ -document.addEventListener('click', closeSettingsSelect) - -bindSettingsSelect() - - -function bindFileSelectors(){ - for(let ele of document.getElementsByClassName('settingsFileSelButton')){ - - ele.onclick = async e => { - const isJavaExecSel = ele.id === 'settingsJavaExecSel' - const directoryDialog = ele.hasAttribute('dialogDirectory') && ele.getAttribute('dialogDirectory') == 'true' - const properties = directoryDialog ? ['openDirectory', 'createDirectory'] : ['openFile'] - - const options = { - properties - } - - if(ele.hasAttribute('dialogTitle')) { - options.title = ele.getAttribute('dialogTitle') - } - - if(isJavaExecSel && process.platform === 'win32') { - options.filters = [ - { name: Lang.queryJS('settings.fileSelectors.executables'), extensions: ['exe'] }, - { name: Lang.queryJS('settings.fileSelectors.allFiles'), extensions: ['*'] } - ] - } - - const res = await remote.dialog.showOpenDialog(remote.getCurrentWindow(), options) - if(!res.canceled) { - ele.previousElementSibling.value = res.filePaths[0] - if(isJavaExecSel) { - await populateJavaExecDetails(ele.previousElementSibling.value) - } - } - } - } -} - -bindFileSelectors() - - -/** - * General Settings Functions - */ - -/** - * Bind value validators to the settings UI elements. These will - * validate against the criteria defined in the ConfigManager (if - * any). If the value is invalid, the UI will reflect this and saving - * will be disabled until the value is corrected. This is an automated - * process. More complex UI may need to be bound separately. - */ -function initSettingsValidators(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - Array.from(sEls).map((v, index, arr) => { - const vFn = ConfigManager['validate' + v.getAttribute('cValue')] - if(typeof vFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - v.addEventListener('keyup', (e) => { - const v = e.target - if(!vFn(v.value)){ - settingsState.invalid.add(v.id) - v.setAttribute('error', '') - settingsSaveDisabled(true) - } else { - if(v.hasAttribute('error')){ - v.removeAttribute('error') - settingsState.invalid.delete(v.id) - if(settingsState.invalid.size === 0){ - settingsSaveDisabled(false) - } - } - } - }) - } - } - } - - }) -} - -/** - * Load configuration values onto the UI. This is an automated process. - */ -async function initSettingsValues(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - - for(const v of sEls) { - const cVal = v.getAttribute('cValue') - const serverDependent = v.hasAttribute('serverDependent') // Means the first argument is the server id. - const gFn = ConfigManager['get' + cVal] - const gFnOpts = [] - if(serverDependent) { - gFnOpts.push(ConfigManager.getSelectedServer()) - } - if(typeof gFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - // Special Conditions - if(cVal === 'JavaExecutable'){ - v.value = gFn.apply(null, gFnOpts) - await populateJavaExecDetails(v.value) - } else if (cVal === 'DataDirectory'){ - v.value = gFn.apply(null, gFnOpts) - } else if(cVal === 'JVMOptions'){ - v.value = gFn.apply(null, gFnOpts).join(' ') - } else { - v.value = gFn.apply(null, gFnOpts) - } - } else if(v.type === 'checkbox'){ - v.checked = gFn.apply(null, gFnOpts) - } - } else if(v.tagName === 'DIV'){ - if(v.classList.contains('rangeSlider')){ - // Special Conditions - if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ - let val = gFn.apply(null, gFnOpts) - if(val.endsWith('M')){ - val = Number(val.substring(0, val.length-1))/1024 - } else { - val = Number.parseFloat(val) - } - - v.setAttribute('value', val) - } else { - v.setAttribute('value', Number.parseFloat(gFn.apply(null, gFnOpts))) - } - } - } - } - } - -} - -/** - * Save the settings values. - */ -function saveSettingsValues(){ - const sEls = document.getElementById('settingsContainer').querySelectorAll('[cValue]') - Array.from(sEls).map((v, index, arr) => { - const cVal = v.getAttribute('cValue') - const serverDependent = v.hasAttribute('serverDependent') // Means the first argument is the server id. - const sFn = ConfigManager['set' + cVal] - const sFnOpts = [] - if(serverDependent) { - sFnOpts.push(ConfigManager.getSelectedServer()) - } - if(typeof sFn === 'function'){ - if(v.tagName === 'INPUT'){ - if(v.type === 'number' || v.type === 'text'){ - // Special Conditions - if(cVal === 'JVMOptions'){ - if(!v.value.trim()) { - sFnOpts.push([]) - sFn.apply(null, sFnOpts) - } else { - sFnOpts.push(v.value.trim().split(/\s+/)) - sFn.apply(null, sFnOpts) - } - } else { - sFnOpts.push(v.value) - sFn.apply(null, sFnOpts) - } - } else if(v.type === 'checkbox'){ - sFnOpts.push(v.checked) - sFn.apply(null, sFnOpts) - // Special Conditions - if(cVal === 'AllowPrerelease'){ - changeAllowPrerelease(v.checked) - } - } - } else if(v.tagName === 'DIV'){ - if(v.classList.contains('rangeSlider')){ - // Special Conditions - if(cVal === 'MinRAM' || cVal === 'MaxRAM'){ - let val = Number(v.getAttribute('value')) - if(val%1 > 0){ - val = val*1024 + 'M' - } else { - val = val + 'G' - } - - sFnOpts.push(val) - sFn.apply(null, sFnOpts) - } else { - sFnOpts.push(v.getAttribute('value')) - sFn.apply(null, sFnOpts) - } - } - } - } - }) -} - -let selectedSettingsTab = 'settingsTabAccount' - -/** - * Modify the settings container UI when the scroll threshold reaches - * a certain poin. - * - * @param {UIEvent} e The scroll event. - */ -function settingsTabScrollListener(e){ - if(e.target.scrollTop > Number.parseFloat(getComputedStyle(e.target.firstElementChild).marginTop)){ - document.getElementById('settingsContainer').setAttribute('scrolled', '') - } else { - document.getElementById('settingsContainer').removeAttribute('scrolled') - } -} - -/** - * Bind functionality for the settings navigation items. - */ -function setupSettingsTabs(){ - Array.from(document.getElementsByClassName('settingsNavItem')).map((val) => { - if(val.hasAttribute('rSc')){ - val.onclick = () => { - settingsNavItemListener(val) - } - } - }) -} - -/** - * Settings nav item onclick lisener. Function is exposed so that - * other UI elements can quickly toggle to a certain tab from other views. - * - * @param {Element} ele The nav item which has been clicked. - * @param {boolean} fade Optional. True to fade transition. - */ -function settingsNavItemListener(ele, fade = true){ - if(ele.hasAttribute('selected')){ - return - } - const navItems = document.getElementsByClassName('settingsNavItem') - for(let i=0; i { - $(`#${selectedSettingsTab}`).fadeIn({ - duration: 250, - start: () => { - settingsTabScrollListener({ - target: document.getElementById(selectedSettingsTab) - }) - } - }) - }) - } else { - $(`#${prevTab}`).hide(0, () => { - $(`#${selectedSettingsTab}`).show({ - duration: 0, - start: () => { - settingsTabScrollListener({ - target: document.getElementById(selectedSettingsTab) - }) - } - }) - }) - } -} - -const settingsNavDone = document.getElementById('settingsNavDone') - -/** - * Set if the settings save (done) button is disabled. - * - * @param {boolean} v True to disable, false to enable. - */ -function settingsSaveDisabled(v){ - settingsNavDone.disabled = v -} - -async function refreshCatalogSourceViews(){ - await CatalogManager.getInstalledProfiles() - CatalogManager.applyConfiguredProfile() - - if(typeof refreshInstallView === 'function'){ - await refreshInstallView() - } - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } - if(typeof refreshSelectedProfileButton === 'function'){ - refreshSelectedProfileButton() - } - if(typeof refreshServerStatus === 'function'){ - await refreshServerStatus(true) - } -} - -async function fullSettingsSave() { - const previousCatalogSource = ConfigManager.getLibraryCatalogSource() - saveSettingsValues() - saveModConfiguration() - ConfigManager.save() - saveDropinModConfiguration() - saveShaderpackSettings() - - if(previousCatalogSource !== ConfigManager.getLibraryCatalogSource()){ - await refreshCatalogSourceViews() - } -} - -/* Closes the settings view and saves all data. */ -settingsNavDone.onclick = async () => { - await fullSettingsSave() - switchView(getCurrentView(), VIEWS.landing) -} - -settingsBackButton.onclick = async () => { - await fullSettingsSave() - switchView(getCurrentView(), VIEWS.landing) -} - -/** - * Account Management Tab - */ - -const msftLoginLogger = LoggerUtil.getLogger('Microsoft Login') -const msftLogoutLogger = LoggerUtil.getLogger('Microsoft Logout') - -// Bind the add mojang account button. -document.getElementById('settingsAddMojangAccount').onclick = (e) => { - switchView(getCurrentView(), VIEWS.login, 500, 500, () => { - loginViewOnCancel = VIEWS.settings - loginViewOnSuccess = VIEWS.settings - loginCancelEnabled(true) - }) -} - -// Bind the add microsoft account button. -document.getElementById('settingsAddMicrosoftAccount').onclick = (e) => { - switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { - ipcRenderer.send(MSFT_OPCODE.OPEN_LOGIN, VIEWS.settings, VIEWS.settings) - }) -} - -// Bind reply for Microsoft Login. -ipcRenderer.on(MSFT_OPCODE.REPLY_LOGIN, (_, ...arguments_) => { - if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { - - const viewOnClose = arguments_[2] - console.log(arguments_) - switchView(getCurrentView(), viewOnClose, 500, 500, () => { - - if(arguments_[1] === MSFT_ERROR.NOT_FINISHED) { - // User cancelled. - msftLoginLogger.info('Login cancelled by user.') - return - } - - // Unexpected error. - setOverlayContent( - Lang.queryJS('settings.msftLogin.errorTitle'), - Lang.queryJS('settings.msftLogin.errorMessage'), - Lang.queryJS('settings.msftLogin.okButton') - ) - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - }) - } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { - const queryMap = arguments_[1] - const viewOnClose = arguments_[2] - - // Error from request to Microsoft. - if (Object.prototype.hasOwnProperty.call(queryMap, 'error')) { - switchView(getCurrentView(), viewOnClose, 500, 500, () => { - // TODO Dont know what these errors are. Just show them I guess. - // This is probably if you messed up the app registration with Azure. - let error = queryMap.error // Error might be 'access_denied' ? - let errorDesc = queryMap.error_description - console.log('Error getting authCode, is Azure application registered correctly?') - console.log(error) - console.log(errorDesc) - console.log('Full query map: ', queryMap) - setOverlayContent( - error, - errorDesc, - Lang.queryJS('settings.msftLogin.okButton') - ) - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - - }) - } else { - - msftLoginLogger.info('Acquired authCode, proceeding with authentication.') - - const authCode = queryMap.code - AuthManager.addMicrosoftAccount(authCode).then(value => { - updateSelectedAccount(value) - switchView(getCurrentView(), viewOnClose, 500, 500, async () => { - await prepareSettings() - }) - }) - .catch((displayableError) => { - - let actualDisplayableError - if(isDisplayableError(displayableError)) { - msftLoginLogger.error('Error while logging in.', displayableError) - actualDisplayableError = displayableError - } else { - // Uh oh. - msftLoginLogger.error('Unhandled error during login.', displayableError) - actualDisplayableError = Lang.queryJS('login.error.unknown') - } - - switchView(getCurrentView(), viewOnClose, 500, 500, () => { - setOverlayContent(actualDisplayableError.title, actualDisplayableError.desc, Lang.queryJS('login.tryAgain')) - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - }) - }) - } - } -}) - -/** - * Bind functionality for the account selection buttons. If another account - * is selected, the UI of the previously selected account will be updated. - */ -function bindAuthAccountSelect(){ - Array.from(document.getElementsByClassName('settingsAuthAccountSelect')).map((val) => { - val.onclick = (e) => { - if(val.hasAttribute('selected')){ - return - } - const selectBtns = document.getElementsByClassName('settingsAuthAccountSelect') - for(let i=0; i { - val.onclick = (e) => { - let isLastAccount = false - if(Object.keys(ConfigManager.getAuthAccounts()).length === 1){ - isLastAccount = true - setOverlayContent( - Lang.queryJS('settings.authAccountLogout.lastAccountWarningTitle'), - Lang.queryJS('settings.authAccountLogout.lastAccountWarningMessage'), - Lang.queryJS('settings.authAccountLogout.confirmButton'), - Lang.queryJS('settings.authAccountLogout.cancelButton') - ) - setOverlayHandler(() => { - processLogOut(val, isLastAccount) - toggleOverlay(false) - }) - setDismissHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true, true) - } else { - processLogOut(val, isLastAccount) - } - - } - }) -} - -let msAccDomElementCache -/** - * Process a log out. - * - * @param {Element} val The log out button element. - * @param {boolean} isLastAccount If this logout is on the last added account. - */ -function processLogOut(val, isLastAccount){ - const parent = val.closest('.settingsAuthAccount') - const uuid = parent.getAttribute('uuid') - const prevSelAcc = ConfigManager.getSelectedAccount() - const targetAcc = ConfigManager.getAuthAccount(uuid) - if(targetAcc.type === 'microsoft') { - msAccDomElementCache = parent - switchView(getCurrentView(), VIEWS.waiting, 500, 500, () => { - ipcRenderer.send(MSFT_OPCODE.OPEN_LOGOUT, uuid, isLastAccount) - }) - } else { - AuthManager.removeMojangAccount(uuid).then(() => { - if(!isLastAccount && uuid === prevSelAcc.uuid){ - const selAcc = ConfigManager.getSelectedAccount() - refreshAuthAccountSelected(selAcc.uuid) - updateSelectedAccount(selAcc) - validateSelectedAccount() - } - if(isLastAccount) { - loginOptionsCancelEnabled(false) - loginOptionsViewOnLoginSuccess = VIEWS.settings - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - switchView(getCurrentView(), VIEWS.loginOptions) - } - }) - $(parent).fadeOut(250, () => { - parent.remove() - }) - } -} - -// Bind reply for Microsoft Logout. -ipcRenderer.on(MSFT_OPCODE.REPLY_LOGOUT, (_, ...arguments_) => { - if (arguments_[0] === MSFT_REPLY_TYPE.ERROR) { - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - - if(arguments_.length > 1 && arguments_[1] === MSFT_ERROR.NOT_FINISHED) { - // User cancelled. - msftLogoutLogger.info('Logout cancelled by user.') - return - } - - // Unexpected error. - setOverlayContent( - Lang.queryJS('settings.msftLogout.errorTitle'), - Lang.queryJS('settings.msftLogout.errorMessage'), - Lang.queryJS('settings.msftLogout.okButton') - ) - setOverlayHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true) - }) - } else if(arguments_[0] === MSFT_REPLY_TYPE.SUCCESS) { - - const uuid = arguments_[1] - const isLastAccount = arguments_[2] - const prevSelAcc = ConfigManager.getSelectedAccount() - - msftLogoutLogger.info('Logout Successful. uuid:', uuid) - - AuthManager.removeMicrosoftAccount(uuid) - .then(() => { - if(!isLastAccount && uuid === prevSelAcc.uuid){ - const selAcc = ConfigManager.getSelectedAccount() - refreshAuthAccountSelected(selAcc.uuid) - updateSelectedAccount(selAcc) - validateSelectedAccount() - } - if(isLastAccount) { - loginOptionsCancelEnabled(false) - loginOptionsViewOnLoginSuccess = VIEWS.settings - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - switchView(getCurrentView(), VIEWS.loginOptions) - } - if(msAccDomElementCache) { - msAccDomElementCache.remove() - msAccDomElementCache = null - } - }) - .finally(() => { - if(!isLastAccount) { - switchView(getCurrentView(), VIEWS.settings, 500, 500) - } - }) - - } -}) - -/** - * Refreshes the status of the selected account on the auth account - * elements. - * - * @param {string} uuid The UUID of the new selected account. - */ -function refreshAuthAccountSelected(uuid){ - Array.from(document.getElementsByClassName('settingsAuthAccount')).map((val) => { - const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0] - if(uuid === val.getAttribute('uuid')){ - selBtn.setAttribute('selected', '') - selBtn.innerHTML = Lang.queryJS('settings.authAccountSelect.selectedButton') - } else { - if(selBtn.hasAttribute('selected')){ - selBtn.removeAttribute('selected') - } - selBtn.innerHTML = Lang.queryJS('settings.authAccountSelect.selectButton') - } - }) -} - -const settingsCurrentMicrosoftAccounts = document.getElementById('settingsCurrentMicrosoftAccounts') -const settingsCurrentMojangAccounts = document.getElementById('settingsCurrentMojangAccounts') - -/** - * Add auth account elements for each one stored in the authentication database. - */ -function populateAuthAccounts(){ - const authAccounts = ConfigManager.getAuthAccounts() - const authKeys = Object.keys(authAccounts) - if(authKeys.length === 0){ - return - } - const selectedUUID = ConfigManager.getSelectedAccount()?.uuid ?? null - - let microsoftAuthAccountStr = '' - let mojangAuthAccountStr = '' - - authKeys.forEach((val) => { - const acc = authAccounts[val] - - const accHtml = `
-
- ${acc.displayName} -
-
-
-
-
${Lang.queryJS('settings.authAccountPopulate.username')}
-
${acc.displayName}
-
-
-
${Lang.queryJS('settings.authAccountPopulate.uuid')}
-
${acc.uuid}
-
-
-
- -
- -
-
-
-
` - - if(acc.type === 'microsoft') { - microsoftAuthAccountStr += accHtml - } else { - mojangAuthAccountStr += accHtml - } - - }) - - settingsCurrentMicrosoftAccounts.innerHTML = microsoftAuthAccountStr - settingsCurrentMojangAccounts.innerHTML = mojangAuthAccountStr -} - -/** - * Prepare the accounts tab for display. - */ -function prepareAccountsTab() { - populateAuthAccounts() - bindAuthAccountSelect() - bindAuthAccountLogOut() -} - -/** - * Minecraft Tab - */ - -/** - * Disable decimals, negative signs, and scientific notation. - */ -document.getElementById('settingsGameWidth').addEventListener('keydown', (e) => { - if(/^[-.eE]$/.test(e.key)){ - e.preventDefault() - } -}) -document.getElementById('settingsGameHeight').addEventListener('keydown', (e) => { - if(/^[-.eE]$/.test(e.key)){ - e.preventDefault() - } -}) - -/** - * Mods Tab - */ - -const settingsModsContainer = document.getElementById('settingsModsContainer') - -/** - * Resolve and update the mods on the UI. - */ -async function resolveModsForUI(){ - const serv = ConfigManager.getSelectedServer() - - const distro = await DistroAPI.getDistribution() - const servConf = ConfigManager.getModConfiguration(serv) - - const modStr = parseModulesForUI(distro.getServerById(serv).modules, false, servConf.mods) - - document.getElementById('settingsReqModsContent').innerHTML = modStr.reqMods - document.getElementById('settingsOptModsContent').innerHTML = modStr.optMods -} - -/** - * Recursively build the mod UI elements. - * - * @param {Object[]} mdls An array of modules to parse. - * @param {boolean} submodules Whether or not we are parsing submodules. - * @param {Object} servConf The server configuration object for this module level. - */ -function parseModulesForUI(mdls, submodules, servConf){ - - let reqMods = '' - let optMods = '' - - for(const mdl of mdls){ - - if(mdl.rawModule.type === Type.ForgeMod || mdl.rawModule.type === Type.LiteMod || mdl.rawModule.type === Type.LiteLoader || mdl.rawModule.type === Type.FabricMod){ - - if(mdl.getRequired().value){ - - reqMods += `
-
-
-
-
- ${mdl.rawModule.name} - v${mdl.mavenComponents.version} -
-
- -
- ${mdl.subModules.length > 0 ? `
- ${Object.values(parseModulesForUI(mdl.subModules, true, servConf[mdl.getVersionlessMavenIdentifier()])).join('')} -
` : ''} -
` - - } else { - - const conf = servConf[mdl.getVersionlessMavenIdentifier()] - const val = typeof conf === 'object' ? conf.value : conf - - optMods += `
-
-
-
-
- ${mdl.rawModule.name} - v${mdl.mavenComponents.version} -
-
- -
- ${mdl.subModules.length > 0 ? `
- ${Object.values(parseModulesForUI(mdl.subModules, true, conf.mods)).join('')} -
` : ''} -
` - - } - } - } - - return { - reqMods, - optMods - } - -} - -/** - * Bind functionality to mod config toggle switches. Switching the value - * will also switch the status color on the left of the mod UI. - */ -function bindModsToggleSwitch(){ - const sEls = settingsModsContainer.querySelectorAll('[formod]') - Array.from(sEls).map((v, index, arr) => { - v.onchange = () => { - if(v.checked) { - document.getElementById(v.getAttribute('formod')).setAttribute('enabled', '') - } else { - document.getElementById(v.getAttribute('formod')).removeAttribute('enabled') - } - } - }) -} - - -/** - * Save the mod configuration based on the UI values. - */ -function saveModConfiguration(){ - const serv = ConfigManager.getSelectedServer() - const modConf = ConfigManager.getModConfiguration(serv) - modConf.mods = _saveModConfiguration(modConf.mods) - ConfigManager.setModConfiguration(serv, modConf) -} - -/** - * Recursively save mod config with submods. - * - * @param {Object} modConf Mod config object to save. - */ -function _saveModConfiguration(modConf){ - for(let m of Object.entries(modConf)){ - const tSwitch = settingsModsContainer.querySelectorAll(`[formod='${m[0]}']`) - if(!tSwitch[0].hasAttribute('dropin')){ - if(typeof m[1] === 'boolean'){ - modConf[m[0]] = tSwitch[0].checked - } else { - if(m[1] != null){ - if(tSwitch.length > 0){ - modConf[m[0]].value = tSwitch[0].checked - } - modConf[m[0]].mods = _saveModConfiguration(modConf[m[0]].mods) - } - } - } - } - return modConf -} - -// Drop-in mod elements. - -let CACHE_SETTINGS_MODS_DIR -let CACHE_DROPIN_MODS - -/** - * Resolve any located drop-in mods for this server and - * populate the results onto the UI. - */ -async function resolveDropinModsForUI(){ - const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) - CACHE_SETTINGS_MODS_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.rawServer.id, 'mods') - CACHE_DROPIN_MODS = DropinModUtil.scanForDropinMods(CACHE_SETTINGS_MODS_DIR, serv.rawServer.minecraftVersion) - - let dropinMods = '' - - for(dropin of CACHE_DROPIN_MODS){ - dropinMods += `
-
-
-
-
- ${dropin.name} -
- -
-
-
- -
-
` - } - - document.getElementById('settingsDropinModsContent').innerHTML = dropinMods -} - -/** - * Bind the remove button for each loaded drop-in mod. - */ -function bindDropinModsRemoveButton(){ - const sEls = settingsModsContainer.querySelectorAll('[remmod]') - Array.from(sEls).map((v, index, arr) => { - v.onclick = async () => { - const fullName = v.getAttribute('remmod') - const res = await DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName) - if(res){ - document.getElementById(fullName).remove() - } else { - setOverlayContent( - Lang.queryJS('settings.dropinMods.deleteFailedTitle', { fullName }), - Lang.queryJS('settings.dropinMods.deleteFailedMessage'), - Lang.queryJS('settings.dropinMods.okButton') - ) - setOverlayHandler(null) - toggleOverlay(true) - } - } - }) -} - -/** - * Bind functionality to the file system button for the selected - * server configuration. - */ -function bindDropinModFileSystemButton(){ - const fsBtn = document.getElementById('settingsDropinFileSystemButton') - fsBtn.onclick = () => { - DropinModUtil.validateDir(CACHE_SETTINGS_MODS_DIR) - shell.openPath(CACHE_SETTINGS_MODS_DIR) - } - fsBtn.ondragenter = e => { - e.dataTransfer.dropEffect = 'move' - fsBtn.setAttribute('drag', '') - e.preventDefault() - } - fsBtn.ondragover = e => { - e.preventDefault() - } - fsBtn.ondragleave = e => { - fsBtn.removeAttribute('drag') - } - - fsBtn.ondrop = async e => { - fsBtn.removeAttribute('drag') - e.preventDefault() - - DropinModUtil.addDropinMods(e.dataTransfer.files, CACHE_SETTINGS_MODS_DIR) - await reloadDropinMods() - } -} - -/** - * Save drop-in mod states. Enabling and disabling is just a matter - * of adding/removing the .disabled extension. - */ -function saveDropinModConfiguration(){ - for(dropin of CACHE_DROPIN_MODS){ - const dropinUI = document.getElementById(dropin.fullName) - if(dropinUI != null){ - const dropinUIEnabled = dropinUI.hasAttribute('enabled') - if(DropinModUtil.isDropinModEnabled(dropin.fullName) != dropinUIEnabled){ - DropinModUtil.toggleDropinMod(CACHE_SETTINGS_MODS_DIR, dropin.fullName, dropinUIEnabled).catch(err => { - if(!isOverlayVisible()){ - setOverlayContent( - Lang.queryJS('settings.dropinMods.failedToggleTitle'), - err.message, - Lang.queryJS('settings.dropinMods.okButton') - ) - setOverlayHandler(null) - toggleOverlay(true) - } - }) - } - } - } -} - -// Refresh the drop-in mods when F5 is pressed. -// Only active on the mods tab. -document.addEventListener('keydown', async (e) => { - if(getCurrentView() === VIEWS.settings && selectedSettingsTab === 'settingsTabMods'){ - if(e.key === 'F5'){ - await reloadDropinMods() - saveShaderpackSettings() - await resolveShaderpacksForUI() - } - } -}) - -async function reloadDropinMods(){ - await resolveDropinModsForUI() - bindDropinModsRemoveButton() - bindDropinModFileSystemButton() - bindModsToggleSwitch() -} - -// Shaderpack - -let CACHE_SETTINGS_INSTANCE_DIR -let CACHE_SHADERPACKS -let CACHE_SELECTED_SHADERPACK - -/** - * Load shaderpack information. - */ -async function resolveShaderpacksForUI(){ - const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) - CACHE_SETTINGS_INSTANCE_DIR = path.join(ConfigManager.getInstanceDirectory(), serv.rawServer.id) - CACHE_SHADERPACKS = DropinModUtil.scanForShaderpacks(CACHE_SETTINGS_INSTANCE_DIR) - CACHE_SELECTED_SHADERPACK = DropinModUtil.getEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR) - - setShadersOptions(CACHE_SHADERPACKS, CACHE_SELECTED_SHADERPACK) -} - -function setShadersOptions(arr, selected){ - const cont = document.getElementById('settingsShadersOptions') - cont.innerHTML = '' - for(let opt of arr) { - const d = document.createElement('DIV') - d.innerHTML = opt.name - d.setAttribute('value', opt.fullName) - if(opt.fullName === selected) { - d.setAttribute('selected', '') - document.getElementById('settingsShadersSelected').innerHTML = opt.name - } - d.addEventListener('click', function(e) { - this.parentNode.previousElementSibling.innerHTML = this.innerHTML - for(let sib of this.parentNode.children){ - sib.removeAttribute('selected') - } - this.setAttribute('selected', '') - closeSettingsSelect() - }) - cont.appendChild(d) - } -} - -function saveShaderpackSettings(){ - let sel = 'OFF' - for(let opt of document.getElementById('settingsShadersOptions').childNodes){ - if(opt.hasAttribute('selected')){ - sel = opt.getAttribute('value') - } - } - DropinModUtil.setEnabledShaderpack(CACHE_SETTINGS_INSTANCE_DIR, sel) -} - -function bindShaderpackButton() { - const spBtn = document.getElementById('settingsShaderpackButton') - spBtn.onclick = () => { - const p = path.join(CACHE_SETTINGS_INSTANCE_DIR, 'shaderpacks') - DropinModUtil.validateDir(p) - shell.openPath(p) - } - spBtn.ondragenter = e => { - e.dataTransfer.dropEffect = 'move' - spBtn.setAttribute('drag', '') - e.preventDefault() - } - spBtn.ondragover = e => { - e.preventDefault() - } - spBtn.ondragleave = e => { - spBtn.removeAttribute('drag') - } - - spBtn.ondrop = async e => { - spBtn.removeAttribute('drag') - e.preventDefault() - - DropinModUtil.addShaderpacks(e.dataTransfer.files, CACHE_SETTINGS_INSTANCE_DIR) - saveShaderpackSettings() - await resolveShaderpacksForUI() - } -} - -// Server status bar functions. - -/** - * Load the currently selected server information onto the mods tab. - */ -async function loadSelectedServerOnModsTab(){ - const serv = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) - - for(const el of document.getElementsByClassName('settingsSelServContent')) { - el.innerHTML = ` - -
- ${serv.rawServer.name} - ${serv.rawServer.description} -
-
${serv.rawServer.minecraftVersion}
-
${serv.rawServer.version}
- ${serv.rawServer.mainServer ? `
- - - - - - - - ${Lang.queryJS('settings.serverListing.mainServer')} -
` : ''} -
-
- ` - } -} - -// Bind functionality to the server switch button. -Array.from(document.getElementsByClassName('settingsSwitchServerButton')).forEach(el => { - el.addEventListener('click', async e => { - e.target.blur() - await toggleServerSelection(true) - }) -}) - -/** - * Save mod configuration for the current selected server. - */ -function saveAllModConfigurations(){ - saveModConfiguration() - ConfigManager.save() - saveDropinModConfiguration() -} - -/** - * Function to refresh the current tab whenever the selected - * server is changed. - */ -function animateSettingsTabRefresh(){ - $(`#${selectedSettingsTab}`).fadeOut(500, async () => { - await prepareSettings() - $(`#${selectedSettingsTab}`).fadeIn(500) - }) -} - -/** - * Prepare the Mods tab for display. - */ -async function prepareModsTab(first){ - await resolveModsForUI() - await resolveDropinModsForUI() - await resolveShaderpacksForUI() - bindDropinModsRemoveButton() - bindDropinModFileSystemButton() - bindShaderpackButton() - bindModsToggleSwitch() - await loadSelectedServerOnModsTab() -} - -/** - * Java Tab - */ - -// DOM Cache -const settingsMaxRAMRange = document.getElementById('settingsMaxRAMRange') -const settingsMinRAMRange = document.getElementById('settingsMinRAMRange') -const settingsMaxRAMLabel = document.getElementById('settingsMaxRAMLabel') -const settingsMinRAMLabel = document.getElementById('settingsMinRAMLabel') -const settingsMemoryTotal = document.getElementById('settingsMemoryTotal') -const settingsMemoryAvail = document.getElementById('settingsMemoryAvail') -const settingsJavaExecDetails = document.getElementById('settingsJavaExecDetails') -const settingsJavaReqDesc = document.getElementById('settingsJavaReqDesc') -const settingsJvmOptsLink = document.getElementById('settingsJvmOptsLink') - -// Bind on change event for min memory container. -settingsMinRAMRange.onchange = (e) => { - - // Current range values - const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) - const sMinV = Number(settingsMinRAMRange.getAttribute('value')) - - // Get reference to range bar. - const bar = e.target.getElementsByClassName('rangeSliderBar')[0] - // Calculate effective total memory. - const max = os.totalmem()/1073741824 - - // Change range bar color based on the selected value. - if(sMinV >= max/2){ - bar.style.background = '#e86060' - } else if(sMinV >= max/4) { - bar.style.background = '#e8e18b' - } else { - bar.style.background = null - } - - // Increase maximum memory if the minimum exceeds its value. - if(sMaxV < sMinV){ - const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) - updateRangedSlider(settingsMaxRAMRange, sMinV, - ((sMinV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - settingsMaxRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' - } - - // Update label - settingsMinRAMLabel.innerHTML = sMinV.toFixed(1) + 'G' -} - -// Bind on change event for max memory container. -settingsMaxRAMRange.onchange = (e) => { - // Current range values - const sMaxV = Number(settingsMaxRAMRange.getAttribute('value')) - const sMinV = Number(settingsMinRAMRange.getAttribute('value')) - - // Get reference to range bar. - const bar = e.target.getElementsByClassName('rangeSliderBar')[0] - // Calculate effective total memory. - const max = os.totalmem()/1073741824 - - // Change range bar color based on the selected value. - if(sMaxV >= max/2){ - bar.style.background = '#e86060' - } else if(sMaxV >= max/4) { - bar.style.background = '#e8e18b' - } else { - bar.style.background = null - } - - // Decrease the minimum memory if the maximum value is less. - if(sMaxV < sMinV){ - const sliderMeta = calculateRangeSliderMeta(settingsMaxRAMRange) - updateRangedSlider(settingsMinRAMRange, sMaxV, - ((sMaxV-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - settingsMinRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' - } - settingsMaxRAMLabel.innerHTML = sMaxV.toFixed(1) + 'G' -} - -/** - * Calculate common values for a ranged slider. - * - * @param {Element} v The range slider to calculate against. - * @returns {Object} An object with meta values for the provided ranged slider. - */ -function calculateRangeSliderMeta(v){ - const val = { - max: Number(v.getAttribute('max')), - min: Number(v.getAttribute('min')), - step: Number(v.getAttribute('step')), - } - val.ticks = (val.max-val.min)/val.step - val.inc = 100/val.ticks - return val -} - -/** - * Binds functionality to the ranged sliders. They're more than - * just divs now :'). - */ -function bindRangeSlider(){ - Array.from(document.getElementsByClassName('rangeSlider')).map((v) => { - - // Reference the track (thumb). - const track = v.getElementsByClassName('rangeSliderTrack')[0] - - // Set the initial slider value. - const value = v.getAttribute('value') - const sliderMeta = calculateRangeSliderMeta(v) - - updateRangedSlider(v, value, ((value-sliderMeta.min)/sliderMeta.step)*sliderMeta.inc) - - // The magic happens when we click on the track. - track.onmousedown = (e) => { - - // Stop moving the track on mouse up. - document.onmouseup = (e) => { - document.onmousemove = null - document.onmouseup = null - } - - // Move slider according to the mouse position. - document.onmousemove = (e) => { - - // Distance from the beginning of the bar in pixels. - const diff = e.pageX - v.offsetLeft - track.offsetWidth/2 - - // Don't move the track off the bar. - if(diff >= 0 && diff <= v.offsetWidth-track.offsetWidth/2){ - - // Convert the difference to a percentage. - const perc = (diff/v.offsetWidth)*100 - // Calculate the percentage of the closest notch. - const notch = Number(perc/sliderMeta.inc).toFixed(0)*sliderMeta.inc - - // If we're close to that notch, stick to it. - if(Math.abs(perc-notch) < sliderMeta.inc/2){ - updateRangedSlider(v, sliderMeta.min+(sliderMeta.step*(notch/sliderMeta.inc)), notch) - } - } - } - } - }) -} - -/** - * Update a ranged slider's value and position. - * - * @param {Element} element The ranged slider to update. - * @param {string | number} value The new value for the ranged slider. - * @param {number} notch The notch that the slider should now be at. - */ -function updateRangedSlider(element, value, notch){ - const oldVal = element.getAttribute('value') - const bar = element.getElementsByClassName('rangeSliderBar')[0] - const track = element.getElementsByClassName('rangeSliderTrack')[0] - - element.setAttribute('value', value) - - if(notch < 0){ - notch = 0 - } else if(notch > 100) { - notch = 100 - } - - const event = new MouseEvent('change', { - target: element, - type: 'change', - bubbles: false, - cancelable: true - }) - - let cancelled = !element.dispatchEvent(event) - - if(!cancelled){ - track.style.left = notch + '%' - bar.style.width = notch + '%' - } else { - element.setAttribute('value', oldVal) - } -} - -/** - * Display the total and available RAM. - */ -function populateMemoryStatus(){ - settingsMemoryTotal.innerHTML = Number((os.totalmem()-1073741824)/1073741824).toFixed(1) + 'G' - settingsMemoryAvail.innerHTML = Number(os.freemem()/1073741824).toFixed(1) + 'G' -} - -/** - * Validate the provided executable path and display the data on - * the UI. - * - * @param {string} execPath The executable path to populate against. - */ -async function populateJavaExecDetails(execPath){ - const server = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) - - const details = await validateSelectedJvm(ensureJavaDirIsRoot(execPath), server.effectiveJavaOptions.supported) - - if(details != null) { - settingsJavaExecDetails.innerHTML = Lang.queryJS('settings.java.selectedJava', { version: details.semverStr, vendor: details.vendor }) - } else { - settingsJavaExecDetails.innerHTML = Lang.queryJS('settings.java.invalidSelection') - } -} - -function populateJavaReqDesc(server) { - settingsJavaReqDesc.innerHTML = Lang.queryJS('settings.java.requiresJava', { major: server.effectiveJavaOptions.suggestedMajor }) -} - -function populateJvmOptsLink(server) { - const major = server.effectiveJavaOptions.suggestedMajor - settingsJvmOptsLink.innerHTML = Lang.queryJS('settings.java.availableOptions', { major: major }) - if(major >= 12) { - settingsJvmOptsLink.href = `https://docs.oracle.com/en/java/javase/${major}/docs/specs/man/java.html#extra-options-for-java` - } - else if(major >= 11) { - settingsJvmOptsLink.href = 'https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE' - } - else if(major >= 9) { - settingsJvmOptsLink.href = `https://docs.oracle.com/javase/${major}/tools/java.htm` - } - else { - settingsJvmOptsLink.href = `https://docs.oracle.com/javase/${major}/docs/technotes/tools/${process.platform === 'win32' ? 'windows' : 'unix'}/java.html` - } -} - -function bindMinMaxRam(server) { - // Store maximum memory values. - const SETTINGS_MAX_MEMORY = ConfigManager.getAbsoluteMaxRAM(server.rawServer.javaOptions?.ram) - const SETTINGS_MIN_MEMORY = ConfigManager.getAbsoluteMinRAM(server.rawServer.javaOptions?.ram) - - // Set the max and min values for the ranged sliders. - settingsMaxRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) - settingsMaxRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) - settingsMinRAMRange.setAttribute('max', SETTINGS_MAX_MEMORY) - settingsMinRAMRange.setAttribute('min', SETTINGS_MIN_MEMORY) -} - -/** - * Prepare the Java tab for display. - */ -async function prepareJavaTab(){ - const server = (await DistroAPI.getDistribution()).getServerById(ConfigManager.getSelectedServer()) - bindMinMaxRam(server) - bindRangeSlider(server) - populateMemoryStatus() - populateJavaReqDesc(server) - populateJvmOptsLink(server) -} - -/** - * About Tab - */ - -const settingsTabAbout = document.getElementById('settingsTabAbout') -const settingsAboutChangelogTitle = settingsTabAbout.getElementsByClassName('settingsChangelogTitle')[0] -const settingsAboutChangelogText = settingsTabAbout.getElementsByClassName('settingsChangelogText')[0] -const settingsAboutChangelogButton = settingsTabAbout.getElementsByClassName('settingsChangelogButton')[0] - -// Bind the devtools toggle button. -document.getElementById('settingsAboutDevToolsButton').onclick = (e) => { - let window = remote.getCurrentWindow() - window.toggleDevTools() -} - -/** - * Return whether or not the provided version is a prerelease. - * - * @param {string} version The semver version to test. - * @returns {boolean} True if the version is a prerelease, otherwise false. - */ -function isPrerelease(version){ - const preRelComp = semver.prerelease(version) - return preRelComp != null && preRelComp.length > 0 -} - -/** - * Utility method to display version information on the - * About and Update settings tabs. - * - * @param {string} version The semver version to display. - * @param {Element} valueElement The value element. - * @param {Element} titleElement The title element. - * @param {Element} checkElement The check mark element. - */ -function populateVersionInformation(version, valueElement, titleElement, checkElement){ - valueElement.innerHTML = version - if(isPrerelease(version)){ - titleElement.innerHTML = Lang.queryJS('settings.about.preReleaseTitle') - titleElement.style.color = '#ff886d' - checkElement.style.background = '#ff886d' - } else { - titleElement.innerHTML = Lang.queryJS('settings.about.stableReleaseTitle') - titleElement.style.color = null - checkElement.style.background = null - } -} - -/** - * Retrieve the version information and display it on the UI. - */ -function populateAboutVersionInformation(){ - populateVersionInformation(remote.app.getVersion(), document.getElementById('settingsAboutCurrentVersionValue'), document.getElementById('settingsAboutCurrentVersionTitle'), document.getElementById('settingsAboutCurrentVersionCheck')) -} - -/** - * Fetches the GitHub atom release feed and parses it for the release notes - * of the current version. This value is displayed on the UI. - */ -function populateReleaseNotes(){ - $.ajax({ - url: 'https://github.com/peunsu/MRSLauncher/releases.atom', - success: (data) => { - const version = 'v' + remote.app.getVersion() - const entries = $(data).find('entry') - - for(let i=0; i { - settingsAboutChangelogText.innerHTML = Lang.queryJS('settings.about.releaseNotesFailed') - }) -} - -/** - * Prepare account tab for display. - */ -function prepareAboutTab(){ - populateAboutVersionInformation() - populateReleaseNotes() -} - -/** - * Update Tab - */ - -const settingsTabUpdate = document.getElementById('settingsTabUpdate') -const settingsUpdateTitle = document.getElementById('settingsUpdateTitle') -const settingsUpdateVersionCheck = document.getElementById('settingsUpdateVersionCheck') -const settingsUpdateVersionTitle = document.getElementById('settingsUpdateVersionTitle') -const settingsUpdateVersionValue = document.getElementById('settingsUpdateVersionValue') -const settingsUpdateChangelogTitle = settingsTabUpdate.getElementsByClassName('settingsChangelogTitle')[0] -const settingsUpdateChangelogText = settingsTabUpdate.getElementsByClassName('settingsChangelogText')[0] -const settingsUpdateChangelogCont = settingsTabUpdate.getElementsByClassName('settingsChangelogContainer')[0] -const settingsUpdateActionButton = document.getElementById('settingsUpdateActionButton') - -/** - * Update the properties of the update action button. - * - * @param {string} text The new button text. - * @param {boolean} disabled Optional. Disable or enable the button - * @param {function} handler Optional. New button event handler. - */ -function settingsUpdateButtonStatus(text, disabled = false, handler = null){ - settingsUpdateActionButton.innerHTML = text - settingsUpdateActionButton.disabled = disabled - if(handler != null){ - settingsUpdateActionButton.onclick = handler - } -} - -/** - * Populate the update tab with relevant information. - * - * @param {Object} data The update data. - */ -function populateSettingsUpdateInformation(data){ - if(data != null){ - settingsUpdateTitle.innerHTML = isPrerelease(data.version) ? Lang.queryJS('settings.updates.newPreReleaseTitle') : Lang.queryJS('settings.updates.newReleaseTitle') - settingsUpdateChangelogCont.style.display = null - settingsUpdateChangelogTitle.innerHTML = data.releaseName - settingsUpdateChangelogText.innerHTML = data.releaseNotes - populateVersionInformation(data.version, settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) - - if(process.platform === 'darwin'){ - settingsUpdateButtonStatus(Lang.queryJS('settings.updates.downloadButton'), false, () => { - shell.openExternal(data.darwindownload) - }) - } else { - settingsUpdateButtonStatus(Lang.queryJS('settings.updates.downloadingButton'), true) - } - } else { - settingsUpdateTitle.innerHTML = Lang.queryJS('settings.updates.latestVersionTitle') - settingsUpdateChangelogCont.style.display = 'none' - populateVersionInformation(remote.app.getVersion(), settingsUpdateVersionValue, settingsUpdateVersionTitle, settingsUpdateVersionCheck) - settingsUpdateButtonStatus(Lang.queryJS('settings.updates.checkForUpdatesButton'), false, () => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - settingsUpdateButtonStatus(Lang.queryJS('settings.updates.checkingForUpdatesButton'), true) - } - }) - } -} - -/** - * Prepare update tab for display. - * - * @param {Object} data The update data. - */ -function prepareUpdateTab(data = null){ - populateSettingsUpdateInformation(data) -} - -/** - * Settings preparation functions. - */ - -/** - * Prepare the entire settings UI. - * - * @param {boolean} first Whether or not it is the first load. - */ -async function prepareSettings(first = false) { - if(first){ - setupSettingsTabs() - initSettingsValidators() - prepareUpdateTab() - } else { - await prepareModsTab() - } - await initSettingsValues() - prepareAccountsTab() - await prepareJavaTab() - prepareAboutTab() -} - -// Prepare the settings UI on startup. -//prepareSettings(true) diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js deleted file mode 100644 index 882434a..0000000 --- a/app/assets/js/scripts/uibinder.js +++ /dev/null @@ -1,490 +0,0 @@ -/** - * Initialize UI functions which depend on internal modules. - * Loaded after core UI functions are initialized in uicore.js. - */ -// Requirements -const path = require('path') -const { LoggerUtil: HeliosLoggerUtil } = require('helios-core') -const { Type } = require('helios-distribution-types') - -const AuthManager = require('./assets/js/authmanager') -const ConfigManager = require('./assets/js/configmanager') -const { DistroAPI } = require('./assets/js/distromanager') - -let rscShouldLoad = false -let fatalStartupError = false -let mainUiBootstrapStarted = false -let mainUiBootstrapped = false - -const loggerUIBinder = HeliosLoggerUtil.getLogger('UIBinder') - -// Mapping of each view to their container IDs. -const VIEWS = { - landing: '#landingContainer', - library: '#libraryContainer', - install: '#installContainer', - loginOptions: '#loginOptionsContainer', - login: '#loginContainer', - settings: '#settingsContainer', - welcome: '#welcomeContainer', - waiting: '#waitingContainer' -} - -// The currently shown view container. -let currentView - -/** - * Switch launcher views. - * - * @param {string} current The ID of the current view container. - * @param {*} next The ID of the next view container. - * @param {*} currentFadeTime Optional. The fade out time for the current view. - * @param {*} nextFadeTime Optional. The fade in time for the next view. - * @param {*} onCurrentFade Optional. Callback function to execute when the current - * view fades out. - * @param {*} onNextFade Optional. Callback function to execute when the next view - * fades in. - */ -function switchView(current, next, currentFadeTime = 500, nextFadeTime = 500, onCurrentFade = () => {}, onNextFade = () => {}){ - currentView = next - $(`${current}`).fadeOut(currentFadeTime, async () => { - await onCurrentFade() - $(`${next}`).fadeIn(nextFadeTime, async () => { - await onNextFade() - }) - }) -} - -/** - * Get the currently shown view container. - * - * @returns {string} The currently shown view container. - */ -function getCurrentView(){ - return currentView -} - -async function showMainUI(data){ - - if(!isDev){ - loggerAutoUpdater.info('Initializing..') - ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease()) - } - - await prepareSettings(true) - updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer())) - refreshServerStatus() - setTimeout(() => { - document.getElementById('frameBar').style.backgroundColor = 'rgba(0, 0, 0, 0.5)' - document.body.style.backgroundImage = `url('assets/images/backgrounds/${document.body.getAttribute('bkid')}.png')` - $('#main').show() - - const isLoggedIn = Object.keys(ConfigManager.getAuthAccounts()).length > 0 - - // If this is enabled in a development environment we'll get ratelimited. - // The relaunch frequency is usually far too high. - if(!isDev && isLoggedIn){ - validateSelectedAccount() - } - - if(ConfigManager.isFirstLaunch()){ - currentView = VIEWS.welcome - $(VIEWS.welcome).fadeIn(1000) - } else { - if(isLoggedIn){ - currentView = VIEWS.landing - $(VIEWS.landing).fadeIn(1000) - } else { - loginOptionsCancelEnabled(false) - loginOptionsViewOnLoginSuccess = VIEWS.landing - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - currentView = VIEWS.loginOptions - $(VIEWS.loginOptions).fadeIn(1000) - } - } - - setTimeout(() => { - $('#loadingContainer').fadeOut(500, () => { - $('#loadSpinnerImage').removeClass('rotating') - }) - }, 250) - - }, 750) -} - -async function bootstrapMainUI(source){ - if(mainUiBootstrapped || mainUiBootstrapStarted){ - return - } - - mainUiBootstrapStarted = true - loggerUIBinder.info(`Bootstrapping main UI from ${source}.`) - - try { - const data = await DistroAPI.getDistribution() - syncModConfigurations(data) - ensureJavaSettings(data) - await showMainUI(data) - mainUiBootstrapped = true - loggerUIBinder.info(`Main UI bootstrap succeeded from ${source}.`) - } catch (error) { - loggerUIBinder.error(`Main UI bootstrap failed from ${source}.`, error) - } finally { - mainUiBootstrapStarted = false - } -} - -function showFatalStartupError(){ - setTimeout(() => { - $('#loadingContainer').fadeOut(250, () => { - document.getElementById('overlayContainer').style.background = 'none' - setOverlayContent( - Lang.queryJS('uibinder.startup.fatalErrorTitle'), - Lang.queryJS('uibinder.startup.fatalErrorMessage'), - Lang.queryJS('uibinder.startup.closeButton') - ) - setOverlayHandler(() => { - const window = remote.getCurrentWindow() - window.close() - }) - toggleOverlay(true) - }) - }, 750) -} - -/** - * Common functions to perform after refreshing the distro index. - * - * @param {Object} data The distro index object. - */ -function onDistroRefresh(data){ - updateSelectedServer(data.getServerById(ConfigManager.getSelectedServer())) - refreshServerStatus() - syncModConfigurations(data) - ensureJavaSettings(data) -} - -/** - * Sync the mod configurations with the distro index. - * - * @param {Object} data The distro index object. - */ -function syncModConfigurations(data){ - - const syncedCfgs = [] - - for(let serv of data.servers){ - - const id = serv.rawServer.id - const mdls = serv.modules - const cfg = ConfigManager.getModConfiguration(id) - - if(cfg != null){ - - const modsOld = cfg.mods - const mods = {} - - for(let mdl of mdls){ - const type = mdl.rawModule.type - - if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ - if(!mdl.getRequired().value){ - const mdlID = mdl.getVersionlessMavenIdentifier() - if(modsOld[mdlID] == null){ - mods[mdlID] = scanOptionalSubModules(mdl.subModules, mdl) - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], scanOptionalSubModules(mdl.subModules, mdl), false) - } - } else { - if(mdl.subModules.length > 0){ - const mdlID = mdl.getVersionlessMavenIdentifier() - const v = scanOptionalSubModules(mdl.subModules, mdl) - if(typeof v === 'object'){ - if(modsOld[mdlID] == null){ - mods[mdlID] = v - } else { - mods[mdlID] = mergeModConfiguration(modsOld[mdlID], v, true) - } - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } else { - - const mods = {} - - for(let mdl of mdls){ - const type = mdl.rawModule.type - if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ - if(!mdl.getRequired().value){ - mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl) - } else { - if(mdl.subModules.length > 0){ - const v = scanOptionalSubModules(mdl.subModules, mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessMavenIdentifier()] = v - } - } - } - } - } - - syncedCfgs.push({ - id, - mods - }) - - } - } - - ConfigManager.setModConfigurations(syncedCfgs) - ConfigManager.save() -} - -/** - * Ensure java configurations are present for the available servers. - * - * @param {Object} data The distro index object. - */ -function ensureJavaSettings(data) { - - // Nothing too fancy for now. - for(const serv of data.servers){ - ConfigManager.ensureJavaConfig(serv.rawServer.id, serv.effectiveJavaOptions, serv.rawServer.javaOptions?.ram) - } - - ConfigManager.save() -} - -/** - * Recursively scan for optional sub modules. If none are found, - * this function returns a boolean. If optional sub modules do exist, - * a recursive configuration object is returned. - * - * @returns {boolean | Object} The resolved mod configuration. - */ -function scanOptionalSubModules(mdls, origin){ - if(mdls != null){ - const mods = {} - - for(let mdl of mdls){ - const type = mdl.rawModule.type - // Optional types. - if(type === Type.ForgeMod || type === Type.LiteMod || type === Type.LiteLoader || type === Type.FabricMod){ - // It is optional. - if(!mdl.getRequired().value){ - mods[mdl.getVersionlessMavenIdentifier()] = scanOptionalSubModules(mdl.subModules, mdl) - } else { - if(mdl.hasSubModules()){ - const v = scanOptionalSubModules(mdl.subModules, mdl) - if(typeof v === 'object'){ - mods[mdl.getVersionlessMavenIdentifier()] = v - } - } - } - } - } - - if(Object.keys(mods).length > 0){ - const ret = { - mods - } - if(!origin.getRequired().value){ - ret.value = origin.getRequired().def - } - return ret - } - } - return origin.getRequired().def -} - -/** - * Recursively merge an old configuration into a new configuration. - * - * @param {boolean | Object} o The old configuration value. - * @param {boolean | Object} n The new configuration value. - * @param {boolean} nReq If the new value is a required mod. - * - * @returns {boolean | Object} The merged configuration. - */ -function mergeModConfiguration(o, n, nReq = false){ - if(typeof o === 'boolean'){ - if(typeof n === 'boolean') return o - else if(typeof n === 'object'){ - if(!nReq){ - n.value = o - } - return n - } - } else if(typeof o === 'object'){ - if(typeof n === 'boolean') return typeof o.value !== 'undefined' ? o.value : true - else if(typeof n === 'object'){ - if(!nReq){ - n.value = typeof o.value !== 'undefined' ? o.value : true - } - - const newMods = Object.keys(n.mods) - for(let i=0; i 0 - ? Lang.queryJS('uibinder.validateAccount.failedMessage', { 'account': selectedAcc.displayName }) - : Lang.queryJS('uibinder.validateAccount.failedMessageSelectAnotherAccount', { 'account': selectedAcc.displayName }), - Lang.queryJS('uibinder.validateAccount.loginButton'), - Lang.queryJS('uibinder.validateAccount.selectAnotherAccountButton') - ) - setOverlayHandler(() => { - - const isMicrosoft = selectedAcc.type === 'microsoft' - - if(isMicrosoft) { - // Empty for now - } else { - // Mojang - // For convenience, pre-populate the username of the account. - document.getElementById('loginUsername').value = selectedAcc.username - validateEmail(selectedAcc.username) - } - - loginOptionsViewOnLoginSuccess = getCurrentView() - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - - if(accLen > 0) { - loginOptionsViewOnCancel = getCurrentView() - loginOptionsViewCancelHandler = () => { - if(isMicrosoft) { - ConfigManager.addMicrosoftAuthAccount( - selectedAcc.uuid, - selectedAcc.accessToken, - selectedAcc.username, - selectedAcc.expiresAt, - selectedAcc.microsoft.access_token, - selectedAcc.microsoft.refresh_token, - selectedAcc.microsoft.expires_at - ) - } else { - ConfigManager.addMojangAuthAccount(selectedAcc.uuid, selectedAcc.accessToken, selectedAcc.username, selectedAcc.displayName) - } - ConfigManager.save() - validateSelectedAccount() - } - loginOptionsCancelEnabled(true) - } else { - loginOptionsCancelEnabled(false) - } - toggleOverlay(false) - switchView(getCurrentView(), VIEWS.loginOptions) - }) - setDismissHandler(() => { - if(accLen > 1){ - prepareAccountSelectionList() - $('#overlayContent').fadeOut(250, () => { - bindOverlayKeys(true, 'accountSelectContent', true) - $('#accountSelectContent').fadeIn(250) - }) - } else { - const accountsObj = ConfigManager.getAuthAccounts() - const accounts = Array.from(Object.keys(accountsObj), v => accountsObj[v]) - // This function validates the account switch. - setSelectedAccount(accounts[0].uuid) - toggleOverlay(false) - } - }) - toggleOverlay(true, accLen > 0) - } else { - return true - } - } else { - return true - } -} - -/** - * Temporary function to update the selected account along - * with the relevent UI elements. - * - * @param {string} uuid The UUID of the account. - */ -function setSelectedAccount(uuid){ - const authAcc = ConfigManager.setSelectedAccount(uuid) - ConfigManager.save() - updateSelectedAccount(authAcc) - validateSelectedAccount() -} - -// Synchronous Listener -document.addEventListener('readystatechange', async () => { - - if (document.readyState === 'interactive' || document.readyState === 'complete'){ - if(rscShouldLoad){ - rscShouldLoad = false - if(!fatalStartupError){ - await bootstrapMainUI('readystatechange-event') - } else { - showFatalStartupError() - } - } else if(document.readyState === 'complete' && !fatalStartupError){ - setTimeout(() => { - bootstrapMainUI('readystatechange-fallback') - }, 0) - } - } - -}, false) - -// Actions that must be performed after the distribution index is downloaded. -ipcRenderer.on('distributionIndexDone', async (event, res) => { - if(res) { - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - await bootstrapMainUI('distributionIndexDone') - } else { - rscShouldLoad = true - } - } else { - fatalStartupError = true - if(document.readyState === 'interactive' || document.readyState === 'complete'){ - showFatalStartupError() - } else { - rscShouldLoad = true - } - } -}) - -// Util for development -async function devModeToggle() { - DistroAPI.toggleDevMode(true) - const data = await DistroAPI.refreshDistributionOrFallback() - ensureJavaSettings(data) - updateSelectedServer(data.servers[0]) - syncModConfigurations(data) -} diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js deleted file mode 100644 index 1c3a65f..0000000 --- a/app/assets/js/scripts/uicore.js +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Core UI functions are initialized in this file. This prevents - * unexpected errors from breaking the core features. Specifically, - * actions in this file should not require the usage of any internal - * modules, excluding dependencies. - */ -// Requirements -const $ = require('jquery') -const {ipcRenderer, shell, webFrame} = require('electron') -const remote = require('@electron/remote') -const isDev = require('./assets/js/isdev') -const { LoggerUtil } = require('helios-core') -const Lang = require('./assets/js/langloader') - -const loggerUICore = LoggerUtil.getLogger('UICore') -const loggerAutoUpdater = LoggerUtil.getLogger('AutoUpdater') - -// Log deprecation and process warnings. -process.traceProcessWarnings = true -process.traceDeprecation = true - -// Disable eval function. -window.eval = global.eval = function () { - throw new Error('Sorry, this app does not support window.eval().') -} - -// Display warning when devtools window is opened. -remote.getCurrentWebContents().on('devtools-opened', () => { - console.log('%cThe console is dark and full of terrors.', 'color: white; -webkit-text-stroke: 4px #a02d2a; font-size: 60px; font-weight: bold') - console.log('%cIf you\'ve been told to paste something here, you\'re being scammed.', 'font-size: 16px') - console.log('%cUnless you know exactly what you\'re doing, close this window.', 'font-size: 16px') -}) - -// Disable zoom, needed for darwin. -webFrame.setZoomLevel(0) -webFrame.setVisualZoomLevelLimits(1, 1) - -const BASE_WINDOW_WIDTH = 1400 -const BASE_WINDOW_HEIGHT = 860 -const UI_SCALE_MULTIPLIER = 1.5 - -let responsiveLayoutFrame = null -let lastAppliedZoomFactor = null -let lastLaunchContentWidth = 0 - -function clamp(value, min, max){ - return Math.min(Math.max(value, min), max) -} - -function syncLaunchDetailWidths(){ - const launchContent = document.getElementById('launch_content') - const launchDetails = document.getElementById('launch_details') - const launchButton = document.getElementById('launch_button') - const launchProgress = document.getElementById('launch_progress') - const launchDetailsRight = document.getElementById('launch_details_right') - const launchProgressLabel = document.getElementById('launch_progress_label') - - if(!launchContent || !launchDetails || !launchButton || !launchProgress || !launchDetailsRight || !launchProgressLabel){ - return - } - - const measuredLaunchContentWidth = Math.ceil(launchContent.getBoundingClientRect().width) - if(measuredLaunchContentWidth > 0){ - lastLaunchContentWidth = measuredLaunchContentWidth - } - - const launchContentWidth = lastLaunchContentWidth > 0 - ? lastLaunchContentWidth - : Math.ceil(launchDetails.parentElement?.getBoundingClientRect().width ?? 0) - - if(launchContentWidth <= 0){ - return - } - - const launchButtonWidth = launchButton.getBoundingClientRect().width - const labelWidth = Math.max(64, Math.ceil(launchButtonWidth * 0.48)) - const progressWidth = Math.max(220, Math.floor(launchContentWidth - labelWidth - 48)) - - launchDetails.style.width = `${Math.ceil(launchContentWidth)}px` - launchDetails.style.maxWidth = `${Math.ceil(launchContentWidth)}px` - launchProgress.style.width = `${progressWidth}px` - launchDetailsRight.style.width = `${progressWidth}px` - launchDetailsRight.style.maxWidth = `${progressWidth}px` - launchProgressLabel.style.width = `${labelWidth}px` - launchProgressLabel.style.minWidth = `${labelWidth}px` - launchProgressLabel.style.maxWidth = `${labelWidth}px` -} - -window.syncLaunchDetailWidths = syncLaunchDetailWidths - -function applyResponsiveLayout(){ - const currentWindow = remote.getCurrentWindow() - const contentBounds = currentWindow.getContentBounds() - const scale = clamp( - Math.min(contentBounds.width / BASE_WINDOW_WIDTH, contentBounds.height / BASE_WINDOW_HEIGHT) * UI_SCALE_MULTIPLIER, - 0.72, - 2 - ) - - if(lastAppliedZoomFactor == null || Math.abs(lastAppliedZoomFactor - scale) > 0.001){ - lastAppliedZoomFactor = scale - webFrame.setZoomFactor(scale) - } - document.documentElement.style.setProperty('--launcher-scale', scale.toFixed(3)) - - window.requestAnimationFrame(() => { - syncLaunchDetailWidths() - }) -} - -function queueResponsiveLayout(){ - if(responsiveLayoutFrame != null){ - cancelAnimationFrame(responsiveLayoutFrame) - } - responsiveLayoutFrame = requestAnimationFrame(() => { - responsiveLayoutFrame = null - applyResponsiveLayout() - }) -} - -const responsiveWindow = remote.getCurrentWindow() -responsiveWindow.on('resize', queueResponsiveLayout) -responsiveWindow.on('maximize', queueResponsiveLayout) -responsiveWindow.on('unmaximize', queueResponsiveLayout) - -// Initialize auto updates in production environments. -let updateCheckListener -if(!isDev){ - ipcRenderer.on('autoUpdateNotification', (event, arg, info) => { - switch(arg){ - case 'checking-for-update': - loggerAutoUpdater.info('Checking for update..') - settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkingForUpdateButton'), true) - break - case 'update-available': - loggerAutoUpdater.info('New update available', info.version) - - if(process.platform === 'darwin'){ - info.darwindownload = `https://github.com/peunsu/MRSLauncher/releases/download/v${info.version}/MRS-Launcher-setup-${info.version}${process.arch === 'arm64' ? '-arm64' : '-x64'}.dmg` - showUpdateUI(info) - } - - populateSettingsUpdateInformation(info) - break - case 'update-downloaded': - loggerAutoUpdater.info('Update ' + info.version + ' ready to be installed.') - settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.installNowButton'), false, () => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } - }) - showUpdateUI(info) - break - case 'update-not-available': - loggerAutoUpdater.info('No new update found.') - settingsUpdateButtonStatus(Lang.queryJS('uicore.autoUpdate.checkForUpdatesButton')) - break - case 'ready': - updateCheckListener = setInterval(() => { - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - }, 1800000) - ipcRenderer.send('autoUpdateAction', 'checkForUpdate') - break - case 'realerror': - if(info != null && info.code != null){ - if(info.code === 'ERR_UPDATER_INVALID_RELEASE_FEED'){ - loggerAutoUpdater.info('No suitable releases found.') - } else if(info.code === 'ERR_XML_MISSED_ELEMENT'){ - loggerAutoUpdater.info('No releases found.') - } else { - loggerAutoUpdater.error('Error during update check..', info) - loggerAutoUpdater.debug('Error Code:', info.code) - } - } - break - default: - loggerAutoUpdater.info('Unknown argument', arg) - break - } - }) -} - -/** - * Send a notification to the main process changing the value of - * allowPrerelease. If we are running a prerelease version, then - * this will always be set to true, regardless of the current value - * of val. - * - * @param {boolean} val The new allow prerelease value. - */ -function changeAllowPrerelease(val){ - ipcRenderer.send('autoUpdateAction', 'allowPrereleaseChange', val) -} - -function showUpdateUI(info){ - //TODO Make this message a bit more informative `${info.version}` - document.getElementById('image_seal_container').setAttribute('update', true) - document.getElementById('image_seal_container').onclick = () => { - /*setOverlayContent('Update Available', 'A new update for the launcher is available. Would you like to install now?', 'Install', 'Later') - setOverlayHandler(() => { - if(!isDev){ - ipcRenderer.send('autoUpdateAction', 'installUpdateNow') - } else { - console.error('Cannot install updates in development environment.') - toggleOverlay(false) - } - }) - setDismissHandler(() => { - toggleOverlay(false) - }) - toggleOverlay(true, true)*/ - switchView(getCurrentView(), VIEWS.settings, 500, 500, () => { - settingsNavItemListener(document.getElementById('settingsNavUpdate'), false) - }) - } -} - -/* jQuery Example -$(function(){ - loggerUICore.info('UICore Initialized'); -})*/ - -document.addEventListener('readystatechange', function () { - if (document.readyState === 'interactive'){ - loggerUICore.info('UICore Initializing..') - - // Bind close button. - Array.from(document.getElementsByClassName('fCb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.close() - }) - }) - - // Bind restore down button. - Array.from(document.getElementsByClassName('fRb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - if(window.isMaximized()){ - window.unmaximize() - } else { - window.maximize() - } - document.activeElement.blur() - }) - }) - - // Bind minimize button. - Array.from(document.getElementsByClassName('fMb')).map((val) => { - val.addEventListener('click', e => { - const window = remote.getCurrentWindow() - window.minimize() - document.activeElement.blur() - }) - }) - - // Remove focus from social media buttons once they're clicked. - Array.from(document.getElementsByClassName('mediaURL')).map(val => { - val.addEventListener('click', e => { - document.activeElement.blur() - }) - }) - - } else if(document.readyState === 'complete'){ - queueResponsiveLayout() - setTimeout(() => { - queueResponsiveLayout() - }, 150) - } - -}, false) - -/** - * Open web links in the user's default browser. - */ -$(document).on('click', 'a[href^="http"]', function(event) { - event.preventDefault() - shell.openExternal(this.href) -}) - -/** - * Opens DevTools window if you hold (ctrl + shift + i). - * This will crash the program if you are using multiple - * DevTools, for example the chrome debugger in VS Code. - */ -document.addEventListener('keydown', function (e) { - if((e.key === 'I' || e.key === 'i') && e.ctrlKey && e.shiftKey){ - let window = remote.getCurrentWindow() - window.toggleDevTools() - } -}) diff --git a/app/assets/js/scripts/welcome.js b/app/assets/js/scripts/welcome.js deleted file mode 100644 index 1f3df6d..0000000 --- a/app/assets/js/scripts/welcome.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Script for welcome.ejs - */ -document.getElementById('welcomeButton').addEventListener('click', e => { - loginOptionsCancelEnabled(false) // False by default, be explicit. - loginOptionsViewOnLoginSuccess = VIEWS.landing - loginOptionsViewOnLoginCancel = VIEWS.loginOptions - switchView(VIEWS.welcome, VIEWS.loginOptions) -}) diff --git a/app/assets/js/serverruntime.js b/app/assets/js/serverruntime.js deleted file mode 100644 index 7178fa8..0000000 --- a/app/assets/js/serverruntime.js +++ /dev/null @@ -1,219 +0,0 @@ -const childProcess = require('child_process') -const fs = require('fs-extra') -const path = require('path') - -const ConfigManager = require('./configmanager') -const PortManager = require('./portmanager') -const ProfileAssetManager = require('./profileassetmanager') - -const runtimes = new Map() -const READY_PATTERNS = [ - /Done \([0-9.]+s\)!/i, - /For help, type "help"/i -] - -function getRuntime(profileId){ - if(!runtimes.has(profileId)){ - runtimes.set(profileId, { - serverProcess: null, - logs: [], - status: 'stopped', - ready: false, - readyPromise: null, - readyResolver: null, - readyRejecter: null - }) - } - - return runtimes.get(profileId) -} - -function appendLog(runtime, line){ - runtime.logs.push(line) - if(runtime.logs.length > 200){ - runtime.logs.shift() - } -} - -function createReadyPromise(runtime){ - runtime.ready = false - runtime.readyPromise = new Promise((resolve, reject) => { - runtime.readyResolver = resolve - runtime.readyRejecter = reject - }) - return runtime.readyPromise -} - -function resolveReady(runtime){ - if(runtime.ready === true){ - return - } - - runtime.ready = true - runtime.status = 'running' - if(runtime.readyResolver){ - runtime.readyResolver(runtime) - } - runtime.readyResolver = null - runtime.readyRejecter = null -} - -function rejectReady(runtime, error){ - runtime.ready = false - if(runtime.readyRejecter){ - runtime.readyRejecter(error) - } - runtime.readyResolver = null - runtime.readyRejecter = null -} - -function resolveServerJavaExecutable(){ - const selectedServerId = ConfigManager.getSelectedServer() - if(selectedServerId != null){ - const configured = ConfigManager.getJavaExecutable(selectedServerId) - if(configured){ - return configured - } - } - - return 'java' -} - -async function writeServerFiles(profile, serverDirectory){ - await fs.writeFile(path.join(serverDirectory, 'eula.txt'), 'eula=true\n', 'utf8') - - const properties = [ - ['enable-query', 'false'], - ['max-players', String(profile.serverMaxPlayers ?? 20)], - ['online-mode', 'true'], - ['server-port', String(profile.serverPort ?? 25565)], - ['white-list', profile.serverWhitelistEnabled ? 'true' : 'false'], - ['level-name', 'world'] - ] - - await fs.writeFile( - path.join(serverDirectory, 'server.properties'), - properties.map(([key, value]) => `${key}=${value}`).join('\n') + '\n', - 'utf8' - ) -} - -function buildLaunchCommand(profile, javaExecutable){ - const memoryMb = Number.isFinite(Number(profile.serverMemoryMb)) - ? Math.max(512, Number(profile.serverMemoryMb)) - : 4096 - - return `"${javaExecutable}" -Xms${memoryMb}M -Xmx${memoryMb}M -jar server.jar nogui` -} - -exports.startHostedProfile = async function(profile){ - if(profile.serverEnabled !== true){ - throw new Error('서버 사용이 꺼진 프로필입니다.') - } - - const runtime = getRuntime(profile.id) - if(runtime.serverProcess != null){ - if(runtime.readyPromise != null){ - await runtime.readyPromise.catch(() => {}) - } - return runtime - } - - const installedServer = await ProfileAssetManager.ensureServerJarInstalled(profile) - if(installedServer?.serverJarPath == null){ - throw new Error('로컬 서버를 시작하려면 버킷 JAR 업로드가 필요합니다.') - } - - await ProfileAssetManager.ensureServerWorldInstalled(profile, installedServer.serverDirectory) - await writeServerFiles(profile, installedServer.serverDirectory) - await PortManager.ensurePortAvailability(profile) - - const command = buildLaunchCommand(profile, resolveServerJavaExecutable()) - appendLog(runtime, `[launcher] starting server: ${command}`) - - runtime.status = 'starting' - createReadyPromise(runtime) - - const serverProcess = childProcess.spawn(command, { - cwd: installedServer.serverDirectory, - shell: true, - detached: false - }) - - runtime.serverProcess = serverProcess - - const readyFallbackTimer = setTimeout(() => { - if(runtime.serverProcess != null){ - resolveReady(runtime) - } - }, 25000) - - serverProcess.stdout?.on('data', (chunk) => { - chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => { - appendLog(runtime, `[server] ${line}`) - if(READY_PATTERNS.some((pattern) => pattern.test(line))){ - resolveReady(runtime) - } - }) - }) - - serverProcess.stderr?.on('data', (chunk) => { - chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => { - appendLog(runtime, `[server:err] ${line}`) - }) - }) - - serverProcess.on('close', () => { - clearTimeout(readyFallbackTimer) - runtime.serverProcess = null - runtime.status = 'stopped' - runtime.ready = false - rejectReady(runtime, new Error('서버 프로세스가 종료되었습니다.')) - PortManager.releaseProfilePort(profile.id).catch(() => {}) - }) - - await runtime.readyPromise.catch(() => {}) - return runtime -} - -exports.stopHostedProfile = async function(profileId){ - const runtime = getRuntime(profileId) - - if(runtime.serverProcess != null){ - runtime.status = 'stopping' - try { - runtime.serverProcess.stdin?.write('stop\n') - } catch (error) { - void error - } - - setTimeout(() => { - if(runtime.serverProcess != null){ - runtime.serverProcess.kill() - } - }, 5000) - } - - runtime.status = 'stopped' - runtime.ready = false - await PortManager.releaseProfilePort(profileId).catch(() => {}) -} - -exports.getHostedProfileState = function(profileId){ - const runtime = getRuntime(profileId) - return { - status: runtime.status, - running: runtime.serverProcess != null, - ready: runtime.ready, - logs: [...runtime.logs] - } -} - -exports.hasRunningProfiles = function(){ - for(const runtime of runtimes.values()){ - if(runtime.serverProcess != null){ - return true - } - } - return false -} diff --git a/app/assets/js/serverstatus.js b/app/assets/js/serverstatus.js deleted file mode 100644 index 9729f9c..0000000 --- a/app/assets/js/serverstatus.js +++ /dev/null @@ -1,65 +0,0 @@ -const net = require('net') - -/** - * Retrieves the status of a minecraft server. - * - * @param {string} address The server address. - * @param {number} port Optional. The port of the server. Defaults to 25565. - * @returns {Promise.} A promise which resolves to an object containing - * status information. - */ -exports.getStatus = function(address, port = 25565){ - - if(port == null || port == ''){ - port = 25565 - } - if(typeof port === 'string'){ - port = parseInt(port) - } - - return new Promise((resolve, reject) => { - const socket = net.connect(port, address, () => { - let buff = Buffer.from([0xFE, 0x01]) - socket.write(buff) - }) - - socket.setTimeout(2500, () => { - socket.end() - reject({ - code: 'ETIMEDOUT', - errno: 'ETIMEDOUT', - address, - port - }) - }) - - socket.on('data', (data) => { - if(data != null && data != ''){ - let server_info = data.toString().split('\x00\x00\x00') - const NUM_FIELDS = 6 - if(server_info != null && server_info.length >= NUM_FIELDS){ - resolve({ - online: true, - version: server_info[2].replace(/\u0000/g, ''), - motd: server_info[3].replace(/\u0000/g, ''), - onlinePlayers: server_info[4].replace(/\u0000/g, ''), - maxPlayers: server_info[5].replace(/\u0000/g,'') - }) - } else { - resolve({ - online: false - }) - } - } - socket.end() - }) - - socket.on('error', (err) => { - socket.destroy() - reject(err) - // ENOTFOUND = Unable to resolve. - // ECONNREFUSED = Unable to connect to port. - }) - }) - -} \ No newline at end of file diff --git a/app/assets/js/vanillapatch.js b/app/assets/js/vanillapatch.js deleted file mode 100644 index ed335b0..0000000 --- a/app/assets/js/vanillapatch.js +++ /dev/null @@ -1,23 +0,0 @@ -const { DistributionIndexProcessor } = require('helios-core/dl') -const { Type } = require('helios-distribution-types') - -if(!DistributionIndexProcessor.prototype.__mrsVanillaPatchApplied){ - const originalLoadModLoaderVersionJson = DistributionIndexProcessor.prototype.loadModLoaderVersionJson - - DistributionIndexProcessor.prototype.loadModLoaderVersionJson = async function(...args){ - const server = this.distribution?.getServerById?.(this.serverId) - const hasModLoader = server?.modules?.some(({ rawModule }) => { - return rawModule?.type === Type.ForgeHosted - || rawModule?.type === Type.Forge - || rawModule?.type === Type.Fabric - }) === true - - if(!hasModLoader){ - return null - } - - return originalLoadModLoaderVersionJson.apply(this, args) - } - - DistributionIndexProcessor.prototype.__mrsVanillaPatchApplied = true -} diff --git a/app/assets/lang/_custom.toml b/app/assets/lang/_custom.toml deleted file mode 100644 index ca4ab89..0000000 --- a/app/assets/lang/_custom.toml +++ /dev/null @@ -1,47 +0,0 @@ -# Custom Language File for Launcher Customizer - -[ejs.app] -title = "MRS Launcher" - -[ejs.landing] -mediaHomeURL = "https://mysticred.space" -mediaLinkURL = "#" -mediaMapURL = "https://map.mysticred.space" -mediaGitHubURL = "https://github.com/peunsu/MRSLauncher" -mediaXURL = "#" -mediaInstagramURL = "#" -mediaYouTubeURL = "#" -mediaDiscordURL = "https://discord.gg/Z8j6ahF4MJ" -accountPreviewLabel = "계정" -libraryButton = "라이브러리" -installButton = "설치" -launchButtonPlaceholder = "• 라이브러리에서 선택" - -[ejs.settings] -backButton = "메인으로" -sourceGithubLink = "https://github.com/peunsu/MRSLauncher" -supportLink = "https://github.com/peunsu/MRSLauncher/issues" - -[ejs.library] -eyebrow = "Library" -title = "내 라이브러리" -subtitle = "설치한 프로필을 선택하고 바로 실행하거나, 서버 주소를 입력해 자동 접속을 준비할 수 있습니다." -backButton = "메인으로" -settingsButton = "설정" -installPageButton = "설치 페이지" -launchPageButton = "실행 화면" -emptyTitle = "설치된 프로필이 없습니다." -emptyDescription = "설치 페이지에서 네가 배포한 모드팩이나 서버 클라이언트를 먼저 추가하세요." - -[ejs.install] -eyebrow = "Install" -title = "설치 페이지" -subtitle = "관리자가 미리 등록한 프로필을 둘러보고, 설명과 상세 내용을 확인한 뒤 내 라이브러리에 추가합니다." -backButton = "메인으로" -libraryPageButton = "라이브러리" -notice = "설치 페이지는 읽기 전용 카탈로그입니다. 프로필 제목, 요약, 상세 설명은 관리자가 미리 등록하며, 클라이언트는 라이브러리에 추가만 할 수 있습니다." - -[ejs.welcome] -welcomeHeader = "Mystic Red Space" -welcomeDescription = "2017년, 작은 모드팩 서버로 시작한 Mystic Red Space는 오랜 시간동안 모드팩 유저의 사랑을 받으면서 현재까지 수십 개 이상의 모드팩 서버를 제공하였습니다. 이제 MRS는 단순한 마인크래프트 모드팩 서버를 넘어서, 더 많은 사람들이 마인크래프트 모드 정보를 쉽게 접하고 모드팩에 대한 관심을 키울 수 있는 모드 커뮤니티로 한 걸음 나아가고 있습니다." -welcomeDescCTA = "모드팩을 사랑하는 당신을 위한 최고의 선물이 되겠습니다." diff --git a/app/assets/lang/en_US.toml b/app/assets/lang/en_US.toml deleted file mode 100644 index 291f6bf..0000000 --- a/app/assets/lang/en_US.toml +++ /dev/null @@ -1,373 +0,0 @@ -[ejs.landing] -updateAvailableTooltip = "Update Available" -usernamePlaceholder = "Username" -usernameEditButton = "Edit" -accountMenuLogout = "Log Out" -settingsTooltip = "Settings" -serverStatus = "SERVER" -serverStatusPlaceholder = "OFFLINE" -mojangStatus = "MOJANG STATUS" -mojangStatusTooltipTitle = "Services" -mojangStatusNETitle = "Non Essential" -newsButton = "NEWS" -launchButton = "PLAY" -launchButtonPlaceholder = "• No Server Selected" -launchDetails = "Please wait.." -newsNavigationStatus = "{currentPage} of {totalPages}" -newsErrorLoadSpan = "Checking for News.." -newsErrorFailedSpan = "Failed to Load News" -newsErrorRetryButton = "Try Again" -newsErrorNoneSpan = "No News" - -[ejs.login] -loginCancelText = "Cancel" -loginSubheader = "MINECRAFT LOGIN" -loginEmailError = "* Invalid Value" -loginEmailPlaceholder = "EMAIL OR USERNAME" -loginPasswordError = "* Required" -loginPasswordPlaceholder = "PASSWORD" -loginForgotPasswordLink = "https://minecraft.net/password/forgot/" -loginForgotPasswordText = "forgot password?" -loginRememberMeText = "remember me?" -loginButtonText = "LOGIN" -loginNeedAccountLink = "https://minecraft.net/store/minecraft-java-edition/" -loginNeedAccountText = "Need an Account?" -loginPasswordDisclaimer1 = "Your password is sent directly to mojang and never stored." -loginPasswordDisclaimer2 = "{appName} is not affiliated with Mojang AB." - -[ejs.loginOptions] -loginOptionsTitle = "Login Options" -loginWithMicrosoft = "Login with Microsoft" -loginWithMojang = "Login with Mojang" -cancelButton = "Cancel" - -[ejs.overlay] -serverSelectHeader = "Available Servers" -serverSelectConfirm = "Select" -serverSelectCancel = "Cancel" -accountSelectHeader = "Select an Account" -accountSelectConfirm = "Select" -accountSelectCancel = "Cancel" - -[ejs.settings] -navHeaderText = "Settings" -navAccount = "Account" -navMinecraft = "Minecraft" -navMods = "Mods" -navJava = "Java" -navLauncher = "Launcher" -navAbout = "About" -navUpdates = "Updates" -navDone = "Done" -tabAccountHeaderText = "Account Settings" -tabAccountHeaderDesc = "Add new accounts or manage existing ones." -microsoftAccount = "Microsoft" -addMicrosoftAccount = "+ Add Microsoft Account" -mojangAccount = "Mojang" -addMojangAccount = "+ Add Mojang Account" -minecraftTabHeaderText = "Minecraft Settings" -minecraftTabHeaderDesc = "Options related to game launch." -gameResolutionTitle = "Game Resolution" -launchFullscreenTitle = "Launch in fullscreen." -autoConnectTitle = "Automatically connect to the server on launch." -launchDetachedTitle = "Launch game process detached from launcher." -launchDetachedDesc = "If the game is not detached, closing the launcher will also close the game." -tabModsHeaderText = "Mod Settings" -tabModsHeaderDesc = "Enable or disable mods." -switchServerButton = "Switch" -requiredMods = "Required Mods" -optionalMods = "Optional Mods" -dropinMods = "Drop-in Mods" -addMods = "Add Mods" -dropinRefreshNote = "(F5 to Refresh)" -shaderpacks = "Shaderpacks" -shaderpackDesc = "Enable or disable shaders. Please note, shaders will only run smoothly on powerful setups. You may add custom packs here." -selectShaderpack = "Select Shaderpack" -tabJavaHeaderText = "Java Settings" -tabJavaHeaderDesc = "Manage the Java configuration (advanced)." -memoryTitle = "Memory" -maxRAM = "Maximum RAM" -minRAM = "Minimum RAM" -memoryDesc = "The recommended minimum RAM is 3 gigabytes. Setting the minimum and maximum values to the same value may reduce lag." -memoryTotalTitle = "Total" -memoryAvailableTitle = "Available" -javaExecutableTitle = "Java Executable" -javaExecSelDialogTitle = "Select Java Executable" -javaExecSelButtonText = "Choose File" -javaExecDesc = "The Java executable is validated before game launch." -javaPathDesc = "The path should end with {pathSuffix}." -jvmOptsTitle = "Additional JVM Options" -jvmOptsDesc = "Options to be provided to the JVM at runtime. -Xms and -Xmx should not be included." -launcherTabHeaderText = "Launcher Settings" -launcherTabHeaderDesc = "Options related to the launcher itself." -allowPrereleaseTitle = "Allow Pre-Release Updates." -allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.
This will always be true if you are using a pre-release version." -catalogSourceTitle = "Catalog URL" -catalogSourcePlaceholder = "https://your-domain.example/catalog.json" -catalogSourceDesc = "Leave blank to use the automatic default catalog source.
For remote operation, enter the catalog.json URL served by the admin site." -dataDirectoryTitle = "Data Directory" -selectDataDirectory = "Select Data Directory" -chooseFolder = "Choose Folder" -dataDirectoryDesc = "All game files and local Java installations will be stored in the data directory.
Screenshots and world saves are stored in the instance folder for the corresponding server configuration." -aboutTabHeaderText = "About" -aboutTabHeaderDesc = "View information and release notes for the current version." -aboutTitle = "{appName}" -stableRelease = "Stable Release" -versionText = "Version " -sourceGithub = "GitHub" -sourceOriginalGithub = "Original GitHub" -support = "Support" -devToolsConsole = "DevTools Console" -releaseNotes = "Release Notes" -changelog = "Changelog" -noReleaseNotes = "No Release Notes" -viewReleaseNotes = "View Release Notes on GitHub" -launcherUpdatesHeaderText = "Launcher Updates" -launcherUpdatesHeaderDesc = "Download, install, and review updates for the launcher." -checkForUpdates = "Check for Updates" -whatsNew = "What's New" -updateReleaseNotes = "Update Release Notes" - -[ejs.waiting] -waitingText = "Waiting for Microsoft.." - -[ejs.welcome] -continueButton = "CONTINUE" - - -[js.discord] -waiting = "Waiting for Client.." -state = "Server: {shortId}" - -[js.index] -microsoftLoginTitle = "Microsoft Login" -microsoftLogoutTitle = "Microsoft Logout" - -[js.login] -login = "LOGIN" -loggingIn = "LOGGING IN" -success = "SUCCESS" -tryAgain = "Try Again" - -[js.login.error] -invalidValue = "* Invalid Value" -requiredValue = "* Required" - -[js.login.error.unknown] -title = "Unknown Error During Login" -desc = "An unknown error has occurred. Please see the console for details." - -[js.landing.launch] -pleaseWait = "Please wait.." -failureTitle = "Error During Launch" -failureText = "See console (CTRL + Shift + i) for more details." -okay = "Okay" - -[js.landing.selectedAccount] -noAccountSelected = "No Account Selected" -logoutFailed = "Failed to log out of the selected account. Please try again." - -[js.landing.selectedProfile] -noSelection = "Select from Library" -loading = "Loading profile.." - -[js.landing.profileStatus] -label = "PROFILE" - -[js.landing.mapStatus] -label = "STATUS" -ready = "READY TO RUN" -notReady = "SETUP REQUIRED" - -[js.landing.selectedServer] -noSelection = "No Server Selected" -loading = "Loading.." - -[js.landing.serverStatus] -server = "SERVER" -offline = "OFFLINE" -players = "PLAYERS" - -[js.landing.portStatus] -label = "PORT" -failed = "AUTO OPEN FAILED" - -[js.landing.localServer] -starting = "Starting local server.." -missingJarTitle = "Local Server Setup Incomplete" -missingJarText = "The launch flow tried to start a local server because no connection address was set, but no bucket JAR is registered for this profile. Upload a server JAR in the admin site or enter a connection address in the library." -failureTitle = "Local Server Start Failed" -failureText = "Could not start the local server. Check the console (CTRL + Shift + i) for details." - -[js.landing.systemScan] -checking = "Checking system info.." -noCompatibleJava = "No Compatible
Java Installation Found" -installJavaMessage = "In order to launch Minecraft, you need a 64-bit installation of Java {major}. Would you like us to install a copy?" -installJava = "Install Java" -installJavaManually = "Install Manually" -javaDownloadPrepare = "Preparing Java Download.." -javaDownloadFailureTitle = "Error During Java Download" -javaDownloadFailureText = "See console (CTRL + Shift + i) for more details." -javaRequired = "Java is Required
to Launch" -javaRequiredMessage = 'A valid x64 installation of Java {major} is required to launch.

Please refer to our Java Management Guide for instructions on how to manually install Java.' -javaRequiredDismiss = "I Understand" -javaRequiredCancel = "Go Back" - -[js.landing.downloadJava] -findJdkFailure = "Failed to find OpenJDK distribution." -javaDownloadCorruptedError = "Downloaded JDK has a bad hash, the file may be corrupted." -extractingJava = "Extracting Java" -javaInstalled = "Java Installed!" - -[js.landing.dlAsync] -loadingServerInfo = "Loading server information.." -fatalError = "Fatal Error" -unableToLoadDistributionIndex = "Could not load a copy of the distribution index. See the console (CTRL + Shift + i) for more details." -pleaseWait = "Please wait.." -errorDuringLaunchTitle = "Error During Launch" -seeConsoleForDetails = "See console (CTRL + Shift + i) for more details." -validatingFileIntegrity = "Validating file integrity.." -errorDuringFileVerificationTitle = "Error During File Verification" -downloadingFiles = "Downloading files.." -errorDuringFileDownloadTitle = "Error During File Download" -preparingToLaunch = "Preparing to launch.." -launchingGame = "Launching game.." -launchWrapperNotDownloaded = "The main file, LaunchWrapper, failed to download properly. As a result, the game cannot launch.

To fix this issue, temporarily turn off your antivirus software and launch the game again.

If you have time, please submit an issue and let us know what antivirus software you use. We'll contact them and try to straighten things out." -doneEnjoyServer = "Done. Enjoy the server!" -checkConsoleForDetails = "Please check the console (CTRL + Shift + i) for more details." - -[js.landing.news] -checking = "Checking for News" - -[js.landing.discord] -loading = "Loading game.." -joining = "Sailing to Westeros!" -joined = "Exploring the Realm!" - -[js.overlay] -dismiss = "Dismiss" - -[js.settings.fileSelectors] -executables = "Executables" -allFiles = "All Files" - -[js.settings.mstfLogin] -errorTitle = "Something Went Wrong" -errorMessage = "Microsoft authentication failed. Please try again." -okButton = "OK" - -[js.settings.mstfLogout] -errorTitle = "Something Went Wrong" -errorMessage = "Microsoft logout failed. Please try again." -okButton = "OK" - -[js.settings.authAccountSelect] -selectButton = "Select Account" -selectedButton = "Selected Account ✔" - -[js.settings.authAccountLogout] -lastAccountWarningTitle = "Warning
This is Your Last Account" -lastAccountWarningMessage = "In order to use the launcher you must be logged into at least one account. You will need to login again after.

Are you sure you want to log out?" -confirmButton = "I'm Sure" -cancelButton = "Cancel" - -[js.settings.authAccountPopulate] -username = "Username" -uuid = "UUID" -selectAccount = "Select Account" -selectedAccount = "Selected Account ✓" -logout = "Log Out" - -[js.settings.dropinMods] -removeButton = "Remove" -deleteFailedTitle = "Failed to Delete
Drop-in Mod {fullName}" -deleteFailedMessage = "Make sure the file is not in use and try again." -failedToggleTitle = "Failed to Toggle
One or More Drop-in Mods" -okButton = "Okay" - -[js.settings.serverListing] -mainServer = "Main Server" - -[js.settings.java] -selectedJava = "Selected: Java {version} ({vendor})" -invalidSelection = "Invalid Selection" -requiresJava = "Requires Java {major} x64." -availableOptions = "Available Options for Java {major} (HotSpot VM)" - -[js.settings.about] -preReleaseTitle = "Pre-release" -stableReleaseTitle = "Stable Release" -releaseNotesFailed = "Failed to load release notes." - -[js.settings.updates] -newReleaseTitle = "New Release Available" -newPreReleaseTitle = "New Pre-release Available" -downloadingButton = "Downloading.." -downloadButton = 'Download from GitHubClose the launcher and run the dmg to update.' -latestVersionTitle = "You Are Running the Latest Version" -checkForUpdatesButton = "Check for Updates" -checkingForUpdatesButton = "Checking for Updates.." - -[js.settings.msftLogin] -errorTitle = "Microsoft Login Failed" -errorMessage = "We were unable to authenticate your Microsoft account. Please try again." -okButton = "OK" - -[js.uibinder.startup] -fatalErrorTitle = "Fatal Error: Unable to Load Distribution Index" -fatalErrorMessage = "A connection could not be established to our servers to download the distribution index. No local copies were available to load.

The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. Ensure you are connected to the internet and relaunch the application." -closeButton = "Close" - -[js.uibinder.validateAccount] -failedMessageTitle = "Failed to Refresh Login" -failedMessage = "We were unable to refresh the login for {account}. Please select another account or login again." -failedMessageSelectAnotherAccount = "We were unable to refresh the login for {account}. Please login again." -loginButton = "Login" -selectAnotherAccountButton = "Select Another Account" - -[js.uicore.autoUpdate] -checkingForUpdateButton = "Checking for Updates..." -installNowButton = "Install Now" -checkForUpdatesButton = "Check for Updates" - -[js.auth.microsoft.error] -noProfileTitle = "Error During Login:
Profile Not Set Up" -noProfileDesc = "Your Microsoft account does not yet have a Minecraft profile set up. If you have recently purchased the game or redeemed it through Xbox Game Pass, you have to set up your profile on Minecraft.net.

If you have not yet purchased the game, you can also do that on Minecraft.net." -noXboxAccountTitle = "Error During Login:
No Xbox Account" -noXboxAccountDesc = "Your Microsoft account has no Xbox account associated with it." -xblBannedTitle = "Error During Login:
Xbox Live Unavailable" -xblBannedDesc = "Your Microsoft account is from a country where Xbox Live is not available or banned." -under18Title = "Error During Login:
Parental Approval Required" -under18Desc = "Accounts for users under the age of 18 must be added to a Family by an adult." -unknownTitle = "Unknown Error During Login" -unknownDesc = "An unknown error has occurred. Please see the console for details." - -[js.auth.mojang.error] -methodNotAllowedTitle = "Internal Error:
Method Not Allowed" -methodNotAllowedDesc = "Method not allowed. Please report this error." -notFoundTitle = "Internal Error:
Not Found" -notFoundDesc = "The authentication endpoint was not found. Please report this issue." -accountMigratedTitle = "Error During Login:
Account Migrated" -accountMigratedDesc = "You've attempted to login with a migrated account. Try again using the account email as the username." -invalidCredentialsTitle = "Error During Login:
Invalid Credentials" -invalidCredentialsDesc = "The email or password you've entered is incorrect. Please try again." -tooManyAttemptsTitle = "Error During Login:
Too Many Attempts" -tooManyAttemptsDesc = "There have been too many login attempts with this account recently. Please try again later." -invalidTokenTitle = "Error During Login:
Invalid Token" -invalidTokenDesc = "The provided access token is invalid." -tokenHasProfileTitle = "Error During Login:
Token Has Profile" -tokenHasProfileDesc = "Access token already has a profile assigned. Selecting profiles is not implemented yet." -credentialsMissingTitle = "Error During Login:
Credentials Missing" -credentialsMissingDesc = "Username/password was not submitted or password is less than 3 characters." -invalidSaltVersionTitle = "Error During Login:
Invalid Salt Version" -invalidSaltVersionDesc = "Invalid salt version." -unsupportedMediaTypeTitle = "Internal Error:
Unsupported Media Type" -unsupportedMediaTypeDesc = "Unsupported media type. Please report this error." -accountGoneTitle = "Error During Login:
Account Migrated" -accountGoneDesc = "Account has been migrated to a Microsoft account. Please log in with Microsoft." -unreachableTitle = "Error During Login:
Unreachable" -unreachableDesc = "Unable to reach the authentication servers. Ensure that they are online and you are connected to the internet." -gameNotPurchasedTitle = "Error During Login:
Game Not Purchased" -gameNotPurchasedDesc = "The account you are trying to login with has not purchased a copy of Minecraft. You may purchase a copy on Minecraft.net" -unknownErrorTitle = "Unknown Error During Login" -unknownErrorDesc = "An unknown error has occurred. Please see the console for details." diff --git a/app/assets/lang/ko_KR.toml b/app/assets/lang/ko_KR.toml deleted file mode 100644 index 9ce36d5..0000000 --- a/app/assets/lang/ko_KR.toml +++ /dev/null @@ -1,373 +0,0 @@ -[ejs.landing] -updateAvailableTooltip = "업데이트 가능" -usernamePlaceholder = "사용자 이름" -usernameEditButton = "편집" -accountMenuLogout = "로그아웃" -settingsTooltip = "설정" -serverStatus = "서버" -serverStatusPlaceholder = "오프라인" -mojangStatus = "MOJANG 상태" -mojangStatusTooltipTitle = "서비스" -mojangStatusNETitle = "Non Essential" -newsButton = "NEWS" -launchButton = "PLAY" -launchButtonPlaceholder = "• 선택된 서버 없음" -launchDetails = "잠시만 기다려주세요.." -newsNavigationStatus = "{currentPage} / {totalPages}" -newsErrorLoadSpan = "뉴스를 확인하는 중.." -newsErrorFailedSpan = "뉴스 불러오기 실패" -newsErrorRetryButton = "다시 시도" -newsErrorNoneSpan = "뉴스 없음" - -[ejs.login] -loginCancelText = "취소" -loginSubheader = "마인크래프트 로그인" -loginEmailError = "* 유효하지 않은 값" -loginEmailPlaceholder = "이메일 또는 사용자 이름" -loginPasswordError = "* 필수 항목" -loginPasswordPlaceholder = "비밀번호" -loginForgotPasswordLink = "https://minecraft.net/password/forgot/" -loginForgotPasswordText = "비밀번호를 잊으셨나요?" -loginRememberMeText = "로그인 저장" -loginButtonText = "로그인" -loginNeedAccountLink = "https://minecraft.net/store/minecraft-java-edition/" -loginNeedAccountText = "계정이 없으신가요?" -loginPasswordDisclaimer1 = "비밀번호는 Mojang에 직접 전송되며 저장되지 않습니다." -loginPasswordDisclaimer2 = "{appName}는 Mojang AB와 관련이 없습니다." - -[ejs.loginOptions] -loginOptionsTitle = "로그인 옵션" -loginWithMicrosoft = "Microsoft 계정으로 로그인" -loginWithMojang = "Mojang 계정으로 로그인" -cancelButton = "취소" - -[ejs.overlay] -serverSelectHeader = "서버 선택" -serverSelectConfirm = "선택" -serverSelectCancel = "취소" -accountSelectHeader = "계정 선택" -accountSelectConfirm = "선택" -accountSelectCancel = "취소" - -[ejs.settings] -navHeaderText = "설정" -navAccount = "계정" -navMinecraft = "마인크래프트" -navMods = "모드" -navJava = "Java" -navLauncher = "런처" -navAbout = "정보" -navUpdates = "업데이트" -navDone = "완료" -tabAccountHeaderText = "계정 설정" -tabAccountHeaderDesc = "새로운 계정을 추가하거나 기존 계정을 관리합니다." -microsoftAccount = "Microsoft" -addMicrosoftAccount = "+ Microsoft 계정 추가" -mojangAccount = "Mojang" -addMojangAccount = "+ Mojang 계정 추가" -minecraftTabHeaderText = "마인크래프트 설정" -minecraftTabHeaderDesc = "게임 실행과 관련된 설정입니다." -gameResolutionTitle = "게임 해상도" -launchFullscreenTitle = "전체 화면으로 실행합니다." -autoConnectTitle = "게임 실행 후 자동으로 서버에 접속합니다." -launchDetachedTitle = "런처와 게임 프로세스를 분리하여 실행합니다." -launchDetachedDesc = "이 옵션을 사용하면 런처를 닫아도 게임이 계속 실행됩니다." -tabModsHeaderText = "모드 설정" -tabModsHeaderDesc = "모드를 활성화하거나 비활성화합니다." -switchServerButton = "변경" -requiredMods = "필수 모드" -optionalMods = "선택 모드" -dropinMods = "사용자 추가 모드" -addMods = "모드 추가" -dropinRefreshNote = "(F5를 눌러 새로고침)" -shaderpacks = "쉐이더팩" -shaderpackDesc = "쉐이더를 활성화하거나 비활성화합니다. 쉐이더가 원활하게 작동하려면 높은 컴퓨터 성능이 필요합니다. 여기서 쉐이더팩을 추가할 수 있습니다." -selectShaderpack = "쉐이더팩 선택" -tabJavaHeaderText = "Java 설정" -tabJavaHeaderDesc = "Java 설정을 관리합니다 (고급 설정)." -memoryTitle = "메모리" -maxRAM = "최대 RAM" -minRAM = "최소 RAM" -memoryDesc = "권장하는 최소 RAM은 4GB입니다. 최소와 최대 RAM을 같은 값으로 설정하면 게임 렉을 줄일 수 있습니다." -memoryTotalTitle = "전체" -memoryAvailableTitle = "사용 가능" -javaExecutableTitle = "Java 실행 파일" -javaExecSelDialogTitle = "Java 실행 파일 선택" -javaExecSelButtonText = "파일 선택" -javaExecDesc = "게임 실행 전에 Java 실행 파일의 유효성을 검사합니다." -javaPathDesc = "Java 실행 파일의 경로는 반드시 {pathSuffix}로 끝나야 합니다." -jvmOptsTitle = "JVM 인수 설정" -jvmOptsDesc = "실행 시 JVM에 전달할 추가 인수를 설정합니다. -Xms-Xmx는 포함되지 않아야 합니다." -launcherTabHeaderText = "런처 설정" -launcherTabHeaderDesc = "런처와 관련된 설정입니다." -allowPrereleaseTitle = "프리릴리즈 업데이트를 허용합니다." -allowPrereleaseDesc = "프리릴리즈는 안정성이 보장되지 않은 기능을 포함할 수 있습니다.
현재 실행 중인 런처가 프리릴리즈 버전이라면, 이 설정은 항상 활성화됩니다." -catalogSourceTitle = "카탈로그 주소" -catalogSourcePlaceholder = "https://your-domain.example/catalog.json" -catalogSourceDesc = "비워두면 기본 카탈로그를 자동으로 찾습니다.
원격 운영 시에는 관리자 사이트가 제공하는 catalog.json URL을 입력하세요." -dataDirectoryTitle = "데이터 디렉토리" -selectDataDirectory = "데이터 디렉토리 선택" -chooseFolder = "폴더 선택" -dataDirectoryDesc = "모든 게임 파일과 로컬 Java 설치는 데이터 디렉토리에 저장됩니다.
스크린샷과 월드 데이터는 서버 설정에서 지정한 인스턴스 폴더에 저장됩니다." -aboutTabHeaderText = "정보" -aboutTabHeaderDesc = "런처 정보와 릴리즈 노트를 확인합니다." -aboutTitle = "{appName}" -stableRelease = "Stable Release" -versionText = "Version " -sourceGithub = "GitHub" -sourceOriginalGithub = "Original GitHub" -support = "Support" -devToolsConsole = "DevTools Console" -releaseNotes = "릴리즈 노트" -changelog = "변경 로그" -noReleaseNotes = "릴리즈 노트 없음" -viewReleaseNotes = "GitHub에서 릴리즈 노트 보기" -launcherUpdatesHeaderText = "런처 업데이트" -launcherUpdatesHeaderDesc = "런처 업데이트를 확인하고 설치합니다." -checkForUpdates = "업데이트 확인" -whatsNew = "새로운 기능" -updateReleaseNotes = "업데이트 릴리즈 노트" - -[ejs.waiting] -waitingText = "Microsoft의 응답을 기다리는 중.." - -[ejs.welcome] -continueButton = "계속" - - -[js.discord] -waiting = "클라이언트 대기 중.." -state = "Server: {shortId}" - -[js.index] -microsoftLoginTitle = "Microsoft 로그인" -microsoftLogoutTitle = "Microsoft 로그아웃" - -[js.login] -login = "로그인" -loggingIn = "로그인 중" -success = "성공" -tryAgain = "다시 시도" - -[js.login.error] -invalidValue = "* 유효하지 않은 값" -requiredValue = "* 필수 항목" - -[js.login.error.unknown] -title = "알 수 없는 로그인 오류" -desc = "로그인 중 알 수 없는 오류가 발생했습니다. 콘솔에서 자세한 내용을 확인하세요." - -[js.landing.launch] -pleaseWait = "잠시만 기다려주세요.." -failureTitle = "실행 중 오류 발생" -failureText = "콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." -okay = "확인" - -[js.landing.selectedAccount] -noAccountSelected = "선택된 계정 없음" -logoutFailed = "계정을 로그아웃하지 못했습니다. 다시 시도해 주세요." - -[js.landing.selectedProfile] -noSelection = "라이브러리에서 선택" -loading = "프로필 불러오는 중.." - -[js.landing.profileStatus] -label = "프로필" - -[js.landing.mapStatus] -label = "실행 상태" -ready = "실행 가능" -notReady = "실행 준비 필요" - -[js.landing.selectedServer] -noSelection = "선택된 서버 없음" -loading = "로딩 중.." - -[js.landing.serverStatus] -server = "서버" -offline = "오프라인" -players = "플레이어" - -[js.landing.portStatus] -label = "포트" -failed = "자동 포트 개방 실패" - -[js.landing.localServer] -starting = "로컬 서버 시작 중.." -missingJarTitle = "로컬 서버 실행 준비 부족" -missingJarText = "접속 주소가 비어 있어 로컬 서버를 실행하려고 했지만, 버킷 JAR이 등록되지 않았습니다. 관리자 사이트에서 서버 JAR을 업로드하거나 라이브러리에서 접속 주소를 입력해 주세요." -failureTitle = "로컬 서버 실행 실패" -failureText = "로컬 서버를 시작하지 못했습니다. 콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." - -[js.landing.systemScan] -checking = "시스템 정보 확인 중.." -noCompatibleJava = "호환되는 Java가
설치되지 않음" -installJavaMessage = "마인크래프트 실행을 위해 64비트 Java {major} 설치가 필요합니다. Java를 설치할까요?" -installJava = "Java 설치" -installJavaManually = "수동 설치" -javaDownloadPrepare = "Java 다운로드 준비 중.." -javaDownloadFailureTitle = "Java 다운로드 중 오류 발생" -javaDownloadFailureText = "콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." -javaRequired = "실행을 위해
Java가 필요합니다" -javaRequiredMessage = "실행을 위해 유효한 64비트 Java {major} 를 설치해야 합니다.

Java Management Guide를 참조하여 Java 수동 설치 방법을 확인하세요." -javaRequiredDismiss = "확인" -javaRequiredCancel = "돌아가기" - -[js.landing.downloadJava] -findJdkFailure = "OpenJDK를 찾을 수 없습니다." -javaDownloadCorruptedError = "다운로드한 JDK가 손상되었습니다." -extractingJava = "Java 압축 해제 중.." -javaInstalled = "Java 설치 완료!" - -[js.landing.dlAsync] -loadingServerInfo = "서버 정보 로딩 중.." -fatalError = "치명적 오류 발생" -unableToLoadDistributionIndex = "배포 인덱스를 불러올 수 없습니다. 콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." -pleaseWait = "잠시만 기다려주세요.." -errorDuringLaunchTitle = "실행 중 오류 발생" -seeConsoleForDetails = "콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." -validatingFileIntegrity = "파일 무결성 검사 중.." -errorDuringFileVerificationTitle = "파일 검사 중 오류 발생" -downloadingFiles = "파일 다운로드 중.." -errorDuringFileDownloadTitle = "파일 다운로드 중 오류 발생" -preparingToLaunch = "실행 준비 중.." -launchingGame = "게임 실행 중.." -launchWrapperNotDownloaded = "게임 실행을 위한 메인 파일(LaunchWrapper)을 다운로드할 수 없습니다.

문제 해결을 위해 임시로 안티바이러스 프로그램을 비활성화한 뒤 게임을 다시 실행해 보세요.

만약 이 문제가 계속된다면, GitHub 이슈 페이지에 사용 중인 안티바이러스 프로그램과 함께 문제를 제보해 주세요." -doneEnjoyServer = "완료. 즐거운 시간 되세요!" -checkConsoleForDetails = "콘솔 (CTRL + Shift + i) 에서 자세한 내용을 확인하세요." - -[js.landing.news] -checking = "뉴스 확인 중" - -[js.landing.discord] -loading = "게임 로딩 중.." -joining = "서버 접속 중.." -joined = "Mystic Red Space" - -[js.overlay] -dismiss = "닫기" - -[js.settings.fileSelectors] -executables = "실행 파일" -allFiles = "모든 파일" - -[js.settings.mstfLogin] -errorTitle = "오류 발생" -errorMessage = "Microsoft 로그인에 실패했습니다. 다시 시도해 주세요." -okButton = "확인" - -[js.settings.mstfLogout] -errorTitle = "오류 발생" -errorMessage = "Microsoft 로그아웃에 실패했습니다. 다시 시도해 주세요." -okButton = "확인" - -[js.settings.authAccountSelect] -selectButton = "계정 선택" -selectedButton = "선택된 계정 ✔" - -[js.settings.authAccountLogout] -lastAccountWarningTitle = "경고
마지막 남은 계정을 로그아웃합니다." -lastAccountWarningMessage = "런처를 사용하려면 최소한 하나의 계정은 로그인되어 있어야 합니다.

정말 로그아웃 하시겠습니까?" -confirmButton = "확인" -cancelButton = "취소" - -[js.settings.authAccountPopulate] -username = "사용자 이름" -uuid = "UUID" -selectAccount = "계정 선택" -selectedAccount = "선택된 계정 ✓" -logout = "로그아웃" - -[js.settings.dropinMods] -removeButton = "제거" -deleteFailedTitle = "사용자 추가 모드 삭제 실패
{fullName}" -deleteFailedMessage = "삭제하려는 파일이 사용 중인지 확인하고 다시 시도해 주세요." -failedToggleTitle = "사용자 추가 모드를
활성화/비활성화할 수 없습니다." -okButton = "확인" - -[js.settings.serverListing] -mainServer = "메인 서버" - -[js.settings.java] -selectedJava = "선택: Java {version} ({vendor})" -invalidSelection = "유효하지 않은 선택" -requiresJava = "64비트 Java {major}가 필요합니다." -availableOptions = "Java {major} (HotSpot VM)의 사용 가능한 JVM 인수" - -[js.settings.about] -preReleaseTitle = "Pre-release" -stableReleaseTitle = "Stable Release" -releaseNotesFailed = "릴리즈 노트를 불러올 수 없습니다." - -[js.settings.updates] -newReleaseTitle = "새로운 Release 업데이트 가능" -newPreReleaseTitle = "새로운 Pre-Release 업데이트 가능" -downloadingButton = "다운로드 중.." -downloadButton = 'GitHub에서 다운로드런처를 종료하고 dmg 파일을 실행하여 업데이트하세요.' -latestVersionTitle = "최신 버전을 사용하고 있습니다" -checkForUpdatesButton = "업데이트 확인" -checkingForUpdatesButton = "업데이트 확인 중.." - -[js.settings.msftLogin] -errorTitle = "Microsoft 로그인 실패" -errorMessage = "Microsoft 계정으로 로그인할 수 없습니다. 다시 시도해 주세요." -okButton = "확인" - -[js.uibinder.startup] -fatalErrorTitle = "치명적 오류: 배포 인덱스를 불러올 수 없습니다." -fatalErrorMessage = "배포 인덱스 다운로드 서버에 연결할 수 없습니다. 로컬 복사본도 존재하지 않습니다.

배포 인덱스는 최신 서버 정보를 제공하는 필수 파일로 런처를 실행하기 위해 필요합니다. 인터넷 연결을 확인하고 프로그램을 다시 실행해 보세요." -closeButton = "닫기" - -[js.uibinder.validateAccount] -failedMessageTitle = "로그인 갱신 실패" -failedMessage = "{account}의 로그인을 갱신할 수 없습니다. 다른 계정을 선택하거나 다시 로그인하세요." -failedMessageSelectAnotherAccount = "{account}의 로그인을 갱신할 수 없습니다. 다시 로그인하세요." -loginButton = "로그인" -selectAnotherAccountButton = "다른 계정 선택" - -[js.uicore.autoUpdate] -checkingForUpdateButton = "업데이트 확인 중..." -installNowButton = "설치하기" -checkForUpdatesButton = "업데이트 확인" - -[js.auth.microsoft.error] -noProfileTitle = "로그인 중 오류 발생:
프로필이 설정되지 않음" -noProfileDesc = "Microsoft 계정에 마인크래프트 프로필이 설정되지 않았습니다. 마인크래프트를 구매했거나 Xbox Game Pass를 사용 중이라면, Minecraft.net에서 로그인하여 프로필을 설정하세요.

아직 게임을 구매하지 않았다면, Minecraft.net에서 구매하세요." -noXboxAccountTitle = "로그인 중 오류 발생:
Xbox 계정 없음" -noXboxAccountDesc = "Microsoft 계정에 연결된 Xbox 계정이 없습니다." -xblBannedTitle = "로그인 중 오류 발생:
Xbox Live 이용 불가" -xblBannedDesc = "Xbox Live 이용이 제한되거나 차단된 국가의 Microsoft 계정으로 로그인할 수 없습니다." -under18Title = "로그인 중 오류 발생:
부모 동의 필요" -under18Desc = "18세 미만 유저의 계정은 부모의 가족 구성원으로 등록되어 있어야 합니다." -unknownTitle = "로그인 중 알 수 없는 오류 발생" -unknownDesc = "로그인 중 알 수 없는 오류가 발생했습니다. 콘솔에서 자세한 내용을 확인하세요." - -[js.auth.mojang.error] -methodNotAllowedTitle = "내부 오류 발생:
허용되지 않은 메소드" -methodNotAllowedDesc = "메소드가 허용되지 않습니다. 개발자에게 이 문제를 보고하세요." -notFoundTitle = "내부 오류 발생:
엔드포인트를 찾을 수 없음" -notFoundDesc = "인증 엔드포인트를 찾을 수 없습니다. 개발자에게 이 문제를 보고하세요." -accountMigratedTitle = "로그인 중 오류 발생:
마이그레이션된 계정" -accountMigratedDesc = "마이그레이션된 계정으로 로그인을 시도했습니다. 사용자 이름에 계정 이메일 주소를 입력하여 로그인하세요." -invalidCredentialsTitle = "로그인 중 오류 발생:
잘못된 자격 증명" -invalidCredentialsDesc = "이메일 또는 비밀번호가 잘못되었습니다. 다시 시도하세요." -tooManyAttemptsTitle = "로그인 중 오류 발생:
시도 횟수 초과" -tooManyAttemptsDesc = "로그인 시도 횟수가 너무 많습니다. 잠시 후 다시 시도하세요." -invalidTokenTitle = "로그인 중 오류 발생:
유효하지 않은 토큰" -invalidTokenDesc = "유효하지 않은 액세스 토큰입니다." -tokenHasProfileTitle = "로그인 중 오류 발생:
프로필이 할당된 토큰" -tokenHasProfileDesc = "액세스 토큰에 이미 프로필이 할당되어 있습니다. 프로필 선택은 아직 지원되지 않습니다." -credentialsMissingTitle = "로그인 중 오류 발생:
자격 증명 누락" -credentialsMissingDesc = "사용자 이름 또는 비밀번호가 비어있거나, 비밀번호가 3글자 미만으로 입력되었습니다." -invalidSaltVersionTitle = "로그인 중 오류 발생:
유효하지 않은 Salt 버전" -invalidSaltVersionDesc = "유효하지 않은 Salt 버전입니다." -unsupportedMediaTypeTitle = "내부 오류 발새:
지원되지 않는 미디어 타입" -unsupportedMediaTypeDesc = "지원되지 않는 미디어 타입입니다. 개발자에게 이 문제를 보고하세요." -accountGoneTitle = "로그인 중 오류 발생:
마이그레이션된 계정" -accountGoneDesc = "Microsoft로 마이그레이션된 계정입니다. Microsoft 계정으로 로그인하세요." -unreachableTitle = "로그인 중 오류 발생:
서버에 연결할 수 없음" -unreachableDesc = "인증 서버에 연결할 수 없습니다. 인증 서버가 온라인 상태이고 인터넷 연결이 정상인지 확인하세요." -gameNotPurchasedTitle = "로그인 중 오류 발생:
게임을 구매하지 않음" -gameNotPurchasedDesc = "로그인을 시도한 계정은 마인크래프트를 구매하지 않았습니다. Minecraft.net에서 게임을 구매하세요." -unknownErrorTitle = "로그인 중 알 수 없는 오류 발생" -unknownErrorDesc = "로그인 중 알 수 없는 오류가 발생했습니다. 콘솔에서 자세한 내용을 확인하세요." diff --git a/app/assets/launcher/catalog.json b/app/assets/launcher/catalog.json deleted file mode 100644 index e8a0f7e..0000000 --- a/app/assets/launcher/catalog.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": 1, - "profiles": [ - { - "id": "template-map-base", - "name": "Map Base Template", - "description": "맵만 사용하는 기본 프로필 예시입니다.", - "details": "맵 기반 기본 프로필입니다. 월드 ZIP과 distribution 파일만 있으면 싱글플레이 실행 흐름으로 사용할 수 있습니다.", - "modsEnabled": false, - "pluginsEnabled": false, - "serverEnabled": false, - "distributionUrl": "admin/data/distributions/template-map-base.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/map-base.zip", - "worldDirectoryName": "Map Base" - }, - { - "id": "template-map-mods", - "name": "Map + Mods Template", - "description": "맵과 모드를 함께 쓰는 프로필 예시입니다.", - "details": "맵 기반에 모드 구성이 포함된 프로필입니다. distribution 파일은 모드가 포함된 클라이언트용으로 준비하면 됩니다.", - "modsEnabled": true, - "pluginsEnabled": false, - "serverEnabled": false, - "distributionUrl": "admin/data/distributions/template-map-mods.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/map-mods.zip", - "worldDirectoryName": "Map Mods" - }, - { - "id": "template-map-plugin-server", - "name": "Map + Plugin Server Template", - "description": "맵, 플러그인, 서버를 함께 쓰는 프로필 예시입니다.", - "details": "플러그인을 켜면 서버도 같이 사용합니다. 주소를 비우면 로컬 서버를 띄우고, 주소를 입력하면 해당 서버로 바로 접속하는 흐름에 맞춘 예시입니다.", - "modsEnabled": false, - "pluginsEnabled": true, - "serverEnabled": true, - "distributionUrl": "admin/data/distributions/template-map-plugin-server.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/plugin-map.zip", - "worldDirectoryName": "Plugin Map", - "serverJarUrl": "https://example.com/server/paper.jar", - "serverPort": 25565, - "serverMemoryMb": 4096, - "serverMaxPlayers": 20, - "serverWhitelistEnabled": false - } - ] -} diff --git a/app/frame.ejs b/app/frame.ejs deleted file mode 100644 index 1151f91..0000000 --- a/app/frame.ejs +++ /dev/null @@ -1,33 +0,0 @@ -
-
-
-
- <%if (process.platform === 'darwin') { %> -
-
- - - -
-
- <% } else{ %> -
-
- <%= lang('app.title') %> -
-
- - - -
-
- <% } %> -
-
-
\ No newline at end of file diff --git a/app/install.ejs b/app/install.ejs deleted file mode 100644 index ba88d9e..0000000 --- a/app/install.ejs +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/app/landing.ejs b/app/landing.ejs deleted file mode 100644 index 9f502a5..0000000 --- a/app/landing.ejs +++ /dev/null @@ -1,250 +0,0 @@ - diff --git a/app/library.ejs b/app/library.ejs deleted file mode 100644 index 9ae279a..0000000 --- a/app/library.ejs +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/app/login.ejs b/app/login.ejs deleted file mode 100644 index cc7184f..0000000 --- a/app/login.ejs +++ /dev/null @@ -1,65 +0,0 @@ - \ No newline at end of file diff --git a/app/loginOptions.ejs b/app/loginOptions.ejs deleted file mode 100644 index 20aa67c..0000000 --- a/app/loginOptions.ejs +++ /dev/null @@ -1,34 +0,0 @@ - \ No newline at end of file diff --git a/app/overlay.ejs b/app/overlay.ejs deleted file mode 100644 index 7faab15..0000000 --- a/app/overlay.ejs +++ /dev/null @@ -1,41 +0,0 @@ - \ No newline at end of file diff --git a/app/settings.ejs b/app/settings.ejs deleted file mode 100644 index 841f716..0000000 --- a/app/settings.ejs +++ /dev/null @@ -1,413 +0,0 @@ - diff --git a/app/waiting.ejs b/app/waiting.ejs deleted file mode 100644 index c64726f..0000000 --- a/app/waiting.ejs +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/app/welcome.ejs b/app/welcome.ejs deleted file mode 100644 index 4641b4c..0000000 --- a/app/welcome.ejs +++ /dev/null @@ -1,25 +0,0 @@ - \ No newline at end of file diff --git a/build/icon.png b/build/icon.png deleted file mode 100644 index 5ab9783..0000000 Binary files a/build/icon.png and /dev/null differ diff --git a/dev-app-update.yml b/dev-app-update.yml deleted file mode 100644 index 3c2924f..0000000 --- a/dev-app-update.yml +++ /dev/null @@ -1,3 +0,0 @@ -owner: peunsu -repo: MRSLauncher -provider: github diff --git a/docs/MicrosoftAuth.md b/docs/MicrosoftAuth.md deleted file mode 100644 index d7f1c09..0000000 --- a/docs/MicrosoftAuth.md +++ /dev/null @@ -1,52 +0,0 @@ -# Microsoft Authentication - -Authenticating with Microsoft is fully supported by Helios Launcher. - -## Acquiring an Entra Client ID - -1. Navigate to https://portal.azure.com -2. In the search bar, search for **Microsoft Entra ID**. -3. In Microsoft Entra ID, go to **App Registrations** on the left pane (Under *Manage*). -4. Click **New Registration**. - - Set **Name** to be your launcher's name. - - Set **Supported account types** to *Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)* - - Leave **Redirect URI** blank. - - Register the application. -5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered. -6. Click **Authentication** on the left pane (Under *Manage*). -7. Click **Add Platform**. - - Select **Mobile and desktop applications**. - - Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**. - - Select **Configure** to finish adding the platform. -8. Go to **Certificates & secrets**. - - Select **Client secrets**. - - Click **New client secret**. - - Set a description. - - Click **Add**. - - Don't copy the client secret, adding one is just a requirement from Microsoft. -8. Navigate back to **Overview**. -9. Copy **Application (client) ID**. - - -## Adding the Entra Client ID to Helios Launcher. - -In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id. - -Note: Entra Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets - -Then relaunch your app, and login. You'll be greeted with an error message, because the app isn't whitelisted yet. Microsoft needs some activity on the app before whitelisting it. __Trying to log in before requesting whitelist is mandatory.__ - -## Requesting whitelisting from Microsoft - -1. Ensure you have completed every step of this doc page. -2. Fill [this form](https://aka.ms/mce-reviewappid) with the required information. Remember this is a new appID for approval. You can find both the Client ID and the Tenant ID on the overview page in the Azure Portal. -3. Give Microsoft some time to review your app. -4. Once you have received Microsoft's approval, allow up to 24 hours for the changes to apply. - ----- - -You can now authenticate with Microsoft through the launcher. - -References: -- https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app -- https://help.minecraft.net/hc/en-us/articles/16254801392141 diff --git a/docs/admin-site.md b/docs/admin-site.md deleted file mode 100644 index 418ab56..0000000 --- a/docs/admin-site.md +++ /dev/null @@ -1,66 +0,0 @@ -# 관리자 사이트 - -런처 설치 페이지에 표시되는 프로필을 JSON 직접 수정 없이 웹 UI로 관리하는 로컬 관리자 사이트입니다. - -## 실행 - -```bash -npm run admin -``` - -기본 주소: - -- `http://127.0.0.1:8787` - -공개 주소로 운영할 때: - -- `LAUNCHER_PUBLIC_BASE_URL=https://your-domain.example npm run admin` -- 그러면 관리자 사이트 상단의 `앱 연결용 catalog URL`이 그 공개 주소 기준으로 표시됩니다. - -## 현재 구현 범위 - -- 프로필 추가 / 수정 / 삭제 / 복제 -- `맵` 기본 + `모드`, `플러그인`, `서버` 체크 조합 -- `distribution.json` 업로드 / 새로 만들기 / 입력 폼 편집 -- 월드 ZIP 업로드 -- 서버용 버킷 JAR 업로드 -- 서버 포트 / 메모리 / 최대 인원수 / 화이트리스트 설정 -- 저장 시 아래 두 파일을 동시에 갱신 - - `admin/data/catalog.json` - - `app/assets/launcher/catalog.json` - -## distribution 편집 - -- `distribution 파일` 칸에서 JSON 업로드 가능 -- `폼 편집` 버튼으로 현재 연결된 `distribution.json`을 입력 폼으로 수정 가능 -- `새로 만들기` 버튼으로 샘플 템플릿에서 새 `distribution.json` 생성 가능 -- 편집기에서는 버전, RSS, 서버 이름, 마인크래프트 버전 같은 기본 정보를 설명과 함께 입력합니다. -- 기존 모듈 목록과 추가 서버 정보는 저장 시 그대로 보존됩니다. -- 저장된 distribution 파일은 아래에 생성됩니다. - - `admin/data/distributions/` - -## 업로드 동작 - -업로드 버튼으로 올린 파일은 아래에 저장됩니다. - -- `admin/data/uploads/` - -카탈로그에는 현재 프로젝트 기준 상대 경로가 저장됩니다. - -예: - -- `admin/data/uploads/1715000000000-paper.jar` - -## 주의 - -- 접속 주소는 관리자 사이트에서 다루지 않습니다. -- 접속 주소는 사용자가 런처 라이브러리에서 직접 입력합니다. -- 지금 1차 버전은 로컬 운영용입니다. -- 기본 서버 바인딩은 `127.0.0.1` 이라 같은 PC에서만 접속됩니다. - -## 추천 운영 방식 - -1. 관리자 사이트에서 프로필과 자료 파일을 입력 -2. 상단의 `앱 연결용 catalog URL`을 복사 -3. 런처 설정의 `카탈로그 주소`에 붙여넣기 -4. 저장 후 실제 표시와 실행 확인 diff --git a/docs/distro.md b/docs/distro.md deleted file mode 100644 index 762bb81..0000000 --- a/docs/distro.md +++ /dev/null @@ -1,592 +0,0 @@ -# Distribution Index - -You can use [Nebula](https://github.com/dscalzi/Nebula) to automate the generation of a distribution index. - -The most up to date and accurate descriptions of the distribution spec can be viewed in [helios-distribution-types](https://github.com/dscalzi/helios-distribution-types). - -The distribution index is written in JSON. The general format of the index is as posted below. - -```json -{ - "version": "1.0.0", - "discord": { - "clientId": "12334567890123456789", - "smallImageText": "WesterosCraft", - "smallImageKey": "seal-circle" - }, - "rss": "https://westeroscraft.com/articles/index.rss", - "servers": [ - { - "id": "Example_Server", - "name": "WesterosCraft Example Client", - "description": "Example WesterosCraft server. Connect for fun!", - "icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png", - "version": "0.0.1", - "address": "mc.westeroscraft.com:1337", - "minecraftVersion": "1.11.2", - "discord": { - "shortId": "Example", - "largeImageText": "WesterosCraft Example Server", - "largeImageKey": "server-example" - }, - "mainServer": true, - "autoconnect": true, - "modules": [ - "Module Objects Here" - ] - } - ] -} -``` - -## Distro Index Object - -#### Example -```JSON -{ - "version": "1.0.0", - "discord": { - "clientId": "12334567890123456789", - "smallImageText": "WesterosCraft", - "smallImageKey": "seal-circle" - }, - "rss": "https://westeroscraft.com/articles/index.rss", - "servers": [] -} -``` - -### `DistroIndex.version: string/semver` - -The version of the index format. Will be used in the future to gracefully push updates. - -### `DistroIndex.discord: object` - -Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to). - -**Properties** - -* `discord.clientId: string` - Client ID for th Application registered with Discord. -* `discord.smallImageText: string` - Tootltip for the `smallImageKey`. -* `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork. - - -### `DistroIndex.rss: string/url` - -A URL to a RSS feed. Used for loading news. - ---- - -## Server Object - -#### Example -```JSON -{ - "id": "Example_Server", - "name": "WesterosCraft Example Client", - "description": "Example WesterosCraft server. Connect for fun!", - "icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png", - "version": "0.0.1", - "address": "mc.westeroscraft.com:1337", - "minecraftVersion": "1.11.2", - "discord": { - "shortId": "Example", - "largeImageText": "WesterosCraft Example Server", - "largeImageKey": "server-example" - }, - "mainServer": true, - "autoconnect": true, - "modules": [] -} -``` - -### `Server.id: string` - -The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**. - -### `Server.name: string` - -The name of the server. This is what users see on the UI. - -### `Server.description: string` - -A brief description of the server. Displayed on the UI to provide users more information. - -### `Server.icon: string/url` - -A URL to the server's icon. Will be displayed on the UI. - -### `Server.version: string/semver` - -The version of the server configuration. - -### `Server.address: string/url` - -The server's IP address. - -### `Server.minecraftVersion: string` - -The version of minecraft that the server is running. - -### `Server.discord: object` - -Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to). - -**Properties** - -* `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId` -* `discord.largeImageText: string` - Ttooltip for the `largeImageKey`. -* `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork. - -### `Server.mainServer: boolean` - -Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false. - -### `Server.autoconnect: boolean` - -Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled. - -### `Server.javaOptions: JavaOptions` - -**OPTIONAL** - -Sever-specific Java options. If not provided, defaults are used by the client. - -### `Server.modules: Module[]` - -An array of module objects. - ---- - -## JavaOptions Object - -Server-specific Java options. - -#### Example -```JSON -{ - "supported": ">=17", - "suggestedMajor": 17, - "platformOptions": [ - { - "platform": "darwin", - "architecture": "arm64", - "distribution": "CORRETTO" - } - ], - "ram": { - "recommended": 3072, - "minimum": 2048 - } -} -``` - -### `JavaOptions.platformOptions: JavaPlatformOptions[]` - -**OPTIONAL** - -Platform-specific java rules for this server configuration. Validation rules will be delegated to the client for any undefined properties. Java validation can be configured for specific platforms and architectures. The most specific ruleset will be applied. - -Maxtrix Precedence (Highest - Lowest) - - Current platform, current architecture (ex. win32 x64). - - Current platform, any architecture (ex. win32). - - Java Options base properties. - - Client logic (default logic in the client). - -Properties: - - - `platformOptions.platform: string` - The platform that this validation matrix applies to. - - `platformOptions.architecture: string` - Optional. The architecture that this validation matrix applies to. If omitted, applies to all architectures. - - `platformOptions.distribution: string` - Optional. See `JavaOptions.distribution`. - - `platformOptions.supported: string` - Optional. See `JavaOptions.supported`. - - `platformOptions.suggestedMajor: number` - Optional. See `JavaOptions.suggestedMajor`. - -### `JavaOptions.ram: object` - -**OPTIONAL** - -This allows you to require a minimum and recommended amount of RAM per server instance. The minimum is the smallest value the user can select in the settings slider. The recommended value will be the default value selected for that server. These values are specified in megabytes and must be an interval of 512. This allows configuration in intervals of half gigabytes. In the above example, the recommended ram value is 3 GB (3072 MB) and the minimum is 2 GB (2048 MB). - - - `ram.recommended: number` - The recommended amount of RAM in megabytes. Must be an interval of 512. - - `ram.minimum: number` - The absolute minimum amount of RAM in megabytes. Must be an interval of 512. - -### `JavaOptions.distribution: string` - -**OPTIONAL** - -Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific). - -### `JavaOptions.supported: string` - -**OPTIONAL** - -A semver range of supported JDK versions. - -Java version syntax is platform dependent. - -JDK 8 and prior -``` -1.{major}.{minor}_{patch}-b{build} -Ex. 1.8.0_152-b16 -``` - -JDK 9+ -``` -{major}.{minor}.{patch}+{build} -Ex. 11.0.12+7 -``` - -For processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, `1.{major}.{minor}_{patch}-b{build}` will be translated to `{major}.{minor}.{patch}+{build}`. - -If specified, you must also specify suggestedMajor. - -If omitted, the client will decide based on the game version. - -### `JavaOptions.suggestedMajor: number` - -**OPTIONAL** - -The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version. - -NOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range. - -Common use case: - - supported: '>=17.x' - - suggestedMajor: 17 - -More involved: - - supported: '>=16 <20' - - suggestedMajor: 17 - -Given a wider support range, it becomes necessary to specify which major version in the range is the suggested. - ---- - -## Module Object - -A module is a generic representation of a file required to run the minecraft client. - -#### Example -```JSON -{ - "id": "com.example:artifact:1.0.0@jar.pack.xz", - "name": "Artifact 1.0.0", - "type": "Library", - "artifact": { - "size": 4231234, - "MD5": "7f30eefe5c51e1ae0939dab2051db75f", - "url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz" - }, - "subModules": [ - { - "id": "examplefile", - "name": "Example File", - "type": "File", - "artifact": { - "size": 23423, - "MD5": "169a5e6cf30c2cc8649755cdc5d7bad7", - "path": "examplefile.txt", - "url": "http://files.site.com/examplefile.txt" - } - } - ] -} -``` - -The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used. - -### `Module.id: string` - -The ID of the module. All modules that are not of type `File` **MUST** use a maven identifier. Version information and other metadata is pulled from the identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`. - -**Template** - -`my.group:arifact:version@extension` - -`my/group/artifact/version/artifact-version.extension` - -**Example** - -`net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar` - -`net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar` - -If the module's artifact does not declare the `path` property, its path will be resolved from the ID. - -### `Module.name: string` - -The name of the module. Used on the UI. - -### `Module.type: string` - -The type of the module. - -### `Module.classpath: boolean` - -**OPTIONAL** - -If the module is of type `Library`, whether the library should be added to the classpath. Defaults to true. - -### `Module.required: Required` - -**OPTIONAL** - -Defines whether or not the module is required. If omitted, then the module will be required. - -Only applicable for modules of type: -* `ForgeMod` -* `LiteMod` -* `LiteLoader` - - -### `Module.artifact: Artifact` - -The download artifact for the module. - -### `Module.subModules: Module[]` - -**OPTIONAL** - -An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit. - - -## Artifact Object - -The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file. - -Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property. - -The resolved/provided paths are appended to a base path depending on the module's declared type. - -| Type | Path | -| ---- | ---- | -| `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | -| `Fabric` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | -| `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | -| `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) | -| `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) | -| `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) | -| `FabricMod` | ({`commonDirectory`}/mods/fabric/{`path` OR resolved}) | -| `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) | - -The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json. - -### `Artifact.size: number` - -The size of the artifact. - -### `Artifact.MD5: string` - -The MD5 hash of the artifact. This will be used to validate local artifacts. - -### `Artifact.path: string` - -**OPTIONAL** - -A relative path to where the file will be saved. This is appended to the base path for the module's declared type. - -If this is not specified, the path will be resolved based on the module's ID. - -### `Artifact.url: string/url` - -The artifact's download url. - -## Required Object - -### `Required.value: boolean` - -**OPTIONAL** - -If the module is required. Defaults to true if this property is omited. - -### `Required.def: boolean` - -**OPTIONAL** - -If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited. - ---- - -## Module Types - -### ForgeHosted - -The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules. - -Ex. - -```json -{ - "id": "net.minecraftforge:forge:1.11.2-13.20.1.2429", - "name": "Minecraft Forge 1.11.2-13.20.1.2429", - "type": "ForgeHosted", - "artifact": { - "size": 4450992, - "MD5": "3fcc9b0104f0261397d3cc897e55a1c5", - "url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar" - }, - "subModules": [ - { - "id": "net.minecraft:launchwrapper:1.12", - "name": "Mojang (LaunchWrapper)", - "type": "Library", - "artifact": { - "size": 32999, - "MD5": "934b2d91c7c5be4a49577c9e6b40e8da", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar" - } - } - ] -} -``` - -All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work. - -There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented. - ---- - -### Fabric - -The module type `Fabric` represents the fabric mod loader. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. - -Ex. - -```json -{ - "id": "net.fabricmc:fabric-loader:0.15.0", - "name": "Fabric (fabric-loader)", - "type": "Fabric", - "artifact": { - "size": 1196222, - "MD5": "a43d5a142246801343b6cedef1c102c4", - "url": "http://localhost:8080/repo/lib/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar" - }, - "subModules": [ - { - "id": "1.20.1-fabric-0.15.0", - "name": "Fabric (version.json)", - "type": "VersionManifest", - "artifact": { - "size": 2847, - "MD5": "69a2bd43452325ba1bc882fa0904e054", - "url": "http://localhost:8080/repo/versions/1.20.1-fabric-0.15.0/1.20.1-fabric-0.15.0.json" - } - } -} -``` - -Fabric works similarly to Forge 1.13+. - ---- - -### LiteLoader - -The module type `LiteLoader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `ForgeMod` and `Litemod` modules. - -Ex. -```json -{ - "id": "com.mumfrey:liteloader:1.11.2", - "name": "Liteloader (1.11.2)", - "type": "LiteLoader", - "required": { - "value": false, - "def": false - }, - "artifact": { - "size": 1685422, - "MD5": "3a98b5ed95810bf164e71c1a53be568d", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar" - }, - "subModules": [ - "All LiteMods go here" - ] -} -``` - ---- - -### Library - -The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process. - -Ex. - -```json -{ - "id": "net.sf.jopt-simple:jopt-simple:4.6", - "name": "Jopt-simple 4.6", - "type": "Library", - "artifact": { - "size": 62477, - "MD5": "13560a58a79b46b82057686543e8d727", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar" - } -} -``` - ---- - -### ForgeMod - -The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). - -Ex. -```json -{ - "id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133", - "name": "WesterosBlocks (3.0.0-beta-6-133)", - "type": "ForgeMod", - "artifact": { - "size": 16321712, - "MD5": "5a89e2ab18916c18965fc93a0766cc6e", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar" - } -} -``` - ---- - -### LiteMod - -The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34). - -Ex. -```json -{ - "id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod", - "name": "Macro/Keybind Mod (0.14.4-1.11.2)", - "type": "LiteMod", - "required": { - "value": false, - "def": false - }, - "artifact": { - "size": 1670811, - "MD5": "16080785577b391d426c62c8d3138558", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod" - } -} -``` - ---- - -### File - -The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory. - -Ex. - -```json -{ - "id": "com.westeroscraft:westeroscraftrp:2017-08-16", - "name": "WesterosCraft Resource Pack (2017-08-16)", - "type": "file", - "artifact": { - "size": 45241339, - "MD5": "ec2d9fdb14d5c2eafe5975a240202f1a", - "path": "resourcepacks/WesterosCraft.zip", - "url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip" - } -} -``` diff --git a/docs/launcher-catalog.md b/docs/launcher-catalog.md deleted file mode 100644 index b99bec2..0000000 --- a/docs/launcher-catalog.md +++ /dev/null @@ -1,85 +0,0 @@ -# Launcher Catalog - -프로필은 관리자 측에서 미리 등록합니다. - -- 기본 로컬 파일: `app/assets/launcher/catalog.json` -- 또는 운영용 원격 JSON - -클라이언트는 설치 페이지에서 이 카탈로그를 읽기 전용으로 확인하고, 원하는 항목만 자기 라이브러리에 추가합니다. - -## 형식 - -```json -{ - "version": 1, - "profiles": [ - { - "id": "my-map-profile", - "name": "My Map Profile", - "description": "설명", - "details": "설치 페이지 상세 패널에 표시할 긴 설명", - "modsEnabled": true, - "pluginsEnabled": false, - "serverEnabled": false, - "distributionUrl": "admin/data/distributions/my-map-profile.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/my-map.zip", - "worldDirectoryName": "My Map" - }, - { - "id": "my-plugin-server-profile", - "name": "My Plugin Server Profile", - "description": "플러그인 서버 포함 프로필", - "details": "주소가 없으면 로컬 서버를 실행하고, 주소가 있으면 해당 서버로 접속합니다.", - "modsEnabled": false, - "pluginsEnabled": true, - "serverEnabled": true, - "distributionUrl": "admin/data/distributions/my-plugin-server-profile.distribution.json", - "worldArchiveUrl": "https://example.com/worlds/plugin-map.zip", - "worldDirectoryName": "Plugin Map", - "serverJarUrl": "https://example.com/server/paper.jar", - "serverPort": 25565, - "serverMemoryMb": 4096, - "serverMaxPlayers": 20, - "serverWhitelistEnabled": false - } - ] -} -``` - -## 필드 - -- `id`: 내부 식별자 -- `name`: 라이브러리/설치 페이지 표시 이름 -- `description`: 표시 설명 -- `details`: 설치 페이지 상세 패널에 표시할 긴 설명 -- `modsEnabled`: 모드 기능 사용 여부 -- `pluginsEnabled`: 플러그인 기능 사용 여부. 켜면 서버도 같이 사용 -- `serverEnabled`: 서버 기능 사용 여부 -- `distributionUrl`: Helios distribution.json URL 또는 로컬 경로 -- `worldArchiveUrl`: 월드 ZIP 또는 로컬 경로 -- `worldDirectoryName`: 게임 `saves/` 아래와 로컬 서버 `world/`에 사용할 월드 폴더 이름 -- `serverJarUrl`: 로컬 서버 실행에 사용할 버킷 JAR 경로 또는 URL -- `serverPort`: 로컬 서버 포트 -- `serverMemoryMb`: 로컬 서버 최대 메모리(MB) -- `serverMaxPlayers`: 로컬 서버 최대 인원수 -- `serverWhitelistEnabled`: 로컬 서버 화이트리스트 사용 여부 - -## 런처가 계산하는 상태 - -아래 값들은 런처가 내부적으로 계산하는 상태라 파일에 직접 넣지 않아도 됩니다. - -- `launchReady`: 실행에 필요한 필드가 모두 있는지 여부 -- `hostReady`: 서버 기능 프로필이 로컬 호스팅 가능한지 여부 - -판정 기준: - -- 공통 실행: `distributionUrl`, `worldArchiveUrl`, `worldDirectoryName` -- 로컬 서버 실행 추가 조건: `serverEnabled=true` 이고 `serverJarUrl` 존재 - -## 현재 구현 범위 - -- 맵 기반 싱글플레이 실행 -- 맵 + 모드 클라이언트 실행 -- 서버 기능 프로필의 직접 주소 입력 접속 -- 서버 기능 프로필의 로컬 버킷 JAR 실행 -- 자동 포트 개방 상태 표시 diff --git a/docs/portforwarding-free-connection-plan.md b/docs/portforwarding-free-connection-plan.md deleted file mode 100644 index da20a22..0000000 --- a/docs/portforwarding-free-connection-plan.md +++ /dev/null @@ -1,264 +0,0 @@ -# Portforwarding-Free Connection Plan - -이 문서는 “포트포워딩 없이 런처만으로 접속” 기능을 나중에 구현하기 위한 설계 초안이다. 현재는 구현하지 않았고, `server-pack` 프로필의 `tunnelCommand` 자리에 외부 도구를 연결할 수 있는 수준까지만 준비되어 있다. - -## 목표 - -- 호스트 사용자가 공유기 포트포워딩을 직접 설정하지 않아도 됨 -- 접속 사용자는 런처에서 세션 주소 또는 세션 코드를 받아 바로 접속 가능 -- 가능하면 버튼 몇 번으로 `서버 실행 -> 세션 공개 -> 다른 사용자 접속` 흐름이 끝나야 함 -- 관리자가 런처와 백엔드를 함께 운영할 수 있어야 함 - -## 원하는 사용자 흐름 - -### 호스트 - -1. 라이브러리에서 `server-pack` 프로필 선택 -2. `서버 실행` 클릭 -3. 런처가 서버 프로세스를 시작 -4. 런처가 백엔드에 세션 생성 요청 -5. 백엔드가 공개 접속 주소 또는 세션 코드를 발급 -6. 호스트는 그 주소/코드를 다른 사용자에게 전달 - -### 접속자 - -1. 같은 프로필을 라이브러리에 추가 -2. 호스트가 보낸 주소 또는 세션 코드를 입력 -3. 런처가 실제 접속 주소를 해석 -4. 마인크래프트를 자동 실행하고 해당 서버로 바로 접속 - -## 필요한 구성요소 - -### 1. Session API - -역할: - -- 세션 생성 -- 세션 갱신 -- 세션 종료 -- 세션 조회 -- 세션 코드와 실제 릴레이 주소 매핑 - -예상 엔드포인트: - -- `POST /api/sessions` -- `PATCH /api/sessions/:id` -- `DELETE /api/sessions/:id` -- `GET /api/sessions/:code` - -세션 데이터 예시: - -```json -{ - "id": "sess_123", - "profileId": "my-server-pack", - "hostUserId": "uuid-or-account-id", - "relayAddress": "relay.example.com:34192", - "accessCode": "ABCD-EFGH", - "status": "active", - "createdAt": "2026-05-04T10:00:00Z", - "lastHeartbeatAt": "2026-05-04T10:01:00Z" -} -``` - -### 2. Relay or Tunnel Layer - -실제로 포트포워딩 없는 접속을 가능하게 하는 핵심이다. - -선택지: - -- 자체 Relay 서버 운영 -- 기존 터널링 솔루션 연동 -- TURN 비슷한 중계 방식 -- 전용 게임 프록시 운영 - -이 프로젝트에서는 우선 아래 두 단계 접근이 현실적이다. - -1. 외부 터널링 도구 연동 -2. 자체 세션/중계 서버로 점진적 전환 - -### 3. Launcher Host Agent - -호스트 측 런처가 해야 하는 일: - -- 로컬 서버 실행 상태 확인 -- 백엔드에 세션 생성 -- 터널 또는 릴레이 프로세스 시작 -- 공개 주소 수신 -- 주기적 heartbeat 전송 -- 서버 종료 시 세션 종료 - -### 4. Launcher Join Resolver - -접속자 측 런처가 해야 하는 일: - -- 세션 코드 입력 UI 제공 -- 코드로 세션 조회 -- 실제 접속 주소 해석 -- 선택 프로필과 세션 프로필 일치 여부 확인 -- 게임 실행 시 자동 접속 파라미터 전달 - -## 권장 1차 구현 방향 - -가장 빠른 방향은 “전용 Relay”를 바로 만드는 것이 아니라, “Session API + 터널 도구 연동”부터 시작하는 것이다. - -### 이유 - -- 현재 런처에 `server-pack`, `tunnelCommand`, 공개 주소 표시 흐름이 이미 들어가 있음 -- 가장 적은 변경으로 실사용 가능 여부를 먼저 검증할 수 있음 -- 이후 자체 릴레이로 갈아탈 때 UI와 라이브러리 모델을 그대로 유지할 수 있음 - -### 1차 구성 - -- 런처: - - 서버 실행 - - 터널 명령 실행 - - 터널 출력에서 공개 주소 파싱 - - Session API에 주소 등록 -- 백엔드: - - 세션 코드 발급 - - 코드 -> 주소 매핑 - - heartbeat 만료 처리 -- 접속자 런처: - - 세션 코드 입력 - - 코드 조회 - - 자동 접속 - -## 이후 2차 구현 방향 - -터널 명령에 외부 도구를 직접 의존하지 않고, 전용 서비스로 이동한다. - -### 추가되는 것 - -- 릴레이 노드 -- 호스트 인증 토큰 -- 세션별 임시 접속 권한 -- 트래픽/세션 제한 -- 관리자 대시보드 - -### 기대 효과 - -- 터널 도구 설치 부담 감소 -- 사용자 경험 단순화 -- 접속 로그와 세션 통계 수집 가능 -- 악용 방지 정책 적용 가능 - -## 런처 쪽에 나중에 추가할 UI - -### 라이브러리 - -- `세션 만들기` -- `세션 코드 복사` -- `세션 종료` -- `세션 상태 보기` - -### 설치/실행 - -- `세션 코드로 접속` -- 최근 접속 세션 기록 -- 세션 만료 안내 - -### 설정 - -- 세션 API 주소 -- 릴레이 지역 선택 -- 호스트 세션 자동 재등록 여부 - -## 프로필 스키마 확장 예정 - -나중에 `catalog.json` 또는 분리된 관리자 설정에 아래 필드가 추가될 수 있다. - -```json -{ - "sessionBackendUrl": "https://session.example.com", - "sessionJoinMode": "code", - "sessionCodeLength": 8, - "relayMode": "external-tunnel", - "relayRegion": "ap-northeast-2" -} -``` - -## 보안 고려사항 - -필수 항목: - -- 세션 생성은 인증된 사용자만 가능해야 함 -- 호스트 heartbeat가 끊기면 세션 자동 만료 -- 세션 코드는 충분히 예측 불가능해야 함 -- 동일 계정의 세션 수 제한 필요 -- 악의적 트래픽에 대한 rate limit 필요 -- 프로필 ID 불일치 세션 차단 또는 경고 필요 - -추가 고려: - -- 세션 코드에 만료 시간 포함 -- 백엔드 서명 토큰 사용 -- 서버 로그 업로드 여부 -- 접속자 IP/세션 기록 보관 정책 - -## 장애 대응 - -### 호스트 측 - -- 서버는 켜졌는데 릴레이 주소 발급 실패 -- 터널은 살아있는데 heartbeat 실패 -- 서버 프로세스 종료 후 세션 정리 누락 - -대응: - -- 런처가 재시도 정책 보유 -- 종료 이벤트에서 세션 삭제 호출 -- stale session 정리 배치 작업 필요 - -### 접속자 측 - -- 세션 코드는 맞는데 만료됨 -- 주소는 받았는데 릴레이가 이미 죽음 -- 잘못된 프로필로 접속 시도 - -대응: - -- 사용자에게 구체적인 실패 메시지 제공 -- 세션 재조회 버튼 -- 프로필 불일치 시 설치 페이지 이동 유도 - -## 구현 순서 제안 - -### 1단계 - -- Session API 설계 -- 세션 코드 발급/조회/만료 로직 구현 -- 런처에 세션 코드 입력 UI 추가 - -### 2단계 - -- 현재 `tunnelCommand` 기반 흐름과 Session API 연결 -- 호스트 세션 등록/heartbeat/종료 처리 -- 접속자 자동 접속 완료 - -### 3단계 - -- 자체 Relay 서비스 검토 -- 전용 호스트 인증 및 운영 정책 도입 -- 외부 터널 도구 의존도 축소 - -## 현재 코드와 연결되는 지점 - -- `app/assets/js/serverruntime.js` - - 서버 실행 후 공개 주소 확보 지점 -- `app/assets/js/scripts/library.js` - - 호스트 공개 주소 표시 및 수동 주소 입력 UI -- `app/assets/js/processbuilder.js` - - 자동 접속 인자 전달 지점 -- `app/assets/js/catalogmanager.js` - - 프로필 메타데이터 확장 지점 - -## 결론 - -“포트포워딩 없이 접속”은 런처 버튼 하나로 끝나는 기능처럼 보여도, 실제로는 런처만의 문제가 아니라 세션 백엔드와 릴레이 계층이 함께 있어야 성립한다. - -따라서 다음 구현 목표는 아래가 적절하다. - -1. `Session API` 설계 -2. `server-pack + tunnelCommand` 흐름을 세션 코드 모델과 연결 -3. 이후 전용 Relay 서비스로 확장 diff --git a/docs/sample_distribution.json b/docs/sample_distribution.json deleted file mode 100644 index af2bf74..0000000 --- a/docs/sample_distribution.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0", - "rss": "", - "servers": [ - { - "id": "example-profile", - "name": "Example Profile", - "description": "관리자 사이트에서 만든 distribution 템플릿입니다.", - "version": "1.20.1", - "minecraftVersion": "1.20.1", - "address": "127.0.0.1:25565", - "mainServer": true, - "autoconnect": false, - "modules": [] - } - ] -} diff --git a/electron-builder.yml b/electron-builder.yml deleted file mode 100644 index ec4963e..0000000 --- a/electron-builder.yml +++ /dev/null @@ -1,51 +0,0 @@ -appId: 'mrslauncher' -productName: 'MRS Launcher' -artifactName: 'MRS-Launcher-setup-${version}.${ext}' - -copyright: 'Copyright © 2018-2026 Daniel Scalzi, Copyright © 2024 peunsu' - -asar: true -compression: 'maximum' - -files: - - '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.nvmrc,eslint.config.mjs}' - -extraResources: - - 'libraries' - -# Windows Configuration -win: - target: - - target: 'nsis' - arch: 'x64' - -# Windows Installer Configuration -nsis: - oneClick: false - perMachine: false - allowElevation: true - allowToChangeInstallationDirectory: true - -# macOS Configuration -mac: - target: - - target: 'dmg' - arch: - - 'x64' - - 'arm64' - artifactName: 'MRS-Launcher-setup-${version}-${arch}.${ext}' - category: 'public.app-category.games' - -# Linux Configuration -linux: - target: 'AppImage' - maintainer: 'Daniel Scalzi, peunsu' - vendor: 'Daniel Scalzi, peunsu' - synopsis: 'Modded Minecraft Launcher' - description: 'Custom launcher which allows users to join modded servers. All mods, configurations, and updates are handled automatically.' - category: 'Game' - - -directories: - buildResources: 'build' - output: 'dist' \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 9a628f1..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import js from '@eslint/js'; -import { defineConfig } from 'eslint/config'; -import stylistic from '@stylistic/eslint-plugin'; -import globals from 'globals'; - -export default defineConfig( - { - ignores: ['**/dist/**', 'node_modules', 'eslint.config.mjs'], - }, - js.configs.recommended, - { - languageOptions: { - globals: globals.node, - ecmaVersion: 2024 - } - }, - { - files: ['**/*.js'], - plugins: { - '@stylistic': stylistic, - }, - rules: { - '@stylistic/semi': ['error', 'never'], - '@stylistic/quotes': ['error', 'single'], - '@stylistic/indent': ['error', 4], - '@stylistic/member-delimiter-style': ['error', { - multiline: { - delimiter: 'none', - requireLast: false - }, - singleline: { - delimiter: 'comma', - requireLast: false - } - }], - '@stylistic/linebreak-style': ['error', 'windows'], - 'no-var': ['error'], - 'no-control-regex': 'off', - 'no-unused-vars': ['error', { - vars: 'all', - args: 'after-used', - caughtErrorsIgnorePattern: '^_', - ignoreRestSiblings: false, - argsIgnorePattern: '^_|^reject$' - }], - } - }, - { - files: ['app/assets/js/scripts/*.js'], - rules: { - 'no-unused-vars': 'off', - 'no-undef': 'off' - } - } -); \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index b909bde..0000000 --- a/index.js +++ /dev/null @@ -1,16 +0,0 @@ -const fs = require('fs') -const path = require('path') - -const compiledMainPath = path.join(__dirname, 'dist', 'main', 'index.js') -const legacyMainPath = path.join(__dirname, 'index.legacy.js') - -if(fs.existsSync(compiledMainPath)){ - try { - require(compiledMainPath) - } catch (error) { - console.warn('[launcher] compiled TypeScript main process failed, falling back to legacy entry.', error) - require(legacyMainPath) - } -} else { - require(legacyMainPath) -} diff --git a/index.legacy.js b/index.legacy.js deleted file mode 100644 index 860ae0c..0000000 --- a/index.legacy.js +++ /dev/null @@ -1,371 +0,0 @@ -const remoteMain = require('@electron/remote/main') -remoteMain.initialize() - -// Requirements -const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron') -const autoUpdater = require('electron-updater').autoUpdater -const ejse = require('ejs-electron') -const fs = require('fs') -const isDev = require('./app/assets/js/isdev') -const path = require('path') -const semver = require('semver') -const { pathToFileURL } = require('url') -const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require('./app/assets/js/ipcconstants') -const LangLoader = require('./app/assets/js/langloader') -const smokeExitEnabled = process.env.LAUNCHER_SMOKE_EXIT === '1' -const smokeExitDelayMs = Number.parseInt(process.env.LAUNCHER_SMOKE_EXIT_DELAY_MS ?? '5000', 10) - -// Setup Lang -LangLoader.setupLanguage() - -// Setup auto updater. -function initAutoUpdater(event, data) { - - if(data){ - autoUpdater.allowPrerelease = true - } else { - // Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1) - // autoUpdater.allowPrerelease = true - } - - if(isDev){ - autoUpdater.autoInstallOnAppQuit = false - autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml') - } - if(process.platform === 'darwin'){ - autoUpdater.autoDownload = false - } - autoUpdater.on('update-available', (info) => { - event.sender.send('autoUpdateNotification', 'update-available', info) - }) - autoUpdater.on('update-downloaded', (info) => { - event.sender.send('autoUpdateNotification', 'update-downloaded', info) - }) - autoUpdater.on('update-not-available', (info) => { - event.sender.send('autoUpdateNotification', 'update-not-available', info) - }) - autoUpdater.on('checking-for-update', () => { - event.sender.send('autoUpdateNotification', 'checking-for-update') - }) - autoUpdater.on('error', (err) => { - event.sender.send('autoUpdateNotification', 'realerror', err) - }) -} - -// Open channel to listen for update actions. -ipcMain.on('autoUpdateAction', (event, arg, data) => { - switch(arg){ - case 'initAutoUpdater': - console.log('Initializing auto updater.') - initAutoUpdater(event, data) - event.sender.send('autoUpdateNotification', 'ready') - break - case 'checkForUpdate': - autoUpdater.checkForUpdates() - .catch(err => { - event.sender.send('autoUpdateNotification', 'realerror', err) - }) - break - case 'allowPrereleaseChange': - if(!data){ - const preRelComp = semver.prerelease(app.getVersion()) - if(preRelComp != null && preRelComp.length > 0){ - autoUpdater.allowPrerelease = true - } else { - autoUpdater.allowPrerelease = data - } - } else { - autoUpdater.allowPrerelease = data - } - break - case 'installUpdateNow': - autoUpdater.quitAndInstall() - break - default: - console.log('Unknown argument', arg) - break - } -}) -// Redirect distribution index event from preloader to renderer. -ipcMain.on('distributionIndexDone', (event, res) => { - event.sender.send('distributionIndexDone', res) -}) - -// Handle trash item. -ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => { - try { - await shell.trashItem(args[0]) - return { - result: true - } - } catch(error) { - return { - result: false, - error: error - } - } -}) - -// Disable hardware acceleration. -// https://electronjs.org/docs/tutorial/offscreen-rendering -app.disableHardwareAcceleration() - - -const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?' - -// Microsoft Auth Login -let msftAuthWindow -let msftAuthSuccess -let msftAuthViewSuccess -let msftAuthViewOnClose -ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => { - if (msftAuthWindow) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose) - return - } - msftAuthSuccess = false - msftAuthViewSuccess = arguments_[0] - msftAuthViewOnClose = arguments_[1] - msftAuthWindow = new BrowserWindow({ - title: LangLoader.queryJS('index.microsoftLoginTitle'), - backgroundColor: '#222222', - width: 520, - height: 600, - frame: true, - icon: getPlatformIcon('Icon') - }) - - msftAuthWindow.on('closed', () => { - msftAuthWindow = undefined - }) - - msftAuthWindow.on('close', () => { - if(!msftAuthSuccess) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose) - } - }) - - msftAuthWindow.webContents.on('did-navigate', (_, uri) => { - if (uri.startsWith(REDIRECT_URI_PREFIX)) { - let queryMap = {} - - new URL(uri).searchParams.forEach((v, k) => { - queryMap[k] = v; - }); - - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess) - - msftAuthSuccess = true - msftAuthWindow.close() - msftAuthWindow = null - } - }) - - msftAuthWindow.removeMenu() - msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`) -}) - -// Microsoft Auth Logout -let msftLogoutWindow -let msftLogoutSuccess -let msftLogoutSuccessSent -ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => { - if (msftLogoutWindow) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN) - return - } - - msftLogoutSuccess = false - msftLogoutSuccessSent = false - msftLogoutWindow = new BrowserWindow({ - title: LangLoader.queryJS('index.microsoftLogoutTitle'), - backgroundColor: '#222222', - width: 520, - height: 600, - frame: true, - icon: getPlatformIcon('Icon') - }) - - msftLogoutWindow.on('closed', () => { - msftLogoutWindow = undefined - }) - - msftLogoutWindow.on('close', () => { - if(!msftLogoutSuccess) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED) - } else if(!msftLogoutSuccessSent) { - msftLogoutSuccessSent = true - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) - } - }) - - msftLogoutWindow.webContents.on('did-navigate', (_, uri) => { - if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) { - msftLogoutSuccess = true - setTimeout(() => { - if(!msftLogoutSuccessSent) { - msftLogoutSuccessSent = true - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) - } - - if(msftLogoutWindow) { - msftLogoutWindow.close() - msftLogoutWindow = null - } - }, 5000) - } - }) - - msftLogoutWindow.removeMenu() - msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout') -}) - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -let win - -function createWindow() { - - win = new BrowserWindow({ - width: 1120, - height: 700, - minWidth: 960, - minHeight: 600, - icon: getPlatformIcon('Icon'), - frame: false, - webPreferences: { - preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'), - nodeIntegration: true, - contextIsolation: false - }, - backgroundColor: '#171614' - }) - remoteMain.enable(win.webContents) - - const data = { - bkid: Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)), - lang: (str, placeHolders) => LangLoader.queryEJS(str, placeHolders) - } - Object.entries(data).forEach(([key, val]) => ejse.data(key, val)) - - win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString()) - - if(smokeExitEnabled){ - win.webContents.once('did-finish-load', () => { - setTimeout(() => { - app.quit() - }, Number.isFinite(smokeExitDelayMs) ? smokeExitDelayMs : 5000) - }) - } - - /*win.once('ready-to-show', () => { - win.show() - })*/ - - win.removeMenu() - - win.resizable = true - - win.on('closed', () => { - win = null - }) -} - -function createMenu() { - - if(process.platform === 'darwin') { - - // Extend default included application menu to continue support for quit keyboard shortcut - let applicationSubMenu = { - label: 'Application', - submenu: [{ - label: 'About Application', - selector: 'orderFrontStandardAboutPanel:' - }, { - type: 'separator' - }, { - label: 'Quit', - accelerator: 'Command+Q', - click: () => { - app.quit() - } - }] - } - - // New edit menu adds support for text-editing keyboard shortcuts - let editSubMenu = { - label: 'Edit', - submenu: [{ - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - selector: 'undo:' - }, { - label: 'Redo', - accelerator: 'Shift+CmdOrCtrl+Z', - selector: 'redo:' - }, { - type: 'separator' - }, { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - selector: 'cut:' - }, { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - selector: 'copy:' - }, { - label: 'Paste', - accelerator: 'CmdOrCtrl+V', - selector: 'paste:' - }, { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - selector: 'selectAll:' - }] - } - - // Bundle submenus into a single template and build a menu object with it - let menuTemplate = [applicationSubMenu, editSubMenu] - let menuObject = Menu.buildFromTemplate(menuTemplate) - - // Assign it to the application - Menu.setApplicationMenu(menuObject) - - } - -} - -function getPlatformIcon(filename){ - let ext - switch(process.platform) { - case 'win32': - ext = 'ico' - break - case 'darwin': - case 'linux': - default: - ext = 'png' - break - } - - return path.join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`) -} - -app.on('ready', createWindow) -app.on('ready', createMenu) - -app.on('window-all-closed', () => { - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit() - } -}) - -app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - createWindow() - } -}) diff --git a/libraries/java/PackXZExtract.jar b/libraries/java/PackXZExtract.jar deleted file mode 100644 index 9a779a7..0000000 Binary files a/libraries/java/PackXZExtract.jar and /dev/null differ diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 7962cd2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6902 +0,0 @@ -{ - "name": "mrslauncher", - "version": "3.0.9", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mrslauncher", - "version": "3.0.9", - "license": "UNLICENSED", - "dependencies": { - "@electron/remote": "^2.1.3", - "adm-zip": "^0.5.16", - "discord-rpc-patch": "^4.0.1", - "ejs": "^3.1.10", - "ejs-electron": "^3.0.0", - "electron-updater": "^6.7.3", - "express": "^5.2.1", - "fs-extra": "^11.3.3", - "github-syntax-dark": "^0.5.0", - "got": "^11.8.5", - "helios-core": "~2.3.0", - "helios-distribution-types": "^1.3.0", - "jquery": "^3.7.1", - "lodash.merge": "^4.6.2", - "multer": "^2.1.1", - "nat-upnp": "^1.1.1", - "semver": "^7.7.3", - "toml": "^3.0.0" - }, - "devDependencies": { - "@stylistic/eslint-plugin": "^5.6.1", - "@types/node": "^22.0.0", - "electron": "^39.2.7", - "electron-builder": "^26.4.0", - "eslint": "^9.39.2", - "typescript": "^5.0.0" - }, - "engines": { - "node": "22.x.x" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@electron/get/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "got": "^11.7.0", - "graceful-fs": "^4.2.11", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^11.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^7.5.6", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/remote": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.3.tgz", - "integrity": "sha512-XlpxC8S4ttj/v2d+PKp9na/3Ev8bV7YWNL7Cw5b9MAWgTphEml7iYgbc7V0r9D6yDOfOkj06bchZgOZdlWJGNA==", - "license": "MIT", - "peerDependencies": { - "electron": ">= 13.0.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, - "node_modules/@stylistic/eslint-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.6.1.tgz", - "integrity": "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.0", - "@typescript-eslint/types": "^8.47.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "estraverse": "^5.3.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "license": "MIT", - "engines": { - "node": ">=12.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.6.0.tgz", - "integrity": "sha512-P2naoSaGOqJY54cqTceO9lms2M790UM7BA8AlOuaolQhRp/LOshAVc4vzVlYFw4YNPtiuBJqdAhWALuoEKnayQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.6.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.6", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.6.0", - "electron-builder-squirrel-windows": "26.6.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, - "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", - "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", - "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", - "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", - "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "license": "MIT", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/discord-rpc-patch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/discord-rpc-patch/-/discord-rpc-patch-4.0.1.tgz", - "integrity": "sha512-rnHZzNzUcSNdPZCRf18Nza6Nir4i3ljO0HIoSRadD0uEQwKa8PgqCf/tLUr/HJyJQ3NuXNHACMsOC6/JRhojmQ==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "ws": "^7.3.1" - } - }, - "node_modules/dmg-builder": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.6.0.tgz", - "integrity": "sha512-IkGlOLfJ3q7y9iaDMnNSArDdPg3Ntx8Ps6aL7yTEIpL6znA+t5L/LRTAGFz1J/12hM/NiNEYg0LoBEheqGdZXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.6.0", - "builder-util": "26.4.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ejs-electron": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ejs-electron/-/ejs-electron-3.0.0.tgz", - "integrity": "sha512-U84psVz+oyyo06N+dOvb6FRVyZj/Pl2MLt7Iayq8DnlOz8jc1pfnGbxCeBQqbqNCkJpiS6MOPcPbRMvaLukf3g==", - "license": "MIT", - "dependencies": { - "ejs": "^3.1.9", - "mime": "^3.0.0" - } - }, - "node_modules/electron": { - "version": "39.2.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", - "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.6.0.tgz", - "integrity": "sha512-57JzccIwhqVRw83RaTdMLnSjzLL0dRQcp8r8oD7piRNBQh8UcCPaKeFmuJIzJabAAvQhG0+gx3F0pOVEOVXYwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.6.0", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.6.0", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.6.0.tgz", - "integrity": "sha512-uKc/N0qPcygd2YDr52wfj07XOJPMG5KNT1ZTrumtmsykdBGreV1/poDcG5d/0KmoOpmxlkrnNJekM3eDvPzlQQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.6.0", - "builder-util": "26.4.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", - "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/electron-updater": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", - "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", - "license": "MIT", - "dependencies": { - "builder-util-runtime": "9.5.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/electron-winstaller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-winstaller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/github-syntax-dark": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/github-syntax-dark/-/github-syntax-dark-0.5.0.tgz", - "integrity": "sha512-RoFaoOEmvX25k0FEV4PJ0y5McWZOI6ZKfFee7YzxKUzz2SErzFec9f2IpW2zbijzImx8s9Zsd5YvO01XxX7xVg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "optional": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helios-core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/helios-core/-/helios-core-2.3.0.tgz", - "integrity": "sha512-duePk0XCqpZartpluj2Z9sc9EAlOVk7It7jD2RdbwuGAblH6gL9wga+9eCIy1/z2oRlURaSWZDyx/EucpoLrTQ==", - "license": " LGPL-3.0-only", - "dependencies": { - "fastq": "^1.20.1", - "fs-extra": "^11.3.3", - "got": "^11.8.6", - "luxon": "^3.7.2", - "node-stream-zip": "^1.15.0", - "semver": "^7.7.3", - "tar-fs": "^3.1.1", - "triple-beam": "^1.4.1", - "winreg": "^1.2.5", - "winston": "^3.19.0" - } - }, - "node_modules/helios-distribution-types": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/helios-distribution-types/-/helios-distribution-types-1.3.0.tgz", - "integrity": "sha512-MP66JRHvmuE9yDoZoKeFDh3stsHger0w/cRcJAlV7UYw5ztR3m/uLbWdbfFV68B1Yc0+hDIiuFsuJT/Ve9xuiw==", - "license": "UNLICENSED" - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" - }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/jsprim/node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/jsprim/node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "type-is": "^1.6.18" - }, - "engines": { - "node": ">= 10.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nat-upnp": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nat-upnp/-/nat-upnp-1.1.1.tgz", - "integrity": "sha512-b1Q+sf9fHGCXhlWErNgTTEto8A02MnNysw3vx3kD1657+/Ae23vPEAB6QBh+9RqLL4+xw/LmjVTiLy6A7Cx0xw==", - "dependencies": { - "async": "^2.1.5", - "ip": "^1.1.4", - "request": "^2.79.0", - "xml2js": "~0.1.14" - } - }, - "node_modules/nat-upnp/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", - "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-stream-zip": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/antelle" - } - }, - "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", - "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", - "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "license": "BlueOak-1.0.0" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT", - "optional": true - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/winreg": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.5.tgz", - "integrity": "sha512-uf7tHf+tw0B1y+x+mKTLHkykBgK2KMs3g+KlzmyMbLvICSHQyB/xOFjTT8qZ3oeTFyU7Bbj4FzXitGG6jvKhYw==", - "license": "BSD-2-Clause" - }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml2js": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.1.14.tgz", - "integrity": "sha512-pbdws4PPPNc1HPluSUKamY4GWMk592K7qwcj6BExbVOhhubub8+pMda/ql68b6L3luZs/OGjGSB5goV7SnmgnA==", - "dependencies": { - "sax": ">=0.1.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7621a34..0000000 --- a/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "mrslauncher", - "version": "3.0.9", - "productName": "MRS Launcher", - "description": "Modded Minecraft Launcher", - "author": "Daniel Scalzi (https://github.com/dscalzi/), peunsu (https://github.com/peunsu)", - "license": "UNLICENSED", - "homepage": "https://github.com/peunsu/MRSLauncher", - "bugs": { - "url": "https://github.com/peunsu/MRSLauncher/issues" - }, - "private": true, - "main": "index.js", - "scripts": { - "admin": "node admin/server.js", - "build": "tsc -p tsconfig.json", - "start": "npm run build && electron .", - "smoke": "npm run build && node scripts/smoke-runner.js", - "smoke:win": "npm run smoke", - "dist": "npm run build && electron-builder build", - "dist:win": "npm run dist -- -w", - "dist:mac": "npm run dist -- -m", - "dist:linux": "npm run dist -- -l", - "lint": "eslint ." - }, - "engines": { - "node": "22.x.x" - }, - "dependencies": { - "@electron/remote": "^2.1.3", - "adm-zip": "^0.5.16", - "discord-rpc-patch": "^4.0.1", - "ejs": "^3.1.10", - "ejs-electron": "^3.0.0", - "electron-updater": "^6.7.3", - "express": "^5.2.1", - "fs-extra": "^11.3.3", - "github-syntax-dark": "^0.5.0", - "got": "^11.8.5", - "helios-core": "~2.3.0", - "helios-distribution-types": "^1.3.0", - "jquery": "^3.7.1", - "lodash.merge": "^4.6.2", - "multer": "^2.1.1", - "nat-upnp": "^1.1.1", - "semver": "^7.7.3", - "toml": "^3.0.0" - }, - "devDependencies": { - "@stylistic/eslint-plugin": "^5.6.1", - "@types/node": "^22.0.0", - "electron": "^39.2.7", - "electron-builder": "^26.4.0", - "eslint": "^9.39.2", - "typescript": "^5.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/peunsu/MRSLauncher.git" - } -} diff --git a/scripts/smoke-runner.js b/scripts/smoke-runner.js deleted file mode 100644 index aa51f8c..0000000 --- a/scripts/smoke-runner.js +++ /dev/null @@ -1,24 +0,0 @@ -const { spawn } = require('child_process') -const electronBinary = require('electron') - -const electronArgs = ['.'] - -if(process.platform === 'linux'){ - electronArgs.push('--no-sandbox', '--disable-gpu') -} - -const child = spawn(electronBinary, electronArgs, { - stdio: 'inherit', - env: { - ...process.env, - LAUNCHER_SMOKE_EXIT: '1', - LAUNCHER_SMOKE_EXIT_DELAY_MS: process.env.LAUNCHER_SMOKE_EXIT_DELAY_MS || '5000' - } -}) - -child.on('exit', (code, signal) => { - if(signal != null){ - process.exit(1) - } - process.exit(code ?? 0) -}) diff --git a/src/main/index.ts b/src/main/index.ts deleted file mode 100644 index 8a4b77e..0000000 --- a/src/main/index.ts +++ /dev/null @@ -1,372 +0,0 @@ -const remoteMain = require('@electron/remote/main') -remoteMain.initialize() - -// Requirements -const { app, BrowserWindow, ipcMain, Menu, shell } = require('electron') -const autoUpdater = require('electron-updater').autoUpdater -const ejse = require('ejs-electron') -const fs = require('fs') -const path = require('path') -const semver = require('semver') -const { pathToFileURL } = require('url') -const appRoot = path.join(__dirname, '..', '..') -const isDev = require(path.join(appRoot, 'app', 'assets', 'js', 'isdev')) -const { AZURE_CLIENT_ID, MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR, SHELL_OPCODE } = require(path.join(appRoot, 'app', 'assets', 'js', 'ipcconstants')) -const LangLoader = require(path.join(appRoot, 'app', 'assets', 'js', 'langloader')) -const smokeExitEnabled = process.env.LAUNCHER_SMOKE_EXIT === '1' -const smokeExitDelayMs = Number.parseInt(process.env.LAUNCHER_SMOKE_EXIT_DELAY_MS ?? '5000', 10) - -// Setup Lang -LangLoader.setupLanguage() - -// Setup auto updater. -function initAutoUpdater(event, data) { - - if(data){ - autoUpdater.allowPrerelease = true - } else { - // Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1) - // autoUpdater.allowPrerelease = true - } - - if(isDev){ - autoUpdater.autoInstallOnAppQuit = false - autoUpdater.updateConfigPath = path.join(appRoot, 'dev-app-update.yml') - } - if(process.platform === 'darwin'){ - autoUpdater.autoDownload = false - } - autoUpdater.on('update-available', (info) => { - event.sender.send('autoUpdateNotification', 'update-available', info) - }) - autoUpdater.on('update-downloaded', (info) => { - event.sender.send('autoUpdateNotification', 'update-downloaded', info) - }) - autoUpdater.on('update-not-available', (info) => { - event.sender.send('autoUpdateNotification', 'update-not-available', info) - }) - autoUpdater.on('checking-for-update', () => { - event.sender.send('autoUpdateNotification', 'checking-for-update') - }) - autoUpdater.on('error', (err) => { - event.sender.send('autoUpdateNotification', 'realerror', err) - }) -} - -// Open channel to listen for update actions. -ipcMain.on('autoUpdateAction', (event, arg, data) => { - switch(arg){ - case 'initAutoUpdater': - console.log('Initializing auto updater.') - initAutoUpdater(event, data) - event.sender.send('autoUpdateNotification', 'ready') - break - case 'checkForUpdate': - autoUpdater.checkForUpdates() - .catch(err => { - event.sender.send('autoUpdateNotification', 'realerror', err) - }) - break - case 'allowPrereleaseChange': - if(!data){ - const preRelComp = semver.prerelease(app.getVersion()) - if(preRelComp != null && preRelComp.length > 0){ - autoUpdater.allowPrerelease = true - } else { - autoUpdater.allowPrerelease = data - } - } else { - autoUpdater.allowPrerelease = data - } - break - case 'installUpdateNow': - autoUpdater.quitAndInstall() - break - default: - console.log('Unknown argument', arg) - break - } -}) -// Redirect distribution index event from preloader to renderer. -ipcMain.on('distributionIndexDone', (event, res) => { - event.sender.send('distributionIndexDone', res) -}) - -// Handle trash item. -ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => { - try { - await shell.trashItem(args[0]) - return { - result: true - } - } catch(error) { - return { - result: false, - error: error - } - } -}) - -// Disable hardware acceleration. -// https://electronjs.org/docs/tutorial/offscreen-rendering -app.disableHardwareAcceleration() - - -const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?' - -// Microsoft Auth Login -let msftAuthWindow -let msftAuthSuccess -let msftAuthViewSuccess -let msftAuthViewOnClose -ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => { - if (msftAuthWindow) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose) - return - } - msftAuthSuccess = false - msftAuthViewSuccess = arguments_[0] - msftAuthViewOnClose = arguments_[1] - msftAuthWindow = new BrowserWindow({ - title: LangLoader.queryJS('index.microsoftLoginTitle'), - backgroundColor: '#222222', - width: 520, - height: 600, - frame: true, - icon: getPlatformIcon('Icon') - }) - - msftAuthWindow.on('closed', () => { - msftAuthWindow = undefined - }) - - msftAuthWindow.on('close', () => { - if(!msftAuthSuccess) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose) - } - }) - - msftAuthWindow.webContents.on('did-navigate', (_, uri) => { - if (uri.startsWith(REDIRECT_URI_PREFIX)) { - let queryMap = {} - - new URL(uri).searchParams.forEach((v, k) => { - queryMap[k] = v; - }); - - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess) - - msftAuthSuccess = true - msftAuthWindow.close() - msftAuthWindow = null - } - }) - - msftAuthWindow.removeMenu() - msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${AZURE_CLIENT_ID}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`) -}) - -// Microsoft Auth Logout -let msftLogoutWindow -let msftLogoutSuccess -let msftLogoutSuccessSent -ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => { - if (msftLogoutWindow) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN) - return - } - - msftLogoutSuccess = false - msftLogoutSuccessSent = false - msftLogoutWindow = new BrowserWindow({ - title: LangLoader.queryJS('index.microsoftLogoutTitle'), - backgroundColor: '#222222', - width: 520, - height: 600, - frame: true, - icon: getPlatformIcon('Icon') - }) - - msftLogoutWindow.on('closed', () => { - msftLogoutWindow = undefined - }) - - msftLogoutWindow.on('close', () => { - if(!msftLogoutSuccess) { - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED) - } else if(!msftLogoutSuccessSent) { - msftLogoutSuccessSent = true - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) - } - }) - - msftLogoutWindow.webContents.on('did-navigate', (_, uri) => { - if(uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) { - msftLogoutSuccess = true - setTimeout(() => { - if(!msftLogoutSuccessSent) { - msftLogoutSuccessSent = true - ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount) - } - - if(msftLogoutWindow) { - msftLogoutWindow.close() - msftLogoutWindow = null - } - }, 5000) - } - }) - - msftLogoutWindow.removeMenu() - msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout') -}) - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -let win - -function createWindow() { - - win = new BrowserWindow({ - width: 1120, - height: 700, - minWidth: 960, - minHeight: 600, - icon: getPlatformIcon('Icon'), - frame: false, - webPreferences: { - preload: path.join(appRoot, 'app', 'assets', 'js', 'preloader.js'), - nodeIntegration: true, - contextIsolation: false - }, - backgroundColor: '#171614' - }) - remoteMain.enable(win.webContents) - - const data = { - bkid: Math.floor((Math.random() * fs.readdirSync(path.join(appRoot, 'app', 'assets', 'images', 'backgrounds')).length)), - lang: (str, placeHolders) => LangLoader.queryEJS(str, placeHolders) - } - Object.entries(data).forEach(([key, val]) => ejse.data(key, val)) - - win.loadURL(pathToFileURL(path.join(appRoot, 'app', 'app.ejs')).toString()) - - if(smokeExitEnabled){ - win.webContents.once('did-finish-load', () => { - setTimeout(() => { - app.quit() - }, Number.isFinite(smokeExitDelayMs) ? smokeExitDelayMs : 5000) - }) - } - - /*win.once('ready-to-show', () => { - win.show() - })*/ - - win.removeMenu() - - win.resizable = true - - win.on('closed', () => { - win = null - }) -} - -function createMenu() { - - if(process.platform === 'darwin') { - - // Extend default included application menu to continue support for quit keyboard shortcut - let applicationSubMenu = { - label: 'Application', - submenu: [{ - label: 'About Application', - selector: 'orderFrontStandardAboutPanel:' - }, { - type: 'separator' - }, { - label: 'Quit', - accelerator: 'Command+Q', - click: () => { - app.quit() - } - }] - } - - // New edit menu adds support for text-editing keyboard shortcuts - let editSubMenu = { - label: 'Edit', - submenu: [{ - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - selector: 'undo:' - }, { - label: 'Redo', - accelerator: 'Shift+CmdOrCtrl+Z', - selector: 'redo:' - }, { - type: 'separator' - }, { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - selector: 'cut:' - }, { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - selector: 'copy:' - }, { - label: 'Paste', - accelerator: 'CmdOrCtrl+V', - selector: 'paste:' - }, { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - selector: 'selectAll:' - }] - } - - // Bundle submenus into a single template and build a menu object with it - let menuTemplate = [applicationSubMenu, editSubMenu] - let menuObject = Menu.buildFromTemplate(menuTemplate) - - // Assign it to the application - Menu.setApplicationMenu(menuObject) - - } - -} - -function getPlatformIcon(filename){ - let ext - switch(process.platform) { - case 'win32': - ext = 'ico' - break - case 'darwin': - case 'linux': - default: - ext = 'png' - break - } - - return path.join(appRoot, 'app', 'assets', 'images', `${filename}.${ext}`) -} - -app.on('ready', createWindow) -app.on('ready', createMenu) - -app.on('window-all-closed', () => { - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit() - } -}) - -app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - createWindow() - } -}) diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts deleted file mode 100644 index c63dac7..0000000 --- a/src/types/shims.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@electron/remote/main' -declare module 'ejs-electron' -declare module 'electron-updater' -declare module 'semver' diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index cff61c4..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", - "rootDir": "src", - "outDir": "dist", - "strict": false, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmitOnError": false, - "resolveJsonModule": true, - "types": ["node"] - }, - "include": [ - "src/**/*.ts", - "src/**/*.d.ts" - ] -}