diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 899fe66..5ecc2b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Restore + Build solution shell: pwsh run: | - dotnet build EverythingToJpeg.slnx -c Release --nologo + dotnet build Everything2Everything.slnx -c Release --nologo - name: Build MSIX shell: pwsh @@ -38,6 +38,6 @@ jobs: - name: Upload MSIX artifact uses: actions/upload-artifact@v4 with: - name: EverythingToJpeg-x64-msix-${{ github.sha }} - path: packaging/dist/EverythingToJpeg-x64.msix + 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 index 3066cb1..f4fdbad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,32 +64,32 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: EverythingToJpeg-x64-msix - path: packaging/dist/EverythingToJpeg-x64.msix + 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/EverythingToJpeg-x64.msix + files: packaging/dist/Everything2Everything-x64.msix generate_release_notes: true body: | - ## EverythingToJpeg + ## Everything2Everything 모든 파일을 우클릭 한 번으로 JPEG 로 변환합니다. ### 설치 방법 - 1. 아래 `EverythingToJpeg-x64.msix` 다운로드. + 1. 아래 `Everything2Everything-x64.msix` 다운로드. 2. 관리자 PowerShell: ```powershell # 자체 서명 인증서를 신뢰 저장소에 등록 (PFX 별도 보유 필요) Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\TrustedPeople ` - -FilePath .\EverythingToJpeg-DevCert.pfx ` - -Password (ConvertTo-SecureString 'EverythingToJpegDev' -AsPlainText -Force) + -FilePath .\Everything2Everything-DevCert.pfx ` + -Password (ConvertTo-SecureString 'Everything2EverythingDev' -AsPlainText -Force) # MSIX 사이드로드 - Add-AppxPackage -Path .\EverythingToJpeg-x64.msix -ForceApplicationShutdown + Add-AppxPackage -Path .\Everything2Everything-x64.msix -ForceApplicationShutdown ``` 3. PNG/HEIC/PDF 등 파일 우클릭 → "JPEG로 빠른 변환" 또는 "JPEG로 변환…" diff --git a/.gitignore b/.gitignore index 5221139..09eb868 100644 --- a/.gitignore +++ b/.gitignore @@ -30,12 +30,12 @@ packaging/Layout/ packaging/dist/ # C++ project intermediate -src/EverythingToJpeg.Shell/x64/ -src/EverythingToJpeg.Shell/Win32/ -src/EverythingToJpeg.Shell/Debug/ -src/EverythingToJpeg.Shell/Release/ -src/EverythingToJpeg.Shell/.vs/ -src/EverythingToJpeg.Shell/Everythi*/ +src/Everything2Everything.Shell/x64/ +src/Everything2Everything.Shell/Win32/ +src/Everything2Everything.Shell/Debug/ +src/Everything2Everything.Shell/Release/ +src/Everything2Everything.Shell/.vs/ +src/Everything2Everything.Shell/Everythi*/ *.tlog *.obj *.pch diff --git a/Everything2Everything.slnx b/Everything2Everything.slnx new file mode 100644 index 0000000..1224e0b --- /dev/null +++ b/Everything2Everything.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/EverythingToJpeg.slnx b/EverythingToJpeg.slnx deleted file mode 100644 index b50fab5..0000000 --- a/EverythingToJpeg.slnx +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/README.md b/README.md index 4c9d5f0..d6a68a6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,27 @@ -# EverythingToJpeg +# Everything2Everything -Windows 우클릭 컨텍스트 메뉴에서 한 방에 JPEG로. PNG · GIF · BMP · TIFF · WebP · AVIF · HEIC · RAW · PSD · PDF · DOCX 를 지원합니다. +Windows 우클릭 한 방에 **모든 것을 모든 것으로** 양방향 변환. 이미지 ↔ 이미지, 이미지 ↔ PDF, 문서 ↔ PDF/이미지, HTML → PDF/이미지를 통합 매트릭스로 처리합니다. -- **빠른 변환** — 다이얼로그 없이 원본 폴더의 `<원본명>_jpeg/` 하위에 즉시 저장 -- **변환…** — 옵션 다이얼로그(품질, 출력 위치, 이름 충돌, 크기 제한, PDF DPI) -- 진행 상황 + 썸네일 + 드래그 & 드롭 (메인 창) +> 이 프로젝트는 단방향 *EverythingToJpeg* 에서 양방향 *Everything2Everything* 으로 피보팅된 결과물입니다. + +- **카스케이드 컨텍스트 메뉴** — 입력 파일에 따라 가능한 출력 형식만 자동 노출 +- **양방향 이미지 매트릭스** — PNG ↔ JPEG ↔ WebP ↔ AVIF ↔ TIFF ↔ BMP ↔ GIF +- **HTML → PDF**, **DOCX/HWPX ↔ PDF** 같은 문서 변환 포함 +- 출력 형식별 인코딩 옵션(JPEG quality, WebP lossless, AVIF speed 등) 분리 +- 진행 상황 + 썸네일 + 드래그 & 드롭 메인 창 + +## 변환 매트릭스 + +| Provider | 입력 | 출력 | +|---|---|---| +| **MagickProvider** | PNG · JPEG · WebP · AVIF · BMP · TIFF · GIF · PSD · RAW (NEF/CR2/CR3/ARW/DNG/RAF/ORF/RW2/SRW/PEF) | PNG · JPEG · WebP · AVIF · BMP · TIFF · GIF | +| **HeicProvider** | HEIC · HEIF | PNG · JPEG · WebP · AVIF · BMP · TIFF · GIF | +| **PdfProvider** | PDF | PNG · JPEG · WebP · AVIF · BMP · TIFF | +| **HtmlProvider** | HTML · HTM | PNG · JPEG · WebP · AVIF · BMP · TIFF · **PDF** | +| **DocxProvider** | DOCX · DOC | **PDF** + 이미지 7종 | +| **HwpxProvider** | HWP · HWPX | **PDF** + 이미지 7종 | + +총 200+ 변환 쌍 지원. 알파 채널은 출력 포맷이 지원할 때 자동 보존, 미지원이면 흰색(또는 설정된 배경색)으로 평탄화합니다. ## 빠른 시작 @@ -12,65 +29,100 @@ Windows 우클릭 컨텍스트 메뉴에서 한 방에 JPEG로. PNG · GIF · BM ```powershell # 솔루션 빌드 -dotnet build EverythingToJpeg.slnx -c Release +dotnet build Everything2Everything.slnx -c Release # 단일 폴더 publish (framework-dependent, .NET 9 Desktop Runtime 필요) -dotnet publish src\EverythingToJpeg.App\EverythingToJpeg.App.csproj ` +dotnet publish src\Everything2Everything.App\Everything2Everything.App.csproj ` -c Release -r win-x64 --self-contained false -o publish # .NET 런타임 동봉 (단일 사용자 배포가 편함) -dotnet publish src\EverythingToJpeg.App\EverythingToJpeg.App.csproj ` +dotnet publish src\Everything2Everything.App\Everything2Everything.App.csproj ` -c Release -r win-x64 --self-contained true ` -p:PublishSingleFile=false -o publish-self ``` -산출물: `publish\EverythingToJpeg.exe` +산출물: `publish\Everything2Everything.exe` ### 2) 컨텍스트 메뉴 등록 -`EverythingToJpeg.exe`를 한 번 실행 → "컨텍스트 메뉴 등록" 클릭. 또는 CLI: - ```powershell -.\EverythingToJpeg.exe register -.\EverythingToJpeg.exe unregister +.\Everything2Everything.exe register +.\Everything2Everything.exe unregister ``` -> Windows 11 메인 우클릭 메뉴가 아니라 "추가 옵션 표시(Shift+우클릭)" 메뉴에 노출됩니다. 메인 메뉴 노출은 Phase 2에서 IExplorerCommand + MSIX 로 추가 예정. +> Windows 11 기본 우클릭에서는 **추가 옵션 표시(Shift+우클릭)** 안에 노출됩니다. 메인 메뉴 노출은 MSIX + IExplorerCommand DLL이 필요합니다 — `packaging/BuildAndSign.ps1` 참고. ### 3) 사용 -- 파일 우클릭 → "추가 옵션 표시" → **JPEG로 빠른 변환** 또는 **JPEG로 변환…** -- 또는 메인 창에 파일/폴더를 끌어다 놓기 +#### 우클릭 카스케이드 +파일 우클릭 → **Everything2Everything으로 변환** → 서브메뉴에서 출력 형식 선택. 각 입력의 매트릭스에 따라 가능한 출력만 표시됩니다. -## 지원 현황 +| 입력 → 노출되는 서브메뉴 항목 | +|---| +| `.png` → JPEG · WebP · AVIF · GIF · TIFF · BMP · 변환… | +| `.heic` → JPEG · PNG · WebP · AVIF · GIF · TIFF · BMP · 변환… | +| `.pdf` → JPEG · PNG · WebP · AVIF · TIFF · BMP · 변환… | +| `.docx` → PDF · JPEG · PNG · WebP · AVIF · TIFF · BMP · 변환… | +| `.html` → PDF · JPEG · PNG · WebP · AVIF · TIFF · BMP · 변환… | -| 형식 | 상태 | 참고 | -|---|---|---| -| PNG · BMP · JPEG · WebP · AVIF · PSD · TIFF · GIF · RAW(NEF/CR2/CR3/ARW/DNG/RAF/ORF/RW2/SRW/PEF) | ✅ 준비됨 | Magick.NET 14.x | -| HEIC · HEIF | ✅ 준비됨 | PhotoSauce + libheif 디코드 | -| PDF | ✅ 준비됨 | PDFtoImage(PDFium) | -| HTML · HTM | ✅ 준비됨 | WebView2 헤드리스 + CDP `Page.captureScreenshot` 풀페이지 | -| DOCX · DOC | ⚙ 외부 도구 필요 | Microsoft Word 또는 LibreOffice 자동 감지 | -| HWP · HWPX | ⚙ 외부 도구 필요 | LibreOffice + [H2Orestart](https://github.com/ebandal/H2Orestart) 확장 | +"변환…" 항목은 메인 창을 띄워 사이드바에서 출력 형식 ComboBox로 직접 선택할 수 있습니다. + +#### 메인 창 +파일을 드래그 & 드롭하거나 Ctrl+O. 사이드바의 **TARGET FORMAT** ComboBox는 **큐의 모든 파일이 변환 가능한 출력의 교집합**만 보여줍니다 (이종 입력을 섞으면 자동 필터링). + +#### CLI + +```powershell +# 로 변환 (배치 가능) +.\Everything2Everything.exe to png photo.heic shot.jpg banner.webp +.\Everything2Everything.exe to pdf doc.docx report.html +.\Everything2Everything.exe to webp *.png + +# 빠른 변환 (기본 출력 .jpg, 컨텍스트 메뉴 호환) +.\Everything2Everything.exe quick photo.heic + +# 메인 창에서 출력 형식 선택 +.\Everything2Everything.exe dialog photo.heic + +# 진단 +.\Everything2Everything.exe diagnose +``` + +## 외부 도구 의존성 + +| 변환 | 필요 도구 | +|---|---| +| 모든 이미지 ↔ 이미지, RAW/PSD 디코딩 | (내장) Magick.NET 14 | +| HEIC/HEIF 디코딩 | (내장) PhotoSauce + libheif | +| PDF ↔ 이미지 | (내장) PDFtoImage(PDFium) | +| HTML → 이미지/PDF | Microsoft Edge **WebView2 Runtime** (Win11 기본 포함) | +| DOCX/DOC → PDF/이미지 | **Microsoft Word** 또는 **LibreOffice** (자동 감지) | +| HWP/HWPX → PDF/이미지 | **LibreOffice** + [**H2Orestart**](https://github.com/ebandal/H2Orestart) 확장 | + +메인 창 시작 시 가용성을 자동 점검해 사이드바에 안내하고, 미설치 도구는 다이얼로그에서 다운로드 링크를 제공합니다. ## 기술 스택 - **.NET 9 + WPF**, Windows 10.0.19041.0+ -- UI: **WPF-UI 4.3** (Win11 Fluent 2 — Mica 백드롭, Segoe UI Variable 타입 램프) -- 변환 엔진: Magick.NET, PDFtoImage, PhotoSauce.MagicScaler + Libheif +- UI: **WPF-UI 4.3** (Win11 Fluent 2) +- 변환 엔진: Magick.NET, PDFtoImage, PhotoSauce.MagicScaler + Libheif, WebView2 (CDP) -## 로드맵 +## 아키텍처 -| 단계 | 상태 | 내용 | -|---|---|---| -| Phase 1 | ✅ | 레지스트리 컨텍스트 메뉴 (Win11 "추가 옵션 표시"), 핵심 변환, Fluent UI | -| Phase 2 | ✅ | C++ IExplorerCommand DLL + MSIX Sparse Package | -| Phase 3 | ✅ | HTML(WebView2), HWP/HWPX(LibreOffice + H2Orestart) 실구현 | -| Phase 4 | ✅ | **자체 서명 MSIX 자동화** — `packaging/BuildAndSign.ps1` 한 방으로 인증서 생성 + 서명 + 패키지 | -| Phase 5 | ✅ | GitHub 리모트 + Actions 릴리즈 워크플로 | -| Phase 6 | ✅ | Past Results 영구 저장 (`%LocalAppData%\EverythingToJpeg\history.jsonl`) | -| Phase 7 | ✅ | 단축키 (Ctrl+O 추가, Ctrl+Enter 변환, Esc 닫기, F5 새로고침), 코드 정리 | -| Phase 8 | ✅ | 시작 시 Provider 가용성 자동 체크, 사이드바에 외부 도구 필요 안내 | +``` +입력 파일 → ProviderRegistry.TryGet(input ext, output ext) + ↓ + (input, output) → IConverterProvider 매트릭스 인덱스 + ↓ + provider.ConvertAsync(source, outDir, outExt, options, ...) +``` + +핵심 추상화: +- `ConversionPair(InputExtension, OutputExtension)` — 단방향 쌍 +- `ProviderCapability.SupportedConversions: IReadOnlyList` +- `ProviderRegistry` — `Dictionary<(input, output), Provider>` 매트릭스 인덱싱 + `OutputsForFile()` 쿼리 +- `ConversionEngine.ConvertOneAsync(source, outputExt, options)` — 동일 입출력 자동 skip + 출력 ext 기반 서브폴더 suffix(`_png/`, `_pdf/`) +- `ConvertOptions` — 형식별 sub-record (`Jpeg.Quality`, `Webp.Lossless`, `Avif.Speed`, `PdfRender.Dpi`, `HtmlRender.FullPage` 등) ## 키보드 단축키 @@ -80,51 +132,54 @@ dotnet publish src\EverythingToJpeg.App\EverythingToJpeg.App.csproj ` | Ctrl+Enter | Process Queue (변환 시작) | | Esc | 창 닫기 | | F5 | 통계 새로고침 | -| Active Queue 행 클릭 | 우측 Preview에 즉시 표시 | -| Past Results 행 클릭 | 원본 파일이 있으면 Preview 표시 | ## 두 가지 사용 방식 -### A) Portable EXE — 가장 가벼움 (Phase 1) +### A) Portable EXE — 가장 가벼움 - `dotnet publish` 산출물 그대로 사용 -- 우클릭 → **추가 옵션 표시** → "JPEG로 빠른 변환" / "JPEG로 변환…" +- 우클릭 → **추가 옵션 표시** → "Everything2Everything으로 변환" → 카스케이드 서브메뉴 - 인증서·서명 불필요 -### B) MSIX 패키지 — Win11 메인 메뉴 노출 (Phase 2) -- `packaging/BuildMsix.ps1` 로 MSIX 빌드 +### B) MSIX 패키지 — Win11 메인 메뉴 노출 +- `packaging/BuildMsix.ps1`로 MSIX 빌드 - 자체 서명 인증서를 `LocalMachine\TrustedPeople`에 임포트 후 사이드로드 -- 우클릭 → 바로 메인 메뉴에 항목 노출 +- 우클릭 → 메인 메뉴 바로 노출 (IExplorerCommand DLL 사용) - 자세한 절차는 [packaging/README.md](packaging/README.md) ## 프로젝트 구조 ``` -everythingToJpeg/ -├── EverythingToJpeg.slnx +Everything2Everything/ +├── Everything2Everything.slnx └── src/ - ├── EverythingToJpeg.Core/ — 변환 엔진, Provider 추상화 - │ ├── Providers/ — IConverterProvider + Capability 메타데이터 - │ ├── Converters/ — Magick / Heic / Pdf / Docx / Html / Hwpx - │ ├── ConversionEngine.cs - │ └── EverythingToJpegBootstrap.cs - └── EverythingToJpeg.App/ — WPF + CLI 통합 진입점 - ├── App.xaml(.cs) — CLI 라우터 - ├── Cli/CliRouter.cs — verb: quick / dialog / register / diagnose - ├── Shell/ContextMenuRegistrar.cs — HKCU 레지스트리 등록 - └── Views/ — Fluent UI 화면 + ├── Everything2Everything.Core/ — 변환 엔진, Provider 추상화 + │ ├── Providers/ + │ │ ├── ConversionPair.cs — (입력ext, 출력ext) 단방향 쌍 + │ │ ├── ProviderCapability.cs — SupportedConversions + 입출력 헬퍼 + │ │ ├── ProviderRegistry.cs — N×M 매트릭스 인덱싱 + │ │ └── IConverterProvider.cs + │ ├── Converters/ — Magick / Heic / Pdf / Docx / Html / Hwpx + │ ├── ConversionEngine.cs — outputExt 라우팅 + 서브폴더 suffix + │ ├── ConvertOptions.cs — 형식별 sub-options + │ └── Everything2EverythingBootstrap.cs + └── Everything2Everything.App/ — WPF + CLI 통합 진입점 + ├── App.xaml(.cs) + ├── Cli/CliRouter.cs — verb: to / quick / dialog / register / diagnose + ├── Shell/ContextMenuRegistrar.cs — HKCU 카스케이드 등록 + └── Views/ — Fluent UI 화면 ``` -## Provider 전략 (확장 포인트) +## Provider 확장 -새 형식을 지원하려면 `IConverterProvider`를 구현하고 `EverythingToJpegBootstrap.CreateDefault()`에 등록합니다. `ProviderCapability`에 다음을 명시하세요: +새 변환 쌍을 추가하려면 `IConverterProvider`를 구현하고 `Everything2EverythingBootstrap.CreateDefault()`에 등록합니다. `ProviderCapability.SupportedConversions`에 (입력ext, 출력ext) 쌍 리스트를 정의하면 `ProviderRegistry`가 매트릭스 인덱스에 자동 편입하며, 컨텍스트 메뉴 카스케이드 + 메인 창 ComboBox에 자동 반영됩니다. -- `Status` — `Available` / `Preview` / `RequiresExternal` / `ComingSoon` / `Disabled` -- `Extensions` — 자동 라우팅 + 컨텍스트 메뉴 등록 키 -- `ExternalDependencies` — UI에 자동 노출되는 외부 도구 -- `RoadmapNote` — 사용자에게 보여줄 향후 계획 - -`ComingSoon` 상태는 메인 창의 "지원 형식" 섹션에 자동 노출되지만 컨텍스트 메뉴 등록에서는 자동 제외됩니다. +```csharp +// 매트릭스 헬퍼 — 모든 입력 × 모든 출력 카르테시안 곱 +SupportedConversions: ProviderCapability.PairsFromMatrix( + new[] { ".png", ".jpg", ".webp" }, + new[] { ".png", ".jpg", ".pdf" }), +``` ## 라이선스 -MIT (예정). +MIT. diff --git a/packaging/BuildAndSign.ps1 b/packaging/BuildAndSign.ps1 index 2440418..ec84b78 100644 --- a/packaging/BuildAndSign.ps1 +++ b/packaging/BuildAndSign.ps1 @@ -1,20 +1,20 @@ #Requires -Version 5.1 # 한방에 빌드+자체서명: 인증서 자동 생성 → MSIX 빌드 → 서명까지 일관 처리. # 산출: -# - packaging/dist/EverythingToJpeg-x64.msix (서명됨) -# - packaging/EverythingToJpeg-DevCert.pfx (5대 PC 신뢰 등록용) +# - packaging/dist/Everything2Everything-x64.msix (서명됨) +# - packaging/Everything2Everything-DevCert.pfx (5대 PC 신뢰 등록용) [CmdletBinding()] param( - [string]$Subject = 'CN=EverythingToJpegDev', - [string]$Password = 'EverythingToJpegDev', + [string]$Subject = 'CN=Everything2EverythingDev', + [string]$Password = 'Everything2EverythingDev', [string]$Configuration = 'Release', [string]$Platform = 'x64' ) $ErrorActionPreference = 'Stop' $packagingDir = $PSScriptRoot -$pfxPath = Join-Path $packagingDir 'EverythingToJpeg-DevCert.pfx' +$pfxPath = Join-Path $packagingDir 'Everything2Everything-DevCert.pfx' $securePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force # ---- 1) 인증서 ---- @@ -36,7 +36,7 @@ if (-not $existing) { -CertStoreLocation 'Cert:\CurrentUser\My' ` -HashAlgorithm SHA256 ` -NotAfter (Get-Date).AddYears(5) ` - -FriendlyName 'EverythingToJpeg Dev' + -FriendlyName 'Everything2Everything Dev' } else { Write-Host "[1/3] 기존 인증서 재사용 (Thumbprint $($existing.Thumbprint))" @@ -62,7 +62,7 @@ Write-Host ' 1. PFX 파일을 5대 PC 각각에 복사:' Write-Host " $pfxPath" Write-Host ' 2. 각 PC에서 관리자 PowerShell:' Write-Host ' cd packaging' -Write-Host " .\Install-EverythingToJpeg.ps1 -PfxPath .\EverythingToJpeg-DevCert.pfx -MsixPath .\dist\EverythingToJpeg-x64.msix" +Write-Host " .\Install-Everything2Everything.ps1 -PfxPath .\Everything2Everything-DevCert.pfx -MsixPath .\dist\Everything2Everything-x64.msix" Write-Host ' PFX 비밀번호:' $Password Write-Host '' Write-Host ' 3. 우클릭 → JPEG로 빠른 변환 / JPEG로 변환… 이 메인 메뉴에 노출됨.' diff --git a/packaging/BuildMsix.ps1 b/packaging/BuildMsix.ps1 index 14dd699..2d51377 100644 --- a/packaging/BuildMsix.ps1 +++ b/packaging/BuildMsix.ps1 @@ -25,8 +25,8 @@ $distDir = Join-Path $packagingDir 'dist' $manifestPath = Join-Path $packagingDir 'Package.appxmanifest' $assetsSrc = Join-Path $packagingDir 'Assets' -$appProj = Join-Path $repoRoot 'src\EverythingToJpeg.App\EverythingToJpeg.App.csproj' -$shellProj = Join-Path $repoRoot 'src\EverythingToJpeg.Shell\EverythingToJpeg.Shell.vcxproj' +$appProj = Join-Path $repoRoot 'src\Everything2Everything.App\Everything2Everything.App.csproj' +$shellProj = Join-Path $repoRoot 'src\Everything2Everything.Shell\Everything2Everything.Shell.vcxproj' function Find-WindowsSdkTool { param([string]$ToolName) @@ -91,12 +91,12 @@ if (-not $nuget) { } $packagesDir = Join-Path $repoRoot 'packages' -& $nugetCmd restore (Join-Path $repoRoot 'src\EverythingToJpeg.Shell\packages.config') -PackagesDirectory $packagesDir | Out-Host +& $nugetCmd restore (Join-Path $repoRoot 'src\Everything2Everything.Shell\packages.config') -PackagesDirectory $packagesDir | Out-Host if ($LASTEXITCODE -ne 0) { throw 'NuGet restore 실패' } & $msbuild $shellProj /p:Configuration=$Configuration /p:Platform=$Platform /m /v:minimal | Out-Host if ($LASTEXITCODE -ne 0) { throw 'Shell build 실패' } -$shellDll = Join-Path $repoRoot ("src\EverythingToJpeg.Shell\$Platform\$Configuration\EverythingToJpeg.Shell.dll") +$shellDll = Join-Path $repoRoot ("src\Everything2Everything.Shell\$Platform\$Configuration\Everything2Everything.Shell.dll") if (-not (Test-Path $shellDll)) { throw "Shell DLL 산출물 없음: $shellDll" } # ---- 3) Layout 디렉토리 ---- @@ -118,7 +118,7 @@ Copy-Item -Path $manifestPath -Destination (Join-Path $layoutDir 'AppxManifest.x Write-Host '' Write-Host '[4/5] makeappx pack' if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null } -$msixPath = Join-Path $distDir ("EverythingToJpeg-$($Platform.ToLower()).msix") +$msixPath = Join-Path $distDir ("Everything2Everything-$($Platform.ToLower()).msix") if (Test-Path $msixPath) { Remove-Item $msixPath -Force } & $makeappx pack /d $layoutDir /p $msixPath /o | Out-Host if ($LASTEXITCODE -ne 0) { throw 'makeappx pack 실패' } diff --git a/packaging/CreateDevCert.ps1 b/packaging/CreateDevCert.ps1 index 9889cc2..7ca7e52 100644 --- a/packaging/CreateDevCert.ps1 +++ b/packaging/CreateDevCert.ps1 @@ -4,8 +4,8 @@ [CmdletBinding()] param( - [string]$Subject = 'CN=EverythingToJpegDev', - [string]$OutputPfx = (Join-Path $PSScriptRoot 'EverythingToJpeg-DevCert.pfx'), + [string]$Subject = 'CN=Everything2EverythingDev', + [string]$OutputPfx = (Join-Path $PSScriptRoot 'Everything2Everything-DevCert.pfx'), [securestring]$Password ) @@ -28,7 +28,7 @@ $cert = New-SelfSignedCertificate ` -CertStoreLocation 'Cert:\CurrentUser\My' ` -HashAlgorithm SHA256 ` -NotAfter (Get-Date).AddYears(5) ` - -FriendlyName 'EverythingToJpeg Dev' + -FriendlyName 'Everything2Everything Dev' Write-Host "Thumbprint: $($cert.Thumbprint)" Write-Host "Exporting PFX: $OutputPfx" diff --git a/packaging/GenerateAssets.ps1 b/packaging/GenerateAssets.ps1 index 6813a8f..1ed5d21 100644 --- a/packaging/GenerateAssets.ps1 +++ b/packaging/GenerateAssets.ps1 @@ -49,8 +49,8 @@ function New-LogoPng { } Write-Host 'Generating placeholder logos…' -New-LogoPng -Width 50 -Height 50 -Path (Join-Path $assetsDir 'StoreLogo.png') -Label 'E2J' -New-LogoPng -Width 44 -Height 44 -Path (Join-Path $assetsDir 'Square44x44Logo.png') -Label 'E2J' -New-LogoPng -Width 150 -Height 150 -Path (Join-Path $assetsDir 'Square150x150Logo.png')-Label 'E2J' -New-LogoPng -Width 310 -Height 150 -Path (Join-Path $assetsDir 'Wide310x150Logo.png') -Label 'EverythingToJpeg' +New-LogoPng -Width 50 -Height 50 -Path (Join-Path $assetsDir 'StoreLogo.png') -Label 'E2E' +New-LogoPng -Width 44 -Height 44 -Path (Join-Path $assetsDir 'Square44x44Logo.png') -Label 'E2E' +New-LogoPng -Width 150 -Height 150 -Path (Join-Path $assetsDir 'Square150x150Logo.png')-Label 'E2E' +New-LogoPng -Width 310 -Height 150 -Path (Join-Path $assetsDir 'Wide310x150Logo.png') -Label 'Everything2Everything' Write-Host 'Done.' diff --git a/packaging/Install-EverythingToJpeg.ps1 b/packaging/Install-Everything2Everything.ps1 similarity index 88% rename from packaging/Install-EverythingToJpeg.ps1 rename to packaging/Install-Everything2Everything.ps1 index df2e633..df01884 100644 --- a/packaging/Install-EverythingToJpeg.ps1 +++ b/packaging/Install-Everything2Everything.ps1 @@ -2,14 +2,14 @@ #Requires -RunAsAdministrator # 5대 PC에서 MSIX 사이드로드 설치 — 1회 셋업 스크립트. # 사용법: -# PowerShell (관리자) > .\Install-EverythingToJpeg.ps1 -PfxPath .\EverythingToJpeg-DevCert.pfx -MsixPath .\EverythingToJpeg.msix +# PowerShell (관리자) > .\Install-Everything2Everything.ps1 -PfxPath .\Everything2Everything-DevCert.pfx -MsixPath .\Everything2Everything.msix [CmdletBinding()] param( [Parameter(Mandatory)] [string]$PfxPath, [Parameter(Mandatory)] [string]$MsixPath, [securestring]$PfxPassword, - [string]$Password = 'EverythingToJpegDev' + [string]$Password = 'Everything2EverythingDev' ) $ErrorActionPreference = 'Stop' diff --git a/packaging/Package.appxmanifest b/packaging/Package.appxmanifest index f79706a..67e4b52 100644 --- a/packaging/Package.appxmanifest +++ b/packaging/Package.appxmanifest @@ -10,13 +10,13 @@ IgnorableNamespaces="uap rescap desktop desktop4 desktop5 com"> - EverythingToJpeg + Everything2Everything YunChan Assets\StoreLogo.png @@ -31,12 +31,12 @@ - @@ -47,12 +47,12 @@ - + diff --git a/packaging/README.md b/packaging/README.md index 92b9e26..af6a6b4 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -1,6 +1,6 @@ -# Phase 2 — MSIX 패키징 +# MSIX 패키징 -Win11 메인 우클릭 메뉴에 "JPEG로 빠른 변환" / "JPEG로 변환…"을 띄우는 정공법. +Win11 메인 우클릭 메뉴에 "Everything2Everything: 빠른 변환 (JPEG)" / "Everything2Everything: 변환…"을 띄우는 정공법. 레지스트리 기반 카스케이드 메뉴는 [Portable EXE 방식](../README.md#a-portable-exe--가장-가벼움)을 참고하세요. ## 구성 @@ -11,18 +11,18 @@ packaging/ ├── GenerateAssets.ps1 — placeholder PNG 일괄 생성 ├── CreateDevCert.ps1 — 자체 서명 코드사이닝 인증서 생성 + PFX export ├── BuildMsix.ps1 — .NET publish + C++ DLL 빌드 + makeappx + (선택) signtool -└── Install-EverythingToJpeg.ps1 — 5대 PC 1회 설치 스크립트 +└── Install-Everything2Everything.ps1 — 5대 PC 1회 설치 스크립트 ``` -C++ Shell DLL은 `src/EverythingToJpeg.Shell/` 에 있고 `BuildMsix.ps1` 안에서 자동 빌드됩니다. +C++ Shell DLL은 `src/Everything2Everything.Shell/` 에 있고 `BuildMsix.ps1` 안에서 자동 빌드됩니다. ## 1회: 자체 서명 인증서 만들기 ```powershell cd packaging .\CreateDevCert.ps1 -# Subject 기본값: CN=EverythingToJpegDev (Package.appxmanifest의 Publisher와 일치) -# 비밀번호 입력 → EverythingToJpeg-DevCert.pfx 생성 +# Subject 기본값: CN=Everything2EverythingDev (Package.appxmanifest의 Publisher와 일치) +# 비밀번호 입력 → Everything2Everything-DevCert.pfx 생성 ``` 출력된 Thumbprint를 `BuildMsix.ps1 -CertThumbprint <값>` 으로 사용하거나, PFX 파일을 5대 PC에 복사해서 설치 시 사용합니다. @@ -32,13 +32,13 @@ cd packaging ### 미서명 (Phase 1 그대로 사용 가능, 메인 메뉴 노출은 안 됨) ```powershell .\BuildMsix.ps1 -# 산출: packaging/dist/EverythingToJpeg-x64.msix +# 산출: packaging/dist/Everything2Everything-x64.msix ``` ### 서명 ```powershell # 방법 1: PFX 사용 -.\BuildMsix.ps1 -Sign -PfxPath .\EverythingToJpeg-DevCert.pfx +.\BuildMsix.ps1 -Sign -PfxPath .\Everything2Everything-DevCert.pfx # 방법 2: 인증서 저장소의 Thumbprint .\BuildMsix.ps1 -Sign -CertThumbprint AABBCCDD... @@ -47,9 +47,9 @@ cd packaging ## 5대 PC 설치 (관리자 PowerShell) ```powershell -.\Install-EverythingToJpeg.ps1 ` - -PfxPath .\EverythingToJpeg-DevCert.pfx ` - -MsixPath .\EverythingToJpeg-x64.msix +.\Install-Everything2Everything.ps1 ` + -PfxPath .\Everything2Everything-DevCert.pfx ` + -MsixPath .\Everything2Everything-x64.msix ``` 스크립트가 자동으로: @@ -64,7 +64,7 @@ cd packaging 자체 서명 만들기조차 귀찮을 때: ```powershell # 개발자 모드 켜기: 설정 → 개인 정보 및 보안 → 개발자용 → 켜기 -Add-AppxPackage -AllowUnsigned -Path .\EverythingToJpeg-x64.msix +Add-AppxPackage -AllowUnsigned -Path .\Everything2Everything-x64.msix ``` > Win11 24H2부터 `-AllowUnsigned` 지원. 이전 버전은 자체 서명 권장. diff --git a/src/EverythingToJpeg.App/App.xaml b/src/Everything2Everything.App/App.xaml similarity index 96% rename from src/EverythingToJpeg.App/App.xaml rename to src/Everything2Everything.App/App.xaml index 7516c02..9ab7715 100644 --- a/src/EverythingToJpeg.App/App.xaml +++ b/src/Everything2Everything.App/App.xaml @@ -1,8 +1,8 @@ - diff --git a/src/EverythingToJpeg.App/App.xaml.cs b/src/Everything2Everything.App/App.xaml.cs similarity index 85% rename from src/EverythingToJpeg.App/App.xaml.cs rename to src/Everything2Everything.App/App.xaml.cs index 6ed2c32..6fb04b5 100644 --- a/src/EverythingToJpeg.App/App.xaml.cs +++ b/src/Everything2Everything.App/App.xaml.cs @@ -1,13 +1,13 @@ using System.Windows; -using EverythingToJpeg.App.Cli; -using EverythingToJpeg.App.Views; -using EverythingToJpeg.Core; +using Everything2Everything.App.Cli; +using Everything2Everything.App.Views; +using Everything2Everything.Core; -namespace EverythingToJpeg.App; +namespace Everything2Everything.App; public partial class App : Application { - public ConversionEngine Engine { get; } = EverythingToJpegBootstrap.CreateDefault(); + public ConversionEngine Engine { get; } = Everything2EverythingBootstrap.CreateDefault(); protected override async void OnStartup(StartupEventArgs e) { @@ -39,12 +39,12 @@ public partial class App : Application case CliRouter.Mode.Quick: if (parsed.Files.Count == 0) { - MessageBox.Show("변환할 파일이 없습니다.", "EverythingToJpeg", + MessageBox.Show("변환할 파일이 없습니다.", "Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Information); Shutdown(1); return; } - await RunQuickAsync(parsed.Files); + await RunQuickAsync(parsed.Files, parsed.OutputExtension ?? ".jpg"); return; case CliRouter.Mode.Dialog: @@ -79,11 +79,11 @@ public partial class App : Application window.Show(); } - private async Task RunQuickAsync(IReadOnlyList files) + private async Task RunQuickAsync(IReadOnlyList files, string outputExtension) { - var logPath = Path.Combine(Path.GetTempPath(), "EverythingToJpeg_quick.log"); + var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_quick.log"); var log = new System.Text.StringBuilder(); - log.AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Quick start, {files.Count} file(s)"); + log.AppendLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Quick start → {outputExtension}, {files.Count} file(s)"); foreach (var f in files) log.AppendLine($" src: {f}"); var progress = new QuickProgressWindow(files.Count); @@ -93,7 +93,7 @@ public partial class App : Application { var options = ConvertOptions.Quick(); var reporter = new Progress(p => progress.Report(p)); - var results = await Engine.ConvertManyAsync(files, options, reporter); + var results = await Engine.ConvertManyAsync(files, ".jpg", options, reporter); foreach (var r in results) { @@ -110,7 +110,7 @@ public partial class App : Application log.AppendLine($" EXCEPTION {ex.GetType().Name}: {ex.Message}"); log.AppendLine(ex.ToString()); try { progress.Close(); } catch { } - MessageBox.Show($"변환 중 오류: {ex.Message}\n\n로그: {logPath}", "EverythingToJpeg", + MessageBox.Show($"변환 중 오류: {ex.Message}\n\n로그: {logPath}", "Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Error); } finally @@ -121,7 +121,7 @@ public partial class App : Application private static void WireGlobalExceptionLogging() { - var path = Path.Combine(Path.GetTempPath(), "EverythingToJpeg_unhandled.log"); + var path = Path.Combine(Path.GetTempPath(), "Everything2Everything_unhandled.log"); void Append(string source, Exception? ex) { @@ -141,7 +141,7 @@ public partial class App : Application Append("Application.DispatcherUnhandledException", e.Exception); MessageBox.Show( "예기치 못한 오류:\n\n" + e.Exception.Message + "\n\n로그: " + path, - "EverythingToJpeg", + "Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Error); e.Handled = true; }; diff --git a/src/EverythingToJpeg.App/AssemblyInfo.cs b/src/Everything2Everything.App/AssemblyInfo.cs similarity index 100% rename from src/EverythingToJpeg.App/AssemblyInfo.cs rename to src/Everything2Everything.App/AssemblyInfo.cs diff --git a/src/EverythingToJpeg.App/Cli/CliRouter.cs b/src/Everything2Everything.App/Cli/CliRouter.cs similarity index 64% rename from src/EverythingToJpeg.App/Cli/CliRouter.cs rename to src/Everything2Everything.App/Cli/CliRouter.cs index 613be16..4f39092 100644 --- a/src/EverythingToJpeg.App/Cli/CliRouter.cs +++ b/src/Everything2Everything.App/Cli/CliRouter.cs @@ -1,7 +1,7 @@ -using EverythingToJpeg.App.Shell; -using EverythingToJpeg.Core; +using Everything2Everything.App.Shell; +using Everything2Everything.Core; -namespace EverythingToJpeg.App.Cli; +namespace Everything2Everything.App.Cli; internal static class CliRouter { @@ -16,7 +16,7 @@ internal static class CliRouter Help, } - public sealed record ParsedArgs(Mode Mode, IReadOnlyList Files); + public sealed record ParsedArgs(Mode Mode, IReadOnlyList Files, string? OutputExtension = null); public static ParsedArgs Parse(string[] args) { @@ -26,9 +26,18 @@ internal static class CliRouter var verb = args[0].Trim().ToLowerInvariant(); var rest = args.Skip(1).Where(a => !string.IsNullOrWhiteSpace(a)).ToList(); + if (verb == "to") + { + if (rest.Count < 2) + return new ParsedArgs(Mode.Help, Array.Empty()); + var outputExt = NormalizeExt(rest[0]); + var files = ExpandFiles(rest.Skip(1)); + return new ParsedArgs(Mode.Quick, files, outputExt); + } + return verb switch { - "quick" => new ParsedArgs(Mode.Quick, ExpandFiles(rest)), + "quick" => new ParsedArgs(Mode.Quick, ExpandFiles(rest), ".jpg"), "dialog" => new ParsedArgs(Mode.Dialog, ExpandFiles(rest)), "register" => new ParsedArgs(Mode.Register, Array.Empty()), "unregister" => new ParsedArgs(Mode.Unregister, Array.Empty()), @@ -39,6 +48,12 @@ internal static class CliRouter }; } + private static string NormalizeExt(string ext) + { + var trimmed = ext.Trim().ToLowerInvariant(); + return trimmed.StartsWith('.') ? trimmed : "." + trimmed; + } + private static IReadOnlyList ExpandFiles(IEnumerable raw) { var list = new List(); @@ -61,37 +76,41 @@ internal static class CliRouter public static string HelpText() { - var engine = EverythingToJpegBootstrap.CreateDefault(); - var supported = string.Join(", ", - engine.Providers.Implemented.SelectMany(p => p.Capability.Extensions).Distinct().OrderBy(e => e)); + var engine = Everything2EverythingBootstrap.CreateDefault(); + var inputs = string.Join(", ", + engine.Providers.Implemented.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e)); + var outputs = string.Join(", ", + engine.Providers.Implemented.SelectMany(p => p.Capability.OutputExtensions).Distinct().OrderBy(e => e)); var coming = string.Join(", ", - engine.Providers.ComingSoon.SelectMany(p => p.Capability.Extensions).Distinct().OrderBy(e => e)); + engine.Providers.ComingSoon.SelectMany(p => p.Capability.InputExtensions).Distinct().OrderBy(e => e)); return $""" - EverythingToJpeg — 모든 것을 JPEG로 + Everything2Everything — 모든 것을 모든 것으로 (양방향 변환) 사용: - EverythingToJpeg.exe quick <파일들...> 빠른 변환 (다이얼로그 없이 즉시) - EverythingToJpeg.exe dialog <파일들...> 상세 옵션 다이얼로그 표시 - EverythingToJpeg.exe register 컨텍스트 메뉴 등록 (현재 사용자) - EverythingToJpeg.exe unregister 컨텍스트 메뉴 해제 - EverythingToJpeg.exe diagnose 지원 형식·외부 도구 진단 - EverythingToJpeg.exe 메인 창 표시 + Everything2Everything.exe to <파일들...> 로 변환 (예: to png a.jpg b.heic) + Everything2Everything.exe quick <파일들...> 빠른 변환 (기본 출력 .jpg) + Everything2Everything.exe dialog <파일들...> 메인 창에서 출력 형식 선택 + Everything2Everything.exe register 컨텍스트 메뉴 등록 (현재 사용자) + Everything2Everything.exe unregister 컨텍스트 메뉴 해제 + Everything2Everything.exe diagnose 지원 매트릭스·외부 도구 진단 + Everything2Everything.exe 메인 창 표시 - 지원 (지금): {supported} - 지원 예정 : {coming} + 입력 형식: {inputs} + 출력 형식: {outputs} + 지원 예정: {coming} """; } public static int RunRegister(bool register) { - var logPath = Path.Combine(Path.GetTempPath(), "EverythingToJpeg_register.log"); + var logPath = Path.Combine(Path.GetTempPath(), "Everything2Everything_register.log"); var log = new System.Text.StringBuilder(); log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] RunRegister start, register={register}"); try { log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Creating engine..."); - var engine = EverythingToJpegBootstrap.CreateDefault(); + var engine = Everything2EverythingBootstrap.CreateDefault(); log.AppendLine($"[{DateTime.Now:HH:mm:ss.fff}] Engine OK. Implemented providers: {string.Join(",", engine.Providers.Implemented.Select(p => p.Capability.Id))}"); if (register) diff --git a/src/EverythingToJpeg.App/Cli/ConsoleHelper.cs b/src/Everything2Everything.App/Cli/ConsoleHelper.cs similarity index 92% rename from src/EverythingToJpeg.App/Cli/ConsoleHelper.cs rename to src/Everything2Everything.App/Cli/ConsoleHelper.cs index 345f187..74d564e 100644 --- a/src/EverythingToJpeg.App/Cli/ConsoleHelper.cs +++ b/src/Everything2Everything.App/Cli/ConsoleHelper.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace EverythingToJpeg.App.Cli; +namespace Everything2Everything.App.Cli; internal static class ConsoleHelper { diff --git a/src/EverythingToJpeg.App/EverythingToJpeg.App.csproj b/src/Everything2Everything.App/Everything2Everything.App.csproj similarity index 77% rename from src/EverythingToJpeg.App/EverythingToJpeg.App.csproj rename to src/Everything2Everything.App/Everything2Everything.App.csproj index b68e912..8aba88f 100644 --- a/src/EverythingToJpeg.App/EverythingToJpeg.App.csproj +++ b/src/Everything2Everything.App/Everything2Everything.App.csproj @@ -6,8 +6,8 @@ enable enable true - EverythingToJpeg - EverythingToJpeg.App + Everything2Everything + Everything2Everything.App app.manifest 10.0.17763.0 10.0.17763.0 @@ -15,7 +15,7 @@ - + diff --git a/src/EverythingToJpeg.App/GlobalUsings.cs b/src/Everything2Everything.App/GlobalUsings.cs similarity index 100% rename from src/EverythingToJpeg.App/GlobalUsings.cs rename to src/Everything2Everything.App/GlobalUsings.cs diff --git a/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs new file mode 100644 index 0000000..d63e7b2 --- /dev/null +++ b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs @@ -0,0 +1,177 @@ +using Everything2Everything.Core; +using Everything2Everything.Core.Providers; +using Microsoft.Win32; + +namespace Everything2Everything.App.Shell; + +internal static class ContextMenuRegistrar +{ + private const string MainVerb = "Everything2Everything"; + private const string MainLabel = "Everything2Everything으로 변환"; + + private const string SubmenuKeyPrefix = "Everything2Everything.SubMenu."; + + private static readonly (string Ext, string Label, string SortPrefix)[] PopularOutputs = + { + (".jpg", "JPEG (.jpg)", "01"), + (".png", "PNG (.png)", "02"), + (".webp", "WebP (.webp)", "03"), + (".pdf", "PDF (.pdf)", "04"), + (".txt", "텍스트 (.txt) — OCR", "05"), + (".docx", "Word (.docx) — OCR", "06"), + (".avif", "AVIF (.avif)", "07"), + (".gif", "GIF (.gif)", "08"), + (".tif", "TIFF (.tif)", "09"), + (".bmp", "BMP (.bmp)", "10"), + }; + + public static void Register(ConversionEngine engine) + { + var exe = GetAppExecutablePath(); + var icon = exe + ",0"; + + foreach (var ext in CollectInputExtensions(engine)) + { + var outputs = engine.Providers.OutputsForInput(ext) + .Select(o => o.ToLowerInvariant()) + .ToHashSet(); + + var availableOutputs = PopularOutputs + .Where(p => outputs.Contains(p.Ext) && !string.Equals(p.Ext, ext, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (availableOutputs.Count == 0) continue; + + WriteCascade(ext, exe, icon, availableOutputs); + } + + NotifyShell(); + } + + public static void Unregister(ConversionEngine engine) + { + foreach (var ext in CollectInputExtensions(engine)) + { + DeleteVerb(ext, MainVerb); + DeleteVerb(ext, "Everything2Everything.Quick"); + DeleteVerb(ext, "Everything2Everything.Dialog"); + DeleteSubmenuTree(ext); + } + NotifyShell(); + } + + private static IEnumerable CollectInputExtensions(ConversionEngine engine) + { + return engine.Providers.Implemented + .Where(p => p.Capability.CanRegisterContextMenu) + .SelectMany(p => p.Capability.InputExtensions) + .Select(e => e.StartsWith('.') ? e : "." + e) + .Select(e => e.ToLowerInvariant()) + .Distinct(); + } + + private static void WriteCascade( + string ext, + string exe, + string icon, + IReadOnlyList<(string Ext, string Label, string SortPrefix)> availableOutputs) + { + var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.'); + + var verbPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell\{MainVerb}"; + using (var verbKey = Registry.CurrentUser.CreateSubKey(verbPath, writable: true) + ?? throw new InvalidOperationException($"레지스트리 키 생성 실패: {verbPath}")) + { + verbKey.SetValue(null, MainLabel, RegistryValueKind.String); + verbKey.SetValue("MUIVerb", MainLabel, RegistryValueKind.String); + verbKey.SetValue("Icon", icon, RegistryValueKind.String); + verbKey.SetValue("SubCommands", "", RegistryValueKind.String); + verbKey.SetValue("ExtendedSubCommandsKey", submenuKeyName, RegistryValueKind.String); + try { verbKey.DeleteSubKeyTree("command", throwOnMissingSubKey: false); } catch { } + } + + var submenuShellPath = $@"Software\Classes\{submenuKeyName}\shell"; + using (var existing = Registry.CurrentUser.OpenSubKey(submenuShellPath, writable: true)) + { + if (existing is not null) + { + foreach (var name in existing.GetSubKeyNames()) + { + try { existing.DeleteSubKeyTree(name, throwOnMissingSubKey: false); } catch { } + } + } + } + + foreach (var (outExt, outLabel, sortPrefix) in availableOutputs) + { + var subVerbName = $"{sortPrefix}_{outExt.TrimStart('.')}"; + var cliExt = outExt.TrimStart('.'); + WriteSubmenuItem(submenuKeyName, subVerbName, outLabel, icon, + $"\"{exe}\" to {cliExt} \"%1\""); + } + + WriteSubmenuItem(submenuKeyName, "98_dialog", "변환… (옵션 선택)", icon, + $"\"{exe}\" dialog \"%1\""); + } + + private static void WriteSubmenuItem(string submenuKeyName, string verbName, string label, string icon, string command) + { + var path = $@"Software\Classes\{submenuKeyName}\shell\{verbName}"; + using var key = Registry.CurrentUser.CreateSubKey(path, writable: true) + ?? throw new InvalidOperationException($"서브메뉴 키 생성 실패: {path}"); + + key.SetValue(null, label, RegistryValueKind.String); + key.SetValue("MUIVerb", label, RegistryValueKind.String); + key.SetValue("Icon", icon, RegistryValueKind.String); + + using var commandKey = key.CreateSubKey("command", writable: true) + ?? throw new InvalidOperationException("command 하위 키 생성 실패"); + commandKey.SetValue(null, command, RegistryValueKind.String); + } + + private static void DeleteVerb(string ext, string verb) + { + var parentPath = $@"Software\Classes\SystemFileAssociations\{ext}\shell"; + try + { + using var parent = Registry.CurrentUser.OpenSubKey(parentPath, writable: true); + parent?.DeleteSubKeyTree(verb, throwOnMissingSubKey: false); + } + catch + { + } + } + + private static void DeleteSubmenuTree(string ext) + { + var submenuKeyName = SubmenuKeyPrefix + ext.TrimStart('.'); + var path = $@"Software\Classes\{submenuKeyName}"; + try + { + Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false); + } + catch + { + } + } + + private static string GetAppExecutablePath() + { + var exe = Environment.ProcessPath; + if (!string.IsNullOrEmpty(exe) && File.Exists(exe)) return exe; + return AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar) + + Path.DirectorySeparatorChar + "Everything2Everything.exe"; + } + + private static void NotifyShell() + { + try { NativeMethods.SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero); } + catch { } + } + + private static class NativeMethods + { + [System.Runtime.InteropServices.DllImport("shell32.dll")] + public static extern void SHChangeNotify(int wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); + } +} diff --git a/src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml similarity index 95% rename from src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml rename to src/Everything2Everything.App/Views/DiagnoseWindow.xaml index d5b95ec..29d1e71 100644 --- a/src/EverythingToJpeg.App/Views/DiagnoseWindow.xaml +++ b/src/Everything2Everything.App/Views/DiagnoseWindow.xaml @@ -1,4 +1,4 @@ - + diff --git a/src/EverythingToJpeg.App/Views/MainWindow.xaml b/src/Everything2Everything.App/Views/MainWindow.xaml similarity index 96% rename from src/EverythingToJpeg.App/Views/MainWindow.xaml rename to src/Everything2Everything.App/Views/MainWindow.xaml index ed650c5..48f6a48 100644 --- a/src/EverythingToJpeg.App/Views/MainWindow.xaml +++ b/src/Everything2Everything.App/Views/MainWindow.xaml @@ -1,4 +1,4 @@ - - - - + + + + + + - - + + + - + - + + ToolTip="비워두면 원본 폴더 옆 서브폴더에 저장"/>