mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
- Management app: sidebar window with Dashboard (usage insights), Devices (slot usage, deactivate this Mac), Writing Modes (custom + app-aware map), Dictionary, Snippets, Shortcuts, AI Model, Privacy, General, License, About. - New local stores: Preferences (launch-at-login, appearance), UsageStats (privacy-friendly local insights), CustomModesStore, SnippetStore. - License model now tracks device name + activation usage/limit for the Devices screen. - README rewritten as a conversion-focused product page (no internal cost/setup). - Legal entity set to FounderGem LLC across README, LICENSE, Info.plist, About. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.7 KiB
Swift
91 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
/// Privacy-friendly, fully-local usage insights. Counts *how often* features are
|
|
/// used — never *what* was written. Stored only on this Mac.
|
|
@MainActor
|
|
final class UsageStats: ObservableObject {
|
|
struct Snapshot: Codable, Equatable {
|
|
var totalCorrections = 0
|
|
var totalAIActions = 0
|
|
var totalWords = 0
|
|
var perDay: [String: Int] = [:] // "yyyy-MM-dd" -> actions that day
|
|
}
|
|
|
|
@Published private(set) var data: Snapshot
|
|
|
|
private let key = "GrammaGem.usageStats"
|
|
private let d = UserDefaults.standard
|
|
|
|
private static let fmt: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "yyyy-MM-dd"
|
|
f.locale = Locale(identifier: "en_US_POSIX")
|
|
f.timeZone = .current
|
|
return f
|
|
}()
|
|
|
|
init() {
|
|
if let raw = d.data(forKey: key),
|
|
let decoded = try? JSONDecoder().decode(Snapshot.self, from: raw) {
|
|
data = decoded
|
|
} else {
|
|
data = Snapshot()
|
|
}
|
|
}
|
|
|
|
func recordCorrection(words: Int) {
|
|
data.totalCorrections += 1
|
|
data.totalWords += max(0, words)
|
|
bumpToday()
|
|
persist()
|
|
}
|
|
|
|
func recordAIAction(words: Int) {
|
|
data.totalAIActions += 1
|
|
data.totalWords += max(0, words)
|
|
bumpToday()
|
|
persist()
|
|
}
|
|
|
|
/// A playful, clearly-estimated "time saved" figure (local only).
|
|
var estimatedMinutesSaved: Int {
|
|
(data.totalCorrections * 8 + data.totalAIActions * 45) / 60
|
|
}
|
|
|
|
/// Counts for the last 7 calendar days, oldest → newest, for a small chart.
|
|
func last7Days() -> [(label: String, count: Int)] {
|
|
let cal = Calendar.current
|
|
let today = Date()
|
|
return (0..<7).reversed().map { offset in
|
|
let day = cal.date(byAdding: .day, value: -offset, to: today) ?? today
|
|
let key = Self.fmt.string(from: day)
|
|
let short = day.formatted(.dateTime.weekday(.narrow))
|
|
return (label: short, count: data.perDay[key] ?? 0)
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
data = Snapshot()
|
|
persist()
|
|
}
|
|
|
|
private func bumpToday() {
|
|
let k = Self.fmt.string(from: Date())
|
|
data.perDay[k, default: 0] += 1
|
|
// Keep only ~60 days of history.
|
|
if data.perDay.count > 70 {
|
|
let keep = Set((0..<60).compactMap { off -> String? in
|
|
guard let day = Calendar.current.date(byAdding: .day, value: -off, to: Date()) else { return nil }
|
|
return Self.fmt.string(from: day)
|
|
})
|
|
data.perDay = data.perDay.filter { keep.contains($0.key) }
|
|
}
|
|
}
|
|
|
|
private func persist() {
|
|
if let raw = try? JSONEncoder().encode(data) {
|
|
d.set(raw, forKey: key)
|
|
}
|
|
}
|
|
}
|