From 9b9e01b107cfa9fe9849e4b4c42f9538bb3f2cc5 Mon Sep 17 00:00:00 2001 From: Lyz Coote Date: Wed, 18 Feb 2026 16:58:48 +0100 Subject: [PATCH] feat: refactor screenshot capture implementation and update UI reload labels --- backend/utils/screenshot_windows.go | 140 ++---------------- frontend/messages/en.json | 3 +- frontend/messages/it.json | 5 +- .../src/routes/(app)/settings/+page.svelte | 52 +++---- go.mod | 4 + go.sum | 9 ++ 6 files changed, 52 insertions(+), 161 deletions(-) diff --git a/backend/utils/screenshot_windows.go b/backend/utils/screenshot_windows.go index 6ac8206..6a4a9b1 100644 --- a/backend/utils/screenshot_windows.go +++ b/backend/utils/screenshot_windows.go @@ -8,32 +8,18 @@ import ( "image/png" "syscall" "unsafe" + + "github.com/kbinani/screenshot" ) var ( - user32 = syscall.NewLazyDLL("user32.dll") - gdi32 = syscall.NewLazyDLL("gdi32.dll") - dwmapi = syscall.NewLazyDLL("dwmapi.dll") + user32 = syscall.NewLazyDLL("user32.dll") + dwmapi = syscall.NewLazyDLL("dwmapi.dll") // user32 functions - getForegroundWindow = user32.NewProc("GetForegroundWindow") - getWindowRect = user32.NewProc("GetWindowRect") - getClientRect = user32.NewProc("GetClientRect") - getDC = user32.NewProc("GetDC") - releaseDC = user32.NewProc("ReleaseDC") - findWindowW = user32.NewProc("FindWindowW") - getWindowDC = user32.NewProc("GetWindowDC") - printWindow = user32.NewProc("PrintWindow") - clientToScreen = user32.NewProc("ClientToScreen") - - // gdi32 functions - createCompatibleDC = gdi32.NewProc("CreateCompatibleDC") - createCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap") - selectObject = gdi32.NewProc("SelectObject") - bitBlt = gdi32.NewProc("BitBlt") - deleteDC = gdi32.NewProc("DeleteDC") - deleteObject = gdi32.NewProc("DeleteObject") - getDIBits = gdi32.NewProc("GetDIBits") + getForegroundWindow = user32.NewProc("GetForegroundWindow") + getWindowRect = user32.NewProc("GetWindowRect") + findWindowW = user32.NewProc("FindWindowW") // dwmapi functions dwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute") @@ -47,39 +33,7 @@ type RECT struct { Bottom int32 } -// POINT structure for Windows API -type POINT struct { - X int32 - Y int32 -} - -// BITMAPINFOHEADER structure -type BITMAPINFOHEADER struct { - BiSize uint32 - BiWidth int32 - BiHeight int32 - BiPlanes uint16 - BiBitCount uint16 - BiCompression uint32 - BiSizeImage uint32 - BiXPelsPerMeter int32 - BiYPelsPerMeter int32 - BiClrUsed uint32 - BiClrImportant uint32 -} - -// BITMAPINFO structure -type BITMAPINFO struct { - BmiHeader BITMAPINFOHEADER - BmiColors [1]uint32 -} - const ( - SRCCOPY = 0x00CC0020 - DIB_RGB_COLORS = 0 - BI_RGB = 0 - PW_CLIENTONLY = 1 - PW_RENDERFULLCONTENT = 2 DWMWA_EXTENDED_FRAME_BOUNDS = 9 ) @@ -113,82 +67,10 @@ func CaptureWindowByHandle(hwnd uintptr) (*image.RGBA, error) { return nil, fmt.Errorf("invalid window dimensions: %dx%d", width, height) } - // Get window DC - hdcWindow, _, err := getWindowDC.Call(hwnd) - if hdcWindow == 0 { - return nil, fmt.Errorf("GetWindowDC failed: %v", err) - } - defer releaseDC.Call(hwnd, hdcWindow) - - // Create compatible DC - hdcMem, _, err := createCompatibleDC.Call(hdcWindow) - if hdcMem == 0 { - return nil, fmt.Errorf("CreateCompatibleDC failed: %v", err) - } - defer deleteDC.Call(hdcMem) - - // Create compatible bitmap - hBitmap, _, err := createCompatibleBitmap.Call(hdcWindow, uintptr(width), uintptr(height)) - if hBitmap == 0 { - return nil, fmt.Errorf("CreateCompatibleBitmap failed: %v", err) - } - defer deleteObject.Call(hBitmap) - - // Select bitmap into DC - oldBitmap, _, _ := selectObject.Call(hdcMem, hBitmap) - defer selectObject.Call(hdcMem, oldBitmap) - - // Try PrintWindow first (works better with layered/composited windows) - ret, _, _ = printWindow.Call(hwnd, hdcMem, PW_RENDERFULLCONTENT) - if ret == 0 { - // Fallback to BitBlt - ret, _, err = bitBlt.Call( - hdcMem, 0, 0, uintptr(width), uintptr(height), - hdcWindow, 0, 0, - SRCCOPY, - ) - if ret == 0 { - return nil, fmt.Errorf("BitBlt failed: %v", err) - } - } - - // Prepare BITMAPINFO - bmi := BITMAPINFO{ - BmiHeader: BITMAPINFOHEADER{ - BiSize: uint32(unsafe.Sizeof(BITMAPINFOHEADER{})), - BiWidth: int32(width), - BiHeight: -int32(height), // Negative for top-down DIB - BiPlanes: 1, - BiBitCount: 32, - BiCompression: BI_RGB, - }, - } - - // Allocate buffer for pixel data - pixelDataSize := width * height * 4 - pixelData := make([]byte, pixelDataSize) - - // Get the bitmap bits - ret, _, err = getDIBits.Call( - hdcMem, - hBitmap, - 0, - uintptr(height), - uintptr(unsafe.Pointer(&pixelData[0])), - uintptr(unsafe.Pointer(&bmi)), - DIB_RGB_COLORS, - ) - if ret == 0 { - return nil, fmt.Errorf("GetDIBits failed: %v", err) - } - - // Convert BGRA to RGBA - img := image.NewRGBA(image.Rect(0, 0, width, height)) - for i := 0; i < len(pixelData); i += 4 { - img.Pix[i+0] = pixelData[i+2] // R <- B - img.Pix[i+1] = pixelData[i+1] // G <- G - img.Pix[i+2] = pixelData[i+0] // B <- R - img.Pix[i+3] = pixelData[i+3] // A <- A + // Using kbinani/screenshot to capture the rectangle on screen + img, err := screenshot.CaptureRect(image.Rect(int(rect.Left), int(rect.Top), int(rect.Right), int(rect.Bottom))) + if err != nil { + return nil, fmt.Errorf("screenshot.CaptureRect failed: %v", err) } return img, nil diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f339c52..1c39487 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -43,7 +43,8 @@ "settings_danger_reset_button": "Reset data", "settings_danger_reload_ui_label": "Reload UI", "settings_danger_reload_app_label": "Reload App", - "settings_danger_reload_hint": "Reloads the application interface. Useful if the UI becomes unresponsive.", + "settings_danger_reload_all_label": "Reload UI/App", + "settings_danger_reload_hint": "Reloads the application interface. Useful if it becomes unresponsive.", "settings_danger_reload_button_ui": "Reload UI", "settings_danger_reload_button_app": "Reload app", "settings_danger_reset_dialog_title": "Are you absolutely sure?", diff --git a/frontend/messages/it.json b/frontend/messages/it.json index b05159a..30fbeac 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -42,8 +42,9 @@ "settings_danger_reset_hint": "Questo cancellerà tutte le tue impostazioni e riporterà l'app allo stato predefinito.", "settings_danger_reset_button": "Reimposta dati", "settings_danger_reload_ui_label": "Ricarica UI", - "settings_danger_reload__app_label": "Ricarica App", - "settings_danger_reload_hint": "Ricarica l'interfaccia dell'applicazione. Utile se l'UI non risponde.", + "settings_danger_reload_app_label": "Ricarica App", + "settings_danger_reload_all_label": "Ricarica UI/App", + "settings_danger_reload_hint": "Ricarica l'interfaccia dell'applicazione. Utile se non risponde.", "settings_danger_reload_button_ui": "Ricarica UI", "settings_danger_reload_button_app": "Ricarica app", "settings_danger_reset_dialog_title": "Sei assolutamente sicuro?", diff --git a/frontend/src/routes/(app)/settings/+page.svelte b/frontend/src/routes/(app)/settings/+page.svelte index a98a9c8..0be3fa1 100644 --- a/frontend/src/routes/(app)/settings/+page.svelte +++ b/frontend/src/routes/(app)/settings/+page.svelte @@ -195,10 +195,14 @@ }); // Sync update checker setting to backend config.ini - let previousUpdateCheckerEnabled = form.enableUpdateChecker; + let previousUpdateCheckerEnabled = $state(undefined); $effect(() => { (async () => { if (!browser) return; + if (previousUpdateCheckerEnabled === undefined) { + previousUpdateCheckerEnabled = form.enableUpdateChecker; + return; + } if (form.enableUpdateChecker !== previousUpdateCheckerEnabled) { try { await SetUpdateCheckerEnabled(form.enableUpdateChecker ?? true); @@ -354,7 +358,7 @@ }); -
+
@@ -868,39 +872,29 @@ class="flex items-center justify-between gap-4 rounded-lg border border-destructive/30 bg-card p-4" >
- +
{m.settings_danger_reload_hint()}
- - {m.settings_danger_reload_button_ui()} - -
- -
-
- -
- {m.settings_danger_reload_hint()} -
+
+ + {m.settings_danger_reload_button_ui()} + +
- -
diff --git a/go.mod b/go.mod index 70b57cd..16b02c9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.4 require ( github.com/jaypipes/ghw v0.21.2 + github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 github.com/teamwork/tnef v0.0.0-20200108124832-7deabccfdb32 github.com/wailsapp/wails/v2 v2.11.0 golang.org/x/sys v0.40.0 @@ -13,18 +14,21 @@ require ( require ( github.com/bep/debounce v1.2.1 // indirect + github.com/gen2brain/shm v0.1.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jaypipes/pcidb v1.1.1 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/jezek/xgb v1.1.1 // indirect github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/gosod v1.0.4 // indirect github.com/leaanthony/slicer v1.6.0 // indirect github.com/leaanthony/u v1.1.1 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index bb2bed3..17be66f 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3IS github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY= +github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -20,6 +22,10 @@ github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6h github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= @@ -36,6 +42,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/ github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= @@ -95,6 +103,7 @@ golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=