mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
- 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.
116 lines
4.9 KiB
Swift
116 lines
4.9 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
struct ExclusionsView: View {
|
|
@EnvironmentObject private var exclusions: Exclusions
|
|
@State private var newDomain = ""
|
|
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "Page Blocker",
|
|
subtitle: "Keep GrammaGem out of the way in sensitive apps and sites."
|
|
) {
|
|
Card {
|
|
Label("How it works", systemImage: "hand.raised")
|
|
.font(.headline)
|
|
Text("When the frontmost app or website is on these lists, GrammaGem stops monitoring and the hotkeys do nothing — useful for password managers, banking, and private work.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
}
|
|
|
|
// Blocked apps
|
|
Card {
|
|
HStack {
|
|
Text("Blocked apps").font(.headline)
|
|
Spacer()
|
|
addAppMenu
|
|
}
|
|
if exclusions.blockedApps.isEmpty {
|
|
Text("No apps blocked.").font(.callout).foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(exclusions.blockedApps, id: \.self) { bundle in
|
|
HStack {
|
|
Image(systemName: "app.dashed").foregroundStyle(.secondary)
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(exclusions.appName(for: bundle))
|
|
Text(bundle).font(.caption2.monospaced()).foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
Button(role: .destructive) { exclusions.removeApp(bundle) } label: {
|
|
Image(systemName: "trash")
|
|
}.buttonStyle(.borderless)
|
|
}
|
|
if bundle != exclusions.blockedApps.last { Divider() }
|
|
}
|
|
}
|
|
Divider()
|
|
HStack(spacing: 8) {
|
|
Text("Suggestions:").font(.caption).foregroundStyle(.secondary)
|
|
ForEach(Exclusions.suggestedApps, id: \.bundleID) { item in
|
|
if !exclusions.blockedApps.contains(item.bundleID) {
|
|
Button(item.name) { exclusions.addApp(item.bundleID) }
|
|
.buttonStyle(.bordered).controlSize(.small)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blocked domains
|
|
Card {
|
|
Text("Blocked websites").font(.headline)
|
|
if exclusions.blockedDomains.isEmpty {
|
|
Text("No websites blocked.").font(.callout).foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(exclusions.blockedDomains, id: \.self) { domain in
|
|
HStack {
|
|
Image(systemName: "globe").foregroundStyle(.secondary)
|
|
Text(domain)
|
|
Spacer()
|
|
Button(role: .destructive) { exclusions.removeDomain(domain) } label: {
|
|
Image(systemName: "trash")
|
|
}.buttonStyle(.borderless)
|
|
}
|
|
if domain != exclusions.blockedDomains.last { Divider() }
|
|
}
|
|
}
|
|
HStack {
|
|
TextField("Add a domain (e.g. mybank.com)", text: $newDomain)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onSubmit(addDomain)
|
|
Button("Block", action: addDomain)
|
|
.disabled(newDomain.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
Text("Website blocking works best in Safari; other browsers are matched when possible.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var addAppMenu: some View {
|
|
Menu {
|
|
ForEach(runningApps(), id: \.bundleID) { item in
|
|
Button(item.name) { exclusions.addApp(item.bundleID) }
|
|
}
|
|
} label: {
|
|
Label("Add app", systemImage: "plus")
|
|
}
|
|
.menuStyle(.borderlessButton)
|
|
.fixedSize()
|
|
}
|
|
|
|
private func addDomain() {
|
|
exclusions.addDomain(newDomain)
|
|
newDomain = ""
|
|
}
|
|
|
|
private func runningApps() -> [(name: String, bundleID: String)] {
|
|
NSWorkspace.shared.runningApplications
|
|
.filter { $0.activationPolicy == .regular && $0.bundleIdentifier != nil }
|
|
.compactMap { app -> (String, String)? in
|
|
guard let id = app.bundleIdentifier else { return nil }
|
|
return (app.localizedName ?? id, id)
|
|
}
|
|
.sorted { $0.0.localizedCaseInsensitiveCompare($1.0) == .orderedAscending }
|
|
.map { (name: $0.0, bundleID: $0.1) }
|
|
}
|
|
}
|