diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css index 9bdc7dd..258e9a4 100644 --- a/app/assets/css/launcher.css +++ b/app/assets/css/launcher.css @@ -3293,6 +3293,7 @@ input:checked + .toggleSwitchSlider:before { font-weight: 900; text-shadow: 0px 0px 20px #949494; margin-left: 12px; + white-space: nowrap; } /* Wrapper container for the mojang status bar. */ @@ -3463,6 +3464,7 @@ input:checked + .toggleSwitchSlider:before { line-height: 36px; display: flex; transition: 0.25s ease; + white-space: nowrap; } /* * * @@ -3488,6 +3490,7 @@ input:checked + .toggleSwitchSlider:before { padding: 0px; transition: 0.25s ease; outline: none; + white-space: nowrap; } #launch_button:hover, #launch_button:focus { @@ -3543,6 +3546,7 @@ input:checked + .toggleSwitchSlider:before { line-height: 30px; padding: 0px; transition: 0.25s ease; + white-space: nowrap; } #server_selection_button:hover, #server_selection_button:focus { @@ -4150,8 +4154,8 @@ input:checked + .toggleSwitchSlider:before { } .launcherCardGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + display: flex; + flex-direction: column; gap: 16px; } @@ -4163,9 +4167,9 @@ input:checked + .toggleSwitchSlider:before { .launcherListItem { display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; + flex-direction: column; + align-items: stretch; + gap: 18px; padding: 22px 24px; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 20px; @@ -4188,6 +4192,13 @@ input:checked + .toggleSwitchSlider:before { gap: 8px; } +.launcherListItemTop { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; +} + .launcherListTitleRow { display: flex; align-items: flex-start; @@ -4206,6 +4217,9 @@ input:checked + .toggleSwitchSlider:before { margin: 0; font-size: 24px; color: #ffffff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .launcherListMeta { @@ -4228,6 +4242,22 @@ input:checked + .toggleSwitchSlider:before { 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 { @@ -4241,6 +4271,7 @@ input:checked + .toggleSwitchSlider:before { 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"] { @@ -4264,6 +4295,9 @@ input:checked + .toggleSwitchSlider:before { margin: 0; font-size: 26px; color: #ffffff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .launcherCardMeta { @@ -4283,6 +4317,7 @@ input:checked + .toggleSwitchSlider:before { text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.8); + white-space: nowrap; } .launcherCardDescription, @@ -4428,6 +4463,7 @@ input:checked + .toggleSwitchSlider:before { 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 { diff --git a/app/assets/js/scripts/install.js b/app/assets/js/scripts/install.js index b1117be..bfc6f30 100644 --- a/app/assets/js/scripts/install.js +++ b/app/assets/js/scripts/install.js @@ -4,15 +4,8 @@ const ConfigManager = require('./assets/js/configmanager') const ProfileAssetManager = require('./assets/js/profileassetmanager') const installCatalogList = document.getElementById('installCatalogList') -const installDetailTitle = document.getElementById('installDetailTitle') -const installDetailSummary = document.getElementById('installDetailSummary') -const installDetailMeta = document.getElementById('installDetailMeta') -const installDetailInfo = document.getElementById('installDetailInfo') -const installDetailBody = document.getElementById('installDetailBody') -const installDetailAddButton = document.getElementById('installDetailAddButton') -let selectedProfileId = null -let latestCatalog = null +let expandedProfileId = null function describeProfileKind(kind){ switch(kind){ @@ -74,98 +67,123 @@ function buildDetailText(profile){ } } -function renderDetailPanel(profile){ - const installedIds = new Set( - ConfigManager.getInstalledLibraryProfiles().map((installedProfile) => installedProfile.id) - ) - const installed = installedIds.has(profile.id) +function toggleExpandedProfile(profileId){ + expandedProfileId = expandedProfileId === profileId ? null : profileId +} - installDetailTitle.textContent = profile.name - installDetailSummary.textContent = profile.description || '설명이 없습니다.' - installDetailMeta.innerHTML = '' - installDetailMeta.appendChild(createInstallBadge(describeProfileKind(profile.kind))) +async function installProfile(profile){ + const installedProfile = await CatalogManager.installProfile(profile.id) + await ProfileAssetManager.prefetchProfileAssets(installedProfile) + if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){ + await ProfileAssetManager.ensureServerBundleInstalled(installedProfile) + } + + if(typeof refreshSelectedProfileButton === 'function'){ + refreshSelectedProfileButton() + } + if(typeof refreshServerStatus === 'function'){ + refreshServerStatus(true) + } + if(typeof refreshLibraryView === 'function'){ + await refreshLibraryView() + } +} + +function createExpandedDetail(profile, installed){ + const detailSection = document.createElement('div') + detailSection.className = 'launcherExpandableDetail' + detailSection.addEventListener('click', (event) => { + event.stopPropagation() + }) + + const badgeRow = document.createElement('div') + badgeRow.className = 'launcherExpandableMeta' + badgeRow.appendChild(createInstallBadge(describeProfileKind(profile.kind))) if(installed){ - installDetailMeta.appendChild(createInstallBadge('라이브러리 보유')) + badgeRow.appendChild(createInstallBadge('설치됨')) } if(!profile.launchReady){ - installDetailMeta.appendChild(createInstallBadge('실행 준비 필요')) + badgeRow.appendChild(createInstallBadge('실행 준비 필요')) } if(profile.kind === 'server-pack' && profile.hostReady){ - installDetailMeta.appendChild(createInstallBadge('로컬 호스팅 가능')) + badgeRow.appendChild(createInstallBadge('호스팅 가능')) } - installDetailInfo.innerHTML = '' - installDetailInfo.appendChild(createInfoLine('프로필 ID', profile.id)) - installDetailInfo.appendChild(createInfoLine('종류', describeProfileKind(profile.kind))) - installDetailInfo.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요')) + const infoBlock = document.createElement('div') + infoBlock.className = 'launcherInfoBlock' + infoBlock.appendChild(createInfoLine('프로필 ID', profile.id)) + infoBlock.appendChild(createInfoLine('종류', describeProfileKind(profile.kind))) + infoBlock.appendChild(createInfoLine('실행 준비', profile.launchReady ? '완료' : '추가 설정 필요')) if(profile.defaultServerAddress){ - installDetailInfo.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress)) + infoBlock.appendChild(createInfoLine('기본 주소', profile.defaultServerAddress)) } if(profile.kind === 'map' && profile.worldDirectoryName){ - installDetailInfo.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName)) + infoBlock.appendChild(createInfoLine('월드 폴더', profile.worldDirectoryName)) } if(profile.kind === 'server-pack'){ - installDetailInfo.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '관리자 설정 필요')) + infoBlock.appendChild(createInfoLine('로컬 호스팅', profile.hostReady ? '가능' : '관리자 설정 필요')) } if(profile.launchIssues.length > 0){ - installDetailInfo.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / '))) + infoBlock.appendChild(createInfoLine('확인 필요', profile.launchIssues.join(' / '))) } else if(profile.hostIssues.length > 0){ - installDetailInfo.appendChild(createInfoLine('호스팅 참고', profile.hostIssues.join(' / '))) + infoBlock.appendChild(createInfoLine('호스팅 참고', profile.hostIssues.join(' / '))) } - installDetailBody.textContent = buildDetailText(profile) - installDetailAddButton.disabled = installed || !profile.launchReady - installDetailAddButton.textContent = installed ? '이미 라이브러리에 있음' : '라이브러리에 추가' - installDetailAddButton.onclick = async () => { - try { - const installedProfile = await CatalogManager.installProfile(profile.id) - await ProfileAssetManager.prefetchProfileAssets(installedProfile) - if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){ - await ProfileAssetManager.ensureServerBundleInstalled(installedProfile) - } - if(typeof refreshSelectedProfileButton === 'function'){ - refreshSelectedProfileButton() - } - renderDetailPanel(profile) - await renderInstallView() - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } + const bodyGroup = document.createElement('div') + bodyGroup.className = 'launcherFieldGroup' + + const bodyLabel = document.createElement('label') + bodyLabel.className = 'launcherFieldLabel' + bodyLabel.textContent = '자세한 내용' + + const body = document.createElement('div') + body.className = 'launcherDetailBody' + body.textContent = buildDetailText(profile) + + bodyGroup.appendChild(bodyLabel) + bodyGroup.appendChild(body) + + const actions = document.createElement('div') + actions.className = 'launcherCardActions' + + const installButton = document.createElement('button') + installButton.className = 'launcherPrimaryButton' + installButton.textContent = installed ? '설치됨' : '라이브러리에 추가' + installButton.disabled = installed || !profile.launchReady + installButton.addEventListener('click', async (event) => { + event.stopPropagation() + try { + await installProfile(profile) + await renderInstallView() showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`) } catch (error) { console.error(error) const message = error instanceof Error ? error.message : '프로필 설치 중 오류가 발생했습니다.' showInstallMessage('설치 실패', message) } - } -} + }) -function renderEmptyDetailPanel(){ - installDetailTitle.textContent = '프로필을 선택하세요' - installDetailSummary.textContent = '아래 목록에서 모드팩, 맵, 서버팩을 고르면 자세한 설명과 설치 조건을 볼 수 있습니다.' - installDetailMeta.innerHTML = '' - installDetailInfo.innerHTML = '' - installDetailBody.textContent = '관리자가 등록한 프로필 상세 설명이 여기에 표시됩니다.' - installDetailAddButton.disabled = true - installDetailAddButton.textContent = '라이브러리에 추가' - installDetailAddButton.onclick = null -} + const openLibraryButton = document.createElement('button') + openLibraryButton.className = 'launcherSecondaryButton' + openLibraryButton.textContent = '라이브러리 열기' + openLibraryButton.addEventListener('click', async (event) => { + event.stopPropagation() + if(typeof refreshLibraryView === 'function'){ + await refreshLibraryView() + } + switchView(getCurrentView(), VIEWS.library) + }) -function selectProfile(profileId){ - selectedProfileId = profileId - if(latestCatalog == null){ - renderEmptyDetailPanel() - return - } + actions.appendChild(installButton) + actions.appendChild(openLibraryButton) - const profile = latestCatalog.profiles.find((entry) => entry.id === profileId) - if(profile == null){ - renderEmptyDetailPanel() - return - } + detailSection.appendChild(badgeRow) + detailSection.appendChild(infoBlock) + detailSection.appendChild(bodyGroup) + detailSection.appendChild(actions) - renderDetailPanel(profile) + return detailSection } async function renderInstallView(){ @@ -173,11 +191,14 @@ async function renderInstallView(){ try { const catalog = await CatalogManager.loadCatalog() - latestCatalog = catalog const installedIds = new Set( ConfigManager.getInstalledLibraryProfiles().map((profile) => profile.id) ) + if(expandedProfileId != null && !catalog.profiles.some((profile) => profile.id === expandedProfileId)){ + expandedProfileId = null + } + if(catalog.sourceError != null){ const warningCard = document.createElement('article') warningCard.className = 'launcherCard' @@ -186,12 +207,18 @@ async function renderInstallView(){ } for(const profile of catalog.profiles){ + const installed = installedIds.has(profile.id) + const expanded = expandedProfileId === profile.id + const row = document.createElement('article') row.className = 'launcherListItem' - if(profile.id === selectedProfileId){ + if(expanded){ row.setAttribute('selected', 'true') } + const top = document.createElement('div') + top.className = 'launcherListItemTop' + const main = document.createElement('div') main.className = 'launcherListItemMain' @@ -208,15 +235,13 @@ async function renderInstallView(){ const meta = document.createElement('div') meta.className = 'launcherListMeta' meta.appendChild(createInstallBadge(describeProfileKind(profile.kind))) - if(installedIds.has(profile.id)){ + if(installed){ meta.appendChild(createInstallBadge('설치됨')) } if(profile.kind === 'server-pack' && profile.hostReady){ meta.appendChild(createInstallBadge('호스팅 가능')) } - textGroup.appendChild(title) - const description = document.createElement('p') description.className = 'launcherListDescription' description.textContent = profile.description || '설명이 없습니다.' @@ -226,36 +251,23 @@ async function renderInstallView(){ const detailButton = document.createElement('button') detailButton.className = 'launcherSecondaryButton' - detailButton.textContent = '자세히 보기' + detailButton.textContent = expanded ? '간단히 보기' : '자세히 보기' detailButton.addEventListener('click', async (event) => { event.stopPropagation() - selectProfile(profile.id) + toggleExpandedProfile(profile.id) await renderInstallView() }) const installButton = document.createElement('button') installButton.className = 'launcherPrimaryButton' - installButton.textContent = installedIds.has(profile.id) ? '설치됨' : '라이브러리에 추가' - installButton.disabled = installedIds.has(profile.id) || !profile.launchReady + installButton.textContent = installed ? '설치됨' : '라이브러리에 추가' + installButton.disabled = installed || !profile.launchReady installButton.addEventListener('click', async (event) => { event.stopPropagation() try { - const installedProfile = await CatalogManager.installProfile(profile.id) - await ProfileAssetManager.prefetchProfileAssets(installedProfile) - if(installedProfile.kind === 'server-pack' && installedProfile.hostReady){ - await ProfileAssetManager.ensureServerBundleInstalled(installedProfile) - } - if(typeof refreshSelectedProfileButton === 'function'){ - refreshSelectedProfileButton() - } - if(typeof refreshServerStatus === 'function'){ - refreshServerStatus(true) - } - selectProfile(profile.id) + await installProfile(profile) + expandedProfileId = profile.id await renderInstallView() - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } showInstallMessage('추가 완료', `${profile.name} 프로필을 라이브러리에 추가했습니다.`) } catch (error) { console.error(error) @@ -264,37 +276,39 @@ async function renderInstallView(){ } }) - actions.appendChild(detailButton) - actions.appendChild(installButton) - + textGroup.appendChild(title) titleRow.appendChild(textGroup) titleRow.appendChild(meta) main.appendChild(titleRow) main.appendChild(description) - row.appendChild(main) - row.appendChild(actions) + + actions.appendChild(detailButton) + actions.appendChild(installButton) + + top.appendChild(main) + top.appendChild(actions) + row.appendChild(top) + + if(expanded){ + row.appendChild(createExpandedDetail(profile, installed)) + } + row.addEventListener('click', async () => { - selectProfile(profile.id) + toggleExpandedProfile(profile.id) await renderInstallView() }) + installCatalogList.appendChild(row) } if(catalog.profiles.length === 0){ - renderEmptyDetailPanel() - return + const emptyCard = document.createElement('article') + emptyCard.className = 'launcherCard' + emptyCard.innerHTML = '

등록된 프로필이 없습니다

관리자가 카탈로그에 프로필을 추가하면 여기에 표시됩니다.

' + installCatalogList.appendChild(emptyCard) } - - const selectedProfileStillExists = catalog.profiles.some((profile) => profile.id === selectedProfileId) - if(!selectedProfileStillExists){ - selectedProfileId = catalog.profiles[0].id - } - - selectProfile(selectedProfileId) } catch (error) { console.error(error) - latestCatalog = null - renderEmptyDetailPanel() const errorCard = document.createElement('article') errorCard.className = 'launcherCard' @@ -307,14 +321,6 @@ document.getElementById('installBackButton').addEventListener('click', () => { switchView(getCurrentView(), VIEWS.landing) }) -document.getElementById('installDetailOpenLibraryButton').addEventListener('click', async () => { - if(typeof refreshLibraryView === 'function'){ - await refreshLibraryView() - } - switchView(getCurrentView(), VIEWS.library) -}) - window.refreshInstallView = renderInstallView -renderEmptyDetailPanel() renderInstallView() })() diff --git a/app/assets/js/scripts/uicore.js b/app/assets/js/scripts/uicore.js index c11e396..144dc56 100644 --- a/app/assets/js/scripts/uicore.js +++ b/app/assets/js/scripts/uicore.js @@ -35,6 +35,69 @@ remote.getCurrentWebContents().on('devtools-opened', () => { webFrame.setZoomLevel(0) webFrame.setVisualZoomLevelLimits(1, 1) +const BASE_WINDOW_WIDTH = 1400 +const BASE_WINDOW_HEIGHT = 860 + +let responsiveLayoutFrame = null + +function clamp(value, min, max){ + return Math.min(Math.max(value, min), max) +} + +function syncLaunchDetailWidths(){ + const launchContent = document.getElementById('launch_content') + const launchDetails = document.getElementById('launch_details') + const launchButton = document.getElementById('launch_button') + const launchProgress = document.getElementById('launch_progress') + const launchDetailsRight = document.getElementById('launch_details_right') + const launchProgressLabel = document.getElementById('launch_progress_label') + + if(!launchContent || !launchDetails || !launchButton || !launchProgress || !launchDetailsRight || !launchProgressLabel){ + return + } + + const launchContentWidth = launchContent.getBoundingClientRect().width + const launchButtonWidth = launchButton.getBoundingClientRect().width + const labelWidth = Math.max(64, Math.ceil(launchButtonWidth * 0.48)) + const progressWidth = Math.max(220, Math.floor(launchContentWidth - labelWidth - 48)) + + launchDetails.style.maxWidth = `${Math.ceil(launchContentWidth)}px` + launchProgress.style.width = `${progressWidth}px` + launchDetailsRight.style.maxWidth = `${progressWidth}px` + launchProgressLabel.style.width = `${labelWidth}px` + launchProgressLabel.style.minWidth = `${labelWidth}px` + launchProgressLabel.style.maxWidth = `${labelWidth}px` +} + +function applyResponsiveLayout(){ + const scale = clamp( + Math.min(window.innerWidth / BASE_WINDOW_WIDTH, window.innerHeight / BASE_WINDOW_HEIGHT), + 0.72, + 1.45 + ) + + webFrame.setZoomFactor(scale) + document.documentElement.style.setProperty('--launcher-scale', scale.toFixed(3)) + + window.requestAnimationFrame(() => { + syncLaunchDetailWidths() + }) +} + +function queueResponsiveLayout(){ + if(responsiveLayoutFrame != null){ + cancelAnimationFrame(responsiveLayoutFrame) + } + responsiveLayoutFrame = requestAnimationFrame(() => { + responsiveLayoutFrame = null + applyResponsiveLayout() + }) +} + +window.addEventListener('resize', () => { + queueResponsiveLayout() +}) + // Initialize auto updates in production environments. let updateCheckListener if(!isDev){ @@ -174,20 +237,10 @@ document.addEventListener('readystatechange', function () { }) } else if(document.readyState === 'complete'){ - - //266.01 - //170.8 - //53.21 - // Bind progress bar length to length of bot wrapper - //const targetWidth = document.getElementById("launch_content").getBoundingClientRect().width - //const targetWidth2 = document.getElementById("server_selection").getBoundingClientRect().width - //const targetWidth3 = document.getElementById("launch_button").getBoundingClientRect().width - - document.getElementById('launch_details').style.maxWidth = 266.01 - document.getElementById('launch_progress').style.width = 170.8 - document.getElementById('launch_details_right').style.maxWidth = 170.8 - document.getElementById('launch_progress_label').style.width = 53.21 - + queueResponsiveLayout() + setTimeout(() => { + queueResponsiveLayout() + }, 150) } }, false) @@ -210,4 +263,4 @@ document.addEventListener('keydown', function (e) { let window = remote.getCurrentWindow() window.toggleDevTools() } -}) \ No newline at end of file +}) diff --git a/app/install.ejs b/app/install.ejs index 16b04e0..ba88d9e 100644 --- a/app/install.ejs +++ b/app/install.ejs @@ -13,24 +13,6 @@
<%- lang('install.notice') %>
-
-
-
- 프로필을 선택하세요 - 아래 목록에서 모드팩, 맵, 서버팩을 고르면 자세한 설명과 설치 조건을 볼 수 있습니다. -
-
-
-
-
- -
관리자가 등록한 프로필 상세 설명이 여기에 표시됩니다.
-
-
- - -
-
diff --git a/index.legacy.js b/index.legacy.js index 5f456f7..bf258fd 100644 --- a/index.legacy.js +++ b/index.legacy.js @@ -227,8 +227,10 @@ let win function createWindow() { win = new BrowserWindow({ - width: 980, - height: 552, + width: 1400, + height: 860, + minWidth: 1120, + minHeight: 700, icon: getPlatformIcon('Icon'), frame: false, webPreferences: { diff --git a/src/main/index.ts b/src/main/index.ts index d4565d8..92cc8ca 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -228,8 +228,10 @@ let win function createWindow() { win = new BrowserWindow({ - width: 980, - height: 552, + width: 1400, + height: 860, + minWidth: 1120, + minHeight: 700, icon: getPlatformIcon('Icon'), frame: false, webPreferences: {