업데이트 진행 창 추가 + Shell.exe 직접 실행 시 런처로 넘기기
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
649599de48
commit
a5411d13b0
2
dist/README.txt
vendored
2
dist/README.txt
vendored
@ -1,5 +1,5 @@
|
||||
UbisamBase 플랫폼 배포 폴더 (빌드할 때 자동 생성 - 직접 고치지 마세요)
|
||||
갱신 시각 : 2026-09-21 22:26:56
|
||||
갱신 시각 : 2026-09-21 22:36:26
|
||||
빌드 구성 : Release
|
||||
원본 소스 : D:\업무\03_개발\Source\용접기\08_UbisamPlatform\
|
||||
----
|
||||
|
||||
BIN
dist/UbisamBase.Core.dll
vendored
BIN
dist/UbisamBase.Core.dll
vendored
Binary file not shown.
BIN
dist/UbisamBase.Launcher.exe
vendored
BIN
dist/UbisamBase.Launcher.exe
vendored
Binary file not shown.
BIN
dist/UbisamBase.Shell.exe
vendored
BIN
dist/UbisamBase.Shell.exe
vendored
Binary file not shown.
2
dist/platform-version.json
vendored
2
dist/platform-version.json
vendored
@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026-09-21 22:26:56",
|
||||
"version": "2026-09-21 22:36:26",
|
||||
"configuration": "Release"
|
||||
}
|
||||
|
||||
@ -16,3 +16,5 @@
|
||||
| 2026-09-21 | 업데이트 안내창을 플랫폼 디자인으로 교체(Launcher 자체 XAML, 시스템 테마 따름) |
|
||||
| 2026-09-21 | `ApiService` 추가 — 사내 API를 앱이 대신 호출(토큰은 설정 파일에만, 호출 간격 제한·타임아웃 포함) |
|
||||
| 2026-09-21 | 차트 종류별 디자인 정리 — 면적·계단선·누적/가로 막대·원형·도넛·산점 추가, 계열 팔레트(Chart.Series1~6)와 툴팁·범례 테마 적용 |
|
||||
| 2026-09-21 | 업데이트 진행 창 추가 — 런처를 누르자마자 확인/받기 상태와 진행률을 보여주고, 다 받은 뒤 설치 여부를 묻는다 |
|
||||
| 2026-09-21 | Shell.exe를 직접 실행하면 런처로 넘기도록 — 진입점을 런처 하나로 통일(업데이트 확인 누락 방지) |
|
||||
|
||||
@ -665,9 +665,17 @@ dist의 버전이 지금 것보다 새것인가? → 아니면 커밋만
|
||||
자동 업데이트인가? → 예: 바로 받기 / 아니오: "업데이트할까요?" 물어보고 예일 때만 받기
|
||||
```
|
||||
|
||||
- 확인·받기는 몇 초가 걸리므로 그동안 **진행 창**이 뜬다(확인 중 → 새 버전을 찾았습니다 → 받기 완료,
|
||||
진행률은 git이 흘리는 값을 그대로 읽어 표시한다). 받기가 **끝난 뒤에** 설치 여부를 묻는다 —
|
||||
"예"를 누르고 또 기다리지 않게 하기 위함이다.
|
||||
- 어느 단계에서 막히든(설정 꺼짐 · 네트워크 없음 · git 미설치 · 저장소 접근 실패) **조용히 넘어가고
|
||||
기존 버전으로 실행한다.** 업데이트 때문에 프로그램이 안 켜지는 일은 없어야 하기 때문이다.
|
||||
- 물어봤을 때 "아니오"를 고르면 아무것도 기록하지 않는다 — 다음에 켤 때 다시 물어본다.
|
||||
- 물어봤을 때 "나중에"를 고르면 아무것도 기록하지 않는다 — 다음에 켤 때 다시 물어본다.
|
||||
- 확인 과정은 `D:\UbisamPlatform\update-log.txt`에 한 줄씩 남는다(실행할 때마다 덮어씀).
|
||||
"왜 업데이트 창이 안 뜨지?" 싶을 때 이 파일을 먼저 본다.
|
||||
- **프로그램의 진입점은 항상 런처다.** 앱 폴더에는 `UbisamBase.Shell.exe`도 같이 있지만 그것을 직접
|
||||
실행하면 갱신 없이 옛 파일로 뜨게 되므로, Shell은 자기가 직접 실행된 것을 알아채면 조용히 런처를
|
||||
띄우고 종료한다(`App.RedirectToLauncher`). 바로가기를 만들 때도 런처를 가리켜야 한다.
|
||||
- 받는 것은 `dist` 폴더뿐이다(`--depth 1 --filter=blob:none --sparse`). 소스 이력은 받지 않는다.
|
||||
- git 자격 증명 창이 뜨지 않게 막아 두었다(`GIT_TERMINAL_PROMPT=0`). 사내 비공개 저장소라면 그 PC에서
|
||||
한 번은 `git` 자격 증명이 저장돼 있어야 한다.
|
||||
|
||||
@ -4,7 +4,9 @@ using System.IO;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace UbisamBase.Launcher;
|
||||
|
||||
@ -13,10 +15,15 @@ namespace UbisamBase.Launcher;
|
||||
/// 새 버전이면 받아서 덮어쓴다. 순서는 이렇다.
|
||||
///
|
||||
/// <code>
|
||||
/// 설정에서 켜져 있나? → 인터넷 연결됐나? → 저장소에 새 커밋이 있나? → 그 안의 업데이트 파일이
|
||||
/// 지금 것보다 새 버전인가? → 자동 업데이트면 바로 받기 / 아니면 물어보고 "예"일 때만 받기
|
||||
/// 설정에서 켜져 있나? → 인터넷 연결됐나? → (진행 창 표시) 저장소에 새 커밋이 있나?
|
||||
/// → 업데이트 파일 받기(진행률 표시) → 지금 것보다 새 버전인가?
|
||||
/// → 자동 업데이트면 바로 설치 / 아니면 물어보고 "예"일 때만 설치
|
||||
/// </code>
|
||||
///
|
||||
/// 받는 동안 <see cref="ProgressWindow"/>를 띄운다 — 예전에는 아무것도 안 보여서 몇 초 동안
|
||||
/// 프로그램이 안 켜지는 것처럼 보였다. 설치 여부는 <b>다 받은 뒤에</b> 묻는다(그래야 "예"를 누른
|
||||
/// 다음에 또 기다리지 않는다).
|
||||
///
|
||||
/// 어느 단계에서 막히든(설정 꺼짐 · 네트워크 없음 · git 없음 · 저장소 접근 실패) 조용히 넘어가고
|
||||
/// 기존 파일로 그대로 실행한다 — 업데이트 때문에 프로그램이 안 켜지는 일은 없어야 한다.
|
||||
///
|
||||
@ -96,80 +103,57 @@ internal static class PlatformUpdater
|
||||
return; // git이 깔려 있지 않은 PC — 업데이트 기능만 조용히 쉰다.
|
||||
}
|
||||
|
||||
// 2) 업데이트 할 게 있는가 — 원격 브랜치의 최신 커밋이 마지막에 설치한 커밋과 같으면 끝.
|
||||
var branch = string.IsNullOrWhiteSpace(settings.Branch) ? "main" : settings.Branch.Trim();
|
||||
if (!TryRunGit($"ls-remote \"{settings.RepositoryUrl}\" \"refs/heads/{branch}\"", null, LsRemoteTimeoutMs, out var lsRemote))
|
||||
// 여기서부터 몇 초가 걸릴 수 있다 — 무엇을 하고 있는지 창으로 보여준다.
|
||||
var progress = new ProgressWindow();
|
||||
progress.Show();
|
||||
|
||||
Download? download = null;
|
||||
|
||||
// 받기는 배경 스레드에서 하고, 이 스레드는 DispatcherFrame으로 화면을 계속 그린다.
|
||||
// (Application.Run을 쓸 수 없다 — Shell이 나중에 자기 Application을 만든다. Dispatcher를
|
||||
// 종료시키는 방식도 안 된다 — 같은 스레드에서 Shell이 계속 돌아야 하기 때문이다.)
|
||||
var frame = new DispatcherFrame();
|
||||
var worker = new Thread(() =>
|
||||
{
|
||||
Note("원격 확인 실패(주소·권한·네트워크) — 건너뜀");
|
||||
return; // 주소가 틀렸거나 접근 권한이 없다 — 실행을 막지 않는다.
|
||||
}
|
||||
|
||||
var remoteCommit = FirstToken(lsRemote);
|
||||
Note($"원격 커밋:{Short(remoteCommit)} / 설치된 커밋:{Short(settings.InstalledCommit)}");
|
||||
|
||||
if (string.IsNullOrEmpty(remoteCommit) || remoteCommit == settings.InstalledCommit)
|
||||
{
|
||||
Note("새 커밋 없음 — 끝");
|
||||
return;
|
||||
}
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "UbisamPlatformUpdate_" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var distName = string.IsNullOrWhiteSpace(settings.DistPath) ? "dist" : settings.DistPath.Trim().Replace('\\', '/');
|
||||
|
||||
// 업데이트 파일만 얕게 받는다 — 소스 전체 이력을 받지 않는다.
|
||||
if (!TryRunGit(
|
||||
$"clone --depth 1 --branch \"{branch}\" --filter=blob:none --sparse \"{settings.RepositoryUrl}\" \"{tempDir}\"",
|
||||
null, CloneTimeoutMs, out _))
|
||||
download = Prepare(platformDir, settings, progress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Note("업데이트 파일을 내려받지 못함(clone 실패) — 건너뜀");
|
||||
return;
|
||||
Note("받는 중 예외 : " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
progress.Dispatcher.BeginInvoke(new Action(() => frame.Continue = false));
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
|
||||
worker.Start();
|
||||
Dispatcher.PushFrame(frame);
|
||||
progress.Close();
|
||||
|
||||
if (download == null)
|
||||
{
|
||||
return; // 할 일이 없거나 중간에 막혔다 — 이유는 로그에 남아 있다.
|
||||
}
|
||||
|
||||
if (!TryRunGit($"sparse-checkout set \"{distName}\"", tempDir, CloneTimeoutMs, out _))
|
||||
try
|
||||
{
|
||||
Note("업데이트 폴더를 꺼내지 못함(sparse-checkout 실패) — 건너뜀");
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteDist = Path.Combine(tempDir, distName.Replace('/', '\\'));
|
||||
if (!Directory.Exists(remoteDist))
|
||||
{
|
||||
Note($"저장소에 {distName} 폴더가 없음 — 커밋만 기록");
|
||||
// 저장소에 아직 업데이트 파일이 없다 — 이 커밋은 확인했다고 기록만 하고 넘어간다.
|
||||
settings.InstalledCommit = remoteCommit;
|
||||
settings.Save(SettingsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteVersion = ReadVersion(Path.Combine(remoteDist, VersionFileName));
|
||||
var localVersion = ReadVersion(Path.Combine(platformDir, VersionFileName));
|
||||
|
||||
// 버전은 배포 시각("yyyy-MM-dd HH:mm:ss")이라 문자열 비교로 앞뒤가 가려진다.
|
||||
// 원격이 더 새것일 때만 업데이트한다 — 소스만 바뀐 커밋이면 확인 기록만 남긴다.
|
||||
Note($"원격 버전:{remoteVersion} / 지금 버전:{localVersion}");
|
||||
|
||||
if (string.IsNullOrEmpty(remoteVersion) || string.CompareOrdinal(remoteVersion, localVersion) <= 0)
|
||||
{
|
||||
Note("더 새 버전이 아님 — 커밋만 기록");
|
||||
settings.InstalledCommit = remoteCommit;
|
||||
settings.Save(SettingsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 자동 업데이트가 아니면 물어본다.
|
||||
if (!settings.AutoUpdate && !UpdateDialog.Ask(localVersion, remoteVersion))
|
||||
if (!settings.AutoUpdate && !UpdateDialog.Ask(download.LocalVersion, download.RemoteVersion))
|
||||
{
|
||||
Note("사용자가 \"나중에\"를 선택 — 다음 실행 때 다시 물어본다");
|
||||
// 기록을 남기지 않는다 — 다음에 켤 때 다시 물어본다.
|
||||
return;
|
||||
return; // 기록을 남기지 않는다 — 다음에 켤 때 다시 물어본다.
|
||||
}
|
||||
|
||||
var failed = CopyAll(remoteDist, platformDir);
|
||||
var failed = CopyAll(download.DistDir, platformDir);
|
||||
Note($"업데이트 완료 — 바꾸지 못한 파일:{failed}개");
|
||||
settings.InstalledCommit = remoteCommit;
|
||||
settings.InstalledVersion = remoteVersion;
|
||||
|
||||
settings.InstalledCommit = download.Commit;
|
||||
settings.InstalledVersion = download.RemoteVersion;
|
||||
settings.Save(SettingsPath);
|
||||
|
||||
if (failed > 0)
|
||||
@ -181,10 +165,130 @@ internal static class PlatformUpdater
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDeleteDirectory(download.TempDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>원격을 확인하고, 새 버전이면 업데이트 파일을 임시 폴더에 받아 온다.
|
||||
/// 받을 것이 없으면 null. 화면 표시는 <paramref name="progress"/>로 알린다.</summary>
|
||||
private static Download? Prepare(string platformDir, Settings settings, ProgressWindow progress)
|
||||
{
|
||||
progress.SetStage("업데이트 확인 중", "저장소에서 최신 버전을 확인하고 있습니다.");
|
||||
progress.SetPercent(6);
|
||||
|
||||
var branch = string.IsNullOrWhiteSpace(settings.Branch) ? "main" : settings.Branch.Trim();
|
||||
if (!TryRunGit($"ls-remote \"{settings.RepositoryUrl}\" \"refs/heads/{branch}\"", null, LsRemoteTimeoutMs, out var lsRemote))
|
||||
{
|
||||
Note("원격 확인 실패(주소·권한·네트워크) — 건너뜀");
|
||||
return null;
|
||||
}
|
||||
|
||||
var remoteCommit = FirstToken(lsRemote);
|
||||
Note($"원격 커밋:{Short(remoteCommit)} / 설치된 커밋:{Short(settings.InstalledCommit)}");
|
||||
|
||||
if (string.IsNullOrEmpty(remoteCommit) || remoteCommit == settings.InstalledCommit)
|
||||
{
|
||||
Note("새 커밋 없음 — 끝");
|
||||
progress.SetStage("최신 상태입니다", "받을 새 버전이 없습니다.");
|
||||
progress.SetPercent(100);
|
||||
Thread.Sleep(400); // 창이 깜빡이고 사라지지 않도록 잠깐 보여준다.
|
||||
return null;
|
||||
}
|
||||
|
||||
progress.SetStage("새 버전을 찾았습니다", "업데이트 파일을 불러오고 있습니다.");
|
||||
progress.SetPercent(12);
|
||||
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "UbisamPlatformUpdate_" + Guid.NewGuid().ToString("N"));
|
||||
var distName = string.IsNullOrWhiteSpace(settings.DistPath) ? "dist" : settings.DistPath.Trim().Replace('\\', '/');
|
||||
var keepTemp = false;
|
||||
|
||||
try
|
||||
{
|
||||
// 업데이트 파일만 얕게 받는다 — 소스 전체 이력을 받지 않는다.
|
||||
// --progress를 줘야 진행률이 나온다(출력이 리디렉트되면 git은 기본적으로 조용하다).
|
||||
if (!TryRunGit(
|
||||
$"clone --progress --depth 1 --branch \"{branch}\" --filter=blob:none --sparse \"{settings.RepositoryUrl}\" \"{tempDir}\"",
|
||||
null, CloneTimeoutMs, out _, p => progress.SetPercent(12 + (p * 0.58))))
|
||||
{
|
||||
Note("업데이트 파일을 내려받지 못함(clone 실패) — 건너뜀");
|
||||
return null;
|
||||
}
|
||||
|
||||
progress.SetPercent(70);
|
||||
|
||||
if (!TryRunGit($"sparse-checkout set \"{distName}\"", tempDir, CloneTimeoutMs, out _, p => progress.SetPercent(70 + (p * 0.22))))
|
||||
{
|
||||
Note("업데이트 폴더를 꺼내지 못함(sparse-checkout 실패) — 건너뜀");
|
||||
return null;
|
||||
}
|
||||
|
||||
progress.SetPercent(94);
|
||||
|
||||
var remoteDist = Path.Combine(tempDir, distName.Replace('/', '\\'));
|
||||
if (!Directory.Exists(remoteDist))
|
||||
{
|
||||
Note($"저장소에 {distName} 폴더가 없음 — 커밋만 기록");
|
||||
settings.InstalledCommit = remoteCommit;
|
||||
settings.Save(SettingsPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var remoteVersion = ReadVersion(Path.Combine(remoteDist, VersionFileName));
|
||||
var localVersion = ReadVersion(Path.Combine(platformDir, VersionFileName));
|
||||
Note($"원격 버전:{remoteVersion} / 지금 버전:{localVersion}");
|
||||
|
||||
// 버전은 배포 시각("yyyy-MM-dd HH:mm:ss")이라 문자열 비교로 앞뒤가 가려진다.
|
||||
// 원격이 더 새것일 때만 설치한다 — 소스만 바뀐 커밋이면 확인 기록만 남긴다.
|
||||
if (string.IsNullOrEmpty(remoteVersion) || string.CompareOrdinal(remoteVersion, localVersion) <= 0)
|
||||
{
|
||||
Note("더 새 버전이 아님 — 커밋만 기록");
|
||||
settings.InstalledCommit = remoteCommit;
|
||||
settings.Save(SettingsPath);
|
||||
progress.SetStage("최신 상태입니다", "받을 새 버전이 없습니다.");
|
||||
progress.SetPercent(100);
|
||||
Thread.Sleep(400);
|
||||
return null;
|
||||
}
|
||||
|
||||
progress.SetStage("받기 완료", "설치할지 확인합니다.");
|
||||
progress.SetPercent(100);
|
||||
Thread.Sleep(350); // 100%가 눈에 보이도록 잠깐 둔다.
|
||||
|
||||
keepTemp = true; // 설치까지 쓰므로 여기서 지우지 않는다.
|
||||
return new Download(tempDir, remoteDist, remoteCommit, remoteVersion, localVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!keepTemp)
|
||||
{
|
||||
TryDeleteDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>받아둔 업데이트 파일 한 벌.</summary>
|
||||
private sealed class Download
|
||||
{
|
||||
public Download(string tempDir, string distDir, string commit, string remoteVersion, string localVersion)
|
||||
{
|
||||
TempDir = tempDir;
|
||||
DistDir = distDir;
|
||||
Commit = commit;
|
||||
RemoteVersion = remoteVersion;
|
||||
LocalVersion = localVersion;
|
||||
}
|
||||
|
||||
public string TempDir { get; }
|
||||
|
||||
public string DistDir { get; }
|
||||
|
||||
public string Commit { get; }
|
||||
|
||||
public string RemoteVersion { get; }
|
||||
|
||||
public string LocalVersion { get; }
|
||||
}
|
||||
|
||||
private static string Short(string commit)
|
||||
=> string.IsNullOrEmpty(commit) ? "(없음)" : commit.Substring(0, Math.Min(7, commit.Length));
|
||||
@ -248,7 +352,12 @@ internal static class PlatformUpdater
|
||||
return failed;
|
||||
}
|
||||
|
||||
private static bool TryRunGit(string arguments, string? workingDirectory, int timeoutMs, out string output)
|
||||
/// <summary>
|
||||
/// git을 한 번 실행한다. <paramref name="onPercent"/>를 주면 진행률(0~100)을 알려준다 —
|
||||
/// git은 진행률을 stderr에 줄바꿈 없이(\r) 흘리므로 줄 단위가 아니라 글자 단위로 읽는다.
|
||||
/// stdout과 stderr를 각각 다른 스레드에서 읽는 이유는, 한쪽 버퍼가 차서 서로 기다리는 것을 막기 위함이다.
|
||||
/// </summary>
|
||||
private static bool TryRunGit(string arguments, string? workingDirectory, int timeoutMs, out string output, Action<double>? onPercent = null)
|
||||
{
|
||||
output = string.Empty;
|
||||
|
||||
@ -276,8 +385,11 @@ internal static class PlatformUpdater
|
||||
return false;
|
||||
}
|
||||
|
||||
var stdout = process.StandardOutput.ReadToEnd();
|
||||
process.StandardError.ReadToEnd();
|
||||
var stdout = string.Empty;
|
||||
var outReader = new Thread(() => stdout = process.StandardOutput.ReadToEnd()) { IsBackground = true };
|
||||
var errReader = new Thread(() => ReadProgress(process.StandardError, onPercent)) { IsBackground = true };
|
||||
outReader.Start();
|
||||
errReader.Start();
|
||||
|
||||
if (!process.WaitForExit(timeoutMs))
|
||||
{
|
||||
@ -293,6 +405,9 @@ internal static class PlatformUpdater
|
||||
return false;
|
||||
}
|
||||
|
||||
outReader.Join(2000);
|
||||
errReader.Join(2000);
|
||||
|
||||
output = stdout;
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
@ -302,6 +417,49 @@ internal static class PlatformUpdater
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>stderr를 글자 단위로 읽어 "…: 37%" 같은 진행률만 뽑아낸다.</summary>
|
||||
private static void ReadProgress(StreamReader reader, Action<double>? onPercent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = new StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var read = reader.Read();
|
||||
if (read < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var ch = (char)read;
|
||||
if (ch == '\r' || ch == '\n')
|
||||
{
|
||||
if (onPercent != null && line.Length > 0)
|
||||
{
|
||||
var match = Regex.Match(line.ToString(), @"(\d{1,3})%");
|
||||
if (match.Success && double.TryParse(match.Groups[1].Value, out var value))
|
||||
{
|
||||
onPercent(value);
|
||||
}
|
||||
}
|
||||
|
||||
line.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Length < 200)
|
||||
{
|
||||
line.Append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 읽기가 끊겨도 실행에는 지장이 없다.
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string dir)
|
||||
{
|
||||
try
|
||||
|
||||
63
src/UbisamBase.Launcher/ProgressWindow.xaml
Normal file
63
src/UbisamBase.Launcher/ProgressWindow.xaml
Normal file
@ -0,0 +1,63 @@
|
||||
<Window x:Class="UbisamBase.Launcher.ProgressWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Width="420"
|
||||
SizeToContent="Height"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
FontFamily="Barlow, Segoe UI"
|
||||
FontSize="14"
|
||||
MouseLeftButtonDown="Window_MouseLeftButtonDown">
|
||||
|
||||
<!-- 업데이트 안내창(UpdateDialog)과 같은 언어: 위 accent 띠, accent 칩 아이콘, 한 판 구성.
|
||||
버튼이 없다 — 확인/취소는 받기가 끝난 뒤 UpdateDialog에서 묻는다. -->
|
||||
<Border x:Name="RootBorder" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<Border x:Name="AccentStrip" DockPanel.Dock="Top" Height="3"/>
|
||||
|
||||
<StackPanel Margin="26,22,26,22">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="IconChip" Width="40" Height="40" CornerRadius="20" VerticalAlignment="Top" Margin="0,0,14,0">
|
||||
<Path x:Name="IconPath"
|
||||
Data="M12,4 L12,14.5 M7.2,10 L12,15 L16.8,10 M5.5,19 L18.5,19"
|
||||
Width="20" Height="20"
|
||||
Stretch="Uniform"
|
||||
StrokeThickness="1.7"
|
||||
StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round"
|
||||
StrokeLineJoin="Round"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock x:Name="HeadlineText"
|
||||
Text="업데이트 확인 중"
|
||||
FontFamily="Barlow Condensed, Segoe UI"
|
||||
FontSize="19"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="저장소에서 최신 버전을 확인하고 있습니다."
|
||||
FontSize="12.5"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 진행 막대 — 기본 ProgressBar 대신 직접 그린다(테마 색을 그대로 쓰기 위함). -->
|
||||
<Border x:Name="TrackBorder" Height="6" CornerRadius="3" Margin="0,18,0,0" HorizontalAlignment="Stretch">
|
||||
<Border x:Name="FillBorder" Height="6" CornerRadius="3" HorizontalAlignment="Left" Width="0"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PercentText"
|
||||
Text="0%"
|
||||
FontSize="11.5"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,7,0,0"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
112
src/UbisamBase.Launcher/ProgressWindow.xaml.cs
Normal file
112
src/UbisamBase.Launcher/ProgressWindow.xaml.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace UbisamBase.Launcher;
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트를 확인하고 받는 동안 보여주는 창. 런처를 누르자마자 떠서 "지금 뭘 하고 있는지"를 알린다
|
||||
/// (예전에는 받는 동안 아무것도 안 보여서 프로그램이 안 켜지는 것처럼 보였다).
|
||||
///
|
||||
/// 받기가 끝나면 이 창은 닫히고, 설치 여부는 UpdateDialog가 묻는다.
|
||||
/// 색은 UpdateDialog와 같은 방식으로(윈도우 밝게/어둡게) 직접 넣는다 — Core를 참조할 수 없기 때문.
|
||||
/// </summary>
|
||||
public partial class ProgressWindow : Window
|
||||
{
|
||||
private double percent;
|
||||
|
||||
public ProgressWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyTheme(IsSystemLightTheme());
|
||||
SizeChanged += (_, _) => UpdateFillWidth(animate: false);
|
||||
}
|
||||
|
||||
/// <summary>제목과 설명을 바꾼다. 어느 스레드에서 불러도 된다.</summary>
|
||||
public void SetStage(string headline, string status)
|
||||
=> Dispatcher.Invoke(() =>
|
||||
{
|
||||
HeadlineText.Text = headline;
|
||||
StatusText.Text = status;
|
||||
});
|
||||
|
||||
/// <summary>0~100. 뒤로 가지 않도록 지금 값보다 큰 값만 반영한다 — git이 단계마다 0%부터 다시 세기 때문.</summary>
|
||||
public void SetPercent(double value)
|
||||
=> Dispatcher.Invoke(() =>
|
||||
{
|
||||
var clamped = Math.Max(0, Math.Min(100, value));
|
||||
if (clamped <= percent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
percent = clamped;
|
||||
PercentText.Text = ((int)percent) + "%";
|
||||
UpdateFillWidth(animate: true);
|
||||
});
|
||||
|
||||
private void UpdateFillWidth(bool animate)
|
||||
{
|
||||
var target = Math.Max(0, TrackBorder.ActualWidth * percent / 100.0);
|
||||
|
||||
if (!animate)
|
||||
{
|
||||
FillBorder.BeginAnimation(WidthProperty, null);
|
||||
FillBorder.Width = target;
|
||||
return;
|
||||
}
|
||||
|
||||
// 숫자가 툭툭 튀지 않도록 짧게 이어 붙인다.
|
||||
FillBorder.BeginAnimation(WidthProperty, new DoubleAnimation(target, TimeSpan.FromMilliseconds(220))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut },
|
||||
FillBehavior = FillBehavior.HoldEnd
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplyTheme(bool light)
|
||||
{
|
||||
var background = Brush(light ? "#FFFFFFFF" : "#FF2A2E34");
|
||||
var foreground = Brush(light ? "#FF1D1F20" : "#FFE8EAED");
|
||||
var border = Brush(light ? "#FFD6D6D9" : "#FF3A3F47");
|
||||
var muted = Brush(light ? "#FF98989B" : "#FF8A9099");
|
||||
var accent = Brush(light ? "#FF2196F3" : "#FF4DA6F5");
|
||||
var track = Brush(light ? "#FFEDEDEF" : "#FF23262B");
|
||||
var chip = Brush(light ? "#142196F3" : "#1F4DA6F5");
|
||||
|
||||
Background = background;
|
||||
Foreground = foreground;
|
||||
RootBorder.BorderBrush = border;
|
||||
AccentStrip.Background = accent;
|
||||
IconChip.Background = chip;
|
||||
IconPath.Stroke = accent;
|
||||
HeadlineText.Foreground = foreground;
|
||||
StatusText.Foreground = muted;
|
||||
PercentText.Foreground = muted;
|
||||
TrackBorder.Background = track;
|
||||
FillBorder.Background = accent;
|
||||
}
|
||||
|
||||
private static SolidColorBrush Brush(string hex)
|
||||
=> new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
|
||||
|
||||
private static bool IsSystemLightTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
|
||||
return key?.GetValue("AppsUseLightTheme") is not int value || value != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => DragMove();
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using UbisamBase.Core.Bootstrap;
|
||||
|
||||
@ -10,10 +12,60 @@ namespace UbisamBase.Shell;
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private const string LauncherFileName = "UbisamBase.Launcher.exe";
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
if (RedirectToLauncher())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UbisamAppBuilder.Run(AppDomain.CurrentDomain.BaseDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 exe를 직접 실행한 경우 런처로 넘긴다.
|
||||
///
|
||||
/// 프로그램의 정식 진입점은 <b>항상 런처</b>다 — 런처만이 배포 폴더(D:\UbisamPlatform\bin)를
|
||||
/// 최신으로 맞추고 업데이트를 확인한다. 폴더에 exe가 둘 보여서 이걸 직접 누르는 일이 생기는데,
|
||||
/// 그러면 갱신 없이 옛 파일로 떠서 "왜 업데이트가 안 되지?"가 된다. 그래서 조용히 런처로 넘긴다.
|
||||
///
|
||||
/// 런처는 Shell을 <b>같은 프로세스에서</b> 로드하므로, 런처로 실행된 경우 이 프로세스의 exe
|
||||
/// 이름은 런처다 — 그때는 아무 일도 하지 않는다.
|
||||
/// </summary>
|
||||
private bool RedirectToLauncher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var currentExe = Process.GetCurrentProcess().MainModule?.FileName;
|
||||
|
||||
if (string.Equals(Path.GetFileName(currentExe), LauncherFileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false; // 런처가 띄운 정상 경로
|
||||
}
|
||||
|
||||
var launcherPath = Path.Combine(baseDir, LauncherFileName);
|
||||
if (!File.Exists(launcherPath))
|
||||
{
|
||||
return false; // 런처 없이 배포된 환경 — 그대로 실행한다
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(launcherPath)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = baseDir
|
||||
});
|
||||
|
||||
Shutdown();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false; // 넘기지 못하면 그냥 이대로 실행한다 — 프로그램이 안 켜지는 것보다 낫다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user