feat(tools): add first-run external tool installer wizard and catalog
첫 실행 시 외부 도구 설치 마법사를 띄우고 7종(FFmpeg·LibreOffice·Pandoc·ImageMagick·H2Orestart·agy·Codex)을 기본 전체 선택 상태로 순차 설치한다. - Core: ExternalToolCatalog(정적 카탈로그, winget ID 실검증), ExternalToolInstaller(winget 무인 실행·재감지 판정, IExternalCommandRunner 시드로 테스트 가능) - Core: ExternalToolDetector에 winget FFmpeg 스캔 + Pandoc/ImageMagick 감지 추가 - App: ToolSetupWindow + ToolSetupViewModel, 첫 실행 플래그(setup.tools.done), 설정 창 '외부 도구 설치/검증' 재진입 버튼 - Tests: ExternalToolInstallerTests 5건 - 기존 미커밋 작업(광고 Ads 지원: AdBannerControl/AdLargeCardControl/AdService, AI 백엔드 agy·switchboard·codex 채팅 클라이언트, 관련 테스트) 포함
This commit is contained in:
parent
f00f6567f4
commit
6a41c4a0dc
37 changed files with 2545 additions and 69 deletions
184
src/Everything2Everything.App/ViewModels/ToolSetupViewModel.cs
Normal file
184
src/Everything2Everything.App/ViewModels/ToolSetupViewModel.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Everything2Everything.Core.Converters;
|
||||
|
||||
namespace Everything2Everything.App.ViewModels;
|
||||
|
||||
public enum ToolInstallState
|
||||
{
|
||||
NotInstalled,
|
||||
Installed,
|
||||
Busy,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// <summary>설치 마법사 목록의 한 행. 카탈로그 정의 + 선택/상태/진행 표현.</summary>
|
||||
public sealed class ToolInstallItem : INotifyPropertyChanged
|
||||
{
|
||||
public ExternalToolDefinition Definition { get; }
|
||||
public ToolInstallItem(ExternalToolDefinition definition)
|
||||
{
|
||||
Definition = definition;
|
||||
RefreshStatus();
|
||||
}
|
||||
|
||||
public string Name => Definition.DisplayName;
|
||||
public string Description => Definition.Description;
|
||||
|
||||
private bool _isSelected = true;
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set { _isSelected = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
private ToolInstallState _state;
|
||||
public ToolInstallState State
|
||||
{
|
||||
get => _state;
|
||||
private set
|
||||
{
|
||||
_state = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
OnPropertyChanged(nameof(StatusBrush));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusText => State switch
|
||||
{
|
||||
ToolInstallState.Installed => "설치됨",
|
||||
ToolInstallState.Busy => "설치 중…",
|
||||
ToolInstallState.Failed => "실패",
|
||||
_ => "미설치",
|
||||
};
|
||||
|
||||
public bool IsBusy => State == ToolInstallState.Busy;
|
||||
|
||||
public Brush StatusBrush => State switch
|
||||
{
|
||||
ToolInstallState.Installed => BrushOf("FsStatusSuccess"),
|
||||
ToolInstallState.Busy => BrushOf("FsStatusInfo"),
|
||||
ToolInstallState.Failed => BrushOf("FsStatusDanger"),
|
||||
_ => BrushOf("FsStatusWarn"),
|
||||
};
|
||||
|
||||
private string? _detail;
|
||||
public string? Detail
|
||||
{
|
||||
get => _detail;
|
||||
private set { _detail = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public void RefreshStatus()
|
||||
=> State = Definition.IsInstalled() ? ToolInstallState.Installed : ToolInstallState.NotInstalled;
|
||||
|
||||
public void ApplyResult(bool success, string? detail)
|
||||
{
|
||||
Detail = detail;
|
||||
State = success ? ToolInstallState.Installed : ToolInstallState.Failed;
|
||||
}
|
||||
|
||||
public void BeginInstall() => State = ToolInstallState.Busy;
|
||||
|
||||
private static Brush BrushOf(string key)
|
||||
=> (Application.Current?.TryFindResource(key) as Brush)
|
||||
?? _fallbacks.GetValueOrDefault(key, _warn);
|
||||
|
||||
private static readonly Brush _info = Fixed(0x1565C0);
|
||||
private static readonly Brush _success = Fixed(0x2E7D32);
|
||||
private static readonly Brush _warn = Fixed(0xC77700);
|
||||
private static readonly Brush _danger = Fixed(0xC62828);
|
||||
private static readonly Dictionary<string, Brush> _fallbacks = new()
|
||||
{
|
||||
["FsStatusInfo"] = _info,
|
||||
["FsStatusSuccess"] = _success,
|
||||
["FsStatusWarn"] = _warn,
|
||||
["FsStatusDanger"] = _danger,
|
||||
};
|
||||
|
||||
private static Brush Fixed(uint rgb)
|
||||
{
|
||||
var c = (int)rgb;
|
||||
return new SolidColorBrush(Color.FromRgb((byte)(c >> 16), (byte)(c >> 8), (byte)c));
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
/// <summary>설치 마법사의 루트 뷰모델: 목록 + 전체 선택 + 선택분 순차 설치.</summary>
|
||||
public sealed class ToolSetupViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly ExternalToolInstaller _installer;
|
||||
private bool _isInstalling;
|
||||
private bool _selectAll = true;
|
||||
|
||||
public ObservableCollection<ToolInstallItem> Tools { get; } = new();
|
||||
|
||||
public ToolSetupViewModel(IEnumerable<ExternalToolDefinition>? definitions = null, ExternalToolInstaller? installer = null)
|
||||
{
|
||||
_installer = installer ?? new ExternalToolInstaller();
|
||||
foreach (var def in definitions ?? ExternalToolCatalog.All)
|
||||
Tools.Add(new ToolInstallItem(def));
|
||||
}
|
||||
|
||||
public bool IsInstalling
|
||||
{
|
||||
get => _isInstalling;
|
||||
private set { _isInstalling = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool SelectAll
|
||||
{
|
||||
get => _selectAll;
|
||||
set
|
||||
{
|
||||
_selectAll = value;
|
||||
foreach (var t in Tools) t.IsSelected = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshStatus()
|
||||
{
|
||||
foreach (var t in Tools) t.RefreshStatus();
|
||||
}
|
||||
|
||||
public async Task InstallSelectedAsync(CancellationToken ct)
|
||||
{
|
||||
if (IsInstalling) return;
|
||||
IsInstalling = true;
|
||||
try
|
||||
{
|
||||
foreach (var item in Tools.Where(t => t.IsSelected).ToList())
|
||||
await InstallOneAsync(item, ct).ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsInstalling = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallOneAsync(ToolInstallItem item, CancellationToken ct)
|
||||
{
|
||||
item.BeginInstall();
|
||||
var result = await _installer.InstallAsync(item.Definition, ct).ConfigureAwait(true);
|
||||
var detail = string.IsNullOrWhiteSpace(result.Output)
|
||||
? result.Message
|
||||
: Tail(result.Output, 600);
|
||||
item.ApplyResult(result.Success, detail);
|
||||
}
|
||||
|
||||
private static string Tail(string s, int max)
|
||||
=> s.Length <= max ? s : "…" + s[^max..];
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue