Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

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
}
}
}