Enhance mod synchronization UI and functionality. Added a dropdown for selecting mod tags, improved state management for mod version checks, and updated the performModSync function to accept an optional tag parameter. Refined styles for better layout and user experience, including adjustments to padding and grid gaps.
This commit is contained in:
parent
09d800eee9
commit
9486a624cb
|
|
@ -47,7 +47,6 @@ const HEALTHCHECK_ENDPOINT_FALLBACK = SERVER_HEALTHCHECK_URL;
|
|||
const MIN_SERVER_CHECK_SCREEN_MS = 1500;
|
||||
const IS_DEV = import.meta.env.DEV;
|
||||
const DEV_UI_ENABLED = IS_DEV && import.meta.env.VITE_DEV_UI === "1";
|
||||
const INSTALL_GUIDE_URL = APP_CONFIG.sptInstallGuideUrl;
|
||||
|
||||
const App = () => {
|
||||
const [screen, setScreen] = useState<Screen>("serverCheck");
|
||||
|
|
@ -88,6 +87,7 @@ const App = () => {
|
|||
const [modCheckInProgress, setModCheckInProgress] = useState(false);
|
||||
const [modVersionStatus, setModVersionStatus] = useState<ModVersionStatus | undefined>();
|
||||
const [modCheckMessage, setModCheckMessage] = useState<string | undefined>();
|
||||
const [selectedModTag, setSelectedModTag] = useState("");
|
||||
const [sptInstallInfo, setSptInstallInfo] = useState<SptInstallInfo | undefined>();
|
||||
const [sptInstallMessage, setSptInstallMessage] = useState<string | undefined>();
|
||||
const [sptPathInput, setSptPathInput] = useState("");
|
||||
|
|
@ -533,6 +533,43 @@ const App = () => {
|
|||
return "blocked";
|
||||
})();
|
||||
|
||||
const modTagOptions = useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
if (modVersionStatus?.currentTag) {
|
||||
tags.add(modVersionStatus.currentTag);
|
||||
}
|
||||
if (modVersionStatus?.latestTag) {
|
||||
tags.add(modVersionStatus.latestTag);
|
||||
}
|
||||
modVersionStatus?.latestMinorTags?.forEach((tag) => tags.add(tag));
|
||||
return Array.from(tags);
|
||||
}, [modVersionStatus]);
|
||||
|
||||
const startReady = Boolean(sptInstallInfo?.ok && modVersionStatus?.ok && !syncInProgress);
|
||||
const startStatusLabel = syncInProgress
|
||||
? "동기화 중"
|
||||
: startReady
|
||||
? "준비됨"
|
||||
: !sptInstallInfo?.ok
|
||||
? "경로 필요"
|
||||
: !modVersionStatus?.ok
|
||||
? "버전 확인 필요"
|
||||
: "준비 안됨";
|
||||
const startStatusClass = syncInProgress ? "idle" : startReady ? "ok" : "blocked";
|
||||
|
||||
useEffect(() => {
|
||||
if (modTagOptions.length === 0) {
|
||||
if (selectedModTag) {
|
||||
setSelectedModTag("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const preferred = modVersionStatus?.latestTag ?? modTagOptions[0];
|
||||
if (!modTagOptions.includes(selectedModTag)) {
|
||||
setSelectedModTag(preferred ?? "");
|
||||
}
|
||||
}, [modTagOptions, modVersionStatus?.latestTag, selectedModTag]);
|
||||
|
||||
const serverStatusLedClass = serverCheckInProgress ? "idle" : serverHealthy ? "ok" : "blocked";
|
||||
const gitStatusLedClass = (() => {
|
||||
if (gitCheckInProgress) {
|
||||
|
|
@ -799,16 +836,21 @@ const App = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const performModSync = async (mode: "required" | "optional" | "force") => {
|
||||
const performModSync = async (
|
||||
mode: "required" | "optional" | "force",
|
||||
overrideTag?: string
|
||||
) => {
|
||||
if (syncInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setSyncInProgress(true);
|
||||
const trimmedOverride = overrideTag?.trim();
|
||||
const targetTag =
|
||||
mode === "force"
|
||||
trimmedOverride ||
|
||||
(mode === "force"
|
||||
? modVersionStatus?.currentTag ?? modVersionStatus?.latestTag
|
||||
: modVersionStatus?.latestTag;
|
||||
: modVersionStatus?.latestTag);
|
||||
|
||||
try {
|
||||
const targetDir = sptInstallInfo?.path?.trim();
|
||||
|
|
@ -1128,6 +1170,71 @@ const App = () => {
|
|||
{modVersionStatus?.currentTag ?? "-"} → {modVersionStatus?.latestTag ?? "-"}
|
||||
</span>
|
||||
<span className={`status ${modStatusClass}`}>{modStatusLabel}</span>
|
||||
<div className="row-actions">
|
||||
<select
|
||||
value={selectedModTag}
|
||||
onChange={(event) => setSelectedModTag(event.target.value)}
|
||||
disabled={modTagOptions.length === 0 || syncInProgress}
|
||||
aria-label="모드 버전 선택"
|
||||
>
|
||||
{modTagOptions.length === 0 && <option value="">버전 없음</option>}
|
||||
{modTagOptions.map((tag) => (
|
||||
<option key={tag} value={tag}>
|
||||
{tag}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-action"
|
||||
onClick={() => performModSync("optional", selectedModTag)}
|
||||
disabled={syncInProgress || !selectedModTag}
|
||||
aria-label="적용"
|
||||
title="적용(패치)"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<div className="sync-menu">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-action"
|
||||
onClick={() => performModSync("optional")}
|
||||
disabled={syncInProgress || !modVersionStatus?.latestTag}
|
||||
aria-label="동기화"
|
||||
title="동기화"
|
||||
>
|
||||
⟳
|
||||
</button>
|
||||
<div className="sync-menu-panel" role="menu" aria-label="동기화 메뉴">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => performModSync("optional")}
|
||||
disabled={syncInProgress || !modVersionStatus?.latestTag}
|
||||
>
|
||||
동기화
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => performModSync("force")}
|
||||
disabled={syncInProgress}
|
||||
>
|
||||
덮어쓰기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-action ghost"
|
||||
onClick={() => runModVersionCheck(true)}
|
||||
disabled={modCheckInProgress}
|
||||
aria-label="새로고침"
|
||||
title="새로고침"
|
||||
>
|
||||
⟲
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{modVersionStatus?.latestMinorTags && modVersionStatus.latestMinorTags.length > 0 && (
|
||||
<div className="helper">
|
||||
|
|
@ -1152,7 +1259,17 @@ const App = () => {
|
|||
)}
|
||||
{sptInstallMessage && <div className="notice">{sptInstallMessage}</div>}
|
||||
{modCheckMessage && <div className="notice">{modCheckMessage}</div>}
|
||||
{DEV_UI_ENABLED && (
|
||||
{DEV_UI_ENABLED && (
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={simulateSyncFail}
|
||||
onChange={(event) => setSimulateSyncFail(event.target.checked)}
|
||||
/>
|
||||
동기화 실패 시뮬레이션
|
||||
</label>
|
||||
)}
|
||||
{DEV_UI_ENABLED && (
|
||||
<div className="notice">
|
||||
<strong>개발 모드 경로</strong>
|
||||
<div className="modal-help">
|
||||
|
|
@ -1200,57 +1317,12 @@ const App = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="spt-actions compact-actions">
|
||||
<div className="section-head">
|
||||
<h3>실행</h3>
|
||||
<span className={`status ${syncInProgress ? "idle" : "ok"}`}>
|
||||
{syncInProgress ? "동기화 중" : "준비됨"}
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" onClick={handleLaunch} disabled={syncInProgress}>
|
||||
{syncInProgress ? "동기화 중..." : "게임 시작"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => performModSync("optional")}
|
||||
disabled={syncInProgress || !modVersionStatus?.latestTag}
|
||||
>
|
||||
{syncInProgress ? "동기화 중..." : "동기화"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => performModSync("force")}
|
||||
disabled={syncInProgress}
|
||||
>
|
||||
강제 재동기화
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => runModVersionCheck(true)}
|
||||
disabled={modCheckInProgress}
|
||||
>
|
||||
{modCheckInProgress ? "확인 중..." : "버전 재확인"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => window.open(INSTALL_GUIDE_URL, "_blank")}
|
||||
>
|
||||
설치 가이드 열기
|
||||
</button>
|
||||
{DEV_UI_ENABLED && (
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={simulateSyncFail}
|
||||
onChange={(event) => setSimulateSyncFail(event.target.checked)}
|
||||
/>
|
||||
동기화 실패 시뮬레이션
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card spt-launch">
|
||||
<button type="button" onClick={handleLaunch} disabled={!startReady}>
|
||||
{syncInProgress ? "동기화 중..." : "게임 시작"}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<footer className="footer compact-footer footer-right-only">
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ body {
|
|||
font-family: "Segoe UI", sans-serif;
|
||||
background: #0f1218;
|
||||
color: #f5f7fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -28,7 +29,7 @@ body {
|
|||
.card {
|
||||
background: #171b22;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid #242a35;
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +158,7 @@ button:disabled {
|
|||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +170,7 @@ button:disabled {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.profile-summary {
|
||||
|
|
@ -203,12 +204,12 @@ button:disabled {
|
|||
|
||||
.spt-info {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spt-actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +244,53 @@ button:disabled {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.row-actions select {
|
||||
height: 34px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
padding: 0;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
font-size: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sync-menu {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sync-menu-panel {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
left: 0;
|
||||
display: none;
|
||||
grid-auto-flow: row;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
min-width: 120px;
|
||||
background: #141923;
|
||||
border: 1px solid #242a35;
|
||||
border-radius: 10px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.sync-menu:hover .sync-menu-panel,
|
||||
.sync-menu:focus-within .sync-menu-panel {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.sync-menu-panel button {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
padding: 6px 8px;
|
||||
font-size: 14px;
|
||||
|
|
@ -278,10 +326,20 @@ button:disabled {
|
|||
align-items: center;
|
||||
background: #141923;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #242a35;
|
||||
}
|
||||
|
||||
.spt-launch {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.spt-launch button {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.compact-footer {
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
Loading…
Reference in New Issue