diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5ecc2b0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +permissions: + contents: read + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore + Build solution + shell: pwsh + run: | + dotnet build Everything2Everything.slnx -c Release --nologo + + - name: Build MSIX + shell: pwsh + run: | + ./packaging/GenerateAssets.ps1 + ./packaging/BuildMsix.ps1 -Configuration Release -Platform x64 + + - name: Upload MSIX artifact + uses: actions/upload-artifact@v4 + with: + name: Everything2Everything-x64-msix-${{ github.sha }} + path: packaging/dist/Everything2Everything-x64.msix + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f4fdbad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,98 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: '릴리즈 태그 (예: v0.1.0)' + required: true + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Generate placeholder logos + shell: pwsh + run: ./packaging/GenerateAssets.ps1 + + - name: Decode PFX (if secret provided) + id: pfx + shell: pwsh + env: + PFX_BASE64: ${{ secrets.PFX_BASE64 }} + run: | + if ($env:PFX_BASE64) { + $bytes = [Convert]::FromBase64String($env:PFX_BASE64) + $path = Join-Path $env:RUNNER_TEMP 'cert.pfx' + [IO.File]::WriteAllBytes($path, $bytes) + "pfx_path=$path" >> $env:GITHUB_OUTPUT + Write-Host '✅ PFX 디코딩 완료 — 서명 모드로 빌드' + } else { + Write-Host 'ℹ PFX_BASE64 secret 없음 — unsigned 모드로 빌드' + } + + - name: Build MSIX (signed if PFX, else unsigned) + shell: pwsh + env: + PFX_PASSWORD: ${{ secrets.PFX_PASSWORD }} + run: | + $pfx = '${{ steps.pfx.outputs.pfx_path }}' + if ($pfx) { + $sec = ConvertTo-SecureString -String $env:PFX_PASSWORD -AsPlainText -Force + ./packaging/BuildMsix.ps1 -Configuration Release -Platform x64 -Sign -PfxPath $pfx -PfxPassword $sec + } else { + ./packaging/BuildMsix.ps1 -Configuration Release -Platform x64 + } + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: Everything2Everything-x64-msix + path: packaging/dist/Everything2Everything-x64.msix + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + files: packaging/dist/Everything2Everything-x64.msix + generate_release_notes: true + body: | + ## Everything2Everything + + 모든 파일을 우클릭 한 번으로 JPEG 로 변환합니다. + + ### 설치 방법 + 1. 아래 `Everything2Everything-x64.msix` 다운로드. + 2. 관리자 PowerShell: + ```powershell + # 자체 서명 인증서를 신뢰 저장소에 등록 (PFX 별도 보유 필요) + Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\TrustedPeople ` + -FilePath .\Everything2Everything-DevCert.pfx ` + -Password (ConvertTo-SecureString 'Everything2EverythingDev' -AsPlainText -Force) + + # MSIX 사이드로드 + Add-AppxPackage -Path .\Everything2Everything-x64.msix -ForceApplicationShutdown + ``` + 3. PNG/HEIC/PDF 등 파일 우클릭 → "JPEG로 빠른 변환" 또는 "JPEG로 변환…" + + > 이 패키지는 자체 서명입니다. 첫 PC 1회만 인증서 등록이 필요합니다. + + ### 변경사항 diff --git a/.gitignore b/.gitignore index 124dfc0..8a9fc76 100644 --- a/.gitignore +++ b/.gitignore @@ -47,9 +47,6 @@ src/Everything2Everything.Shell/Everythi*/ # .NET artifacts dir from BuildMsix.ps1 artifacts/ -# test assets -test_assets/ - # NuGet packages restore + nuget.exe cache packages/ tools/* diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest index 0aa68c7..e43af0e 100644 --- a/packaging/Package.appxmanifest +++ b/packaging/Package.appxmanifest @@ -1,6 +1,6 @@ - + Everything2Everything YunChan diff --git a/src/Everything2Everything.App/App.xaml.cs b/src/Everything2Everything.App/App.xaml.cs index 83bb4e0..44b3cba 100644 --- a/src/Everything2Everything.App/App.xaml.cs +++ b/src/Everything2Everything.App/App.xaml.cs @@ -76,27 +76,15 @@ public partial class App : Application private void ShowMainWindow() { - RunFirstRunSetupIfNeeded(); - var adService = _services.GetService(); - var window = new MainWindow(Engine, Settings, null, adService); + var window = new MainWindow(Engine, Settings); MainWindow = window; window.Show(); window.Activate(); } - /// 첫 실행이면 외부 도구 설치 마법사를 한 번 띄우고 완료 플래그를 남긴다(설정 창에서 재진입 가능). - 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 files) { - var adService = _services.GetService(); - var window = new Views.MainWindow(Engine, Settings, files, adService); + var window = new Views.MainWindow(Engine, Settings, files); MainWindow = window; window.Show(); window.Activate(); diff --git a/src/Everything2Everything.App/Everything2Everything.App.csproj b/src/Everything2Everything.App/Everything2Everything.App.csproj index 69062d0..48e53ff 100644 --- a/src/Everything2Everything.App/Everything2Everything.App.csproj +++ b/src/Everything2Everything.App/Everything2Everything.App.csproj @@ -1,7 +1,7 @@ - 1.0.18 + 1.0.11 WinExe net9.0-windows10.0.19041.0 enable diff --git a/src/Everything2Everything.App/ViewModels/AdViewModel.cs b/src/Everything2Everything.App/ViewModels/AdViewModel.cs deleted file mode 100644 index b5f1b9c..0000000 --- a/src/Everything2Everything.App/ViewModels/AdViewModel.cs +++ /dev/null @@ -1,106 +0,0 @@ -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 _execute; - private readonly Func? _canExecute; - - public AdCommand(Action execute, Func? 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 { } - } - } -} diff --git a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs index 9d3e5bf..5c2da02 100644 --- a/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs +++ b/src/Everything2Everything.App/ViewModels/OptionsViewModel.cs @@ -66,13 +66,6 @@ public partial class OptionsViewModel : ObservableObject [ObservableProperty] private int _channelsIndex; // 0=원본,1=모노,2=스테레오 [ObservableProperty] private bool _loudnorm; - // ── 상세 인코딩 옵션 (폴드아웃 Expander 상태 및 전문 규격) ──────────────────────────────── - [ObservableProperty] private bool _isAdvancedExpanded = true; // 사용자 피드백: 슬라이더/옵션을 바로 볼 수 있도록 기본 전개 - [ObservableProperty] private int _pdfCompressLevelIndex; // 0=Light, 1=Strong, 2=Max - [ObservableProperty] private int _pdfDpiIndex = 1; // 0=150, 1=200, 2=300 - [ObservableProperty] private bool _imageLossless; // WebP/PNG 무손실 - [ObservableProperty] private bool _progressive; // JPEG 프로그레시브 웹 로딩 - /// 현재 상태로 불변 ConvertOptions를 구성한다(기존 MainWindow.BuildOptions와 동일 동작 + 영상/오디오). public ConvertOptions ToConvertOptions() { @@ -84,23 +77,15 @@ public partial class OptionsViewModel : ObservableObject _ => "summarize", }; - var dpi = PdfDpiIndex switch - { - 0 => 150, - 2 => 300, - _ => 200, - }; - return new ConvertOptions { OnCollision = ConflictRule, OutputLocation = hasCustom ? OutputLocation.Custom : OutputLocation.SubfolderBesideSource, CustomOutputDirectory = hasCustom ? CustomOutputDirectory!.Trim() : null, KeepExifWhenPossible = !StripMetadata, - Jpeg = new JpegEncodingOptions { Quality = Quality, Progressive = Progressive }, - Webp = new WebpEncodingOptions { Quality = Quality, Lossless = ImageLossless }, + Jpeg = new JpegEncodingOptions { Quality = Quality }, + Webp = new WebpEncodingOptions { Quality = Quality }, Avif = new AvifEncodingOptions { Quality = Math.Clamp(Quality - 30, 1, 100) }, - PdfRender = new PdfRenderOptions { Dpi = dpi }, Ai = new AiOptions { Task = aiTask, diff --git a/src/Everything2Everything.App/ViewModels/ToolSetupViewModel.cs b/src/Everything2Everything.App/ViewModels/ToolSetupViewModel.cs deleted file mode 100644 index 40dea61..0000000 --- a/src/Everything2Everything.App/ViewModels/ToolSetupViewModel.cs +++ /dev/null @@ -1,185 +0,0 @@ -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, -} - -/// 설치 마법사 목록의 한 행. 카탈로그 정의 + 선택/상태/진행 표현. -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 _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)); -} - -/// 설치 마법사의 루트 뷰모델: 목록 + 전체 선택 + 선택분 순차 설치. -public sealed class ToolSetupViewModel : INotifyPropertyChanged -{ - private readonly ExternalToolInstaller _installer; - private bool _isInstalling; - private bool _selectAll = true; - - public ObservableCollection Tools { get; } = new(); - - public ToolSetupViewModel(IEnumerable? 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 - { - // 이미 설치된 도구는 재설치하지 않고 건너뛴다(감지 상태가 Installed인 항목 제외). - foreach (var item in Tools.Where(t => t.IsSelected && t.State != ToolInstallState.Installed).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)); -} diff --git a/src/Everything2Everything.App/Views/AdBannerControl.xaml b/src/Everything2Everything.App/Views/AdBannerControl.xaml deleted file mode 100644 index 19d5b9c..0000000 --- a/src/Everything2Everything.App/Views/AdBannerControl.xaml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Everything2Everything.App/Views/AdBannerControl.xaml.cs b/src/Everything2Everything.App/Views/AdBannerControl.xaml.cs deleted file mode 100644 index 8791a0f..0000000 --- a/src/Everything2Everything.App/Views/AdBannerControl.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Windows.Controls; - -namespace Everything2Everything.App.Views; - -public partial class AdBannerControl : UserControl -{ - public AdBannerControl() - { - InitializeComponent(); - } -} diff --git a/src/Everything2Everything.App/Views/AdLargeCardControl.xaml b/src/Everything2Everything.App/Views/AdLargeCardControl.xaml deleted file mode 100644 index 63fce7e..0000000 --- a/src/Everything2Everything.App/Views/AdLargeCardControl.xaml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Everything2Everything.App/Views/AdLargeCardControl.xaml.cs b/src/Everything2Everything.App/Views/AdLargeCardControl.xaml.cs deleted file mode 100644 index 9cd71c5..0000000 --- a/src/Everything2Everything.App/Views/AdLargeCardControl.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Windows.Controls; - -namespace Everything2Everything.App.Views; - -public partial class AdLargeCardControl : UserControl -{ - public AdLargeCardControl() - { - InitializeComponent(); - } -} diff --git a/src/Everything2Everything.App/Views/CategoryGlyphs.cs b/src/Everything2Everything.App/Views/CategoryGlyphs.cs index 658ae97..2532e89 100644 --- a/src/Everything2Everything.App/Views/CategoryGlyphs.cs +++ b/src/Everything2Everything.App/Views/CategoryGlyphs.cs @@ -50,7 +50,7 @@ public static class CategoryGlyphs public static ImageSource ForCategory(string category) { if (Cache.TryGetValue(category, out var cached)) return cached; - var uri = new Uri($"pack://application:,,,/Everything2Everything;component/Assets/glyph-{category}.png", UriKind.Absolute); + var uri = new Uri($"pack://application:,,,/Assets/glyph-{category}.png", UriKind.Absolute); var img = new BitmapImage(); img.BeginInit(); img.CacheOption = BitmapCacheOption.OnLoad; diff --git a/src/Everything2Everything.App/Views/DiagnoseWindow.xaml b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml index 848d52a..3e10c7a 100644 --- a/src/Everything2Everything.App/Views/DiagnoseWindow.xaml +++ b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml @@ -12,7 +12,6 @@ - diff --git a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml index adce8d2..41ada10 100644 --- a/src/Everything2Everything.App/Views/FormatShiftTheme.xaml +++ b/src/Everything2Everything.App/Views/FormatShiftTheme.xaml @@ -521,100 +521,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Padding="32,6,10,6" Text="{Binding SearchText, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}" + ToolTip="파일명 또는 확장자로 검색"/> + - - - - - - - - - - - - + + + + + + @@ -773,13 +724,8 @@ - - - - - - - + + @@ -798,28 +744,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + @@ -941,12 +849,12 @@ CornerRadius="10,0,0,10"/> - + - - - + + + @@ -972,8 +880,7 @@ + VerticalAlignment="Center"/> - + + VerticalAlignment="Center"/> - - - - - + - - - - - - - - - - - - - - + + + + + + + + + + - + - - - - + + + + + VerticalAlignment="Center"/> @@ -1230,17 +1083,13 @@ - - - - - + @@ -1251,8 +1100,8 @@ - - + @@ -1261,7 +1110,7 @@ Stroke="{StaticResource FsTextSecondary}" StrokeThickness="2" VerticalAlignment="Center" Data="M1,12 C1,12 5,4 12,4 C19,4 23,12 23,12 C23,12 19,20 12,20 C5,20 1,12 1,12 Z M12,9 A3,3 0 1,1 12,15 A3,3 0 1,1 12,9 Z"/> - @@ -1275,62 +1124,58 @@ - + - - - - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - + + + + + + + @@ -1339,60 +1184,6 @@ BorderBrush="{StaticResource FsBorderSubtle}" BorderThickness="0,1,0,0"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1486,30 +1269,16 @@ - - - - - - - - - - - + Background="#CC090A0C" IsHitTestVisible="False"> + + + + diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs index 0805fde..0b8d28c 100644 --- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs +++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs @@ -12,37 +12,14 @@ 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; namespace Everything2Everything.App.Views; -public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyChanged +public partial class MainWindow : Wpf.Ui.Controls.FluentWindow { - public event PropertyChangedEventHandler? PropertyChanged; - private void OnPropertyChanged([CallerMemberName] string? name = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - - private bool _hasSelectedItem; - public bool HasSelectedItem - { - get => _hasSelectedItem; - set - { - if (_hasSelectedItem != value) - { - _hasSelectedItem = value; - OnPropertyChanged(); - } - } - } - - public ICommand ClearSearchCommand { get; } - public ICommand SelectAllQueueCommand => BatchSelectAllCommand; - public ICommand DeleteSelectedQueueCommand => BatchRemoveSelectedCommand; - private readonly ConversionEngine _engine; private readonly ISettingsStore _settings; private readonly OptionsViewModel _options = new(); @@ -135,22 +112,15 @@ 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 AdViewModel AdVm { get; } - - public MainWindow(ConversionEngine engine, ISettingsStore settings, IReadOnlyList? initialFiles = null, IAdService? adService = null) + public MainWindow(ConversionEngine engine, ISettingsStore settings, IReadOnlyList? initialFiles = null) { _engine = engine; _settings = settings; - AdVm = new AdViewModel(adService ?? new AdService()); AddFilesCommand = new RelayCommand(_ => PickAndAddFiles()); ProcessQueueCommand = new RelayCommand(_ => OnProcessQueueClick(this, new RoutedEventArgs()), @@ -175,7 +145,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC ToggleInspectorCommand = new RelayCommand(_ => ToggleInspector()); RemoveQueueItemCommand = new RelayCommand(p => RemoveQueueItem(p as QueueItem)); OpenFolderCommand = new RelayCommand(p => OpenFolderForPath(p as string)); - TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Active")); + TabCommand = new RelayCommand(p => ShowTab(p as string ?? "Past")); ConflictRuleCommand = new RelayCommand(p => SetConflictRule(p as string)); CombineToggleCommand = new RelayCommand(_ => UpdateCombineState(SelectedOutputExtension)); OutputFormatChangedCommand = new RelayCommand(_ => OnOutputFormatSelected()); @@ -183,34 +153,15 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC DragLeaveCommand = new RelayCommand(_ => DropHintOverlay.Visibility = Visibility.Collapsed); DropCommand = new RelayCommand(p => HandleFilesDropped(p as DragEventArgs)); QueueRowCommand = new RelayCommand(p => HandleQueueRowClick(p as MouseButtonEventArgs)); - ClearSearchCommand = new RelayCommand(_ => - { - SearchText = ""; - if (SearchBox is not null) SearchBox.Text = ""; - }); PastRowCommand = new RelayCommand(p => HandlePastRowClick(p as MouseButtonEventArgs)); InitializeComponent(); - Title = "Everything2Everything"; - if (AppTitleBar is not null) AppTitleBar.Title = "Everything2Everything"; - - if (AdvancedOptionsExpander is not null) - { - AdvancedOptionsExpander.IsExpanded = true; - AdvancedOptionsExpander.Expanded += (_, _) => _options.IsAdvancedExpanded = true; - AdvancedOptionsExpander.Collapsed += (_, _) => _options.IsAdvancedExpanded = false; - } if (SmartPresetCombo is not null) { SmartPresetCombo.SelectionChanged += OnSmartPresetChanged; } - if (OutputFormatCombo is not null) - { - OutputFormatCombo.SelectionChanged += (_, _) => OnOutputFormatSelected(); - } - // ActiveQueueList/PastResultsList의 ItemsSource는 XAML이 ActiveQueue/PastResults에 바인딩(선언적). InitializeOutputFormats(); @@ -251,7 +202,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC return; } - CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (진단 도구 참조)"; + CapabilityStatusText.Text = $"⚠ {notReady.Count}개 형식이 외부 도구를 기다립니다 (Diagnose 참조)"; CapabilityStatusText.Visibility = Visibility.Visible; } @@ -337,9 +288,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC foreach (var path in paths) { if (!File.Exists(path) || existing.Contains(path)) continue; - var item = QueueItem.FromPath(path); - item.PropertyChanged += OnQueueItemPropertyChanged; - _activeQueue.Add(item); + _activeQueue.Add(QueueItem.FromPath(path)); } UpdateBadges(); UpdateProcessQueueButton(); @@ -353,18 +302,9 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC } } - private void OnQueueItemPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(QueueItem.IsSelected)) - { - UpdateQueueSummary(); - } - } - private void RemoveQueueItem(QueueItem? item) { if (item is null) return; - item.PropertyChanged -= OnQueueItemPropertyChanged; _activeQueue.Remove(item); UpdateBadges(); UpdateProcessQueueButton(); @@ -396,40 +336,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC var count = _pastResults.Sum(g => g.Entries.Count); TabPastBadge.Text = count.ToString(CultureInfo.InvariantCulture); UpdatePastResultsVisibility(); - UpdateQueueSummary(); - UpdatePastResultsTelemetry(); - } - - private void UpdateQueueSummary() - { - if (QueueSummaryCountText is null || QueueSummarySizeText is null) return; - var count = _activeQueue.Count; - long totalBytes = _activeQueue.Sum(q => q.SourceSizeBytes); - QueueSummaryCountText.Text = $"총 {count}개 항목"; - QueueSummarySizeText.Text = HumanizeBytes(totalBytes); - - var selectedCount = _activeQueue.Count(q => q.IsSelected); - if (QueueSelectedBadge is not null && QueueSelectedText is not null) - { - if (selectedCount > 0) - { - QueueSelectedBadge.Visibility = Visibility.Visible; - QueueSelectedText.Text = $"{selectedCount}개 선택됨"; - } - else - { - QueueSelectedBadge.Visibility = Visibility.Collapsed; - } - } - } - - private void UpdatePastResultsTelemetry() - { - if (PastTotalCountText is null || PastTotalSavingsText is null) return; - var totalCount = _pastResults.Sum(g => g.Entries.Count); - var totalSavings = _pastResults.Sum(g => g.SessionSavingsBytes); - PastTotalCountText.Text = $"{totalCount}개 파일"; - PastTotalSavingsText.Text = HumanizeBytes(totalSavings); } private void UpdateProcessQueueButton() @@ -437,26 +343,22 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC var count = _activeQueue.Count; if (_cts is not null) { - ProcessQueueButton.Content = $"변환 처리 중… ({count}개)"; - ProcessQueueButton.ToolTip = "파일 변환이 진행 중입니다."; + ProcessQueueButton.Content = $"변환 처리 중… ({count}개 파일)"; ProcessQueueButton.IsEnabled = false; } else if (count == 0) { - ProcessQueueButton.Content = "파일을 드래그하여 추가"; - ProcessQueueButton.ToolTip = "대기열에 변환할 파일을 추가하세요 (단축키: Ctrl + O)"; + ProcessQueueButton.Content = "대기 중 — 파일을 드래그하여 추가하세요"; ProcessQueueButton.IsEnabled = false; } else if (string.IsNullOrEmpty(SelectedOutputExtension)) { - ProcessQueueButton.Content = "공통 형식 없음"; - ProcessQueueButton.ToolTip = "선택된 파일들 간에 호환 가능한 공통 출력 형식이 없습니다."; + ProcessQueueButton.Content = "변환 불가 (공통 형식 없음)"; ProcessQueueButton.IsEnabled = false; } else { - ProcessQueueButton.Content = $"변환 시작 ({count}개 파일)"; - ProcessQueueButton.ToolTip = $"대기열 일괄 변환 시작 ({count}개 파일) [단축키: Ctrl + Enter]"; + ProcessQueueButton.Content = $"대기열 일괄 변환 시작 ({count}개) [Ctrl + Enter]"; ProcessQueueButton.IsEnabled = true; } } @@ -603,44 +505,10 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC var info = FileInspectorBuilder.Build(filePath); PreviewFileName.Text = string.IsNullOrEmpty(fileName) ? info.FileName : fileName; PreviewFilePath.Text = string.IsNullOrEmpty(filePath) ? info.FullPath : filePath; - var fmt = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel; - var sz = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText; - PreviewFormatText.Text = fmt; - PreviewSizeText.Text = sz; + PreviewFormatText.Text = string.IsNullOrEmpty(formatLabel) || formatLabel == "—" ? info.Extension.TrimStart('.').ToUpperInvariant() : formatLabel; + PreviewSizeText.Text = string.IsNullOrEmpty(sizeText) || sizeText == "—" ? info.FormattedSize : sizeText; PreviewDimText.Text = info.DimensionsOrMeta; - PreviewPageText.Text = info.Category.ToKoreanLabel(); - UpdateConversionPipelineCard(fmt, sz); - HasSelectedItem = !string.IsNullOrEmpty(filePath); - } - - private void UpdateConversionPipelineCard(string? formatLabel, string? sizeText) - { - if (ConversionPipelineCard is null || PipelineSourceText is null || PipelineTargetText is null) return; - - if (string.IsNullOrEmpty(formatLabel) || formatLabel == "—") - { - PipelineSourceText.Text = "선택 대기"; - PipelineTargetText.Text = (SelectedOutputExtension ?? ".jpg").TrimStart('.').ToUpperInvariant(); - if (PipelineLossText is not null) PipelineLossText.Text = "파일을 선택하면 변환 정보 표시"; - if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "대기 중"; - return; - } - - PipelineSourceText.Text = $"{formatLabel} · {sizeText ?? "—"}"; - var targetExt = (SelectedOutputExtension ?? ".jpg").TrimStart('.').ToUpperInvariant(); - PipelineTargetText.Text = targetExt; - - var isSame = string.Equals(formatLabel, targetExt, StringComparison.OrdinalIgnoreCase); - if (isSame) - { - if (PipelineLossText is not null) PipelineLossText.Text = "무손실 재압축 및 메타 정돈"; - if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "최적화"; - } - else - { - if (PipelineLossText is not null) PipelineLossText.Text = $"{formatLabel} → {targetExt} 변환"; - if (PipelineEstimatedText is not null) PipelineEstimatedText.Text = "실시간 매칭"; - } + PreviewPageText.Text = info.Category.ToString(); } private void ShowPreviewLoading() @@ -836,18 +704,6 @@ 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(categoryName, true, out var cat)) @@ -877,7 +733,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC { bool select = parameter is true; BatchQueueService.SetSelectionAll(_activeQueue, select); - UpdateQueueSummary(); } private void BatchRemoveSelected() @@ -1188,9 +1043,10 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC private static string FormatDateLabel(DateOnly date) { var today = DateOnly.FromDateTime(DateTime.Today); - if (date == today) return $"오늘 ({date:M월 d일})"; - if (date == today.AddDays(-1)) return $"어제 ({date:M월 d일})"; - return date.ToString("yyyy년 M월 d일 (ddd)", CultureInfo.GetCultureInfo("ko-KR")); + var label = date == today ? "Today" + : date == today.AddDays(-1) ? "Yesterday" + : date.ToString("dddd", CultureInfo.GetCultureInfo("en-US")); + return $"{label}, {date:MMM d}"; } private void ApplyAppDataStats() @@ -1242,7 +1098,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC else if (TabPastBtn.IsChecked == true) { var confirm = MessageBox.Show(this, - "변환 기록 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.", + "Past Results 전체를 삭제하시겠습니까?\n영구 저장된 이력도 함께 삭제됩니다.", "Everything2Everything", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (confirm != MessageBoxResult.OK) return; @@ -1461,7 +1317,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC UpdateCombineState(keepExt); UpdateProcessQueueButton(); UpdateSmartPresetsForFormat(keepExt); - UpdateConversionPipelineCard(PreviewFormatText?.Text, PreviewSizeText?.Text); if (OutputFormatHint is not null) { @@ -1507,7 +1362,6 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC UpdateQualityPanelForFormat(ext); UpdateOutputDestHint(ext); UpdateSmartPresetsForFormat(ext); - UpdateConversionPipelineCard(PreviewFormatText?.Text, PreviewSizeText?.Text); } } @@ -1532,53 +1386,21 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow, INotifyPropertyC private void UpdateQualityPanelForFormat(string? extension) { + if (QualityPanel is null || QualityLabelText is null) return; var ext = extension?.ToLowerInvariant(); - var isImageQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif"; - var isVideo = ext is ".mp4" or ".mkv" or ".webm" or ".mov" or ".avi"; - var isAudio = ext is ".mp3" or ".aac" or ".m4a" or ".opus" or ".ogg" or ".flac" or ".wav"; - var isPdf = ext is ".pdf"; - - if (QualityPanel is not null) + var supportsQuality = ext is ".jpg" or ".jpeg" or ".webp" or ".avif"; + QualityPanel.Visibility = supportsQuality ? Visibility.Visible : Visibility.Collapsed; + QualityLabelText.Text = ext switch { - QualityPanel.Visibility = isImageQuality ? Visibility.Visible : Visibility.Collapsed; - if (QualityLabelText is not null) - { - QualityLabelText.Text = ext switch - { - ".jpg" or ".jpeg" => "JPEG 압축 품질", - ".webp" => "WebP 압축 품질", - ".avif" => "AVIF 압축 품질", - _ => "압축 품질", - }; - } - } - - if (VideoQuickPanel is not null) - VideoQuickPanel.Visibility = isVideo ? Visibility.Visible : Visibility.Collapsed; - if (AudioQuickPanel is not null) - AudioQuickPanel.Visibility = isAudio ? Visibility.Visible : Visibility.Collapsed; - 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; + ".jpg" or ".jpeg" => "JPEG QUALITY", + ".webp" => "WEBP QUALITY", + ".avif" => "AVIF QUALITY", + _ => "ENCODING QUALITY", + }; 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; - } - /// 출력이 영상/오디오/PDF/이미지일 때 상세 인코딩 폴드아웃 서브패널 노출(Fluent 2 점진적 공개 패턴). private void UpdateMediaPanelForFormat(string? extension) { @@ -1649,17 +1471,9 @@ public sealed class QueueItem : INotifyPropertyChanged public string StateText { get => _state; - set { _state = value; Raise(nameof(StateText)); Raise(nameof(DisplayStateText)); Raise(nameof(IsDone)); } + set { _state = value; Raise(nameof(StateText)); Raise(nameof(IsDone)); } } - /// 사용자에게 표시되는 정제된 한국어 상태 텍스트 (AGENTS.md Rule 3). - public string DisplayStateText => _state switch - { - "queued" => "대기 중", - "done" => "변환 완료", - _ => _state, - }; - public Brush StateBrush => _state switch { "queued" => (Application.Current?.TryFindResource("FsTextTertiary") as Brush) ?? Brushes.Gray, @@ -1733,7 +1547,7 @@ public sealed class DateGroup : INotifyPropertyChanged public string DateTitle { get; } public ObservableCollection Entries { get; } = new(); public long SessionSavingsBytes { get; set; } - public string SessionSavingsText => $"세션 절감: {MainWindow.HumanizeBytes(SessionSavingsBytes)}"; + public string SessionSavingsText => $"Session Savings: {MainWindow.HumanizeBytes(SessionSavingsBytes)}"; public DateGroup(string dateTitle) { DateTitle = dateTitle; } @@ -1777,7 +1591,7 @@ public sealed record HistoryRow( FormatLabel: label, FormatBrush: brush, FileName: Path.GetFileName(e.SourcePath), - MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount}개 파일", + MetaLine: $"{e.Timestamp:HH:mm:ss} • {e.OutputCount} output(s)", SizeText: MainWindow.HumanizeBytes(e.SourceSizeBytes), SavingsText: $"{arrow} {MainWindow.HumanizeBytes(Math.Abs(saved))}", SourcePath: e.SourcePath, diff --git a/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml b/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml index 25117b7..ba98883 100644 --- a/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml +++ b/src/Everything2Everything.App/Views/QuickOptionsWindow.xaml @@ -13,7 +13,6 @@ - @@ -74,20 +73,6 @@ - - - - - - - - - - -