From ddc36043cc890472902d6d73a1d37f2c20aba144 Mon Sep 17 00:00:00 2001 From: huangxida Date: Tue, 13 Jan 2026 10:29:40 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E8=BE=93=E5=85=A5=E9=80=9F?= =?UTF-8?q?=E7=8E=87=E9=A9=B1=E5=8A=A8=E7=8A=B6=E6=80=81=E6=A0=8F=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A2=9C=E8=89=B2=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据近期输入速率自动计算颜色,并在菜单栏实时展示。 提供图标着色或状态圆点两种展示方式,可在设置中切换。 动态颜色仅在开启后生效,低速时保持默认样式。 展示方式与颜色更新保持同步,确保菜单栏反馈直观一致。 --- KeyStats/MenuBarController.swift | 59 +++++++- KeyStats/SettingsViewController.swift | 50 ++++++- KeyStats/StatsManager.swift | 149 ++++++++++++++++++++- KeyStats/StatsPopoverViewController.swift | 2 +- KeyStats/en.lproj/Localizable.strings | 4 + KeyStats/zh-Hans.lproj/Localizable.strings | 4 + 6 files changed, 261 insertions(+), 7 deletions(-) diff --git a/KeyStats/MenuBarController.swift b/KeyStats/MenuBarController.swift index 3f298e6..7eb61d7 100644 --- a/KeyStats/MenuBarController.swift +++ b/KeyStats/MenuBarController.swift @@ -1,5 +1,10 @@ import Cocoa +enum DynamicIconColorStyle: String { + case icon + case dot +} + /// 菜单栏控制器 class MenuBarController { @@ -7,6 +12,7 @@ class MenuBarController { private var statusView: MenuBarStatusView? private var popover: NSPopover! private var eventMonitor: Any? + private let dynamicIconColorStyleKey = "dynamicIconColorStyle" init() { setupStatusItem() @@ -99,12 +105,21 @@ class MenuBarController { private func updateMenuBarAppearance() { let parts = StatsManager.shared.getMenuBarTextParts() + let tintColor = StatsManager.shared.enableDynamicIconColor + ? StatsManager.shared.currentIconTintColor + : nil + let styleValue = UserDefaults.standard.string(forKey: dynamicIconColorStyleKey) ?? DynamicIconColorStyle.icon.rawValue + let style = DynamicIconColorStyle(rawValue: styleValue) ?? .icon + if let statusView = statusView { statusView.update(keysText: parts.keys, clicksText: parts.clicks) + statusView.updateIconColor(tintColor, style: style) statusItem.length = statusView.intrinsicContentSize.width } else if let button = statusItem.button { button.attributedTitle = makeStatusTitle(keysText: parts.keys, clicksText: parts.clicks) + button.contentTintColor = style == .icon ? tintColor : nil } + } private func makeStatusTitle(keysText: String, clicksText: String) -> NSAttributedString { @@ -154,7 +169,9 @@ class MenuBarController { // MARK: - 菜单栏自定义视图 class MenuBarStatusView: NSView { + private let iconContainer = NSView() private let imageView = NSImageView() + private let colorDotView = NSView() private let topLabel = NSTextField(labelWithString: "0") private let bottomLabel = NSTextField(labelWithString: "0") private let stack = NSStackView() @@ -192,6 +209,16 @@ class MenuBarStatusView: NSView { imageView.imageAlignment = .alignCenter imageView.contentTintColor = .labelColor imageView.translatesAutoresizingMaskIntoConstraints = false + + iconContainer.translatesAutoresizingMaskIntoConstraints = false + iconContainer.addSubview(imageView) + + colorDotView.wantsLayer = true + colorDotView.layer?.cornerRadius = 3 + colorDotView.layer?.backgroundColor = NSColor.clear.cgColor + colorDotView.translatesAutoresizingMaskIntoConstraints = false + colorDotView.isHidden = true + iconContainer.addSubview(colorDotView) topLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .semibold) bottomLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .medium) @@ -210,7 +237,7 @@ class MenuBarStatusView: NSView { stack.alignment = .centerY stack.spacing = 4 stack.translatesAutoresizingMaskIntoConstraints = false - stack.addArrangedSubview(imageView) + stack.addArrangedSubview(iconContainer) stack.addArrangedSubview(textStack) addSubview(stack) @@ -219,8 +246,16 @@ class MenuBarStatusView: NSView { stackTrailingConstraint = stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding) NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 18), - imageView.heightAnchor.constraint(equalToConstant: 18), + iconContainer.widthAnchor.constraint(equalToConstant: 18), + iconContainer.heightAnchor.constraint(equalToConstant: 18), + imageView.leadingAnchor.constraint(equalTo: iconContainer.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: iconContainer.trailingAnchor), + imageView.topAnchor.constraint(equalTo: iconContainer.topAnchor), + imageView.bottomAnchor.constraint(equalTo: iconContainer.bottomAnchor), + colorDotView.widthAnchor.constraint(equalToConstant: 6), + colorDotView.heightAnchor.constraint(equalToConstant: 6), + colorDotView.leadingAnchor.constraint(equalTo: iconContainer.leadingAnchor, constant: -3), + colorDotView.topAnchor.constraint(equalTo: iconContainer.topAnchor, constant: -3), stackLeadingConstraint, stackTrailingConstraint, stack.centerYAnchor.constraint(equalTo: centerYAnchor) @@ -250,6 +285,24 @@ class MenuBarStatusView: NSView { needsLayout = true } + func updateIconColor(_ color: NSColor?, style: DynamicIconColorStyle) { + guard let color = color else { + imageView.contentTintColor = .labelColor + colorDotView.isHidden = true + return + } + + switch style { + case .icon: + imageView.contentTintColor = color + colorDotView.isHidden = true + case .dot: + imageView.contentTintColor = .labelColor + colorDotView.layer?.backgroundColor = color.cgColor + colorDotView.isHidden = false + } + } + private func updateHorizontalPadding(hasText: Bool) { horizontalPadding = hasText ? 6 : 4 stackLeadingConstraint.constant = horizontalPadding diff --git a/KeyStats/SettingsViewController.swift b/KeyStats/SettingsViewController.swift index d684e13..d18950b 100644 --- a/KeyStats/SettingsViewController.swift +++ b/KeyStats/SettingsViewController.swift @@ -6,6 +6,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { private var showKeyPressesButton: NSButton! private var showMouseClicksButton: NSButton! private var launchAtLoginButton: NSButton! + private var dynamicIconColorButton: NSButton! + private var dynamicIconColorStylePopUp: NSPopUpButton! private var resetButton: NSButton! private var showThresholdsButton: NSButton! private var thresholdStack: NSStackView! @@ -17,6 +19,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { private let thresholdMinimum = 0 private let thresholdMaximum = 1_000_000 private let thresholdStep = 100.0 + private let dynamicIconColorStyleKey = "dynamicIconColorStyle" private lazy var thresholdFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -71,12 +74,33 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { target: self, action: #selector(toggleShowThresholds)) - let optionsStack = NSStackView(views: [showKeyPressesButton, showMouseClicksButton, launchAtLoginButton, showThresholdsButton]) + dynamicIconColorButton = NSButton(checkboxWithTitle: NSLocalizedString("settings.dynamicIconColor", comment: ""), + target: self, + action: #selector(toggleDynamicIconColor)) + + dynamicIconColorStylePopUp = NSPopUpButton() + let iconStyleTitle = NSLocalizedString("settings.dynamicIconColorStyle.icon", comment: "") + let dotStyleTitle = NSLocalizedString("settings.dynamicIconColorStyle.dot", comment: "") + dynamicIconColorStylePopUp.addItems(withTitles: [iconStyleTitle, dotStyleTitle]) + dynamicIconColorStylePopUp.item(at: 0)?.representedObject = DynamicIconColorStyle.icon.rawValue + dynamicIconColorStylePopUp.item(at: 1)?.representedObject = DynamicIconColorStyle.dot.rawValue + dynamicIconColorStylePopUp.target = self + dynamicIconColorStylePopUp.action = #selector(dynamicIconColorStyleChanged) + + let styleLabel = NSTextField(labelWithString: NSLocalizedString("settings.dynamicIconColorStyle", comment: "")) + styleLabel.font = NSFont.systemFont(ofSize: 13) + let styleRow = NSStackView(views: [styleLabel, dynamicIconColorStylePopUp]) + styleRow.orientation = .horizontal + styleRow.alignment = .centerY + styleRow.spacing = 8 + styleRow.translatesAutoresizingMaskIntoConstraints = false + + let optionsStack = NSStackView(views: [showKeyPressesButton, showMouseClicksButton, launchAtLoginButton, dynamicIconColorButton, styleRow, showThresholdsButton]) optionsStack.orientation = .vertical optionsStack.alignment = .leading optionsStack.spacing = 8 optionsStack.translatesAutoresizingMaskIntoConstraints = false - + keyPressThresholdField = makeThresholdField() keyPressThresholdStepper = makeThresholdStepper(action: #selector(keyPressThresholdStepperChanged)) clickThresholdField = makeThresholdField() @@ -130,12 +154,23 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { showKeyPressesButton.state = StatsManager.shared.showKeyPressesInMenuBar ? .on : .off showMouseClicksButton.state = StatsManager.shared.showMouseClicksInMenuBar ? .on : .off launchAtLoginButton.state = LaunchAtLoginManager.shared.isEnabled ? .on : .off + dynamicIconColorButton.state = StatsManager.shared.enableDynamicIconColor ? .on : .off + updateDynamicIconColorStyleSelection() let notificationsEnabled = StatsManager.shared.notificationsEnabled showThresholdsButton.state = notificationsEnabled ? .on : .off thresholdStack.isHidden = !notificationsEnabled updateThresholdUI() } + private func updateDynamicIconColorStyleSelection() { + let styleValue = UserDefaults.standard.string(forKey: dynamicIconColorStyleKey) ?? DynamicIconColorStyle.icon.rawValue + let style = DynamicIconColorStyle(rawValue: styleValue) ?? .icon + if let item = dynamicIconColorStylePopUp.itemArray.first(where: { ($0.representedObject as? String) == style.rawValue }) { + dynamicIconColorStylePopUp.select(item) + } + dynamicIconColorStylePopUp.isEnabled = StatsManager.shared.enableDynamicIconColor + } + // MARK: - 通知阈值 private enum ThresholdType { @@ -263,6 +298,17 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { StatsManager.shared.showMouseClicksInMenuBar = (showMouseClicksButton.state == .on) } + @objc private func toggleDynamicIconColor() { + StatsManager.shared.enableDynamicIconColor = dynamicIconColorButton.state == .on + updateDynamicIconColorStyleSelection() + } + + @objc private func dynamicIconColorStyleChanged() { + guard let rawValue = dynamicIconColorStylePopUp.selectedItem?.representedObject as? String else { return } + UserDefaults.standard.set(rawValue, forKey: dynamicIconColorStyleKey) + StatsManager.shared.menuBarUpdateHandler?() + } + @objc private func toggleLaunchAtLogin() { let shouldEnable = launchAtLoginButton.state == .on do { diff --git a/KeyStats/StatsManager.swift b/KeyStats/StatsManager.swift index ca02fec..f565daa 100644 --- a/KeyStats/StatsManager.swift +++ b/KeyStats/StatsManager.swift @@ -1,5 +1,6 @@ import Foundation import Cocoa +import UserNotifications private let metersPerPixel: Double = 0.000264583 @@ -70,6 +71,8 @@ class StatsManager { private let keyPressNotifyThresholdKey = "keyPressNotifyThreshold" private let clickNotifyThresholdKey = "clickNotifyThreshold" private let notificationsEnabledKey = "notificationsEnabled" + private let enableDynamicIconColorKey = "enableDynamicIconColor" + private let dynamicIconColorStyleKey = "dynamicIconColorStyle" private let dateFormatter: DateFormatter private var history: [String: DailyStats] = [:] private var saveTimer: Timer? @@ -77,7 +80,19 @@ class StatsManager { private var midnightCheckTimer: Timer? private let saveInterval: TimeInterval = 2.0 private let statsUpdateDebounceInterval: TimeInterval = 0.3 + private let inputRateWindowSeconds: TimeInterval = 3.0 + private let inputRateBucketInterval: TimeInterval = 0.5 + private let inputRateApmThresholds: [Double] = [0, 80, 160, 240] + private let inputRateLock = NSLock() private var isReadyForUpdates = false + private lazy var inputRateBuckets: [Int] = { + let bucketCount = max(1, Int(inputRateWindowSeconds / inputRateBucketInterval)) + return Array(repeating: 0, count: bucketCount) + }() + private var inputRateBucketIndex = 0 + private var inputRateTimer: Timer? + private(set) var currentInputRatePerSecond: Double = 0 + private(set) var currentIconTintColor: NSColor? var menuBarUpdateHandler: (() -> Void)? var statsUpdateHandler: (() -> Void)? @@ -123,6 +138,28 @@ class StatsManager { } } + /// 设置:是否启用动态图标颜色 + var enableDynamicIconColor: Bool { + didSet { + userDefaults.set(enableDynamicIconColor, forKey: enableDynamicIconColorKey) + let applyChanges = { [weak self] in + guard let self = self else { return } + if self.enableDynamicIconColor { + self.resetInputRateBuckets() + self.startInputRateTracking() + } else { + self.stopInputRateTracking() + } + self.updateCurrentInputRate() + } + if Thread.isMainThread { + applyChanges() + return + } + DispatchQueue.main.async(execute: applyChanges) + } + } + private var lastNotifiedKeyPresses: Int = 0 private var lastNotifiedClicks: Int = 0 @@ -141,12 +178,13 @@ class StatsManager { dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" - // 加载设置(默认为 true) + // 加载设置(按键/点击默认 true,通知/动态图标默认 false) showKeyPressesInMenuBar = userDefaults.object(forKey: showKeyPressesKey) as? Bool ?? true showMouseClicksInMenuBar = userDefaults.object(forKey: showMouseClicksKey) as? Bool ?? true notificationsEnabled = userDefaults.object(forKey: notificationsEnabledKey) as? Bool ?? false keyPressNotifyThreshold = userDefaults.object(forKey: keyPressNotifyThresholdKey) as? Int ?? 1000 clickNotifyThreshold = userDefaults.object(forKey: clickNotifyThresholdKey) as? Int ?? 1000 + enableDynamicIconColor = userDefaults.object(forKey: enableDynamicIconColorKey) as? Bool ?? false // 先初始化 currentStats 为默认值 let calendar = Calendar.current @@ -164,6 +202,11 @@ class StatsManager { isReadyForUpdates = true saveStats() + if enableDynamicIconColor { + resetInputRateBuckets() + startInputRateTracking() + updateCurrentInputRate() + } setupMidnightReset() } @@ -176,6 +219,7 @@ class StatsManager { if let keyName = keyName, !keyName.isEmpty { currentStats.keyPressCounts[keyName, default: 0] += 1 } + registerInputEvent() notifyMenuBarUpdate() notifyStatsUpdate() notifyKeyPressThresholdIfNeeded() @@ -184,6 +228,7 @@ class StatsManager { func incrementLeftClicks() { ensureCurrentDay() currentStats.leftClicks += 1 + registerInputEvent() notifyMenuBarUpdate() notifyStatsUpdate() notifyClickThresholdIfNeeded() @@ -192,6 +237,7 @@ class StatsManager { func incrementRightClicks() { ensureCurrentDay() currentStats.rightClicks += 1 + registerInputEvent() notifyMenuBarUpdate() notifyStatsUpdate() notifyClickThresholdIfNeeded() @@ -206,9 +252,108 @@ class StatsManager { func addScrollDistance(_ distance: Double) { ensureCurrentDay() currentStats.scrollDistance += abs(distance) + registerInputEvent() scheduleDebouncedStatsUpdate() } + // MARK: - 输入速率 + + func registerInputEvent() { + guard enableDynamicIconColor else { return } + inputRateLock.lock() + inputRateBuckets[inputRateBucketIndex] += 1 + inputRateLock.unlock() + } + + private func resetInputRateBuckets() { + inputRateLock.lock() + inputRateBuckets = Array(repeating: 0, count: inputRateBuckets.count) + inputRateBucketIndex = 0 + inputRateLock.unlock() + } + + private func startInputRateTracking() { + if !Thread.isMainThread { + DispatchQueue.main.async { [weak self] in + self?.startInputRateTracking() + } + return + } + + inputRateTimer?.invalidate() + inputRateTimer = Timer.scheduledTimer(withTimeInterval: inputRateBucketInterval, repeats: true) { [weak self] _ in + self?.advanceInputRateBucket() + } + if let timer = inputRateTimer { + RunLoop.main.add(timer, forMode: .common) + } + } + + private func stopInputRateTracking() { + inputRateTimer?.invalidate() + inputRateTimer = nil + } + + private func advanceInputRateBucket() { + inputRateLock.lock() + inputRateBucketIndex = (inputRateBucketIndex + 1) % inputRateBuckets.count + inputRateBuckets[inputRateBucketIndex] = 0 + inputRateLock.unlock() + updateCurrentInputRate() + } + + private func updateCurrentInputRate() { + inputRateLock.lock() + let totalEvents = inputRateBuckets.reduce(0, +) + inputRateLock.unlock() + currentInputRatePerSecond = Double(totalEvents) / inputRateWindowSeconds + currentIconTintColor = enableDynamicIconColor ? colorForRate(currentInputRatePerSecond) : nil + notifyMenuBarUpdate() + } + + private func colorForRate(_ ratePerSecond: Double) -> NSColor? { + let apm = ratePerSecond * 60 + let thresholds = inputRateApmThresholds + if apm < thresholds[1] { return nil } + if apm >= thresholds[3] { return .systemRed } + + if apm <= thresholds[2] { + let progress = (apm - thresholds[1]) / (thresholds[2] - thresholds[1]) + let lightGreen = lightenColor(.systemGreen, fraction: 0.6) + return interpolateColor(from: lightGreen, to: .systemGreen, progress: progress) + } + + let progress = (apm - thresholds[2]) / (thresholds[3] - thresholds[2]) + return interpolateColor(from: .systemYellow, to: .systemRed, progress: progress) + } + + private func interpolateColor(from: NSColor, to: NSColor, progress: Double) -> NSColor { + let fromColor = from.usingColorSpace(.deviceRGB) ?? from + let toColor = to.usingColorSpace(.deviceRGB) ?? to + var fr: CGFloat = 0 + var fg: CGFloat = 0 + var fb: CGFloat = 0 + var fa: CGFloat = 0 + var tr: CGFloat = 0 + var tg: CGFloat = 0 + var tb: CGFloat = 0 + var ta: CGFloat = 0 + fromColor.getRed(&fr, green: &fg, blue: &fb, alpha: &fa) + toColor.getRed(&tr, green: &tg, blue: &tb, alpha: &ta) + let t = CGFloat(max(0, min(1, progress))) + return NSColor( + red: fr + (tr - fr) * t, + green: fg + (tg - fg) * t, + blue: fb + (tb - fb) * t, + alpha: fa + (ta - fa) * t + ) + } + + private func lightenColor(_ color: NSColor, fraction: CGFloat) -> NSColor { + let resolved = color.usingColorSpace(.deviceRGB) ?? color + return resolved.blended(withFraction: min(max(fraction, 0), 1), of: .white) ?? resolved + } + // MARK: - 通知阈值 private func updateNotificationBaselines() { @@ -334,6 +479,8 @@ class StatsManager { statsUpdateTimer = nil midnightCheckTimer?.invalidate() midnightCheckTimer = nil + inputRateTimer?.invalidate() + inputRateTimer = nil saveStats() } diff --git a/KeyStats/StatsPopoverViewController.swift b/KeyStats/StatsPopoverViewController.swift index f4ac0a8..7a687d9 100644 --- a/KeyStats/StatsPopoverViewController.swift +++ b/KeyStats/StatsPopoverViewController.swift @@ -328,7 +328,7 @@ class StatsPopoverViewController: NSViewController { historySummaryLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), // 底部分隔线 - bottomSeparator.topAnchor.constraint(equalTo: historySummaryLabel.bottomAnchor, constant: 12), + bottomSeparator.topAnchor.constraint(equalTo: historySummaryLabel.bottomAnchor, constant: 16), bottomSeparator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), bottomSeparator.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), diff --git a/KeyStats/en.lproj/Localizable.strings b/KeyStats/en.lproj/Localizable.strings index bab79ee..6ae6ba9 100644 --- a/KeyStats/en.lproj/Localizable.strings +++ b/KeyStats/en.lproj/Localizable.strings @@ -5,6 +5,10 @@ "button.permission" = "Grant Permission"; "button.launchAtLogin" = "Open at Login"; "settings.title" = "Settings"; +"settings.dynamicIconColor" = "Dynamic Icon Color"; +"settings.dynamicIconColorStyle" = "Dynamic Color Display"; +"settings.dynamicIconColorStyle.icon" = "Tint Icon"; +"settings.dynamicIconColorStyle.dot" = "Status Dot"; "settings.windowTitle" = "KeyStats Settings"; "button.back" = "Back"; "setting.showKeyPresses" = "Show Key Presses in Menu Bar"; diff --git a/KeyStats/zh-Hans.lproj/Localizable.strings b/KeyStats/zh-Hans.lproj/Localizable.strings index a0d9e31..eb307ec 100644 --- a/KeyStats/zh-Hans.lproj/Localizable.strings +++ b/KeyStats/zh-Hans.lproj/Localizable.strings @@ -5,6 +5,10 @@ "button.permission" = "获取权限"; "button.launchAtLogin" = "开机启动"; "settings.title" = "设置"; +"settings.dynamicIconColor" = "动态图标颜色"; +"settings.dynamicIconColorStyle" = "动态颜色显示"; +"settings.dynamicIconColorStyle.icon" = "图标着色"; +"settings.dynamicIconColorStyle.dot" = "状态圆点"; "settings.windowTitle" = "KeyStats设置"; "button.back" = "返回"; "setting.showKeyPresses" = "在菜单栏显示按键数"; From 4cdbe7ec1a7277015635d836325fdf7708c80b71 Mon Sep 17 00:00:00 2001 From: huangxida Date: Tue, 13 Jan 2026 12:57:06 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A2=9C=E8=89=B2=E8=AF=B4=E6=98=8E=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在设置页增加问号帮助弹窗,展示输入速率与颜色范围说明及截图。 --- .../DynamicColorTip1.imageset/Contents.json | 13 +++ .../DynamicColorTip1.imageset/tip1.png | Bin 0 -> 9033 bytes .../DynamicColorTip2.imageset/Contents.json | 13 +++ .../DynamicColorTip2.imageset/tip2.png | Bin 0 -> 9058 bytes .../DynamicColorTip3.imageset/Contents.json | 13 +++ .../DynamicColorTip3.imageset/tip3.png | Bin 0 -> 8915 bytes .../DynamicColorTip4.imageset/Contents.json | 13 +++ .../DynamicColorTip4.imageset/tip4.png | Bin 0 -> 9515 bytes .../DynamicColorTip5.imageset/Contents.json | 13 +++ .../DynamicColorTip5.imageset/tip5.png | Bin 0 -> 9517 bytes .../DynamicColorTip6.imageset/Contents.json | 13 +++ .../DynamicColorTip6.imageset/tip6.png | Bin 0 -> 9466 bytes KeyStats/SettingsViewController.swift | 103 +++++++++++++++++- KeyStats/en.lproj/Localizable.strings | 2 + KeyStats/zh-Hans.lproj/Localizable.strings | 2 + 15 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip1.imageset/tip1.png create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip2.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip2.imageset/tip2.png create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip3.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip3.imageset/tip3.png create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip4.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip4.imageset/tip4.png create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip5.imageset/tip5.png create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip6.imageset/Contents.json create mode 100644 KeyStats/Assets.xcassets/DynamicColorTip6.imageset/tip6.png diff --git a/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json new file mode 100644 index 0000000..b282990 --- /dev/null +++ b/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "tip1.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/tip1.png b/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/tip1.png new file mode 100644 index 0000000000000000000000000000000000000000..e7dfc704b91453385c28aa1ddae67c212e55eb3e GIT binary patch literal 9033 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

@aata~U%HGH>8xP~7YE8;6bMSfdVDG7? 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 F{|EiD1Iqva literal 0 HcmV?d00001 diff --git a/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json new file mode 100644 index 0000000..82bdaa2 --- /dev/null +++ b/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "tip5.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/tip5.png b/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/tip5.png new file mode 100644 index 0000000000000000000000000000000000000000..74449979f32647f09e2c0bbc7d24ee7d0ff1f7d3 GIT binary patch literal 9517 zcmZ{JWmKHavhLsncXxujy9Kwwg1fs8&Hy2}4gmro1PBgE@Zc8Qg9UdF&Y%MfaFcKE zv(LF_-P>!`TlH2~Jtf^gYORjf1}bA?P+$N60Blti1>HaV^&gT%NBr{*sKO`!05B?? z<>j?i<>l$LeLNkU-RuDXmH78rXs`7aNn$2CL~^4@;@)IT=J-YcbYD3Y+%mJ%zra(& zilDPHL{;|Uj8oA=rcPirLLlcL;lxVl&`On4q(;LOe!Yf5V(qXNd{)^szjOX*d`6Vj*^W~nuVDaW_fUB^SF=b7bLXXmBhbA;QIJJKkImt-iHSi){8c*GwH&+b zB$?Wp@aP~Z#5eRqV257U6sJ6R#Fpnr}hffg4g8;H0C!3$rsHTu_3%@ZaQ7RaV zceCli!``Mdlgob?i^p;-`1S%2F}4|}2cXOO>>bQ4f>J4eBfn@eM2N2QQ+|<40K|v8 z3(U}*q{1H|ye3x-uSmaW`lwp>kqVfQRCdGfjLaj~$Qqgc98NR%+&G3cd_^%(p03v) zw)s2mDRT1cAQ8!qQv(Ah+X2CoKRiv3DNJW(Y}}z=3*@ha9P%-Usv2ONo3!!68At*f z@-7o?rSf z3wN7vj*w#N>Vkf2T$Mr(-00_ha8~D*<`CY*he%wTw6{qF(b7{`GaJO`iEv;=?__^i zwFlg61Lz#a`!uQnpC_??lsHu>e3BsU)sU=tgG}@WLmO>Lmi7Yx^ScDv2BPE}k#jV9 zI)c84=r@?Ua?E&WFC#MLTnh-wiN3r&{Os_X z&KFA@$^7koLC7RP5UV4)^9Q;{jJ_4u0(#?Ha$Z85H;kJoUKpQHNPk41D>Wll#ol(G zoRh;az?hjmJexYmbUx_VQ;M8;s6DSi73 z;g@gXhC(JnS`8-GQC~7*CX<}pOiPIeXqF=sShE7yc7-;))eZ3{w|;91BFcO8%NzvkGB?M9oFZggvJGX zg|b{u=ZR;Ep)PLQpX^Cw40gerNraGo76YCMX2AvwEa(y6aA>krH$`l-x9Tm!?&aAG)`nFZs zXmo89+SJ(evu4fq@rTmlz`8@SX4bf=seoyfd&7yQL_#iPRjm!!CITAYzt}(h6f&RZ zLTlIcvvc_=C$cDVp4f$HlW4vWBhRC=iR4$#32NH1bqZ8#Bs#_2y;2F)bPo<&#~Px-8~yAcz8WHixd~E zzeEK&uCjQ|jGT=87G}J1&h0m)@PEZA;)gIwD+DWfB!Kbk37Fy!Qw-vOaULm8WZT4g z6pYCY3Fb5*Oh85;-aI)30!i_Vqs={&3SWj7AU{MEMlE9|k?wFcf7iCP9_S%VBqH%9 zp$7Ib54=3pgpa(~`~^}nIWCCyGuP?+Z2 z|K@JYxCQW*7tZLQYZFjLAy(-iARNX`i!f_iIb6|FFU+F)sW(EncfFUhTRjnOu%UN; zsXL~7I(=&2I%>c}Y4p>~q6yLD-S@(flBbcL_wrxChC-Fhl|akKBZ?!T64$lrWaDN1 za{cBnUgv1*v?d8ViG7Lzb1mP#KH^^%Z47o^`yX-4hx6-uMzn2SXbH>c z!v#C}f^65BV>Km2wuLc8*4MT=ODbMheE;n0xSy z{cK@FRq~vH4}dC_D$tu3x^W_p5Zj6?fYU_7C#eg{2>#wo?K0s>d!TZX>ObIjLwhMR zx4Jv+cSSmCNdY;csg^DXwwY0#9_~rsF2evO=aJ-w`7!T~S=|<%qB+`5tVxpkSDk@> z+$EBNxj&s)-+XHQ9Snsr?W+%R3vdURc{fk5xvqy?89rD-Zp5w{EdAP-g2oR_44>Pd z(&tyJ^IJCkwtF;h#yOhadS0)mw1(aSd#Df0*H-s$`A<7q@{Le9$bP@*W@N3wp{j%62Dw^MquYLc_odjUgGdJTyrO&4D&iu2poof@N+)Z=HVs-p0+Q9grpo*M0tV zl?`p#S0BrAv{moi>M{TeJkDK9Z9Mcs$DrQ$%469Xi0Fr2KKuYrN`%jS2(lfh0OzNS zsysobLsP4?AK_bUCsNMkK4Nh=a%DPFZ|}49%fBg_uOYqB{>3N-aC2n2bjFsfm}g0f zba`*S@FMsdEQlButLc2#b@e!bKlJ6v9Zovrg(ko|UKssnc6GBiQgzVO1hD^+(E-Q^ z6abVz62c#nMxgwUtc1V{K>A1jXWor<1|a|22Ka~nU8#TQug||Y;%CJFSiJs>^dDLB zuc0su&u{t|+!?#puk08z~OwUMupriQq!r#rW`ou`dGcc8o1UoU`U zp!grr-QL%lF3{c010)_O#qf`X_#gSNnumez9}{0!DF!1=Z8~{RAA34sZeDI)25AgB zIyy-oI|p%H1*Lzx|M`<*aPsx_66fIw2ngT~5a9Ooapd6>6BFa%<>%q&=lWy81q$-; zwGQO+05Sdx_N6Z&R)LGo*s06ajk7U{d}bu82$$O&-!a|er~=D(t5T&_VykiCr@8Vp1+N8bN*kb|7ibjSV^A$Unc*;_#e&w*Gc|^@#iR| zG5*x|U)Mq!gZ23C&)p?;S5=UE?SxX9vkbDBHy-G^WHG*=mmUCaA(i+qr4~DvYL@ZJ zV@lx=IW)WSka=vprF@F#H}gh#M(73P_VA{AH6Li8{ib(j<3bkk*8Mn;g4@6)k;!SC z#uQ51wSIZ3rE4SMPu958F>i4cntvOF>sr~-ly;N#1dC2KXj`RdGOhIfDj#{Ykf-x? zw$^jyrffIx9^_}0;c?%DQ(~I?>4d6jId={gx{Y=gab5xoA3WQ{z2}Ri6dkkX+-k{c zQx3j&*3 z$^$Il&lzOaAcdH(;uSJ3_^fV{-VJvl&OVEu#^mD@v%q}gj^$R|clU7MXgjo@PhbF) z4C&DLu5`>B;YV5shqF?*)o$lI4?ba%jCVD!hYSQv1tV@kw!e-}ZLrv^AYbEvyVs+` zEuR8<%kt=jb?5EA3qV?C-tSDHZ@B6jD~TO_snB*;pjrYX;n_uvqRt4V>)e>avPA<1 zdGz=y40B+{zOQ6KDWXv?pfmN@>klF4U1-vmxh+THS>$7Gu{;5F4v($|)Q>ZwKygg-wTB&799z4MF-%z{W5^M1C>cUou-XWW%byt5zetv?VC9kK0 zOFd%iG8)3B_91x=-4G3UqlUOQW=Tg!zt(Bkb;V*nHN_fW=i)H$*+lYb8csMr?>H%_ zT8@VL8`bivZJY8-2L147F1BH@_2LEJI=`qisgZdy^@-5CtadmHtfP*8Fwn^hbIvHW zDLIPV8&ocm9+mg0>1v$A69qe#Ge!+hdJ5|=p=DM z%YWp5O;DVWnWtQVDI$s6i-G1M%#KZ1h`OH#Univ9rxbPjZfZ_A^RlZ#T#ecZQyS?* zX{b~4d4I?H`f1BckR%_--+krrs3F(TFn|dZLtOaF%Tc1!V8xWwT2*n=d3}|roLArU5-U88so~06Bu}IjgXM!#Oaw(2W%zs@Q9m^tGoKx3 zAoSWOIex_;XO>t^^rsNLOM{H$gF*7@5qvWnREOb-WYFrvmAv4RksJrZi8wP>x6L%= zUNJ>Hz}AnxZb_wyl4%X-56E{!D7R8eeu%f4QuVNj6UySlh5*M`2JiIh_xZA=eBWvf zXh4xRjw;te=4=cQ&f=AoUUs7+cZ9G1Igzm-$V|B##hKBv(TT`v;!-%ud z_m0}mA%vg%pwzIdtb!Zc2M3w!9mWpe{zbB>BX@h~%^@O3goZ95)KrH(!S68Q&XXSv zg@h~bMDCr&XYUTSNzqSiY7x-{EHAS&R61L|c81M|Poxmv$SNgp!kpg8x|V$^&9yoj zLSCQhkqNS!aLfnV!0p(i0G ztIbU|p{xRJ|19r7pewA60mrfspaOX$;cGwhEkxZkzhvo)edQoNtfzyy8$3xst*1A- z0{7q(+~$-w60tYOa9Blem#^FN6jIw0JpbwF8dD?*k}fp4`Sb(cbUqGlTYcQ94qrS5 zFn4{C$iH(MoRCy>K(C9N(voWLkd);8BqQ1Id5?M!cc1oQpF@kM$#FoERs8x61mJYQY*tgbQ-dKh@$qtW+oisn;%xM5@1{hq@OdT3f~`28~=(sM(kzJwcq6xO)brH#j7?*q)`fM_22Z z$`;L6S9i;}0c}E*Q~cxb4C=?c*M#E^DqsZ)Epe(lu0+{A=BVq3!|xKg7PRSx)f3N$>B?ngs63ti??hOrGjOJQ zl9}ypH`l$CJH4Zub?rU;dippK2EqZAA;$cp~lcv3w$;qyXO`a=eosG@GA?~(!Hxx`My%MFt5~z={ zx2J(l4x&<2SgpY5{B@(?r;>d5MzYuk-?rW+!?`_@%-f$D<5>4i^Yg!V^b-~uOu!P} zHmk{9c1^HB+?BX0>1*NJpD^x_nk!nSt-KG~4btTbn#~DjSACWjhC9HoA`q=h#o;U$N zkb$2M?(d}@`-U_b%;TJ9GkrVtntso+zf%ehXL()#GUQS%!*Zm_UH=ki)~c;9a6;JG^6_hph7^@VP2GyW)vcuW-3L z-J-2y>s8H9lbZIruu$*ps~)?_v#{%1>BohL)j`~2$!5p{2Z*Q!i zJ<-V(VYisHQqok#hRVq-GKuP6W%4Z#uaeQb=1DU5J-F3N;G2Z-*#sK;_50MY>~7Is-KmH! zi)+M!q7=0Lg5Y~0BV-no6U+*_^d)WKn(O8|Iey&Zyp(zIEg4y`MNffTmy9AQsv_u6 zJ2|d7BNdJ~jd{yM8f72@`ati)>iyIp9?7O!L@w}YLs^F9SF>p8vZ`#_hY}3?Rwjbg zS2e1O03xu8-+>$w^OSsgtx`Art4~P;{OTX*LqH0(1x@i2DbLwukg%sBWeBw$ zkCT+Q?|NAt_^L_f2Ge16K^SJOE0j}a4Cof2A;eS^)1VN)-R`2Meg6ydrWoQ$W5e<* z9Z!>hvg|k`OWKj=^Kd(_#c6Vf!P{_4E3#U^kaH4zxajIOYdg2f_a#jM$lh~C8478 z;OeVA%v%9+keJ&FDjmN(fNve*T6DqA%5<%6uZ5J0ghEROZyT&@}*{Ddh zgzsgUlAHHx&9R%2i@qPH0?Bb z`^48m&Xd)tw~*7E$08o7hpgudjIgI!5ljE9-&*y~<{wSrhx45c(k|Ggj?&L#@s@X! z?_@$YmB&*i>53+pza?V(o;Q#nS#J%6K(iP!7$i0)Mh>N1x>sN0dQBc*d=Ql%4eXX5 z@5?sLp+d|SfhHveKlzB?4Xw$y1yF@K#rV0d9C&;WDK`E9%nF)4x$c(q_L`r1ln9u> z@y_t!2~F)MO_YB<4)o~h>AqrporYR^#mJU7kg>F5nY}OUU7@B1_T#YVQ?zup9f4H8?Jaf$f&h^?iRKb9gE+^XE#HGL=iH$Y2_dImn&MC* zVs{GdRS|w|HP3oxO%}Jv-a|xBVTuF8}WC&xTUSIRaXZ|Te4|JIKzE+-1vYS zT#>3xd+U`j9cvR-!JK%A{Zoh|H~4&Ltr~pjZP0cg=NdejOLKlPpF?v4In94u)e8Up zBaeDLd{kDl+wg z$*?gY_`d&LrF`54S36cKqKV(f%P-nkJ5GH3vb^}vh=rfk&&vnBA&%0Qzosn1;6t;~ z#H)$eA1-`FkNuvvSxBExO{;z3bwfjYo19kNw)S`_V6t%iWDlc$HP}}{IeST;n?wxU9bT9OmZBrXIA1p{{e2qoqy<-M*VhEF@L z>rc}xwBcL7yH^GD_^1zbj^Hcf&*1I-XV|o>D`(7OMy*2;OXG|z?<2#MY_Oi|5C12q zM*j9VbGR8)hk_V@mESnrUNyo^%}1vD;iE5q)JI8x-qKQ?b;$zP*EDTntdJw^gjDvZ z+sN+9_q)}hm-ZG*{y?owohj>8GZTx=8Ps*TROjxk=!eTb1bHdY(=Of zL4mgS-WD`jn3l;JfNdFSK(~K}=QO3ENRBD+UAEk?0rNO8dzR8tr zwUDvtm@j=ZhK7StEG(0H(Va)Ef5W|z)L)TQ|MWai&k~qi_T1B;nCFjF{HCEe)991p zrS3kejO|5$t~b(wnW)zBvK{`0yUDSGNnfoho&7-pLbZ*nKJW64gQoIZ`(F_BcY2w< z{;7jAdXF-&6}@#S5cFzMO#0OYepqN2zmr_N2tLjX2}cI9zpxOWiul%Qr+ zI>2wuT3vnHL@V37$NfCFn2$4;!N7l)9QW1ykiLEtSzSB3z22~wJaKkZ{VN8O?ROIv zCVOtRr6Q=VlBLbJd^N!sLi^AZ>oWl@+HF!d>7}1Mm3--{g`WaTj{I*+m5IM8F(khGDlw$=)Uci#~|bTjVaOkWEvY|DxKKz+iD>DsOg79es@|TOPS) z!Fm_Eal$Ey9 zI%hJod57H^GY*F3G8Kp85f^|@SC zohE+c0al*K{-vZs8xzs0y=s9xl!7(g`MD@MPP~fas`b-~k;%2HApnQj`I2rFu^U^d z_q}y?Ou?M(q+YK~XpV-Hosg`UaDs+{vgT?58_JZu{HP>zR=#{_e3OGh5RT+w z1C7hql=xl(O$aO)N9Ad8k*J`28@d9oRJ4etnlI%rYAId!N9<=Z!GQsk55!+0a9^c- zd@1y0>Wj%fqf-w=Ew!A2P-7V(F(XvV{grzr%;AgNyu*=%$`D@NuHQ{Daj+U!>4j!RAl;6&$al`)Q2uLJqLeW=(j_k9NV9LyACM0($Uh1~>0WP@g5* zVM<{VgkiEo+2-p;#gI!jrPi!9t9%^y(9(-<->0G>tCjB?lF!Wg&4R^sx5C zaqSw2C7p410%k*oVV5-IUgf2Y5tP|R&Wo;Tx*+xkI)Qrp50da0d%Cna zBZ=jPS$b-rhA#}M6-gopdKM+;*G29GCVH}M8~$)2#mMKdz2n`xh3dGsA3H2((lR_h zX4i({RLI|`*vPAcR}Zga?jqB^>8i&A>s})2ipA>nqePm*HjpbPHKl>A?F>YOPnFzu z1l2F~lnJy_xf;KG)-=7^lZ}80?v^M<&K_LFo_GYbcZ=k-_r}ub;}15+XErxRW-GYa zG~T5RZqkC6+jqQ%*1h`r6-tQQ4?BTV>_YaGD=$$c$oIBti4!-V*Z%1-K0W>Pzy=R= z^Wvg)* z6Lb>kx|u-ObUOG9B3{kiEo`_B)4&p0J8wUYOUhsssXDr4VV$Iri0}`8Xk5{z*4O6S z{gUDIqFBsNjh#cDQ21a^7Cjzj3PULZW~Lz$)AzY#0LO`snZu4XMH#3uu{)=ip_^>a zuz|OZ0&xjutILC135Ae8qB8D6JI=}7&k3by@$QgZP`b|#8=WTAm(4KNqJDUQ zb*GspaH1G$hewk%ZByRr7icsaJQ|mGXSKTUy|nTmRvOKHA+jwShfJ^H0%GZ$+^w}D zDcJo?d%hL}S;&Q@psnBO=3}5})Yav-l4cs8^t~yMU>6gSrcEVN_)PO2Fv`%&uNf`M zknPgP2FiG`q4%*@&Z989E$~MG!gYZld79##n-8AL042Eg;!Nt?R!bizF9T!!86Ce5 z9*1vIH6EOR)S&!+IXK%_*$fAvx9eB{Uw05EG+;PGhN{1Am-n*+8EbG-!7F?`o_2!1 z3^_kRn7uM96u*^H`pMq&MVQYO4j5D8Y2fR)5{3xQhYU#e?d-b(o6>EEsr}8d(3fc_ z9+&K_In9H{GA-_x;p{}E@iU>gM+!5qBd+us*b^oyInh1AWGINub@?fO^2?P&pUyp&8V#i$hjfUF7(!|`@^&zu?uIxfRDlbo-y2N} zGFrSEaG35!1Aa6|DxKv&Ny^=5(?eWz`WWq8W7HNSFy(Fr$AeCrG@fK*8YESV1B*v0U*!u7J)iT>IKt<@#L- zG+9+V!;7nKL`soV(*V-D$Xyz%=tX$ha^eEur~{Ft#1T*OAMDuxDhva1t@e(<0U4EmQGzHpfOy7O<5ME}IO6r!avEteDP6%% zM)e2(;3OakC~YVT(<;9l4G=W67O4)P&dhW1X5&Mu09}FR_4@HqRYySctXyvIaJH0^ z701Z&2JkOQBS zC~(kkvu))gqfqPV(rCYI{w_vfnEX@$C-H(oS2$%fb(&o^_EY+{fdsg^9C{HMm`93c-2L8=RH!sPb(BLbkKfa*pWIsEEju`l#O6j9MF~^pw1{Fv>^hQm?f|$cyR?sR>XkDu<65Gi){E9PUx50_2Thb`)7xN@WQVbSnr~2O_=>L{~vs5T{B5VAKhrtilTi@|~c(p?=j9 z6c&h~CIQ3^p))4Y!HO0~o5O4fVp73y4*^zFk;7B0^7A9G1v0OaHX>017>VK8J5g@{ zeblI3px2S;Wgx+XH|E&$l9>HiXW{Vq&J5TeLe;3>ybVf~u*-i{`ubb&UY_L+H5gM6 z!7%tP-)9WKjrlXItpimdT+^6!4z(eegaaQtkai8p89fV$s3Yt|x)HuI;=1$jgyaU@ z7bCTceN7dS+7%UR{5=aUa+joJg6CTjJX+=i(?aqx8NJVJg(yuyvY)m4tuV1cy<_hP ziHSZ@0nxjA%}un7d5ynuD5kj&2@GKk0W1;EDdanCO^0hnba;6QGlLC-S-Vor$f{*( z{?tILL323#f4oZmrCsh@+idVJS`>y)NM zr%W!9{L*SPBuNHxp9{GsY}Js;QouPE2^UH)O?gC)O+kqo`k_+tC~)b06I|1%xy28X z^!G~C^8IALV>c%^eKtK_qxVLxOGK4ys_?31Fj_IjGv;oVou!`Hd*wWQxV61?zuiBp zB8Wr}M^^|7Lq|g2LXRM*WNKA=%S6ifmfVfAL1VJ9uBfp=nm;V=4}-M=~p(Iw%OC(MT(PGV@i{3lcHrJ<)CSoNxYrp-K+^O3GE`TU6Kp*JAu2% z%Hqo0N-3xkRM|seREPb8Q>; zllV0mTC)P;0*Ew)^l0q#r};jlKGr@QMF!vzP#1U!eW!^s0 zw)m78QV=qiKW#EM-tlg{rtcIR6oxl2*U`8W3ONJo?ShwfYimQ?;?Cdv5i?{lm@vF* z7S6R2ju&$pZI#d#(T=mr{yr6Zl)BR}sJK_TKjKp55wM;7j;7vuo$Mc~TmGYK$jP}K~ctsbr7pE1e6zLfEM7%+uPD&eF zA8knKL#ITmggZ+TA0Hp*6iJnREE2HzkdJsD`ZaVB;}g*)Yh#^?nMq$4e#~n^7eWf9 z9$??wBgKaSw6!TW>7+S19ywYALJy&9y3r8rKC+BZa}iH8Z>NzvknLcZd7s~+<@i&vcZT=8frVY^*!G4(`$SdWO(W1^)dSpJkd z=+dqOTtK|ZKdD=|WRM7Cf4;*1jguH;0A1Q!QkMVvg?vPP0Dt>(J9DdQG(c-r{p4J2 zNbP9i$f9{ri=9ko#J~s&ulKp`t54C>KvxRryRSA+1+YTt!}xqGqXP z<2UCM6d09W^kz)Ydyko>^6!s$=LM_3+AiJqnP&sow+TM7)vz(L-!=!eteq+IifKM@ zx4rW+g8?HHh4?mjG5BCB>up748fA5PU}w9rnJZ3jU6=WLhnNOKrd)lamh3viABK8( zI^()FDossicAB=+6}F>$4Wz>vmpoJ+C+-HY-J05Zt?vzmSA%Swl`W7q$l;~*lVZ2wxZzS~<+iR!4 z)>p>PXt@H&6UaSXI8Ij&L2wbxI9%9J%6GzQZpq$tjTE+{PE@z$nqoOG%_QNjSV=c$YI z)22W2Ltm`S&aCtp&;y;lAHc!cw zIo#HJ`o@VJ0qcy1A~vP20+HAfC8{F9cj=m?`7dm&Lcvj51^a0~#1fAlZ=-3S{1;=gT_UijaZ@Irrm{>9<*;QwQxk%#afS@^Fh z7(hJm!qA*$b=?2}Lb|^MC#UxI1OR{+u+h)~>nJJ+nmIYJnV377TCjOKIREtm2zv^? zhz=HD6KYQfdq+1xPZ64bECgTVziM_G>VHhYb|N%7iYn9~Cszw zS9422H7V(TyT81N&{%`P&Vuah9v&WS9$airu2$^t1Ox=wIXKxlIayyUSlzrF!6u%p zj&8L70{Jf-DGN6ZfDEIe)gJG}qP>u=A$-2TbxUxNw1ltvKbYGDF)a@BBhvKLi%v~f0Z16v5Q z|L>mvj!^h7l%S@Yg)7L(!NI~2{67F+VE?I?g#G{0{#S>+&EGl-+M769iO_hmnp;?! zKR(xA|YF|7ibjSYh`6UnKv+_#ego*G&F{@iLX7=r8&G z*Rl{rXE?ZhS-V7na#9i+)<_kZgGq+7ntjJDEz=bdmvxjg2>A15#$<~r{74#~#f@G! zmYSwwG5|S@yTtIK@epos7@}zi(%yDNetVsO_$PanKkfkcS7Jp2UA;L3TIj#xbNko^ zarDr)op+6PoBnf9d!+}vn^~|su07t>y1#9I*sGp)g!Ub3*)8pjtqN*!+SUsW^f`#J z-qVgw$Eo)8LviZ$v$F`|77vx|9)eCouElTE9=1+>dP4k`@Q~r$(qg2JiX0-%gV-{9 zihlei_E3=3;79p-6k$yhrxdubt8xE$(tA&RQ+*@;0E<0lFcCAMzbF3ye84LVkc?qC zJN199kNbh{TXH;_I;S+f7?SM+krq#%WviSZim0UOp+a zij1v;a4e+%S|a(-H;RdC7ycv5l~*G|vKG(F8i7OdGbJ0S*bZ2506pb$asS1x?P8;$ zf1?YQ6!*BA3&S|2oew&G^t*wfn86zLaxyTiY$8RCjfZP*C5<-OMfaG{66I(c|qQwFxD09+~gB z$1fDPl65jNnfFJ&Nv5=$<9HuFOjU`d=3H-myc0V0*#gByjcpQ^HyJO5%W>sUfC|zv zcTUZ+tweu#?RieQEpROh`qx3u=1%U#Y})+@@}gmdVHT3&1xL@4zqifGZkb(IF)CCG?tvW3x2=5-X52BNsn z4i9gWqew+{L*Y8r=ZGcxN*R@tGK|=mI1cp=WvEE91<=x$ahg@E(?W1AUv4Tas$+8@ zy=aousWa6l1%$rlH;S9F6$1mo$}*S7eXx2K{A0?eFl7QO4an{x6`d564ow0{loVNRqO}?8?X`lzdjkMiG&ZV_UCgl7xrPFRu z-ZfR)Gh(j5@n<;2YDD7P(+?Qn5xNER#&L$l`&?EPP$^Z?hs8{QOEU(Q*BdC60QYqi zz0#FnHZ$L`LF2&(%6rZr;6<@`H;Z~QJO-VT zH7DbEtFV}$-vC?_EJ#wiygxXe;BH0UxfAB0QLQF%4lwnGn@{`o6@d=2$MI21jMKOWcA^dZW|+|B8a%to}l*bxX?Jh zZ9b*kj11?wt-HPlOS_@%(8}@wT|>DDABmI2@`vm7?!wN1wXV!|&;2`%cDI9$m3R^N zhuINwQ9pAS>}m12)%&*i*5wDX;L=N7c-v@UjvitcEx8_GK+?zQY#-5uD6W=%ummB` z=BM4W31qU&h^u4KoA$=K+Yl-oKIxzD7U7*0W(h?L{oHwP941w(@qKd-_wC5EVNG6_ z*;}*QRRNDhF@xX9Mn&AuYBo^(Z|>@a_A`5DGu|QP(c@j8d4wNsI;HjIQi?AB^8a%_ zqbO?ZoTAOOkZK(=6h^itJTu!jD4*$8dvJIZ_dJoA3430<6OuQKi>mKtam|LF7EOSo zls)j}A_|18ubU6qzN_8;`9j1iv+;*gtfooe?l&1<8C~^yqlpk))EAF6`nLv78-rAC zXI=dP5>PqI_m(nS8e~i?ej_>sPU|-Hsg78%#@3o2oO>PBlPh!9hWf;hHzoV9+s=j9 zUdIxc6O98@S(w}Hsi(7P_i16<*^ld7)^`j53xYXbwq&bET;&KgcpQTRY%CBwvfWE2 z_cyLyJYoLp7eg1TVcOfb+vb52Z}b!F)viPz$c}dS@~5t5WU@JcVYE^85^$tG;#^WJ zl-}n1^ScdqQ0;Ko>=T~r$=H<@Uw0t5v!v1IdZt)a;X9qRu5>6rg5#9UH z87H?p%4V-hjxO%Z{7 z+vU<8g~kDH5a$k7#D2jrBvt1E58UjBe%X^hAD=6Bdr(Cw;^yh3MrU2Mm}_&1Go%zB ze^6vjaj!van2$d;@Xaa)_wXovP8CIR4~DoR<`>&k}COTD=| zzBRJ*pz+wk-|wO-N0S6#6~8R;!vzjzw2x~(HmUU0Wp1@E?tK!y%8z)y;8MmVOBmkZ zo7_BpdhB&-_twohJYDmvt(|-nD3*eP-u`s=nGQ2fsyDWPocC1_gcAWn{I6pI&VI|p z9fzQ&SAdsazUR&P-nZ4#!$RH z=T^(w;!lYjLiyaW?rO7enb?C`Do~q3fXmY6yk^!6dtBnODnb5CT6(+bwtuy-7y}jZ zs-Q{L+F%im)$r7#{=deRgh_CgUlyGH(Yw{Qa#UX3t9(RDKWR80W{WCzDWa#qc`K zOnlbU$vH|!IVq_C4SX*arjY@a8|{FpgaNU}6i!uP70H4iNoE0d0m2pAh$1nIKu%~+ zh|Iw7t73T@?8>eb7@DZ^gR%(Zu68s_)GgUcVxP7I$8SAKJ>X{iqsx@XYJ}Xhy?ZSi z73SL$e4KCcX0FcXq7NSHW+~o5)fb?;>ZConXdpsn-YYRyvys|BAjZ!PeRg1LwAO;0 zdPpw&w`vslq8j~xbz?o6bBZGA6j&e$`bAogqiq_qBw@8X6Oly=cbnKVq9#!2rgi}& z&7jV7Y${69QL1Il&!x+TvR7tOl}wW-p?W_$AW6H7na#D>MtGmjdq3H$|8nw$JgvQ4 zv^6%Mb}^r0R2ekit?$o9ezx|4a@N`Iz$tj0wr6S5c zs$Mor3)=;!YcCs74Y|o&XN$IaJTJWtQaiGMt;Y0zws%Wk3hib8-7YP9D+>-Ljq{3* z0jDsKl_iD~64)l6V2bI#(FCTLTkPaPe>8L^=bwEB-%pB;PFda_Uldyv@Et)ud2*8q z`t4{EiJYqP+6|i?8odSVnD;VHhkH-a-<&2UaM~fFGc-FJM|KsK&2mhWRMn`uW&f5~ z)K8IXm&c1cFREYYL{$BLV>>e5=jtD%t-4V%O0jEUw8`TI2*Hl=^C=!7pKtf|akw36 zz7bESG9xUIH6Jh6S2J}86B94mpq2(v+~@U{C35X#zvv_IG020YyjQovXyY9ag>KT^(Wtmo4M6o(ykdgX8IxFw3j zya(*fwn9)W)aUjZyXCtK3=>d^$OOn(zV!7*-V!SjxM`Z+U8*FhV6uJ#*zd#;KZ2{+ ztDdI6w4FXU2|svD=GDQxFQZ@XQ?f4!GVbdYN7z-Y zfD^i<=QLB|)_*|jt>6X%0ymp_`^ml?&_s48bWKai5?P)B6dddZ!&AQMXcgr%5`WlN ziAnw@brI5;J)Oetclf@o?l3zY`1NadvzL!WiZ)p*ibP50EKPs9RF?qe8m?%pOq49b z;SF1RVg~9i({jr#*O_FWBv9uqM~1#DEgmWp(_Rt3y#*~gKCwudX|%M$!P5}8#U~Xn z4{v3=B&VZpvO#7~4nE1?sci@WjxcIRlMg`lh=hA$2F~Q{TddNK8PfSxznREibv=ct zalZxfvAnbCm8l+YA6`vVJJsJ$E>jME9tx(SZkHE??D4yqQ+?cCYi?-sGD}}IER7sG zj@kk#lJ9Zm@8&*2!iTmNAf_0(ek69aZ+LIe(sYveWYpXe4*1}_X3ApHR{HDHtxGM| z2Y3W8$yR=#hAWcz-<5GGi+O8MSKNvZ#ELL~d-i^Lz7M!cr+>chdXOg~@03nM$$fy(ho;^688fp)atZ%F#b52 zE#`l*_Bel6AKbpllBfC+3w-N0F4WeV5%KGI~?49eqhxy#GU39p9h-Jwc!a(0P0TGJ0 zQaVhhykJETj}C@2YHh~M2t+c+e+V~JGroCW@=hT~g*{(1F+9>NCf%=9W-YYpZOVy) zYtsi5R0wq}eaqmNz=5UF9T{fpYJGUMso~~3@2N32Zr?NYp zo#H7$0?{14FWMJ9a7d+@jOYVJ&ZmRd2D_;?#CsyxB0LYYi>8va-{COV3;LxXQ&SYFYO9}YZiM1UpEnj@~m+i+Vm^PpxU zTf*y51c;341O@YZ>9-BYInm`gO{W_0>Likp)ZcM&Z6b)q7%Ckx+i%Y@v1G9@mpuph zWm}hmzcmwMCDibb$Z9|(F^!O@G*hzS-&Z!A1`XP!O>xNWU`^c&>*&&*qxWss!B?jgZfjUyU=d z>wX3rZ2|4dQayNn_MUZR^0l4s>EruSagZzAbEHZ=#&8eMg(ri^pLp0(Vq{N9*0(01 z+wVFGwC9U7zZ6T-CI4zn!Db;qMCGvQK)AIQKDc<=B_nFP-+Pdu66&<`&DnC_bcu}x zsn5{#*d^kwOa8Ezwv}jef$V0oe_U9t_?TNEy7!yds5MFt_n^N5oRB~)feyp8KQGZo zrg7OGrCt3PkRGHoH?B_*q!inz`@OsI=ee22ibl!NMb}6ca)6lI>4512?X>0v*-An( zyNWG`nIDB(#KCe0-1k73!d3AZ1|7FcMsEi$sY3k*T^18N|56_cy>(bzx6-=o0XJOnZ$eSgCKY_a0N&(-N5912kohgvvqk-19UfWk5|bd8GcjYxx=DmNavTlapRh)SFo;-yz{#iuh)J* zJq04?DkLy-{+g1KFP8~+(;$z6o~d!rSTHiGc+rS$7>d-HPjr&0k$x@MBa>C0KE}NhZR&D$#OCWw_Fz7WMbj=`@ixtPP2|L_l_9=i`gtG^^ zSW7|nfjMU)U%I1-18h#>B#xp1LufFve2V)H2^Zn|t++y;BWKtjL z72}7V75`aF+V_b?y~4DqAHs$9TvejyEGmD;@)G2jXJ}SbbKtzqi{ns{9m zZms z@ig|;n8$Hit6i`vbw13XA!KRdM#_7;7^Ct<^vNJc;~nJ`-KPYyU&&k(Qd?uj`JefF zsn9^n{buECv5joF+HIT)t}MZgEo5RFs?mFVddGbpY%h17FbX-?$ABcf{ogG2@0G5% zsg3r#cFeQLfv?Tr>-=w1d+g;X1c!8l5_6iG-7YCmpuH{_?Y`WZV`->{qs=u*xnfy1 zX>;M43D3DFej}MA1z&A!4V#lDNZEFQSC$DC7|C*^rb+K<(Gt=T4>>~O0zSnW;a9CL z@^!`wMVm5{W|Ck(Tc`6q0*9vHU;7dTc6k&rykhVN_@ePWJmYASEA^Lm$8rDt;3D^4 LNvcxPIQahnXfPDV literal 0 HcmV?d00001 diff --git a/KeyStats/SettingsViewController.swift b/KeyStats/SettingsViewController.swift index d18950b..8d26fbb 100644 --- a/KeyStats/SettingsViewController.swift +++ b/KeyStats/SettingsViewController.swift @@ -8,6 +8,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { private var launchAtLoginButton: NSButton! private var dynamicIconColorButton: NSButton! private var dynamicIconColorStylePopUp: NSPopUpButton! + private var dynamicIconColorHelpButton: NSButton! + private lazy var dynamicIconColorHelpPopover: NSPopover = makeDynamicIconColorHelpPopover() private var resetButton: NSButton! private var showThresholdsButton: NSButton! private var thresholdStack: NSStackView! @@ -78,6 +80,18 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { target: self, action: #selector(toggleDynamicIconColor)) + dynamicIconColorHelpButton = NSButton() + dynamicIconColorHelpButton.bezelStyle = .helpButton + dynamicIconColorHelpButton.title = "" + dynamicIconColorHelpButton.target = self + dynamicIconColorHelpButton.action = #selector(showDynamicIconColorHelp) + + let dynamicIconColorRow = NSStackView(views: [dynamicIconColorButton, dynamicIconColorHelpButton]) + dynamicIconColorRow.orientation = .horizontal + dynamicIconColorRow.alignment = .centerY + dynamicIconColorRow.spacing = 6 + dynamicIconColorRow.translatesAutoresizingMaskIntoConstraints = false + dynamicIconColorStylePopUp = NSPopUpButton() let iconStyleTitle = NSLocalizedString("settings.dynamicIconColorStyle.icon", comment: "") let dotStyleTitle = NSLocalizedString("settings.dynamicIconColorStyle.dot", comment: "") @@ -95,7 +109,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { styleRow.spacing = 8 styleRow.translatesAutoresizingMaskIntoConstraints = false - let optionsStack = NSStackView(views: [showKeyPressesButton, showMouseClicksButton, launchAtLoginButton, dynamicIconColorButton, styleRow, showThresholdsButton]) + let optionsStack = NSStackView(views: [showKeyPressesButton, showMouseClicksButton, launchAtLoginButton, dynamicIconColorRow, styleRow, showThresholdsButton]) optionsStack.orientation = .vertical optionsStack.alignment = .leading optionsStack.spacing = 8 @@ -171,6 +185,85 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { dynamicIconColorStylePopUp.isEnabled = StatsManager.shared.enableDynamicIconColor } + private func makeDynamicIconColorHelpPopover() -> NSPopover { + let popover = NSPopover() + popover.behavior = .transient + popover.contentSize = NSSize(width: 360, height: 420) + popover.contentViewController = makeDynamicIconColorHelpViewController() + return popover + } + + private func makeDynamicIconColorHelpViewController() -> NSViewController { + let viewController = NSViewController() + let container = NSView() + container.translatesAutoresizingMaskIntoConstraints = false + viewController.view = container + + let titleLabel = NSTextField(labelWithString: NSLocalizedString("settings.dynamicIconColorHelp.title", comment: "")) + titleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + let bodyLabel = NSTextField(wrappingLabelWithString: NSLocalizedString("settings.dynamicIconColorHelp.body", comment: "")) + bodyLabel.font = NSFont.systemFont(ofSize: 12) + bodyLabel.textColor = .secondaryLabelColor + bodyLabel.translatesAutoresizingMaskIntoConstraints = false + + let imageStack = NSStackView() + imageStack.orientation = .vertical + imageStack.alignment = .centerX + imageStack.spacing = 8 + imageStack.translatesAutoresizingMaskIntoConstraints = false + + let imageNames = [ + "DynamicColorTip1", + "DynamicColorTip2", + "DynamicColorTip3", + "DynamicColorTip4", + "DynamicColorTip5", + "DynamicColorTip6" + ] + for name in imageNames { + guard let image = NSImage(named: name) else { continue } + let imageView = NSImageView(image: image) + imageView.imageScaling = .scaleProportionallyDown + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.widthAnchor.constraint(equalToConstant: 320).isActive = true + imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 180).isActive = true + imageStack.addArrangedSubview(imageView) + } + + let scrollView = NSScrollView() + scrollView.drawsBackground = false + scrollView.hasVerticalScroller = true + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.documentView = imageStack + + NSLayoutConstraint.activate([ + imageStack.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor), + imageStack.trailingAnchor.constraint(equalTo: scrollView.contentView.trailingAnchor), + imageStack.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), + imageStack.bottomAnchor.constraint(equalTo: scrollView.contentView.bottomAnchor), + imageStack.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor) + ]) + + let contentStack = NSStackView(views: [titleLabel, bodyLabel, scrollView]) + contentStack.orientation = .vertical + contentStack.alignment = .leading + contentStack.spacing = 10 + contentStack.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(contentStack) + + NSLayoutConstraint.activate([ + contentStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12), + contentStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12), + contentStack.topAnchor.constraint(equalTo: container.topAnchor, constant: 12), + contentStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12), + scrollView.heightAnchor.constraint(equalToConstant: 220) + ]) + + return viewController + } + // MARK: - 通知阈值 private enum ThresholdType { @@ -308,6 +401,14 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { UserDefaults.standard.set(rawValue, forKey: dynamicIconColorStyleKey) StatsManager.shared.menuBarUpdateHandler?() } + + @objc private func showDynamicIconColorHelp(_ sender: NSButton) { + if dynamicIconColorHelpPopover.isShown { + dynamicIconColorHelpPopover.performClose(nil) + return + } + dynamicIconColorHelpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxY) + } @objc private func toggleLaunchAtLogin() { let shouldEnable = launchAtLoginButton.state == .on diff --git a/KeyStats/en.lproj/Localizable.strings b/KeyStats/en.lproj/Localizable.strings index 6ae6ba9..fc7e5b9 100644 --- a/KeyStats/en.lproj/Localizable.strings +++ b/KeyStats/en.lproj/Localizable.strings @@ -9,6 +9,8 @@ "settings.dynamicIconColorStyle" = "Dynamic Color Display"; "settings.dynamicIconColorStyle.icon" = "Tint Icon"; "settings.dynamicIconColorStyle.dot" = "Status Dot"; +"settings.dynamicIconColorHelp.title" = "Dynamic Color Guide"; +"settings.dynamicIconColorHelp.body" = "Colors are calculated from recent input rate and shown in the menu bar.\n\nRate: a 3-second sliding window counts input events (keyboard/mouse/scroll) with 0.5-second buckets; current rate = total events in 3 seconds ÷ 3, in events per second.\nAPM: APM = events per second × 60.\nColor ranges:\n• APM < 80: no tint (default)\n• 80–160: light green → green\n• 160–240: yellow → red\n• ≥ 240: red\n\nChoose between icon tint and status dot in settings.\nDynamic color only applies when enabled; low rates keep the default style.\nDisplay mode and color updates stay in sync for clear feedback."; "settings.windowTitle" = "KeyStats Settings"; "button.back" = "Back"; "setting.showKeyPresses" = "Show Key Presses in Menu Bar"; diff --git a/KeyStats/zh-Hans.lproj/Localizable.strings b/KeyStats/zh-Hans.lproj/Localizable.strings index eb307ec..070b87b 100644 --- a/KeyStats/zh-Hans.lproj/Localizable.strings +++ b/KeyStats/zh-Hans.lproj/Localizable.strings @@ -9,6 +9,8 @@ "settings.dynamicIconColorStyle" = "动态颜色显示"; "settings.dynamicIconColorStyle.icon" = "图标着色"; "settings.dynamicIconColorStyle.dot" = "状态圆点"; +"settings.dynamicIconColorHelp.title" = "动态颜色说明"; +"settings.dynamicIconColorHelp.body" = "根据近期输入速率自动计算颜色,并在菜单栏实时展示。\n\n速率计算:使用 3 秒滑动窗口统计输入事件次数(键盘/鼠标/滚轮),每 0.5 秒滚动一个桶;当前速率 = 3 秒内总事件数 ÷ 3,得到每秒事件数。\nAPM 换算:APM = 每秒事件数 × 60。\n颜色范围:\n• APM < 80:不着色(保持默认)\n• 80–160:从浅绿渐变到绿\n• 160–240:从黄渐变到红\n• ≥ 240:红色\n\n提供图标着色或状态圆点两种展示方式,可在设置中切换。\n动态颜色仅在开启后生效,低速时保持默认样式。\n展示方式与颜色更新保持同步,确保菜单栏反馈直观一致。"; "settings.windowTitle" = "KeyStats设置"; "button.back" = "返回"; "setting.showKeyPresses" = "在菜单栏显示按键数"; From ab063ca76168a2e85383810f2f5fca1eea7549fb Mon Sep 17 00:00:00 2001 From: huangxida Date: Tue, 13 Jan 2026 14:18:16 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E9=A2=9C=E8=89=B2=E8=AF=B4=E6=98=8E=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整问号悬停弹窗布局,隐藏未启用时的显示方式选项,并更新说明文案与图片排布。 --- .../DynamicColorTip1.imageset/Contents.json | 8 + .../DynamicColorTip2.imageset/Contents.json | 8 + .../DynamicColorTip3.imageset/Contents.json | 8 + .../DynamicColorTip4.imageset/Contents.json | 8 + .../DynamicColorTip5.imageset/Contents.json | 8 + .../DynamicColorTip6.imageset/Contents.json | 8 + KeyStats/SettingsViewController.swift | 161 +++++++++++++----- KeyStats/en.lproj/Localizable.strings | 6 +- KeyStats/zh-Hans.lproj/Localizable.strings | 6 +- 9 files changed, 173 insertions(+), 48 deletions(-) diff --git a/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json index b282990..ffd6544 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip1.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip1.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/Assets.xcassets/DynamicColorTip2.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip2.imageset/Contents.json index 44e0429..6d14b39 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip2.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip2.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip2.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/Assets.xcassets/DynamicColorTip3.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip3.imageset/Contents.json index 2b5af39..0bca63e 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip3.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip3.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip3.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/Assets.xcassets/DynamicColorTip4.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip4.imageset/Contents.json index 7ede9c4..2e77718 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip4.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip4.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip4.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json index 82bdaa2..9a51522 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip5.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip5.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/Assets.xcassets/DynamicColorTip6.imageset/Contents.json b/KeyStats/Assets.xcassets/DynamicColorTip6.imageset/Contents.json index 7b56139..20cb6d6 100644 --- a/KeyStats/Assets.xcassets/DynamicColorTip6.imageset/Contents.json +++ b/KeyStats/Assets.xcassets/DynamicColorTip6.imageset/Contents.json @@ -4,6 +4,14 @@ "filename" : "tip6.png", "idiom" : "universal", "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/KeyStats/SettingsViewController.swift b/KeyStats/SettingsViewController.swift index 8d26fbb..ad540f7 100644 --- a/KeyStats/SettingsViewController.swift +++ b/KeyStats/SettingsViewController.swift @@ -8,8 +8,14 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { private var launchAtLoginButton: NSButton! private var dynamicIconColorButton: NSButton! private var dynamicIconColorStylePopUp: NSPopUpButton! + private var dynamicIconColorStyleRow: NSStackView! private var dynamicIconColorHelpButton: NSButton! private lazy var dynamicIconColorHelpPopover: NSPopover = makeDynamicIconColorHelpPopover() + private weak var dynamicIconColorHelpContentView: NSView? + private var helpButtonTrackingArea: NSTrackingArea? + private var helpPopoverTrackingArea: NSTrackingArea? + private var isHoveringHelpButton = false + private var isHoveringHelpPopover = false private var resetButton: NSButton! private var showThresholdsButton: NSButton! private var thresholdStack: NSStackView! @@ -51,6 +57,12 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { updateState() } + override func viewDidLayout() { + super.viewDidLayout() + updateHelpButtonTrackingArea() + updateHelpPopoverTrackingArea() + } + // MARK: - UI private func setupUI() { @@ -83,8 +95,10 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { dynamicIconColorHelpButton = NSButton() dynamicIconColorHelpButton.bezelStyle = .helpButton dynamicIconColorHelpButton.title = "" - dynamicIconColorHelpButton.target = self - dynamicIconColorHelpButton.action = #selector(showDynamicIconColorHelp) + dynamicIconColorHelpButton.controlSize = .mini + dynamicIconColorHelpButton.translatesAutoresizingMaskIntoConstraints = false + dynamicIconColorHelpButton.widthAnchor.constraint(equalToConstant: 12).isActive = true + dynamicIconColorHelpButton.heightAnchor.constraint(equalToConstant: 12).isActive = true let dynamicIconColorRow = NSStackView(views: [dynamicIconColorButton, dynamicIconColorHelpButton]) dynamicIconColorRow.orientation = .horizontal @@ -108,6 +122,7 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { styleRow.alignment = .centerY styleRow.spacing = 8 styleRow.translatesAutoresizingMaskIntoConstraints = false + dynamicIconColorStyleRow = styleRow let optionsStack = NSStackView(views: [showKeyPressesButton, showMouseClicksButton, launchAtLoginButton, dynamicIconColorRow, styleRow, showThresholdsButton]) optionsStack.orientation = .vertical @@ -182,13 +197,15 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { if let item = dynamicIconColorStylePopUp.itemArray.first(where: { ($0.representedObject as? String) == style.rawValue }) { dynamicIconColorStylePopUp.select(item) } - dynamicIconColorStylePopUp.isEnabled = StatsManager.shared.enableDynamicIconColor + let isEnabled = StatsManager.shared.enableDynamicIconColor + dynamicIconColorStylePopUp.isEnabled = isEnabled + dynamicIconColorStyleRow.isHidden = !isEnabled } private func makeDynamicIconColorHelpPopover() -> NSPopover { let popover = NSPopover() popover.behavior = .transient - popover.contentSize = NSSize(width: 360, height: 420) + popover.contentSize = NSSize(width: 420, height: 520) popover.contentViewController = makeDynamicIconColorHelpViewController() return popover } @@ -198,6 +215,8 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false viewController.view = container + dynamicIconColorHelpContentView = container + updateHelpPopoverTrackingArea() let titleLabel = NSTextField(labelWithString: NSLocalizedString("settings.dynamicIconColorHelp.title", comment: "")) titleLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold) @@ -208,45 +227,41 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { bodyLabel.textColor = .secondaryLabelColor bodyLabel.translatesAutoresizingMaskIntoConstraints = false - let imageStack = NSStackView() - imageStack.orientation = .vertical - imageStack.alignment = .centerX - imageStack.spacing = 8 - imageStack.translatesAutoresizingMaskIntoConstraints = false + let imageGridStack = NSStackView() + imageGridStack.orientation = .vertical + imageGridStack.alignment = .centerX + imageGridStack.spacing = 8 + imageGridStack.translatesAutoresizingMaskIntoConstraints = false let imageNames = [ - "DynamicColorTip1", - "DynamicColorTip2", "DynamicColorTip3", - "DynamicColorTip4", + "DynamicColorTip2", + "DynamicColorTip1", + "DynamicColorTip6", "DynamicColorTip5", - "DynamicColorTip6" + "DynamicColorTip4" ] - for name in imageNames { + var currentRow: NSStackView? + for (index, name) in imageNames.enumerated() { + if index % 3 == 0 { + let row = NSStackView() + row.orientation = .horizontal + row.alignment = .centerY + row.spacing = 8 + row.translatesAutoresizingMaskIntoConstraints = false + imageGridStack.addArrangedSubview(row) + currentRow = row + } guard let image = NSImage(named: name) else { continue } let imageView = NSImageView(image: image) imageView.imageScaling = .scaleProportionallyDown imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.widthAnchor.constraint(equalToConstant: 320).isActive = true - imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 180).isActive = true - imageStack.addArrangedSubview(imageView) + imageView.widthAnchor.constraint(equalToConstant: 64).isActive = true + imageView.heightAnchor.constraint(lessThanOrEqualToConstant: 44).isActive = true + currentRow?.addArrangedSubview(imageView) } - let scrollView = NSScrollView() - scrollView.drawsBackground = false - scrollView.hasVerticalScroller = true - scrollView.translatesAutoresizingMaskIntoConstraints = false - scrollView.documentView = imageStack - - NSLayoutConstraint.activate([ - imageStack.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor), - imageStack.trailingAnchor.constraint(equalTo: scrollView.contentView.trailingAnchor), - imageStack.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor), - imageStack.bottomAnchor.constraint(equalTo: scrollView.contentView.bottomAnchor), - imageStack.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor) - ]) - - let contentStack = NSStackView(views: [titleLabel, bodyLabel, scrollView]) + let contentStack = NSStackView(views: [titleLabel, bodyLabel, imageGridStack]) contentStack.orientation = .vertical contentStack.alignment = .leading contentStack.spacing = 10 @@ -257,13 +272,83 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { contentStack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12), contentStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12), contentStack.topAnchor.constraint(equalTo: container.topAnchor, constant: 12), - contentStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12), - scrollView.heightAnchor.constraint(equalToConstant: 220) + contentStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12) ]) return viewController } + private func updateHelpButtonTrackingArea() { + if let trackingArea = helpButtonTrackingArea { + dynamicIconColorHelpButton.removeTrackingArea(trackingArea) + } + let trackingArea = NSTrackingArea( + rect: dynamicIconColorHelpButton.bounds, + options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], + owner: self, + userInfo: ["dynamicIconColorHelpButton": true] + ) + dynamicIconColorHelpButton.addTrackingArea(trackingArea) + helpButtonTrackingArea = trackingArea + } + + private func updateHelpPopoverTrackingArea() { + guard let container = dynamicIconColorHelpContentView else { return } + if let trackingArea = helpPopoverTrackingArea { + container.removeTrackingArea(trackingArea) + } + let trackingArea = NSTrackingArea( + rect: container.bounds, + options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], + owner: self, + userInfo: ["dynamicIconColorHelpPopover": true] + ) + container.addTrackingArea(trackingArea) + helpPopoverTrackingArea = trackingArea + } + + override func mouseEntered(with event: NSEvent) { + if event.trackingArea?.userInfo?["dynamicIconColorHelpButton"] as? Bool == true { + isHoveringHelpButton = true + showDynamicIconColorHelpPopover() + return + } + if event.trackingArea?.userInfo?["dynamicIconColorHelpPopover"] as? Bool == true { + isHoveringHelpPopover = true + return + } + super.mouseEntered(with: event) + } + + override func mouseExited(with event: NSEvent) { + if event.trackingArea?.userInfo?["dynamicIconColorHelpButton"] as? Bool == true { + isHoveringHelpButton = false + scheduleHelpPopoverCloseIfNeeded() + return + } + if event.trackingArea?.userInfo?["dynamicIconColorHelpPopover"] as? Bool == true { + isHoveringHelpPopover = false + scheduleHelpPopoverCloseIfNeeded() + return + } + super.mouseExited(with: event) + } + + private func showDynamicIconColorHelpPopover() { + guard let button = dynamicIconColorHelpButton else { return } + if dynamicIconColorHelpPopover.isShown { return } + dynamicIconColorHelpPopover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) + } + + private func scheduleHelpPopoverCloseIfNeeded() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + guard let self = self else { return } + if !self.isHoveringHelpButton && !self.isHoveringHelpPopover { + self.dynamicIconColorHelpPopover.performClose(nil) + } + } + } + // MARK: - 通知阈值 private enum ThresholdType { @@ -402,14 +487,6 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate { StatsManager.shared.menuBarUpdateHandler?() } - @objc private func showDynamicIconColorHelp(_ sender: NSButton) { - if dynamicIconColorHelpPopover.isShown { - dynamicIconColorHelpPopover.performClose(nil) - return - } - dynamicIconColorHelpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .maxY) - } - @objc private func toggleLaunchAtLogin() { let shouldEnable = launchAtLoginButton.state == .on do { diff --git a/KeyStats/en.lproj/Localizable.strings b/KeyStats/en.lproj/Localizable.strings index fc7e5b9..e01e2c0 100644 --- a/KeyStats/en.lproj/Localizable.strings +++ b/KeyStats/en.lproj/Localizable.strings @@ -5,12 +5,12 @@ "button.permission" = "Grant Permission"; "button.launchAtLogin" = "Open at Login"; "settings.title" = "Settings"; -"settings.dynamicIconColor" = "Dynamic Icon Color"; -"settings.dynamicIconColorStyle" = "Dynamic Color Display"; +"settings.dynamicIconColor" = "Enable Dynamic Icon Color"; +"settings.dynamicIconColorStyle" = "Display When Enabled"; "settings.dynamicIconColorStyle.icon" = "Tint Icon"; "settings.dynamicIconColorStyle.dot" = "Status Dot"; "settings.dynamicIconColorHelp.title" = "Dynamic Color Guide"; -"settings.dynamicIconColorHelp.body" = "Colors are calculated from recent input rate and shown in the menu bar.\n\nRate: a 3-second sliding window counts input events (keyboard/mouse/scroll) with 0.5-second buckets; current rate = total events in 3 seconds ÷ 3, in events per second.\nAPM: APM = events per second × 60.\nColor ranges:\n• APM < 80: no tint (default)\n• 80–160: light green → green\n• 160–240: yellow → red\n• ≥ 240: red\n\nChoose between icon tint and status dot in settings.\nDynamic color only applies when enabled; low rates keep the default style.\nDisplay mode and color updates stay in sync for clear feedback."; +"settings.dynamicIconColorHelp.body" = "Colors are calculated from recent input rate and shown in the menu bar.\n\nAPM (events per minute) ranges:\n• < 80: no tint (default)\n• 80–160: light green → green\n• 160–240: yellow → red\n• ≥ 240: red"; "settings.windowTitle" = "KeyStats Settings"; "button.back" = "Back"; "setting.showKeyPresses" = "Show Key Presses in Menu Bar"; diff --git a/KeyStats/zh-Hans.lproj/Localizable.strings b/KeyStats/zh-Hans.lproj/Localizable.strings index 070b87b..45b0373 100644 --- a/KeyStats/zh-Hans.lproj/Localizable.strings +++ b/KeyStats/zh-Hans.lproj/Localizable.strings @@ -5,12 +5,12 @@ "button.permission" = "获取权限"; "button.launchAtLogin" = "开机启动"; "settings.title" = "设置"; -"settings.dynamicIconColor" = "动态图标颜色"; -"settings.dynamicIconColorStyle" = "动态颜色显示"; +"settings.dynamicIconColor" = "启用动态图标颜色"; +"settings.dynamicIconColorStyle" = "启用后显示方式"; "settings.dynamicIconColorStyle.icon" = "图标着色"; "settings.dynamicIconColorStyle.dot" = "状态圆点"; "settings.dynamicIconColorHelp.title" = "动态颜色说明"; -"settings.dynamicIconColorHelp.body" = "根据近期输入速率自动计算颜色,并在菜单栏实时展示。\n\n速率计算:使用 3 秒滑动窗口统计输入事件次数(键盘/鼠标/滚轮),每 0.5 秒滚动一个桶;当前速率 = 3 秒内总事件数 ÷ 3,得到每秒事件数。\nAPM 换算:APM = 每秒事件数 × 60。\n颜色范围:\n• APM < 80:不着色(保持默认)\n• 80–160:从浅绿渐变到绿\n• 160–240:从黄渐变到红\n• ≥ 240:红色\n\n提供图标着色或状态圆点两种展示方式,可在设置中切换。\n动态颜色仅在开启后生效,低速时保持默认样式。\n展示方式与颜色更新保持同步,确保菜单栏反馈直观一致。"; +"settings.dynamicIconColorHelp.body" = "根据近期输入速率自动计算颜色,并在菜单栏实时展示。\n\nAPM(每分钟输入事件数)颜色范围:\n• < 80:不着色(保持默认)\n• 80–160:从浅绿渐变到绿\n• 160–240:从黄渐变到红\n• ≥ 240:红色"; "settings.windowTitle" = "KeyStats设置"; "button.back" = "返回"; "setting.showKeyPresses" = "在菜单栏显示按键数";