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
|
|
@ -76,15 +76,27 @@ public partial class App : Application
|
|||
|
||||
private void ShowMainWindow()
|
||||
{
|
||||
var window = new MainWindow(Engine, Settings);
|
||||
RunFirstRunSetupIfNeeded();
|
||||
var adService = _services.GetService<Everything2Everything.Core.Ads.IAdService>();
|
||||
var window = new MainWindow(Engine, Settings, null, adService);
|
||||
MainWindow = window;
|
||||
window.Show();
|
||||
window.Activate();
|
||||
}
|
||||
|
||||
/// <summary>첫 실행이면 외부 도구 설치 마법사를 한 번 띄우고 완료 플래그를 남긴다(설정 창에서 재진입 가능).</summary>
|
||||
private void RunFirstRunSetupIfNeeded()
|
||||
{
|
||||
if (Settings.Get("setup.tools.done") == "1") return;
|
||||
var wizard = new ToolSetupWindow();
|
||||
wizard.ShowDialog();
|
||||
Settings.Set("setup.tools.done", "1");
|
||||
}
|
||||
|
||||
private void ShowConvertDialog(IReadOnlyList<string> files)
|
||||
{
|
||||
var window = new Views.MainWindow(Engine, Settings, files);
|
||||
var adService = _services.GetService<Everything2Everything.Core.Ads.IAdService>();
|
||||
var window = new Views.MainWindow(Engine, Settings, files, adService);
|
||||
MainWindow = window;
|
||||
window.Show();
|
||||
window.Activate();
|
||||
|
|
|
|||
106
src/Everything2Everything.App/ViewModels/AdViewModel.cs
Normal file
106
src/Everything2Everything.App/ViewModels/AdViewModel.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using Everything2Everything.Core.Ads;
|
||||
|
||||
namespace Everything2Everything.App.ViewModels;
|
||||
|
||||
public class AdViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly IAdService _adService;
|
||||
private AdItem? _currentBannerAd;
|
||||
private AdItem? _currentLargeAd;
|
||||
private bool _isBannerVisible = true;
|
||||
private bool _isLargeCardVisible = true;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public AdItem? CurrentBannerAd
|
||||
{
|
||||
get => _currentBannerAd;
|
||||
set { _currentBannerAd = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public AdItem? CurrentLargeAd
|
||||
{
|
||||
get => _currentLargeAd;
|
||||
set { _currentLargeAd = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsBannerVisible
|
||||
{
|
||||
get => _isBannerVisible;
|
||||
set { _isBannerVisible = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsLargeCardVisible
|
||||
{
|
||||
get => _isLargeCardVisible;
|
||||
set { _isLargeCardVisible = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public ICommand ClickBannerAdCommand { get; }
|
||||
public ICommand ClickLargeAdCommand { get; }
|
||||
public ICommand DismissBannerCommand { get; }
|
||||
public ICommand DismissLargeCardCommand { get; }
|
||||
|
||||
public AdViewModel(IAdService adService)
|
||||
{
|
||||
_adService = adService ?? throw new ArgumentNullException(nameof(adService));
|
||||
|
||||
CurrentBannerAd = _adService.GetNextBannerAd();
|
||||
CurrentLargeAd = _adService.GetNextLargeCardAd();
|
||||
|
||||
ClickBannerAdCommand = new AdCommand(_ =>
|
||||
{
|
||||
if (CurrentBannerAd != null)
|
||||
_adService.OpenAdUrl(CurrentBannerAd);
|
||||
});
|
||||
|
||||
ClickLargeAdCommand = new AdCommand(_ =>
|
||||
{
|
||||
if (CurrentLargeAd != null)
|
||||
_adService.OpenAdUrl(CurrentLargeAd);
|
||||
});
|
||||
|
||||
DismissBannerCommand = new AdCommand(_ => IsBannerVisible = false);
|
||||
DismissLargeCardCommand = new AdCommand(_ => IsLargeCardVisible = false);
|
||||
}
|
||||
|
||||
public void RotateBannerAd()
|
||||
{
|
||||
CurrentBannerAd = _adService.GetNextBannerAd();
|
||||
}
|
||||
|
||||
public void RotateLargeCardAd()
|
||||
{
|
||||
CurrentLargeAd = _adService.GetNextLargeCardAd();
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private sealed class AdCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Func<object?, bool>? _canExecute;
|
||||
|
||||
public AdCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
|
||||
public void Execute(object? parameter) => _execute(parameter);
|
||||
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
99
src/Everything2Everything.App/Views/AdBannerControl.xaml
Normal file
99
src/Everything2Everything.App/Views/AdBannerControl.xaml
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<UserControl x:Class="Everything2Everything.App.Views.AdBannerControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="56" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Visibility="{Binding IsBannerVisible, Converter={StaticResource BoolToVis}}"
|
||||
Style="{StaticResource FsDoubleBezelShellStyle}"
|
||||
Margin="0,4,0,0">
|
||||
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="12,8">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 1. [AD] / [스폰서] 배지 -->
|
||||
<Border Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
Padding="6,2"
|
||||
CornerRadius="4"
|
||||
Background="#1A00E5FF"
|
||||
BorderBrush="{StaticResource FsAccentCyan}"
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentBannerAd.BadgeText, FallbackValue=AD}"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource FsAccentCyan}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
|
||||
<!-- 2. 광고 타이틀 및 설명 -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0,0,12,0">
|
||||
<TextBlock Text="{Binding CurrentBannerAd.Title, FallbackValue=스폰서십 안내}"
|
||||
Style="{StaticResource FsLabelStyle}"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding CurrentBannerAd.Description, FallbackValue=Everything2Everything을 후원하고 최신 기능을 가장 먼저 만나보세요.}"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Foreground="{StaticResource FsTextSecondary}"
|
||||
Margin="0,2,0,0"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 3. CTA 액션 버튼 -->
|
||||
<Button Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
Padding="12,4"
|
||||
Height="28"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding ClickBannerAdCommand}"
|
||||
ToolTip="광고 및 안내 링크 열기">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentBannerAd.CtaText, FallbackValue='자세히 보기'}"
|
||||
FontSize="11"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<ui:SymbolIcon Symbol="Open24"
|
||||
FontSize="11"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- 4. 광고 닫기 버튼 -->
|
||||
<Button Grid.Column="3"
|
||||
Width="24" Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource FsIconButtonStyle}"
|
||||
Command="{Binding DismissBannerCommand}"
|
||||
ToolTip="광고 닫기">
|
||||
<ui:SymbolIcon Symbol="Dismiss24"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource FsTextTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</UserControl>
|
||||
11
src/Everything2Everything.App/Views/AdBannerControl.xaml.cs
Normal file
11
src/Everything2Everything.App/Views/AdBannerControl.xaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.Windows.Controls;
|
||||
|
||||
namespace Everything2Everything.App.Views;
|
||||
|
||||
public partial class AdBannerControl : UserControl
|
||||
{
|
||||
public AdBannerControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
117
src/Everything2Everything.App/Views/AdLargeCardControl.xaml
Normal file
117
src/Everything2Everything.App/Views/AdLargeCardControl.xaml
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<UserControl x:Class="Everything2Everything.App.Views.AdLargeCardControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="220" d:DesignWidth="320">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border Visibility="{Binding IsLargeCardVisible, Converter={StaticResource BoolToVis}}"
|
||||
Style="{StaticResource FsDoubleBezelShellStyle}"
|
||||
Margin="0,16,0,0">
|
||||
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="16">
|
||||
<StackPanel>
|
||||
<!-- 1. 헤더: 스폰서 배지 + 닫기 버튼 -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Padding="6,2"
|
||||
CornerRadius="4"
|
||||
Background="#1A3B82F6"
|
||||
BorderBrush="{StaticResource FsAccentBlue}"
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentLargeAd.BadgeText, FallbackValue='스폰서 추천'}"
|
||||
FontSize="10"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource FsAccentBlue}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Width="22" Height="22"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource FsIconButtonStyle}"
|
||||
Command="{Binding DismissLargeCardCommand}"
|
||||
ToolTip="광고 닫기">
|
||||
<ui:SymbolIcon Symbol="Dismiss24"
|
||||
FontSize="10"
|
||||
Foreground="{StaticResource FsTextTertiary}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- 2. 본문: 아이콘/이미지 + 타이틀 + 설명 -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0"
|
||||
Width="36" Height="36"
|
||||
CornerRadius="8"
|
||||
Background="{StaticResource FsBgInput}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="1"
|
||||
Margin="0,0,12,0"
|
||||
VerticalAlignment="Center">
|
||||
<Image Width="20" Height="20"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="pack://application:,,,/Everything2Everything;component/Assets/logo-mark.png"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentLargeAd.Title, FallbackValue='비즈니스 미디어 솔루션'}"
|
||||
Style="{StaticResource FsLabelStyle}"
|
||||
Foreground="{StaticResource FsTextPrimary}"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding CurrentLargeAd.Description, FallbackValue='고성능 무손실 일괄 변환을 업무 환경에 적용하세요.'}"
|
||||
Style="{StaticResource FsCaptionStyle}"
|
||||
Foreground="{StaticResource FsTextSecondary}"
|
||||
Margin="0,3,0,0"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 3. 하단 전면 CTA 버튼 -->
|
||||
<Button Style="{StaticResource FsIslandPrimaryButtonStyle}"
|
||||
Height="34"
|
||||
Command="{Binding ClickLargeAdCommand}"
|
||||
ToolTip="자세히 보기">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding CurrentLargeAd.CtaText, FallbackValue='솔루션 살펴보기'}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<ui:SymbolIcon Symbol="ArrowRight24"
|
||||
FontSize="12"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using System.Windows.Controls;
|
||||
|
||||
namespace Everything2Everything.App.Views;
|
||||
|
||||
public partial class AdLargeCardControl : UserControl
|
||||
{
|
||||
public AdLargeCardControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:local="clr-namespace:Everything2Everything.App.Views"
|
||||
Title="Everything2Everything"
|
||||
Icon="pack://application:,,,/Everything2Everything;component/Assets/app-icon.png"
|
||||
Width="1280" Height="960"
|
||||
|
|
@ -327,6 +328,26 @@
|
|||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- E. AI 텍스트 변환 작업 선택기 (TXT/MD) -->
|
||||
<StackPanel x:Name="AiQuickPanel" Margin="0,0,0,6" Visibility="Collapsed">
|
||||
<TextBlock Text="AI 변환 작업" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,4"/>
|
||||
<ComboBox x:Name="AiTaskQuickCombo"
|
||||
AutomationProperties.AutomationId="AiTaskQuickCombo"
|
||||
SelectedIndex="{Binding Options.AiTaskIndex, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"
|
||||
SelectionChanged="OnAiTaskSelectionChanged">
|
||||
<ComboBoxItem Content="핵심 요약 (Summarize)"/>
|
||||
<ComboBoxItem Content="다국어 번역 (Translate)"/>
|
||||
<ComboBoxItem Content="오탈자 및 문법 교정 (Proofread)"/>
|
||||
</ComboBox>
|
||||
<StackPanel x:Name="AiTargetLanguagePanel" Margin="0,8,0,0" Visibility="Collapsed">
|
||||
<TextBlock Text="번역 대상 언어" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="AiTargetLanguageBox"
|
||||
Style="{StaticResource FsPathInputStyle}"
|
||||
Padding="10,6"
|
||||
Text="{Binding Options.TargetLanguage, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 5. 상세 인코딩 설정 (폴드아웃 Expander: 프리셋 직하단 위치) -->
|
||||
<Expander x:Name="AdvancedOptionsExpander"
|
||||
Margin="0,6,0,6"
|
||||
|
|
@ -666,17 +687,18 @@
|
|||
<Border Grid.Row="0" Grid.Column="0"
|
||||
Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}"
|
||||
BorderThickness="0,0,1,1" Padding="16,8">
|
||||
BorderThickness="0,0,1,1" Padding="12,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="175"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*" MinWidth="110"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Search Box with Live Watermark & Clear Button -->
|
||||
<Grid Grid.Column="0" Width="175" VerticalAlignment="Center">
|
||||
<!-- Search Box with Live Watermark & Clear Button (Flex Width) -->
|
||||
<Grid Grid.Column="0" Margin="0,0,8,0" VerticalAlignment="Center">
|
||||
<TextBox x:Name="SearchBox" Style="{StaticResource FsPathInputStyle}"
|
||||
Padding="28,5,24,5" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="파일명 또는 확장자로 검색" VerticalAlignment="Center"/>
|
||||
ToolTip="파일명 또는 확장자로 검색" VerticalAlignment="Center" HorizontalAlignment="Stretch"/>
|
||||
<ui:SymbolIcon Symbol="Search24" FontSize="13" Foreground="{StaticResource FsTextTertiary}"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="8,0,0,0" IsHitTestVisible="False"/>
|
||||
<TextBlock x:Name="SearchPlaceholderText"
|
||||
|
|
@ -720,49 +742,26 @@
|
|||
<ui:SymbolIcon Symbol="Dismiss24" FontSize="10" Foreground="{StaticResource FsTextTertiary}" VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- Category Filter Chips & Inspector Toggle -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="All">
|
||||
<TextBlock Text="전체" FontSize="11" VerticalAlignment="Center"/>
|
||||
</Button>
|
||||
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="Image">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="Image24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="이미지" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="Document">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="Document24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="문서" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="Media">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="Video24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="미디어" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Margin="0,0,3,0" Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding FilterCategoryCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
CommandParameter="Data">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="Database24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="데이터" FontSize="11" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Border Width="1" Height="14" Background="{StaticResource FsBorderSubtle}" Margin="5,0"/>
|
||||
<Button Padding="7,3" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
<!-- 카테고리 필터 폴드아웃 드롭다운 -->
|
||||
<ComboBox x:Name="CategoryFilterCombo" Grid.Column="1"
|
||||
AutomationProperties.AutomationId="CategoryFilterCombo"
|
||||
MinWidth="90" Height="28" Padding="10,4"
|
||||
FontSize="11" VerticalAlignment="Center"
|
||||
ToolTip="카테고리 필터 (전체 · 이미지 · 문서 · 미디어 · 데이터)"
|
||||
SelectionChanged="OnCategoryFilterSelectionChanged">
|
||||
<ComboBoxItem Tag="All" IsSelected="True" Content="전체"/>
|
||||
<ComboBoxItem Tag="Image" Content="이미지"/>
|
||||
<ComboBoxItem Tag="Document" Content="문서"/>
|
||||
<ComboBoxItem Tag="Media" Content="미디어"/>
|
||||
<ComboBoxItem Tag="Data" Content="데이터"/>
|
||||
</ComboBox>
|
||||
<!-- 구분선 & 미리보기 토글 버튼 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="6,0,0,0">
|
||||
<Border Width="1" Height="14" Background="{StaticResource FsBorderSubtle}" Margin="2,0,6,0"/>
|
||||
<Button Padding="7,4" Height="28" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Command="{Binding ToggleInspectorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||
ToolTip="미리보기 패널 접기/펼치기">
|
||||
ToolTip="미리보기 패널 접기/펼치기"
|
||||
VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<ui:SymbolIcon Symbol="PreviewLink24" FontSize="11" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<TextBlock Text="미리보기" FontSize="11" VerticalAlignment="Center"/>
|
||||
|
|
@ -774,8 +773,13 @@
|
|||
|
||||
<!-- 좌측: Active Queue OR Past Results -->
|
||||
<Grid Grid.Row="1" Grid.Column="0">
|
||||
<!-- Active Queue view (default) -->
|
||||
<Grid x:Name="ActiveQueueView" Visibility="Visible">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Active Queue view (default) -->
|
||||
<Grid x:Name="ActiveQueueView" Grid.Row="0" Visibility="Visible">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
|
|
@ -1012,7 +1016,7 @@
|
|||
</Grid>
|
||||
|
||||
<!-- Past Results view -->
|
||||
<Grid x:Name="PastResultsContainer" Visibility="Collapsed">
|
||||
<Grid x:Name="PastResultsContainer" Grid.Row="0" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
|
|
@ -1229,6 +1233,9 @@
|
|||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- 가로 슬림 배너 광고 슬롯 -->
|
||||
<local:AdBannerControl Grid.Row="1" Margin="12,4,12,8"
|
||||
DataContext="{Binding AdVm, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
</Grid>
|
||||
<!-- /좌측 컬럼 끝 -->
|
||||
|
||||
|
|
@ -1468,6 +1475,10 @@
|
|||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- 대형 카드 광고 슬롯 (스폰서십 및 솔루션 안내) -->
|
||||
<local:AdLargeCardControl Margin="0,12,0,0"
|
||||
DataContext="{Binding AdVm, RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Everything2Everything.App.Shell;
|
|||
using Everything2Everything.App.ViewModels;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Filters;
|
||||
using Everything2Everything.Core.Ads;
|
||||
using Everything2Everything.Core.Inspector;
|
||||
using Everything2Everything.Core.Presets;
|
||||
using LossClass = Everything2Everything.Core.Providers.LossClass;
|
||||
|
|
@ -134,15 +135,22 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC
|
|||
if (_selectedCategory != value)
|
||||
{
|
||||
_selectedCategory = value;
|
||||
if (CategoryFilterCombo != null && CategoryFilterCombo.SelectedIndex != (int)value)
|
||||
{
|
||||
CategoryFilterCombo.SelectedIndex = (int)value;
|
||||
}
|
||||
ApplyQueueFilters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MainWindow(ConversionEngine engine, ISettingsStore settings, IReadOnlyList<string>? initialFiles = null)
|
||||
public AdViewModel AdVm { get; }
|
||||
|
||||
public MainWindow(ConversionEngine engine, ISettingsStore settings, IReadOnlyList<string>? initialFiles = null, IAdService? adService = null)
|
||||
{
|
||||
_engine = engine;
|
||||
_settings = settings;
|
||||
AdVm = new AdViewModel(adService ?? new AdService());
|
||||
|
||||
AddFilesCommand = new RelayCommand(_ => PickAndAddFiles());
|
||||
ProcessQueueCommand = new RelayCommand(_ => OnProcessQueueClick(this, new RoutedEventArgs()),
|
||||
|
|
@ -828,6 +836,18 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC
|
|||
}
|
||||
}
|
||||
|
||||
private void OnCategoryFilterSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (CategoryFilterCombo?.SelectedItem is ComboBoxItem item && item.Tag is string tag)
|
||||
{
|
||||
ApplyFilterCategory(tag);
|
||||
}
|
||||
else if (CategoryFilterCombo != null && CategoryFilterCombo.SelectedIndex >= 0)
|
||||
{
|
||||
SelectedCategory = (FilterCategory)CategoryFilterCombo.SelectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilterCategory(string? categoryName)
|
||||
{
|
||||
if (Enum.TryParse<FilterCategory>(categoryName, true, out var cat))
|
||||
|
|
@ -1540,9 +1560,25 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC
|
|||
if (PdfQuickPanel is not null)
|
||||
PdfQuickPanel.Visibility = isPdf ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
var isText = ext is ".txt" or ".md";
|
||||
if (AiQuickPanel is not null)
|
||||
AiQuickPanel.Visibility = isText ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (AiTargetLanguagePanel is not null && AiTaskQuickCombo is not null)
|
||||
AiTargetLanguagePanel.Visibility = (isText && AiTaskQuickCombo.SelectedIndex == 1)
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
|
||||
UpdateMediaPanelForFormat(extension);
|
||||
}
|
||||
|
||||
private void OnAiTaskSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (AiTargetLanguagePanel is null || AiTaskQuickCombo is null) return;
|
||||
AiTargetLanguagePanel.Visibility = AiTaskQuickCombo.SelectedIndex == 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>출력이 영상/오디오/PDF/이미지일 때 상세 인코딩 폴드아웃 서브패널 노출(Fluent 2 점진적 공개 패턴).</summary>
|
||||
private void UpdateMediaPanelForFormat(string? extension)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -74,6 +74,20 @@
|
|||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI 텍스트 처리 (txt/md) -->
|
||||
<StackPanel x:Name="AiQuickPanel" Margin="0,18,0,0" Visibility="Collapsed">
|
||||
<TextBlock Text="AI 작업 선택" Style="{StaticResource FsCaptionStyle}" Margin="0,0,0,4"/>
|
||||
<ComboBox SelectedIndex="{Binding AiTaskIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="핵심 요약 (Summarize)"/>
|
||||
<ComboBoxItem Content="자연스러운 번역 (Translate)"/>
|
||||
<ComboBoxItem Content="오탈자 및 문법 교정 (Proofread)"/>
|
||||
</ComboBox>
|
||||
<TextBox Margin="0,8,0,0" Style="{StaticResource FsPathInputStyle}"
|
||||
Padding="10,6"
|
||||
Text="{Binding TargetLanguage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="번역 시 대상 언어 (예: 영어, 일본어, 중국어)"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<Grid Margin="0,22,0,0">
|
||||
<Button HorizontalAlignment="Left" Content="자세히 옵션…"
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ public partial class QuickOptionsWindow : FluentWindow
|
|||
var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi";
|
||||
var isLossyAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg";
|
||||
var isImageQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif";
|
||||
var isText = ext is ".txt" or ".md";
|
||||
|
||||
VideoQuickPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed;
|
||||
AudioQuickPanel.Visibility = (isVideo || isLossyAudio) ? Visibility.Visible : Visibility.Collapsed;
|
||||
ImageQualityPanel.Visibility = isImageQuality ? Visibility.Visible : Visibility.Collapsed;
|
||||
AiQuickPanel.Visibility = isText ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 영상의 오디오 트랙임을 구분
|
||||
if (isVideo) AudioPanelLabel.Text = "오디오 비트레이트";
|
||||
|
|
|
|||
|
|
@ -39,12 +39,48 @@
|
|||
|
||||
<TextBlock Text="기본 백엔드" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<ComboBox x:Name="BackendCombo" Margin="0,0,0,16">
|
||||
<ComboBoxItem Content="자동 (API 키 우선, 없으면 Codex)"/>
|
||||
<ComboBoxItem Content="자동 (API 키 우선 · Switchboard · Antigravity · Codex)"/>
|
||||
<ComboBoxItem Content="Switchboard 게이트웨이 (http://127.0.0.1:8787 · 로컬/LAN)"/>
|
||||
<ComboBoxItem Content="Antigravity CLI (agy · Gemini OAuth · 무료/구독)"/>
|
||||
<ComboBoxItem Content="OpenAI (API 키)"/>
|
||||
<ComboBoxItem Content="Anthropic (API 키)"/>
|
||||
<ComboBoxItem Content="Codex CLI (ChatGPT 구독 OAuth)"/>
|
||||
</ComboBox>
|
||||
|
||||
<!-- Switchboard Gateway -->
|
||||
<TextBlock Text="Switchboard 게이트웨이 (로컬/LAN 멀티에이전트 오케스트레이션)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="SwitchboardEndpointBox" Grid.Column="0"
|
||||
Style="{StaticResource FsPathInputStyle}"
|
||||
TextChanged="OnSwitchboardEndpointChanged"/>
|
||||
<Button x:Name="SwitchboardVerifyBtn" Grid.Column="1" Content="테스트"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
|
||||
Padding="12,6" Click="OnVerifySwitchboard"/>
|
||||
<Button x:Name="SwitchboardStartBtn" Grid.Column="2" Content="게이트웨이 기동"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
|
||||
Padding="12,6" Click="OnStartSwitchboardGateway"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
|
||||
<Ellipse x:Name="SwitchboardDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="SwitchboardStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Antigravity CLI (OAuth) -->
|
||||
<TextBlock Text="Antigravity CLI (agy · Google OAuth · API 키 불필요)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,16">
|
||||
<Ellipse x:Name="AgyDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="AgyStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
|
||||
<Button x:Name="AgyVerifyBtn" Content="테스트" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyAgy" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- OpenAI -->
|
||||
<TextBlock Text="OpenAI API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,4">
|
||||
|
|
@ -111,6 +147,9 @@
|
|||
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
||||
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
|
||||
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,12"/>
|
||||
<Button Content="외부 도구 설치 / 검증" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Padding="14,6" HorizontalAlignment="Right" Margin="0,0,0,12"
|
||||
Click="OnOpenToolInstaller"/>
|
||||
<CheckBox x:Name="GpuToggle" Content="영상 변환 시 GPU 가속(NVENC) 시도 — 없으면 CPU 자동 전환"
|
||||
Foreground="{StaticResource FsTextSecondary}" IsChecked="True" Margin="0,0,0,16"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,15 @@ public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
|||
var backend = (_settings.Get("ai.backend") ?? "auto").ToLowerInvariant();
|
||||
BackendCombo.SelectedIndex = backend switch
|
||||
{
|
||||
"openai" => 1,
|
||||
"anthropic" => 2,
|
||||
"codex" => 3,
|
||||
"switchboard" or "gateway" => 1,
|
||||
"agy" => 2,
|
||||
"openai" => 3,
|
||||
"anthropic" => 4,
|
||||
"codex" => 5,
|
||||
_ => 0,
|
||||
};
|
||||
ModelBox.Text = _settings.Get("ai.model") ?? string.Empty;
|
||||
SwitchboardEndpointBox.Text = _settings.Get("switchboard.endpoint") ?? "http://127.0.0.1:8787";
|
||||
|
||||
SetKeyStatus(OpenAiDot, OpenAiStatus, _settings.Contains("openai.apikey"), HasEnv("OPENAI_API_KEY"));
|
||||
SetKeyStatus(AnthropicDot, AnthropicStatus, _settings.Contains("anthropic.apikey"), HasEnv("ANTHROPIC_API_KEY"));
|
||||
|
|
@ -58,6 +61,9 @@ public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
|||
private void OnAnthropicKeyChanged(object sender, RoutedEventArgs e)
|
||||
=> AnthropicVerifyBtn.IsEnabled = AnthropicKeyBox.Password.StartsWith("sk-ant-", StringComparison.Ordinal);
|
||||
|
||||
private void OnSwitchboardEndpointChanged(object sender, TextChangedEventArgs e)
|
||||
=> _ = CheckSwitchboardGatewayAsync();
|
||||
|
||||
private async void OnVerifyOpenAi(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(OpenAiDot, OpenAiStatus, OpenAiVerifyBtn,
|
||||
new OpenAiChatClient(OpenAiKeyBox.Password), ModelOr("gpt-4o-mini"));
|
||||
|
|
@ -66,6 +72,63 @@ public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
|||
=> await VerifyAsync(AnthropicDot, AnthropicStatus, AnthropicVerifyBtn,
|
||||
new AnthropicChatClient(AnthropicKeyBox.Password), ModelOr("claude-3-5-sonnet-latest"));
|
||||
|
||||
private async void OnVerifySwitchboard(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var ep = string.IsNullOrWhiteSpace(SwitchboardEndpointBox.Text)
|
||||
? (_settings.Get("switchboard.endpoint") ?? "http://127.0.0.1:8787")
|
||||
: SwitchboardEndpointBox.Text.Trim();
|
||||
await VerifyAsync(SwitchboardDot, SwitchboardStatus, SwitchboardVerifyBtn,
|
||||
new SwitchboardChatClient(ep), ModelOr(string.Empty));
|
||||
await CheckSwitchboardGatewayAsync();
|
||||
}
|
||||
|
||||
private void OnStartSwitchboardGateway(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
@"D:\workspace\Switchboard\start-gateway-lan.cmd",
|
||||
@"D:\workspace\Switchboard\start-gateway-agy.cmd",
|
||||
@"D:\workspace\Switchboard\start-gateway.cmd",
|
||||
};
|
||||
string? cmdPath = candidates.FirstOrDefault(System.IO.File.Exists);
|
||||
if (cmdPath is null)
|
||||
{
|
||||
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var relativeCandidates = new[]
|
||||
{
|
||||
System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "..", "..", "..", "..", "Switchboard", "start-gateway-lan.cmd")),
|
||||
System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "..", "..", "..", "..", "Switchboard", "start-gateway-agy.cmd")),
|
||||
System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "..", "..", "..", "..", "Switchboard", "start-gateway.cmd")),
|
||||
};
|
||||
cmdPath = relativeCandidates.FirstOrDefault(System.IO.File.Exists);
|
||||
}
|
||||
|
||||
if (cmdPath is not null)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = cmdPath,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = System.IO.Path.GetDirectoryName(cmdPath)!,
|
||||
});
|
||||
SwitchboardStatus.Text = "게이트웨이 기동 중… 잠시 후 테스트를 누르세요.";
|
||||
}
|
||||
else
|
||||
{
|
||||
SwitchboardStatus.Text = "런처 스크립트를 찾을 수 없습니다 (start-gateway-lan.cmd).";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SwitchboardStatus.Text = "기동 실패: " + Trunc(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnVerifyAgy(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(AgyDot, AgyStatus, AgyVerifyBtn, new AgyChatClient(), ModelOr(string.Empty));
|
||||
|
||||
private async void OnVerifyCodex(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(CodexDot, CodexStatus, CodexVerifyBtn, new CodexChatClient(), ModelOr(string.Empty));
|
||||
|
||||
|
|
@ -107,23 +170,59 @@ public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
|||
LibreDot.Fill = Res(libre ? "FsStatusSuccess" : "FsStatusWarn");
|
||||
LibreStatus.Text = libre ? "준비됨" : "미설치";
|
||||
|
||||
_ = CheckSwitchboardGatewayAsync();
|
||||
|
||||
var agy = ExternalToolDetector.IsAgyAvailable(out _);
|
||||
AgyDot.Fill = Res(agy ? "FsStatusSuccess" : "FsTextTertiary");
|
||||
AgyStatus.Text = agy ? "설치됨 — 키 없이 사용 가능" : "미설치 (winget install Google.AntigravityCLI)";
|
||||
AgyVerifyBtn.IsEnabled = agy;
|
||||
|
||||
var codex = ExternalToolDetector.IsCodexAvailable();
|
||||
CodexDot.Fill = Res(codex ? "FsStatusSuccess" : "FsTextTertiary");
|
||||
CodexStatus.Text = codex ? "설치됨 — 키 없이 사용 가능" : "미설치 (npm i -g @openai/codex)";
|
||||
CodexVerifyBtn.IsEnabled = codex;
|
||||
}
|
||||
|
||||
private async Task CheckSwitchboardGatewayAsync()
|
||||
{
|
||||
if (SwitchboardDot is null || SwitchboardStatus is null) return;
|
||||
var ep = string.IsNullOrWhiteSpace(SwitchboardEndpointBox?.Text)
|
||||
? (_settings.Get("switchboard.endpoint") ?? "http://127.0.0.1:8787")
|
||||
: SwitchboardEndpointBox.Text.Trim();
|
||||
var (ok, profile, resolvedEp) = await ExternalToolDetector.CheckSwitchboardGatewayHealthAsync(ep);
|
||||
if (ok)
|
||||
{
|
||||
SwitchboardDot.Fill = Res("FsStatusSuccess");
|
||||
SwitchboardStatus.Text = string.IsNullOrWhiteSpace(profile)
|
||||
? $"실행 중 ({resolvedEp})"
|
||||
: $"실행 중 ({resolvedEp} · {profile})";
|
||||
if (SwitchboardStartBtn is not null) SwitchboardStartBtn.IsEnabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SwitchboardDot.Fill = Res("FsTextTertiary");
|
||||
SwitchboardStatus.Text = $"미실행 ({ep})";
|
||||
if (SwitchboardStartBtn is not null) SwitchboardStartBtn.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSave(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var backend = BackendCombo.SelectedIndex switch
|
||||
{
|
||||
1 => "openai",
|
||||
2 => "anthropic",
|
||||
3 => "codex",
|
||||
1 => "switchboard",
|
||||
2 => "agy",
|
||||
3 => "openai",
|
||||
4 => "anthropic",
|
||||
5 => "codex",
|
||||
_ => "auto",
|
||||
};
|
||||
_settings.Set("ai.backend", backend);
|
||||
|
||||
var sbEndpoint = SwitchboardEndpointBox.Text?.Trim();
|
||||
if (!string.IsNullOrEmpty(sbEndpoint))
|
||||
_settings.Set("switchboard.endpoint", sbEndpoint);
|
||||
|
||||
var model = ModelBox.Text?.Trim();
|
||||
if (string.IsNullOrEmpty(model)) _settings.Remove("ai.model");
|
||||
else _settings.Set("ai.model", model);
|
||||
|
|
@ -137,6 +236,13 @@ public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
|||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnOpenToolInstaller(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var win = new ToolSetupWindow { Owner = this };
|
||||
win.ShowDialog();
|
||||
RefreshToolStatus();
|
||||
}
|
||||
|
||||
private void OnDownloadFfmpeg(object sender, RoutedEventArgs e)
|
||||
=> OpenUrl("https://github.com/BtbN/FFmpeg-Builds/releases");
|
||||
|
||||
|
|
|
|||
88
src/Everything2Everything.App/Views/ToolSetupWindow.xaml
Normal file
88
src/Everything2Everything.App/Views/ToolSetupWindow.xaml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<ui:FluentWindow x:Class="Everything2Everything.App.Views.ToolSetupWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
Title="처음 설정 — 외부 도구 설치"
|
||||
Width="640" Height="680"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowBackdropType="Mica"
|
||||
Background="{DynamicResource FsBgBase}">
|
||||
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ControlsDictionary/>
|
||||
<ResourceDictionary Source="FormatShiftTheme.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Background="{StaticResource FsBgBase}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:TitleBar Grid.Row="0" Title="처음 설정" ShowMaximize="False" ShowMinimize="False"/>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="24,8,24,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="필요한 외부 도구를 설치합니다."
|
||||
Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="16"/>
|
||||
<TextBlock Text="변환에 필요한 외부 도구 목록입니다. 기본값은 모두 선택된 상태이며, 아래 버튼으로 한 번에 순차 설치합니다. 이미 설치된 도구는 설치 단계에서 확인만 됩니다. (H2Orestart는 라이선스상 자동 설치 대신 직접 추가가 필요합니다.)"
|
||||
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,16"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Tools}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{StaticResource FsDoubleBezelShellStyle}" Margin="0,0,0,10" HorizontalAlignment="Stretch">
|
||||
<Border Style="{StaticResource FsDoubleBezelCoreStyle}" Padding="16,12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||
<StackPanel Grid.Column="1" Margin="0,0,12,0">
|
||||
<TextBlock Text="{Binding Name}" Style="{StaticResource FsBodyStyle}" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding Description}" Style="{StaticResource FsCaptionStyle}"
|
||||
TextWrapping="Wrap" Margin="0,3,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Ellipse Width="9" Height="9" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{Binding StatusBrush}" VerticalAlignment="Center" Margin="0,0,7,0"/>
|
||||
<TextBlock Text="{Binding StatusText}" Style="{StaticResource FsCaptionStyle}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Border Grid.Row="2" Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}" BorderThickness="0,1,0,0" Padding="24,14">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding SelectAll}" Content="전체 선택"
|
||||
Foreground="{StaticResource FsTextSecondary}" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="나중에" Style="{StaticResource FsSecondaryButtonStyle}" Padding="20,8" Click="OnLater"/>
|
||||
<Button x:Name="InstallButton" Content="설치 시작"
|
||||
Style="{StaticResource FsPrimaryButtonStyle}" Padding="20,8" Margin="8,0,0,0" Click="OnInstall"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
46
src/Everything2Everything.App/Views/ToolSetupWindow.xaml.cs
Normal file
46
src/Everything2Everything.App/Views/ToolSetupWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using System.Threading;
|
||||
using System.Windows;
|
||||
using Everything2Everything.App.ViewModels;
|
||||
|
||||
namespace Everything2Everything.App.Views;
|
||||
|
||||
/// <summary>첫 실행 외부 도구 설치 마법사. 선택된(기본 전체) 도구를 순차 설치하고 상태를 갱신한다.</summary>
|
||||
public partial class ToolSetupWindow : Wpf.Ui.Controls.FluentWindow
|
||||
{
|
||||
private readonly ToolSetupViewModel _vm;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public ToolSetupWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = new ToolSetupViewModel();
|
||||
DataContext = _vm;
|
||||
}
|
||||
|
||||
private async void OnInstall(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_vm.IsInstalling) return;
|
||||
_cts = new CancellationTokenSource();
|
||||
InstallButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
await _vm.InstallSelectedAsync(_cts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
InstallButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLater(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
Close();
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue