276d2bb6-619d-4888-ba11-8a6.../Sources/ui/ActionGrid.swift

146 lines
5.0 KiB
Swift
Raw Normal View History

//
// ActionGrid.swift
// plugin
//
// 2x2 grid of the four primary actions plus the segmented sub-options
// for translate-target and rewrite-tone. Tappable even when input is
// empty store decides whether to short-circuit.
//
import SwiftUI
struct ActionGrid: View {
@ObservedObject var store: ClipperStore
var body: some View {
VStack(spacing: 10) {
HStack(spacing: 10) {
actionCard(
icon: "globe",
title: "翻译",
subtitle: targetSubLabel,
accent: ClipperTheme.cyan
) {
store.runAction(.translate(target: store.translateTarget))
}
actionCard(
icon: "list.bullet.rectangle",
title: "总结",
subtitle: "3 条要点",
accent: ClipperTheme.lime
) {
store.runAction(.summarize)
}
}
HStack(spacing: 10) {
actionCard(
icon: "bubble.left.and.bubble.right",
title: "改语气",
subtitle: store.rewriteTone.label,
accent: ClipperTheme.amber
) {
store.runAction(.rewrite(tone: store.rewriteTone))
}
actionCard(
icon: "envelope",
title: "写邮件回复",
subtitle: "礼貌专业",
accent: Color(red: 0xFF/255, green: 0x9E/255, blue: 0xC4/255)
) {
store.runAction(.emailReply)
}
}
// Segmented controls for the two actions that have options.
VStack(spacing: 6) {
segmented(
title: "翻译目标",
options: ClipperAction.TranslateTarget.allCases.map { ($0.label, $0) },
selection: $store.translateTarget
)
segmented(
title: "改写语气",
options: ClipperAction.ToneOption.allCases.map { ($0.label, $0) },
selection: $store.rewriteTone
)
}
}
}
private var targetSubLabel: String {
"译为 " + store.translateTarget.label
}
// MARK: subviews
private func actionCard(icon: String,
title: String,
subtitle: String,
accent: Color,
action: @escaping () -> Void) -> some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 6) {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(accent)
Text(title)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(ClipperTheme.fgPrimary)
Text(subtitle)
.font(.system(size: 10.5))
.foregroundColor(ClipperTheme.fg55)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(ClipperTheme.overlay06)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(ClipperTheme.overlay08, lineWidth: 0.5)
)
)
}
.buttonStyle(.plain)
}
private func segmented<T: Hashable>(
title: String,
options: [(String, T)],
selection: Binding<T>
) -> some View {
HStack(spacing: 6) {
Text(title)
.font(.system(size: 10.5, weight: .medium))
.foregroundColor(ClipperTheme.fg45)
.frame(width: 56, alignment: .leading)
HStack(spacing: 4) {
ForEach(options, id: \.1) { (label, value) in
let isSel = selection.wrappedValue == value
Button {
selection.wrappedValue = value
} label: {
Text(label)
.font(.system(size: 11, weight: .semibold))
.foregroundColor(isSel
? Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255)
: ClipperTheme.fg70)
.padding(.horizontal, 10)
.frame(height: 22)
.background(
Capsule().fill(isSel ? ClipperTheme.lime : ClipperTheme.overlay04)
)
}
.buttonStyle(.plain)
}
Spacer(minLength: 0)
}
}
}
}