feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
294
src/Paca.App/Services/AppDebugScreenshotService.cs
Normal file
294
src/Paca.App/Services/AppDebugScreenshotService.cs
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Paca.App.Core;
|
||||
|
||||
namespace Paca.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// PACA 데스크톱 애플리케이션 화면 디버깅 캡처 서비스.
|
||||
/// Win32 GDI / PrintWindow / RenderTargetBitmap 하이브리드 파이프라인으로
|
||||
/// WPF 및 WebView2(HWND) 자식 컨트롤이 포함된 실제 앱 윈도우 전체를 스크린샷으로 캡처하여 저장한다.
|
||||
/// </summary>
|
||||
public static class AppDebugScreenshotService
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBkgnd, uint nFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool BringWindowToTop(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
private const int SW_RESTORE = 9;
|
||||
private const uint PW_RENDERFULLCONTENT = 0x00000002;
|
||||
private static DispatcherTimer? _triggerTimer;
|
||||
|
||||
public static string GetDefaultScreenshotPath() =>
|
||||
Path.Combine(AppPaths.DataDir, "debug_screenshot.png");
|
||||
|
||||
public static string ResolveTriggerPath(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return GetDefaultScreenshotPath();
|
||||
return input.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대상 Window의 현재 화면을 PNG 파일로 캡처하여 저장한다.
|
||||
/// </summary>
|
||||
public static bool CaptureWindow(Window window, string outputPath)
|
||||
{
|
||||
if (window == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(outputPath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
var helper = new WindowInteropHelper(window);
|
||||
var hWnd = helper.Handle;
|
||||
|
||||
if (hWnd != IntPtr.Zero && GetWindowRect(hWnd, out var rect))
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowWindow(hWnd, SW_RESTORE);
|
||||
SetForegroundWindow(hWnd);
|
||||
BringWindowToTop(hWnd);
|
||||
System.Threading.Thread.Sleep(200);
|
||||
GetWindowRect(hWnd, out rect);
|
||||
}
|
||||
catch { }
|
||||
|
||||
int width = rect.Right - rect.Left;
|
||||
int height = rect.Bottom - rect.Top;
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var bmp = new System.Drawing.Bitmap(width, height);
|
||||
using (var g = System.Drawing.Graphics.FromImage(bmp))
|
||||
{
|
||||
var hdc = g.GetHdc();
|
||||
try
|
||||
{
|
||||
PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
|
||||
}
|
||||
finally
|
||||
{
|
||||
g.ReleaseHdc(hdc);
|
||||
}
|
||||
|
||||
// WebView2 자식 컨트롤 실시간 렌더링 화면 비트맵 오버레이
|
||||
OverlayAdBanners(window, g, width, height);
|
||||
}
|
||||
|
||||
bmp.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
|
||||
if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// GDI 캡처 실패 시 하단 WPF RenderTargetBitmap 폴백 진행
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 폴백: 순수 WPF 비주얼 트리 렌더링
|
||||
int rtbWidth = (int)Math.Max(1, window.ActualWidth > 0 ? window.ActualWidth : window.Width);
|
||||
int rtbHeight = (int)Math.Max(1, window.ActualHeight > 0 ? window.ActualHeight : window.Height);
|
||||
if (rtbWidth <= 1) rtbWidth = 1024;
|
||||
if (rtbHeight <= 1) rtbHeight = 768;
|
||||
|
||||
var rtb = new RenderTargetBitmap(rtbWidth, rtbHeight, 96, 96, PixelFormats.Pbgra32);
|
||||
rtb.Render(window);
|
||||
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
||||
using (var fs = File.Create(outputPath))
|
||||
{
|
||||
encoder.Save(fs);
|
||||
}
|
||||
|
||||
return File.Exists(outputPath) && new FileInfo(outputPath).Length > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부 신호 파일(%AppData%/VideoDownloader/screenshot_trigger.req)을 감지하여
|
||||
/// 백그라운드에서 실시간 스크린샷을 찍도록 타이머를 구동한다.
|
||||
/// </summary>
|
||||
public static void StartFileTriggerWatcher(Window window, Action<string>? onCaptured = null)
|
||||
{
|
||||
if (_triggerTimer != null) return;
|
||||
|
||||
var triggerFile = Path.Combine(AppPaths.DataDir, "screenshot_trigger.req");
|
||||
|
||||
_triggerTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
_triggerTimer.Tick += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(triggerFile))
|
||||
{
|
||||
string target = "";
|
||||
try
|
||||
{
|
||||
target = File.ReadAllText(triggerFile);
|
||||
File.Delete(triggerFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 읽기 경합 방어
|
||||
return;
|
||||
}
|
||||
|
||||
var savePath = ResolveTriggerPath(target);
|
||||
bool ok = CaptureWindow(window, savePath);
|
||||
if (ok)
|
||||
{
|
||||
onCaptured?.Invoke(savePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 백그라운드 폴러 방어
|
||||
}
|
||||
};
|
||||
|
||||
_triggerTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 앱 실행 후 指定된 지연 시간(기본 3초) 뒤에 자동으로 스크린샷을 찍어 저장한다.
|
||||
/// </summary>
|
||||
public static void ScheduleAutoCapture(Window window, int delayMs = 3000, string? targetPath = null)
|
||||
{
|
||||
Task.Delay(delayMs).ContinueWith(_ =>
|
||||
{
|
||||
window.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var dest = ResolveTriggerPath(targetPath);
|
||||
CaptureWindow(window, dest);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static void OverlayAdBanners(Window window, System.Drawing.Graphics g, int windowWidth, int windowHeight)
|
||||
{
|
||||
try
|
||||
{
|
||||
window.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var banners = FindVisualChildren<Paca.App.Controls.DesktopAdBannerControl>(window);
|
||||
foreach (var banner in banners)
|
||||
{
|
||||
if (banner.Visibility != Visibility.Visible || banner.ActualWidth <= 0 || banner.ActualHeight <= 0)
|
||||
continue;
|
||||
|
||||
var previewPath = Path.Combine(AppPaths.DataDir, "ad_previews", $"ad_{banner.Placement}.png");
|
||||
if (!File.Exists(previewPath)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var transform = banner.TransformToAncestor(window);
|
||||
var point = transform.Transform(new Point(0, 0));
|
||||
|
||||
using var fs = new FileStream(previewPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var adImg = System.Drawing.Image.FromStream(fs);
|
||||
int x = (int)point.X;
|
||||
int y = (int)point.Y;
|
||||
int w = (int)banner.ActualWidth;
|
||||
int h = (int)banner.ActualHeight;
|
||||
|
||||
if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.BottomAppBanner)
|
||||
{
|
||||
x += (w - 728) / 2;
|
||||
y += (h - 90) / 2;
|
||||
w = 728;
|
||||
h = 90;
|
||||
}
|
||||
else if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.DownloadViewBanner)
|
||||
{
|
||||
x += (w - 300) / 2;
|
||||
y += (h - 250) / 2;
|
||||
w = 300;
|
||||
h = 250;
|
||||
}
|
||||
|
||||
g.DrawImage(adImg, x, y, w, h);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = Path.Combine(AppPaths.DataDir, "ad_debug.log");
|
||||
File.AppendAllText(p, $"[{DateTime.Now:HH:mm:ss.fff}] Overlay exception for {banner.Placement}: {ex.Message}\n");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
|
||||
{
|
||||
if (depObj == null) yield break;
|
||||
int count = VisualTreeHelper.GetChildrenCount(depObj);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(depObj, i);
|
||||
if (child is T match)
|
||||
{
|
||||
yield return match;
|
||||
}
|
||||
|
||||
foreach (var childOfChild in FindVisualChildren<T>(child))
|
||||
{
|
||||
yield return childOfChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue