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>
140 lines
5.6 KiB
Swift
140 lines
5.6 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Shortcuts
|
|
|
|
struct ShortcutsView: View {
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "Shortcuts",
|
|
subtitle: "Global hotkeys that work in every app."
|
|
) {
|
|
Card {
|
|
shortcutRow(name: "Fix selection", keys: ["⌘", ";"], detail: "Grammar + clarity, in place")
|
|
Divider()
|
|
shortcutRow(name: "Ask GrammaGem", keys: ["⌘", "'"], detail: "Rewrite with an instruction")
|
|
}
|
|
Card {
|
|
Label("Custom shortcuts", systemImage: "wand.and.stars")
|
|
.font(.headline)
|
|
Text("A built-in recorder for fully customizable shortcuts is on the roadmap. For now the defaults above are active system-wide.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func shortcutRow(name: String, keys: [String], detail: String) -> some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(name).fontWeight(.medium)
|
|
Text(detail).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
HStack(spacing: 4) {
|
|
ForEach(keys, id: \.self) { k in
|
|
Text(k)
|
|
.font(.system(.body, design: .monospaced))
|
|
.padding(.horizontal, 8).padding(.vertical, 4)
|
|
.background(Color(nsColor: .controlColor), in: RoundedRectangle(cornerRadius: 6))
|
|
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Color.primary.opacity(0.12)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - AI Model
|
|
|
|
struct ModelView: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@EnvironmentObject private var gate: FeatureGate
|
|
@EnvironmentObject private var model: ModelManager
|
|
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "AI Model",
|
|
subtitle: "The local model that powers rewrites, tone, and Ask. Runs entirely on your Mac."
|
|
) {
|
|
Card {
|
|
Picker("Model", selection: $model.selectedRepo) {
|
|
Text("Qwen2.5-3B · balanced (default)").tag(AppConfig.Model.defaultRepo)
|
|
Text("Qwen2.5-7B · higher quality").tag(AppConfig.Model.largeRepo)
|
|
}
|
|
.disabled(!gate.entitlements.canUseLargeModel)
|
|
|
|
if !gate.entitlements.canUseLargeModel {
|
|
HStack { Image(systemName: "lock.fill").foregroundStyle(GG.gold); Text("The larger model is available on the lifetime license.").font(.caption).foregroundStyle(.secondary) }
|
|
}
|
|
Divider()
|
|
ModelDownloadControls().environmentObject(app)
|
|
}
|
|
|
|
Card {
|
|
Label("Private by default", systemImage: "lock.shield")
|
|
.font(.headline)
|
|
Text("The model is downloaded once from Hugging Face and then runs offline. It is never bundled in the app, and it never sends your text anywhere.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Privacy
|
|
|
|
struct PrivacyView: View {
|
|
@EnvironmentObject private var usage: UsageStats
|
|
@State private var confirmReset = false
|
|
|
|
var body: some View {
|
|
DetailScaffold(
|
|
title: "Privacy",
|
|
subtitle: "What touches the network — and what never does."
|
|
) {
|
|
Card {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "checkmark.shield.fill").font(.title2).foregroundStyle(GG.emerald)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("0 bytes of your writing leave this Mac").font(.headline)
|
|
Text("Every correction and rewrite runs locally. No telemetry is collected.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
Card {
|
|
Text("The only times GrammaGem uses the network").font(.headline)
|
|
networkRow("One-time AI model download", "Hugging Face, on first run")
|
|
Divider()
|
|
networkRow("License activation & checks", "To enforce your device cap")
|
|
Divider()
|
|
networkRow("App updates", "Signed update checks")
|
|
Text("None of these ever include the text you write.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
|
|
Card {
|
|
Text("Local data").font(.headline)
|
|
Text("Your dictionary, snippets, modes, and usage counts live only on this Mac.")
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
Button(role: .destructive) { confirmReset = true } label: {
|
|
Label("Reset usage statistics", systemImage: "arrow.counterclockwise")
|
|
}
|
|
.confirmationDialog("Reset all local usage statistics?", isPresented: $confirmReset) {
|
|
Button("Reset", role: .destructive) { usage.reset() }
|
|
Button("Cancel", role: .cancel) {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func networkRow(_ title: String, _ detail: String) -> some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "arrow.up.forward.circle").foregroundStyle(.secondary)
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(title)
|
|
Text(detail).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|