Files
openless/Sources/OpenLessUI/CapsuleView.swift
T
baiqing 88a5a97812 feat: 1.0.03 — 侧边栏重构 + 词汇表 chip 布局 + 风格页 + 关掉自动学词
- 侧边栏 4 → 6 项:首页 / 历史记录 / 词汇表 / 风格 / 帮助中心 / 设置
- 词典 → 词汇表,整页改 chip 流式布局:phrase + hitCount,hover ✕ 删除,
  批量输入(每行一个),重置统计 / 清除全部
- DictionaryEntry 加 hitCount + 兼容旧 dictionary.json 解码
- DictationCoordinator 在润色后扫描最终文本递增命中数;
  删除 learnTerms 自动学词路径
- 润色模式 → 风格 (StyleTab):4 张模式卡 + 启用开关;
  关闭时跳过 Ark 直接插入原文 (UserPreferences.polishEnabled)
- 帮助中心:快速上手 4 步、快捷键速查、FAQ、GitHub 外链
- 设置 Tab 合并 Volc ASR + Ark 凭据、录音、授权、隐私、关于 + 检查更新
- 胶囊:NSPanel 220×96, hasShadow=false;macOS 26 分支去掉 SwiftUI 阴影
  → 解决"红绿灯外圈黑边"
- 侧边栏视觉:strokeBorder 0.08 → 0.04、阴影 radius 16 → 6
  → 解决"灰色边栏 + 黑色卡顿"
- 首页底部新增今日概览 + 风格预览
2026-04-28 11:16:40 +08:00

180 lines
5.7 KiB
Swift

import SwiftUI
public struct CapsuleView: View {
private let state: CapsuleState
private let level: Float
private let onCancel: () -> Void
private let onConfirm: () -> Void
public init(
state: CapsuleState,
level: Float,
onCancel: @escaping () -> Void = {},
onConfirm: @escaping () -> Void = {}
) {
self.state = state
self.level = level
self.onCancel = onCancel
self.onConfirm = onConfirm
}
public var body: some View {
HStack(spacing: 8) {
cancelButton
centerView
confirmButton
}
.padding(.horizontal, 8)
.frame(width: 176, height: 42)
.openLessGlass()
.modifier(InputBarChrome(state: state))
}
@ViewBuilder
private var cancelButton: some View {
Button(action: onCancel) {
Image(systemName: "xmark")
.font(.system(size: 15, weight: .semibold))
.frame(width: 28, height: 28)
.foregroundStyle(state == .cancelled ? Color.red : Color.primary.opacity(0.78))
.background(.thinMaterial, in: Circle())
.overlay(Circle().strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.8))
.opacity(state == .hidden ? 0 : 1)
}
.buttonStyle(.plain)
.disabled(!isControlEnabled)
.help("取消")
}
@ViewBuilder
private var confirmButton: some View {
Button(action: onConfirm) {
Image(systemName: "checkmark")
.font(.system(size: 15, weight: .semibold))
.frame(width: 28, height: 28)
.foregroundStyle(state == .inserted ? Color.green : Color.primary)
.background(Color.white.opacity(0.92), in: Circle())
.overlay(Circle().strokeBorder(Color.primary.opacity(0.08), lineWidth: 0.8))
.opacity(state == .hidden ? 0 : 1)
}
.buttonStyle(.plain)
.disabled(!isControlEnabled)
.help("结束并整理")
}
@ViewBuilder
private var centerView: some View {
switch state {
case .listening:
AudioBars(level: level).frame(width: centerWidth)
case .processing:
HStack(spacing: 6) {
ProgressDots()
Text("正在思考中")
.font(.system(size: 10.5, weight: .medium))
.foregroundStyle(.primary)
}
.frame(width: centerWidth)
case .inserted:
statusText("已插入", color: .secondary)
case .cancelled:
statusText("已取消", color: .secondary)
case .copied:
statusText("已复制 ⌘V", color: .secondary)
case .error(let msg):
statusText(msg, color: .red)
case .warning(let msg):
statusText(msg, color: .orange)
case .hidden:
EmptyView()
}
}
private var centerWidth: CGFloat { 84 }
private var isControlEnabled: Bool {
state == .listening
}
private func statusText(_ text: String, color: Color) -> some View {
Text(text)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(color)
.lineLimit(1)
.frame(width: centerWidth)
}
}
private struct InputBarChrome: ViewModifier {
let state: CapsuleState
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
// macOS 26 用真 Liquid Glass,自带的 elevation 已经够了;不再叠 SwiftUI 阴影,
// 否则胶囊外圈会出现一圈灰边(panel 透明区被阴影染色)。
content
.overlay(
Capsule().strokeBorder(Color.white.opacity(0.34), lineWidth: 1)
)
} else {
content
.background(Color.white.opacity(0.18), in: Capsule())
.overlay(
Capsule().strokeBorder(Color.white.opacity(0.36), lineWidth: 1)
)
.shadow(color: .black.opacity(0.18), radius: 18, y: 8)
}
}
}
private struct AudioBars: View {
let level: Float
var body: some View {
HStack(spacing: 3) {
ForEach(0..<5, id: \.self) { i in
Capsule()
.fill(Color.accentColor.opacity(0.82))
.frame(width: 3, height: barHeight(index: i))
.animation(.spring(response: 0.18, dampingFraction: 0.7), value: level)
}
}
.frame(width: 42)
}
private func barHeight(index: Int) -> CGFloat {
// 中间高、两边低;level=0 时仍有 4pt 基线,避免视觉静止
let base: CGFloat = 4
let maxHeight: CGFloat = 18
let voice = CGFloat(min(1, max(0, level)))
let envelope: [CGFloat] = [0.55, 0.85, 1.0, 0.85, 0.55]
return base + (maxHeight - base) * voice * envelope[index]
}
}
private struct ProgressDots: View {
@State private var phase = 0.0
var body: some View {
HStack(spacing: 4) {
ForEach(0..<3, id: \.self) { i in
Circle()
.fill(Color.accentColor.opacity(0.85))
.frame(width: 4, height: 4)
.opacity(dotOpacity(index: i))
}
}
.frame(width: 20)
.onAppear {
withAnimation(.linear(duration: 0.9).repeatForever(autoreverses: false)) {
phase = 1.0
}
}
}
private func dotOpacity(index: Int) -> Double {
let p = (phase + Double(index) * 0.33).truncatingRemainder(dividingBy: 1)
return 0.3 + abs(sin(p * .pi)) * 0.7
}
}