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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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 @@
-
\ 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.