feat(ui): AI 설정창 + Codex CLI OAuth 백엔드 + ⚙ 진입점
사용자 원래 요구였던 Codex non-interactive OAuth를 실제 동작까지 검증해 통합. ChatGPT 구독으로 API 키 없이 AI 변환. - CodexChatClient: codex exec --skip-git-repo-check --ephemeral -o <file> - (stdin 프롬프트). 실제 한글→영어 번역 검증 완료 - ExternalProcessRunner: stdin 지원 + UTF-8 인코딩(한글 깨짐 해결) - ExternalToolDetector.IsCodexAvailable() + public 승격 - LlmProvider: codex 백엔드 + auto 폴백(키 없으면 Codex) - SettingsWindow: OpenAI/Anthropic 키(2단계 Verify) + Codex(OAuth) + 외부도구 상태/다운로드 CTA + 모델 선택 - App.Settings를 LlmProvider와 공유(키 저장 즉시 반영) - MainWindow 네비바 ⚙ 설정 진입점 - 디자인: Cursor/Zed/Raycast 설정 UX + Radix 상태 토큰 영감
This commit is contained in:
parent
faffb8f205
commit
2a3d220db4
12 changed files with 480 additions and 8 deletions
|
|
@ -338,4 +338,41 @@
|
|||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- PasswordBox (API 키 입력) -->
|
||||
<Style x:Key="FsPasswordInputStyle" TargetType="PasswordBox">
|
||||
<Setter Property="Background" Value="{StaticResource FsBgInput}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource FsTextPrimary}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource FsBorderSubtle}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource FsFontMono}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource FsTextPrimary}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="PasswordBox">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource FsAccentBlue}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- 손실 등급 dot (8px 원) -->
|
||||
<Style x:Key="FsLossDotStyle" TargetType="Ellipse">
|
||||
<Setter Property="Width" Value="8"/>
|
||||
<Setter Property="Height" Value="8"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
|
|
|||
|
|
@ -334,6 +334,9 @@
|
|||
|
||||
<!-- Actions -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button Content="⚙ 설정" Margin="0,0,8,0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnSettingsClick"/>
|
||||
<Button Content="Register Menu" Margin="0,0,8,0"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Click="OnRegisterClick"/>
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
|
|||
CapabilityStatusText.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void OnSettingsClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var win = new SettingsWindow(((App)Application.Current).Settings) { Owner = this };
|
||||
win.ShowDialog();
|
||||
_ = RefreshCapabilityStatusAsync();
|
||||
RefreshAvailableOutputFormats();
|
||||
}
|
||||
|
||||
private void PickAndAddFiles()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
|
|
|
|||
162
src/Everything2Everything.App/Views/SettingsWindow.xaml
Normal file
162
src/Everything2Everything.App/Views/SettingsWindow.xaml
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<ui:FluentWindow x:Class="Everything2Everything.App.Views.SettingsWindow"
|
||||
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="560" Height="720"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ExtendsContentIntoTitleBar="True"
|
||||
WindowBackdropType="Mica"
|
||||
Background="{StaticResource FsBgBase}">
|
||||
|
||||
<Grid>
|
||||
<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>
|
||||
|
||||
<!-- ===== AI 카드 ===== -->
|
||||
<Border Background="{StaticResource FsBgSurface}"
|
||||
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="20" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="AI 텍스트 변환" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
||||
<TextBlock Text="요약 · 번역 · 교정에 사용됩니다 (종량 과금 · 네트워크 필요). 키가 없으면 AI 변환만 비활성됩니다."
|
||||
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,16"/>
|
||||
|
||||
<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="OpenAI (API 키)"/>
|
||||
<ComboBoxItem Content="Anthropic (API 키)"/>
|
||||
<ComboBoxItem Content="Codex CLI (ChatGPT 구독 OAuth)"/>
|
||||
</ComboBox>
|
||||
|
||||
<!-- OpenAI -->
|
||||
<TextBlock Text="OpenAI API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<PasswordBox x:Name="OpenAiKeyBox" Grid.Column="0"
|
||||
Style="{StaticResource FsPasswordInputStyle}"
|
||||
PasswordChanged="OnOpenAiKeyChanged"/>
|
||||
<Button x:Name="OpenAiVerifyBtn" Grid.Column="1" Content="확인"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
|
||||
IsEnabled="False" Click="OnVerifyOpenAi"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<Ellipse x:Name="OpenAiDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
|
||||
<TextBlock x:Name="OpenAiStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Anthropic -->
|
||||
<TextBlock Text="Anthropic API Key" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<PasswordBox x:Name="AnthropicKeyBox" Grid.Column="0"
|
||||
Style="{StaticResource FsPasswordInputStyle}"
|
||||
PasswordChanged="OnAnthropicKeyChanged"/>
|
||||
<Button x:Name="AnthropicVerifyBtn" Grid.Column="1" Content="확인"
|
||||
Style="{StaticResource FsSecondaryButtonStyle}" Margin="8,0,0,0"
|
||||
IsEnabled="False" Click="OnVerifyAnthropic"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<Ellipse x:Name="AnthropicDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
|
||||
<TextBlock x:Name="AnthropicStatus" Text="키 미설정" Style="{StaticResource FsCaptionStyle}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Codex CLI (OAuth) -->
|
||||
<TextBlock Text="Codex CLI (OAuth · API 키 불필요)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
|
||||
<Ellipse x:Name="CodexDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsTextTertiary}" Margin="0,0,7,0"/>
|
||||
<TextBlock x:Name="CodexStatus" Text="확인 중…" Style="{StaticResource FsCaptionStyle}" VerticalAlignment="Center"/>
|
||||
<Button x:Name="CodexVerifyBtn" Content="테스트" Style="{StaticResource FsSecondaryButtonStyle}"
|
||||
Margin="12,0,0,0" Padding="10,4" IsEnabled="False" Click="OnVerifyCodex"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 모델 -->
|
||||
<TextBlock Text="모델 (선택)" Style="{StaticResource FsLabelStyle}" Margin="0,0,0,6"/>
|
||||
<TextBox x:Name="ModelBox" Style="{StaticResource FsPathInputStyle}"/>
|
||||
<TextBlock Text="비워두면 기본 모델 (OpenAI: gpt-4o-mini, Anthropic: claude-3-5-sonnet-latest)"
|
||||
Style="{StaticResource FsCaptionStyle}" Margin="0,6,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ===== 외부 도구 카드 ===== -->
|
||||
<Border Background="{StaticResource FsBgSurface}"
|
||||
BorderBrush="{StaticResource FsBorderHairline}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="20">
|
||||
<StackPanel>
|
||||
<TextBlock Text="외부 도구" Style="{StaticResource FsBodyStyle}" FontWeight="SemiBold" FontSize="15"/>
|
||||
<TextBlock Text="설치하면 영상/오디오·한글/Word 변환이 자동 활성화됩니다."
|
||||
Style="{StaticResource FsCaptionStyle}" TextWrapping="Wrap" Margin="0,4,0,16"/>
|
||||
|
||||
<!-- FFmpeg -->
|
||||
<Grid Margin="0,0,0,14">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Ellipse x:Name="FfmpegDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0"/>
|
||||
<TextBlock Text="FFmpeg" Style="{StaticResource FsBodyStyle}" FontWeight="Medium"/>
|
||||
<TextBlock x:Name="FfmpegStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="영상/오디오 변환 (mp4·webm·mp3·flac…)" Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadFfmpeg"/>
|
||||
<Button Content="폴더 열기" Style="{StaticResource FsSecondaryButtonStyle}" Margin="6,0,0,0" Click="OnOpenFfmpegFolder"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- LibreOffice -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Ellipse x:Name="LibreDot" Style="{StaticResource FsLossDotStyle}"
|
||||
Fill="{StaticResource FsStatusWarn}" Margin="0,0,7,0"/>
|
||||
<TextBlock Text="LibreOffice" Style="{StaticResource FsBodyStyle}" FontWeight="Medium"/>
|
||||
<TextBlock x:Name="LibreStatus" Text="미설치" Style="{StaticResource FsCaptionStyle}" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="한글/Word/문서 변환 (HWP·DOCX→PDF/이미지). 한글은 H2Orestart 확장 필요." Style="{StaticResource FsCaptionStyle}" Margin="15,3,0,0" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button Content="다운로드" Style="{StaticResource FsSecondaryButtonStyle}" Click="OnDownloadLibre"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ===== 푸터 ===== -->
|
||||
<Border Grid.Row="2" Background="{StaticResource FsBgPanel}"
|
||||
BorderBrush="{StaticResource FsBorderSubtle}" BorderThickness="0,1,0,0" Padding="24,14">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="닫기" Style="{StaticResource FsSecondaryButtonStyle}" Padding="18,8" Click="OnClose"/>
|
||||
<Button Content="저장" Style="{StaticResource FsPrimaryButtonStyle}" Padding="22,8" Margin="8,0,0,0" Click="OnSave"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:FluentWindow>
|
||||
157
src/Everything2Everything.App/Views/SettingsWindow.xaml.cs
Normal file
157
src/Everything2Everything.App/Views/SettingsWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Everything2Everything.Core;
|
||||
using Everything2Everything.Core.Converters;
|
||||
|
||||
namespace Everything2Everything.App.Views;
|
||||
|
||||
public partial class SettingsWindow : Wpf.Ui.Controls.FluentWindow
|
||||
{
|
||||
private readonly ISettingsStore _settings;
|
||||
|
||||
public SettingsWindow(ISettingsStore settings)
|
||||
{
|
||||
_settings = settings;
|
||||
InitializeComponent();
|
||||
LoadSettings();
|
||||
RefreshToolStatus();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
var backend = (_settings.Get("ai.backend") ?? "auto").ToLowerInvariant();
|
||||
BackendCombo.SelectedIndex = backend switch
|
||||
{
|
||||
"openai" => 1,
|
||||
"anthropic" => 2,
|
||||
"codex" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
ModelBox.Text = _settings.Get("ai.model") ?? string.Empty;
|
||||
|
||||
SetKeyStatus(OpenAiDot, OpenAiStatus, _settings.Contains("openai.apikey"), HasEnv("OPENAI_API_KEY"));
|
||||
SetKeyStatus(AnthropicDot, AnthropicStatus, _settings.Contains("anthropic.apikey"), HasEnv("ANTHROPIC_API_KEY"));
|
||||
}
|
||||
|
||||
private static bool HasEnv(string name) => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(name));
|
||||
|
||||
private void SetKeyStatus(Ellipse dot, TextBlock text, bool stored, bool env)
|
||||
{
|
||||
if (stored) { dot.Fill = Res("FsStatusSuccess"); text.Text = "저장됨 (변경하려면 새 키 입력)"; }
|
||||
else if (env) { dot.Fill = Res("FsStatusInfo"); text.Text = "환경변수에서 감지됨"; }
|
||||
else { dot.Fill = Res("FsTextTertiary"); text.Text = "키 미설정"; }
|
||||
}
|
||||
|
||||
private static Brush Res(string key) => (Brush)Application.Current.Resources[key];
|
||||
|
||||
// --- 2단계 Verify: 형식 검사로 버튼 활성화 → 클릭 시에만 실제 핑 ---
|
||||
|
||||
private void OnOpenAiKeyChanged(object sender, RoutedEventArgs e)
|
||||
=> OpenAiVerifyBtn.IsEnabled = OpenAiKeyBox.Password.StartsWith("sk-", StringComparison.Ordinal);
|
||||
|
||||
private void OnAnthropicKeyChanged(object sender, RoutedEventArgs e)
|
||||
=> AnthropicVerifyBtn.IsEnabled = AnthropicKeyBox.Password.StartsWith("sk-ant-", StringComparison.Ordinal);
|
||||
|
||||
private async void OnVerifyOpenAi(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(OpenAiDot, OpenAiStatus, OpenAiVerifyBtn,
|
||||
new OpenAiChatClient(OpenAiKeyBox.Password), ModelOr("gpt-4o-mini"));
|
||||
|
||||
private async void OnVerifyAnthropic(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(AnthropicDot, AnthropicStatus, AnthropicVerifyBtn,
|
||||
new AnthropicChatClient(AnthropicKeyBox.Password), ModelOr("claude-3-5-sonnet-latest"));
|
||||
|
||||
private async void OnVerifyCodex(object sender, RoutedEventArgs e)
|
||||
=> await VerifyAsync(CodexDot, CodexStatus, CodexVerifyBtn, new CodexChatClient(), ModelOr(string.Empty));
|
||||
|
||||
private string ModelOr(string fallback)
|
||||
=> string.IsNullOrWhiteSpace(ModelBox.Text) ? fallback : ModelBox.Text.Trim();
|
||||
|
||||
private async Task VerifyAsync(Ellipse dot, TextBlock text, Button btn, IChatClient client, string model)
|
||||
{
|
||||
btn.IsEnabled = false;
|
||||
dot.Fill = Res("FsStatusInfo");
|
||||
text.Text = "확인 중…";
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
||||
var reply = await client.CompleteAsync("Reply with exactly: OK", "ping", model, 8, cts.Token);
|
||||
dot.Fill = Res("FsStatusSuccess");
|
||||
text.Text = string.IsNullOrWhiteSpace(reply) ? $"확인됨 ({client.Name})" : $"확인됨 — {client.Name}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dot.Fill = Res("FsStatusDanger");
|
||||
text.Text = "실패: " + Trunc(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
btn.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Trunc(string s) => s.Length > 64 ? s[..64] + "…" : s;
|
||||
|
||||
private void RefreshToolStatus()
|
||||
{
|
||||
var ffmpeg = ExternalToolDetector.TryFindFfmpeg(out _);
|
||||
FfmpegDot.Fill = Res(ffmpeg ? "FsStatusSuccess" : "FsStatusWarn");
|
||||
FfmpegStatus.Text = ffmpeg ? "준비됨" : "미설치";
|
||||
|
||||
var libre = ExternalToolDetector.TryFindLibreOfficeSoffice(out _);
|
||||
LibreDot.Fill = Res(libre ? "FsStatusSuccess" : "FsStatusWarn");
|
||||
LibreStatus.Text = libre ? "준비됨" : "미설치";
|
||||
|
||||
var codex = ExternalToolDetector.IsCodexAvailable();
|
||||
CodexDot.Fill = Res(codex ? "FsStatusSuccess" : "FsTextTertiary");
|
||||
CodexStatus.Text = codex ? "설치됨 — 키 없이 사용 가능" : "미설치 (npm i -g @openai/codex)";
|
||||
CodexVerifyBtn.IsEnabled = codex;
|
||||
}
|
||||
|
||||
private void OnSave(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var backend = BackendCombo.SelectedIndex switch
|
||||
{
|
||||
1 => "openai",
|
||||
2 => "anthropic",
|
||||
3 => "codex",
|
||||
_ => "auto",
|
||||
};
|
||||
_settings.Set("ai.backend", backend);
|
||||
|
||||
var model = ModelBox.Text?.Trim();
|
||||
if (string.IsNullOrEmpty(model)) _settings.Remove("ai.model");
|
||||
else _settings.Set("ai.model", model);
|
||||
|
||||
if (OpenAiKeyBox.Password.Length > 0) _settings.Set("openai.apikey", OpenAiKeyBox.Password);
|
||||
if (AnthropicKeyBox.Password.Length > 0) _settings.Set("anthropic.apikey", AnthropicKeyBox.Password);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnDownloadFfmpeg(object sender, RoutedEventArgs e)
|
||||
=> OpenUrl("https://github.com/BtbN/FFmpeg-Builds/releases");
|
||||
|
||||
private void OnDownloadLibre(object sender, RoutedEventArgs e)
|
||||
=> OpenUrl("https://www.libreoffice.org/download/");
|
||||
|
||||
private void OnOpenFfmpegFolder(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dir = System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Everything2Everything", "ffmpeg");
|
||||
Directory.CreateDirectory(dir);
|
||||
OpenUrl(dir);
|
||||
}
|
||||
|
||||
private static void OpenUrl(string target)
|
||||
{
|
||||
try { Process.Start(new ProcessStartInfo(target) { UseShellExecute = true }); }
|
||||
catch { /* 브라우저/탐색기 실행 실패 무시 */ }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue