276d2bb6-619d-4888-ba11-8a6.../Sources/ui/ResultView.swift
徐翔宇 4e22618364 v0.1.0: initial 划词侠 — AI Text Actions plugin for MioIsland
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>
2026-04-27 20:21:17 +08:00

109 lines
3.9 KiB
Swift

//
// ResultView.swift
// plugin
//
// Scrollable result text + / / buttons.
//
import SwiftUI
struct ResultView: View {
@ObservedObject var store: ClipperStore
let actionLabel: String
let output: String
@State private var copyConfirmed = false
var body: some View {
VStack(spacing: 10) {
// Action header
HStack(spacing: 6) {
Image(systemName: "sparkles")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(ClipperTheme.lime)
Text(actionLabel)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(ClipperTheme.fgPrimary)
Spacer()
Text("\(output.count)")
.font(.system(size: 10.5))
.foregroundColor(ClipperTheme.fg40)
.monospacedDigit()
}
// Result body scrollable.
ScrollView {
Text(output)
.font(.system(size: 13))
.foregroundColor(ClipperTheme.fg85)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
.padding(.horizontal, 12)
.padding(.vertical, 10)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(ClipperTheme.overlay04)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(ClipperTheme.overlay08, lineWidth: 0.5)
)
)
// Action buttons
HStack(spacing: 8) {
resultButton(label: copyConfirmed ? "已复制" : "复制",
icon: copyConfirmed ? "checkmark" : "doc.on.doc",
tint: copyConfirmed ? ClipperTheme.lime : ClipperTheme.fg70) {
store.copyToPasteboard(output)
copyConfirmed = true
Task {
try? await Task.sleep(nanoseconds: 1_200_000_000)
await MainActor.run { copyConfirmed = false }
}
}
resultButton(label: "重新生成",
icon: "arrow.clockwise",
tint: ClipperTheme.fg70) {
store.regenerate()
}
resultButton(label: "完成",
icon: "checkmark.circle.fill",
tint: Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255),
filled: true) {
store.finishResult()
}
}
}
}
private func resultButton(label: String,
icon: String,
tint: Color,
filled: Bool = false,
action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 11, weight: .semibold))
Text(label)
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(tint)
.frame(maxWidth: .infinity)
.frame(height: 32)
.background(
Capsule()
.fill(filled ? ClipperTheme.lime : ClipperTheme.overlay06)
.overlay(
Capsule()
.stroke(filled ? Color.clear : ClipperTheme.overlay08, lineWidth: 0.5)
)
)
}
.buttonStyle(.plain)
}
}