From 2ba1056c56435f8d98110eca18dba90dc344de66 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 01 Jul 2026 23:57:44 +0800
Subject: [PATCH] Polish Windows overlay preview

---
 privatevoice.src/app.go                              |    4 
 privatevoice.src/scripts/build-windows-preview.sh    |    4 
 CHANGELOG.md                                         |   16 ++++
 privatevoice.src/internal/overlay/overlay_windows.go |  211 ++++++++++++++++++++++++++++++++++++++++++----------
 CODEGRAPH.md                                         |    1 
 5 files changed, 191 insertions(+), 45 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4e5fda..2261dad 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # Changelog
 
+## v2.1.39 (2026-07-01)
+
+### Windows 动态指示视觉升级
+
+- **Windows overlay 升级为轻量炫彩胶囊**:保留原生 Win32 overlay 路线,不改热键、录音和识别链路,只提升视觉表现。
+- **胶囊窗口更轻更透**:降低背景不透明度,加入更柔和的边框和内层高光,减轻之前厚重发闷的观感。
+- **柱条动画更接近 macOS 风格**:柱条从 5 根扩展到 11 根,录音/处理中加入更丰富的动态起伏、光谱过渡和发光层。
+- **仍保留 Windows 原生行为**:继续使用现有顶层窗、非激活、拖拽和定位逻辑,避免把视觉升级扩大成平台重构。
+
+### 构建
+
+- build: `20260701.2356`
+- 说明: 本版本为 Windows 动态指示视觉升级后的预览候选包。
+
+---
+
 ## v2.1.38 (2026-06-29)
 
 ### 性能诊断
diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index ca9cb6d..3d3b984 100644
--- a/CODEGRAPH.md
+++ b/CODEGRAPH.md
@@ -35,6 +35,7 @@
 - Windows preview supports `SenseVoice` only.
 - Non-SenseVoice models remain visible in settings but are disabled through `model.IsModelSupportedInCurrentBuild`.
 - Windows unsupported reason: `windows_preview_unsupported`.
+- Windows overlay remains native Win32/GDI+, with a larger translucent spectrum-style capsule indicator for recording states.
 - Build script: `privatevoice.src/scripts/build-windows-preview.sh`.
 - Evidence verifier: `privatevoice.src/scripts/verify-windows-qa-evidence.sh`.
 - Runtime package includes:
diff --git a/privatevoice.src/app.go b/privatevoice.src/app.go
index 89ea0b6..ce6e70c 100755
--- a/privatevoice.src/app.go
+++ b/privatevoice.src/app.go
@@ -28,8 +28,8 @@
 )
 
 const (
-	appVersion        = "2.1.38"
-	appBuild          = "20260629.2014"
+	appVersion        = "2.1.39"
+	appBuild          = "20260701.2356"
 	appDisplayVersion = appVersion + " (build " + appBuild + ")"
 	appName           = "PrivateVoice Dictation"
 
diff --git a/privatevoice.src/internal/overlay/overlay_windows.go b/privatevoice.src/internal/overlay/overlay_windows.go
index 8bd89f6..b19d38e 100755
--- a/privatevoice.src/internal/overlay/overlay_windows.go
+++ b/privatevoice.src/internal/overlay/overlay_windows.go
@@ -49,10 +49,15 @@
 
 // Colors (ARGB)
 const (
-	clrCapsuleBG = 0xD91C1C1E
-	clrBorder    = 0x20FFFFFF
-	clrText      = 0xFFB4B4B4
+	clrCapsuleBG = 0xA6101318
+	clrCapsuleHi = 0x18FFFFFF
+	clrBorder    = 0x2EFFFFFF
+	clrText      = 0xFFE7EDF5
+	clrTextMuted = 0xFFB6C3D2
 	clrBlue      = 0xFF007AFF
+	clrCyan      = 0xFF2ED3FF
+	clrViolet    = 0xFF7A6CFF
+	clrPink      = 0xFFFF5EB8
 	clrGreen     = 0xFF34C759
 	clrRed       = 0xFFFF3B30
 	clrOrange    = 0xFFFF9500
@@ -60,15 +65,15 @@
 
 // Dimensions (logical px)
 const (
-	capW   = 170
-	capH   = 48
-	capR   = 24
-	barW   = 5
-	barGap = 4
-	barR   = 2.5
-	nBars  = 5
-	txtSz  = 12.0
-	txtGap = 12
+	capW   = 420
+	capH   = 92
+	capR   = 46
+	barW   = 7
+	barGap = 6
+	barR   = 3.5
+	nBars  = 11
+	txtSz  = 17.0
+	txtGap = 20
 )
 
 // ---- Win32 structs ----
@@ -230,13 +235,13 @@
 func statusColor(s Status) uint32 {
 	switch s {
 	case StatusLoading:
-		return clrBlue
+		return clrCyan
 	case StatusReady, StatusDone:
 		return clrGreen
-	case StatusRecording, StatusError:
+	case StatusRecording, StatusFreetalking:
+		return clrCyan
+	case StatusError:
 		return clrRed
-	case StatusFreetalking:
-		return clrBlue
 	case StatusProcessing, StatusCancelled, StatusNoVoice, StatusNoContent:
 		return clrOrange
 	default:
@@ -288,7 +293,7 @@
 		done:     make(chan struct{}),
 	}
 	for i := range o.barHeights {
-		o.barHeights[i] = 14
+		o.barHeights[i] = 16
 	}
 	go o.run()
 	return o
@@ -472,8 +477,8 @@
 				text = cmd.text
 				barClr = statusColor(cmd.status)
 			case vol := <-o.volCh:
-				copy(volumes[0:4], volumes[1:5])
-				volumes[4] = vol
+				copy(volumes[0:nBars-1], volumes[1:nBars])
+				volumes[nBars-1] = vol
 			case pos := <-o.posCh:
 				o.posX, o.posY = pos[0], pos[1]
 				uSetWindowPos.Call(hwnd, 0, uintptr(o.posX), uintptr(o.posY), 0, 0,
@@ -532,7 +537,7 @@
 
 		o.animTime += 0.033
 		o.animate(status, volumes)
-		o.renderFrame(memDC, hwnd, screenDC, font, strFmt, barClr, text, alpha)
+		o.renderFrame(memDC, hwnd, screenDC, font, strFmt, status, barClr, text, alpha)
 	}
 }
 
@@ -544,12 +549,14 @@
 	switch status {
 	case StatusLoading:
 		for i := 0; i < nBars; i++ {
-			pulse := math.Sin(t*2+float64(i)*0.3)*0.5 + 0.5
-			o.barHeights[i] = (10 + pulse*20) * s
+			pulse := math.Sin(t*2.4+float64(i)*0.42)*0.5 + 0.5
+			o.barHeights[i] = (12 + pulse*28) * s
 		}
 	case StatusReady, StatusDone:
 		for i := 0; i < nBars; i++ {
-			o.barHeights[i] = o.barHeights[i]*0.85 + 14*s*0.15
+			pulse := math.Sin(t*1.4+float64(i)*0.25)*0.5 + 0.5
+			target := (15 + pulse*7) * s
+			o.barHeights[i] = o.barHeights[i]*0.78 + target*0.22
 		}
 	case StatusRecording, StatusFreetalking:
 		hasReal := false
@@ -562,29 +569,29 @@
 		for i := 0; i < nBars; i++ {
 			var target float64
 			if hasReal {
-				target = (8 + volumes[i]*32) * s
+				target = (10 + volumes[i]*44) * s
 			} else {
-				wave := math.Sin(t*4+float64(i)*0.8)*0.5 + 0.5
-				rnd := math.Sin(t*7+float64(i)*1.5) * 0.3
-				target = (10 + (wave+rnd)*25) * s
+				wave := math.Sin(t*4.6+float64(i)*0.55)*0.5 + 0.5
+				rnd := math.Sin(t*7.5+float64(i)*1.1) * 0.2
+				target = (12 + (wave+rnd)*30) * s
 			}
-			o.barHeights[i] = o.barHeights[i]*0.6 + target*0.4
+			o.barHeights[i] = o.barHeights[i]*0.52 + target*0.48
 		}
 	case StatusProcessing:
 		for i := 0; i < nBars; i++ {
-			wave := math.Sin(t*3+float64(i)*0.6)*0.5 + 0.5
-			o.barHeights[i] = (12 + wave*18) * s
+			wave := math.Sin(t*3.2+float64(i)*0.5)*0.5 + 0.5
+			o.barHeights[i] = (14 + wave*22) * s
 		}
 	default:
 		for i := 0; i < nBars; i++ {
-			o.barHeights[i] = o.barHeights[i]*0.85 + 14*s*0.15
+			o.barHeights[i] = o.barHeights[i]*0.8 + 16*s*0.2
 		}
 	}
 }
 
 // ---- Rendering ----
 
-func (o *winOverlay) renderFrame(memDC, hwnd, screenDC, font, strFmt uintptr, barClr uint32, text string, alpha byte) {
+func (o *winOverlay) renderFrame(memDC, hwnd, screenDC, font, strFmt uintptr, status Status, barClr uint32, text string, alpha byte) {
 	var gfx uintptr
 	gpFromHDC.Call(memDC, uintptr(unsafe.Pointer(&gfx)))
 	if gfx == 0 {
@@ -596,11 +603,21 @@
 	gpSetTextHint.Call(gfx, textHintAntiAlias)
 	gpClear.Call(gfx, 0) // transparent
 
-	// Capsule background
-	fillRoundRect(gfx, 0, 0, float64(o.sW), float64(o.sH), o.sCapR, clrCapsuleBG)
+	capsuleW := float64(o.sW)
+	capsuleH := float64(o.sH)
+	capsuleR := o.sCapR
+	edgeInset := 1.5 * o.scale
+	contentInsetX := 18.0 * o.scale
+	contentInsetY := 14.0 * o.scale
+	contentH := capsuleH - contentInsetY*2
+
+	// Colored halo + lighter translucent body.
+	fillRoundRect(gfx, 0, 0, capsuleW, capsuleH, capsuleR, scaleAlpha(barClr, 0.16))
+	fillRoundRect(gfx, edgeInset, edgeInset, capsuleW-edgeInset*2, capsuleH-edgeInset*2, maxf(capsuleR-edgeInset, 1), clrCapsuleBG)
+	fillRoundRect(gfx, contentInsetX, contentInsetY, capsuleW-contentInsetX*2, contentH*0.58, maxf(contentH*0.29, 8*o.scale), clrCapsuleHi)
 
 	// Inner border
-	strokeRoundRect(gfx, 0.5, 0.5, float64(o.sW)-1, float64(o.sH)-1, o.sCapR-0.5, clrBorder, 1)
+	strokeRoundRect(gfx, 0.5, 0.5, capsuleW-1, capsuleH-1, maxf(capsuleR-0.5, 1), clrBorder, float32(maxf(1.0*o.scale, 1)))
 
 	// Layout: bars + optional text, centered
 	barsW := float32(float64(nBars)*o.sBarW + float64(nBars-1)*o.sBarGap)
@@ -608,30 +625,66 @@
 	if text != "" && font != 0 {
 		textW = measureText(gfx, font, strFmt, text)
 	}
+	maxTextW := float32(capsuleW-contentInsetX*2) - barsW - float32(o.sTxtGap)
+	if maxTextW < 0 {
+		maxTextW = 0
+	}
+	if textW > maxTextW {
+		textW = maxTextW
+	}
 	totalW := barsW
 	if textW > 0 {
 		totalW += float32(o.sTxtGap) + textW
 	}
 	startX := (float32(o.sW) - totalW) / 2
 
+	// Bar bed glow
+	barBedX := float64(startX) - 12*o.scale
+	barBedW := float64(barsW) + 24*o.scale
+	fillRoundRect(
+		gfx,
+		barBedX,
+		contentInsetY+4*o.scale,
+		barBedW,
+		contentH-8*o.scale,
+		maxf((contentH-8*o.scale)/2, 6*o.scale),
+		scaleAlpha(barClr, 0.10),
+	)
+
 	// Bars
 	for i := 0; i < nBars; i++ {
 		bx := startX + float32(float64(i)*(o.sBarW+o.sBarGap))
 		bh := float32(o.barHeights[i])
-		if bh < float32(6*o.scale) {
-			bh = float32(6 * o.scale)
+		if bh < float32(8*o.scale) {
+			bh = float32(8 * o.scale)
 		}
-		if bh > float32(40*o.scale) {
-			bh = float32(40 * o.scale)
+		if bh > float32(56*o.scale) {
+			bh = float32(56 * o.scale)
 		}
 		by := (float32(o.sH) - bh) / 2
-		fillRoundRect(gfx, float64(bx), float64(by), o.sBarW, float64(bh), o.sBarR, barClr)
+		barColor := barFillColor(status, i, o.animTime, barClr)
+		glowH := float64(bh) + 18*o.scale
+		glowY := float64(by) - 9*o.scale
+		fillRoundRect(
+			gfx,
+			float64(bx)-o.sBarW*0.4,
+			glowY,
+			o.sBarW*1.8,
+			glowH,
+			o.sBarR*1.8,
+			scaleAlpha(barColor, 0.18),
+		)
+		fillRoundRect(gfx, float64(bx), float64(by), o.sBarW, float64(bh), o.sBarR, barColor)
 	}
 
 	// Text
 	if text != "" && font != 0 && strFmt != 0 {
 		tx := startX + barsW + float32(o.sTxtGap)
-		drawText(gfx, font, strFmt, text, tx, 0, textW+4, float32(o.sH), clrText)
+		textColor := uint32(clrText)
+		if status == StatusReady || status == StatusDone {
+			textColor = uint32(clrTextMuted)
+		}
+		drawText(gfx, font, strFmt, text, tx, 0, textW+4, float32(o.sH), textColor)
 	}
 
 	// UpdateLayeredWindow
@@ -642,8 +695,84 @@
 		memDC, uintptr(unsafe.Pointer(&ptSrc)), 0, uintptr(unsafe.Pointer(&blend)), ulwAlpha)
 }
 
+func barFillColor(status Status, index int, t float64, base uint32) uint32 {
+	progress := 0.0
+	if nBars > 1 {
+		progress = float64(index) / float64(nBars-1)
+	}
+	switch status {
+	case StatusRecording, StatusFreetalking:
+		return mixColor(spectralColor(progress+t*0.08), base, 0.18)
+	case StatusLoading, StatusProcessing:
+		return mixColor(spectralColor(progress+t*0.16), base, 0.32)
+	case StatusReady, StatusDone:
+		return mixColor(spectralColor(progress+t*0.03), clrGreen, 0.70)
+	case StatusError:
+		return mixColor(clrRed, clrPink, progress*0.35)
+	case StatusCancelled, StatusNoVoice, StatusNoContent:
+		return mixColor(clrOrange, clrPink, progress*0.22)
+	default:
+		return base
+	}
+}
+
 // ---- GDI+ drawing helpers ----
 
+func clamp01(v float64) float64 {
+	if v < 0 {
+		return 0
+	}
+	if v > 1 {
+		return 1
+	}
+	return v
+}
+
+func maxf(a, b float64) float64 {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+func argb(a, r, g, b byte) uint32 {
+	return uint32(a)<<24 | uint32(r)<<16 | uint32(g)<<8 | uint32(b)
+}
+
+func colorChannels(c uint32) (byte, byte, byte, byte) {
+	return byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c)
+}
+
+func mixColor(a, b uint32, t float64) uint32 {
+	t = clamp01(t)
+	aa, ar, ag, ab := colorChannels(a)
+	ba, br, bg, bb := colorChannels(b)
+	mix := func(x, y byte) byte {
+		return byte(math.Round(float64(x) + (float64(y)-float64(x))*t))
+	}
+	return argb(mix(aa, ba), mix(ar, br), mix(ag, bg), mix(ab, bb))
+}
+
+func scaleAlpha(c uint32, factor float64) uint32 {
+	factor = clamp01(factor)
+	a, r, g, b := colorChannels(c)
+	return argb(byte(math.Round(float64(a)*factor)), r, g, b)
+}
+
+func spectralColor(progress float64) uint32 {
+	progress = progress - math.Floor(progress)
+	switch {
+	case progress < 0.25:
+		return mixColor(clrCyan, clrBlue, progress/0.25)
+	case progress < 0.55:
+		return mixColor(clrBlue, clrViolet, (progress-0.25)/0.30)
+	case progress < 0.80:
+		return mixColor(clrViolet, clrPink, (progress-0.55)/0.25)
+	default:
+		return mixColor(clrPink, 0xFFFF8AA8, (progress-0.80)/0.20)
+	}
+}
+
 func makeRoundRectPath(x, y, w, h, r float64) uintptr {
 	var path uintptr
 	gpCreatePath.Call(fillModeAlternate, uintptr(unsafe.Pointer(&path)))
diff --git a/privatevoice.src/scripts/build-windows-preview.sh b/privatevoice.src/scripts/build-windows-preview.sh
index 680afe1..f715c3d 100755
--- a/privatevoice.src/scripts/build-windows-preview.sh
+++ b/privatevoice.src/scripts/build-windows-preview.sh
@@ -4,8 +4,8 @@
 ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 cd "$ROOT_DIR"
 
-APP_VERSION="2.1.38"
-APP_BUILD="20260629.2014"
+APP_VERSION="2.1.39"
+APP_BUILD="20260701.2356"
 ARCH="${1:-amd64}"
 
 if [[ "$ARCH" != "amd64" ]]; then

--
Gitblit v1.9.3