feat: app wiring — ContentView + AppState + MainTerminalView
- AppState: loads credential from Keychain on launch, persists on pair - ContentView: switches PairingFlowView ↔ MainTerminalView on credential - PairingFlowView: onDismiss → onSuccess(credential) callback - MainTerminalView: auto-resolves/creates session, connects WebSocket stream to TerminalViewController, ModifierBar wired to SessionConnection.send() - piRemoteApp: AppState injected as environmentObject
This commit is contained in:
parent
333797ea36
commit
480a06981c
|
|
@ -0,0 +1,25 @@
|
||||||
|
// AppState.swift — global app state, credential lifecycle
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AppState: ObservableObject {
|
||||||
|
static let shared = AppState()
|
||||||
|
|
||||||
|
@Published var credential: SidecarCredential? = nil
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
// Try loading persisted credential on launch
|
||||||
|
credential = try? Keychain.shared.load(key: "piremote.credential")
|
||||||
|
}
|
||||||
|
|
||||||
|
func didPair(credential: SidecarCredential) {
|
||||||
|
self.credential = credential
|
||||||
|
try? Keychain.shared.save(credential, key: "piremote.credential")
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpair() {
|
||||||
|
credential = nil
|
||||||
|
Keychain.shared.delete(key: "piremote.credential")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
|
// ContentView.swift — root view, switches on pairing state
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
@EnvironmentObject var appState: AppState
|
||||||
VStack(spacing: 16) {
|
|
||||||
Image(systemName: "terminal")
|
|
||||||
.imageScale(.large)
|
|
||||||
.font(.system(size: 60))
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
Text("pi remote")
|
|
||||||
.font(.largeTitle.monospaced())
|
|
||||||
Text("Phase 2 — in development")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
var body: some View {
|
||||||
ContentView()
|
Group {
|
||||||
|
if let credential = appState.credential {
|
||||||
|
MainTerminalView(credential: credential)
|
||||||
|
} else {
|
||||||
|
PairingFlowView { credential in
|
||||||
|
appState.didPair(credential: credential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct piRemoteApp: App {
|
struct piRemoteApp: App {
|
||||||
|
@StateObject private var appState = AppState.shared
|
||||||
@StateObject private var notificationDelegate = NotificationDelegate.shared
|
@StateObject private var notificationDelegate = NotificationDelegate.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environmentObject(appState)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
notificationDelegate.setup()
|
notificationDelegate.setup()
|
||||||
UIApplication.shared.registerForRemoteNotifications()
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ private struct QRPayload: Sendable {
|
||||||
|
|
||||||
struct PairingFlowView: View {
|
struct PairingFlowView: View {
|
||||||
|
|
||||||
/// Called after the credential is saved to Keychain.
|
/// Called with the new credential after pairing succeeds.
|
||||||
var onDismiss: (() -> Void)?
|
var onSuccess: ((SidecarCredential) -> Void)?
|
||||||
|
|
||||||
@State private var state: PairingState = .idle
|
@State private var state: PairingState = .idle
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ struct PairingFlowView: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
onDismiss?()
|
onSuccess?(credential)
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
// MainTerminalView.swift
|
||||||
|
// Wires SessionConnection → TerminalViewController + ModifierBar
|
||||||
|
// MVP: auto-picks the first session (or creates one named "pi")
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct MainTerminalView: View {
|
||||||
|
let credential: SidecarCredential
|
||||||
|
|
||||||
|
@State private var terminalVC = TerminalViewController()
|
||||||
|
@State private var connection: SessionConnection? = nil
|
||||||
|
@State private var statusText = "Connecting…"
|
||||||
|
@State private var cancellables = Set<AnyCancellable>()
|
||||||
|
@EnvironmentObject var appState: AppState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// ── Status bar ──────────────────────────────────────────
|
||||||
|
HStack {
|
||||||
|
Circle()
|
||||||
|
.fill(connection != nil ? Color.green : Color.orange)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
Text(statusText)
|
||||||
|
.font(.caption.monospaced())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Button("Unpair") {
|
||||||
|
appState.unpair()
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color(uiColor: .systemBackground))
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Terminal ────────────────────────────────────────────
|
||||||
|
TerminalViewRepresentable(controller: terminalVC)
|
||||||
|
.ignoresSafeArea(edges: .bottom)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// ── Modifier bar ────────────────────────────────────────
|
||||||
|
ModifierBar { frame in
|
||||||
|
Task {
|
||||||
|
try? await connection?.send(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color(uiColor: .secondarySystemBackground))
|
||||||
|
}
|
||||||
|
.task { await bootstrap() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Bootstrap
|
||||||
|
|
||||||
|
private func bootstrap() async {
|
||||||
|
statusText = "Looking for sessions…"
|
||||||
|
do {
|
||||||
|
let sessionId = try await resolveSession()
|
||||||
|
statusText = "Connecting to \(sessionId)…"
|
||||||
|
let conn = SessionConnection(id: sessionId, credential: credential)
|
||||||
|
|
||||||
|
// Wire stream → terminal
|
||||||
|
conn.stream
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [terminalVC] data in
|
||||||
|
terminalVC.feed(data: data)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// Wire connection state → status text
|
||||||
|
conn.$connectionState
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { state in
|
||||||
|
switch state {
|
||||||
|
case .connected: statusText = "● \(sessionId)"
|
||||||
|
case .connecting: statusText = "Connecting…"
|
||||||
|
case .disconnected: statusText = "Disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
connection = conn
|
||||||
|
await conn.resume(from: conn.scrollback.sizeBytes > 0
|
||||||
|
? ResumeCursor().lastSeq(for: sessionId)
|
||||||
|
: nil)
|
||||||
|
} catch {
|
||||||
|
statusText = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Session resolution
|
||||||
|
|
||||||
|
/// Returns the first existing session id, or creates one named "pi".
|
||||||
|
private func resolveSession() async throws -> String {
|
||||||
|
let url = URL(string: "http://\(credential.host):\(credential.port)/sessions")!
|
||||||
|
var req = URLRequest(url: url)
|
||||||
|
req.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization")
|
||||||
|
|
||||||
|
let (data, _) = try await URLSession.shared.data(for: req)
|
||||||
|
|
||||||
|
struct SessionItem: Decodable { let id: String }
|
||||||
|
let sessions = (try? JSONDecoder().decode([SessionItem].self, from: data)) ?? []
|
||||||
|
|
||||||
|
if let first = sessions.first {
|
||||||
|
return first.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// No sessions — create one
|
||||||
|
var createReq = URLRequest(url: url)
|
||||||
|
createReq.httpMethod = "POST"
|
||||||
|
createReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
createReq.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization")
|
||||||
|
createReq.httpBody = try? JSONEncoder().encode(["name": "pi"])
|
||||||
|
|
||||||
|
let (createData, _) = try await URLSession.shared.data(for: createReq)
|
||||||
|
struct Created: Decodable { let id: String }
|
||||||
|
if let created = try? JSONDecoder().decode(Created.self, from: createData) {
|
||||||
|
return created.id
|
||||||
|
}
|
||||||
|
|
||||||
|
throw URLError(.cannotParseResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */; };
|
05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */; };
|
||||||
09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */; };
|
09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */; };
|
||||||
16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */; };
|
16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */; };
|
||||||
|
19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */; };
|
||||||
1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */; };
|
1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */; };
|
||||||
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B6B497329B98A4508D963B /* FrameCodec.swift */; };
|
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B6B497329B98A4508D963B /* FrameCodec.swift */; };
|
||||||
2D8C05476A83F5CAB9A55A11 /* SidecarCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */; };
|
2D8C05476A83F5CAB9A55A11 /* SidecarCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */; };
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
56096DB64F700FC00C4D58CE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFF032BC30D513204211ADA5 /* Assets.xcassets */; };
|
56096DB64F700FC00C4D58CE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFF032BC30D513204211ADA5 /* Assets.xcassets */; };
|
||||||
5F82D50C477F47893FADA8CB /* PasteSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F776605A4109C047E44A89 /* PasteSheet.swift */; };
|
5F82D50C477F47893FADA8CB /* PasteSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F776605A4109C047E44A89 /* PasteSheet.swift */; };
|
||||||
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FA0A0FD737901834AD5705 /* ThemeTests.swift */; };
|
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FA0A0FD737901834AD5705 /* ThemeTests.swift */; };
|
||||||
|
734F2FECD358816F695D26CD /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DD6C03615573E339057EBF /* AppState.swift */; };
|
||||||
7936EDE3DC79D02CF66F8863 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */; };
|
7936EDE3DC79D02CF66F8863 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */; };
|
||||||
7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */; };
|
7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */; };
|
||||||
873DC9D5342E8F4AF2C5BEE9 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = CCBD990EEA7AD9DCF714DF97 /* Starscream */; };
|
873DC9D5342E8F4AF2C5BEE9 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = CCBD990EEA7AD9DCF714DF97 /* Starscream */; };
|
||||||
|
|
@ -68,8 +70,10 @@
|
||||||
278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierStateTests.swift; sourceTree = "<group>"; };
|
278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierStateTests.swift; sourceTree = "<group>"; };
|
||||||
2E2370A3190FDC144C822FF6 /* piRemote.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = piRemote.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
2E2370A3190FDC144C822FF6 /* piRemote.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = piRemote.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewRepresentable.swift; sourceTree = "<group>"; };
|
39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewRepresentable.swift; sourceTree = "<group>"; };
|
||||||
|
3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTerminalView.swift; sourceTree = "<group>"; };
|
||||||
5205F823929F91450C58D4CA /* WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = "<group>"; };
|
5205F823929F91450C58D4CA /* WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = "<group>"; };
|
||||||
55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REVIEW_NOTES_2.md; sourceTree = "<group>"; };
|
55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REVIEW_NOTES_2.md; sourceTree = "<group>"; };
|
||||||
|
62DD6C03615573E339057EBF /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
|
||||||
658CB2FCA96A8913B1753B1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
658CB2FCA96A8913B1753B1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConnection.swift; sourceTree = "<group>"; };
|
67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConnection.swift; sourceTree = "<group>"; };
|
||||||
6BDDFB0C0D1D6D6FB490BA8D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
6BDDFB0C0D1D6D6FB490BA8D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
|
@ -144,6 +148,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
12767F24EC6ECFA77B280A8D /* FontStore.swift */,
|
12767F24EC6ECFA77B280A8D /* FontStore.swift */,
|
||||||
|
3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */,
|
||||||
C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */,
|
C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */,
|
||||||
E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */,
|
E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */,
|
||||||
A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */,
|
A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */,
|
||||||
|
|
@ -264,6 +269,7 @@
|
||||||
ECEA8716C9698DDD14367AC9 /* App */ = {
|
ECEA8716C9698DDD14367AC9 /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
62DD6C03615573E339057EBF /* AppState.swift */,
|
||||||
AFF032BC30D513204211ADA5 /* Assets.xcassets */,
|
AFF032BC30D513204211ADA5 /* Assets.xcassets */,
|
||||||
188683139B863ED1AC03A1BB /* ContentView.swift */,
|
188683139B863ED1AC03A1BB /* ContentView.swift */,
|
||||||
658CB2FCA96A8913B1753B1C /* Info.plist */,
|
658CB2FCA96A8913B1753B1C /* Info.plist */,
|
||||||
|
|
@ -396,11 +402,13 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
734F2FECD358816F695D26CD /* AppState.swift in Sources */,
|
||||||
F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */,
|
F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */,
|
||||||
909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */,
|
909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */,
|
||||||
B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */,
|
B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */,
|
||||||
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */,
|
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */,
|
||||||
C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */,
|
C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */,
|
||||||
|
19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */,
|
||||||
E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */,
|
E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */,
|
||||||
B8800C5E81FBB0C3CE9C6E7D /* ModifierState.swift in Sources */,
|
B8800C5E81FBB0C3CE9C6E7D /* ModifierState.swift in Sources */,
|
||||||
9855E1E1C856E20B339F2A0A /* NotificationDelegate.swift in Sources */,
|
9855E1E1C856E20B339F2A0A /* NotificationDelegate.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue