├── .gitignore ├── desk ├── table.png ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── desk.entitlements ├── EventMonitor.swift ├── Info.plist ├── AppDelegate.swift ├── DeskController │ ├── ParticlePeripheral.swift │ └── DeskConnect.swift ├── DeskViewController.swift ├── BinUtils.swift └── Base.lproj │ └── Main.storyboard ├── README.md ├── desk.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── forti.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── desk.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── podfile ├── deskTests ├── Info.plist └── deskTests.swift ├── deskUITests ├── Info.plist └── deskUITests.swift └── Podfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | Pods -------------------------------------------------------------------------------- /desk/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fortidude/LinakDeskControl/HEAD/desk/table.png -------------------------------------------------------------------------------- /desk/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinakDeskControl 2 | 3 | Swift status bar app to control Ikea Idasen bluetooth desk 4 | 5 | Work in progress 6 | -------------------------------------------------------------------------------- /desk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /desk.xcodeproj/xcuserdata/forti.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /desk.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /desk.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /podfile: -------------------------------------------------------------------------------- 1 | # Podfile 2 | use_frameworks! 3 | 4 | target 'desk' do 5 | pod 'RxSwift', '~> 5' 6 | pod 'RxCocoa', '~> 5' 7 | end 8 | 9 | # RxTest and RxBlocking make the most sense in the context of unit/integration tests 10 | target 'deskTests' do 11 | pod 'RxBlocking', '~> 5' 12 | pod 'RxTest', '~> 5' 13 | end -------------------------------------------------------------------------------- /desk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /desk/desk.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.device.bluetooth 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /desk.xcodeproj/xcuserdata/forti.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | desk.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 7 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /desk/EventMonitor.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | public class EventMonitor { 4 | private var monitor: Any? 5 | private let mask: NSEvent.EventTypeMask 6 | private let handler: (NSEvent?) -> Void 7 | 8 | public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { 9 | self.mask = mask 10 | self.handler = handler 11 | } 12 | 13 | deinit { 14 | stop() 15 | } 16 | 17 | public func start() { 18 | monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) 19 | } 20 | 21 | public func stop() { 22 | if monitor != nil { 23 | NSEvent.removeMonitor(monitor!) 24 | monitor = nil 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /deskTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /deskUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RxBlocking (5.1.1): 3 | - RxSwift (~> 5) 4 | - RxCocoa (5.1.1): 5 | - RxRelay (~> 5) 6 | - RxSwift (~> 5) 7 | - RxRelay (5.1.1): 8 | - RxSwift (~> 5) 9 | - RxSwift (5.1.1) 10 | - RxTest (5.1.1): 11 | - RxSwift (~> 5) 12 | 13 | DEPENDENCIES: 14 | - RxBlocking (~> 5) 15 | - RxCocoa (~> 5) 16 | - RxSwift (~> 5) 17 | - RxTest (~> 5) 18 | 19 | SPEC REPOS: 20 | trunk: 21 | - RxBlocking 22 | - RxCocoa 23 | - RxRelay 24 | - RxSwift 25 | - RxTest 26 | 27 | SPEC CHECKSUMS: 28 | RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 29 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 30 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 31 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 32 | RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa 33 | 34 | PODFILE CHECKSUM: 7ef378dbdd4c62f31072a67bf2f217617ea1e077 35 | 36 | COCOAPODS: 1.9.1 37 | -------------------------------------------------------------------------------- /deskTests/deskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // deskTests.swift 3 | // deskTests 4 | // 5 | // Created by Forti on 18/05/2020. 6 | // Copyright © 2020 Forti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import desk 11 | 12 | class deskTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() throws { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /desk/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /desk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSBluetoothPeripheralUsageDescription 6 | App uses Bluetooth to control desk 7 | LSUIElement 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIconFile 14 | 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | $(PRODUCT_NAME) 21 | CFBundlePackageType 22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 23 | CFBundleShortVersionString 24 | 1.0 25 | CFBundleVersion 26 | 1 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | Copyright © 2020 Forti. All rights reserved. 31 | NSMainStoryboardFile 32 | Main 33 | NSPrincipalClass 34 | NSApplication 35 | NSSupportsAutomaticTermination 36 | 37 | NSSupportsSuddenTermination 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /deskUITests/deskUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // deskUITests.swift 3 | // deskUITests 4 | // 5 | // Created by Forti on 18/05/2020. 6 | // Copyright © 2020 Forti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class deskUITests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use recording to get started writing UI tests. 32 | // Use XCTAssert and related functions to verify your tests produce the correct results. 33 | } 34 | 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /desk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // desk 4 | // 5 | // Created by Forti on 18/05/2020. 6 | // Copyright © 2020 Forti. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) 16 | let popover = NSPopover() 17 | 18 | var eventMonitor: EventMonitor? 19 | 20 | func applicationDidFinishLaunching(_ aNotification: Notification) { 21 | if let button = statusItem.button { 22 | button.image = NSImage(named: NSImage.Name("table")) 23 | button.action = #selector(togglePopover(_:)) 24 | } 25 | 26 | popover.contentViewController = DeskViewController.freshController() 27 | 28 | eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in 29 | if let strongSelf = self, strongSelf.popover.isShown { 30 | strongSelf.closePopover(sender: event) 31 | } 32 | } 33 | } 34 | 35 | 36 | @objc func togglePopover(_ sender: Any?) { 37 | if popover.isShown { 38 | closePopover(sender: sender) 39 | } else { 40 | showPopover(sender: sender) 41 | } 42 | } 43 | 44 | func showPopover(sender: Any?) { 45 | if let button = statusItem.button { 46 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 47 | } 48 | 49 | eventMonitor?.start() 50 | } 51 | 52 | func closePopover(sender: Any?) { 53 | popover.performClose(sender) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /desk/DeskController/ParticlePeripheral.swift: -------------------------------------------------------------------------------- 1 | import CoreBluetooth 2 | 3 | class ParticlePeripheral: NSObject { 4 | 5 | /// MARK: - Particle services and charcteristics Identifiers 6 | 7 | //GENERIC_ACCESS = "00001800-0000-1000-8000-00805F9B34FB" 8 | //REFERENCE_INPUT = "99FA0030-338A-1024-8A49-009C0215F78A" 9 | //REFERENCE_OUTPUT = "99FA0020-338A-1024-8A49-009C0215F78A" 10 | //DPG = "99FA0010-338A-1024-8A49-009C0215F78A" 11 | //CONTROL = "99FA0001-338A-1024-8A49-009C0215F78A" 12 | 13 | // MOVE_1_DOWN = 70 14 | // MOVE_1_UP = 71 15 | // 16 | // UNDEFINED = 254 ## used as stop 17 | // STOP_MOVING = 255 18 | 19 | /** 20 | All commented are not working 21 | */ 22 | 23 | // public static let serviceGenericAccess = CBUUID.init(string: "00001800-0000-1000-8000-00805F9B34FB") 24 | // public static let serviceReferenceInput = CBUUID.init(string: "99FA0030-338A-1024-8A49-009C0215F78A") 25 | // public static let serviceDPG = CBUUID.init(string: "99FA0010-338A-1024-8A49-009C0215F78A") // DPG (physical control 26 | public static let servicePosition = CBUUID.init(string: "99FA0020-338A-1024-8A49-009C0215F78A") // one 27 | public static let serviceControl = CBUUID.init(string: "99FA0001-338A-1024-8A49-009C0215F78A") // control 28 | 29 | // public static let characteristicDeviceName = CBUUID.init(string: "00002A00-0000-1000-8000-00805F9B34FB") 30 | // public static let characteristicServiceChanges = CBUUID.init(string: "00002A05-0000-1000-8000-00805F9B34FB") 31 | // public static let characteristicManufacturer = CBUUID.init(string: "00002A29-0000-1000-8000-00805F9B34FB") 32 | // public static let characteristicModelNumber = CBUUID.init(string: "00002A24-0000-1000-8000-00805F9B34FB") 33 | // public static let characteristicError = CBUUID.init(string: "99FA0003-338A-1024-8A49-009C0215F78A") 34 | // public static let characteristicDPG = CBUUID.init(string: "99FA0011-338A-1024-8A49-009C0215F78A") 35 | // public static let characteristicMove = CBUUID.init(string: "99FA0031-338A-1024-8A49-009C0215F78A") 36 | public static let characteristicPosition = CBUUID.init(string: "99FA0021-338A-1024-8A49-009C0215F78A") 37 | public static let characteristicControl = CBUUID.init(string: "99FA0002-338A-1024-8A49-009C0215F78A") 38 | 39 | public static let allServices = [ 40 | // ParticlePeripheral.serviceGenericAccess, 41 | // ParticlePeripheral.serviceReferenceInput, 42 | // ParticlePeripheral.serviceDPG, 43 | ParticlePeripheral.servicePosition, 44 | ParticlePeripheral.serviceControl 45 | ] 46 | 47 | public static let allCharacteristics = [ 48 | // ParticlePeripheral.characteristicDeviceName, 49 | // ParticlePeripheral.characteristicServiceChanges, 50 | // ParticlePeripheral.characteristicManufacturer, 51 | // ParticlePeripheral.characteristicModelNumber, 52 | // ParticlePeripheral.characteristicError, 53 | // ParticlePeripheral.characteristicDPG, 54 | // ParticlePeripheral.characteristicHighSpeed, 55 | ParticlePeripheral.characteristicPosition, 56 | ParticlePeripheral.characteristicControl 57 | 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /desk/DeskViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeskViewcontroller.swift 3 | // desk 4 | // 5 | // Created by Forti on 18/05/2020. 6 | // Copyright © 2020 Forti. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DeskViewController: NSViewController { 12 | private var deskConnect: DeskConnect! 13 | private var longClick: NSPressGestureRecognizer? 14 | private var userDefaults: UserDefaults? 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | self.deskConnect = DeskConnect() 19 | 20 | self.buttonUp.isEnabled = false 21 | self.buttonDown.isEnabled = false 22 | 23 | self.deskConnect.currentPosition.asObservable().subscribe({ value in 24 | if let position = value.element { 25 | if (position > 0) { 26 | self.currentValue.stringValue = String(format:"%.1f", position) 27 | self.currentPosition = position 28 | } 29 | } 30 | 31 | }).disposed(by: self.deskConnect.dispose) 32 | 33 | self.deskConnect.deviceName.asObservable().subscribe({ value in 34 | self.deskName.stringValue = "\(value.element ?? "Unknown desk")" 35 | self.buttonUp.isEnabled = true 36 | self.buttonDown.isEnabled = true 37 | }).disposed(by: self.deskConnect.dispose) 38 | 39 | self.userDefaults = UserDefaults.init(suiteName: "positions") 40 | self.initSavedPositions() 41 | 42 | self.buttonUp.sendAction(on: .leftMouseDown) 43 | self.buttonUp.isContinuous = true 44 | self.buttonUp.setPeriodicDelay(0, interval: 0.7) 45 | 46 | self.buttonDown.sendAction(on: .leftMouseDown) 47 | self.buttonDown.isContinuous = true 48 | self.buttonDown.setPeriodicDelay(0, interval: 0.7) 49 | } 50 | 51 | 52 | var currentPosition: Double! 53 | @IBOutlet var currentValue: NSTextField! 54 | @IBOutlet var deskName: NSTextField! 55 | @IBOutlet var buttonUp: NSButton! 56 | @IBOutlet var buttonDown: NSButton! 57 | 58 | @IBOutlet var buttonMoveToSit: NSButton! 59 | @IBOutlet var buttonMoveToStand: NSButton! 60 | 61 | @IBOutlet var sitPosition: NSTextField! 62 | @IBOutlet var standPosition: NSTextField! 63 | 64 | var isMovingToPositionValue = false 65 | var moveToPositionValue = 70.0 66 | 67 | var isWaitingForSecondPress = false 68 | @objc func stopMoving() { 69 | self.deskConnect.stopMoving() 70 | self.isWaitingForSecondPress = true 71 | } 72 | 73 | @objc func clearIsWairingForSecondPress() { 74 | self.isWaitingForSecondPress = false 75 | } 76 | 77 | func handleStopMovingIfSingleClick() { 78 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(clearIsWairingForSecondPress), object: nil) 79 | 80 | if (self.isWaitingForSecondPress == false) { 81 | perform(#selector(stopMoving), with: nil, afterDelay: 0.18) 82 | } 83 | 84 | perform(#selector(clearIsWairingForSecondPress), with: nil, afterDelay: 0.2) 85 | } 86 | 87 | @IBAction func up(_ sender: NSButton) { 88 | self.deskConnect.moveUp() 89 | self.handleStopMovingIfSingleClick() 90 | } 91 | 92 | @IBAction func down(_ sender: NSButton) { 93 | self.deskConnect.moveDown() 94 | self.handleStopMovingIfSingleClick() 95 | } 96 | 97 | @IBAction func saveAsSitPosition(_ sender: NSButton) { 98 | self.userDefaults?.set(self.currentPosition, forKey: "sit-position") 99 | self.sitPosition.stringValue = String(format:"%.1f", self.currentPosition) 100 | } 101 | 102 | @IBAction func saveAsStandPosition(_ sender: NSButton) { 103 | self.userDefaults?.set(self.currentPosition, forKey: "stand-position") 104 | self.standPosition.stringValue = String(format:"%.1f", self.currentPosition) 105 | } 106 | 107 | @IBAction func moveToSitPosition(_ sender: NSButton) { 108 | let position = self.userDefaults?.double(forKey: "sit-position") ?? .nan 109 | if (position != .nan) { 110 | self.deskConnect.moveToPosition(position: position) 111 | } 112 | } 113 | 114 | @IBAction func moveToStandPosition(_ sender: NSButton) { 115 | let position = self.userDefaults?.double(forKey: "stand-position") ?? .nan 116 | if (position != .nan) { 117 | self.deskConnect.moveToPosition(position: position) 118 | } 119 | } 120 | 121 | @IBAction func stop(_ sender: NSButton) { 122 | self.deskConnect.stopMoving() 123 | } 124 | 125 | private func initSavedPositions() { 126 | if let sitPosition = self.userDefaults?.double(forKey: "sit-position") { 127 | self.sitPosition.stringValue = String(format:"%.1f", sitPosition) 128 | } 129 | 130 | if let standPosition = self.userDefaults?.double(forKey: "stand-position") { 131 | self.standPosition.stringValue = String(format:"%.1f", standPosition) 132 | } 133 | } 134 | } 135 | 136 | extension DeskViewController { 137 | // MARK: Storyboard instantiation 138 | static func freshController() -> DeskViewController { 139 | //1. 140 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 141 | //2. 142 | let identifier = NSStoryboard.SceneIdentifier("DeskViewController") 143 | //3. 144 | guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? DeskViewController else { 145 | fatalError("Why cant i find QuotesViewController? - Check Main.storyboard") 146 | } 147 | return viewcontroller 148 | } 149 | } 150 | 151 | -------------------------------------------------------------------------------- /desk/DeskController/DeskConnect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connect.swift 3 | // desk 4 | // 5 | // Created by Forti on 05/06/2020. 6 | // Copyright © 2020 Forti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | import RxSwift 12 | import RxRelay 13 | 14 | let deviceNamePattern = #"Desk[\s0-9].*"#; 15 | class DeskConnect: NSObject, CBPeripheralDelegate, CBCentralManagerDelegate { 16 | private var centralManager: CBCentralManager! 17 | private var peripheral: CBPeripheral! 18 | 19 | private var characteristicPosition: CBCharacteristic! 20 | private var characteristicControl: CBCharacteristic! 21 | 22 | private var valueMoveUp = pack("(value: "") 30 | let currentPosition = BehaviorRelay(value: 0) 31 | let dispose = DisposeBag() 32 | 33 | let deskOffset = 62.5 34 | 35 | override init() { 36 | super.init() 37 | centralManager = CBCentralManager(delegate: self, queue: nil) 38 | } 39 | 40 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 41 | if (central.state != .poweredOn) { 42 | print("Central is not powered on. Bluetooth disabled? @TODO") 43 | } else { 44 | self.centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) 45 | } 46 | } 47 | 48 | /** 49 | ON DISCOVER 50 | */ 51 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 52 | // @TODO 53 | // DeviceName is just a test. let's do collection list with option to connect by user 54 | let deviceName = (advertisementData as NSDictionary).object(forKey: CBAdvertisementDataLocalNameKey) as? NSString 55 | let isDesk = deviceName?.range(of: deviceNamePattern, options:.regularExpression) 56 | if (isDesk?.location != NSNotFound && deviceName != nil) { 57 | self.centralManager.stopScan() 58 | 59 | if let dn = deviceName { 60 | self.deviceName.accept(dn as String) 61 | } 62 | 63 | self.peripheral = peripheral 64 | self.peripheral.delegate = self 65 | self.centralManager.connect(self.peripheral, options: nil) 66 | } 67 | } 68 | 69 | /** 70 | ON CONNECT 71 | */ 72 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { 73 | print(peripheral) 74 | 75 | self.peripheral.discoverServices(ParticlePeripheral.allServices) 76 | } 77 | 78 | /** 79 | ON SERVICES 80 | */ 81 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { 82 | if let services = peripheral.services { 83 | for service in services { 84 | self.peripheral.discoverCharacteristics(ParticlePeripheral.allCharacteristics, for: service) 85 | } 86 | } 87 | } 88 | 89 | /** 90 | ON CHARACTERISTICS 91 | */ 92 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { 93 | if let characteristics = service.characteristics { 94 | for characteristic in characteristics { 95 | self.peripheral.readValue(for: characteristic) 96 | self.peripheral.setNotifyValue(true, for: characteristic) 97 | 98 | 99 | if (characteristic.uuid.uuidString == ParticlePeripheral.characteristicControl.uuidString) { 100 | self.characteristicControl = characteristic 101 | } 102 | 103 | if (characteristic.uuid.uuidString == ParticlePeripheral.characteristicPosition.uuidString) { 104 | self.characteristicPosition = characteristic 105 | self.updatePosition(characteristic: characteristic) 106 | } 107 | } 108 | } 109 | } 110 | 111 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { 112 | self.updatePosition(characteristic: characteristic) 113 | } 114 | 115 | func turnOnContinous() { 116 | 117 | } 118 | 119 | func moveUp() { 120 | print("up") 121 | self.peripheral.writeValue(Data(self.valueMoveUp), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse) 122 | } 123 | 124 | func moveDown() { 125 | print("down") 126 | self.peripheral.writeValue(Data(self.valueMoveDown), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse) 127 | } 128 | 129 | private func updatePosition(characteristic: CBCharacteristic) { 130 | if (characteristic.value != nil && characteristic.uuid.uuidString == self.characteristicPosition.uuid.uuidString) { 131 | let byteArray = [UInt8](characteristic.value!) 132 | if (byteArray.indices.contains(0) && byteArray.indices.contains(1)) { 133 | do { 134 | let positionWrapped = try unpack(" (requiredPosition - 0.75) && formattedPosition < (requiredPosition + 0.75)) { 144 | self.moveToPositionTimer?.invalidate() 145 | self.moveToPositionValue = nil 146 | self.stopMoving() 147 | } 148 | } 149 | } 150 | } catch let error as NSError { 151 | print("Error, update position: \(error)") 152 | } 153 | } 154 | } 155 | } 156 | 157 | @objc func stopMoving() { 158 | self.peripheral.writeValue(Data(self.valueStopMove), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse) 159 | } 160 | 161 | func moveToPosition(position: Double) { 162 | self.moveToPositionValue = position 163 | self.handleMoveToPosition() 164 | 165 | self.moveToPositionTimer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { (Timer) in 166 | print("moving", self.currentPosition.value) 167 | if (self.moveToPositionValue == nil) { 168 | Timer.invalidate() 169 | } else { 170 | self.handleMoveToPosition() 171 | } 172 | } 173 | } 174 | 175 | private func handleMoveToPosition() { 176 | let positionRequired = self.moveToPositionValue ?? .nan 177 | if (positionRequired < self.currentPosition.value) { 178 | self.moveDown() 179 | } else if (positionRequired > self.currentPosition.value) { 180 | self.moveUp() 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /desk/BinUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BinUtils.swift 3 | // BinUtils 4 | // 5 | // Created by Nicolas Seriot on 12/03/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreFoundation 11 | 12 | // MARK: protocol UnpackedType 13 | 14 | public protocol Unpackable {} 15 | 16 | extension NSString: Unpackable {} 17 | extension Bool: Unpackable {} 18 | extension Int: Unpackable {} 19 | extension Double: Unpackable {} 20 | 21 | // MARK: protocol DataConvertible 22 | 23 | protocol DataConvertible {} 24 | 25 | extension DataConvertible { 26 | 27 | init?(data: Data) { 28 | guard data.count == MemoryLayout.size else { return nil } 29 | self = data.withUnsafeBytes { $0.load(as: Self.self) } 30 | } 31 | 32 | init?(bytes: [UInt8]) { 33 | let data = Data(bytes) 34 | self.init(data:data) 35 | } 36 | 37 | var data: Data { 38 | var value = self 39 | return Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) 40 | } 41 | } 42 | 43 | extension Bool : DataConvertible { } 44 | 45 | extension Int8 : DataConvertible { } 46 | extension Int16 : DataConvertible { } 47 | extension Int32 : DataConvertible { } 48 | extension Int64 : DataConvertible { } 49 | 50 | extension UInt8 : DataConvertible { } 51 | extension UInt16 : DataConvertible { } 52 | extension UInt32 : DataConvertible { } 53 | extension UInt64 : DataConvertible { } 54 | 55 | extension Float32 : DataConvertible { } 56 | extension Float64 : DataConvertible { } 57 | 58 | // MARK: String extension 59 | 60 | extension String { 61 | subscript (from:Int, to:Int) -> String { 62 | return NSString(string: self).substring(with: NSMakeRange(from, to-from)) 63 | } 64 | } 65 | 66 | // MARK: Data extension 67 | 68 | extension Data { 69 | var bytes : [UInt8] { 70 | return [UInt8](self) 71 | } 72 | } 73 | 74 | // MARK: functions 75 | 76 | public func hexlify(_ data:Data) -> String { 77 | 78 | // similar to hexlify() in Python's binascii module 79 | // https://docs.python.org/2/library/binascii.html 80 | 81 | var s = String() 82 | var byte: UInt8 = 0 83 | 84 | for i in 0 ..< data.count { 85 | NSData(data: data).getBytes(&byte, range: NSMakeRange(i, 1)) 86 | s = s.appendingFormat("%02x", byte) 87 | } 88 | 89 | return s as String 90 | } 91 | 92 | public func unhexlify(_ string:String) -> Data? { 93 | 94 | // similar to unhexlify() in Python's binascii module 95 | // https://docs.python.org/2/library/binascii.html 96 | 97 | let s = string.uppercased().replacingOccurrences(of: " ", with: "") 98 | 99 | let nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted 100 | if let range = s.rangeOfCharacter(from: nonHexCharacterSet) { 101 | print("-- found non hex character at range \(range)") 102 | return nil 103 | } 104 | 105 | var data = Data(capacity: s.count / 2) 106 | 107 | for i in stride(from: 0, to:s.count, by:2) { 108 | let byteString = s[i, i+2] 109 | let byte = UInt8(byteString.withCString { strtoul($0, nil, 16) }) 110 | data.append([byte] as [UInt8], count: 1) 111 | } 112 | 113 | return data 114 | } 115 | 116 | func readIntegerType(_ type:T.Type, bytes:[UInt8], loc:inout Int) -> T { 117 | let size = MemoryLayout.size 118 | let sub = Array(bytes[loc..<(loc+size)]) 119 | loc += size 120 | return T(bytes: sub)! 121 | } 122 | 123 | func readFloatingPointType(_ type:T.Type, bytes:[UInt8], loc:inout Int, isBigEndian:Bool) -> T { 124 | let size = MemoryLayout.size 125 | let sub = Array(bytes[loc..<(loc+size)]) 126 | loc += size 127 | let sub_ = isBigEndian ? sub.reversed() : sub 128 | return T(bytes: sub_)! 129 | } 130 | 131 | func isBigEndianFromMandatoryByteOrderFirstCharacter(_ format:String) -> Bool { 132 | 133 | guard let firstChar = format.first else { assertionFailure("empty format"); return false } 134 | 135 | let s = NSString(string: String(firstChar)) 136 | let c = s.substring(to: 1) 137 | 138 | if c == "@" { assertionFailure("native size and alignment is unsupported") } 139 | 140 | if c == "=" || c == "<" { return false } 141 | if c == ">" || c == "!" { return true } 142 | 143 | assertionFailure("format '\(format)' first character must be among '=<>!'") 144 | 145 | return false 146 | } 147 | 148 | // akin to struct.calcsize(fmt) 149 | func numberOfBytesInFormat(_ format:String) -> Int { 150 | 151 | var numberOfBytes = 0 152 | 153 | var n = 0 // repeat counter 154 | 155 | var mutableFormat = format 156 | 157 | while !mutableFormat.isEmpty { 158 | 159 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 160 | 161 | if let i = Int(String(c)) , 0...9 ~= i { 162 | if n > 0 { n *= 10 } 163 | n += i 164 | continue 165 | } 166 | 167 | if c == "s" { 168 | numberOfBytes += max(n,1) 169 | n = 0 170 | continue 171 | } 172 | 173 | let repeatCount = max(n,1) 174 | 175 | switch(c) { 176 | 177 | case "@", "<", "=", ">", "!", " ": 178 | () 179 | case "c", "b", "B", "x", "?": 180 | numberOfBytes += 1 * repeatCount 181 | case "h", "H": 182 | numberOfBytes += 2 * repeatCount 183 | case "i", "l", "I", "L", "f": 184 | numberOfBytes += 4 * repeatCount 185 | case "q", "Q", "d": 186 | numberOfBytes += 8 * repeatCount 187 | case "P": 188 | numberOfBytes += MemoryLayout.size * repeatCount 189 | default: 190 | assertionFailure("-- unsupported format \(c)") 191 | } 192 | 193 | n = 0 194 | } 195 | 196 | return numberOfBytes 197 | } 198 | 199 | func formatDoesMatchDataLength(_ format:String, data:Data) -> Bool { 200 | let sizeAccordingToFormat = numberOfBytesInFormat(format) 201 | let dataLength = data.count 202 | if sizeAccordingToFormat != dataLength { 203 | print("format \"\(format)\" expects \(sizeAccordingToFormat) bytes but data is \(dataLength) bytes") 204 | return false 205 | } 206 | 207 | return true 208 | } 209 | 210 | /* 211 | pack() and unpack() should behave as Python's struct module https://docs.python.org/2/library/struct.html BUT: 212 | - native size and alignment '@' is not supported 213 | - as a consequence, the byte order specifier character is mandatory and must be among "=<>!" 214 | - native byte order '=' assumes a little-endian system (eg. Intel x86) 215 | - Pascal strings 'p' and native pointers 'P' are not supported 216 | */ 217 | 218 | public enum BinUtilsError: Error { 219 | case formatDoesMatchDataLength(format:String, dataSize:Int) 220 | case unsupportedFormat(character:Character) 221 | } 222 | 223 | public func pack(_ format:String, _ objects:[Any], _ stringEncoding:String.Encoding=String.Encoding.windowsCP1252) -> Data { 224 | 225 | var objectsQueue = objects 226 | 227 | var mutableFormat = format 228 | 229 | var mutableData = Data() 230 | 231 | var isBigEndian = false 232 | 233 | let firstCharacter = mutableFormat.remove(at: mutableFormat.startIndex) 234 | 235 | switch(firstCharacter) { 236 | case "<", "=": 237 | isBigEndian = false 238 | case ">", "!": 239 | isBigEndian = true 240 | case "@": 241 | assertionFailure("native size and alignment '@' is unsupported'") 242 | default: 243 | assertionFailure("unsupported format chacracter'") 244 | } 245 | 246 | var n = 0 // repeat counter 247 | 248 | while !mutableFormat.isEmpty { 249 | 250 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 251 | 252 | if let i = Int(String(c)) , 0...9 ~= i { 253 | if n > 0 { n *= 10 } 254 | n += i 255 | continue 256 | } 257 | 258 | var o : Any = 0 259 | 260 | if c == "s" { 261 | o = objectsQueue.remove(at: 0) 262 | 263 | guard let stringData = (o as! String).data(using: .utf8) else { assertionFailure(); return Data() } 264 | var bytes = stringData.bytes 265 | 266 | let expectedSize = max(1, n) 267 | 268 | // pad ... 269 | while bytes.count < expectedSize { bytes.append(0x00) } 270 | 271 | // ... or trunk 272 | if bytes.count > expectedSize { bytes = Array(bytes[0.. [Unpackable] { 338 | 339 | assert(CFByteOrderGetCurrent() == 1 /* CFByteOrderLittleEndian */, "\(#file) assumes little endian, but host is big endian") 340 | 341 | let isBigEndian = isBigEndianFromMandatoryByteOrderFirstCharacter(format) 342 | 343 | if formatDoesMatchDataLength(format, data: data) == false { 344 | throw BinUtilsError.formatDoesMatchDataLength(format:format, dataSize:data.count) 345 | } 346 | 347 | var a : [Unpackable] = [] 348 | 349 | var loc = 0 350 | 351 | let bytes = data.bytes 352 | 353 | var n = 0 // repeat counter 354 | 355 | var mutableFormat = format 356 | 357 | mutableFormat.remove(at: mutableFormat.startIndex) // consume byte-order specifier 358 | 359 | while !mutableFormat.isEmpty { 360 | 361 | let c = mutableFormat.remove(at: mutableFormat.startIndex) 362 | 363 | if let i = Int(String(c)) , 0...9 ~= i { 364 | if n > 0 { n *= 10 } 365 | n += i 366 | continue 367 | } 368 | 369 | if c == "s" { 370 | let length = max(n,1) 371 | let sub = Array(bytes[loc.. /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 333 | showEnvVarsInLog = 0; 334 | }; 335 | B8BEB05CD77D4B2DB12B1C87 /* [CP] Check Pods Manifest.lock */ = { 336 | isa = PBXShellScriptBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | ); 340 | inputFileListPaths = ( 341 | ); 342 | inputPaths = ( 343 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 344 | "${PODS_ROOT}/Manifest.lock", 345 | ); 346 | name = "[CP] Check Pods Manifest.lock"; 347 | outputFileListPaths = ( 348 | ); 349 | outputPaths = ( 350 | "$(DERIVED_FILE_DIR)/Pods-deskTests-checkManifestLockResult.txt", 351 | ); 352 | runOnlyForDeploymentPostprocessing = 0; 353 | shellPath = /bin/sh; 354 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 355 | showEnvVarsInLog = 0; 356 | }; 357 | C4CA290866A3904FBFBFFFFC /* [CP] Embed Pods Frameworks */ = { 358 | isa = PBXShellScriptBuildPhase; 359 | buildActionMask = 2147483647; 360 | files = ( 361 | ); 362 | inputFileListPaths = ( 363 | "${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", 364 | ); 365 | name = "[CP] Embed Pods Frameworks"; 366 | outputFileListPaths = ( 367 | "${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | shellPath = /bin/sh; 371 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks.sh\"\n"; 372 | showEnvVarsInLog = 0; 373 | }; 374 | F3D2B8907716502B0FF5881A /* [CP] Embed Pods Frameworks */ = { 375 | isa = PBXShellScriptBuildPhase; 376 | buildActionMask = 2147483647; 377 | files = ( 378 | ); 379 | inputFileListPaths = ( 380 | "${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks-${CONFIGURATION}-input-files.xcfilelist", 381 | ); 382 | name = "[CP] Embed Pods Frameworks"; 383 | outputFileListPaths = ( 384 | "${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks-${CONFIGURATION}-output-files.xcfilelist", 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | shellPath = /bin/sh; 388 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks.sh\"\n"; 389 | showEnvVarsInLog = 0; 390 | }; 391 | /* End PBXShellScriptBuildPhase section */ 392 | 393 | /* Begin PBXSourcesBuildPhase section */ 394 | 89BCA3402472E51B009C815D /* Sources */ = { 395 | isa = PBXSourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | 89B309CB2475D94300204950 /* BinUtils.swift in Sources */, 399 | 89BCA37B2472F5A5009C815D /* DeskViewController.swift in Sources */, 400 | 89BCA3482472E51B009C815D /* AppDelegate.swift in Sources */, 401 | 896C0E80248BE5D100578941 /* EventMonitor.swift in Sources */, 402 | 892FB916248AC2EA0055E2D1 /* DeskConnect.swift in Sources */, 403 | 89BCA37F24730B27009C815D /* ParticlePeripheral.swift in Sources */, 404 | ); 405 | runOnlyForDeploymentPostprocessing = 0; 406 | }; 407 | 89BCA3552472E51C009C815D /* Sources */ = { 408 | isa = PBXSourcesBuildPhase; 409 | buildActionMask = 2147483647; 410 | files = ( 411 | 89BCA35E2472E51C009C815D /* deskTests.swift in Sources */, 412 | ); 413 | runOnlyForDeploymentPostprocessing = 0; 414 | }; 415 | 89BCA3602472E51C009C815D /* Sources */ = { 416 | isa = PBXSourcesBuildPhase; 417 | buildActionMask = 2147483647; 418 | files = ( 419 | 89BCA3692472E51C009C815D /* deskUITests.swift in Sources */, 420 | ); 421 | runOnlyForDeploymentPostprocessing = 0; 422 | }; 423 | /* End PBXSourcesBuildPhase section */ 424 | 425 | /* Begin PBXTargetDependency section */ 426 | 89BCA35B2472E51C009C815D /* PBXTargetDependency */ = { 427 | isa = PBXTargetDependency; 428 | target = 89BCA3432472E51B009C815D /* desk */; 429 | targetProxy = 89BCA35A2472E51C009C815D /* PBXContainerItemProxy */; 430 | }; 431 | 89BCA3662472E51C009C815D /* PBXTargetDependency */ = { 432 | isa = PBXTargetDependency; 433 | target = 89BCA3432472E51B009C815D /* desk */; 434 | targetProxy = 89BCA3652472E51C009C815D /* PBXContainerItemProxy */; 435 | }; 436 | /* End PBXTargetDependency section */ 437 | 438 | /* Begin PBXVariantGroup section */ 439 | 89BCA3502472E51C009C815D /* Main.storyboard */ = { 440 | isa = PBXVariantGroup; 441 | children = ( 442 | 89BCA3512472E51C009C815D /* Base */, 443 | ); 444 | name = Main.storyboard; 445 | sourceTree = ""; 446 | }; 447 | /* End PBXVariantGroup section */ 448 | 449 | /* Begin XCBuildConfiguration section */ 450 | 89BCA36B2472E51C009C815D /* Debug */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ALWAYS_SEARCH_USER_PATHS = NO; 454 | CLANG_ANALYZER_NONNULL = YES; 455 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 456 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 457 | CLANG_CXX_LIBRARY = "libc++"; 458 | CLANG_ENABLE_MODULES = YES; 459 | CLANG_ENABLE_OBJC_ARC = YES; 460 | CLANG_ENABLE_OBJC_WEAK = YES; 461 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 462 | CLANG_WARN_BOOL_CONVERSION = YES; 463 | CLANG_WARN_COMMA = YES; 464 | CLANG_WARN_CONSTANT_CONVERSION = YES; 465 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 466 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 467 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 468 | CLANG_WARN_EMPTY_BODY = YES; 469 | CLANG_WARN_ENUM_CONVERSION = YES; 470 | CLANG_WARN_INFINITE_RECURSION = YES; 471 | CLANG_WARN_INT_CONVERSION = YES; 472 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 474 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 476 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 477 | CLANG_WARN_STRICT_PROTOTYPES = YES; 478 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 479 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 480 | CLANG_WARN_UNREACHABLE_CODE = YES; 481 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 482 | COPY_PHASE_STRIP = NO; 483 | DEBUG_INFORMATION_FORMAT = dwarf; 484 | ENABLE_STRICT_OBJC_MSGSEND = YES; 485 | ENABLE_TESTABILITY = YES; 486 | GCC_C_LANGUAGE_STANDARD = gnu11; 487 | GCC_DYNAMIC_NO_PIC = NO; 488 | GCC_NO_COMMON_BLOCKS = YES; 489 | GCC_OPTIMIZATION_LEVEL = 0; 490 | GCC_PREPROCESSOR_DEFINITIONS = ( 491 | "DEBUG=1", 492 | "$(inherited)", 493 | ); 494 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 495 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 496 | GCC_WARN_UNDECLARED_SELECTOR = YES; 497 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 498 | GCC_WARN_UNUSED_FUNCTION = YES; 499 | GCC_WARN_UNUSED_VARIABLE = YES; 500 | MACOSX_DEPLOYMENT_TARGET = 10.15; 501 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 502 | MTL_FAST_MATH = YES; 503 | ONLY_ACTIVE_ARCH = YES; 504 | SDKROOT = macosx; 505 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 506 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 507 | }; 508 | name = Debug; 509 | }; 510 | 89BCA36C2472E51C009C815D /* Release */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | ALWAYS_SEARCH_USER_PATHS = NO; 514 | CLANG_ANALYZER_NONNULL = YES; 515 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 516 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 517 | CLANG_CXX_LIBRARY = "libc++"; 518 | CLANG_ENABLE_MODULES = YES; 519 | CLANG_ENABLE_OBJC_ARC = YES; 520 | CLANG_ENABLE_OBJC_WEAK = YES; 521 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 522 | CLANG_WARN_BOOL_CONVERSION = YES; 523 | CLANG_WARN_COMMA = YES; 524 | CLANG_WARN_CONSTANT_CONVERSION = YES; 525 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 526 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 527 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 528 | CLANG_WARN_EMPTY_BODY = YES; 529 | CLANG_WARN_ENUM_CONVERSION = YES; 530 | CLANG_WARN_INFINITE_RECURSION = YES; 531 | CLANG_WARN_INT_CONVERSION = YES; 532 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 533 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 534 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 535 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 536 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 537 | CLANG_WARN_STRICT_PROTOTYPES = YES; 538 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 539 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 540 | CLANG_WARN_UNREACHABLE_CODE = YES; 541 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 542 | COPY_PHASE_STRIP = NO; 543 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 544 | ENABLE_NS_ASSERTIONS = NO; 545 | ENABLE_STRICT_OBJC_MSGSEND = YES; 546 | GCC_C_LANGUAGE_STANDARD = gnu11; 547 | GCC_NO_COMMON_BLOCKS = YES; 548 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 549 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 550 | GCC_WARN_UNDECLARED_SELECTOR = YES; 551 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 552 | GCC_WARN_UNUSED_FUNCTION = YES; 553 | GCC_WARN_UNUSED_VARIABLE = YES; 554 | MACOSX_DEPLOYMENT_TARGET = 10.15; 555 | MTL_ENABLE_DEBUG_INFO = NO; 556 | MTL_FAST_MATH = YES; 557 | SDKROOT = macosx; 558 | SWIFT_COMPILATION_MODE = wholemodule; 559 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 560 | }; 561 | name = Release; 562 | }; 563 | 89BCA36E2472E51C009C815D /* Debug */ = { 564 | isa = XCBuildConfiguration; 565 | baseConfigurationReference = 9BA38D4D1449B8631FE186B6 /* Pods-desk.debug.xcconfig */; 566 | buildSettings = { 567 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 568 | CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; 569 | CODE_SIGN_ENTITLEMENTS = desk/desk.entitlements; 570 | CODE_SIGN_STYLE = Automatic; 571 | COMBINE_HIDPI_IMAGES = YES; 572 | DEVELOPMENT_TEAM = R3V78XR6AA; 573 | ENABLE_HARDENED_RUNTIME = YES; 574 | ENABLE_PREVIEWS = YES; 575 | INFOPLIST_FILE = desk/Info.plist; 576 | LD_RUNPATH_SEARCH_PATHS = ( 577 | "$(inherited)", 578 | "@executable_path/../Frameworks", 579 | ); 580 | MACOSX_DEPLOYMENT_TARGET = 10.15; 581 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.desk; 582 | PRODUCT_NAME = "$(TARGET_NAME)"; 583 | SWIFT_VERSION = 5.0; 584 | }; 585 | name = Debug; 586 | }; 587 | 89BCA36F2472E51C009C815D /* Release */ = { 588 | isa = XCBuildConfiguration; 589 | baseConfigurationReference = A9ADE9ECDE2FB81BC7FBA812 /* Pods-desk.release.xcconfig */; 590 | buildSettings = { 591 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 592 | CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; 593 | CODE_SIGN_ENTITLEMENTS = desk/desk.entitlements; 594 | CODE_SIGN_STYLE = Automatic; 595 | COMBINE_HIDPI_IMAGES = YES; 596 | DEVELOPMENT_TEAM = R3V78XR6AA; 597 | ENABLE_HARDENED_RUNTIME = YES; 598 | ENABLE_PREVIEWS = YES; 599 | INFOPLIST_FILE = desk/Info.plist; 600 | LD_RUNPATH_SEARCH_PATHS = ( 601 | "$(inherited)", 602 | "@executable_path/../Frameworks", 603 | ); 604 | MACOSX_DEPLOYMENT_TARGET = 10.15; 605 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.desk; 606 | PRODUCT_NAME = "$(TARGET_NAME)"; 607 | SWIFT_VERSION = 5.0; 608 | }; 609 | name = Release; 610 | }; 611 | 89BCA3712472E51C009C815D /* Debug */ = { 612 | isa = XCBuildConfiguration; 613 | baseConfigurationReference = 4DF47CCA12A7BD5C2D7AA387 /* Pods-deskTests.debug.xcconfig */; 614 | buildSettings = { 615 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 616 | BUNDLE_LOADER = "$(TEST_HOST)"; 617 | CODE_SIGN_STYLE = Automatic; 618 | COMBINE_HIDPI_IMAGES = YES; 619 | DEVELOPMENT_TEAM = R3V78XR6AA; 620 | INFOPLIST_FILE = deskTests/Info.plist; 621 | LD_RUNPATH_SEARCH_PATHS = ( 622 | "$(inherited)", 623 | "@executable_path/../Frameworks", 624 | "@loader_path/../Frameworks", 625 | ); 626 | MACOSX_DEPLOYMENT_TARGET = 10.15; 627 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskTests; 628 | PRODUCT_NAME = "$(TARGET_NAME)"; 629 | SWIFT_VERSION = 5.0; 630 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desk.app/Contents/MacOS/desk"; 631 | }; 632 | name = Debug; 633 | }; 634 | 89BCA3722472E51C009C815D /* Release */ = { 635 | isa = XCBuildConfiguration; 636 | baseConfigurationReference = 6268E187AE56B61FCA99580A /* Pods-deskTests.release.xcconfig */; 637 | buildSettings = { 638 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 639 | BUNDLE_LOADER = "$(TEST_HOST)"; 640 | CODE_SIGN_STYLE = Automatic; 641 | COMBINE_HIDPI_IMAGES = YES; 642 | DEVELOPMENT_TEAM = R3V78XR6AA; 643 | INFOPLIST_FILE = deskTests/Info.plist; 644 | LD_RUNPATH_SEARCH_PATHS = ( 645 | "$(inherited)", 646 | "@executable_path/../Frameworks", 647 | "@loader_path/../Frameworks", 648 | ); 649 | MACOSX_DEPLOYMENT_TARGET = 10.15; 650 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskTests; 651 | PRODUCT_NAME = "$(TARGET_NAME)"; 652 | SWIFT_VERSION = 5.0; 653 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desk.app/Contents/MacOS/desk"; 654 | }; 655 | name = Release; 656 | }; 657 | 89BCA3742472E51C009C815D /* Debug */ = { 658 | isa = XCBuildConfiguration; 659 | buildSettings = { 660 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 661 | CODE_SIGN_STYLE = Automatic; 662 | COMBINE_HIDPI_IMAGES = YES; 663 | DEVELOPMENT_TEAM = R3V78XR6AA; 664 | INFOPLIST_FILE = deskUITests/Info.plist; 665 | LD_RUNPATH_SEARCH_PATHS = ( 666 | "$(inherited)", 667 | "@executable_path/../Frameworks", 668 | "@loader_path/../Frameworks", 669 | ); 670 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskUITests; 671 | PRODUCT_NAME = "$(TARGET_NAME)"; 672 | SWIFT_VERSION = 5.0; 673 | TEST_TARGET_NAME = desk; 674 | }; 675 | name = Debug; 676 | }; 677 | 89BCA3752472E51C009C815D /* Release */ = { 678 | isa = XCBuildConfiguration; 679 | buildSettings = { 680 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 681 | CODE_SIGN_STYLE = Automatic; 682 | COMBINE_HIDPI_IMAGES = YES; 683 | DEVELOPMENT_TEAM = R3V78XR6AA; 684 | INFOPLIST_FILE = deskUITests/Info.plist; 685 | LD_RUNPATH_SEARCH_PATHS = ( 686 | "$(inherited)", 687 | "@executable_path/../Frameworks", 688 | "@loader_path/../Frameworks", 689 | ); 690 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskUITests; 691 | PRODUCT_NAME = "$(TARGET_NAME)"; 692 | SWIFT_VERSION = 5.0; 693 | TEST_TARGET_NAME = desk; 694 | }; 695 | name = Release; 696 | }; 697 | /* End XCBuildConfiguration section */ 698 | 699 | /* Begin XCConfigurationList section */ 700 | 89BCA33F2472E51B009C815D /* Build configuration list for PBXProject "desk" */ = { 701 | isa = XCConfigurationList; 702 | buildConfigurations = ( 703 | 89BCA36B2472E51C009C815D /* Debug */, 704 | 89BCA36C2472E51C009C815D /* Release */, 705 | ); 706 | defaultConfigurationIsVisible = 0; 707 | defaultConfigurationName = Release; 708 | }; 709 | 89BCA36D2472E51C009C815D /* Build configuration list for PBXNativeTarget "desk" */ = { 710 | isa = XCConfigurationList; 711 | buildConfigurations = ( 712 | 89BCA36E2472E51C009C815D /* Debug */, 713 | 89BCA36F2472E51C009C815D /* Release */, 714 | ); 715 | defaultConfigurationIsVisible = 0; 716 | defaultConfigurationName = Release; 717 | }; 718 | 89BCA3702472E51C009C815D /* Build configuration list for PBXNativeTarget "deskTests" */ = { 719 | isa = XCConfigurationList; 720 | buildConfigurations = ( 721 | 89BCA3712472E51C009C815D /* Debug */, 722 | 89BCA3722472E51C009C815D /* Release */, 723 | ); 724 | defaultConfigurationIsVisible = 0; 725 | defaultConfigurationName = Release; 726 | }; 727 | 89BCA3732472E51C009C815D /* Build configuration list for PBXNativeTarget "deskUITests" */ = { 728 | isa = XCConfigurationList; 729 | buildConfigurations = ( 730 | 89BCA3742472E51C009C815D /* Debug */, 731 | 89BCA3752472E51C009C815D /* Release */, 732 | ); 733 | defaultConfigurationIsVisible = 0; 734 | defaultConfigurationName = Release; 735 | }; 736 | /* End XCConfigurationList section */ 737 | }; 738 | rootObject = 89BCA33C2472E51B009C815D /* Project object */; 739 | } 740 | -------------------------------------------------------------------------------- /desk/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Default 530 | 531 | 532 | 533 | 534 | 535 | 536 | Left to Right 537 | 538 | 539 | 540 | 541 | 542 | 543 | Right to Left 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 729 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 760 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 907 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | --------------------------------------------------------------------------------