grammargem/Sources/GrammarGem/App/MainWindow/MainWindowView.swift
BigMarc 3d749e8139 feat: Implement LiveMonitor for real-time grammar and spelling checks across applications
- Added LiveMonitor class to scan text areas in the frontmost window for grammar and spelling issues.
- Introduced DetectedField struct to represent text areas with issues, including suggestions for corrections.
- Implemented functionality to apply corrections and dismiss suggestions.
- Added Permissions class to manage Accessibility permissions required for monitoring.
- Created SnippetStore for managing text-expansion snippets, including persistence to UserDefaults.
- Developed TextCapture class to read and write text from the frontmost application using Accessibility API and clipboard fallback.
- Implemented TextFieldScanner to traverse the Accessibility tree and collect editable text elements.
- Added TextReplacementCoordinator to orchestrate the text correction process.
- Integrated UpdaterManager for handling application updates using Sparkle.
- Created comprehensive unit tests for various components, including grammar corrections and entitlement checks.
2026-06-18 16:49:55 +02:00

183 lines
5.9 KiB
Swift

import SwiftUI
/// Brand accents for the app UI (mirrors the website CI).
enum GG {
static let emerald = Color(red: 0x0E / 255, green: 0x7C / 255, blue: 0x5A / 255)
static let gold = Color(red: 0xC9 / 255, green: 0xA2 / 255, blue: 0x4B / 255)
}
/// Sections of the management window.
enum MainSection: String, CaseIterable, Identifiable {
case dashboard, devices, license
case modes, dictionary, snippets
case shortcuts, model, privacy, exclusions, general
case about
var id: String { rawValue }
var title: String {
switch self {
case .dashboard: return "Dashboard"
case .devices: return "Devices"
case .license: return "License"
case .modes: return "Writing Modes"
case .dictionary: return "Dictionary"
case .snippets: return "Snippets"
case .shortcuts: return "Shortcuts"
case .model: return "AI Model"
case .privacy: return "Privacy"
case .exclusions: return "Page Blocker"
case .general: return "General"
case .about: return "About"
}
}
var systemImage: String {
switch self {
case .dashboard: return "square.grid.2x2"
case .devices: return "laptopcomputer"
case .license: return "key"
case .modes: return "slider.horizontal.3"
case .dictionary: return "character.book.closed"
case .snippets: return "text.badge.plus"
case .shortcuts: return "command"
case .model: return "cpu"
case .privacy: return "lock.shield"
case .exclusions: return "hand.raised"
case .general: return "gearshape"
case .about: return "info.circle"
}
}
/// Sidebar groups, in order.
static let groups: [(String, [MainSection])] = [
("Overview", [.dashboard]),
("Account", [.devices, .license]),
("Writing", [.modes, .dictionary, .snippets]),
("App", [.shortcuts, .model, .privacy, .exclusions, .general]),
("", [.about]),
]
}
/// The full GrammaGem management window: a clean sidebar + detail layout.
struct MainWindowView: View {
@State private var selection: MainSection?
init(initialSection: MainSection = .dashboard) {
_selection = State(initialValue: initialSection)
}
var body: some View {
NavigationSplitView {
List(selection: $selection) {
ForEach(MainSection.groups, id: \.0) { group, items in
Section(group) {
ForEach(items) { section in
Label(section.title, systemImage: section.systemImage)
.tag(section)
}
}
}
}
.navigationSplitViewColumnWidth(min: 198, ideal: 216, max: 260)
.listStyle(.sidebar)
} detail: {
detail(for: selection ?? .dashboard)
}
.tint(GG.emerald)
.frame(minWidth: 820, minHeight: 560)
}
@ViewBuilder
private func detail(for section: MainSection) -> some View {
switch section {
case .dashboard: DashboardView()
case .devices: DevicesView()
case .license: LicenseView()
case .modes: WritingModesView()
case .dictionary: DictionaryView()
case .snippets: SnippetsView()
case .shortcuts: ShortcutsView()
case .model: ModelView()
case .privacy: PrivacyView()
case .exclusions: ExclusionsView()
case .general: GeneralView()
case .about: AboutView()
}
}
}
// MARK: - Reusable layout
/// A scrollable detail page with a title/subtitle header and a max content width.
struct DetailScaffold<Content: View>: View {
let title: String
var subtitle: String? = nil
@ViewBuilder var content: Content
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 22) {
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.largeTitle.bold())
if let subtitle {
Text(subtitle).font(.body).foregroundStyle(.secondary)
}
}
content
}
.frame(maxWidth: 720, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(28)
}
}
}
/// A soft card container used throughout the window.
struct Card<Content: View>: View {
var padding: CGFloat = 18
@ViewBuilder var content: Content
var body: some View {
VStack(alignment: .leading, spacing: 12) { content }
.padding(padding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.primary.opacity(0.07), lineWidth: 1)
)
}
}
/// A small labelled metric tile.
struct StatTile: View {
let value: String
let label: String
var systemImage: String? = nil
var tint: Color = .primary
var body: some View {
Card {
HStack(spacing: 8) {
if let systemImage {
Image(systemName: systemImage).foregroundStyle(tint)
}
Text(value).font(.system(.title, design: .rounded).weight(.semibold)).foregroundStyle(tint)
}
Text(label).font(.caption).foregroundStyle(.secondary)
}
}
}
/// A locked-feature badge used to surface upgrade triggers cleanly.
struct UpgradeBadge: View {
var body: some View {
Label("Lifetime", systemImage: "lock.fill")
.font(.caption2.weight(.semibold))
.padding(.horizontal, 7).padding(.vertical, 3)
.background(GG.gold.opacity(0.18), in: Capsule())
.foregroundStyle(GG.gold)
}
}