mirror of
https://github.com/carey314/mio-plugin-clipper.git
synced 2026-08-10 07:44: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>
166 lines
6.8 KiB
Swift
166 lines
6.8 KiB
Swift
//
|
||
// 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)
|
||
)
|
||
)
|
||
}
|
||
}
|