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>
119 lines
4.9 KiB
Swift
119 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Dictionary
|
|
|
|
struct DictionaryView: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@EnvironmentObject private var gate: FeatureGate
|
|
@EnvironmentObject private var dictionary: PersonalDictionary
|
|
@State private var newWord = ""
|
|
@State private var message: String?
|
|
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "Personal Dictionary",
|
|
subtitle: "Words GrammaGem should never “correct” — names, brands, jargon."
|
|
) {
|
|
Card {
|
|
HStack {
|
|
TextField("Add a word…", text: $newWord)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onSubmit(add)
|
|
Button("Add", action: add)
|
|
.disabled(newWord.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
if let message {
|
|
Label(message, systemImage: "lock").font(.caption).foregroundStyle(.orange)
|
|
}
|
|
let cap = gate.entitlements.dictionaryCap
|
|
Text(cap == Int.max
|
|
? "\(dictionary.entries.count) entries · unlimited"
|
|
: "\(dictionary.entries.count) / \(cap) entries")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
|
|
Card {
|
|
if dictionary.entries.isEmpty {
|
|
Text("No entries yet.").foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(dictionary.entries, id: \.self) { word in
|
|
HStack {
|
|
Text(word)
|
|
Spacer()
|
|
Button(role: .destructive) { app.removeDictionaryEntry(word) } label: {
|
|
Image(systemName: "trash")
|
|
}.buttonStyle(.borderless)
|
|
}
|
|
if word != dictionary.entries.last { Divider() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func add() {
|
|
switch app.addDictionaryEntry(newWord) {
|
|
case .allowed: newWord = ""; message = nil
|
|
case .denied(let m): message = m
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Snippets
|
|
|
|
struct SnippetsView: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@EnvironmentObject private var gate: FeatureGate
|
|
@EnvironmentObject private var snippets: SnippetStore
|
|
@State private var trigger = ""
|
|
@State private var expansion = ""
|
|
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "Snippets",
|
|
subtitle: "Type a short trigger, expand it into anything. Stored only on this Mac."
|
|
) {
|
|
if gate.snippetsEnabled {
|
|
Card {
|
|
HStack(alignment: .top) {
|
|
TextField("Trigger (e.g. ;addr)", text: $trigger).textFieldStyle(.roundedBorder).frame(width: 160)
|
|
TextField("Expansion…", text: $expansion, axis: .vertical)
|
|
.lineLimit(1...4).textFieldStyle(.roundedBorder)
|
|
Button("Add") {
|
|
snippets.add(trigger: trigger, expansion: expansion)
|
|
trigger = ""; expansion = ""
|
|
}
|
|
.disabled(trigger.trimmingCharacters(in: .whitespaces).isEmpty
|
|
|| expansion.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
}
|
|
Card {
|
|
if snippets.snippets.isEmpty {
|
|
Text("No snippets yet.").foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(snippets.snippets) { s in
|
|
HStack(alignment: .top) {
|
|
Text(s.trigger).font(.callout.monospaced()).foregroundStyle(GG.emerald)
|
|
Text(s.expansion).foregroundStyle(.secondary).lineLimit(2)
|
|
Spacer()
|
|
Button(role: .destructive) { snippets.remove(s) } label: {
|
|
Image(systemName: "trash")
|
|
}.buttonStyle(.borderless)
|
|
}
|
|
if s.id != snippets.snippets.last?.id { Divider() }
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Card {
|
|
HStack { Text("Text expansion").font(.headline); UpgradeBadge() }
|
|
Text("Snippets are part of the lifetime license. Save your common replies, addresses, and boilerplate, and expand them with a keystroke anywhere.")
|
|
.foregroundStyle(.secondary)
|
|
Button("Unlock snippets") { app.showMainWindow(select: .license) }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|