mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
193 lines
6.8 KiB
Swift
193 lines
6.8 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
var body: some View {
|
|
TabView {
|
|
GeneralSettings()
|
|
.tabItem { Label("General", systemImage: "gearshape") }
|
|
LicenseSettings()
|
|
.tabItem { Label("License", systemImage: "key") }
|
|
ModelSettings()
|
|
.tabItem { Label("Model", systemImage: "cpu") }
|
|
DictionarySettings()
|
|
.tabItem { Label("Dictionary", systemImage: "character.book.closed") }
|
|
}
|
|
.padding(20)
|
|
}
|
|
}
|
|
|
|
// MARK: - General
|
|
|
|
private struct GeneralSettings: View {
|
|
@EnvironmentObject private var app: AppState
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("Shortcuts") {
|
|
LabeledContent("Fix selection", value: "⌘;")
|
|
LabeledContent("Ask GrammarGem", value: "⌘'")
|
|
Text("Custom shortcuts coming via the KeyboardShortcuts recorder (see README).")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Section("App-aware switching") {
|
|
if app.gate.appAwareEnabled {
|
|
Toggle("Auto-apply a Writing Mode based on the active app", isOn: $app.appAwareEnabled)
|
|
} else {
|
|
Label("App-aware switching is part of the lifetime upgrade.", systemImage: "lock")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
Section("Permissions") {
|
|
LabeledContent("Accessibility",
|
|
value: app.permissions.accessibilityTrusted ? "Granted" : "Needed")
|
|
if !app.permissions.accessibilityTrusted {
|
|
Button("Open onboarding") { app.showOnboarding() }
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|
|
|
|
// MARK: - License
|
|
|
|
private struct LicenseSettings: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@State private var keyField = ""
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("Plan") {
|
|
LabeledContent("Current tier", value: app.license.tier.displayName)
|
|
LabeledContent("Device cap", value: "\(app.license.tier.deviceCap) Mac\(app.license.tier.deviceCap == 1 ? "" : "s")")
|
|
}
|
|
|
|
if app.license.isLicensed {
|
|
Section("This device") {
|
|
Button(app.license.isWorking ? "Working…" : "Deactivate this device") {
|
|
Task { await app.license.deactivateThisDevice() }
|
|
}
|
|
.disabled(app.license.isWorking)
|
|
Text("Frees a slot so you can move the license to another Mac.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
} else {
|
|
Section("Activate") {
|
|
TextField("Paste your license key", text: $keyField)
|
|
.textFieldStyle(.roundedBorder)
|
|
Button(app.license.isWorking ? "Activating…" : "Activate") {
|
|
Task { await app.license.activate(key: keyField) }
|
|
}
|
|
.disabled(app.license.isWorking || keyField.isEmpty)
|
|
}
|
|
}
|
|
|
|
if let error = app.license.lastError {
|
|
Section {
|
|
Label(error, systemImage: "exclamationmark.triangle")
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
Link("Manage your license", destination: AppConfig.modelPortalURL)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|
|
|
|
// MARK: - Model
|
|
|
|
private struct ModelSettings: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@ObservedObject private var model: ModelManager
|
|
|
|
init() {
|
|
_model = ObservedObject(wrappedValue: AppState.shared.model)
|
|
}
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section("On-device model") {
|
|
Picker("Model", selection: $model.selectedRepo) {
|
|
Text("Qwen2.5-3B (default)").tag(AppConfig.Model.defaultRepo)
|
|
Text("Qwen2.5-7B (more RAM)")
|
|
.tag(AppConfig.Model.largeRepo)
|
|
}
|
|
.disabled(!app.gate.entitlements.canUseLargeModel)
|
|
if !app.gate.entitlements.canUseLargeModel {
|
|
Label("The larger 7B model is available on paid plans.", systemImage: "lock")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
ModelDownloadControls().environmentObject(app)
|
|
}
|
|
Section {
|
|
Text("Downloaded once from Hugging Face, then everything runs offline. The model is never bundled in the app binary.")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|
|
|
|
// MARK: - Dictionary
|
|
|
|
private struct DictionarySettings: View {
|
|
@EnvironmentObject private var app: AppState
|
|
@ObservedObject private var dict: PersonalDictionary
|
|
@State private var newWord = ""
|
|
@State private var capMessage: String?
|
|
|
|
init() {
|
|
// Bound at render via environment; we re-observe the shared instance.
|
|
_dict = ObservedObject(wrappedValue: AppState.shared.dictionary)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
TextField("Add a word GrammarGem should never correct", text: $newWord)
|
|
.textFieldStyle(.roundedBorder)
|
|
.onSubmit(add)
|
|
Button("Add", action: add).disabled(newWord.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
}
|
|
|
|
if let capMessage {
|
|
Label(capMessage, systemImage: "lock").font(.caption).foregroundStyle(.orange)
|
|
}
|
|
|
|
let cap = app.gate.entitlements.dictionaryCap
|
|
Text(cap == Int.max
|
|
? "\(dict.entries.count) entries · unlimited"
|
|
: "\(dict.entries.count) / \(cap) entries")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
|
|
List {
|
|
ForEach(dict.entries, id: \.self) { word in
|
|
HStack {
|
|
Text(word)
|
|
Spacer()
|
|
Button {
|
|
app.removeDictionaryEntry(word)
|
|
} label: { Image(systemName: "trash") }
|
|
.buttonStyle(.borderless)
|
|
}
|
|
}
|
|
}
|
|
.frame(minHeight: 220)
|
|
}
|
|
}
|
|
|
|
private func add() {
|
|
let decision = app.addDictionaryEntry(newWord)
|
|
switch decision {
|
|
case .allowed:
|
|
newWord = ""
|
|
capMessage = nil
|
|
case .denied(let message):
|
|
capMessage = message
|
|
}
|
|
}
|
|
}
|