MEDIABROWSER 개명·업데이트 채널 전환·아이콘 tofu 근본 수정 (BACKLOG 13차) — ①개명: installer.iss AppName/출력 MediaBrowser-Setup-*(AppId·exe·데이터 경로 유지로 업그레이드 호환), 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser-<ver>-arm64.apk), 창 타이틀·모바일 표시명·obtainium.json·README. ②UpdateChecker: apiBase 주입(기본=자체 Forgejo /api/v1, GitHub 는 apiBase 로 호환), osx 자산 tar.gz 허용(기존 dmg 만 → mac 영원히 자산 없음 결함 수정), AppConfig 기본 UpdateRepo=Video-Downloader/releases+UpdateApiBase — 테스트 4건. ③아이콘 tofu(사용자 보고 "절대 안고쳐지네") 근본 원인: Shared.xaml 암시적 TextBlock 스타일의 FontFamily=UiFont 강제가 버튼 ContentPresenter 자동 TextBlock 의 IconFont 상속을 덮어 모든 PUA 글리프가 notdef — 폰트 설치/글리프 존재/HW·SW 렌더/RTB/DPI/런타임 8·10/자체포함·공유 전부 배제 후 실창 픽셀 비교로 격리, 콘텐츠 TextBlock 의 실폰트가 Pretendard 체인임을 테스트로 실증. 수정: 암시 스타일 FontFamily 제거(창 루트 상속으로 통일, PairingWindow/PlayerWindow 는 SetResourceReference), PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳 정리, 순수 글리프 2곳 IconFont 명시. 회귀 방어: IconGlyphXamlTests(정적 스캔)·IconContentFontTests(실창 폰트 단정)·IconGlyphRenderTests(픽셀 비교) — 462/462 GREEN, 수정 전후 실창 캡처로 글리프 렌더 확인
This commit is contained in:
parent
c8b061959b
commit
02995f4153
24 changed files with 451 additions and 64 deletions
|
|
@ -38,3 +38,13 @@ Grep 으로 심볼 위치를 찾고 해당 구간만 Read 한다. 분할 리팩
|
||||||
|
|
||||||
`VideoDownloader.App` 은 `InternalsVisibleTo("VideoDownloader.Tests")` 가 걸려 있다.
|
`VideoDownloader.App` 은 `InternalsVisibleTo("VideoDownloader.Tests")` 가 걸려 있다.
|
||||||
테스트를 위해 멤버를 `public` 으로 승격하지 말고 `internal` 로 두면 된다.
|
테스트를 위해 멤버를 `public` 으로 승격하지 말고 `internal` 로 두면 된다.
|
||||||
|
|
||||||
|
## 아이콘 글리프 (PUA · Segoe Fluent Icons)
|
||||||
|
|
||||||
|
- **암시적(키 없는) TextBlock 스타일에 FontFamily 를 넣지 않는다.** 버튼 ContentPresenter 가 만드는
|
||||||
|
자동 TextBlock 이 IconFont 상속을 잃어 모든 아이콘이 tofu(□)가 된다(2026-08-18 실결함 — 원인 규명에
|
||||||
|
실캡처·픽셀 비교까지 필요했다). UI 폰트 통일은 각 창 루트의 `FontFamily="{DynamicResource UiFont}"`
|
||||||
|
상속으로 한다. 코드 생성 창은 `SetResourceReference(FontFamilyProperty, "UiFont")`.
|
||||||
|
- PUA 글리프는 IconFont 보장 스타일(IconButton·FluentIcon·CaptionButton 계열)이나 명시
|
||||||
|
`FontFamily="{DynamicResource IconFont}"` 에서만 쓴다. **한 문자열에 PUA 와 일반 텍스트를 섞지 않는다.**
|
||||||
|
`Infrastructure/IconGlyphXamlTests` 가 정적으로, `E2E/IconContentFontTests` 가 실창에서 이를 강제한다.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ git status --short
|
||||||
```
|
```
|
||||||
|
|
||||||
- 하는 일: `VideoDownloader.App` 을 `win-x64`/`win-arm64` self-contained 로 publish (`publish/<arch>/`)
|
- 하는 일: `VideoDownloader.App` 을 `win-x64`/`win-arm64` self-contained 로 publish (`publish/<arch>/`)
|
||||||
→ Inno Setup 으로 설치자 컴파일 → `out/VideoDownloader-Setup-<arch>.exe`
|
→ Inno Setup 으로 설치자 컴파일 → `out/MediaBrowser-Setup-<arch>.exe`
|
||||||
- 필요: **Inno Setup 6** (`winget install --id JRSoftware.InnoSetup -e`).
|
- 필요: **Inno Setup 6** (`winget install --id JRSoftware.InnoSetup -e`).
|
||||||
없으면 스크립트가 명시적으로 throw 한다.
|
없으면 스크립트가 명시적으로 throw 한다.
|
||||||
- 특정 아키텍처만: `-Archs x64`
|
- 특정 아키텍처만: `-Archs x64`
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ jobs:
|
||||||
run: ./build/publish.ps1 -Config Release -Version $env:APP_VERSION
|
run: ./build/publish.ps1 -Config Release -Version $env:APP_VERSION
|
||||||
- name: 릴리스 업로드
|
- name: 릴리스 업로드
|
||||||
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
||||||
run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files out/VideoDownloader-Setup-x64.exe, out/VideoDownloader-Setup-arm64.exe
|
run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files out/MediaBrowser-Setup-x64.exe, out/MediaBrowser-Setup-arm64.exe
|
||||||
|
|
||||||
# ── Android APK (키스토어 시크릿 없으면 디버그 서명 APK) ──
|
# ── Android APK (키스토어 시크릿 없으면 디버그 서명 APK) ──
|
||||||
android:
|
android:
|
||||||
|
|
@ -126,10 +126,10 @@ jobs:
|
||||||
$apk = Get-ChildItem out/android/*-Signed.apk | Select-Object -First 1
|
$apk = Get-ChildItem out/android/*-Signed.apk | Select-Object -First 1
|
||||||
if (-not $apk) { $apk = Get-ChildItem out/android/*.apk | Select-Object -First 1 }
|
if (-not $apk) { $apk = Get-ChildItem out/android/*.apk | Select-Object -First 1 }
|
||||||
if (-not $apk) { throw "APK 산출물 없음" }
|
if (-not $apk) { throw "APK 산출물 없음" }
|
||||||
Copy-Item $apk.FullName "out/VideoDownloader-$env:APP_VERSION-arm64.apk"
|
Copy-Item $apk.FullName "out/MediaBrowser-$env:APP_VERSION-arm64.apk"
|
||||||
- name: 릴리스 업로드
|
- name: 릴리스 업로드
|
||||||
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
||||||
run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files "out/VideoDownloader-$env:APP_VERSION-arm64.apk"
|
run: ./build/ci/upload-release-asset.ps1 -Tag $env:RELTAG -Files "out/MediaBrowser-$env:APP_VERSION-arm64.apk"
|
||||||
|
|
||||||
# ── Linux 셸 (tar.gz self-contained — Cli + Avalonia, x64 네이티브 + arm64 교차) ──
|
# ── Linux 셸 (tar.gz self-contained — Cli + Avalonia, x64 네이티브 + arm64 교차) ──
|
||||||
linux:
|
linux:
|
||||||
|
|
@ -163,11 +163,11 @@ jobs:
|
||||||
for rid in linux-x64 linux-arm64; do
|
for rid in linux-x64 linux-arm64; do
|
||||||
dotnet publish VideoDownloader.Cli -c Release -r "$rid" --self-contained -o "out/cli-$rid"
|
dotnet publish VideoDownloader.Cli -c Release -r "$rid" --self-contained -o "out/cli-$rid"
|
||||||
dotnet publish VideoDownloader.Avalonia -c Release -r "$rid" --self-contained -o "out/avalonia-$rid"
|
dotnet publish VideoDownloader.Avalonia -c Release -r "$rid" --self-contained -o "out/avalonia-$rid"
|
||||||
tar -C out -czf "out/videodownloader-$rid.tar.gz" "cli-$rid" "avalonia-$rid"
|
tar -C out -czf "out/mediabrowser-$rid.tar.gz" "cli-$rid" "avalonia-$rid"
|
||||||
done
|
done
|
||||||
- name: 릴리스 업로드
|
- name: 릴리스 업로드
|
||||||
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
||||||
run: ./build/ci/upload-release-asset.sh "$RELTAG" out/videodownloader-linux-x64.tar.gz out/videodownloader-linux-arm64.tar.gz
|
run: ./build/ci/upload-release-asset.sh "$RELTAG" out/mediabrowser-linux-x64.tar.gz out/mediabrowser-linux-arm64.tar.gz
|
||||||
|
|
||||||
# ── macOS 셸 (tar.gz — arm64 네이티브 + x64 교차. 서명·공증은 D4, Apple Developer Program 후) ──
|
# ── macOS 셸 (tar.gz — arm64 네이티브 + x64 교차. 서명·공증은 D4, Apple Developer Program 후) ──
|
||||||
macos:
|
macos:
|
||||||
|
|
@ -201,8 +201,8 @@ jobs:
|
||||||
for rid in osx-arm64 osx-x64; do
|
for rid in osx-arm64 osx-x64; do
|
||||||
dotnet publish VideoDownloader.Cli -c Release -r "$rid" --self-contained -o "out/cli-$rid"
|
dotnet publish VideoDownloader.Cli -c Release -r "$rid" --self-contained -o "out/cli-$rid"
|
||||||
dotnet publish VideoDownloader.Avalonia -c Release -r "$rid" --self-contained -o "out/avalonia-$rid"
|
dotnet publish VideoDownloader.Avalonia -c Release -r "$rid" --self-contained -o "out/avalonia-$rid"
|
||||||
tar -C out -czf "out/videodownloader-$rid.tar.gz" "cli-$rid" "avalonia-$rid"
|
tar -C out -czf "out/mediabrowser-$rid.tar.gz" "cli-$rid" "avalonia-$rid"
|
||||||
done
|
done
|
||||||
- name: 릴리스 업로드
|
- name: 릴리스 업로드
|
||||||
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
env: { RELEASE_TOKEN: "${{ secrets.RELEASE_TOKEN }}" }
|
||||||
run: ./build/ci/upload-release-asset.sh "$RELTAG" out/videodownloader-osx-arm64.tar.gz out/videodownloader-osx-x64.tar.gz
|
run: ./build/ci/upload-release-asset.sh "$RELTAG" out/mediabrowser-osx-arm64.tar.gz out/mediabrowser-osx-x64.tar.gz
|
||||||
|
|
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: win-${{ matrix.arch }}
|
name: win-${{ matrix.arch }}
|
||||||
path: out/VideoDownloader-Setup-${{ matrix.arch }}.exe
|
path: out/MediaBrowser-Setup-${{ matrix.arch }}.exe
|
||||||
|
|
||||||
# ── Linux 셸 (tar.gz self-contained — Avalonia + Cli) ──
|
# ── Linux 셸 (tar.gz self-contained — Avalonia + Cli) ──
|
||||||
linux:
|
linux:
|
||||||
|
|
@ -74,11 +74,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
dotnet publish VideoDownloader.Cli -c Release -r ${{ matrix.rid }} --self-contained -o out/cli-${{ matrix.rid }}
|
dotnet publish VideoDownloader.Cli -c Release -r ${{ matrix.rid }} --self-contained -o out/cli-${{ matrix.rid }}
|
||||||
dotnet publish VideoDownloader.Avalonia -c Release -r ${{ matrix.rid }} --self-contained -o out/avalonia-${{ matrix.rid }}
|
dotnet publish VideoDownloader.Avalonia -c Release -r ${{ matrix.rid }} --self-contained -o out/avalonia-${{ matrix.rid }}
|
||||||
tar -C out -czf out/videodownloader-${{ matrix.rid }}.tar.gz cli-${{ matrix.rid }} avalonia-${{ matrix.rid }}
|
tar -C out -czf out/mediabrowser-${{ matrix.rid }}.tar.gz cli-${{ matrix.rid }} avalonia-${{ matrix.rid }}
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.rid }}
|
name: ${{ matrix.rid }}
|
||||||
path: out/videodownloader-${{ matrix.rid }}.tar.gz
|
path: out/mediabrowser-${{ matrix.rid }}.tar.gz
|
||||||
|
|
||||||
# ── macOS 셸 (tar.gz — D4 서명·공증은 Developer ID 시크릿 등록 후 확장) ──
|
# ── macOS 셸 (tar.gz — D4 서명·공증은 Developer ID 시크릿 등록 후 확장) ──
|
||||||
macos:
|
macos:
|
||||||
|
|
@ -100,12 +100,12 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
dotnet publish VideoDownloader.Cli -c Release -r ${{ matrix.rid }} --self-contained -o out/cli-${{ matrix.rid }}
|
dotnet publish VideoDownloader.Cli -c Release -r ${{ matrix.rid }} --self-contained -o out/cli-${{ matrix.rid }}
|
||||||
dotnet publish VideoDownloader.Avalonia -c Release -r ${{ matrix.rid }} --self-contained -o out/avalonia-${{ matrix.rid }}
|
dotnet publish VideoDownloader.Avalonia -c Release -r ${{ matrix.rid }} --self-contained -o out/avalonia-${{ matrix.rid }}
|
||||||
tar -C out -czf out/videodownloader-${{ matrix.rid }}.tar.gz cli-${{ matrix.rid }} avalonia-${{ matrix.rid }}
|
tar -C out -czf out/mediabrowser-${{ matrix.rid }}.tar.gz cli-${{ matrix.rid }} avalonia-${{ matrix.rid }}
|
||||||
# D4: MACOS_P12/KEYCHAIN_PASS/NOTARY_PROFILE 시크릿 등록 시 codesign+notarytool+DMG 스텝 추가
|
# D4: MACOS_P12/KEYCHAIN_PASS/NOTARY_PROFILE 시크릿 등록 시 codesign+notarytool+DMG 스텝 추가
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.rid }}
|
name: ${{ matrix.rid }}
|
||||||
path: out/videodownloader-${{ matrix.rid }}.tar.gz
|
path: out/mediabrowser-${{ matrix.rid }}.tar.gz
|
||||||
|
|
||||||
# ── Android APK (D2 — 서명 키스토어 시크릿 없으면 미서명 APK) ──
|
# ── Android APK (D2 — 서명 키스토어 시크릿 없으면 미서명 APK) ──
|
||||||
android:
|
android:
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ dotnet build VideoDownloader.Mobile -f net10.0-android
|
||||||
## 배포
|
## 배포
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Windows 설치자 (x64 + arm64) → out/VideoDownloader-Setup-{arch}.exe
|
# Windows 설치자 (x64 + arm64) → out/MediaBrowser-Setup-{arch}.exe
|
||||||
./build/publish.ps1 -Config Release -Version 1.0.0
|
./build/publish.ps1 -Config Release -Version 1.0.0
|
||||||
|
|
||||||
# macOS / Linux 원격 빌드 (.env 의 빌드머신 자격 증명 사용) → out/remote/<rid>/
|
# macOS / Linux 원격 빌드 (.env 의 빌드머신 자격 증명 사용) → out/remote/<rid>/
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ public sealed class PairingWindow : Window
|
||||||
{
|
{
|
||||||
public PairingWindow(ServerHost server, string deviceName)
|
public PairingWindow(ServerHost server, string deviceName)
|
||||||
{
|
{
|
||||||
Title = "기기 연결 — Video Downloader";
|
Title = "기기 연결 - MediaBrowser";
|
||||||
Width = 420; Height = 560;
|
Width = 420; Height = 560;
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
SetResourceReference(FontFamilyProperty, "UiFont"); // 암시 스타일 폰트 강제 제거 후 창 루트 상속으로 통일
|
||||||
|
|
||||||
Background = System.Windows.Media.Brushes.White;
|
Background = System.Windows.Media.Brushes.White;
|
||||||
|
|
||||||
var lanIp = GetLanIp();
|
var lanIp = GetLanIp();
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,11 @@ public sealed class PlayerWindow : Window
|
||||||
_config = config;
|
_config = config;
|
||||||
_playlist = playlist;
|
_playlist = playlist;
|
||||||
|
|
||||||
Title = "재생목록 — Video Downloader";
|
Title = "재생목록 - MediaBrowser";
|
||||||
Width = 900; Height = 620;
|
Width = 900; Height = 620;
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
SetResourceReference(FontFamilyProperty, "UiFont"); // 암시 스타일 폰트 강제 제거 후 창 루트 상속으로 통일
|
||||||
|
|
||||||
|
|
||||||
var bar = new Border { Background = System.Windows.Media.Brushes.Black, Padding = new Thickness(8, 6, 8, 6) };
|
var bar = new Border { Background = System.Windows.Media.Brushes.Black, Padding = new Thickness(8, 6, 8, 6) };
|
||||||
var dock = new DockPanel();
|
var dock = new DockPanel();
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ public partial class MainWindow
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var current = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
var current = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||||
var result = await new VideoDownloader.Core.Update.UpdateChecker().CheckAsync(repo, current, "win-x64");
|
var result = await new VideoDownloader.Core.Update.UpdateChecker().CheckAsync(repo, current, "win-x64", _config.UpdateApiBase);
|
||||||
if (result.IsNewer)
|
if (result.IsNewer)
|
||||||
{
|
{
|
||||||
UpdateBadge.Visibility = Visibility.Visible;
|
UpdateBadge.Visibility = Visibility.Visible;
|
||||||
|
|
@ -656,7 +656,7 @@ public partial class MainWindow
|
||||||
private void LibraryViewToggle_Click(object sender, RoutedEventArgs e)
|
private void LibraryViewToggle_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_libraryGrid = !_libraryGrid;
|
_libraryGrid = !_libraryGrid;
|
||||||
LibraryViewToggle.Content = _libraryGrid ? "\uE8FD; 그리드" : "\uE71B; 리스트";
|
LibraryViewToggle.Content = _libraryGrid ? "그리드" : "리스트";
|
||||||
RefreshLibrary();
|
RefreshLibrary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
xmlns:feat="clr-namespace:VideoDownloader.App.Browser.Features"
|
xmlns:feat="clr-namespace:VideoDownloader.App.Browser.Features"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
FontFamily="{DynamicResource UiFont}"
|
FontFamily="{DynamicResource UiFont}"
|
||||||
Title="Video Downloader" Height="820" Width="1300"
|
Title="MediaBrowser" Height="820" Width="1300"
|
||||||
Background="{DynamicResource WindowBgGradient}"
|
Background="{DynamicResource WindowBgGradient}"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
UseLayoutRounding="True"
|
UseLayoutRounding="True"
|
||||||
ToolTipService.InitialShowDelay="400"
|
ToolTipService.InitialShowDelay="400"
|
||||||
ToolTipService.ShowDuration="8000"
|
ToolTipService.ShowDuration="8000"
|
||||||
AutomationProperties.Name="Video Downloader 메인 창"
|
AutomationProperties.Name="MediaBrowser 메인 창"
|
||||||
WindowStyle="None" ResizeMode="CanResize">
|
WindowStyle="None" ResizeMode="CanResize">
|
||||||
|
|
||||||
<!-- 보더리스: WindowChrome 이 실제 엣지 리사이즈 + 올바른 최대화(작업표시줄 존중) + DWM 그림자 담당 -->
|
<!-- 보더리스: WindowChrome 이 실제 엣지 리사이즈 + 올바른 최대화(작업표시줄 존중) + DWM 그림자 담당 -->
|
||||||
|
|
@ -273,7 +273,7 @@
|
||||||
<Border Width="26" Height="26" CornerRadius="7" Background="{DynamicResource AccentGradient}">
|
<Border Width="26" Height="26" CornerRadius="7" Background="{DynamicResource AccentGradient}">
|
||||||
<TextBlock Text="" Style="{StaticResource FluentIcon}" Foreground="White" FontSize="14"/>
|
<TextBlock Text="" Style="{StaticResource FluentIcon}" Foreground="White" FontSize="14"/>
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Text="Video Downloader" Foreground="{DynamicResource TextPrimary}"
|
<TextBlock Text="MediaBrowser" Foreground="{DynamicResource TextPrimary}"
|
||||||
FontWeight="SemiBold" FontSize="13.5" VerticalAlignment="Center" Margin="9,0,0,0"/>
|
FontWeight="SemiBold" FontSize="13.5" VerticalAlignment="Center" Margin="9,0,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
@ -389,7 +389,7 @@
|
||||||
Background="Transparent" BorderThickness="0" Padding="0"
|
Background="Transparent" BorderThickness="0" Padding="0"
|
||||||
AutomationProperties.Name="클립보드 링크 받기 제안">
|
AutomationProperties.Name="클립보드 링크 받기 제안">
|
||||||
<Border Background="{DynamicResource AccentFadeBrush}" CornerRadius="9" Padding="9,3">
|
<Border Background="{DynamicResource AccentFadeBrush}" CornerRadius="9" Padding="9,3">
|
||||||
<TextBlock Text=" 링크 붙여넣고 받기" FontSize="10.5" Foreground="{DynamicResource TextPrimary}"/>
|
<TextBlock Text="링크 붙여넣고 받기" FontSize="10.5" Foreground="{DynamicResource TextPrimary}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -500,7 +500,7 @@
|
||||||
Foreground="{DynamicResource TextMuted}" Visibility="Collapsed"
|
Foreground="{DynamicResource TextMuted}" Visibility="Collapsed"
|
||||||
AutomationProperties.Name="잠긴 탭"/>
|
AutomationProperties.Name="잠긴 탭"/>
|
||||||
<Button DockPanel.Dock="Right" Style="{StaticResource CardActionButton}"
|
<Button DockPanel.Dock="Right" Style="{StaticResource CardActionButton}"
|
||||||
Content="" FontSize="9" Padding="5,3" Margin="4,0,0,0"
|
Content="" FontFamily="{DynamicResource IconFont}" FontSize="9" Padding="5,3" Margin="4,0,0,0"
|
||||||
Click="TabClose_Click" ToolTip="탭 닫기 (Ctrl+W)"
|
Click="TabClose_Click" ToolTip="탭 닫기 (Ctrl+W)"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
AutomationProperties.Name="탭 닫기"/>
|
AutomationProperties.Name="탭 닫기"/>
|
||||||
|
|
@ -676,9 +676,9 @@
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<!-- UC-D06/D07 모두 일시정지/재개 -->
|
<!-- UC-D06/D07 모두 일시정지/재개 -->
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||||
<Button Style="{StaticResource CardActionButton}" Content=" 모두 일시정지" Click="PauseAllDownloads_Click" Margin="0,0,5,0"
|
<Button Style="{StaticResource CardActionButton}" Content="모두 일시정지" Click="PauseAllDownloads_Click" Margin="0,0,5,0"
|
||||||
AutomationProperties.Name="모두 일시정지"/>
|
AutomationProperties.Name="모두 일시정지"/>
|
||||||
<Button Style="{StaticResource CardActionButton}" Content=" 모두 재개" Click="ResumeAllDownloads_Click" Margin="0,0,5,0"
|
<Button Style="{StaticResource CardActionButton}" Content="모두 재개" Click="ResumeAllDownloads_Click" Margin="0,0,5,0"
|
||||||
AutomationProperties.Name="모두 재개"/>
|
AutomationProperties.Name="모두 재개"/>
|
||||||
<Button Style="{StaticResource CardActionButton}" Content="CSV" Click="ExportDownloads_Click"
|
<Button Style="{StaticResource CardActionButton}" Content="CSV" Click="ExportDownloads_Click"
|
||||||
ToolTip="다운로드 목록 CSV 내보내기" AutomationProperties.Name="목록 CSV 내보내기"/>
|
ToolTip="다운로드 목록 CSV 내보내기" AutomationProperties.Name="목록 CSV 내보내기"/>
|
||||||
|
|
@ -762,7 +762,7 @@
|
||||||
Background="{DynamicResource CardBg}" BorderBrush="{DynamicResource ErrorBrush}" BorderThickness="1"
|
Background="{DynamicResource CardBg}" BorderBrush="{DynamicResource ErrorBrush}" BorderThickness="1"
|
||||||
CornerRadius="10" Padding="12,8" Visibility="Collapsed" Panel.ZIndex="41">
|
CornerRadius="10" Padding="12,8" Visibility="Collapsed" Panel.ZIndex="41">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Button DockPanel.Dock="Right" Style="{StaticResource CardActionButton}" Content="" Click="ErrorBanner_Close"
|
<Button DockPanel.Dock="Right" Style="{StaticResource CardActionButton}" Content="" FontFamily="{DynamicResource IconFont}" Click="ErrorBanner_Close"
|
||||||
AutomationProperties.Name="오류 배너 닫기"/>
|
AutomationProperties.Name="오류 배너 닫기"/>
|
||||||
<Expander x:Name="ErrorDetailExpander" Header="오류가 발생했습니다 — 펼쳐서 상세 보기"
|
<Expander x:Name="ErrorDetailExpander" Header="오류가 발생했습니다 — 펼쳐서 상세 보기"
|
||||||
Foreground="{DynamicResource ErrorBrush}" FontSize="12"
|
Foreground="{DynamicResource ErrorBrush}" FontSize="12"
|
||||||
|
|
@ -778,7 +778,7 @@
|
||||||
<Border Style="{StaticResource EmptyStateStyle}" Background="{DynamicResource CardBg}" MaxWidth="460"
|
<Border Style="{StaticResource EmptyStateStyle}" Background="{DynamicResource CardBg}" MaxWidth="460"
|
||||||
VerticalAlignment="Center" HorizontalAlignment="Center" Padding="28">
|
VerticalAlignment="Center" HorizontalAlignment="Center" Padding="28">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="Video Downloader 에 오세요" Style="{StaticResource Type.H1}" Margin="0,0,0,8"/>
|
<TextBlock Text="MediaBrowser 에 오세요" Style="{StaticResource Type.H1}" Margin="0,0,0,8"/>
|
||||||
<TextBlock Style="{StaticResource Type.Body}"
|
<TextBlock Style="{StaticResource Type.Body}"
|
||||||
Text="영상을 재생하면 자동으로 감지해 오른쪽 패널에 카드가 생깁니다. 받기 버튼으로 저장하고, 휴대폰 QR 페어링으로 밖에서도 볼 수 있어요."/>
|
Text="영상을 재생하면 자동으로 감지해 오른쪽 패널에 카드가 생깁니다. 받기 버튼으로 저장하고, 휴대폰 QR 페어링으로 밖에서도 볼 수 있어요."/>
|
||||||
<TextBlock Style="{StaticResource Type.Caption}" Margin="0,10,0,0"
|
<TextBlock Style="{StaticResource Type.Caption}" Margin="0,10,0,0"
|
||||||
|
|
@ -1073,7 +1073,7 @@
|
||||||
<TextBlock Text="미디어 재생목록" Foreground="{DynamicResource TextPrimary}" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
<TextBlock Text="미디어 재생목록" Foreground="{DynamicResource TextPrimary}" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<!-- UC-M11 셔플 -->
|
<!-- UC-M11 셔플 -->
|
||||||
<Button x:Name="PlaylistShuffle" Style="{StaticResource CardActionButton}" Content=" 셔플" Click="PlaylistShuffle_Click" Margin="0,0,5,0"
|
<Button x:Name="PlaylistShuffle" Style="{StaticResource CardActionButton}" Content="셔플" Click="PlaylistShuffle_Click" Margin="0,0,5,0"
|
||||||
AutomationProperties.Name="재생목록 셔플"/>
|
AutomationProperties.Name="재생목록 셔플"/>
|
||||||
<!-- UC-M12 반복 모드 -->
|
<!-- UC-M12 반복 모드 -->
|
||||||
<Button x:Name="PlaylistLoop" Style="{StaticResource CardActionButton}" Content="반복 끔" Click="PlaylistLoop_Click" Margin="0,0,5,0"
|
<Button x:Name="PlaylistLoop" Style="{StaticResource CardActionButton}" Content="반복 끔" Click="PlaylistLoop_Click" Margin="0,0,5,0"
|
||||||
|
|
@ -1158,7 +1158,7 @@
|
||||||
<TextBlock Text="라이브러리" Style="{StaticResource Type.H1}" FontSize="15" VerticalAlignment="Center"/>
|
<TextBlock Text="라이브러리" Style="{StaticResource Type.H1}" FontSize="15" VerticalAlignment="Center"/>
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<!-- UC-M02 그리드/리스트 전환 -->
|
<!-- UC-M02 그리드/리스트 전환 -->
|
||||||
<Button x:Name="LibraryViewToggle" Style="{StaticResource CardActionButton}" Content=" 그리드" Click="LibraryViewToggle_Click"
|
<Button x:Name="LibraryViewToggle" Style="{StaticResource CardActionButton}" Content="그리드" Click="LibraryViewToggle_Click"
|
||||||
Margin="0,0,6,0" AutomationProperties.Name="라이브러리 보기 전환"/>
|
Margin="0,0,6,0" AutomationProperties.Name="라이브러리 보기 전환"/>
|
||||||
<!-- UC-M04 정렬 메뉴 -->
|
<!-- UC-M04 정렬 메뉴 -->
|
||||||
<Menu x:Name="LibrarySortMenu" Background="Transparent">
|
<Menu x:Name="LibrarySortMenu" Background="Transparent">
|
||||||
|
|
|
||||||
|
|
@ -2208,7 +2208,7 @@ public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
public MainWindowAutomationPeer(Window owner) : base(owner) { }
|
public MainWindowAutomationPeer(Window owner) : base(owner) { }
|
||||||
|
|
||||||
protected override string GetNameCore() => "Video Downloader 메인 창";
|
protected override string GetNameCore() => "MediaBrowser 메인 창";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public partial class SettingsWindow : Window
|
||||||
_sections.Add(SectionAbout);
|
_sections.Add(SectionAbout);
|
||||||
|
|
||||||
// UC-S20 버전 정보
|
// UC-S20 버전 정보
|
||||||
VersionText.Text = $"Video Downloader {CurrentVersion()} · {MainWindow.ProfileFolder}";
|
VersionText.Text = $"MediaBrowser {CurrentVersion()} · {MainWindow.ProfileFolder}";
|
||||||
|
|
||||||
// UC-S08 Ctrl+S 저장
|
// UC-S08 Ctrl+S 저장
|
||||||
PreviewKeyDown += (_, e) =>
|
PreviewKeyDown += (_, e) =>
|
||||||
|
|
@ -257,7 +257,7 @@ public partial class SettingsWindow : Window
|
||||||
{
|
{
|
||||||
var current = CurrentVersion();
|
var current = CurrentVersion();
|
||||||
var rid = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64";
|
var rid = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "win-arm64" : "win-x64";
|
||||||
var result = await new VideoDownloader.Core.Update.UpdateChecker().CheckAsync(repo, current, rid);
|
var result = await new VideoDownloader.Core.Update.UpdateChecker().CheckAsync(repo, current, rid, Config.UpdateApiBase);
|
||||||
if (result.IsNewer)
|
if (result.IsNewer)
|
||||||
{
|
{
|
||||||
UpdateResultText.Text = $"새 버전 {result.LatestVersion} (현재 {current})";
|
UpdateResultText.Text = $"새 버전 {result.LatestVersion} (현재 {current})";
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,10 @@
|
||||||
<SolidColorBrush Color="#E91E8C"/>
|
<SolidColorBrush Color="#E91E8C"/>
|
||||||
</x:Array>
|
</x:Array>
|
||||||
|
|
||||||
<!-- 암시 스타일: 모든 텍스트가 UI 폰트로 통일(Foreground 는 상속 존중) -->
|
<!-- 암시 스타일: FontFamily 를 여기서 강제하면 버튼 ContentPresenter 의 자동 TextBlock 이
|
||||||
|
IconFont 상속을 잃어 모든 아이콘 글리프가 tofu(□)가 된다(2026-08-18 실결함).
|
||||||
|
UI 폰트 통일은 각 창 루트의 FontFamily="{DynamicResource UiFont}" 상속으로 해결한다. -->
|
||||||
<Style TargetType="TextBlock">
|
<Style TargetType="TextBlock">
|
||||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
|
||||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ToolTip">
|
<Style TargetType="ToolTip">
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,12 @@ public class AppConfig
|
||||||
public bool RemoteAccessEnabled { get; set; } = false;
|
public bool RemoteAccessEnabled { get; set; } = false;
|
||||||
public string? RemoteAccessToken { get; set; }
|
public string? RemoteAccessToken { get; set; }
|
||||||
|
|
||||||
/// <summary>업데이트 확인 대상 GitHub 저장소("owner/repo") — DEPLOYMENT_PLAN §6 v1. 비면 확인 안 함.</summary>
|
/// <summary>업데이트 확인 대상 저장소("owner/repo") — DEPLOYMENT_PLAN §6 v1. 비면 확인 안 함.
|
||||||
public string UpdateRepo { get; set; } = "";
|
/// 기본은 자체 Forgejo 공개 릴리스 저장소(§5-bis).</summary>
|
||||||
|
public string UpdateRepo { get; set; } = "Video-Downloader/releases";
|
||||||
|
|
||||||
|
/// <summary>업데이트 채널 API 베이스. Forgejo 는 /api/v1 접두 포함, GitHub 는 "https://api.github.com".</summary>
|
||||||
|
public string UpdateApiBase { get; set; } = Update.UpdateChecker.DefaultApiBase;
|
||||||
|
|
||||||
// ==================== UI 복잡 버전 (docs/UI_USECASES.md) ====================
|
// ==================== UI 복잡 버전 (docs/UI_USECASES.md) ====================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,22 @@ using System.Text.Json;
|
||||||
|
|
||||||
namespace VideoDownloader.Core.Update;
|
namespace VideoDownloader.Core.Update;
|
||||||
|
|
||||||
/// <summary>GitHub 릴리스의 설치 자산 하나.</summary>
|
/// <summary>릴리스의 설치 자산 하나.</summary>
|
||||||
public sealed record UpdateAsset(string Name, string DownloadUrl);
|
public sealed record UpdateAsset(string Name, string DownloadUrl);
|
||||||
|
|
||||||
/// <summary>업데이트 확인 결과. LatestVersion 은 태그의 v 접두를 뗀 정규형.</summary>
|
/// <summary>업데이트 확인 결과. LatestVersion 은 태그의 v 접두를 뗀 정규형.</summary>
|
||||||
public sealed record UpdateCheckResult(bool IsNewer, string CurrentVersion, string LatestVersion, string ReleaseUrl, UpdateAsset? Asset);
|
public sealed record UpdateCheckResult(bool IsNewer, string CurrentVersion, string LatestVersion, string ReleaseUrl, UpdateAsset? Asset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DEPLOYMENT_PLAN §6 자동 업데이트 v1 — GitHub Releases latest 태그 폴링(무의존·전 OS 공통).
|
/// DEPLOYMENT_PLAN §6 자동 업데이트 v1 — Releases latest 태그 폴링(무의존·전 OS 공통).
|
||||||
/// 설치를 자동 적용하지 않고 릴리스/자산 URL 만 돌려준다(설치는 사람 관문).
|
/// 기본 채널은 자체 Forgejo 공개 릴리스 저장소(§5-bis)이고, apiBase 로 GitHub 등 다른 채널도 쓸 수 있다
|
||||||
|
/// (Forgejo 와 GitHub 의 releases/latest 응답 형태가 동일). 설치는 사람 관문 — URL 만 돌려준다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateChecker
|
public sealed class UpdateChecker
|
||||||
{
|
{
|
||||||
|
/// <summary>기본 업데이트 채널 API 베이스 — Forgejo 는 /api/v1 접두 포함.</summary>
|
||||||
|
public const string DefaultApiBase = "https://git.chanpaca.net/api/v1";
|
||||||
|
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
public UpdateChecker(HttpMessageHandler? handler = null)
|
public UpdateChecker(HttpMessageHandler? handler = null)
|
||||||
|
|
@ -49,7 +53,7 @@ public sealed class UpdateChecker
|
||||||
{
|
{
|
||||||
"win" => name.EndsWith(".exe") || name.EndsWith(".msi"),
|
"win" => name.EndsWith(".exe") || name.EndsWith(".msi"),
|
||||||
"linux" => name.EndsWith(".tar.gz") || name.EndsWith(".appimage") || name.EndsWith(".deb"),
|
"linux" => name.EndsWith(".tar.gz") || name.EndsWith(".appimage") || name.EndsWith(".deb"),
|
||||||
"osx" => name.EndsWith(".dmg"),
|
"osx" => name.EndsWith(".dmg") || name.EndsWith(".tar.gz"), // 프리뷰는 tar.gz 배포(§5-bis), 정식 서명판은 dmg
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if (osMatch) return asset;
|
if (osMatch) return asset;
|
||||||
|
|
@ -57,10 +61,13 @@ public sealed class UpdateChecker
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>repo("owner/repo")의 latest 릴리스를 조회한다. API 실패는 HttpRequestException 으로 드러난다.</summary>
|
/// <summary>
|
||||||
public async Task<UpdateCheckResult> CheckAsync(string repo, string currentVersion, string rid, CancellationToken ct = default)
|
/// repo("owner/repo")의 latest 릴리스를 조회한다. API 실패는 HttpRequestException 으로 드러난다.
|
||||||
|
/// apiBase 미지정이면 기본 채널(자체 Forgejo). GitHub 는 "https://api.github.com" 을 넘긴다.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UpdateCheckResult> CheckAsync(string repo, string currentVersion, string rid, string? apiBase = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var res = await _http.GetAsync($"https://api.github.com/repos/{repo}/releases/latest", ct);
|
using var res = await _http.GetAsync($"{(apiBase ?? DefaultApiBase).TrimEnd('/')}/repos/{repo}/releases/latest", ct);
|
||||||
res.EnsureSuccessStatusCode();
|
res.EnsureSuccessStatusCode();
|
||||||
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync(ct));
|
using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync(ct));
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<MauiXamlInflator>SourceGen</MauiXamlInflator>
|
<MauiXamlInflator>SourceGen</MauiXamlInflator>
|
||||||
|
|
||||||
<!-- Display name -->
|
<!-- Display name -->
|
||||||
<ApplicationTitle>VideoDownloader.Mobile</ApplicationTitle>
|
<ApplicationTitle>MediaBrowser</ApplicationTitle>
|
||||||
|
|
||||||
<!-- App Identifier -->
|
<!-- App Identifier -->
|
||||||
<ApplicationId>com.companyname.videodownloader.mobile</ApplicationId>
|
<ApplicationId>com.companyname.videodownloader.mobile</ApplicationId>
|
||||||
|
|
|
||||||
71
VideoDownloader.Tests/E2E/IconContentFontTests.cs
Normal file
71
VideoDownloader.Tests/E2E/IconContentFontTests.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using VideoDownloader.App;
|
||||||
|
using VideoDownloader.Tests.Infrastructure;
|
||||||
|
|
||||||
|
namespace VideoDownloader.Tests.E2E;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 아이콘 tofu 실결함(사용자 보고 "아이콘은 절대 안고쳐지네") 회귀 테스트 —
|
||||||
|
/// 근본 원인: Shared.xaml 의 암시적 TextBlock 스타일이 FontFamily 를 UiFont 로 강제해,
|
||||||
|
/// 버튼 ContentPresenter 가 만드는 자동 TextBlock 이 IconFont 상속을 잃고 PUA 글리프가 notdef 가 됐다.
|
||||||
|
/// 아이콘 버튼의 실제 콘텐츠 TextBlock 이 아이콘 폰트로 해석되는지 실창에서 검증한다.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("E2E")]
|
||||||
|
public class IconContentFontTests
|
||||||
|
{
|
||||||
|
private static TextBlock? FindTextBlock(DependencyObject root)
|
||||||
|
{
|
||||||
|
if (root is TextBlock tb) return tb;
|
||||||
|
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
|
||||||
|
{
|
||||||
|
if (FindTextBlock(VisualTreeHelper.GetChild(root, i)) is { } found) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task 아이콘_버튼의_콘텐츠_TextBlock_은_아이콘폰트로_렌더된다()
|
||||||
|
{
|
||||||
|
var dataDir = Path.Combine(Path.GetTempPath(), $"vd_iconfont_{Guid.NewGuid():N}");
|
||||||
|
Environment.SetEnvironmentVariable("VD_DATA_DIR", dataDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var sta = StaDispatcher.Start();
|
||||||
|
WpfTestApp.Ensure(sta.Dispatcher);
|
||||||
|
var (navFamily, captionFamily, bodyFamily) = await sta.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
// 앱과 동일 조건: 절대 pack URI 테마 재로드 후 실창
|
||||||
|
VideoDownloader.App.App.ApplyTheme("dark");
|
||||||
|
var win = new MainWindow { Left = 20, Top = 700, WindowStartupLocation = WindowStartupLocation.Manual };
|
||||||
|
win.Show();
|
||||||
|
win.UpdateLayout();
|
||||||
|
|
||||||
|
var nav = (Button)win.FindName("NavBack")!;
|
||||||
|
var navText = FindTextBlock(nav)!;
|
||||||
|
|
||||||
|
// 캡션(최대화/복원) 버튼 — CaptionButton 스타일 경유
|
||||||
|
var caption = win.FindName("MaxRestoreBtn") as Button;
|
||||||
|
var captionText = caption != null ? FindTextBlock(caption) : null;
|
||||||
|
|
||||||
|
// 일반 본문 TextBlock 은 여전히 UI 폰트를 따라야 한다(암시 스타일의 원래 목적)
|
||||||
|
var status = (TextBlock)win.FindName("StatusText")!;
|
||||||
|
|
||||||
|
var result = (navText.FontFamily.Source, captionText?.FontFamily.Source ?? "(캡션 버튼 없음)", status.FontFamily.Source);
|
||||||
|
win.Close();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("Segoe Fluent Icons", navFamily);
|
||||||
|
if (captionFamily != "(캡션 버튼 없음)")
|
||||||
|
Assert.Equal("Segoe Fluent Icons", captionFamily);
|
||||||
|
Assert.Contains("Pretendard", bodyFamily); // 본문은 UI 폰트 유지
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("VD_DATA_DIR", null);
|
||||||
|
try { Directory.Delete(dataDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
VideoDownloader.Tests/E2E/IconGlyphRenderTests.cs
Normal file
116
VideoDownloader.Tests/E2E/IconGlyphRenderTests.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using VideoDownloader.Tests.Infrastructure;
|
||||||
|
|
||||||
|
namespace VideoDownloader.Tests.E2E;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 아이콘 tofu 실결함(사용자 보고: "아이콘은 절대 안고쳐지네") 재현 시도 —
|
||||||
|
/// 테마 사전을 앱과 같은 방식으로 로드했을 때 FluentIcon/IconButton 경유 글리프가
|
||||||
|
/// 명시적 Segoe Fluent Icons 렌더와 픽셀 단위로 같은지 검증한다.
|
||||||
|
/// 같지 않고 본문 폰트 렌더(tofu)와 같다면 리소스 해석이 깨진 것이다.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("E2E")]
|
||||||
|
public class IconGlyphRenderTests
|
||||||
|
{
|
||||||
|
private static byte[] Render(FrameworkElement el)
|
||||||
|
{
|
||||||
|
el.Measure(new Size(48, 48));
|
||||||
|
el.Arrange(new Rect(0, 0, 48, 48));
|
||||||
|
el.UpdateLayout();
|
||||||
|
var rtb = new RenderTargetBitmap(48, 48, 96, 96, PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(el);
|
||||||
|
var px = new byte[48 * 48 * 4];
|
||||||
|
rtb.CopyPixels(px, 48 * 4, 0);
|
||||||
|
return px;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Lit(byte[] px)
|
||||||
|
{
|
||||||
|
var lit = 0;
|
||||||
|
for (var i = 3; i < px.Length; i += 4) if (px[i] > 60) lit++;
|
||||||
|
return lit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FluentIcon_스타일_글리프는_명시적_아이콘폰트_렌더와_동일하다()
|
||||||
|
{
|
||||||
|
using var sta = StaDispatcher.Start();
|
||||||
|
WpfTestApp.Ensure(sta.Dispatcher);
|
||||||
|
var (family, litStyled, litExplicit, litBody, sameAsExplicit, sameAsBody) = await sta.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
TextBlock Make() => new()
|
||||||
|
{
|
||||||
|
Text = "", FontSize = 14, Foreground = Brushes.White,
|
||||||
|
Background = Brushes.Black, Width = 48, Height = 48,
|
||||||
|
};
|
||||||
|
|
||||||
|
var styled = Make();
|
||||||
|
styled.Style = (Style)Application.Current.FindResource("FluentIcon");
|
||||||
|
|
||||||
|
var explicitIcon = Make();
|
||||||
|
explicitIcon.FontFamily = new FontFamily("Segoe Fluent Icons");
|
||||||
|
|
||||||
|
var body = Make();
|
||||||
|
body.FontFamily = (FontFamily)Application.Current.FindResource("UiFont");
|
||||||
|
|
||||||
|
var pStyled = Render(styled);
|
||||||
|
var pExplicit = Render(explicitIcon);
|
||||||
|
var pBody = Render(body);
|
||||||
|
|
||||||
|
return (styled.FontFamily.Source, Lit(pStyled), Lit(pExplicit), Lit(pBody),
|
||||||
|
pStyled.SequenceEqual(pExplicit), pStyled.SequenceEqual(pBody));
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(litExplicit > 0, "기준(명시적 Segoe Fluent Icons) 렌더 자체가 비어 있음 — 폰트 미설치?");
|
||||||
|
Assert.True(sameAsExplicit && !sameAsBody,
|
||||||
|
$"FluentIcon 경유 글리프가 아이콘 폰트로 렌더되지 않음 — 해석된 폰트: '{family}', " +
|
||||||
|
$"픽셀(스타일/명시/본문): {litStyled}/{litExplicit}/{litBody}, 본문과 동일: {sameAsBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task 앱_시작_시퀀스를_그대로_태워도_FluentIcon_글리프가_렌더된다()
|
||||||
|
{
|
||||||
|
// 실제 App.OnStartup 과 동일한 순서: ApplyTheme(절대 pack URI 재로드) → 강조색 → 폰트 → 모션
|
||||||
|
var dataDir = Path.Combine(Path.GetTempPath(), $"vd_icon_{Guid.NewGuid():N}");
|
||||||
|
Environment.SetEnvironmentVariable("VD_DATA_DIR", dataDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var sta = StaDispatcher.Start();
|
||||||
|
WpfTestApp.Ensure(sta.Dispatcher);
|
||||||
|
var (family, same, litStyled, litExplicit) = await sta.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
VideoDownloader.App.App.ApplyTheme("dark");
|
||||||
|
VideoDownloader.App.App.ApplyAccent(null);
|
||||||
|
VideoDownloader.App.App.ApplyFontSettings();
|
||||||
|
VideoDownloader.App.App.ApplyReduceMotion(false);
|
||||||
|
|
||||||
|
var styled = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\uE76B", FontSize = 14, Foreground = Brushes.White,
|
||||||
|
Background = Brushes.Black, Width = 48, Height = 48,
|
||||||
|
Style = (Style)Application.Current.FindResource("FluentIcon"),
|
||||||
|
};
|
||||||
|
var explicitIcon = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\uE76B", FontSize = 14, Foreground = Brushes.White,
|
||||||
|
Background = Brushes.Black, Width = 48, Height = 48,
|
||||||
|
FontFamily = new FontFamily("Segoe Fluent Icons"),
|
||||||
|
};
|
||||||
|
var pStyled = Render(styled);
|
||||||
|
var pExplicit = Render(explicitIcon);
|
||||||
|
return (styled.FontFamily.Source, pStyled.SequenceEqual(pExplicit), Lit(pStyled), Lit(pExplicit));
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(same,
|
||||||
|
$"앱 시작 시퀀스 후 FluentIcon 글리프 깨짐 — 해석된 폰트: '{family}', 픽셀(스타일/명시): {litStyled}/{litExplicit}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("VD_DATA_DIR", null);
|
||||||
|
try { Directory.Delete(dataDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
VideoDownloader.Tests/Infrastructure/IconGlyphXamlTests.cs
Normal file
89
VideoDownloader.Tests/Infrastructure/IconGlyphXamlTests.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace VideoDownloader.Tests.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 아이콘 글리프(PUA, Segoe Fluent Icons)를 본문 폰트 요소에 넣으면 tofu(□)로 렌더된다
|
||||||
|
/// (2026-08-18 실행 캡처로 확인 — "□ 모두 일시정지"). XAML/코드 정적 검사로 재발을 막는다.
|
||||||
|
/// 규칙:
|
||||||
|
/// ① PUA 를 쓰는 Content/Text 는 IconFont 보장 스타일(IconFont 를 설정하는 스타일 또는 그 BasedOn 파생)에
|
||||||
|
/// 있거나 요소에 FontFamily=IconFont 를 명시한다.
|
||||||
|
/// ② 한 문자열에 PUA 와 일반 텍스트를 섞지 않는다 — 폰트가 하나뿐이라 어느 한쪽은 반드시 깨진다.
|
||||||
|
/// </summary>
|
||||||
|
public class IconGlyphXamlTests
|
||||||
|
{
|
||||||
|
private static readonly Regex PuaEntity = new(@"&#x(E[0-9A-Fa-f]{3});", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>Shared.xaml 에서 IconFont 를 보장하는 스타일 키 집합(BasedOn 파생 포함).</summary>
|
||||||
|
private static HashSet<string> IconFontStyles()
|
||||||
|
{
|
||||||
|
var shared = SourceTree.SharedXaml;
|
||||||
|
var direct = new HashSet<string>();
|
||||||
|
var basedOn = new Dictionary<string, string>();
|
||||||
|
foreach (Match style in Regex.Matches(shared, "<Style x:Key=\"(\\w+)\"([^>]*)>(.*?)</Style>", RegexOptions.Singleline))
|
||||||
|
{
|
||||||
|
var key = style.Groups[1].Value;
|
||||||
|
if (style.Groups[3].Value.Contains("IconFont")) direct.Add(key);
|
||||||
|
var b = Regex.Match(style.Groups[2].Value, "BasedOn=\"\\{StaticResource (\\w+)\\}\"");
|
||||||
|
if (b.Success) basedOn[key] = b.Groups[1].Value;
|
||||||
|
}
|
||||||
|
// BasedOn 체인 전개 (파생이 부모의 FontFamily 세터를 상속)
|
||||||
|
bool grew;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
grew = false;
|
||||||
|
foreach (var (child, parent) in basedOn)
|
||||||
|
if (!direct.Contains(child) && direct.Contains(parent)) { direct.Add(child); grew = true; }
|
||||||
|
} while (grew);
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> Violations(string xaml, string fileName)
|
||||||
|
{
|
||||||
|
var iconStyles = IconFontStyles();
|
||||||
|
// 여는 태그 단위로 검사 — Button/TextBlock/Run 등 텍스트 표시 요소
|
||||||
|
foreach (Match tag in Regex.Matches(xaml, @"<(Button|TextBlock|Run|Label)\b[^>]*?>", RegexOptions.Singleline))
|
||||||
|
{
|
||||||
|
var open = tag.Value;
|
||||||
|
foreach (Match attr in Regex.Matches(open, "(?:Content|Text)=\"([^\"]*)\""))
|
||||||
|
{
|
||||||
|
var value = attr.Groups[1].Value;
|
||||||
|
if (!PuaEntity.IsMatch(value)) continue;
|
||||||
|
var line = xaml[..tag.Index].Count(c => c == '\n') + 1;
|
||||||
|
|
||||||
|
var stripped = PuaEntity.Replace(value, "").Trim();
|
||||||
|
if (stripped.Length > 0)
|
||||||
|
{
|
||||||
|
yield return $"{fileName} L{line}: PUA+일반 텍스트 혼합 \"{value}\" — 한 폰트로는 둘 다 못 그린다";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var styleMatch = Regex.Match(open, "Style=\"\\{(?:StaticResource|DynamicResource) (\\w+)\\}\"");
|
||||||
|
var styled = styleMatch.Success && iconStyles.Contains(styleMatch.Groups[1].Value);
|
||||||
|
var explicitFont = open.Contains("FontFamily=\"{DynamicResource IconFont}\"")
|
||||||
|
|| open.Contains("FontFamily=\"{StaticResource IconFont}\"");
|
||||||
|
if (!styled && !explicitFont)
|
||||||
|
yield return $"{fileName} L{line}: PUA 글리프 \"{value}\" 가 IconFont 보장 없이 사용됨(본문 폰트 → tofu)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void XAML_의_PUA_글리프는_IconFont_보장_하에서만_쓴다()
|
||||||
|
{
|
||||||
|
var violations = Violations(SourceTree.MainWindowXaml, "MainWindow.xaml")
|
||||||
|
.Concat(Violations(SourceTree.SettingsWindowXaml, "SettingsWindow.xaml"))
|
||||||
|
.ToList();
|
||||||
|
Assert.True(violations.Count == 0, "tofu 유발 지점:\n" + string.Join("\n", violations));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void 코드비하인드도_PUA_와_일반_텍스트를_섞지_않는다()
|
||||||
|
{
|
||||||
|
// 예: LibraryViewToggle.Content = "; 그리드" — 본문 폰트 버튼이라 tofu + 잉여 세미콜론 노출
|
||||||
|
var offenders = Regex.Matches(SourceTree.MainWindowCs + File.ReadAllText(SourceTree.AppPath("MainWindow.Usecases.cs")),
|
||||||
|
@"""\\uE[0-9A-Fa-f]{3}[^""]*[가-힣A-Za-z][^""]*""")
|
||||||
|
.Select(m => m.Value).ToList();
|
||||||
|
Assert.True(offenders.Count == 0, "코드 내 혼합 문자열:\n" + string.Join("\n", offenders));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,8 +80,8 @@ public class UpdateCheckerTests
|
||||||
Assert.Equal("https://github.com/user/videodownloader/releases/tag/v1.2.0", result.ReleaseUrl);
|
Assert.Equal("https://github.com/user/videodownloader/releases/tag/v1.2.0", result.ReleaseUrl);
|
||||||
Assert.Equal("VideoDownloader-Setup-x64.exe", result.Asset!.Name);
|
Assert.Equal("VideoDownloader-Setup-x64.exe", result.Asset!.Name);
|
||||||
|
|
||||||
// GitHub API 규약: User-Agent 필수 + 올바른 엔드포인트
|
// 기본 채널(자체 Forgejo) 엔드포인트 + User-Agent(GitHub 채널 호환 규약 유지)
|
||||||
Assert.Equal("https://api.github.com/repos/user/videodownloader/releases/latest", handler.LastRequest!.RequestUri!.ToString());
|
Assert.Equal("https://git.chanpaca.net/api/v1/repos/user/videodownloader/releases/latest", handler.LastRequest!.RequestUri!.ToString());
|
||||||
Assert.NotEmpty(handler.LastRequest.Headers.UserAgent);
|
Assert.NotEmpty(handler.LastRequest.Headers.UserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,4 +99,56 @@ public class UpdateCheckerTests
|
||||||
var checker = new UpdateChecker(new FakeHandler(HttpStatusCode.NotFound, """{"message":"Not Found"}"""));
|
var checker = new UpdateChecker(new FakeHandler(HttpStatusCode.NotFound, """{"message":"Not Found"}"""));
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => checker.CheckAsync("user/none", "1.0.0", "win-x64"));
|
await Assert.ThrowsAsync<HttpRequestException>(() => checker.CheckAsync("user/none", "1.0.0", "win-x64"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 배포 채널 전환 (DEPLOYMENT_PLAN §5-bis/§5-ter): 기본 = 자체 Forgejo 공개 릴리스 저장소 ──
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task 최신확인_기본은_Forgejo_공개저장소_API()
|
||||||
|
{
|
||||||
|
var handler = new FakeHandler(HttpStatusCode.OK, ReleaseJson);
|
||||||
|
var checker = new UpdateChecker(handler);
|
||||||
|
|
||||||
|
await checker.CheckAsync("Video-Downloader/releases", "1.0.0", "win-x64");
|
||||||
|
|
||||||
|
// Forgejo 는 /api/v1 접두가 필요 — GitHub 형태(api.github.com/repos/...)와 경로 규칙만 다르다
|
||||||
|
Assert.Equal(
|
||||||
|
"https://git.chanpaca.net/api/v1/repos/Video-Downloader/releases/releases/latest",
|
||||||
|
handler.LastRequest!.RequestUri!.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task 최신확인_apiBase_지정으로_GitHub_채널도_호환()
|
||||||
|
{
|
||||||
|
var handler = new FakeHandler(HttpStatusCode.OK, ReleaseJson);
|
||||||
|
var checker = new UpdateChecker(handler);
|
||||||
|
|
||||||
|
var result = await checker.CheckAsync("user/videodownloader", "1.0.0", "win-x64",
|
||||||
|
apiBase: "https://api.github.com");
|
||||||
|
|
||||||
|
Assert.True(result!.IsNewer);
|
||||||
|
Assert.Equal("https://api.github.com/repos/user/videodownloader/releases/latest",
|
||||||
|
handler.LastRequest!.RequestUri!.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void 자산선택_osx는_tar_gz_도_허용()
|
||||||
|
{
|
||||||
|
// macOS 프리뷰는 dmg 가 아니라 tar.gz 로 배포 중 — dmg 만 허용하면 mac 은 영원히 자산 없음
|
||||||
|
var assets = new[]
|
||||||
|
{
|
||||||
|
new UpdateAsset("mediabrowser-osx-arm64.tar.gz", "https://dl/osx-arm64"),
|
||||||
|
new UpdateAsset("mediabrowser-osx-x64.tar.gz", "https://dl/osx-x64"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("https://dl/osx-arm64", UpdateChecker.SelectAsset(assets, "osx-arm64")!.DownloadUrl);
|
||||||
|
Assert.Equal("https://dl/osx-x64", UpdateChecker.SelectAsset(assets, "osx-x64")!.DownloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void 업데이트_기본채널은_공개릴리스저장소()
|
||||||
|
{
|
||||||
|
var config = new VideoDownloader.Core.Config.AppConfig();
|
||||||
|
Assert.Equal("Video-Downloader/releases", config.UpdateRepo);
|
||||||
|
Assert.Equal("https://git.chanpaca.net/api/v1", config.UpdateApiBase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
; VideoDownloader Windows 설치자 (Inno Setup)
|
; MediaBrowser(구 VideoDownloader) Windows 설치자 (Inno Setup)
|
||||||
; publish.ps1 이 /DARCH=x64 또는 /DARCH=arm64 로 2회 컴파일 → out/VideoDownloader-Setup-<arch>.exe
|
; publish.ps1 이 /DARCH=x64 또는 /DARCH=arm64 로 2회 컴파일 → out/MediaBrowser-Setup-<arch>.exe
|
||||||
|
; 배포명만 MediaBrowser 로 개명 — AppId(GUID)·실행 파일명·데이터 경로는 유지해 기존 설치 업그레이드 호환.
|
||||||
|
|
||||||
#define APP_NAME "VideoDownloader"
|
#define APP_NAME "MediaBrowser"
|
||||||
#define APP_PUBLISHER "VideoDownloader"
|
#define APP_PUBLISHER "MediaBrowser"
|
||||||
#define APP_EXE "VideoDownloader.exe"
|
#define APP_EXE "VideoDownloader.exe"
|
||||||
#ifndef APP_VERSION
|
#ifndef APP_VERSION
|
||||||
#define APP_VERSION "1.0.0"
|
#define APP_VERSION "1.0.0"
|
||||||
|
|
@ -32,12 +33,12 @@ ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
; arm64 전용(네이티브). x64 머신에선 거부.
|
; arm64 전용(네이티브). x64 머신에선 거부.
|
||||||
ArchitecturesAllowed=arm64
|
ArchitecturesAllowed=arm64
|
||||||
AppId={{2E0ADFC1-2072-4457-8C50-ECFCEDECAD73}
|
AppId={{2E0ADFC1-2072-4457-8C50-ECFCEDECAD73}
|
||||||
OutputBaseFilename=VideoDownloader-Setup-arm64
|
OutputBaseFilename=MediaBrowser-Setup-arm64
|
||||||
#else
|
#else
|
||||||
; x64 + arm64(x64 에뮬레이션 폴백) 어디서든 설치 가능한 범용 설치자.
|
; x64 + arm64(x64 에뮬레이션 폴백) 어디서든 설치 가능한 범용 설치자.
|
||||||
ArchitecturesAllowed=x64compatible
|
ArchitecturesAllowed=x64compatible
|
||||||
AppId={{65393C7D-17DC-4B9F-9D0B-F9911092C449}
|
AppId={{65393C7D-17DC-4B9F-9D0B-F9911092C449}
|
||||||
OutputBaseFilename=VideoDownloader-Setup-x64
|
OutputBaseFilename=MediaBrowser-Setup-x64
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"//": "Obtainium 원탭 등록용 앱 설정 (DEPLOYMENT_PLAN §3.1) — 저장소 GitHub 공개 후 <owner>/<repo> 를 실제 값으로 치환",
|
"//": "Obtainium 원탭 등록용 앱 설정 (DEPLOYMENT_PLAN §3.1) — Forgejo 공개 릴리스 저장소 기준",
|
||||||
"id": "com.companyname.videodownloader.mobile",
|
"id": "com.companyname.videodownloader.mobile",
|
||||||
"url": "https://github.com/<owner>/<repo>",
|
"url": "https://git.chanpaca.net/Video-Downloader/releases",
|
||||||
"author": "<owner>",
|
"author": "Video-Downloader",
|
||||||
"name": "VideoDownloader",
|
"name": "MediaBrowser",
|
||||||
"additionalSettings": "{\"apkFilterRegEx\":\"apk$\",\"autoApkFilterByArch\":true,\"includePrereleases\":false}"
|
"additionalSettings": "{\"apkFilterRegEx\":\"apk$\",\"autoApkFilterByArch\":true,\"includePrereleases\":false}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# VideoDownloader 다중 아키텍처 설치자 빌드
|
# MediaBrowser(구 VideoDownloader) 다중 아키텍처 설치자 빌드
|
||||||
# 사용: ./build/publish.ps1 [-Config Release] [-Version 1.0.0]
|
# 사용: ./build/publish.ps1 [-Config Release] [-Version 1.0.0]
|
||||||
# 결과: out/VideoDownloader-Setup-x64.exe, out/VideoDownloader-Setup-arm64.exe
|
# 결과: out/MediaBrowser-Setup-x64.exe, out/MediaBrowser-Setup-arm64.exe
|
||||||
param(
|
param(
|
||||||
[string]$Config = "Release",
|
[string]$Config = "Release",
|
||||||
[string]$Version = "1.0.0",
|
[string]$Version = "1.0.0",
|
||||||
|
|
@ -43,10 +43,10 @@ foreach ($arch in $Archs) {
|
||||||
Write-Host "==== [$arch] Inno Setup 컴파일 ====" -ForegroundColor Cyan
|
Write-Host "==== [$arch] Inno Setup 컴파일 ====" -ForegroundColor Cyan
|
||||||
& $iscc /Q "/DAPP_VERSION=$Version" "/DARCH=$arch" "/DPUBLISH_DIR=$pub" $iss
|
& $iscc /Q "/DAPP_VERSION=$Version" "/DARCH=$arch" "/DPUBLISH_DIR=$pub" $iss
|
||||||
if ($LASTEXITCODE -ne 0) { throw "ISCC 실패: $arch (exit $LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "ISCC 실패: $arch (exit $LASTEXITCODE)" }
|
||||||
Write-Host "완료: out\VideoDownloader-Setup-$arch.exe" -ForegroundColor Green
|
Write-Host "완료: out\MediaBrowser-Setup-$arch.exe" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "설치자 생성 완료:" -ForegroundColor Green
|
Write-Host "설치자 생성 완료:" -ForegroundColor Green
|
||||||
Get-ChildItem (Join-Path $outDir "VideoDownloader-Setup-*.exe") |
|
Get-ChildItem (Join-Path $outDir "MediaBrowser-Setup-*.exe") |
|
||||||
ForEach-Object { Write-Host (" {0,-38} {1:N1} MB" -f $_.Name, ($_.Length / 1MB)) }
|
ForEach-Object { Write-Host (" {0,-38} {1:N1} MB" -f $_.Name, ($_.Length / 1MB)) }
|
||||||
|
|
|
||||||
|
|
@ -281,3 +281,31 @@
|
||||||
샘플 데이터 주입 렌더(플랫폼 4그룹·권장 강조·프리뷰 필), 가이드 목차·FAQ. 라이브 URL 에서 재확인.
|
샘플 데이터 주입 렌더(플랫폼 4그룹·권장 강조·프리뷰 필), 가이드 목차·FAQ. 라이브 URL 에서 재확인.
|
||||||
- 도중 결함 2건 수정: fullPage 캡처로 드러난 reveal 무한 숨김 위험(no-JS 백지) → `html.js` 게이트,
|
- 도중 결함 2건 수정: fullPage 캡처로 드러난 reveal 무한 숨김 위험(no-JS 백지) → `html.js` 게이트,
|
||||||
패널 캡처의 사용자 세션 노출 위험 → 격리 프로필 재캡처로 교체.
|
패널 캡처의 사용자 세션 노출 위험 → 격리 프로필 재캡처로 교체.
|
||||||
|
|
||||||
|
## 13차 (2026-08-18) — 후속 과제 일괄: 개명·업데이트 채널·사이트 자동화 + 아이콘 tofu 근본 수정
|
||||||
|
> 사용자: "ㄱㄱ 다해" — 12차에서 남긴 후속 5건 진행. 아이콘은 "절대 안고쳐지네" 보고를 받아 근본 원인까지 추적.
|
||||||
|
|
||||||
|
- [x] **MEDIABROWSER 개명(배포명)** — installer.iss(AppName/출력 MediaBrowser-Setup-*, AppId GUID·exe·데이터 경로는
|
||||||
|
유지해 업그레이드 호환), publish.ps1, 양쪽 릴리스 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser-<ver>-arm64.apk),
|
||||||
|
창 타이틀·환영 문구·모바일 ApplicationTitle, obtainium.json(공개 저장소 URL), README·release 스킬.
|
||||||
|
- [x] **UpdateChecker 채널 전환** — `CheckAsync(..., apiBase)` 주입(기본 `https://git.chanpaca.net/api/v1`,
|
||||||
|
GitHub 는 apiBase 지정으로 호환), osx 자산에 tar.gz 허용(기존 dmg 만 허용 → mac 영원히 자산 없음 결함),
|
||||||
|
AppConfig 기본 UpdateRepo=`Video-Downloader/releases` + UpdateApiBase. 테스트 4건 추가.
|
||||||
|
- [x] **릴리스 Publish 시 사이트 자동 재배포** — releases 저장소에 `site-deploy.yml`(release published →
|
||||||
|
CLIENT_TOKEN 으로 client site/ 체크아웃 → releases.json 생성 → wrangler 배포). 시크릿 3종 API 등록,
|
||||||
|
workflow_dispatch 검증 성공(deploy success).
|
||||||
|
- [◐] **커스텀 도메인** — Pages 프로젝트에 mediabrowser.chanpaca.net 등록 완료. DNS CNAME 은
|
||||||
|
CLOUDFLARE_API_TOKEN/CF_API_TOKEN 모두 Zone DNS 편집 권한이 없어 보류 — **사용자 액션**: 대시보드에서
|
||||||
|
`mediabrowser` CNAME → `mediabrowser-7r4.pages.dev`(프록시 ON) 추가하면 즉시 활성.
|
||||||
|
- [x] **아이콘 tofu 근본 수정** — 원인: Shared.xaml 의 암시적 TextBlock 스타일이 FontFamily=UiFont 를 강제 →
|
||||||
|
버튼 ContentPresenter 자동 TextBlock 이 IconFont 상속을 잃고 모든 PUA 글리프가 notdef(□). 폰트 설치·글리프 존재·
|
||||||
|
렌더 경로(HW/SW·RTB·DPI·런타임 8/10·자체포함/공유) 전부 배제 후 실창 픽셀 비교로 격리. 수정: 암시 스타일의
|
||||||
|
FontFamily 강제 제거(창 루트 상속으로 통일), PairingWindow/PlayerWindow 에 SetResourceReference.
|
||||||
|
부수 결함: PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳(항상 tofu 로 렌더되던 것) 정리, 순수 글리프 2곳 IconFont 명시.
|
||||||
|
- 미해결 관찰(후속): 첫 실행(빈 프로필)에서 초기 탭이 HomeUrl 로 자동 탐색되지 않고 빈 화면으로 남는 현상.
|
||||||
|
|
||||||
|
### 검증 (13차)
|
||||||
|
- `dotnet test` — **462/462 통과** (기준선 453 → UpdateChecker 4·IconGlyphXaml 2·IconGlyphRender 2·IconContentFont 1 추가).
|
||||||
|
- 아이콘: 수정 전 실창 캡처(□ tofu) → 수정 후 동일 조건 캡처에서 ‹ › ↻ ⌂ 글리프 정상 렌더 확인.
|
||||||
|
실창 회귀 테스트가 콘텐츠 TextBlock 의 FontFamily=Segoe Fluent Icons + 본문 Pretendard 유지를 단정.
|
||||||
|
- android 로컬 재현·리허설은 12차와 동일 경로(변경 없음). 산출물 개명은 다음 리허설/태그에서 반영 확인 예정.
|
||||||
|
|
|
||||||
|
|
@ -196,10 +196,14 @@ Cloudflare Pages 에 올렸다. **https://mediabrowser-7r4.pages.dev** (mediabro
|
||||||
`site/fetch-releases.mjs` 가 공개 저장소 릴리스를 `site/releases.json`(git 무시)으로 굽는다.
|
`site/fetch-releases.mjs` 가 공개 저장소 릴리스를 `site/releases.json`(git 무시)으로 굽는다.
|
||||||
published 릴리스만 포함(draft 제외), 최신판 강조 + 이전 버전 접힘 + OS 자동 감지 권장 표시.
|
published 릴리스만 포함(draft 제외), 최신판 강조 + 이전 버전 접힘 + OS 자동 감지 권장 표시.
|
||||||
- **배포**: `./build/deploy-site.ps1` — .env 의 `CLOUDFLARE_API_TOKEN/ACCOUNT_ID` 로
|
- **배포**: `./build/deploy-site.ps1` — .env 의 `CLOUDFLARE_API_TOKEN/ACCOUNT_ID` 로
|
||||||
`wrangler pages deploy site --project-name mediabrowser`. **릴리스를 Publish 한 뒤 이 스크립트를
|
`wrangler pages deploy site --project-name mediabrowser`. **자동화(2026-08-18)**: 공개 releases
|
||||||
다시 실행해야 사이트 목록이 갱신된다**(후속: 릴리스 CI 에 사이트 재배포 잡 추가).
|
저장소의 `.forgejo/workflows/site-deploy.yml` 이 릴리스 Publish 이벤트에서 사이트를 자동 재배포한다
|
||||||
- **후속**: 커스텀 도메인 · 설치자/실행 파일의 MEDIABROWSER 개명(제품명 통일) · Obtainium `app.json`
|
(시크릿: CLIENT_TOKEN=client 읽기 PAT, CLOUDFLARE_API_TOKEN/ACCOUNT_ID).
|
||||||
의 URL 을 공개 저장소로 갱신.
|
- **개명 완료(2026-08-18)**: 설치자 `MediaBrowser-Setup-<arch>.exe` · tar.gz `mediabrowser-<rid>` ·
|
||||||
|
APK `MediaBrowser-<ver>-arm64.apk` · 앱 타이틀/모바일 표시명 MediaBrowser · obtainium.json 공개 저장소 URL.
|
||||||
|
AppId(GUID)·실행 파일명(VideoDownloader.exe)·데이터 경로는 유지(업그레이드·사용자 데이터 호환).
|
||||||
|
- **후속**: 커스텀 도메인 `mediabrowser.chanpaca.net` — Pages 프로젝트 등록은 완료, DNS CNAME 은 토큰에
|
||||||
|
Zone DNS 편집 권한이 없어 사용자 액션 대기(CNAME `mediabrowser` → `mediabrowser-7r4.pages.dev`, 프록시 ON).
|
||||||
|
|
||||||
## 6. 업데이트 체계
|
## 6. 업데이트 체계
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue