Enable remote launcher catalog source
This commit is contained in:
@@ -32,6 +32,13 @@ npm run admin
|
|||||||
- 월드 ZIP 업로드 가능
|
- 월드 ZIP 업로드 가능
|
||||||
- 서버용 버킷 JAR 업로드 가능
|
- 서버용 버킷 JAR 업로드 가능
|
||||||
- 서버 메모리, 최대 인원수, 화이트리스트, 포트 설정 가능
|
- 서버 메모리, 최대 인원수, 화이트리스트, 포트 설정 가능
|
||||||
|
- 상단의 `앱 연결용 catalog URL`을 런처 설정의 `카탈로그 주소`에 넣으면 앱이 원격 catalog를 읽습니다.
|
||||||
|
|
||||||
|
공개 주소로 관리자 사이트를 띄울 때:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LAUNCHER_PUBLIC_BASE_URL=https://your-domain.example npm run admin
|
||||||
|
```
|
||||||
|
|
||||||
문서:
|
문서:
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<code id="launcherCatalogPath">불러오는 중</code>
|
<code id="launcherCatalogPath">불러오는 중</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="metaRow">
|
<div class="metaRow">
|
||||||
<span class="metaLabel">로컬 catalog URL</span>
|
<span class="metaLabel">앱 연결용 catalog URL</span>
|
||||||
<a id="localCatalogUrl" href="#" target="_blank" rel="noreferrer">불러오는 중</a>
|
<a id="localCatalogUrl" href="#" target="_blank" rel="noreferrer">불러오는 중</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const LAUNCHER_CATALOG_PATH = path.join(PROJECT_ROOT, 'app', 'assets', 'launcher
|
|||||||
const PUBLIC_DIR = path.join(__dirname, 'public')
|
const PUBLIC_DIR = path.join(__dirname, 'public')
|
||||||
const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json')
|
const SAMPLE_DISTRIBUTION_PATH = path.join(PROJECT_ROOT, 'docs', 'sample_distribution.json')
|
||||||
const EXAMPLE_DISTRIBUTION_PREFIX = 'https://example.com/launcher/'
|
const EXAMPLE_DISTRIBUTION_PREFIX = 'https://example.com/launcher/'
|
||||||
|
const PUBLIC_BASE_URL = normalizeText(process.env.LAUNCHER_PUBLIC_BASE_URL).replace(/\/+$/, '')
|
||||||
|
|
||||||
function normalizeText(value){
|
function normalizeText(value){
|
||||||
return typeof value === 'string' ? value.trim() : ''
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
@@ -45,6 +46,13 @@ function normalizeBoolean(value){
|
|||||||
return value === true
|
return value === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPublicBaseUrl(){
|
||||||
|
if(PUBLIC_BASE_URL.length > 0){
|
||||||
|
return PUBLIC_BASE_URL
|
||||||
|
}
|
||||||
|
return `http://${HOST}:${PORT}`
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSafeProjectPath(relativePath){
|
function resolveSafeProjectPath(relativePath){
|
||||||
const resolvedPath = path.resolve(PROJECT_ROOT, relativePath)
|
const resolvedPath = path.resolve(PROJECT_ROOT, relativePath)
|
||||||
if(!resolvedPath.startsWith(PROJECT_ROOT + path.sep) && resolvedPath !== PROJECT_ROOT){
|
if(!resolvedPath.startsWith(PROJECT_ROOT + path.sep) && resolvedPath !== PROJECT_ROOT){
|
||||||
@@ -259,6 +267,7 @@ async function start(){
|
|||||||
|
|
||||||
app.use(express.json({ limit: '5mb' }))
|
app.use(express.json({ limit: '5mb' }))
|
||||||
app.use('/uploads', express.static(UPLOADS_DIR))
|
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.use('/admin/data/distributions', express.static(DISTRIBUTIONS_DIR))
|
||||||
|
|
||||||
app.get('/api/meta', async (_req, res) => {
|
app.get('/api/meta', async (_req, res) => {
|
||||||
@@ -267,7 +276,7 @@ async function start(){
|
|||||||
port: PORT,
|
port: PORT,
|
||||||
runtimeCatalogPath: toProjectRelativePath(RUNTIME_CATALOG_PATH),
|
runtimeCatalogPath: toProjectRelativePath(RUNTIME_CATALOG_PATH),
|
||||||
launcherCatalogPath: toProjectRelativePath(LAUNCHER_CATALOG_PATH),
|
launcherCatalogPath: toProjectRelativePath(LAUNCHER_CATALOG_PATH),
|
||||||
localCatalogUrl: `http://${HOST}:${PORT}/catalog.json`,
|
localCatalogUrl: `${getPublicBaseUrl()}/catalog.json`,
|
||||||
distributionsPath: toProjectRelativePath(DISTRIBUTIONS_DIR)
|
distributionsPath: toProjectRelativePath(DISTRIBUTIONS_DIR)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -362,7 +371,7 @@ async function start(){
|
|||||||
storedName: req.file.filename,
|
storedName: req.file.filename,
|
||||||
size: req.file.size,
|
size: req.file.size,
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
localUrl: `http://${HOST}:${PORT}/uploads/${encodeURIComponent(req.file.filename)}`
|
localUrl: `${getPublicBaseUrl()}/uploads/${encodeURIComponent(req.file.filename)}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -394,7 +403,7 @@ async function start(){
|
|||||||
ok: true,
|
ok: true,
|
||||||
file: {
|
file: {
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
localUrl: `http://${HOST}:${PORT}/${relativePath}`
|
localUrl: `${getPublicBaseUrl()}/${relativePath}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const ConfigManager = require('./configmanager')
|
|||||||
const { DEFAULT_REMOTE_DISTRO_URL, setRemoteDistributionUrl } = require('./distromanager')
|
const { DEFAULT_REMOTE_DISTRO_URL, setRemoteDistributionUrl } = require('./distromanager')
|
||||||
|
|
||||||
const LOCAL_CATALOG_PATH = path.join(__dirname, '..', 'launcher', 'catalog.json')
|
const LOCAL_CATALOG_PATH = path.join(__dirname, '..', 'launcher', 'catalog.json')
|
||||||
|
const ROOT_ASSET_PATH = path.join(__dirname, '..', '..', '..')
|
||||||
|
const DEFAULT_REMOTE_CATALOG_URL = normalizeNullableText(process.env.LAUNCHER_CATALOG_URL) || 'http://127.0.0.1:8787/catalog.json'
|
||||||
|
|
||||||
function normalizeText(value){
|
function normalizeText(value){
|
||||||
return typeof value === 'string' ? value.trim() : ''
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
@@ -28,6 +30,14 @@ function normalizePositiveInteger(value, fallback, minimum = 1){
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRemoteSource(source){
|
||||||
|
return /^https?:\/\//i.test(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAbsoluteFileSystemPath(source){
|
||||||
|
return path.isAbsolute(source) || /^[a-zA-Z]:[\\/]/.test(source)
|
||||||
|
}
|
||||||
|
|
||||||
function deriveFeatureFlags(rawProfile){
|
function deriveFeatureFlags(rawProfile){
|
||||||
const legacyKind = normalizeText(rawProfile?.kind)
|
const legacyKind = normalizeText(rawProfile?.kind)
|
||||||
|
|
||||||
@@ -77,7 +87,24 @@ function resolveLegacyServerJar(rawProfile){
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function toStoredProfile(rawProfile){
|
function resolveProfileSourceValue(value, catalogSource){
|
||||||
|
const nextValue = normalizeNullableText(value)
|
||||||
|
if(nextValue == null){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isRemoteSource(nextValue) || nextValue.startsWith('file://') || isAbsoluteFileSystemPath(nextValue)){
|
||||||
|
return nextValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isRemoteSource(catalogSource)){
|
||||||
|
return new URL(nextValue, catalogSource).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(ROOT_ASSET_PATH, nextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStoredProfile(rawProfile, catalogSource){
|
||||||
const flags = deriveFeatureFlags(rawProfile)
|
const flags = deriveFeatureFlags(rawProfile)
|
||||||
const storedProfile = {
|
const storedProfile = {
|
||||||
id: normalizeText(rawProfile.id),
|
id: normalizeText(rawProfile.id),
|
||||||
@@ -85,19 +112,19 @@ function toStoredProfile(rawProfile){
|
|||||||
kind: deriveLegacyKind(flags),
|
kind: deriveLegacyKind(flags),
|
||||||
description: normalizeText(rawProfile.description),
|
description: normalizeText(rawProfile.description),
|
||||||
details: normalizeText(rawProfile.details),
|
details: normalizeText(rawProfile.details),
|
||||||
distributionUrl: normalizeNullableText(rawProfile.distributionUrl),
|
distributionUrl: resolveProfileSourceValue(rawProfile.distributionUrl, catalogSource),
|
||||||
modsEnabled: flags.modsEnabled,
|
modsEnabled: flags.modsEnabled,
|
||||||
pluginsEnabled: flags.pluginsEnabled,
|
pluginsEnabled: flags.pluginsEnabled,
|
||||||
serverEnabled: flags.serverEnabled,
|
serverEnabled: flags.serverEnabled,
|
||||||
worldArchiveUrl: normalizeNullableText(rawProfile.worldArchiveUrl),
|
worldArchiveUrl: resolveProfileSourceValue(rawProfile.worldArchiveUrl, catalogSource),
|
||||||
worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName),
|
worldDirectoryName: normalizeNullableText(rawProfile.worldDirectoryName),
|
||||||
serverJarUrl: resolveLegacyServerJar(rawProfile),
|
serverJarUrl: resolveProfileSourceValue(resolveLegacyServerJar(rawProfile), catalogSource),
|
||||||
serverDirectoryName: normalizeText(rawProfile.serverDirectoryName) || 'server',
|
serverDirectoryName: normalizeText(rawProfile.serverDirectoryName) || 'server',
|
||||||
serverPort: Number.isFinite(Number(rawProfile.serverPort)) ? Number(rawProfile.serverPort) : 25565,
|
serverPort: Number.isFinite(Number(rawProfile.serverPort)) ? Number(rawProfile.serverPort) : 25565,
|
||||||
serverMemoryMb: normalizePositiveInteger(rawProfile.serverMemoryMb, 4096, 512),
|
serverMemoryMb: normalizePositiveInteger(rawProfile.serverMemoryMb, 4096, 512),
|
||||||
serverMaxPlayers: normalizePositiveInteger(rawProfile.serverMaxPlayers, 20, 1),
|
serverMaxPlayers: normalizePositiveInteger(rawProfile.serverMaxPlayers, 20, 1),
|
||||||
serverWhitelistEnabled: normalizeBoolean(rawProfile.serverWhitelistEnabled),
|
serverWhitelistEnabled: normalizeBoolean(rawProfile.serverWhitelistEnabled),
|
||||||
artwork: normalizeText(rawProfile.artwork)
|
artwork: resolveProfileSourceValue(rawProfile.artwork, catalogSource) ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!storedProfile.serverEnabled){
|
if(!storedProfile.serverEnabled){
|
||||||
@@ -112,8 +139,8 @@ function toStoredProfile(rawProfile){
|
|||||||
return storedProfile
|
return storedProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProfile(rawProfile, sourceType = 'catalog'){
|
function normalizeProfile(rawProfile, sourceType = 'catalog', catalogSource = LOCAL_CATALOG_PATH){
|
||||||
const storedProfile = toStoredProfile(rawProfile)
|
const storedProfile = toStoredProfile(rawProfile, catalogSource)
|
||||||
const launchIssues = []
|
const launchIssues = []
|
||||||
const hostIssues = []
|
const hostIssues = []
|
||||||
|
|
||||||
@@ -168,19 +195,39 @@ exports.getLocalCatalogPath = function(){
|
|||||||
return LOCAL_CATALOG_PATH
|
return LOCAL_CATALOG_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getDefaultRemoteCatalogUrl = function(){
|
||||||
|
return DEFAULT_REMOTE_CATALOG_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCatalogSourceCandidates(configuredSource){
|
||||||
|
const trimmedSource = normalizeNullableText(configuredSource)
|
||||||
|
if(trimmedSource != null){
|
||||||
|
return [trimmedSource]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [DEFAULT_REMOTE_CATALOG_URL, LOCAL_CATALOG_PATH]
|
||||||
|
}
|
||||||
|
|
||||||
exports.loadCatalog = async function(){
|
exports.loadCatalog = async function(){
|
||||||
const configuredSource = ConfigManager.getLibraryCatalogSource()
|
const configuredSource = ConfigManager.getLibraryCatalogSource()
|
||||||
const source = configuredSource != null && configuredSource.trim().length > 0 ? configuredSource.trim() : LOCAL_CATALOG_PATH
|
const sourceCandidates = buildCatalogSourceCandidates(configuredSource)
|
||||||
let rawCatalog = {
|
let rawCatalog = {
|
||||||
version: 1,
|
version: 1,
|
||||||
profiles: []
|
profiles: []
|
||||||
}
|
}
|
||||||
let sourceError = null
|
let sourceError = null
|
||||||
|
let source = sourceCandidates[sourceCandidates.length - 1]
|
||||||
|
|
||||||
try {
|
for(const candidate of sourceCandidates){
|
||||||
rawCatalog = await readCatalogSource(source)
|
try {
|
||||||
} catch (error) {
|
rawCatalog = await readCatalogSource(candidate)
|
||||||
sourceError = error
|
source = candidate
|
||||||
|
sourceError = null
|
||||||
|
break
|
||||||
|
} catch (error) {
|
||||||
|
source = candidate
|
||||||
|
sourceError = error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawProfiles = Array.isArray(rawCatalog.profiles) ? rawCatalog.profiles : []
|
const rawProfiles = Array.isArray(rawCatalog.profiles) ? rawCatalog.profiles : []
|
||||||
@@ -191,7 +238,7 @@ exports.loadCatalog = async function(){
|
|||||||
sourceError,
|
sourceError,
|
||||||
profiles: rawProfiles
|
profiles: rawProfiles
|
||||||
.filter((profile) => profile != null && typeof profile.id === 'string' && typeof profile.name === 'string')
|
.filter((profile) => profile != null && typeof profile.id === 'string' && typeof profile.name === 'string')
|
||||||
.map((profile) => normalizeProfile(profile, 'catalog'))
|
.map((profile) => normalizeProfile(profile, 'catalog', source))
|
||||||
.sort((left, right) => left.name.localeCompare(right.name, 'ko'))
|
.sort((left, right) => left.name.localeCompare(right.name, 'ko'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,7 +476,11 @@ exports.getLibraryCatalogSource = function(){
|
|||||||
* @param {string|null} source A remote URL or local path.
|
* @param {string|null} source A remote URL or local path.
|
||||||
*/
|
*/
|
||||||
exports.setLibraryCatalogSource = function(source){
|
exports.setLibraryCatalogSource = function(source){
|
||||||
config.library.catalogSource = source
|
if(typeof source !== 'string' || source.trim().length === 0){
|
||||||
|
config.library.catalogSource = null
|
||||||
|
} else {
|
||||||
|
config.library.catalogSource = source.trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -998,3 +1002,22 @@ exports.getAllowPrerelease = function(def = false){
|
|||||||
exports.setAllowPrerelease = function(allowPrerelease){
|
exports.setAllowPrerelease = function(allowPrerelease){
|
||||||
config.settings.launcher.allowPrerelease = allowPrerelease
|
config.settings.launcher.allowPrerelease = allowPrerelease
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a library catalog source.
|
||||||
|
*
|
||||||
|
* @param {string} source A remote URL or local path.
|
||||||
|
* @returns {boolean} Whether the value is allowed.
|
||||||
|
*/
|
||||||
|
exports.validateLibraryCatalogSource = function(source){
|
||||||
|
const trimmedSource = typeof source === 'string' ? source.trim() : ''
|
||||||
|
if(trimmedSource.length === 0){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trimmedSource.includes('://')){
|
||||||
|
return /^https?:\/\/\S+$/i.test(trimmedSource) || trimmedSource.startsWith('file://')
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ logger.info('Loading..')
|
|||||||
|
|
||||||
// Load ConfigManager
|
// Load ConfigManager
|
||||||
ConfigManager.load()
|
ConfigManager.load()
|
||||||
CatalogManager.applyConfiguredProfile()
|
|
||||||
|
|
||||||
// Yuck!
|
// Yuck!
|
||||||
// TODO Fix this
|
// TODO Fix this
|
||||||
@@ -44,20 +43,32 @@ function onDistroLoad(data){
|
|||||||
ipcRenderer.send('distributionIndexDone', data != null)
|
ipcRenderer.send('distributionIndexDone', data != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure Distribution is downloaded and cached.
|
async function bootstrapDistribution(){
|
||||||
DistroAPI.getDistribution()
|
try {
|
||||||
.then(heliosDistro => {
|
await CatalogManager.getInstalledProfiles()
|
||||||
logger.info('Loaded distribution index.')
|
} catch (err) {
|
||||||
|
logger.warn('Unable to refresh installed profiles from catalog at startup.', err)
|
||||||
|
}
|
||||||
|
|
||||||
onDistroLoad(heliosDistro)
|
CatalogManager.applyConfiguredProfile()
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.info('Failed to load an older version of the distribution index.')
|
|
||||||
logger.info('Application cannot run.')
|
|
||||||
logger.error(err)
|
|
||||||
|
|
||||||
onDistroLoad(null)
|
// Ensure Distribution is downloaded and cached.
|
||||||
})
|
DistroAPI.getDistribution()
|
||||||
|
.then(heliosDistro => {
|
||||||
|
logger.info('Loaded distribution index.')
|
||||||
|
|
||||||
|
onDistroLoad(heliosDistro)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.info('Failed to load an older version of the distribution index.')
|
||||||
|
logger.info('Application cannot run.')
|
||||||
|
logger.error(err)
|
||||||
|
|
||||||
|
onDistroLoad(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapDistribution()
|
||||||
|
|
||||||
// Clean up temp dir incase previous launches ended unexpectedly.
|
// Clean up temp dir incase previous launches ended unexpectedly.
|
||||||
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
|
fs.remove(path.join(os.tmpdir(), ConfigManager.getTempNativeFolder()), (err) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const {
|
|||||||
ensureJavaDirIsRoot
|
ensureJavaDirIsRoot
|
||||||
} = require('helios-core/java')
|
} = require('helios-core/java')
|
||||||
|
|
||||||
|
const CatalogManager = require('./assets/js/catalogmanager')
|
||||||
const DropinModUtil = require('./assets/js/dropinmodutil')
|
const DropinModUtil = require('./assets/js/dropinmodutil')
|
||||||
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
|
const { MSFT_OPCODE, MSFT_REPLY_TYPE, MSFT_ERROR } = require('./assets/js/ipcconstants')
|
||||||
const settingsBackButton = document.getElementById('settingsBackButton')
|
const settingsBackButton = document.getElementById('settingsBackButton')
|
||||||
@@ -326,22 +327,45 @@ function settingsSaveDisabled(v){
|
|||||||
settingsNavDone.disabled = v
|
settingsNavDone.disabled = v
|
||||||
}
|
}
|
||||||
|
|
||||||
function fullSettingsSave() {
|
async function refreshCatalogSourceViews(){
|
||||||
|
await CatalogManager.getInstalledProfiles()
|
||||||
|
CatalogManager.applyConfiguredProfile()
|
||||||
|
|
||||||
|
if(typeof refreshInstallView === 'function'){
|
||||||
|
await refreshInstallView()
|
||||||
|
}
|
||||||
|
if(typeof refreshLibraryView === 'function'){
|
||||||
|
await refreshLibraryView()
|
||||||
|
}
|
||||||
|
if(typeof refreshSelectedProfileButton === 'function'){
|
||||||
|
refreshSelectedProfileButton()
|
||||||
|
}
|
||||||
|
if(typeof refreshServerStatus === 'function'){
|
||||||
|
await refreshServerStatus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fullSettingsSave() {
|
||||||
|
const previousCatalogSource = ConfigManager.getLibraryCatalogSource()
|
||||||
saveSettingsValues()
|
saveSettingsValues()
|
||||||
saveModConfiguration()
|
saveModConfiguration()
|
||||||
ConfigManager.save()
|
ConfigManager.save()
|
||||||
saveDropinModConfiguration()
|
saveDropinModConfiguration()
|
||||||
saveShaderpackSettings()
|
saveShaderpackSettings()
|
||||||
|
|
||||||
|
if(previousCatalogSource !== ConfigManager.getLibraryCatalogSource()){
|
||||||
|
await refreshCatalogSourceViews()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Closes the settings view and saves all data. */
|
/* Closes the settings view and saves all data. */
|
||||||
settingsNavDone.onclick = () => {
|
settingsNavDone.onclick = async () => {
|
||||||
fullSettingsSave()
|
await fullSettingsSave()
|
||||||
switchView(getCurrentView(), VIEWS.landing)
|
switchView(getCurrentView(), VIEWS.landing)
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsBackButton.onclick = () => {
|
settingsBackButton.onclick = async () => {
|
||||||
fullSettingsSave()
|
await fullSettingsSave()
|
||||||
switchView(getCurrentView(), VIEWS.landing)
|
switchView(getCurrentView(), VIEWS.landing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ launcherTabHeaderText = "Launcher Settings"
|
|||||||
launcherTabHeaderDesc = "Options related to the launcher itself."
|
launcherTabHeaderDesc = "Options related to the launcher itself."
|
||||||
allowPrereleaseTitle = "Allow Pre-Release Updates."
|
allowPrereleaseTitle = "Allow Pre-Release Updates."
|
||||||
allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.<br>This will always be true if you are using a pre-release version."
|
allowPrereleaseDesc = "Pre-Releases include new features which may have not been fully tested or integrated.<br>This will always be true if you are using a pre-release version."
|
||||||
|
catalogSourceTitle = "Catalog URL"
|
||||||
|
catalogSourcePlaceholder = "https://your-domain.example/catalog.json"
|
||||||
|
catalogSourceDesc = "Leave blank to use the automatic default catalog source.<br>For remote operation, enter the catalog.json URL served by the admin site."
|
||||||
dataDirectoryTitle = "Data Directory"
|
dataDirectoryTitle = "Data Directory"
|
||||||
selectDataDirectory = "Select Data Directory"
|
selectDataDirectory = "Select Data Directory"
|
||||||
chooseFolder = "Choose Folder"
|
chooseFolder = "Choose Folder"
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ launcherTabHeaderText = "런처 설정"
|
|||||||
launcherTabHeaderDesc = "런처와 관련된 설정입니다."
|
launcherTabHeaderDesc = "런처와 관련된 설정입니다."
|
||||||
allowPrereleaseTitle = "프리릴리즈 업데이트를 허용합니다."
|
allowPrereleaseTitle = "프리릴리즈 업데이트를 허용합니다."
|
||||||
allowPrereleaseDesc = "프리릴리즈는 안정성이 보장되지 않은 기능을 포함할 수 있습니다.<br>현재 실행 중인 런처가 프리릴리즈 버전이라면, 이 설정은 항상 활성화됩니다."
|
allowPrereleaseDesc = "프리릴리즈는 안정성이 보장되지 않은 기능을 포함할 수 있습니다.<br>현재 실행 중인 런처가 프리릴리즈 버전이라면, 이 설정은 항상 활성화됩니다."
|
||||||
|
catalogSourceTitle = "카탈로그 주소"
|
||||||
|
catalogSourcePlaceholder = "https://your-domain.example/catalog.json"
|
||||||
|
catalogSourceDesc = "비워두면 기본 카탈로그를 자동으로 찾습니다.<br>원격 운영 시에는 관리자 사이트가 제공하는 catalog.json URL을 입력하세요."
|
||||||
dataDirectoryTitle = "데이터 디렉토리"
|
dataDirectoryTitle = "데이터 디렉토리"
|
||||||
selectDataDirectory = "데이터 디렉토리 선택"
|
selectDataDirectory = "데이터 디렉토리 선택"
|
||||||
chooseFolder = "폴더 선택"
|
chooseFolder = "폴더 선택"
|
||||||
|
|||||||
@@ -289,6 +289,23 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settingsFileSelContainer">
|
||||||
|
<div class="settingsFileSelContent">
|
||||||
|
<div class="settingsFieldTitle" id="settingsCatalogSourceTitle"><%- lang('settings.catalogSourceTitle') %></div>
|
||||||
|
<div class="settingsFileSelActions">
|
||||||
|
<div class="settingsFileSelIcon">
|
||||||
|
<svg class="settingsFileSelSVG">
|
||||||
|
<g>
|
||||||
|
<path fill="gray" d="M3.5 5A2.5 2.5 0 0 1 6 2.5h8A2.5 2.5 0 0 1 16.5 5v10A2.5 2.5 0 0 1 14 17.5H6A2.5 2.5 0 0 1 3.5 15V5zm2.5-.5a.5.5 0 0 0-.5.5v10c0 .276.224.5.5.5h8a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5H6z"/>
|
||||||
|
<path fill="lightgray" d="M7 7.25h6v1.5H7zm0 3h6v1.5H7zm0 3h4v1.5H7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input class="settingsFileSelVal" id="settingsCatalogSourceVal" type="text" cValue="LibraryCatalogSource" placeholder="<%- lang('settings.catalogSourcePlaceholder') %>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settingsFileSelDesc"><%- lang('settings.catalogSourceDesc') %></div>
|
||||||
|
</div>
|
||||||
<div class="settingsFileSelContainer">
|
<div class="settingsFileSelContainer">
|
||||||
<div class="settingsFileSelContent">
|
<div class="settingsFileSelContent">
|
||||||
<div class="settingsFieldTitle" id="settingsDataDirTitle"><%- lang('settings.dataDirectoryTitle') %></div>
|
<div class="settingsFieldTitle" id="settingsDataDirTitle"><%- lang('settings.dataDirectoryTitle') %></div>
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ npm run admin
|
|||||||
|
|
||||||
- `http://127.0.0.1:8787`
|
- `http://127.0.0.1:8787`
|
||||||
|
|
||||||
|
공개 주소로 운영할 때:
|
||||||
|
|
||||||
|
- `LAUNCHER_PUBLIC_BASE_URL=https://your-domain.example npm run admin`
|
||||||
|
- 그러면 관리자 사이트 상단의 `앱 연결용 catalog URL`이 그 공개 주소 기준으로 표시됩니다.
|
||||||
|
|
||||||
## 현재 구현 범위
|
## 현재 구현 범위
|
||||||
|
|
||||||
- 프로필 추가 / 수정 / 삭제 / 복제
|
- 프로필 추가 / 수정 / 삭제 / 복제
|
||||||
@@ -54,5 +59,6 @@ npm run admin
|
|||||||
## 추천 운영 방식
|
## 추천 운영 방식
|
||||||
|
|
||||||
1. 관리자 사이트에서 프로필과 자료 파일을 입력
|
1. 관리자 사이트에서 프로필과 자료 파일을 입력
|
||||||
2. 로컬 런처에서 실제 표시와 실행 확인
|
2. 상단의 `앱 연결용 catalog URL`을 복사
|
||||||
3. 이후 필요하면 업로드 경로를 공개 URL 기반으로 확장
|
3. 런처 설정의 `카탈로그 주소`에 붙여넣기
|
||||||
|
4. 저장 후 실제 표시와 실행 확인
|
||||||
|
|||||||
Reference in New Issue
Block a user