Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions KeyStats.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
A1000020 /* AllTimeStatsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000020 /* AllTimeStatsWindowController.swift */; };
A1000021 /* AllTimeStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000021 /* AllTimeStatsViewController.swift */; };
A1000022 /* AppStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000022 /* AppStats.swift */; };
A1000032 /* StatsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000032 /* StatsModels.swift */; };
A1000023 /* AppActivityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000023 /* AppActivityTracker.swift */; };
A1000024 /* AppStatsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000024 /* AppStatsWindowController.swift */; };
A1000025 /* AppStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000025 /* AppStatsViewController.swift */; };
Expand Down Expand Up @@ -58,6 +59,7 @@
A2000020 /* AllTimeStatsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllTimeStatsWindowController.swift; sourceTree = "<group>"; };
A2000021 /* AllTimeStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllTimeStatsViewController.swift; sourceTree = "<group>"; };
A2000022 /* AppStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStats.swift; sourceTree = "<group>"; };
A2000032 /* StatsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsModels.swift; sourceTree = "<group>"; };
A2000023 /* AppActivityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityTracker.swift; sourceTree = "<group>"; };
A2000024 /* AppStatsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStatsWindowController.swift; sourceTree = "<group>"; };
A2000025 /* AppStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStatsViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -101,6 +103,7 @@
A2000023 /* AppActivityTracker.swift */,
A2000003 /* StatsManager.swift */,
A2000022 /* AppStats.swift */,
A2000032 /* StatsModels.swift */,
A2000004 /* MenuBarController.swift */,
A2000005 /* StatsPopoverViewController.swift */,
A2000024 /* AppStatsWindowController.swift */,
Expand Down Expand Up @@ -222,6 +225,7 @@
A1000023 /* AppActivityTracker.swift in Sources */,
A1000003 /* StatsManager.swift in Sources */,
A1000022 /* AppStats.swift in Sources */,
A1000032 /* StatsModels.swift in Sources */,
A1000004 /* MenuBarController.swift in Sources */,
A1000005 /* StatsPopoverViewController.swift in Sources */,
A1000024 /* AppStatsWindowController.swift in Sources */,
Expand Down
211 changes: 6 additions & 205 deletions KeyStats/StatsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,143 +2,11 @@ import Foundation
import Cocoa
import UserNotifications

private let baseMetersPerPixel: Double = 0.000264583

private func baseKeyComponent(_ keyName: String) -> String {
let trimmed = keyName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return "" }
if let last = trimmed.split(separator: "+").last {
return String(last).trimmingCharacters(in: .whitespacesAndNewlines)
}
return trimmed
}

/// 统计数据结构
struct DailyStats: Codable {
var date: Date
var keyPresses: Int
var keyPressCounts: [String: Int]
var leftClicks: Int
var rightClicks: Int
var sideBackClicks: Int
var sideForwardClicks: Int
var mouseDistance: Double // 以像素为单位
var scrollDistance: Double // 以像素为单位
var appStats: [String: AppStats]

init() {
self.date = Calendar.current.startOfDay(for: Date())
self.keyPresses = 0
self.keyPressCounts = [:]
self.leftClicks = 0
self.rightClicks = 0
self.sideBackClicks = 0
self.sideForwardClicks = 0
self.mouseDistance = 0
self.scrollDistance = 0
self.appStats = [:]
}

init(date: Date) {
self.date = Calendar.current.startOfDay(for: date)
self.keyPresses = 0
self.keyPressCounts = [:]
self.leftClicks = 0
self.rightClicks = 0
self.sideBackClicks = 0
self.sideForwardClicks = 0
self.mouseDistance = 0
self.scrollDistance = 0
self.appStats = [:]
}

enum CodingKeys: String, CodingKey {
case date
case keyPresses
case keyPressCounts
case leftClicks
case rightClicks
case sideBackClicks
case sideForwardClicks
// legacy field
case otherClicks
case mouseDistance
case scrollDistance
case appStats
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try container.decodeIfPresent(Date.self, forKey: .date) ?? Calendar.current.startOfDay(for: Date())
keyPresses = try container.decodeIfPresent(Int.self, forKey: .keyPresses) ?? 0
keyPressCounts = try container.decodeIfPresent([String: Int].self, forKey: .keyPressCounts) ?? [:]
leftClicks = try container.decodeIfPresent(Int.self, forKey: .leftClicks) ?? 0
rightClicks = try container.decodeIfPresent(Int.self, forKey: .rightClicks) ?? 0
sideBackClicks = try container.decodeIfPresent(Int.self, forKey: .sideBackClicks) ?? 0
sideForwardClicks = try container.decodeIfPresent(Int.self, forKey: .sideForwardClicks) ?? 0
// Backward compatibility: old builds stored all side clicks in `otherClicks`.
if !container.contains(.sideBackClicks) && !container.contains(.sideForwardClicks) {
sideBackClicks = try container.decodeIfPresent(Int.self, forKey: .otherClicks) ?? 0
}
mouseDistance = try container.decodeIfPresent(Double.self, forKey: .mouseDistance) ?? 0
scrollDistance = try container.decodeIfPresent(Double.self, forKey: .scrollDistance) ?? 0
appStats = try container.decodeIfPresent([String: AppStats].self, forKey: .appStats) ?? [:]
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(date, forKey: .date)
try container.encode(keyPresses, forKey: .keyPresses)
try container.encode(keyPressCounts, forKey: .keyPressCounts)
try container.encode(leftClicks, forKey: .leftClicks)
try container.encode(rightClicks, forKey: .rightClicks)
try container.encode(sideBackClicks, forKey: .sideBackClicks)
try container.encode(sideForwardClicks, forKey: .sideForwardClicks)
try container.encode(mouseDistance, forKey: .mouseDistance)
try container.encode(scrollDistance, forKey: .scrollDistance)
try container.encode(appStats, forKey: .appStats)
}

var totalClicks: Int {
return leftClicks + rightClicks + sideBackClicks + sideForwardClicks
}

var hasAnyActivity: Bool {
return keyPresses > 0 ||
leftClicks > 0 ||
rightClicks > 0 ||
sideBackClicks > 0 ||
sideForwardClicks > 0 ||
mouseDistance > 0 ||
scrollDistance > 0 ||
!keyPressCounts.isEmpty ||
!appStats.isEmpty
}

/// 纠错率 (Delete + ForwardDelete / Total Keys)
var correctionRate: Double {
guard keyPresses > 0 else { return 0 }
let deleteLikeCount = keyPressCounts.reduce(0) { partial, entry in
let base = baseKeyComponent(entry.key)
guard base == "Delete" || base == "ForwardDelete" else { return partial }
return partial + entry.value
}
return Double(deleteLikeCount) / Double(keyPresses)
}

/// 键鼠比 (Keys / Clicks)
var inputRatio: Double {
let clicks = totalClicks
guard clicks > 0 else { return keyPresses > 0 ? Double.infinity : 0 }
return Double(keyPresses) / Double(clicks)
}

/// 格式化鼠标移动距离
extension DailyStats {
var formattedMouseDistance: String {
return StatsManager.shared.formatMouseDistance(mouseDistance)
StatsManager.shared.formatMouseDistance(mouseDistance)
}

/// 格式化滚动距离

var formattedScrollDistance: String {
if scrollDistance >= 10000 {
return String(format: "%.1f kPx", scrollDistance / 1000)
Expand All @@ -148,85 +16,18 @@ struct DailyStats: Codable {
}
}

/// 有史以来统计数据结构
struct AllTimeStats {
var totalKeyPresses: Int
var totalLeftClicks: Int
var totalRightClicks: Int
var totalSideBackClicks: Int
var totalSideForwardClicks: Int
var totalMouseDistance: Double
var totalScrollDistance: Double
var keyPressCounts: [String: Int]
var firstDate: Date?
var lastDate: Date?
var activeDays: Int
var maxDailyKeyPresses: Int
var maxDailyKeyPressesDate: Date?
var maxDailyClicks: Int
var maxDailyClicksDate: Date?
var mostActiveWeekday: Int?
var keyActiveDays: Int
var clickActiveDays: Int

var totalClicks: Int {
return totalLeftClicks + totalRightClicks + totalSideBackClicks + totalSideForwardClicks
}

/// 纠错率 (Delete + ForwardDelete / Total Keys)
var correctionRate: Double {
guard totalKeyPresses > 0 else { return 0 }
let deleteLikeCount = keyPressCounts.reduce(0) { partial, entry in
let base = baseKeyComponent(entry.key)
guard base == "Delete" || base == "ForwardDelete" else { return partial }
return partial + entry.value
}
return Double(deleteLikeCount) / Double(totalKeyPresses)
}

/// 键鼠比 (Keys / Clicks)
var inputRatio: Double {
let clicks = totalClicks
guard clicks > 0 else { return totalKeyPresses > 0 ? Double.infinity : 0 }
return Double(totalKeyPresses) / Double(clicks)
}

/// 格式化鼠标移动距离
extension AllTimeStats {
var formattedMouseDistance: String {
return StatsManager.shared.formatMouseDistance(totalMouseDistance)
StatsManager.shared.formatMouseDistance(totalMouseDistance)
}

/// 格式化滚动距离

var formattedScrollDistance: String {
if totalScrollDistance >= 10000 {
return String(format: "%.1f kPx", totalScrollDistance / 1000)
} else {
return String(format: "%.0f px", totalScrollDistance)
}
}

static func initial() -> AllTimeStats {
return AllTimeStats(
totalKeyPresses: 0,
totalLeftClicks: 0,
totalRightClicks: 0,
totalSideBackClicks: 0,
totalSideForwardClicks: 0,
totalMouseDistance: 0,
totalScrollDistance: 0,
keyPressCounts: [:],
firstDate: nil,
lastDate: nil,
activeDays: 0,
maxDailyKeyPresses: 0,
maxDailyKeyPressesDate: nil,
maxDailyClicks: 0,
maxDailyClicksDate: nil,
mostActiveWeekday: nil,
keyActiveDays: 0,
clickActiveDays: 0
)
}
}

/// 统计数据管理器 - 单例模式
Expand Down
Loading