From 22386a4ae18a875783b6575bc4cda2563b6ba105 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Thu, 8 Jul 2021 23:03:02 +0200 Subject: [PATCH] feat: initialized Bluetooth module (#277) --- Kit/Widgets/Battery.swift | 110 +++++++++++------- Kit/module/module.swift | 2 +- Modules/Bluetooth/Info.plist | 24 ++++ Modules/Bluetooth/config.plist | 49 ++++++++ Modules/Bluetooth/main.swift | 116 +++++++++++++++++++ Modules/Bluetooth/popup.swift | 111 ++++++++++++++++++ Modules/Bluetooth/readers.swift | 179 +++++++++++++++++++++++++++++ Modules/Bluetooth/settings.swift | 112 ++++++++++++++++++ Stats.xcodeproj/project.pbxproj | 187 ++++++++++++++++++++++++++++++- Stats/AppDelegate.swift | 4 +- 10 files changed, 848 insertions(+), 46 deletions(-) create mode 100644 Modules/Bluetooth/Info.plist create mode 100644 Modules/Bluetooth/config.plist create mode 100644 Modules/Bluetooth/main.swift create mode 100644 Modules/Bluetooth/popup.swift create mode 100644 Modules/Bluetooth/readers.swift create mode 100644 Modules/Bluetooth/settings.swift diff --git a/Kit/Widgets/Battery.swift b/Kit/Widgets/Battery.swift index d13b706b..7adc68dc 100644 --- a/Kit/Widgets/Battery.swift +++ b/Kit/Widgets/Battery.swift @@ -18,7 +18,7 @@ public class BatterykWidget: WidgetWrapper { private var colorState: Bool = false private var hideAdditionalWhenFull: Bool = true - private var percentage: Double = 1 + private var percentage: Double? = nil private var time: Int = 0 private var charging: Bool = false private var ACStatus: Bool = false @@ -68,10 +68,11 @@ public class BatterykWidget: WidgetWrapper { if !self.hideAdditionalWhenFull || (self.hideAdditionalWhenFull && self.percentage != 1) { switch self.additional { case "percentage": - let rowWidth = self.drawOneRow( - value: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%", - x: x - ).rounded(.up) + var value = "n/a" + if let percentage = self.percentage { + value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%" + } + let rowWidth = self.drawOneRow(value: value, x: x).rounded(.up) width += rowWidth + Constants.Widget.spacing x += rowWidth + Constants.Widget.spacing case "time": @@ -82,17 +83,25 @@ public class BatterykWidget: WidgetWrapper { width += rowWidth + Constants.Widget.spacing x += rowWidth + Constants.Widget.spacing case "percentageAndTime": + var value = "n/a" + if let percentage = self.percentage { + value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%" + } let rowWidth = self.drawTwoRows( - first: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%", + first: value, second: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat), x: x ).rounded(.up) width += rowWidth + Constants.Widget.spacing x += rowWidth + Constants.Widget.spacing case "timeAndPercentage": + var value = "n/a" + if let percentage = self.percentage { + value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%" + } let rowWidth = self.drawTwoRows( first: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat), - second: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%", + second: value, x: x ).rounded(.up) width += rowWidth + Constants.Widget.spacing @@ -133,17 +142,33 @@ public class BatterykWidget: WidgetWrapper { ctx.restoreGState() width += 2 // add battery point width - let maxWidth = batterySize.width - offset*2 - borderWidth*2 - 1 - let innerWidth: CGFloat = max(1, maxWidth * CGFloat(self.percentage)) - let innerOffset: CGFloat = -offset + borderWidth + 1 - let inner = NSBezierPath(roundedRect: NSRect( - x: batteryFrame.bounds.origin.x + innerOffset, - y: batteryFrame.bounds.origin.y + innerOffset, - width: innerWidth, - height: batterySize.height - offset*2 - borderWidth*2 - 1 - ), xRadius: 1, yRadius: 1) - self.percentage.batteryColor(color: self.colorState).set() - inner.fill() + if let percentage = self.percentage { + let maxWidth = batterySize.width - offset*2 - borderWidth*2 - 1 + let innerWidth: CGFloat = max(1, maxWidth * CGFloat(percentage)) + let innerOffset: CGFloat = -offset + borderWidth + 1 + let inner = NSBezierPath(roundedRect: NSRect( + x: batteryFrame.bounds.origin.x + innerOffset, + y: batteryFrame.bounds.origin.y + innerOffset, + width: innerWidth, + height: batterySize.height - offset*2 - borderWidth*2 - 1 + ), xRadius: 1, yRadius: 1) + percentage.batteryColor(color: self.colorState).set() + inner.fill() + } else { + let attributes = [ + NSAttributedString.Key.font: NSFont.systemFont(ofSize: 11, weight: .regular), + NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor, + NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle() + ] + + let batteryCenter: CGPoint = CGPoint( + x: batteryFrame.bounds.origin.x + (batteryFrame.bounds.width/2), + y: batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2) + ) + + let rect = CGRect(x: batteryCenter.x-2, y: batteryCenter.y-4, width: 8, height: 12) + NSAttributedString.init(string: "?", attributes: attributes).draw(with: rect) + } if self.ACStatus { let batteryCenter: CGPoint = CGPoint( @@ -267,7 +292,7 @@ public class BatterykWidget: WidgetWrapper { return rowWidth } - public func setValue(percentage: Double, ACStatus: Bool, isCharging: Bool, time: Int) { + public func setValue(percentage: Double? = nil, ACStatus: Bool? = nil, isCharging: Bool? = nil, time: Int? = nil) { var updated: Bool = false let timeFormat: String = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat) @@ -275,15 +300,15 @@ public class BatterykWidget: WidgetWrapper { self.percentage = percentage updated = true } - if self.ACStatus != ACStatus { - self.ACStatus = ACStatus + if let status = ACStatus, self.ACStatus != status { + self.ACStatus = status updated = true } - if self.charging != isCharging { - self.charging = isCharging + if let charging = isCharging, self.charging != charging { + self.charging = charging updated = true } - if self.time != time { + if let time = time, self.time != time { self.time = time updated = true } @@ -302,33 +327,32 @@ public class BatterykWidget: WidgetWrapper { // MARK: - Settings public override func settings(width: CGFloat) -> NSView { - let rowHeight: CGFloat = 30 - let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 3) + Constants.Settings.margin + let view = SettingsContainerView(width: width) - let view: NSView = NSView(frame: NSRect( - x: Constants.Settings.margin, - y: Constants.Settings.margin, - width: width - (Constants.Settings.margin*2), - height: height - )) + var additionalOptions = BatteryAdditionals + if self.title == "Bluetooth" { + additionalOptions = additionalOptions.filter({ $0.key == "none" || $0.key == "percentage" }) + } - view.addSubview(selectRow( - frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 2, width: view.frame.width, height: rowHeight), + view.addArrangedSubview(selectRow( + frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row), title: localizedString("Additional information"), action: #selector(toggleAdditional), - items: BatteryAdditionals, + items: additionalOptions, selected: self.additional )) - view.addSubview(toggleTitleRow( - frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 1, width: view.frame.width, height: rowHeight), - title: localizedString("Hide additional information when full"), - action: #selector(toggleHideAdditionalWhenFull), - state: self.hideAdditionalWhenFull - )) + if self.title != "Bluetooth" { + view.addArrangedSubview(toggleTitleRow( + frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row), + title: localizedString("Hide additional information when full"), + action: #selector(toggleHideAdditionalWhenFull), + state: self.hideAdditionalWhenFull + )) + } - view.addSubview(toggleTitleRow( - frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight), + view.addArrangedSubview(toggleTitleRow( + frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row), title: localizedString("Colorize"), action: #selector(toggleColor), state: self.colorState diff --git a/Kit/module/module.swift b/Kit/module/module.swift index ecb1bf7d..b0e7ae86 100644 --- a/Kit/module/module.swift +++ b/Kit/module/module.swift @@ -80,7 +80,7 @@ open class Module: Module_p { private let log: NextLog private var readers: [Reader_p] = [] - public init(popup: Popup_p?, settings: Settings_v?) { + public init(popup: Popup_p? = nil, settings: Settings_v? = nil) { self.config = module_c(in: Bundle(for: type(of: self)).path(forResource: "config", ofType: "plist")!) self.log = NextLog.shared.copy(category: self.config.name) diff --git a/Modules/Bluetooth/Info.plist b/Modules/Bluetooth/Info.plist new file mode 100644 index 00000000..85e3d697 --- /dev/null +++ b/Modules/Bluetooth/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. + + diff --git a/Modules/Bluetooth/config.plist b/Modules/Bluetooth/config.plist new file mode 100644 index 00000000..5750e36b --- /dev/null +++ b/Modules/Bluetooth/config.plist @@ -0,0 +1,49 @@ + + + + + Name + Bluetooth + State + + Widgets + + label + + Default + + Title + BLE + Order + 0 + + mini + + Title + BLE + Default + + Preview + + Title + BLE + Value + 0.98 + + Unsupported colors + + pressure + + Order + 1 + + battery + + Default + + Order + 2 + + + + diff --git a/Modules/Bluetooth/main.swift b/Modules/Bluetooth/main.swift new file mode 100644 index 00000000..503c6985 --- /dev/null +++ b/Modules/Bluetooth/main.swift @@ -0,0 +1,116 @@ +// +// main.swift +// Bluetooth +// +// Created by Serhiy Mytrovtsiy on 08/06/2021. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import Kit +import CoreBluetooth + +public enum BLEType: String { + case iPhone + case airPods + case unknown +} + +public struct BLEDevice { + let uuid: UUID + let name: String + let type: BLEType + + var RSSI: Int? + var batteryLevel: [KeyValue_t] + + var isConnected: Bool + var isPaired: Bool + var isInitialized: Bool + + var peripheral: CBPeripheral? +} + +public class Bluetooth: Module { + private var devicesReader: DevicesReader? = nil + private let popupView: Popup = Popup() + private let settingsView: Settings + + private var selectedBattery: String = "" + + public init() { + self.settingsView = Settings("Bluetooth") + + super.init( + popup: self.popupView, + settings: self.settingsView + ) + guard self.available else { return } + + self.devicesReader = DevicesReader() + self.selectedBattery = Store.shared.string(key: "\(self.config.name)_battery", defaultValue: self.selectedBattery) + + self.settingsView.selectedBatteryHandler = { [unowned self] value in + self.selectedBattery = value + } + + self.devicesReader?.callbackHandler = { [unowned self] value in + self.batteryCallback(value) + } + self.devicesReader?.readyCallback = { [unowned self] in + self.readyHandler() + } + + if let reader = self.devicesReader { + self.addReader(reader) + } + } + + private func batteryCallback(_ raw: [BLEDevice]?) { + guard let value = raw else { + return + } + + let active = value.filter{ $0.isPaired && ($0.isConnected || !$0.batteryLevel.isEmpty) } + DispatchQueue.main.async(execute: { + self.popupView.batteryCallback(active) + }) + self.settingsView.setList(active) + + var battery = active.first?.batteryLevel.first + if self.selectedBattery != "" { + let pair = self.selectedBattery.split(separator: "@") + + guard let device = value.first(where: { $0.name == pair.first! }) else { + error("cannot find selected battery: \(self.selectedBattery)") + return + } + + if pair.count == 1 { + battery = device.batteryLevel.first + } else if pair.count == 2 { + battery = device.batteryLevel.first{ $0.key == pair.last! } + } + } + + self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in + switch w.item { + case let widget as Mini: + guard let percentage = Double(battery?.value ?? "0") else { + return + } + widget.setValue(percentage/100) + case let widget as BatterykWidget: + var percentage: Double? = nil + if let value = battery?.value { + percentage = (Double(value) ?? 0) / 100 + } + widget.setValue(percentage: percentage) + default: break + } + } + } +} diff --git a/Modules/Bluetooth/popup.swift b/Modules/Bluetooth/popup.swift new file mode 100644 index 00000000..8b016ae7 --- /dev/null +++ b/Modules/Bluetooth/popup.swift @@ -0,0 +1,111 @@ +// +// popup.swift +// Bluetooth +// +// Created by Serhiy Mytrovtsiy on 22/06/2021. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +internal class Popup: NSStackView, Popup_p { + public var sizeCallback: ((NSSize) -> Void)? = nil + + private var list: [UUID: BLEView] = [:] + + public init() { + super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0)) + + self.orientation = .vertical + self.spacing = Constants.Popup.margins + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + internal func batteryCallback(_ list: [BLEDevice]) { + let views = self.subviews.filter{ $0 is BLEView }.map{ $0 as! BLEView } + + list.reversed().forEach { (ble: BLEDevice) in + if let view = views.first(where: { $0.uuid == ble.uuid }) { + view.update(ble.batteryLevel) + } else { + self.addArrangedSubview(BLEView( + width: self.frame.width, + uuid: ble.uuid, + name: ble.name, + batteryLevel: ble.batteryLevel + )) + } + } + + let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing + if h > 0 && self.frame.size.height != h { + self.setFrameSize(NSSize(width: self.frame.width, height: h)) + self.sizeCallback?(self.frame.size) + } + } +} + +internal class BLEView: NSStackView { + public var uuid: UUID + + open override var intrinsicContentSize: CGSize { + return CGSize(width: self.bounds.width, height: self.bounds.height) + } + + public init(width: CGFloat, uuid: UUID, name: String, batteryLevel: [KeyValue_t]) { + self.uuid = uuid + + super.init(frame: NSRect(x: 0, y: 0, width: width, height: 30)) + + self.orientation = .horizontal + self.alignment = .centerY + self.spacing = 0 + self.wantsLayer = true + self.layer?.cornerRadius = 2 + + let nameView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16)) + nameView.font = NSFont.systemFont(ofSize: 13, weight: .light) + nameView.stringValue = name + + self.addArrangedSubview(nameView) + self.addArrangedSubview(NSView()) + + batteryLevel.forEach { (pair: KeyValue_t) in + self.addLevel(pair) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayer() { + self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor + } + + public func update(_ batteryLevel: [KeyValue_t]) { + batteryLevel.forEach { (pair: KeyValue_t) in + if let view = self.subviews.first(where: { $0.identifier?.rawValue == pair.key }) as? NSTextField { + view.stringValue = "\(pair.value)%" + } else { + self.addLevel(pair) + } + } + } + + private func addLevel(_ pair: KeyValue_t) { + let valueView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 13)) + valueView.identifier = NSUserInterfaceItemIdentifier(rawValue: pair.key) + valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular) + valueView.stringValue = "\(pair.value)%" + valueView.toolTip = pair.key + self.addArrangedSubview(valueView) + } +} diff --git a/Modules/Bluetooth/readers.swift b/Modules/Bluetooth/readers.swift new file mode 100644 index 00000000..931f4321 --- /dev/null +++ b/Modules/Bluetooth/readers.swift @@ -0,0 +1,179 @@ +// +// readers.swift +// Bluetooth +// +// Created by Serhiy Mytrovtsiy on 08/06/2021. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import Kit +import CoreBluetooth +import IOBluetooth + +internal class DevicesReader: Reader<[BLEDevice]> { + private let ble: BluetoothDelegate = BluetoothDelegate() + + init() { + super.init() + } + + public override func read() { + self.ble.read() + self.callback(self.ble.devices) + } +} + +class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { + private var manager: CBCentralManager! + private let cache = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth") + + private var peripherals: [CBPeripheral] = [] + public var devices: [BLEDevice] = [] + private var characteristicsDict: [UUID: CBCharacteristic] = [:] + + private let batteryServiceUUID = CBUUID(string: "0x180F") + private let batteryCharacteristicsUUID = CBUUID(string: "0x2A19") + + override init() { + super.init() + self.manager = CBCentralManager.init(delegate: self, queue: nil) + } + + public func read() { + IOBluetoothDevice.pairedDevices().forEach { (d) in + guard let device = d as? IOBluetoothDevice, + let cache = self.findInCache(address: device.addressString) else { + return + } + + let rssi = device.rawRSSI() == 127 ? nil : Int(device.rawRSSI()) + + if let idx = self.devices.firstIndex(where: { $0.uuid == cache.uuid }) { + self.devices[idx].isConnected = device.isConnected() + self.devices[idx].isPaired = device.isPaired() + self.devices[idx].RSSI = rssi + } else { + self.devices.append(BLEDevice( + uuid: cache.uuid, + name: device.nameOrAddress, + type: .unknown, + RSSI: rssi, + batteryLevel: cache.batteryLevel, + isConnected: device.isConnected(), + isPaired: device.isPaired(), + isInitialized: false + )) + } + } + } + + private func findInCache(address: String) -> (uuid: UUID, batteryLevel: [KeyValue_t])? { + guard let plist = self.cache, + let deviceCache = plist.object(forKey: "DeviceCache") as? [String: [String: Any]], + let coreCache = plist.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else { + return nil + } + + guard let uuid = coreCache.compactMap({ (key, dict) -> UUID? in + guard let field = dict.first(where: { $0.key == "DeviceAddress" }), + let value = field.value as? String, + value == address else { + return nil + } + return UUID(uuidString: key) + }).first else { + return nil + } + + var batteryLevel: [KeyValue_t] = [] + if let d = deviceCache.first(where: { $0.key == address }) { + d.value.forEach { (key, value) in + guard let value = value as? Int, key == "BatteryPercentCase" || key == "BatteryPercentLeft" || key == "BatteryPercentRight" else { + return + } + + batteryLevel.append(KeyValue_t(key: key, value: "\(value)")) + } + } + + return (uuid, batteryLevel) + } + + // MARK: - CBCentralManagerDelegate + + func centralManagerDidUpdateState(_ central: CBCentralManager) { + if central.state == .poweredOff { + self.manager.stopScan() + } else if central.state == .poweredOn { + self.manager.scanForPeripherals(withServices: nil, options: nil) + } + } + + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + guard let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) else { + return + } + + if self.devices[idx].RSSI == nil { + self.devices[idx].RSSI = Int(truncating: RSSI) + } + + if self.devices[idx].peripheral == nil { + self.devices[idx].peripheral = peripheral + } + + if peripheral.state == .disconnected { + central.connect(peripheral, options: nil) + } else if peripheral.state == .connected && !self.devices[idx].isInitialized { + peripheral.delegate = self + peripheral.discoverServices([batteryServiceUUID]) + self.devices[idx].isInitialized = true + } + } + + // MARK: - CBPeripheralDelegate + + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + guard error == nil else { + print("didDiscoverServices: ", error!) + return + } + + guard let service = peripheral.services?.first(where: { $0.uuid == self.batteryServiceUUID }) else { + print("battery service not found, skipping") + return + } + + peripheral.discoverCharacteristics([self.batteryCharacteristicsUUID], for: service) + } + + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + guard error == nil else { + print("didDiscoverCharacteristicsFor: ", error!) + return + } + + guard let batteryCharacteristics = service.characteristics?.first(where: { $0.uuid == self.batteryCharacteristicsUUID }) else { + print("characteristics not found") + return + } + + self.characteristicsDict[peripheral.identifier] = batteryCharacteristics + peripheral.readValue(for: batteryCharacteristics) + } + + func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + guard error == nil else { + print("didUpdateValueFor: ", error!) + return + } + + if let batteryLevel = characteristic.value?[0], let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) { + self.devices[idx].batteryLevel = [KeyValue_t(key: "battery", value: "\(batteryLevel)")] + } + } +} diff --git a/Modules/Bluetooth/settings.swift b/Modules/Bluetooth/settings.swift new file mode 100644 index 00000000..a791d390 --- /dev/null +++ b/Modules/Bluetooth/settings.swift @@ -0,0 +1,112 @@ +// +// settings.swift +// Bluetooth +// +// Created by Serhiy Mytrovtsiy on 07/07/2021. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +internal class Settings: NSStackView, Settings_v { + public var callback: (() -> Void) = {} + public var selectedBatteryHandler: (String) -> Void = {_ in } + + private let title: String + private var selectedBattery: String + private var button: NSPopUpButton? + + public init(_ title: String) { + self.title = title + self.selectedBattery = Store.shared.string(key: "\(self.title)_battery", defaultValue: "") + + super.init(frame: NSRect( + x: 0, + y: 0, + width: Constants.Settings.width - (Constants.Settings.margin*2), + height: 0 + )) + + self.orientation = .vertical + self.distribution = .gravityAreas + self.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin, + right: Constants.Settings.margin + ) + self.spacing = Constants.Settings.margin + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + internal func load(widgets: [widget_t]) { + self.subviews.forEach{ $0.removeFromSuperview() } + + self.addArrangedSubview(self.deviceSelector()) + + let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing + self.edgeInsets.top + self.edgeInsets.bottom + if self.frame.size.height != h { + self.setFrameSize(NSSize(width: self.bounds.width, height: h)) + } + } + + private func deviceSelector() -> NSView { + let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width - Constants.Settings.margin*2, height: Constants.Settings.row)) + + let rowTitle: NSTextField = LabelField( + frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17), + localizedString("Battery to show") + ) + rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light) + rowTitle.textColor = .textColor + + self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 140, y: -1, width: 140, height: 30)) + self.button!.target = self + self.button?.action = #selector(self.handleSelection) + + view.addSubview(rowTitle) + view.addSubview(self.button!) + + return view + } + + internal func setList(_ list: [BLEDevice]) { + var batteries: [String] = [] + list.forEach { (d: BLEDevice) in + if d.batteryLevel.count == 1 { + batteries.append(d.name) + } else { + d.batteryLevel.forEach { (pair: KeyValue_t) in + batteries.append("\(d.name)@\(pair.key)") + } + } + } + + DispatchQueue.main.async(execute: { + if self.button?.itemTitles.count != batteries.count { + self.button?.removeAllItems() + } + + if batteries != self.button?.itemTitles { + self.button?.addItems(withTitles: batteries.map{ $0.replacingOccurrences(of: "@", with: " - ")}) + if self.selectedBattery != "" { + self.button?.selectItem(withTitle: self.selectedBattery.replacingOccurrences(of: "@", with: " - ")) + } + } + }) + } + + @objc private func handleSelection(_ sender: NSPopUpButton) { + guard let item = sender.selectedItem else { return } + self.selectedBattery = item.title.replacingOccurrences(of: " - ", with: "@") + Store.shared.set(key: "\(self.title)_battery", value: self.selectedBattery) + self.selectedBatteryHandler(self.selectedBattery) + } +} diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index f9fef71c..b17ddbba 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -8,6 +8,12 @@ /* Begin PBXBuildFile section */ 9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; }; + 9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; }; + 9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9A11AAF4266FD7A7000C1C05 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AAF3266FD7A7000C1C05 /* main.swift */; }; + 9A11AB26266FD828000C1C05 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A11AB25266FD828000C1C05 /* config.plist */; }; + 9A11AB36266FD9F4000C1C05 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AB35266FD9F4000C1C05 /* readers.swift */; }; + 9A11AB67266FDB69000C1C05 /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; }; 9A27D5352538A456001BB651 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 9A27D5342538A456001BB651 /* Reachability */; }; 9A2846FE2666A9CC00EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; }; 9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -75,6 +81,7 @@ 9A81C7692449A43600825D92 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7672449A43600825D92 /* main.swift */; }; 9A81C76A2449A43600825D92 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7682449A43600825D92 /* readers.swift */; }; 9A8AE0A326921A2A00B13054 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8AE0A226921A2A00B13054 /* Server.swift */; }; + 9A8B923D2696445C00FD6D83 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8B923C2696445C00FD6D83 /* settings.swift */; }; 9A8DE58E253DEFA9006A748F /* Fans.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A8DE587253DEFA9006A748F /* Fans.framework */; }; 9A8DE58F253DEFA9006A748F /* Fans.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A8DE587253DEFA9006A748F /* Fans.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9A8DE5E4253DF4E2006A748F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8DE5E3253DF4E2006A748F /* main.swift */; }; @@ -85,6 +92,7 @@ 9A90E19624EAD35F00471E9A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E19524EAD35F00471E9A /* main.swift */; }; 9A90E19824EAD3B000471E9A /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A90E19724EAD3B000471E9A /* config.plist */; }; 9A90E1A324EAD66600471E9A /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E1A224EAD66600471E9A /* reader.swift */; }; + 9A94B81F26822DE0001F4F2B /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A94B81E26822DE0001F4F2B /* popup.swift */; }; 9A953A1424B9D22D0038EF4B /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A953A1324B9D22D0038EF4B /* settings.swift */; }; 9A97CED12537331B00742D8F /* CPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; }; 9A97CED22537331B00742D8F /* CPU.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -129,6 +137,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A11AACE266FD77F000C1C05; + remoteInfo = Bluetooth; + }; + 9A11AB69266FDB69000C1C05 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D; + remoteInfo = Kit; + }; 9A2846FC2666A9CC00EC1F6D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9A1410ED229E721100D29793 /* Project object */; @@ -270,6 +292,7 @@ 9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */, 9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */, 9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */, + 9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */, 9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */, 9ABFF8FE248BEBCB00C9041A /* Battery.framework in Embed Frameworks */, 9A3E17D4247A94AF00449CD1 /* Net.framework in Embed Frameworks */, @@ -316,6 +339,11 @@ 98BF5451254DF04C004E9DF5 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 9A00010025CFF9D6001D02B9 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; 9A045EB62594F8D100ED58F2 /* Dashboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = ""; }; + 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bluetooth.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9A11AAD2266FD77F000C1C05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9A11AAF3266FD7A7000C1C05 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 9A11AB25266FD828000C1C05 /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; }; + 9A11AB35266FD9F4000C1C05 /* readers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; }; 9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9A141101229E721200D29793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A27D4A925389EFD001BB651 /* Stats.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stats.entitlements; sourceTree = ""; }; @@ -378,6 +406,7 @@ 9A81C7672449A43600825D92 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 9A81C7682449A43600825D92 /* readers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; }; 9A8AE0A226921A2A00B13054 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; + 9A8B923C2696445C00FD6D83 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; 9A8DE587253DEFA9006A748F /* Fans.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fans.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9A8DE58A253DEFA9006A748F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A8DE5E3253DF4E2006A748F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -388,6 +417,7 @@ 9A90E19524EAD35F00471E9A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 9A90E19724EAD3B000471E9A /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; }; 9A90E1A224EAD66600471E9A /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = ""; }; + 9A94B81E26822DE0001F4F2B /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; }; 9A953A1324B9D22D0038EF4B /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; }; 9A97CE2A25371B2300742D8F /* IntelPowerGadget.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntelPowerGadget.framework; path = ../../../Library/Frameworks/IntelPowerGadget.framework; sourceTree = ""; }; 9A97CECA2537331B00742D8F /* CPU.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CPU.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -441,6 +471,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 9A11AACC266FD77F000C1C05 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A11AB67266FDB69000C1C05 /* Kit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9A1410F2229E721100D29793 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -448,6 +486,7 @@ 9AF9EE0924648751005D2270 /* Disk.framework in Frameworks */, 9AE29ADC249A50350071B02D /* Sensors.framework in Frameworks */, 9ABFF8FD248BEBCB00C9041A /* Battery.framework in Frameworks */, + 9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */, 9A2846FE2666A9CC00EC1F6D /* Kit.framework in Frameworks */, 9A81C75D2449A41400825D92 /* RAM.framework in Frameworks */, 9A8DE58E253DEFA9006A748F /* Fans.framework in Frameworks */, @@ -548,6 +587,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9A11AAD0266FD77F000C1C05 /* Bluetooth */ = { + isa = PBXGroup; + children = ( + 9A11AAF3266FD7A7000C1C05 /* main.swift */, + 9A11AB35266FD9F4000C1C05 /* readers.swift */, + 9A94B81E26822DE0001F4F2B /* popup.swift */, + 9A8B923C2696445C00FD6D83 /* settings.swift */, + 9A11AAD2266FD77F000C1C05 /* Info.plist */, + 9A11AB25266FD828000C1C05 /* config.plist */, + ); + path = Bluetooth; + sourceTree = ""; + }; 9A1410EC229E721100D29793 = { isa = PBXGroup; children = ( @@ -576,6 +628,7 @@ 9A8DE587253DEFA9006A748F /* Fans.framework */, 9ADE6FD8265D032100D2FBA8 /* smc */, 9A2846F72666A9CC00EC1F6D /* Kit.framework */, + 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */, ); name = Products; sourceTree = ""; @@ -787,6 +840,7 @@ 9A8DE588253DEFA9006A748F /* Fans */, 9A3E17CD247A94AF00449CD1 /* Net */, 9ABFF8F7248BEBCB00C9041A /* Battery */, + 9A11AAD0266FD77F000C1C05 /* Bluetooth */, ); path = Modules; sourceTree = ""; @@ -845,6 +899,13 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 9A11AACA266FD77F000C1C05 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9A2846F22666A9CC00EC1F6D /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -911,6 +972,25 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 9A11AACE266FD77F000C1C05 /* Bluetooth */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9A11AADA266FD77F000C1C05 /* Build configuration list for PBXNativeTarget "Bluetooth" */; + buildPhases = ( + 9A11AACA266FD77F000C1C05 /* Headers */, + 9A11AACB266FD77F000C1C05 /* Sources */, + 9A11AACC266FD77F000C1C05 /* Frameworks */, + 9A11AACD266FD77F000C1C05 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9A11AB6A266FDB69000C1C05 /* PBXTargetDependency */, + ); + name = Bluetooth; + productName = Bluetooth; + productReference = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; + productType = "com.apple.product-type.framework"; + }; 9A1410F4229E721100D29793 /* Stats */ = { isa = PBXNativeTarget; buildConfigurationList = 9A141105229E721200D29793 /* Build configuration list for PBXNativeTarget "Stats" */; @@ -936,6 +1016,7 @@ 9A97CED02537331B00742D8F /* PBXTargetDependency */, 9A8DE58D253DEFA9006A748F /* PBXTargetDependency */, 9A2846FD2666A9CC00EC1F6D /* PBXTargetDependency */, + 9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */, ); name = Stats; packageProductDependencies = ( @@ -1166,6 +1247,10 @@ LastUpgradeCheck = 1250; ORGANIZATIONNAME = "Serhiy Mytrovtsiy"; TargetAttributes = { + 9A11AACE266FD77F000C1C05 = { + CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1240; + }; 9A1410F4229E721100D29793 = { CreatedOnToolsVersion = 10.2.1; LastSwiftMigration = 1030; @@ -1262,6 +1347,7 @@ targets = ( 9A1410F4229E721100D29793 /* Stats */, 9A343526243E26A0006B19F9 /* LaunchAtLogin */, + 9ADE6FD7265D032100D2FBA8 /* SMC */, 9A2846F62666A9CC00EC1F6D /* Kit */, 9A97CEC92537331B00742D8F /* CPU */, 9A90E18824EAD2BB00471E9A /* GPU */, @@ -1271,12 +1357,20 @@ 9ABFF8F5248BEBCB00C9041A /* Battery */, 9AE29AD4249A50350071B02D /* Sensors */, 9A8DE586253DEFA9006A748F /* Fans */, - 9ADE6FD7265D032100D2FBA8 /* SMC */, + 9A11AACE266FD77F000C1C05 /* Bluetooth */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 9A11AACD266FD77F000C1C05 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A11AB26266FD828000C1C05 /* config.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9A1410F3229E721100D29793 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1390,6 +1484,17 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 9A11AACB266FD77F000C1C05 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A11AB36266FD9F4000C1C05 /* readers.swift in Sources */, + 9A94B81F26822DE0001F4F2B /* popup.swift in Sources */, + 9A8B923D2696445C00FD6D83 /* settings.swift in Sources */, + 9A11AAF4266FD7A7000C1C05 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9A1410F1229E721100D29793 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1551,6 +1656,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A11AACE266FD77F000C1C05 /* Bluetooth */; + targetProxy = 9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */; + }; + 9A11AB6A266FDB69000C1C05 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A2846F62666A9CC00EC1F6D /* Kit */; + targetProxy = 9A11AB69266FDB69000C1C05 /* PBXContainerItemProxy */; + }; 9A2846FD2666A9CC00EC1F6D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 9A2846F62666A9CC00EC1F6D /* Kit */; @@ -1671,6 +1786,67 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 9A11AAD8266FD77F000C1C05 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = RP2S87B72W; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Modules/Bluetooth/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Bluetooth; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9A11AAD9266FD77F000C1C05 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = RP2S87B72W; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Modules/Bluetooth/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Bluetooth; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 9A141103229E721200D29793 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2528,6 +2704,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 9A11AADA266FD77F000C1C05 /* Build configuration list for PBXNativeTarget "Bluetooth" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A11AAD8266FD77F000C1C05 /* Debug */, + 9A11AAD9266FD77F000C1C05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9A1410F0229E721100D29793 /* Build configuration list for PBXProject "Stats" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index 82baf4cd..3c53c577 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -18,6 +18,7 @@ import Battery import Sensors import GPU import Fans +import Bluetooth let updater = macAppUpdater(user: "exelban", repo: "stats") var modules: [Module] = [ @@ -28,7 +29,8 @@ var modules: [Module] = [ Sensors(), Fans(), Network(), - Battery() + Battery(), + Bluetooth() ] class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {