mirror of
https://github.com/carey314/mio-plugin-clipper.git
synced 2026-08-09 23:34:32 +00:00
Quick AI actions on clipboard or selected text. Four actions: - 翻译 (中/英/日) - 总结 (3-bullet Chinese) - 改语气 (正式/口语/友好/严谨) - 邮件回复 Powered by DeepSeek with Server-Sent Events streaming for typewriter feedback (~600ms first byte). Two source modes: clipboard (no permission) and selected-text via NSAccessibility (graceful fallback to clipboard + permission banner if denied). Five-state UI machine: idle → input → loading (cancellable) → result → missing-config. Last 20 actions persisted to UserDefaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
5.0 KiB
Swift
146 lines
5.0 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
}
|