176 lines
11 KiB
Swift
176 lines
11 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject var daemonConfig: DaemonConfiguration
|
|
@ObservedObject var editorState: EditorState
|
|
@ObservedObject var projectStore: ProjectStore
|
|
|
|
@State private var pendingHost = ""
|
|
@State private var pendingPort = ""
|
|
@State private var wcApiKey = ""
|
|
@State private var connStatus: ConnStatus = .untested
|
|
@State private var showAddProject = false
|
|
|
|
enum ConnStatus { case untested, testing, success, failure(String)
|
|
var label: String { switch self { case .untested: return "Not tested"; case .testing: return "Testing…"; case .success: return "Connected ✓"; case .failure(let m): return "Failed: \(m)" } }
|
|
var color: Color { switch self { case .success: return .green; case .failure: return .red; case .testing: return .secondary; default: return .secondary } }
|
|
static func ==(a: ConnStatus, b: ConnStatus) -> Bool {
|
|
switch (a,b) { case (.success,.success),(.untested,.untested),(.testing,.testing): return true; case (.failure(let x),.failure(let y)): return x==y; default: return false }
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
// ── Daemon ────────────────────────────────────────────────
|
|
Section {
|
|
HStack {
|
|
Label("Tailscale IP", systemImage:"network"); Spacer()
|
|
TextField("100.x.x.x", text:$pendingHost).multilineTextAlignment(.trailing).keyboardType(.numbersAndPunctuation).autocorrectionDisabled().autocapitalization(.none).frame(width:160).onChange(of:pendingHost){_,_ in connStatus = .untested}
|
|
}
|
|
HStack {
|
|
Label("Port", systemImage:"antenna.radiowaves.left.and.right"); Spacer()
|
|
TextField("8080", text:$pendingPort).multilineTextAlignment(.trailing).keyboardType(.numberPad).frame(width:80)
|
|
}
|
|
HStack { Text("Status").foregroundStyle(.secondary); Spacer(); Text(connStatus.label).foregroundStyle(connStatus.color).font(.subheadline) }
|
|
Button("Test Connection") { Task { await testConnection() } }.disabled(pendingHost.isEmpty)
|
|
Button("Save & Apply") { applyConnection() }.buttonStyle(.borderedProminent)
|
|
.disabled(pendingHost.isEmpty || connStatus != .success)
|
|
} header: { Text("Mac Daemon") }
|
|
|
|
// ── Projects ──────────────────────────────────────────────
|
|
Section {
|
|
ForEach(projectStore.projects) { p in
|
|
VStack(alignment:.leading, spacing:2) {
|
|
Text(p.name).font(.body)
|
|
Text(p.rootPath).font(.caption).foregroundStyle(.secondary).lineLimit(1).truncationMode(.middle)
|
|
Text("Scheme: \(p.scheme) · WC: \(p.workingCopyRepoName.isEmpty ? "not set" : p.workingCopyRepoName)").font(.caption2).foregroundStyle(.tertiary)
|
|
}.padding(.vertical,2)
|
|
}.onDelete { projectStore.remove(atOffsets:$0) }
|
|
Button { showAddProject = true } label: { Label("Add Project", systemImage:"plus.circle") }
|
|
} header: { Text("Projects") } footer: { Text("Mac path should be the directory containing project.yml or .xcodeproj") }
|
|
|
|
// ── Editor ────────────────────────────────────────────────
|
|
Section("Editor Appearance") {
|
|
HStack {
|
|
Label("Font Size", systemImage:"textformat.size"); Spacer()
|
|
Stepper("\(Int(editorState.fontSize))pt", value:$editorState.fontSize, in:8...32, step:1).frame(width:160)
|
|
}
|
|
Picker(selection:$editorState.selectedTheme) {
|
|
ForEach(EditorThemeOption.allCases) { o in Text(o.displayName).tag(o) }
|
|
} label: { Label("Theme", systemImage:"paintbrush") }
|
|
Toggle(isOn:$editorState.showLineNumbers) { Label("Line Numbers", systemImage:"list.number") }
|
|
Toggle(isOn:$editorState.lineWrapping) { Label("Line Wrapping", systemImage:"text.word.spacing") }
|
|
HStack {
|
|
Label("Indent Width", systemImage:"arrow.right.to.line"); Spacer()
|
|
Picker("", selection:$editorState.indentWidth) {
|
|
Text("2 spaces").tag(2); Text("4 spaces").tag(4); Text("Tab").tag(0)
|
|
}.labelsHidden().pickerStyle(.menu)
|
|
}
|
|
}
|
|
|
|
// ── Build ─────────────────────────────────────────────────
|
|
Section("Build") {
|
|
HStack {
|
|
Label("Team ID", systemImage:"person.badge.key"); Spacer()
|
|
TextField("XXXXXXXXXX", text:$daemonConfig.developmentTeam).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:120)
|
|
}
|
|
Toggle(isOn:$daemonConfig.allowProvisioningUpdates) { Label("Allow Provisioning Updates", systemImage:"checkmark.seal") }
|
|
Toggle(isOn:$daemonConfig.buildInRelease) { Label("Build in Release", systemImage:"bolt") }
|
|
}
|
|
|
|
// ── Working Copy ──────────────────────────────────────────
|
|
Section {
|
|
HStack {
|
|
Label("API Key", systemImage:"key.fill"); Spacer()
|
|
SecureField("Paste from Working Copy", text:$wcApiKey).multilineTextAlignment(.trailing).autocorrectionDisabled().autocapitalization(.none).frame(width:200)
|
|
.onChange(of:wcApiKey){_,k in UserDefaults.standard.set(k, forKey:"workingCopyAPIKey")}
|
|
}
|
|
let installed = UIApplication.shared.canOpenURL(URL(string:"working-copy://")!)
|
|
Label(installed ? "Working Copy detected ✓" : "Working Copy not installed",
|
|
systemImage: installed ? "checkmark.circle.fill" : "exclamationmark.triangle")
|
|
.foregroundStyle(installed ? .green : .orange).font(.caption)
|
|
Text("For local git over Tailscale, clone in Working Copy using:\nssh://user@100.x.x.x/path/to/repo.git")
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
} header: { Text("Working Copy (Git)") }
|
|
|
|
// ── LSP ───────────────────────────────────────────────────
|
|
Section {
|
|
Toggle(isOn:$daemonConfig.lspEnabled) { Label("sourcekit-lsp Proxy", systemImage:"wand.and.stars") }
|
|
if daemonConfig.lspEnabled {
|
|
HStack {
|
|
Label("Status", systemImage:"circle.fill").foregroundStyle(daemonConfig.lspConnected ? Color.green : Color.red)
|
|
Spacer()
|
|
Text(daemonConfig.lspConnected ? "Running" : "Disconnected").foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
} header: { Text("Language Server (LSP)") }
|
|
|
|
// ── About ─────────────────────────────────────────────────
|
|
Section("About") {
|
|
LabeledContent("Version", value:"1.0.0-alpha")
|
|
LabeledContent("Editor Engine", value:"Runestone 0.5.1 / Tree-sitter")
|
|
LabeledContent("Protocol", value:"REST + WebSocket")
|
|
}
|
|
}
|
|
.navigationTitle("Settings").navigationBarTitleDisplayMode(.inline)
|
|
.onAppear { pendingHost = daemonConfig.host; pendingPort = String(daemonConfig.port)
|
|
wcApiKey = UserDefaults.standard.string(forKey:"workingCopyAPIKey") ?? "" }
|
|
.sheet(isPresented:$showAddProject) { AddProjectView(projectStore:projectStore) }
|
|
}
|
|
}
|
|
|
|
private func testConnection() async {
|
|
connStatus = .testing
|
|
let host = pendingHost.trimmingCharacters(in:.whitespaces)
|
|
let port = Int(pendingPort) ?? 8080
|
|
guard let url = URL(string:"http://\(host):\(port)/health") else { connStatus = .failure("Invalid URL"); return }
|
|
var req = URLRequest(url:url); req.timeoutInterval = 4
|
|
do {
|
|
let (_,resp) = try await URLSession.shared.data(for:req)
|
|
connStatus = (resp as? HTTPURLResponse)?.statusCode == 200 ? .success : .failure("Unexpected response")
|
|
} catch { connStatus = .failure(error.localizedDescription) }
|
|
}
|
|
|
|
private func applyConnection() {
|
|
daemonConfig.host = pendingHost.trimmingCharacters(in:.whitespaces)
|
|
daemonConfig.port = Int(pendingPort) ?? 8080
|
|
}
|
|
}
|
|
|
|
// MARK: - Add Project View
|
|
|
|
struct AddProjectView: View {
|
|
@ObservedObject var projectStore: ProjectStore
|
|
@Environment(\.dismiss) var dismiss
|
|
@State private var name = ""; @State private var path = ""
|
|
@State private var scheme = ""; @State private var wcRepo = ""
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("Project Details") {
|
|
TextField("Name (e.g. NavidromePlayer)", text:$name)
|
|
TextField("/Users/dallas/Dev/NavidromePlayer/NavidromePlayer", text:$path).autocorrectionDisabled().autocapitalization(.none).keyboardType(.URL)
|
|
TextField("Xcode Scheme", text:$scheme).autocorrectionDisabled().autocapitalization(.none)
|
|
}
|
|
Section("Working Copy") {
|
|
TextField("Repo name in Working Copy (e.g. NavidromePlayer)", text:$wcRepo).autocorrectionDisabled().autocapitalization(.none)
|
|
} footer: { Text("Must match the display name in Working Copy's repo list.") }
|
|
}
|
|
.navigationTitle("Add Project").navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement:.cancellationAction) { Button("Cancel") { dismiss() } }
|
|
ToolbarItem(placement:.confirmationAction) {
|
|
Button("Add") {
|
|
projectStore.add(SavedProject(name: name.isEmpty ? scheme : name,
|
|
rootPath: path, scheme: scheme,
|
|
workingCopyRepoName: wcRepo))
|
|
dismiss()
|
|
}.disabled(path.isEmpty || scheme.isEmpty)
|
|
}
|
|
}
|
|
}.presentationDetents([.medium])
|
|
}
|
|
}
|