poke-gate 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +124 -0
- package/README.md +98 -37
- package/assets/logo.png +0 -0
- package/build.sh +51 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_128x128.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_16x16.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_256x256.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_32x32.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_512x512.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Assets.xcassets/Contents.json +6 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/ContentView.swift +8 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +241 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Info.plist +8 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Item.swift +2 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +47 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +140 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +50 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +336 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.xcworkspace/xcuserdata/fka.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/xcuserdata/fka.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/package.json +1 -1
- package/src/mcp-server.js +73 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Combine
|
|
3
|
+
import ScreenCaptureKit
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
class GateService: ObservableObject {
|
|
7
|
+
enum Status: String {
|
|
8
|
+
case stopped = "Stopped"
|
|
9
|
+
case starting = "Starting…"
|
|
10
|
+
case connected = "Connected"
|
|
11
|
+
case disconnected = "Disconnected"
|
|
12
|
+
case error = "Error"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Published var status: Status = .stopped
|
|
16
|
+
@Published var logs: [String] = []
|
|
17
|
+
@Published var userName: String? = nil
|
|
18
|
+
|
|
19
|
+
private var hasAutoStarted = false
|
|
20
|
+
private var process: Process?
|
|
21
|
+
private var outputPipe: Pipe?
|
|
22
|
+
private var shouldRestart = true
|
|
23
|
+
private let maxLogs = 200
|
|
24
|
+
|
|
25
|
+
var apiKey: String {
|
|
26
|
+
get { loadAPIKey() ?? "" }
|
|
27
|
+
set { saveAPIKey(newValue) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var hasAPIKey: Bool {
|
|
31
|
+
let key = loadAPIKey()
|
|
32
|
+
return key != nil && !key!.isEmpty
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func autoStartIfNeeded() {
|
|
36
|
+
guard !hasAutoStarted else { return }
|
|
37
|
+
hasAutoStarted = true
|
|
38
|
+
requestScreenCapturePermission()
|
|
39
|
+
if hasAPIKey {
|
|
40
|
+
start()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func requestScreenCapturePermission() {
|
|
45
|
+
Task {
|
|
46
|
+
do {
|
|
47
|
+
_ = try await SCShareableContent.current
|
|
48
|
+
} catch {
|
|
49
|
+
appendLog("Screen capture permission not granted yet.")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func start() {
|
|
55
|
+
guard hasAPIKey else {
|
|
56
|
+
status = .error
|
|
57
|
+
appendLog("No API key configured.")
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
shouldRestart = true
|
|
61
|
+
fetchUserName()
|
|
62
|
+
launchProcess()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func fetchUserName() {
|
|
66
|
+
guard let key = loadAPIKey() else { return }
|
|
67
|
+
Task {
|
|
68
|
+
let url = URL(string: "https://poke.com/api/v1/user/profile")!
|
|
69
|
+
var request = URLRequest(url: url)
|
|
70
|
+
request.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
|
|
71
|
+
do {
|
|
72
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
73
|
+
guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else { return }
|
|
74
|
+
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
75
|
+
let fullName = json["name"] as? String ?? json["email"] as? String {
|
|
76
|
+
let firstName = fullName.components(separatedBy: CharacterSet.whitespaces.union(CharacterSet(charactersIn: "@"))).first ?? fullName
|
|
77
|
+
self.userName = firstName
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func stop() {
|
|
84
|
+
shouldRestart = false
|
|
85
|
+
killProcess()
|
|
86
|
+
status = .stopped
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func restart() {
|
|
90
|
+
stop()
|
|
91
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
92
|
+
self.start()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private func shellPath() -> String {
|
|
97
|
+
let loginShell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/zsh"
|
|
98
|
+
let pathProc = Process()
|
|
99
|
+
let pathPipe = Pipe()
|
|
100
|
+
pathProc.executableURL = URL(fileURLWithPath: loginShell)
|
|
101
|
+
pathProc.arguments = ["-ilc", "echo $PATH"]
|
|
102
|
+
pathProc.standardOutput = pathPipe
|
|
103
|
+
pathProc.standardError = FileHandle.nullDevice
|
|
104
|
+
pathProc.environment = ["HOME": NSHomeDirectory()]
|
|
105
|
+
try? pathProc.run()
|
|
106
|
+
pathProc.waitUntilExit()
|
|
107
|
+
let data = pathPipe.fileHandleForReading.readDataToEndOfFile()
|
|
108
|
+
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private func launchProcess() {
|
|
112
|
+
killProcess()
|
|
113
|
+
|
|
114
|
+
status = .starting
|
|
115
|
+
appendLog("Starting poke-gate…")
|
|
116
|
+
|
|
117
|
+
let fullPath = shellPath()
|
|
118
|
+
|
|
119
|
+
let proc = Process()
|
|
120
|
+
let pipe = Pipe()
|
|
121
|
+
|
|
122
|
+
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
123
|
+
proc.arguments = ["-c", "npx -y poke-gate --verbose"]
|
|
124
|
+
proc.environment = ProcessInfo.processInfo.environment.merging(
|
|
125
|
+
[
|
|
126
|
+
"POKE_API_KEY": loadAPIKey() ?? "",
|
|
127
|
+
"PATH": fullPath,
|
|
128
|
+
],
|
|
129
|
+
uniquingKeysWith: { _, new in new }
|
|
130
|
+
)
|
|
131
|
+
proc.standardOutput = pipe
|
|
132
|
+
proc.standardError = pipe
|
|
133
|
+
proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
|
|
134
|
+
|
|
135
|
+
let handle = pipe.fileHandleForReading
|
|
136
|
+
handle.readabilityHandler = { [weak self] fh in
|
|
137
|
+
let data = fh.availableData
|
|
138
|
+
guard !data.isEmpty, let line = String(data: data, encoding: .utf8) else { return }
|
|
139
|
+
DispatchQueue.main.async {
|
|
140
|
+
self?.handleOutput(line)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
proc.terminationHandler = { [weak self] proc in
|
|
145
|
+
DispatchQueue.main.async {
|
|
146
|
+
self?.handleTermination(exitCode: proc.terminationStatus)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
do {
|
|
151
|
+
try proc.run()
|
|
152
|
+
self.process = proc
|
|
153
|
+
self.outputPipe = pipe
|
|
154
|
+
} catch {
|
|
155
|
+
status = .error
|
|
156
|
+
appendLog("Failed to start: \(error.localizedDescription)")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func killProcess() {
|
|
161
|
+
if let proc = process, proc.isRunning {
|
|
162
|
+
proc.terminate()
|
|
163
|
+
}
|
|
164
|
+
outputPipe?.fileHandleForReading.readabilityHandler = nil
|
|
165
|
+
process = nil
|
|
166
|
+
outputPipe = nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func handleOutput(_ raw: String) {
|
|
170
|
+
for line in raw.components(separatedBy: .newlines) where !line.isEmpty {
|
|
171
|
+
appendLog(line)
|
|
172
|
+
|
|
173
|
+
if line.contains("Tunnel connected") || line.contains("Ready") {
|
|
174
|
+
status = .connected
|
|
175
|
+
} else if line.contains("Tunnel disconnected") || line.contains("Reconnecting") {
|
|
176
|
+
status = .disconnected
|
|
177
|
+
} else if line.contains("Failed to connect") || line.contains("error") {
|
|
178
|
+
if status != .connected {
|
|
179
|
+
status = .error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func handleTermination(exitCode: Int32) {
|
|
186
|
+
appendLog("Process exited with code \(exitCode)")
|
|
187
|
+
if shouldRestart {
|
|
188
|
+
status = .disconnected
|
|
189
|
+
appendLog("Restarting in 2 seconds…")
|
|
190
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
191
|
+
if self.shouldRestart {
|
|
192
|
+
self.launchProcess()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
status = .stopped
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func appendLog(_ line: String) {
|
|
201
|
+
let ts = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
|
|
202
|
+
logs.append("[\(ts)] \(line)")
|
|
203
|
+
if logs.count > maxLogs {
|
|
204
|
+
logs.removeFirst(logs.count - maxLogs)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// MARK: - Config
|
|
209
|
+
|
|
210
|
+
private var configURL: URL {
|
|
211
|
+
let configDir: URL
|
|
212
|
+
if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"] {
|
|
213
|
+
configDir = URL(fileURLWithPath: xdg)
|
|
214
|
+
} else {
|
|
215
|
+
configDir = FileManager.default.homeDirectoryForCurrentUser
|
|
216
|
+
.appendingPathComponent(".config")
|
|
217
|
+
}
|
|
218
|
+
return configDir
|
|
219
|
+
.appendingPathComponent("poke-gate")
|
|
220
|
+
.appendingPathComponent("config.json")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private func loadAPIKey() -> String? {
|
|
224
|
+
guard let data = try? Data(contentsOf: configURL),
|
|
225
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
226
|
+
let key = json["apiKey"] as? String else {
|
|
227
|
+
return nil
|
|
228
|
+
}
|
|
229
|
+
return key
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private func saveAPIKey(_ key: String) {
|
|
233
|
+
let dir = configURL.deletingLastPathComponent()
|
|
234
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
235
|
+
let json: [String: Any] = ["apiKey": key]
|
|
236
|
+
if let data = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted]) {
|
|
237
|
+
try? data.write(to: configURL)
|
|
238
|
+
}
|
|
239
|
+
objectWillChange.send()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct LogsView: View {
|
|
4
|
+
@ObservedObject var service: GateService
|
|
5
|
+
|
|
6
|
+
var body: some View {
|
|
7
|
+
VStack(spacing: 0) {
|
|
8
|
+
HStack {
|
|
9
|
+
Text("Logs")
|
|
10
|
+
.font(.headline)
|
|
11
|
+
Spacer()
|
|
12
|
+
Button("Clear") {
|
|
13
|
+
service.logs.removeAll()
|
|
14
|
+
}
|
|
15
|
+
.buttonStyle(.plain)
|
|
16
|
+
.foregroundStyle(.secondary)
|
|
17
|
+
.font(.caption)
|
|
18
|
+
}
|
|
19
|
+
.padding(.horizontal, 12)
|
|
20
|
+
.padding(.vertical, 8)
|
|
21
|
+
|
|
22
|
+
Divider()
|
|
23
|
+
|
|
24
|
+
ScrollViewReader { proxy in
|
|
25
|
+
ScrollView {
|
|
26
|
+
LazyVStack(alignment: .leading, spacing: 2) {
|
|
27
|
+
ForEach(Array(service.logs.enumerated()), id: \.offset) { index, line in
|
|
28
|
+
Text(line)
|
|
29
|
+
.font(.system(.caption, design: .monospaced))
|
|
30
|
+
.foregroundStyle(.primary)
|
|
31
|
+
.textSelection(.enabled)
|
|
32
|
+
.id(index)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
.padding(.horizontal, 12)
|
|
36
|
+
.padding(.vertical, 8)
|
|
37
|
+
}
|
|
38
|
+
.onChange(of: service.logs.count) { _, _ in
|
|
39
|
+
if let last = service.logs.indices.last {
|
|
40
|
+
proxy.scrollTo(last, anchor: .bottom)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
.frame(width: 480, height: 320)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
@main
|
|
4
|
+
struct Poke_macOS_GateApp: App {
|
|
5
|
+
@StateObject private var service = GateService()
|
|
6
|
+
|
|
7
|
+
var body: some Scene {
|
|
8
|
+
MenuBarExtra {
|
|
9
|
+
MenuBarContent(service: service)
|
|
10
|
+
.onAppear { service.autoStartIfNeeded() }
|
|
11
|
+
} label: {
|
|
12
|
+
Image(systemName: menuBarIcon)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Window("Logs", id: "logs") {
|
|
16
|
+
LogsView(service: service)
|
|
17
|
+
}
|
|
18
|
+
.defaultSize(width: 480, height: 320)
|
|
19
|
+
|
|
20
|
+
Window("Settings", id: "settings") {
|
|
21
|
+
SettingsView(service: service)
|
|
22
|
+
}
|
|
23
|
+
.windowResizability(.contentSize)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private var menuBarIcon: String {
|
|
27
|
+
switch service.status {
|
|
28
|
+
case .connected: "door.left.hand.open"
|
|
29
|
+
case .starting, .disconnected: "door.left.hand.closed"
|
|
30
|
+
case .error: "exclamationmark.triangle"
|
|
31
|
+
case .stopped: "door.left.hand.closed"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct MenuBarContent: View {
|
|
37
|
+
@ObservedObject var service: GateService
|
|
38
|
+
@Environment(\.openWindow) private var openWindow
|
|
39
|
+
|
|
40
|
+
var body: some View {
|
|
41
|
+
Label(statusText, systemImage: statusIcon)
|
|
42
|
+
.foregroundStyle(statusColor)
|
|
43
|
+
|
|
44
|
+
if service.status == .connected {
|
|
45
|
+
Text("This machine is now accessible via Poke.")
|
|
46
|
+
.font(.caption2)
|
|
47
|
+
.foregroundStyle(.secondary)
|
|
48
|
+
Text("Ask your Poke to run commands or read files.")
|
|
49
|
+
.font(.caption2)
|
|
50
|
+
.foregroundStyle(.secondary)
|
|
51
|
+
} else if service.status == .starting {
|
|
52
|
+
Text("Establishing connection…")
|
|
53
|
+
.font(.caption2)
|
|
54
|
+
.foregroundStyle(.secondary)
|
|
55
|
+
} else if service.status == .error {
|
|
56
|
+
Text("Check Settings or view Logs for details.")
|
|
57
|
+
.font(.caption2)
|
|
58
|
+
.foregroundStyle(.secondary)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Divider()
|
|
62
|
+
|
|
63
|
+
Button("View Logs…") {
|
|
64
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
65
|
+
openWindow(id: "logs")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Button("Settings…") {
|
|
69
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
70
|
+
openWindow(id: "settings")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Divider()
|
|
74
|
+
|
|
75
|
+
if service.status == .connected || service.status == .starting || service.status == .disconnected {
|
|
76
|
+
Button("Restart") {
|
|
77
|
+
service.restart()
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
Button("Start") {
|
|
81
|
+
service.start()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Divider()
|
|
86
|
+
|
|
87
|
+
Button("Quit Poke Gate") {
|
|
88
|
+
service.stop()
|
|
89
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
90
|
+
NSApp.terminate(nil)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
.keyboardShortcut("q")
|
|
94
|
+
|
|
95
|
+
Divider()
|
|
96
|
+
|
|
97
|
+
Text("Poke Gate v0.0.3")
|
|
98
|
+
.font(.caption)
|
|
99
|
+
.foregroundStyle(.secondary)
|
|
100
|
+
Text("Community project — not affiliated with Poke")
|
|
101
|
+
.font(.caption2)
|
|
102
|
+
.foregroundStyle(.secondary)
|
|
103
|
+
Button("GitHub") {
|
|
104
|
+
NSWorkspace.shared.open(URL(string: "https://github.com/f/poke-gate")!)
|
|
105
|
+
}
|
|
106
|
+
.font(.caption)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private var statusText: String {
|
|
110
|
+
switch service.status {
|
|
111
|
+
case .connected:
|
|
112
|
+
if let name = service.userName {
|
|
113
|
+
return "Connected to your Poke, \(name)"
|
|
114
|
+
}
|
|
115
|
+
return "Connected to your Poke"
|
|
116
|
+
case .starting: return "Connecting…"
|
|
117
|
+
case .disconnected: return "Reconnecting…"
|
|
118
|
+
case .error: return "Connection error"
|
|
119
|
+
case .stopped: return "Stopped"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private var statusIcon: String {
|
|
124
|
+
switch service.status {
|
|
125
|
+
case .connected: "circle.fill"
|
|
126
|
+
case .starting, .disconnected: "circle.dotted"
|
|
127
|
+
case .error: "exclamationmark.circle.fill"
|
|
128
|
+
case .stopped: "circle"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private var statusColor: Color {
|
|
133
|
+
switch service.status {
|
|
134
|
+
case .connected: .green
|
|
135
|
+
case .starting, .disconnected: .yellow
|
|
136
|
+
case .error: .red
|
|
137
|
+
case .stopped: .secondary
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
struct SettingsView: View {
|
|
4
|
+
@ObservedObject var service: GateService
|
|
5
|
+
@State private var apiKeyInput: String = ""
|
|
6
|
+
@Environment(\.dismiss) private var dismiss
|
|
7
|
+
|
|
8
|
+
var body: some View {
|
|
9
|
+
VStack(spacing: 16) {
|
|
10
|
+
Text("Poke Gate Settings")
|
|
11
|
+
.font(.headline)
|
|
12
|
+
|
|
13
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
14
|
+
Text("API Key")
|
|
15
|
+
.font(.subheadline)
|
|
16
|
+
.foregroundStyle(.secondary)
|
|
17
|
+
|
|
18
|
+
SecureField("Paste your API key", text: $apiKeyInput)
|
|
19
|
+
.textFieldStyle(.roundedBorder)
|
|
20
|
+
|
|
21
|
+
Link("Get your key at poke.com/kitchen/api-keys",
|
|
22
|
+
destination: URL(string: "https://poke.com/kitchen/api-keys")!)
|
|
23
|
+
.font(.caption)
|
|
24
|
+
.foregroundStyle(.blue)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
HStack {
|
|
28
|
+
Button("Cancel") {
|
|
29
|
+
dismiss()
|
|
30
|
+
}
|
|
31
|
+
.keyboardShortcut(.cancelAction)
|
|
32
|
+
|
|
33
|
+
Spacer()
|
|
34
|
+
|
|
35
|
+
Button("Save") {
|
|
36
|
+
service.apiKey = apiKeyInput
|
|
37
|
+
dismiss()
|
|
38
|
+
service.restart()
|
|
39
|
+
}
|
|
40
|
+
.keyboardShortcut(.defaultAction)
|
|
41
|
+
.disabled(apiKeyInput.isEmpty)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.padding(20)
|
|
45
|
+
.frame(width: 360)
|
|
46
|
+
.onAppear {
|
|
47
|
+
apiKeyInput = service.apiKey
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|