338 lines
12 KiB
Swift
338 lines
12 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import MacToolsPluginKit
|
|
|
|
public final class ActivityBarPluginFactory: NSObject, MacToolsPluginBundleFactory {
|
|
public static func makeProvider(context: PluginRuntimeContext) throws -> any PluginProvider {
|
|
ActivityBarPluginProvider(context: context)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private struct ActivityBarPluginProvider: PluginProvider {
|
|
let context: PluginRuntimeContext
|
|
|
|
func makePlugins() -> [any MacToolsPlugin] {
|
|
[
|
|
ActivityBarPlugin(
|
|
context: context,
|
|
localization: PluginLocalization(bundle: context.resourceBundle)
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class ActivityBarPlugin: MacToolsPlugin, PluginPrimaryPanel, PluginComponentPanel {
|
|
private enum ControlID {
|
|
static let trackingEnabled = "tracking-enabled"
|
|
static let installHooks = "install-hooks"
|
|
static let uninstallHooks = "uninstall-hooks"
|
|
static let resetToday = "reset-today"
|
|
static let openInputMonitoring = "open-input-monitoring"
|
|
}
|
|
|
|
let metadata: PluginMetadata
|
|
|
|
let primaryPanelDescriptor = PluginPrimaryPanelDescriptor(
|
|
controlStyle: .disclosure,
|
|
menuActionBehavior: .keepPresented
|
|
)
|
|
|
|
let descriptor = PluginComponentDescriptor(
|
|
span: PluginComponentSpan(
|
|
width: 4,
|
|
height: PluginComponentPanelLayoutMetrics.default.heightSpan(closestToOriginalSpanHeight: 10)
|
|
)!
|
|
)
|
|
|
|
private let localization: PluginLocalization
|
|
private let controller: ActivityBarController
|
|
private var isExpanded = false
|
|
|
|
var onStateChange: (() -> Void)? {
|
|
didSet {
|
|
controller.onStateChange = onStateChange
|
|
}
|
|
}
|
|
var requestPermissionGuidance: ((String) -> Void)?
|
|
var shortcutBindingResolver: ((String) -> ShortcutBinding?)?
|
|
|
|
init(
|
|
context: PluginRuntimeContext,
|
|
controller: ActivityBarController? = nil,
|
|
localization: PluginLocalization = PluginLocalization(bundle: .main)
|
|
) {
|
|
self.localization = localization
|
|
self.metadata = PluginMetadata(
|
|
id: ActivityBarController.pluginID,
|
|
title: localization.string("metadata.title", defaultValue: "活动统计"),
|
|
iconName: "chart.bar.xaxis",
|
|
iconTint: Color(nsColor: .systemGreen),
|
|
order: 18,
|
|
defaultDescription: localization.string(
|
|
"metadata.description",
|
|
defaultValue: "统计输入、前台应用使用时长和 AI 编程活动"
|
|
)
|
|
)
|
|
self.controller = controller ?? ActivityBarController(context: context, localization: localization)
|
|
}
|
|
|
|
var primaryPanelState: PluginPanelState {
|
|
PluginPanelState(
|
|
subtitle: controller.panelSubtitle,
|
|
isOn: controller.isTrackingEnabled,
|
|
isExpanded: isExpanded,
|
|
isEnabled: true,
|
|
isVisible: true,
|
|
detail: isExpanded ? panelDetail : nil,
|
|
errorMessage: controller.lastErrorMessage
|
|
)
|
|
}
|
|
|
|
var componentPanelState: PluginComponentState {
|
|
PluginComponentState(
|
|
subtitle: controller.componentSubtitle,
|
|
isActive: controller.isTrackingEnabled || controller.isHookListenerRunning,
|
|
isEnabled: true,
|
|
isVisible: true,
|
|
errorMessage: controller.lastErrorMessage
|
|
)
|
|
}
|
|
|
|
var settingsSections: [PluginSettingsSection] {
|
|
[
|
|
PluginSettingsSection(
|
|
id: "input-monitoring",
|
|
title: localization.string("settings.inputMonitoring.title", defaultValue: "输入监控"),
|
|
description: localization.string(
|
|
"settings.inputMonitoring.description",
|
|
defaultValue: "用于统计键盘、鼠标点击和滚动事件。"
|
|
),
|
|
status: inputMonitoringSettingsStatus,
|
|
footnote: controller.inputMonitoringFootnote,
|
|
buttonTitle: localization.string("settings.inputMonitoring.button", defaultValue: "打开系统设置"),
|
|
actionID: ControlID.openInputMonitoring
|
|
),
|
|
PluginSettingsSection(
|
|
id: "ai-hooks",
|
|
title: localization.string("settings.aiHooks.title", defaultValue: "AI 工具 Hook"),
|
|
description: localization.string(
|
|
"settings.aiHooks.description",
|
|
defaultValue: "记录 Claude Code、Cursor 和 Codex 的提示、工具调用与执行时长。"
|
|
),
|
|
status: hookSettingsStatus,
|
|
footnote: controller.hookActionFootnote,
|
|
buttonTitle: hookActionButtonTitle,
|
|
actionID: hookActionControlID
|
|
)
|
|
]
|
|
}
|
|
|
|
func activate(context: PluginRuntimeContext) {
|
|
controller.activate(context: context)
|
|
}
|
|
|
|
func deactivate(reason: PluginDeactivationReason) {
|
|
controller.deactivate(reason: reason)
|
|
}
|
|
|
|
func refresh() {
|
|
controller.refresh()
|
|
}
|
|
|
|
func handleAction(_ action: PluginPanelAction) {
|
|
switch action {
|
|
case let .setSwitch(isEnabled):
|
|
controller.setTrackingEnabled(isEnabled)
|
|
case let .setDisclosureExpanded(value):
|
|
isExpanded = value
|
|
onStateChange?()
|
|
case let .invokeAction(controlID):
|
|
handleAction(controlID: controlID)
|
|
case .setSelection, .setNavigationSelection,
|
|
.clearNavigationSelection, .setDate, .setSlider:
|
|
return
|
|
}
|
|
}
|
|
|
|
func makeView(context: PluginComponentContext) -> AnyView {
|
|
AnyView(
|
|
ActivityBarComponentView(
|
|
controller: controller,
|
|
localization: localization,
|
|
dismiss: context.dismiss
|
|
)
|
|
)
|
|
}
|
|
|
|
func permissionState(for permissionID: String) -> PluginPermissionState {
|
|
PluginPermissionState(isGranted: true, footnote: nil)
|
|
}
|
|
|
|
func handlePermissionAction(id: String) {}
|
|
|
|
func handleSettingsAction(id: String) {
|
|
handleAction(controlID: id)
|
|
}
|
|
|
|
func handleShortcutAction(id: String) {}
|
|
|
|
private var panelDetail: PluginPanelDetail {
|
|
let trackingControl = PluginPanelControl(
|
|
id: ControlID.trackingEnabled,
|
|
kind: .switchRow,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: nil,
|
|
actionTitle: localization.string("panel.action.trackingEnabled", defaultValue: "活动统计"),
|
|
actionIconSystemName: "chart.bar.xaxis",
|
|
isEnabled: true
|
|
)
|
|
|
|
let openSettingsControl = PluginPanelControl(
|
|
id: ControlID.openInputMonitoring,
|
|
kind: .actionRow,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: nil,
|
|
actionTitle: localization.string("panel.action.openInputMonitoring", defaultValue: "打开输入监控设置"),
|
|
actionIconSystemName: "keyboard.badge.eye",
|
|
isEnabled: true
|
|
)
|
|
|
|
let hookActionControl = PluginPanelControl(
|
|
id: hookActionControlID,
|
|
kind: .actionRow,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: nil,
|
|
actionTitle: hookActionPanelTitle,
|
|
actionIconSystemName: controller.areHooksInstalled ? "trash" : "terminal",
|
|
isEnabled: true
|
|
)
|
|
|
|
let resetControl = PluginPanelControl(
|
|
id: ControlID.resetToday,
|
|
kind: .actionRow,
|
|
options: [],
|
|
selectedOptionID: nil,
|
|
dateValue: nil,
|
|
minimumDate: nil,
|
|
displayedComponents: nil,
|
|
datePickerStyle: nil,
|
|
sectionTitle: nil,
|
|
actionTitle: localization.string("panel.action.resetToday", defaultValue: "清空今日统计"),
|
|
actionIconSystemName: "arrow.counterclockwise",
|
|
showsLeadingDivider: true,
|
|
isEnabled: true
|
|
)
|
|
|
|
return PluginPanelDetail(
|
|
primaryControls: [trackingControl, openSettingsControl, hookActionControl, resetControl],
|
|
secondaryPanel: nil
|
|
)
|
|
}
|
|
|
|
private var hookActionControlID: String {
|
|
controller.areHooksInstalled ? ControlID.uninstallHooks : ControlID.installHooks
|
|
}
|
|
|
|
private var hookActionButtonTitle: String {
|
|
if controller.areHooksInstalled {
|
|
return localization.string("settings.aiHooks.uninstallButton", defaultValue: "卸载 Hook")
|
|
}
|
|
|
|
return localization.string("settings.aiHooks.button", defaultValue: "安装或更新 Hook")
|
|
}
|
|
|
|
private var hookActionPanelTitle: String {
|
|
if controller.areHooksInstalled {
|
|
return localization.string("panel.action.uninstallHooks", defaultValue: "卸载 AI Hook")
|
|
}
|
|
|
|
return localization.string("panel.action.installHooks", defaultValue: "安装或更新 AI Hook")
|
|
}
|
|
|
|
private var inputMonitoringSettingsStatus: PluginSettingsSection.Status {
|
|
switch controller.monitorStatus {
|
|
case .running:
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("status.running", defaultValue: "运行中"),
|
|
systemImage: "checkmark.circle.fill",
|
|
tone: .positive
|
|
)
|
|
case .inputMonitoringDenied:
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("status.permissionRequired", defaultValue: "需要授权"),
|
|
systemImage: "exclamationmark.triangle.fill",
|
|
tone: .caution
|
|
)
|
|
case .idle:
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("status.disabled", defaultValue: "未开启"),
|
|
systemImage: "pause.circle",
|
|
tone: .neutral
|
|
)
|
|
}
|
|
}
|
|
|
|
private var hookSettingsStatus: PluginSettingsSection.Status {
|
|
switch controller.hookInstallState {
|
|
case .installed:
|
|
if controller.isHookListenerRunning {
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("hook.status.listening", defaultValue: "监听中"),
|
|
systemImage: "checkmark.circle.fill",
|
|
tone: .positive
|
|
)
|
|
}
|
|
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("hook.status.installed", defaultValue: "已安装"),
|
|
systemImage: "checkmark.circle.fill",
|
|
tone: .positive
|
|
)
|
|
case .failed:
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("hook.status.installFailed", defaultValue: "安装失败"),
|
|
systemImage: "exclamationmark.triangle.fill",
|
|
tone: .caution
|
|
)
|
|
case .notInstalled:
|
|
return PluginSettingsSection.Status(
|
|
text: localization.string("hook.status.notInstalled", defaultValue: "未安装"),
|
|
systemImage: "terminal",
|
|
tone: .neutral
|
|
)
|
|
}
|
|
}
|
|
|
|
private func handleAction(controlID: String) {
|
|
switch controlID {
|
|
case ControlID.installHooks:
|
|
controller.installHooks()
|
|
case ControlID.uninstallHooks:
|
|
controller.uninstallHooks()
|
|
case ControlID.resetToday:
|
|
controller.resetToday()
|
|
case ControlID.openInputMonitoring:
|
|
controller.openInputMonitoringSettings()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|