-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Swift version of app with sandbox + entitlements and proper set…
…up window
- Loading branch information
0 parents
commit 9c89cc9
Showing
31 changed files
with
2,722 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
Packages | ||
.build | ||
xcuserdata | ||
*.xcodeproj | ||
DerivedData/ | ||
.DS_Store | ||
db.sqlite | ||
.swiftpm | ||
/Data/ | ||
.env | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import SwiftUI | ||
import AppKit | ||
import Combine | ||
|
||
//@main | ||
//struct IKEADeskControlApp: App { | ||
// | ||
// @StateObject private var appModel = AppModel.shared | ||
// | ||
// var body: some Scene { | ||
// MenuBarExtra("IKEA Desk Control", systemImage: "arrow.up.and.down") { | ||
// Button("Quit IKEA Desk Control") { | ||
// NSApp.terminate(nil) | ||
// } | ||
// .keyboardShortcut("Q", modifiers: .command) | ||
// } | ||
// } | ||
//} | ||
|
||
@main | ||
struct IKEADeskControlApp { | ||
static func main() { | ||
let app = NSApplication.shared | ||
|
||
let delegate = AppDelegate() | ||
app.delegate = delegate | ||
|
||
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) | ||
} | ||
} | ||
|
||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { | ||
|
||
// MARK: - Private Vars | ||
|
||
private var appModel: AppModel! | ||
|
||
private var statusItem: NSStatusItem! | ||
private var setupResetMenuItem: NSMenuItem! | ||
|
||
private var setupWindow: NSWindow? | ||
private var setupViewModel: SetupViewModel? | ||
|
||
// MARK: - NSApplicationDelegate | ||
|
||
func applicationDidFinishLaunching(_ notification: Notification) { | ||
appModel = AppModel.shared | ||
|
||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) | ||
if let button = statusItem.button { | ||
button.image = NSImage( | ||
systemSymbolName: "arrow.up.and.down", | ||
accessibilityDescription: "IKEA Desk Control" | ||
) | ||
} | ||
|
||
setupMenu() | ||
|
||
updateSetupResetMenuItem() | ||
} | ||
|
||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { | ||
return false | ||
} | ||
|
||
// MARK: - NSWindowDelegate | ||
|
||
func windowShouldClose(_ sender: NSWindow) -> Bool { | ||
setupWindow = nil | ||
setupViewModel = nil | ||
return true | ||
} | ||
|
||
// MARK: - Setup | ||
|
||
private func setupMenu() { | ||
setupResetMenuItem = NSMenuItem( | ||
title: "", | ||
action: #selector(AppDelegate.toggle), | ||
keyEquivalent: "" | ||
) | ||
|
||
let statusMenu = NSMenu() | ||
statusMenu.addItem(setupResetMenuItem) | ||
statusMenu.addItem(NSMenuItem.separator()) | ||
statusMenu.addItem(NSMenuItem( | ||
title: "Quit IKEA Desk Control", | ||
action: #selector(NSApplication.terminate(_:)), | ||
keyEquivalent: "Q" | ||
)) | ||
statusItem.menu = statusMenu | ||
} | ||
|
||
// MARK: - Utils | ||
|
||
private func updateSetupResetMenuItem() { | ||
if appModel.isActive { | ||
setupResetMenuItem.title = "Stop" | ||
} else { | ||
setupResetMenuItem.title = "Setup..." | ||
} | ||
} | ||
|
||
@objc private func toggle() { | ||
if appModel.isActive { | ||
appModel.stop() | ||
updateSetupResetMenuItem() | ||
} else { | ||
showSetupWindow() | ||
} | ||
} | ||
|
||
private func showSetupWindow() { | ||
if let existingWindow = setupWindow { | ||
existingWindow.makeKeyAndOrderFront(nil) | ||
NSApp.activate(ignoringOtherApps: true) | ||
return | ||
} | ||
|
||
let setupWindow = NSWindow( | ||
contentRect: .zero, | ||
styleMask: [.titled, .closable, .fullSizeContentView], | ||
backing: .buffered, | ||
defer: false | ||
) | ||
setupWindow.title = "Setup IKEADeskControl" | ||
setupWindow.isReleasedWhenClosed = false | ||
|
||
let setupViewModel = SetupViewModel { [weak self] in | ||
self?.start(with: $0) | ||
} | ||
|
||
setupWindow.contentView = NSHostingView( | ||
rootView: SetupView(viewModel: setupViewModel) | ||
) | ||
setupWindow.delegate = self | ||
setupWindow.setFrameAutosaveName("com.roebert.IKEADeskControl.config") | ||
|
||
setupWindow.makeKeyAndOrderFront(nil) | ||
NSApp.activate(ignoringOtherApps: true) | ||
|
||
self.setupWindow = setupWindow | ||
self.setupViewModel = setupViewModel | ||
} | ||
|
||
private func start(with configuration: SetupViewModel.StartConfiguration) { | ||
setupWindow?.close() | ||
setupWindow = nil | ||
setupViewModel = nil | ||
|
||
appModel.start( | ||
mqttURL: configuration.mqttURL, | ||
mqttUsername: configuration.mqttUsername, | ||
mqttPassword: configuration.mqttPassword, | ||
mqttIdentifier: configuration.mqttIdentifier | ||
) | ||
updateSetupResetMenuItem() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import Foundation | ||
import SwiftUI | ||
import Logging | ||
import MQTTNIO | ||
|
||
final class AppModel: ObservableObject { | ||
|
||
// MARK: - Types | ||
|
||
private struct Configuration: Codable { | ||
var peripheralId: UUID? | ||
var mqttURL: URL | ||
var mqttUsername: String? | ||
var mqttPassword: String? | ||
var mqttIdentifier: String | ||
} | ||
|
||
// MARK: - Public Vars | ||
|
||
static let shared: AppModel = AppModel() | ||
|
||
var isActive: Bool { | ||
return activeTask != nil | ||
} | ||
|
||
// MARK: - Private Vars | ||
|
||
@KeychainItem("configuration") | ||
private var configuration: Configuration? | ||
|
||
private typealias ActiveTaskData = (DeskController, MQTTController) | ||
private var activeTask: Task<ActiveTaskData, Never>? | ||
|
||
// MARK: - Lifecycle | ||
|
||
private init() { | ||
Self.loggingBootstrap() | ||
|
||
if let configuration = configuration { | ||
activeTask = Task { | ||
await setup( | ||
peripheralId: configuration.peripheralId, | ||
mqttURL: configuration.mqttURL, | ||
mqttUsername: configuration.mqttUsername, | ||
mqttPassword: configuration.mqttPassword, | ||
mqttIdentifier: configuration.mqttIdentifier | ||
) | ||
} | ||
} | ||
} | ||
|
||
private static func loggingBootstrap() { | ||
LoggingSystem.bootstrap { label in | ||
var handler = StreamLogHandler.standardOutput(label: label) | ||
|
||
#if DEBUG | ||
handler.logLevel = .debug | ||
#else | ||
handler.logLevel = .info | ||
#endif | ||
|
||
return handler | ||
} | ||
} | ||
|
||
// MARK: - Start | ||
|
||
func start( | ||
mqttURL: URL, | ||
mqttUsername: String? = nil, | ||
mqttPassword: String? = nil, | ||
mqttIdentifier: String | ||
) { | ||
stop() | ||
|
||
configuration = .init( | ||
mqttURL: mqttURL, | ||
mqttUsername: mqttUsername, | ||
mqttPassword: mqttPassword, | ||
mqttIdentifier: mqttIdentifier | ||
) | ||
|
||
activeTask = Task { | ||
await setup( | ||
peripheralId: nil, | ||
mqttURL: mqttURL, | ||
mqttUsername: mqttUsername, | ||
mqttPassword: mqttPassword, | ||
mqttIdentifier: mqttIdentifier | ||
) | ||
} | ||
} | ||
|
||
func stop() { | ||
activeTask?.cancel() | ||
activeTask = nil | ||
configuration = nil | ||
} | ||
|
||
private func setup( | ||
peripheralId: UUID? = nil, | ||
mqttURL: URL, | ||
mqttUsername: String? = nil, | ||
mqttPassword: String? = nil, | ||
mqttIdentifier: String | ||
) async -> ActiveTaskData { | ||
let deskController = await DeskController( | ||
peripheralId: peripheralId | ||
) | ||
|
||
let credentials: MQTTConfiguration.Credentials? | ||
if let username = mqttUsername, let password = mqttPassword { | ||
credentials = .init(username: username, password: password) | ||
} else { | ||
credentials = nil | ||
} | ||
|
||
let mqttController = await MQTTController( | ||
identifier: mqttIdentifier, | ||
url: mqttURL, | ||
credentials: credentials | ||
) | ||
|
||
await deskController.onConnected { [weak self, weak mqttController] deskState in | ||
self?.configuration?.peripheralId = deskState.peripheralId | ||
|
||
await mqttController?.deskDidConnect(deskState: deskState) | ||
} | ||
|
||
await deskController.onDisconnected { [weak mqttController] in | ||
await mqttController?.deskDidDisconnect() | ||
} | ||
|
||
await deskController.onDeskState { [weak mqttController] in | ||
await mqttController?.didReceiveDeskState($0) | ||
} | ||
|
||
await mqttController.onCommand { [weak deskController] command in | ||
guard let deskController = deskController else { | ||
return | ||
} | ||
|
||
switch command { | ||
case .stop: | ||
try? await deskController.stop() | ||
|
||
case .moveTo(let position): | ||
try? await deskController.move(toPosition: position) | ||
|
||
case .open: | ||
try? await deskController.move(toPosition: DeskController.maximumDeskPosition) | ||
|
||
case .close: | ||
try? await deskController.move(toPosition: DeskController.minimumDeskPosition) | ||
} | ||
} | ||
|
||
await mqttController.start() | ||
await deskController.start() | ||
|
||
return (deskController, mqttController) | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
IKEADeskControl/Assets.xcassets/AccentColor.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
Oops, something went wrong.