276d2bb6-619d-4888-ba11-8a6.../Sources/ui/PermissionBanner.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

166 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// PermissionBanner.swift
// plugin
//
// Two banners share this file:
// Accessibility-permission warning (when sourceMode = .selection)
// Missing-config (state E) full-card with copy-path helper
//
import SwiftUI
struct AccessibilityBanner: View {
var onOpenSettings: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(ClipperTheme.amber)
.font(.system(size: 12))
.padding(.top, 1)
VStack(alignment: .leading, spacing: 4) {
Text("选中文本需要辅助功能权限")
.font(.system(size: 11.5, weight: .semibold))
.foregroundColor(ClipperTheme.fg85)
Text("前往 系统设置 > 隐私与安全性 > 辅助功能,勾选 Mio Island 后回来重试。当前已自动回退到粘贴板。")
.font(.system(size: 10.5))
.foregroundColor(ClipperTheme.fg55)
.fixedSize(horizontal: false, vertical: true)
Button(action: onOpenSettings) {
Text("打开 系统设置")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(ClipperTheme.amber)
}
.buttonStyle(.plain)
.padding(.top, 2)
}
Spacer(minLength: 0)
}
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(ClipperTheme.amber.opacity(0.08))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(ClipperTheme.amber.opacity(0.25), lineWidth: 0.5)
)
)
}
}
struct MissingConfigCard: View {
@ObservedObject var store: ClipperStore
@State private var copied = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Image(systemName: "key.horizontal")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(ClipperTheme.amber)
Text("先填一个 DeepSeek API Key")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(ClipperTheme.fgPrimary)
}
VStack(alignment: .leading, spacing: 6) {
Text("打开下面的配置文件,把 deepseek_api_key 改成你的真实 key然后点 重新加载。")
.font(.system(size: 11.5))
.foregroundColor(ClipperTheme.fg70)
.fixedSize(horizontal: false, vertical: true)
Text(ClipperConfig.configPath)
.font(.system(size: 10.5, design: .monospaced))
.foregroundColor(ClipperTheme.fg55)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(ClipperTheme.overlay04)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(ClipperTheme.overlay08, lineWidth: 0.5)
)
)
.textSelection(.enabled)
}
HStack(spacing: 8) {
Button {
store.copyToPasteboard(ClipperConfig.configPath)
copied = true
Task {
try? await Task.sleep(nanoseconds: 1_200_000_000)
await MainActor.run { copied = false }
}
} label: {
HStack(spacing: 6) {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
.font(.system(size: 11, weight: .semibold))
Text(copied ? "路径已复制" : "复制路径")
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(copied ? ClipperTheme.lime : ClipperTheme.fg70)
.frame(maxWidth: .infinity)
.frame(height: 30)
.background(
Capsule().fill(ClipperTheme.overlay06)
.overlay(Capsule().stroke(ClipperTheme.overlay08, lineWidth: 0.5))
)
}
.buttonStyle(.plain)
Button {
NSWorkspace.shared.open(URL(fileURLWithPath: ClipperConfig.configDir))
} label: {
HStack(spacing: 6) {
Image(systemName: "folder")
.font(.system(size: 11, weight: .semibold))
Text("打开文件夹")
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(ClipperTheme.fg70)
.frame(maxWidth: .infinity)
.frame(height: 30)
.background(
Capsule().fill(ClipperTheme.overlay06)
.overlay(Capsule().stroke(ClipperTheme.overlay08, lineWidth: 0.5))
)
}
.buttonStyle(.plain)
Button {
store.reloadConfig()
} label: {
HStack(spacing: 6) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 11, weight: .semibold))
Text("重新加载")
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(Color(red: 0x0B/255, green: 0x0B/255, blue: 0x0B/255))
.frame(maxWidth: .infinity)
.frame(height: 30)
.background(Capsule().fill(ClipperTheme.lime))
}
.buttonStyle(.plain)
}
Text("没有 key去 platform.deepseek.com 注册一个,新用户有免费额度。")
.font(.system(size: 10.5))
.foregroundColor(ClipperTheme.fg45)
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(ClipperTheme.overlay04)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(ClipperTheme.overlay08, lineWidth: 0.5)
)
)
}
}