diff --git a/OctoPod Mac/AppDelegate.swift b/OctoPod Mac/AppDelegate.swift new file mode 100644 index 00000000..e3f8269f --- /dev/null +++ b/OctoPod Mac/AppDelegate.swift @@ -0,0 +1,149 @@ +// +// AppDelegate.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 6/26/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Cocoa +import UserNotifications + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + lazy var preferencesWindowController = { () -> NSWindowController in + let storyBoard = NSStoryboard(name: "Main", bundle: nil) + guard let pwc = storyBoard.instantiateController(withIdentifier: "PreferencesWindowController") as? NSWindowController else{ + fatalError("Unable to instantiate view controller") + } + (pwc.contentViewController as! PreferencesViewController).delegate = popoverViewController + return pwc + }() + + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) + + lazy var popoverViewController = { () -> PopoverViewController in + let storyBoard = NSStoryboard(name: "Main", bundle: nil) + guard let vc = storyBoard.instantiateController(withIdentifier: "PopoverViewController") as? PopoverViewController + else{ + fatalError("Unable to instantiate view controller") + } + return vc + }() + + func applicationDidFinishLaunching(_ aNotification: Notification) { + if #available(OSX 10.14, *) { + registerForPushNotifications() + } else { + // Fallback on earlier versions + } + let itemImage = NSImage(named: NSImage.Name("Status-Icon")) + itemImage?.size = NSMakeSize(20.0, 20.0); + itemImage?.isTemplate = true + statusItem.button?.image = itemImage + statusItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) + statusItem.button?.target = self + statusItem.button?.action = #selector(statusBarButtonClicked) + statusItem.button?.toolTip = "OctoPod for OctoPrint" + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + lazy var appConfiguration: AppConfiguration = { + return AppConfiguration() + }() + func showQuickView() { + guard let button = statusItem.button else { + fatalError("Cannot get status button") + } + let popoverView = NSPopover() + popoverView.contentViewController = popoverViewController + popoverView.behavior = .transient + popoverView.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) + } + + @objc func showPreferences() { + preferencesWindowController.showWindow(self) + } + + lazy var menu = getMenu() + private func getMenu() -> NSMenu { + let menu = NSMenu() + menu.addItem(NSMenuItem(title: "About OctoPod Mac", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Preferences", action: #selector(showPreferences), keyEquivalent: "P")) + menu.addItem(NSMenuItem.separator()) + menu.addItem(NSMenuItem(title: "Quit OctoPod Mac", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) + return menu + } + + @objc func statusBarButtonClicked(sender: NSStatusBarButton) { + let event = NSApp.currentEvent! + if event.type == NSEvent.EventType.rightMouseUp { + NSMenu.popUpContextMenu(menu, with: event, for: sender) + } else { + showQuickView() + } + } + @available(OSX 10.14, *) + func registerForPushNotifications() { + UNUserNotificationCenter.current() // 1 + .requestAuthorization(options: [.alert, .sound, .badge]) { // 2 + granted, error in + guard granted else { return } + self.getNotificationSettings() + } + } + + @available(OSX 10.14, *) + func getNotificationSettings() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + NSLog("Notification settings: \(settings)") + } + } + + lazy var printerManager: PrinterManager? = { + let context = persistentContainer.viewContext + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + var printerManager = PrinterManager() + printerManager.managedObjectContext = context + return printerManager + }() + + // MARK: - Core Data stack + + lazy var persistentContainer: SharedPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let container = SharedPersistentContainer(name: "OctoPod") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + @IBAction func preferencesFromMainMenuClicked(_ sender: Any) { + showPreferences() + } + +} + + diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/1024.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/1024.png new file mode 100644 index 00000000..e4a391d4 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/1024.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/128.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/128.png new file mode 100644 index 00000000..1988944a Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/128.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/16.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/16.png new file mode 100644 index 00000000..92af0385 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/16.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256-1.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256-1.png new file mode 100644 index 00000000..fe2e5051 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256-1.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256.png new file mode 100644 index 00000000..fe2e5051 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/256.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32-1.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32-1.png new file mode 100644 index 00000000..fc5ab923 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32-1.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32.png new file mode 100644 index 00000000..fc5ab923 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/32.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512-1.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512-1.png new file mode 100644 index 00000000..1b92ede9 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512-1.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512.png new file mode 100644 index 00000000..1b92ede9 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/512.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/64.png b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/64.png new file mode 100644 index 00000000..58664699 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/64.png differ diff --git a/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/Contents.json b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/Contents.json new file mode 100644 index 00000000..d12465d0 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/AppIcon-Mac.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512-1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Contents.json b/OctoPod Mac/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Octopod.imageset/128.png b/OctoPod Mac/Assets.xcassets/Octopod.imageset/128.png new file mode 100644 index 00000000..1988944a Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Octopod.imageset/128.png differ diff --git a/OctoPod Mac/Assets.xcassets/Octopod.imageset/256.png b/OctoPod Mac/Assets.xcassets/Octopod.imageset/256.png new file mode 100644 index 00000000..fe2e5051 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Octopod.imageset/256.png differ diff --git a/OctoPod Mac/Assets.xcassets/Octopod.imageset/512.png b/OctoPod Mac/Assets.xcassets/Octopod.imageset/512.png new file mode 100644 index 00000000..1b92ede9 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Octopod.imageset/512.png differ diff --git a/OctoPod Mac/Assets.xcassets/Octopod.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Octopod.imageset/Contents.json new file mode 100644 index 00000000..868bfdac --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Octopod.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "128.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "256.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "512.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/Contents.json new file mode 100644 index 00000000..87bf5371 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "cancel_32.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cancel_64.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_32.png b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_32.png new file mode 100644 index 00000000..8895937a Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_32.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_64.png b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_64.png new file mode 100644 index 00000000..18664dd8 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Cancel.imageset/cancel_64.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Contents.json b/OctoPod Mac/Assets.xcassets/Print Job/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Print Job/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/Contents.json new file mode 100644 index 00000000..27bffe85 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "pause_32.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pause_64.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_32.png b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_32.png new file mode 100644 index 00000000..eaf01f45 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_32.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_64.png b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_64.png new file mode 100644 index 00000000..0e43da11 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Pause.imageset/pause_64.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/Contents.json new file mode 100644 index 00000000..04fde351 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "play_32.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "play_64.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_32.png b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_32.png new file mode 100644 index 00000000..bbdc08ef Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_32.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_64.png b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_64.png new file mode 100644 index 00000000..c3acb64c Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Print.imageset/play_64.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/Contents.json new file mode 100644 index 00000000..4da36af4 --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "restart_32.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "restart_64.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_32.png b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_32.png new file mode 100644 index 00000000..e98f6ddd Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_32.png differ diff --git a/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_64.png b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_64.png new file mode 100644 index 00000000..8b0df4bc Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Print Job/Restart.imageset/restart_64.png differ diff --git a/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Contents.json b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Contents.json new file mode 100644 index 00000000..bdd46e8d --- /dev/null +++ b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Octopod-48x48.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Octopod-48x48-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48-1.png b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48-1.png new file mode 100644 index 00000000..26e2b090 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48-1.png differ diff --git a/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48.png b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48.png new file mode 100644 index 00000000..26e2b090 Binary files /dev/null and b/OctoPod Mac/Assets.xcassets/Status-Icon.imageset/Octopod-48x48.png differ diff --git a/OctoPod Mac/CameraImageView.swift b/OctoPod Mac/CameraImageView.swift new file mode 100644 index 00000000..9aa7e7ed --- /dev/null +++ b/OctoPod Mac/CameraImageView.swift @@ -0,0 +1,89 @@ +// +// CameraImageView.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 6/29/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Foundation +import Cocoa +import Carbon.HIToolbox +class CameraImageView: NSImageView { + private var currentOrientation = 0 + private lazy var printerStatusOverlay = overlayableLabel() + private var extruderTempDisplay = 0.0 + private var bedTempDisplay = 0.0 + private var progress = "-" + private var timeLeft = "-" + private var progressCompletionDisplay = 0.0 + override func mouseUp(with event: NSEvent) { + if event.clickCount == 2{ + toggleFullScreen() + } + } + override func rotate(byDegrees angle: CGFloat) { + DispatchQueue.main.async { + //restore from existing rotation + super.rotate(byDegrees: CGFloat(-self.currentOrientation)) + //apply new rotation + super.rotate(byDegrees: CGFloat(angle)) + } + currentOrientation = Int(angle) + } + override func keyDown(with event: NSEvent) { + if event.keyCode == kVK_Escape{ + toggleFullScreen() + } + } + private func toggleFullScreen(){ + if !self.isInFullScreenMode{ + self.enterFullScreenMode(NSScreen.main!, withOptions: nil) + //overlayPrinterDetails(visible: true) + + }else{ + self.exitFullScreenMode(options: nil) + (NSApp.delegate as! AppDelegate).showQuickView() + //overlayPrinterDetails(visible: false) + } + } + func setPrinerDetails(printerStatus:String?,actualExtruderTemp:Double?,targetExtruderTemp:Double?,actualBedTemp:Double?,targetBedTemp:Double?,progressPrintTime:Int?,progressPrintTimeLeft:Int?, progressCompletion:Double?){ + extruderTempDisplay = actualExtruderTemp ?? extruderTempDisplay + bedTempDisplay = actualBedTemp ?? bedTempDisplay + let progressPrintTimeLeftDouble = Double(progressPrintTimeLeft ?? 0) + timeLeft = UIUtils.secondsToEstimatedPrintTime(seconds: progressPrintTimeLeftDouble) + progressCompletionDisplay = progressCompletion?.round(to: 1) ?? progressCompletionDisplay + printerStatusOverlay.stringValue = "Extruder:\(extruderTempDisplay) Bed:\(bedTempDisplay) Complete: \(progressCompletionDisplay)%" + } + func overlayPrinterDetails(visible:Bool) { + if(visible){ + let x = 20 + let y = 20 + printerStatusOverlay.setFrameOrigin(CGPoint(x: x, y: y)) + + + self.addSubview(printerStatusOverlay) + } + else{ + printerStatusOverlay.removeFromSuperview() + } + } + + private func overlayableLabel() -> NSTextField { + let screenWidth = NSScreen.main?.frame.width ?? 100 + let label = NSTextField(frame: NSMakeRect(0,0,screenWidth - 200,25)) + label.isEditable = false + label.isSelectable = false + label.textColor = .labelColor + let backgroundColor = NSColor.controlBackgroundColor + label.backgroundColor = backgroundColor.withAlphaComponent(CGFloat(0.5)) + label.drawsBackground = true + label.isBezeled = false + label.alignment = .natural + label.font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: label.controlSize)) + label.lineBreakMode = .byClipping + label.cell?.isScrollable = false + label.cell?.wraps = false + return label + } +} diff --git a/OctoPod Mac/Extensions.swift b/OctoPod Mac/Extensions.swift new file mode 100644 index 00000000..97e5d115 --- /dev/null +++ b/OctoPod Mac/Extensions.swift @@ -0,0 +1,16 @@ +// +// Extensions.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 6/30/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Foundation + +extension Double { + func round(to places: Int) -> Double { + let divisor = pow(10.0, Double(places)) + return (self * divisor).rounded() / divisor + } +} diff --git a/OctoPod Mac/Info.plist b/OctoPod Mac/Info.plist new file mode 100644 index 00000000..1fc17e1d --- /dev/null +++ b/OctoPod Mac/Info.plist @@ -0,0 +1,45 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.utilities + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + + NSHumanReadableCopyright + Copyright © 2020 Gaston Dombiak. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/OctoPod Mac/MJPEG Streaming/MjpegStreamingController.swift b/OctoPod Mac/MJPEG Streaming/MjpegStreamingController.swift new file mode 100644 index 00000000..66d398d6 --- /dev/null +++ b/OctoPod Mac/MJPEG Streaming/MjpegStreamingController.swift @@ -0,0 +1,174 @@ +// +// MjpegStreamingController.swift +// MjpegStreamingKit +// +// Created by Stefano Vettor on 28/03/16. +// Copyright © 2016 Stefano Vettor. All rights reserved. +// +// Modified for better error handling +// Modified to allow image rotation +// + +import Cocoa + +open class MjpegStreamingController: NSObject, URLSessionDataDelegate { + + fileprivate enum Status { + case stopped + case loading + case playing + } + + fileprivate var receivedData: NSMutableData? + fileprivate var dataTask: URLSessionDataTask? + fileprivate var session: Foundation.URLSession! + fileprivate var status: Status = .stopped + + open var authenticationHandler: ((URLAuthenticationChallenge) -> (Foundation.URLSession.AuthChallengeDisposition, URLCredential?))? + open var authenticationFailedHandler: (()->Void)? + open var didStartLoading: (()->Void)? + open var didFinishLoading: (()->Void)? + open var didFinishWithErrors: ((Error)->Void)? + open var didFinishWithHTTPErrors: ((HTTPURLResponse)->Void)? + open var didFetchImage: ((NSImage)->Void)? + open var didRenderImage: ((NSImage)->Void)? + open var contentURL: URL? + open var imageView: NSImageView? + + public override init() { + super.init() + self.session = Foundation.URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) + } + + public convenience init(imageView: NSImageView) { + self.init() + self.imageView = imageView + } + + public convenience init(imageView: NSImageView, contentURL: URL) { + self.init(imageView: imageView) + self.contentURL = contentURL + } + + deinit { + dataTask?.cancel() + } + + open func play(url: URL){ + if status == .playing || status == .loading { + stop() + } + contentURL = url + play() + } + + open func play() { + guard let url = contentURL , status == .stopped else { + return + } + + status = .loading + executeBlock { self.didStartLoading?() } + + receivedData = NSMutableData() + let request = URLRequest(url: url) + dataTask = session.dataTask(with: request) + dataTask?.resume() + } + + open func stop(){ + status = .stopped + dataTask?.cancel() + } + + // MARK: - NSURLSessionDataDelegate + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + if let httpResponse = response as? HTTPURLResponse, let httpErrorHandler = didFinishWithHTTPErrors { + if httpResponse.statusCode == 404 || httpResponse.statusCode > 500 { + httpErrorHandler(httpResponse) + return + } + } + if let imageData = receivedData , imageData.length > 0, + let receivedImage = NSImage(data: imageData as Data) { + + // I'm creating the NSImage before performing didFinishLoading to minimize the interval + // between the actions done by didFinishLoading and the appearance of the first image + var firstTimeImage = false + if status == .loading { + firstTimeImage = true + status = .playing + executeBlock { self.didFinishLoading?() } + } + + executeBlock { + self.imageView?.image = receivedImage + self.didFetchImage?(receivedImage) + } + + if firstTimeImage { + executeBlock { self.didRenderImage?(receivedImage) } + } + } + + receivedData = NSMutableData() + completionHandler(.allow) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + receivedData?.append(data) + } + + // MARK: - NSURLSessionTaskDelegate + + open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + var credential: URLCredential? + var disposition: Foundation.URLSession.AuthChallengeDisposition = .performDefaultHandling + + if challenge.previousFailureCount > 0 { + // User credentials are incorrect + if let onAuthenticationFailed = authenticationFailedHandler { + onAuthenticationFailed() + } + // Cancel authentication flow + completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) + } else { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let trust = challenge.protectionSpace.serverTrust { + credential = URLCredential(trust: trust) + disposition = .useCredential + } + } else if let onAuthentication = authenticationHandler { + (disposition, credential) = onAuthentication(challenge) + } + + completionHandler(disposition, credential) + } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let onError = didFinishWithErrors, let error = error { + if let nsError = error as NSError? { + if nsError.code == NSURLErrorCancelled { + // Do nothing + // Happens when view is disappearing and we cancelled + // ongoing HTTP request + return + } + } + onError(error) + } + } + + // MARK: - Private function + + fileprivate func executeBlock(block: @escaping () -> Void ) { + if imageView == nil { + block() + } else { + DispatchQueue.main.async { block() } + } + } +} diff --git a/OctoPod Mac/MJPEG Streaming/MjpegStreamingKit.h b/OctoPod Mac/MJPEG Streaming/MjpegStreamingKit.h new file mode 100644 index 00000000..7152050b --- /dev/null +++ b/OctoPod Mac/MJPEG Streaming/MjpegStreamingKit.h @@ -0,0 +1,19 @@ +// +// MjpegStreamingKit.h +// MjpegStreamingKit +// +// Created by Stefano Vettor on 28/03/16. +// Copyright © 2016 Stefano Vettor. All rights reserved. +// + +#import + +//! Project version number for MjpegStreamingKit. +FOUNDATION_EXPORT double MjpegStreamingKitVersionNumber; + +//! Project version string for MjpegStreamingKit. +FOUNDATION_EXPORT const unsigned char MjpegStreamingKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/OctoPod Mac/Octopod_Mac.entitlements b/OctoPod Mac/Octopod_Mac.entitlements new file mode 100644 index 00000000..1da788e4 --- /dev/null +++ b/OctoPod Mac/Octopod_Mac.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.aps-environment + development + + diff --git a/OctoPod Mac/PopoverViewController.swift b/OctoPod Mac/PopoverViewController.swift new file mode 100644 index 00000000..60c89e0c --- /dev/null +++ b/OctoPod Mac/PopoverViewController.swift @@ -0,0 +1,448 @@ +// +// ViewController.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 6/27/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Cocoa +import CoreData + +class PopoverViewController: NSViewController, OctoPrintClientDelegate, PreferencesDelegate, OctoPrintSettingsDelegate { + @IBOutlet weak var cameraImageView: CameraImageView! + @IBOutlet weak var connectButton: NSButton! + + @IBOutlet weak var printerStatusLabel: NSTextField! + + @IBOutlet weak var printerStatusValue: NSTextField! + + @IBOutlet weak var actualExtruderTempValue: NSTextField! + @IBOutlet weak var targetExtruderTempValue: NSTextField! + @IBOutlet weak var actualBedTempValue: NSTextField! + @IBOutlet weak var targetBedTempValue: NSTextField! + + @IBOutlet weak var progressPrintTimeValue: NSTextField! + @IBOutlet weak var progressPrintTimeLeftValue: NSTextField! + @IBOutlet weak var progressPrintCompletionValue: NSTextField! + + @IBOutlet weak var progressPercentValue: NSTextField! + @IBOutlet weak var progressBar: NSProgressIndicator! + + @IBOutlet weak var cancelButton: NSButton! + @IBOutlet weak var pauseResumeButton: NSButton! + @IBOutlet weak var extruderTempProgressBar: NSProgressIndicator! + @IBOutlet weak var bedTempProgressBar: NSProgressIndicator! + + @IBOutlet weak var printerNameLabel: NSTextField! + + @IBOutlet weak var octoprintWebButton: NSButton! + + private var serverConnected = false + private var printerConnected: Bool? + private var isPrinting = false + private var isPaused = false + var streamingController: MjpegStreamingController? + private var lastEventReceivedAt = NSDate().timeIntervalSince1970 + + let printerManager: PrinterManager = { return (NSApp.delegate as! AppDelegate).printerManager! }() + + + + lazy var octoPrintClient: OctoPrintClient = { + let octoPrintClient = OctoPrintClient(printerManager: self.printerManager) + octoPrintClient.delegates.append(self) + octoPrintClient.octoPrintSettingsDelegates.append(self) + return octoPrintClient + }() + + + + override func viewDidLoad() { + super.viewDidLoad() + DispatchQueue.main.async { + self.bedTempProgressBar.doubleValue = 0.0 + self.extruderTempProgressBar.doubleValue = 0.0 + } + connectToServer() + _ = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { timer in + if(!self.serverConnected){ + NSLog("Server is not in connect state for \(NSDate().timeIntervalSince1970-self.lastEventReceivedAt) seconds") + self.onStaleReconnect(checkConnection: false) + }else{ + NSLog("Server is conneccted state but no data received for \(NSDate().timeIntervalSince1970-self.lastEventReceivedAt) seconds") + self.onStaleReconnect(checkConnection: true) + } + } + } + + private func onStaleReconnect(checkConnection: Bool){ + if let defaultPrinter = printerManager.getDefaultPrinter() + { + if(checkConnection){ + let isConnected = octoPrintClient.webSocketClient?.isConnected(printer: defaultPrinter) + if(!isConnected!){ + disconnectFromServer() + connectToServer() + } + }else{ + disconnectFromServer() + connectToServer() + } + } + } + + fileprivate func octoPrintCameraAbsoluteUrl(hostname: String, streamUrl: String) -> String { + if streamUrl.isEmpty { + // Should never happen but let's be cautious + return hostname + } + if streamUrl.starts(with: "/") { + // Build absolute URL from relative URL + return hostname + streamUrl + } + // streamURL is an absolute URL so return it + return streamUrl + } + func updatePrinterStatusView(printerStatus:String?,actualExtruderTemp:Double?,targetExtruderTemp:Double?,actualBedTemp:Double?,targetBedTemp:Double?,progressPrintTime:Int?,progressPrintTimeLeft:Int?, progressCompletion:Double?){ + actualBedTempValue.doubleValue = actualBedTemp ?? actualBedTempValue.doubleValue + targetBedTempValue.doubleValue = targetBedTemp ?? targetBedTempValue.doubleValue + printerStatusValue.stringValue = printerStatus ?? "Unknown" + actualExtruderTempValue.doubleValue = actualExtruderTemp ?? actualExtruderTempValue.doubleValue + targetExtruderTempValue.doubleValue = targetExtruderTemp ?? targetExtruderTempValue.doubleValue + progressBar.doubleValue = progressCompletion ?? progressBar.doubleValue + progressPercentValue.doubleValue = progressCompletion?.round(to: 1) ?? progressPercentValue.doubleValue + let progressPrintTimeLeftDouble = Double(progressPrintTimeLeft ?? 0) + progressPrintTimeValue.stringValue = UIUtils.secondsToPrintTime(seconds: progressPrintTime ?? 0) + progressPrintTimeLeftValue.stringValue = UIUtils.secondsToEstimatedPrintTime(seconds: progressPrintTimeLeftDouble) + progressPrintCompletionValue.stringValue = UIUtils.secondsToETA(seconds: progressPrintTimeLeft ?? 0) + progressBar.appearance = NSAppearance(named: .vibrantLight) + + bedTempProgressBar.minValue = 10 + bedTempProgressBar.maxValue = targetBedTempValue.doubleValue < 1.0 ? 100.0 : targetBedTempValue.doubleValue + bedTempProgressBar.doubleValue = actualBedTempValue.doubleValue + bedTempProgressBar.appearance = NSAppearance(named: .vibrantLight) + + extruderTempProgressBar.minValue = 10 + extruderTempProgressBar.maxValue = targetExtruderTempValue.doubleValue < 1.0 ? 250.0 : targetExtruderTempValue.doubleValue + extruderTempProgressBar.doubleValue = actualExtruderTempValue.doubleValue + extruderTempProgressBar.appearance = NSAppearance(named: .vibrantLight) + } + + override var representedObject: Any? { + didSet { + // Update the view, if already loaded. + } + } + + + fileprivate func updateConnectButton(printerConnected: Bool, assumption: Bool) { + DispatchQueue.main.async { + if !printerConnected { + self.printerConnected = false + self.connectButton.title = NSLocalizedString("Connect", comment: "") + } else { + self.printerConnected = true + self.connectButton.title = NSLocalizedString("Disconnect", comment: "") + } + // Only enable button if we are sure about connection state + self.connectButton.isEnabled = !assumption + } + } + + fileprivate func connectToServer() { + if let defaultPrinter = printerManager.getDefaultPrinter() + { + NSLog("Connecting to server") + DispatchQueue.main.async { + self.octoprintWebButton.isEnabled = !defaultPrinter.hostname.isEmpty + } + if !self.serverConnected{ + if (!UIUtils.isValidURL(urlString: defaultPrinter.hostname)){ + UIUtils.showAlert(title: "Invalid URL", message: "\(defaultPrinter.hostname) is not a valid URL. A valid URL should start with http:// or https://") + return + } + octoPrintClient.connectToServer(printer : defaultPrinter) + printerNameLabel.stringValue = defaultPrinter.name + + } + } + + } + private func connectCamera(printer:Printer){ + if(serverConnected){ + NSLog("Connecting camera") + if let imageView = self.cameraImageView { + DispatchQueue.main.async { + imageView.isHidden = false + } + let degrees = [0,90,180,270] + imageView.rotate(byDegrees: CGFloat(degrees[Int(printer.cameraOrientation)])) + streamingController = MjpegStreamingController(imageView: imageView) + if let defaultPrinterStreamURLString = printer.streamUrl{ + let streamUrl = URL(string:octoPrintCameraAbsoluteUrl(hostname:printer.hostname, streamUrl: defaultPrinterStreamURLString)) + streamingController?.play(url: streamUrl!) + + } + } + } + + } + private func disconnectFromServer(){ + octoPrintClient.disconnectFromServer() + NSLog("Disconnecting from server") + serverConnected = false + streamingController?.stop() + DispatchQueue.main.async { + self.updatePrinterStatusView( + printerStatus: "???" , + actualExtruderTemp: 0.0 , + targetExtruderTemp: 0.0 , + actualBedTemp: 0.0 , + targetBedTemp: 0.0, + progressPrintTime: 0, + progressPrintTimeLeft: 0, + progressCompletion: 0.0 + ) + } + self.cameraImageView.isHidden = true + self.updateConnectButton(printerConnected: false, assumption: true) + } + + func notificationAboutToConnectToServer() { + //NSLog("*************") + } + + + func printerStateUpdated(event: CurrentStateEvent) { + lastEventReceivedAt = NSDate().timeIntervalSince1970 + DispatchQueue.main.async { + //Do UI Code here. + if let closed = event.closedOrError { + self.updateConnectButton(printerConnected: !closed, assumption: false) + } + } + if let isPrinting = event.printing { + self.isPrinting = event.printing! + DispatchQueue.main.async { + if(isPrinting){ + self.cancelButton.isEnabled = true + self.pauseResumeButton.isEnabled = true + if #available(OSX 10.14, *) { + self.cancelButton.contentTintColor = .systemRed + self.pauseResumeButton.contentTintColor = .systemYellow + } else { + // Fallback on earlier versions + } + self.pauseResumeButton.title = "Pause" + }else{ + self.cancelButton.isEnabled = false + self.pauseResumeButton.isEnabled = false + if #available(OSX 10.14, *) { + self.cancelButton.contentTintColor = .systemGray + self.pauseResumeButton.contentTintColor = .systemGray + } else { + // Fallback on earlier versions + } + + } + } + } + if let isPaused = event.paused { + self.isPaused = event.paused! + DispatchQueue.main.async { + if(isPaused){ + self.pauseResumeButton.isEnabled = true + self.cancelButton.isEnabled = true + if #available(OSX 10.14, *) { + self.cancelButton.contentTintColor = .systemRed + self.pauseResumeButton.contentTintColor = .systemGreen + } else { + // Fallback on earlier versions + } + + self.pauseResumeButton.title = "Resume" + } + } + } + if let isPausing = event.pausing { + DispatchQueue.main.async { + if(isPausing){ + self.pauseResumeButton.isEnabled = false + self.cancelButton.isEnabled = false + if #available(OSX 10.14, *) { + self.pauseResumeButton.contentTintColor = .systemGray + self.cancelButton.contentTintColor = .systemGray + } else { + // Fallback on earlier versions + } + self.pauseResumeButton.title = "Resume" + } + } + } + + if let isCancelling = event.cancelling { + DispatchQueue.main.async { + if(isCancelling){ + self.pauseResumeButton.isEnabled = false + self.cancelButton.isEnabled = false + if #available(OSX 10.14, *) { + self.cancelButton.contentTintColor = .systemGray + self.pauseResumeButton.contentTintColor = .systemGray + } else { + // Fallback on earlier versions + } + + self.pauseResumeButton.title = "Pause" + } + + } + } + + self.updatePrinterStatusView( + printerStatus: event.state , + actualExtruderTemp: event.tool0TempActual , + targetExtruderTemp: event.tool0TempTarget , + actualBedTemp: event.bedTempActual , + targetBedTemp: event.bedTempTarget, + progressPrintTime: event.progressPrintTime, + progressPrintTimeLeft: event.progressPrintTimeLeft, + progressCompletion: event.progressCompletion + ) + self.cameraImageView.setPrinerDetails(printerStatus: event.state , + actualExtruderTemp: event.tool0TempActual , + targetExtruderTemp: event.tool0TempTarget , + actualBedTemp: event.bedTempActual , + targetBedTemp: event.bedTempTarget, + progressPrintTime: event.progressPrintTime, + progressPrintTimeLeft: event.progressPrintTimeLeft, + progressCompletion: event.progressCompletion) + + } + + func handleConnectionError(error: Error?, response: HTTPURLResponse) { + NSLog("ERROR - handleConnectionError") + self.serverConnected = false + } + + func websocketConnected() { + NSLog("WS Connected") + lastEventReceivedAt = NSDate().timeIntervalSince1970 + if #available(OSX 10.14, *) { + UIUtils.notifyUser(title: "OctoPod",message: "OctoPod is connected to OctoPrint") + } else { + // Fallback on earlier versions + } + self.serverConnected = true + if let defaultPrinter = printerManager.getDefaultPrinter() + { + connectCamera(printer: defaultPrinter) + } + } + + func websocketConnectionFailed(error: Error) { + NSLog("ERROR - websocketConnectionFailed") + disconnectFromServer() + self.serverConnected = false + connectToServer() + } + func printerAdded(printer: Printer) { + NSLog("printer added") + connectToServer() + } + + func printerDeleted(printer: Printer) { + NSLog("printer deleted") + streamingController?.contentURL = URL(string: "") + streamingController?.stop() + disconnectFromServer() + } + + func printerUpdated(printer: Printer) { + NSLog("printer updated") + disconnectFromServer() + connectToServer() + } + + func cameraOrientationChanged(newOrientation: Int) { + if let imageView = cameraImageView { + imageView.rotate(byDegrees: CGFloat(newOrientation)) + } + } + + func cameraPathChanged(streamUrl: String) { + NSLog("Camera path changed. new URL \(streamUrl)") + if let defaultPrinter = printerManager.getDefaultPrinter() + { + connectCamera(printer: defaultPrinter) + } + } + @IBAction func toggleConnection(_ sender: NSButton) { + if printerConnected! { + self.octoPrintClient.disconnectFromPrinter { (requested: Bool, error: Error?, response: HTTPURLResponse) in + if requested { + DispatchQueue.main.async { + self.actualBedTempValue.doubleValue = 0.0 + self.actualExtruderTempValue.doubleValue = 0.0 + self.targetExtruderTempValue.doubleValue = 0.0 + self.targetBedTempValue.doubleValue = 0.0 + } + } + } + }else{ + self.octoPrintClient.connectToPrinter{ (requested: Bool, error: Error?, response: HTTPURLResponse) in + if requested { + // add stuff + } + } + } + } + + @IBAction func togglePauseResume(_ sender: Any) { + pauseResumeButton.isEnabled = false + if(self.isPaused){ + self.octoPrintClient.resumeCurrentJob { (request:Bool, error: Error?, response:HTTPURLResponse) in + NSLog("Resumed") + } + } + else if(self.isPrinting){ + let sure = UIUtils.showConfirm(title: "Confirmation", message: "Do you really want to pause this print?") + if(!sure){ + return + } + self.octoPrintClient.pauseCurrentJob { (request:Bool, error: Error?, response:HTTPURLResponse) in + NSLog("Paused") + } + } + + } + + @IBAction func cancelNSLog(_ sender: Any) { + let sure = UIUtils.showConfirm(title: "Confirmation", message: "Do you really want to cancel this print?") + if(!sure){ + return + } + cancelButton.isEnabled = false + if(self.isPrinting || self.isPaused){ + self.octoPrintClient.cancelCurrentJob { (request:Bool, error: Error?, response:HTTPURLResponse) in + if #available(OSX 10.14, *) { + UIUtils.notifyUser(title: "OctoPod",message: "Print Cancelled") + } else { + // Fallback on earlier versions + } + NSLog("Cancelled") + } + } + } + @IBAction func openOctoprintWebsite(_ sender: Any) { + if let defaultPrinter = printerManager.getDefaultPrinter() + { + NSWorkspace.shared.open(URL(string: defaultPrinter.hostname)!) + } + } + @IBAction func openPreferences(_ sender: Any) { + (NSApp.delegate as! AppDelegate).showPreferences() + } + +} + diff --git a/OctoPod Mac/PreferencesDelegate.swift b/OctoPod Mac/PreferencesDelegate.swift new file mode 100644 index 00000000..8474e696 --- /dev/null +++ b/OctoPod Mac/PreferencesDelegate.swift @@ -0,0 +1,15 @@ +// +// PreferencesDelegate.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 7/4/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Foundation +protocol PreferencesDelegate: class { + func printerAdded(printer: Printer) + func printerDeleted(printer: Printer) + func printerUpdated(printer: Printer) + func cameraOrientationChanged(newOrientation: Int) +} diff --git a/OctoPod Mac/PreferencesViewController.swift b/OctoPod Mac/PreferencesViewController.swift new file mode 100644 index 00000000..87d1ef93 --- /dev/null +++ b/OctoPod Mac/PreferencesViewController.swift @@ -0,0 +1,155 @@ +// +// PreferencesViewController.swift +// OctoPod Mac +// +// Created by Arijit Banerjee on 7/2/20. +// Copyright © 2020 Gaston Dombiak. All rights reserved. +// + +import Foundation +import Cocoa +import CoreData + +class PreferencesViewController: NSViewController{ + + @IBOutlet var printerNameValue : NSTextField! + @IBOutlet weak var printerHostNameValue: NSTextField! + @IBOutlet weak var octoPrintAPITokenValue: NSTextField! + @IBOutlet weak var ignoreSSLValue: NSButton! + @IBOutlet weak var updatePrinterButton: NSButton! + @IBOutlet weak var cameraOrientationValue: NSPopUpButton! + weak var delegate: PreferencesDelegate? + + let printerManager: PrinterManager = { return (NSApp.delegate as! AppDelegate).printerManager! }() + + override func viewDidLoad() { + + super.viewDidLoad() + if let defaultPrinter = printerManager.getDefaultPrinter() + { + printerHostNameValue.stringValue = defaultPrinter.hostname + octoPrintAPITokenValue.stringValue = defaultPrinter.apiKey + printerNameValue.stringValue = defaultPrinter.name + cameraOrientationValue.selectItem(at: Int(defaultPrinter.cameraOrientation)) + ignoreSSLValue.state = defaultPrinter.ignoreSSLCertValidationError ? NSControl.StateValue.on : NSControl.StateValue.off + } + } + + private func validateInput() -> Bool{ + let apiKey = octoPrintAPITokenValue.stringValue + let hostname = printerHostNameValue.stringValue + let printerName = printerNameValue.stringValue + if(apiKey.isEmpty || hostname.isEmpty || printerName.isEmpty){ + return false + } + if (!UIUtils.isValidURL(urlString: hostname)){ + return false + } + return true + } + + + @IBAction func addUpdatePrinter(_ sender: Any) { + if(!validateInput()){ + UIUtils.showAlert(title: "Validation error", message: "Please check printer details. Fields cannot be empty. Make sure your OctoPrint hostname starts with a protocol(http:// or https://)") + return + } + let apiKey = octoPrintAPITokenValue.stringValue + let hostname = printerHostNameValue.stringValue + let printerName = printerNameValue.stringValue + if let defaultPrinter = printerManager.getDefaultPrinter() + //existing printer + { + defaultPrinter.apiKey = apiKey + defaultPrinter.hostname = hostname + defaultPrinter.name = printerName + defaultPrinter.defaultPrinter = true + defaultPrinter.ignoreSSLCertValidationError = ignoreSSLValue.state == .on ? true: false + printerManager.updatePrinter(defaultPrinter) + if let listener = delegate { + listener.printerUpdated(printer: defaultPrinter) + } + UIUtils.showAlert(title: "Info", message: "Printer updated.") + self.view.window!.windowController!.close() + + }else{ + //new printer + let success = printerManager.addPrinter(name: printerName, hostname: hostname, apiKey: apiKey, username: nil, password: nil, position: 0, iCloudUpdate: false) + if(success){ + if let defaultPrinter = printerManager.getDefaultPrinter(){ + defaultPrinter.ignoreSSLCertValidationError = ignoreSSLValue.state == .on ? true: false + printerManager.updatePrinter(defaultPrinter) + if let listener = delegate { + listener.printerAdded(printer: defaultPrinter) + } + UIUtils.showAlert(title: "Info", message: "Printer added.") + self.view.window!.windowController!.close() + } + else{ + UIUtils.showAlert(title: "Error", message: "Cannot add printer") + + } + } + + } + + } + + @IBAction func resetPrinters(_ sender: Any) { + let confirmation = UIUtils.showConfirm(title: "Are you sure?", message: "This will delete all printers registered to OctoPod.") + + if(confirmation){ + if let defaultPrinter = printerManager.getDefaultPrinter(){ + let newObjectContext = printerManager.newPrivateContext() + printerManager.deleteAllPrinters(context: newObjectContext) + + if let listener = delegate { + listener.printerDeleted(printer: defaultPrinter) + } + printerHostNameValue.stringValue = "" + octoPrintAPITokenValue.stringValue = "" + printerNameValue.stringValue = "" + ignoreSSLValue.state = NSControl.StateValue.off + + UIUtils.showAlert(title: "Done", message: "All Printers Deleted") + } + + } + + } + + @IBAction func hostURLChanged(_ sender: Any) { + let inputURL = printerHostNameValue.stringValue + // Add http protocol to URL if no protocol was specified + if !inputURL.lowercased().starts(with: "http") { + printerHostNameValue.stringValue = "http://" + inputURL + } + //updatePrinterButton.isEnabled = validateInput() + + } + + @IBAction func printerNameChanged(_ sender: Any) { + //updatePrinterButton.isEnabled = validateInput() + } + + @IBAction func apiTokenChanged(_ sender: Any) { + //updatePrinterButton.isEnabled = validateInput() + } + @IBAction func gotoGithub(_ sender: NSButton) { + NSWorkspace.shared.open(URL(string: sender.title)!) + } + + @IBAction func cameraOrientationChanged(_ sender: NSPopUpButton) { + let index = sender.indexOfSelectedItem + let degrees = [0,90,180,270] + if let listener = delegate { + listener.cameraOrientationChanged(newOrientation: degrees[index]) + } + NSLog("New orientation = \(degrees[index])") + if let defaultPrinter = printerManager.getDefaultPrinter(){ + defaultPrinter.cameraOrientation = Int16(index) + printerManager.updatePrinter(defaultPrinter) + } + } + +} diff --git a/OctoPod Mac/en.lproj/Main.storyboard b/OctoPod Mac/en.lproj/Main.storyboard new file mode 100644 index 00000000..c71e81fc --- /dev/null +++ b/OctoPod Mac/en.lproj/Main.storyboard @@ -0,0 +1,723 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OctoPod.xcodeproj/project.pbxproj b/OctoPod.xcodeproj/project.pbxproj index 87d18b76..827c4d31 100644 --- a/OctoPod.xcodeproj/project.pbxproj +++ b/OctoPod.xcodeproj/project.pbxproj @@ -292,6 +292,50 @@ 583827DE22E36B9100784706 /* Palette2ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583827DD22E36B9100784706 /* Palette2ViewController.swift */; }; 583827E122E62D8700784706 /* Palette2PortsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583827E022E62D8700784706 /* Palette2PortsViewController.swift */; }; 5843B64A22E9FA1D001EDBAC /* PingPongHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5843B64922E9FA1D001EDBAC /* PingPongHistoryViewController.swift */; }; + DE1F9E6B24A7299B00A609B7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9E6A24A7299B00A609B7 /* AppDelegate.swift */; }; + DE1F9E6D24A7299B00A609B7 /* PopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9E6C24A7299B00A609B7 /* PopoverViewController.swift */; }; + DE1F9E7824A72D0F00A609B7 /* OctoPrintRESTClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAD2DDB21816D62004A4223 /* OctoPrintRESTClient.swift */; }; + DE1F9E7924A72D3400A609B7 /* CustomControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A935621326AEF0050ED9A /* CustomControl.swift */; }; + DE1F9E7A24A72D3400A609B7 /* ExecuteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A936B213316850050ED9A /* ExecuteControl.swift */; }; + DE1F9E7B24A72D3400A609B7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A935C2132709E0050ED9A /* Container.swift */; }; + DE1F9E7C24A72D3400A609B7 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A935821326B3C0050ED9A /* Command.swift */; }; + DE1F9E7D24A72D3400A609B7 /* Script.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A935E213271680050ED9A /* Script.swift */; }; + DE1F9E7E24A72D3400A609B7 /* ControlInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1A935A21326C630050ED9A /* ControlInput.swift */; }; + DE1F9E7F24A72D3400A609B7 /* SystemCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8AB6A221A7390B00E81CE5 /* SystemCommand.swift */; }; + DE1F9E8024A72D5200A609B7 /* IPPlug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAD2DE421817B23004A4223 /* IPPlug.swift */; }; + DE1F9E8124A72D6B00A609B7 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8B292F20E5549B00296D9D /* HTTPClient.swift */; }; + DE1F9E8224A72D7D00A609B7 /* CancelObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88C3E82193F49500BC84DD /* CancelObject.swift */; }; + DE1F9E8324A72DC500A609B7 /* Palette2Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFB776F23D4D46100ADF315 /* Palette2Utils.swift */; }; + DE1F9E8424A7DCA800A609B7 /* Printer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D82873620E3EBC80035AE0B /* Printer.swift */; }; + DE1F9E8524A7DCC200A609B7 /* EnclosureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6F74512443FB8A006847B6 /* EnclosureInput.swift */; }; + DE1F9E8624A7DCC600A609B7 /* EnclosureOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DB932C924475B7700A154FF /* EnclosureOutput.swift */; }; + DE1F9E8724A7DD4000A609B7 /* CurrentStateEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBAE4A020E69D4900D2137D /* CurrentStateEvent.swift */; }; + DE1F9E8824A7DD5100A609B7 /* PrintFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8873F720F534C400DCE987 /* PrintFile.swift */; }; + DE1F9E8E24A7DEDB00A609B7 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DE1F9E8D24A7DEDB00A609B7 /* Starscream */; }; + DE1F9E9024A7E02300A609B7 /* TempHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D92E6C721213B2400F58BA5 /* TempHistory.swift */; }; + DE1F9E9124A7E0DF00A609B7 /* WebSocketClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBAE4A220E6A02A00D2137D /* WebSocketClientDelegate.swift */; }; + DE1F9E9224A7E4F100A609B7 /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D98C1D920E5D096007BB13F /* WebSocketClient.swift */; }; + DE1F9E9424A7ED7300A609B7 /* OctoPrintClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4508A220F956AD00AC0EC8 /* OctoPrintClientDelegate.swift */; }; + DE1F9E9524A7EF3B00A609B7 /* Plugins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE3FE6C2154B9CE000D7D2A /* Plugins.swift */; }; + DE1F9E9624A7EF4100A609B7 /* OctoPrintPluginsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D071A13212682C1005301B4 /* OctoPrintPluginsDelegate.swift */; }; + DE1F9E9724A7EF4800A609B7 /* PrinterProfilesDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFA806F210E1A8C00B5BFAA /* PrinterProfilesDelegate.swift */; }; + DE1F9E9A24A7F0BC00A609B7 /* Terminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D78BA9E21039D770090D79A /* Terminal.swift */; }; + DE1F9E9D24A7F29800A609B7 /* ComplicationContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5D614723D57C2000D988A2 /* ComplicationContentType.swift */; }; + DE1F9EA124A814D300A609B7 /* PrinterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D82873820E3EC3B0035AE0B /* PrinterManager.swift */; }; + DE1F9EA224A81A5400A609B7 /* OctoPod.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3D45280820E15A370094CCB7 /* OctoPod.xcdatamodeld */; }; + DE1F9EA324A81CC700A609B7 /* SharedPersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4497D321B230BE00736F0F /* SharedPersistentContainer.swift */; }; + DE1F9EA424A8284F00A609B7 /* OctoPrintSettingsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5849E3210CC8720068D87A /* OctoPrintSettingsDelegate.swift */; }; + DE1F9EA624A829F300A609B7 /* OctoPrintClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8B293120E556D400296D9D /* OctoPrintClient.swift */; }; + DE1F9EAB24A82F6B00A609B7 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EA924A82F6B00A609B7 /* AppConfiguration.swift */; }; + DE1F9EAC24A82F6B00A609B7 /* AppConfigurationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EAA24A82F6B00A609B7 /* AppConfigurationDelegate.swift */; }; + DE1F9EBB24A87E3B00A609B7 /* MjpegStreamingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EB924A87E3A00A609B7 /* MjpegStreamingController.swift */; }; + DE1F9EC024A9CB0700A609B7 /* CameraImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EBF24A9CB0700A609B7 /* CameraImageView.swift */; }; + DE1F9EC324ABEAC200A609B7 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D33B7A0213F6A0D002A4BE3 /* UIUtils.swift */; }; + DE1F9EC524ABFB6600A609B7 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EC424ABFB6600A609B7 /* Extensions.swift */; }; + DE1F9EC824AEEF7E00A609B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1F9EC724AEEF7E00A609B7 /* PreferencesViewController.swift */; }; + DEE3256F24B06B45000375BD /* PreferencesDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE3256E24B06B45000375BD /* PreferencesDelegate.swift */; }; + DEE3257424B3CAEF000375BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE1F9E6E24A7299C00A609B7 /* Assets.xcassets */; }; + DEF8D00324B58B6B0027E2A9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE1F9E7024A7299C00A609B7 /* Main.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -726,6 +770,21 @@ CB66535C2160B6C200269730 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = ""; }; CB66535D2160B6C200269730 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; CB66535E2160B6C200269730 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + DE1F9E6824A7299B00A609B7 /* OctoPod Mac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "OctoPod Mac.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + DE1F9E6A24A7299B00A609B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DE1F9E6C24A7299B00A609B7 /* PopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverViewController.swift; sourceTree = ""; }; + DE1F9E6E24A7299C00A609B7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = "OctoPod Mac/Assets.xcassets"; sourceTree = SOURCE_ROOT; }; + DE1F9E7324A7299C00A609B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DE1F9EA924A82F6B00A609B7 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppConfiguration.swift; path = "OctoPod TV/AppConfiguration.swift"; sourceTree = SOURCE_ROOT; }; + DE1F9EAA24A82F6B00A609B7 /* AppConfigurationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppConfigurationDelegate.swift; path = "OctoPod TV/AppConfigurationDelegate.swift"; sourceTree = SOURCE_ROOT; }; + DE1F9EB924A87E3A00A609B7 /* MjpegStreamingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MjpegStreamingController.swift; sourceTree = ""; }; + DE1F9EBA24A87E3A00A609B7 /* MjpegStreamingKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MjpegStreamingKit.h; sourceTree = ""; }; + DE1F9EBE24A9AF4E00A609B7 /* Octopod_Mac.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Octopod_Mac.entitlements; sourceTree = ""; }; + DE1F9EBF24A9CB0700A609B7 /* CameraImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraImageView.swift; sourceTree = ""; }; + DE1F9EC424ABFB6600A609B7 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + DE1F9EC724AEEF7E00A609B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; + DEE3256E24B06B45000375BD /* PreferencesDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDelegate.swift; sourceTree = ""; }; + DEF8D00424B58E4A0027E2A9 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -791,6 +850,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE1F9E6524A7299B00A609B7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE1F9E8E24A7DEDB00A609B7 /* Starscream in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -930,6 +997,7 @@ 3DABB21E22C0044D00F32731 /* OctoPod Notification Extension */, 3D53713D240100FC00B28067 /* OctoPod TV */, 3D537155240100FD00B28067 /* OctoPod TVUITests */, + DE1F9E6924A7299B00A609B7 /* OctoPod Mac */, 3D4527FF20E15A370094CCB7 /* Products */, 2AF93107507B57283430D6F0 /* Frameworks */, 60CAB181B212B9D16A9EA3E1 /* Pods */, @@ -947,6 +1015,7 @@ 3DABB21D22C0044D00F32731 /* OctoPod Notification Extension.appex */, 3D53713C240100FC00B28067 /* OctoPod TV.app */, 3D537152240100FD00B28067 /* OctoPod TVUITests.xctest */, + DE1F9E6824A7299B00A609B7 /* OctoPod Mac.app */, ); name = Products; sourceTree = ""; @@ -1367,6 +1436,35 @@ path = Pods; sourceTree = ""; }; + DE1F9E6924A7299B00A609B7 /* OctoPod Mac */ = { + isa = PBXGroup; + children = ( + DE1F9EB824A87E2100A609B7 /* MJPEG Streaming */, + DE1F9EA924A82F6B00A609B7 /* AppConfiguration.swift */, + DE1F9EAA24A82F6B00A609B7 /* AppConfigurationDelegate.swift */, + DE1F9E6A24A7299B00A609B7 /* AppDelegate.swift */, + DE1F9E6C24A7299B00A609B7 /* PopoverViewController.swift */, + DE1F9E6E24A7299C00A609B7 /* Assets.xcassets */, + DE1F9E7024A7299C00A609B7 /* Main.storyboard */, + DE1F9EBE24A9AF4E00A609B7 /* Octopod_Mac.entitlements */, + DE1F9E7324A7299C00A609B7 /* Info.plist */, + DE1F9EBF24A9CB0700A609B7 /* CameraImageView.swift */, + DE1F9EC424ABFB6600A609B7 /* Extensions.swift */, + DE1F9EC724AEEF7E00A609B7 /* PreferencesViewController.swift */, + DEE3256E24B06B45000375BD /* PreferencesDelegate.swift */, + ); + path = "OctoPod Mac"; + sourceTree = ""; + }; + DE1F9EB824A87E2100A609B7 /* MJPEG Streaming */ = { + isa = PBXGroup; + children = ( + DE1F9EB924A87E3A00A609B7 /* MjpegStreamingController.swift */, + DE1F9EBA24A87E3A00A609B7 /* MjpegStreamingKit.h */, + ); + path = "MJPEG Streaming"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1521,13 +1619,33 @@ productReference = 3DABB21D22C0044D00F32731 /* OctoPod Notification Extension.appex */; productType = "com.apple.product-type.app-extension"; }; + DE1F9E6724A7299B00A609B7 /* OctoPod Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE1F9E7724A7299C00A609B7 /* Build configuration list for PBXNativeTarget "OctoPod Mac" */; + buildPhases = ( + DE1F9E6424A7299B00A609B7 /* Sources */, + DE1F9E6524A7299B00A609B7 /* Frameworks */, + DE1F9E6624A7299B00A609B7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OctoPod Mac"; + packageProductDependencies = ( + DE1F9E8D24A7DEDB00A609B7 /* Starscream */, + ); + productName = "OctoPod Mac"; + productReference = DE1F9E6824A7299B00A609B7 /* OctoPod Mac.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 3D4527F620E15A370094CCB7 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1130; + LastSwiftUpdateCheck = 1150; LastUpgradeCheck = 0940; ORGANIZATIONNAME = "Gaston Dombiak"; TargetAttributes = { @@ -1587,6 +1705,9 @@ 3DABB21C22C0044D00F32731 = { CreatedOnToolsVersion = 10.2.1; }; + DE1F9E6724A7299B00A609B7 = { + CreatedOnToolsVersion = 11.5; + }; }; }; buildConfigurationList = 3D4527F920E15A370094CCB7 /* Build configuration list for PBXProject "OctoPod" */; @@ -1624,6 +1745,7 @@ 3DABB21C22C0044D00F32731 /* OctoPod Notification Extension */, 3D53713B240100FC00B28067 /* OctoPod TV */, 3D537151240100FD00B28067 /* OctoPod TVUITests */, + DE1F9E6724A7299B00A609B7 /* OctoPod Mac */, ); }; /* End PBXProject section */ @@ -1707,6 +1829,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE1F9E6624A7299B00A609B7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEF8D00324B58B6B0027E2A9 /* Main.storyboard in Resources */, + DEE3257424B3CAEF000375BD /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -2011,6 +2142,54 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE1F9E6424A7299B00A609B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE1F9E9624A7EF4100A609B7 /* OctoPrintPluginsDelegate.swift in Sources */, + DE1F9E9224A7E4F100A609B7 /* WebSocketClient.swift in Sources */, + DE1F9E7E24A72D3400A609B7 /* ControlInput.swift in Sources */, + DE1F9E9124A7E0DF00A609B7 /* WebSocketClientDelegate.swift in Sources */, + DE1F9EA224A81A5400A609B7 /* OctoPod.xcdatamodeld in Sources */, + DE1F9E7A24A72D3400A609B7 /* ExecuteControl.swift in Sources */, + DE1F9EA124A814D300A609B7 /* PrinterManager.swift in Sources */, + DE1F9E8124A72D6B00A609B7 /* HTTPClient.swift in Sources */, + DE1F9E7F24A72D3400A609B7 /* SystemCommand.swift in Sources */, + DE1F9EAC24A82F6B00A609B7 /* AppConfigurationDelegate.swift in Sources */, + DE1F9E7B24A72D3400A609B7 /* Container.swift in Sources */, + DE1F9E9524A7EF3B00A609B7 /* Plugins.swift in Sources */, + DE1F9E6D24A7299B00A609B7 /* PopoverViewController.swift in Sources */, + DE1F9E8424A7DCA800A609B7 /* Printer.swift in Sources */, + DE1F9E8224A72D7D00A609B7 /* CancelObject.swift in Sources */, + DE1F9E8724A7DD4000A609B7 /* CurrentStateEvent.swift in Sources */, + DE1F9E7824A72D0F00A609B7 /* OctoPrintRESTClient.swift in Sources */, + DE1F9EA424A8284F00A609B7 /* OctoPrintSettingsDelegate.swift in Sources */, + DE1F9E8324A72DC500A609B7 /* Palette2Utils.swift in Sources */, + DE1F9EC324ABEAC200A609B7 /* UIUtils.swift in Sources */, + DE1F9EAB24A82F6B00A609B7 /* AppConfiguration.swift in Sources */, + DE1F9E9024A7E02300A609B7 /* TempHistory.swift in Sources */, + DE1F9E9724A7EF4800A609B7 /* PrinterProfilesDelegate.swift in Sources */, + DE1F9E7D24A72D3400A609B7 /* Script.swift in Sources */, + DE1F9E8824A7DD5100A609B7 /* PrintFile.swift in Sources */, + DE1F9E9A24A7F0BC00A609B7 /* Terminal.swift in Sources */, + DEE3256F24B06B45000375BD /* PreferencesDelegate.swift in Sources */, + DE1F9E7C24A72D3400A609B7 /* Command.swift in Sources */, + DE1F9E7924A72D3400A609B7 /* CustomControl.swift in Sources */, + DE1F9E6B24A7299B00A609B7 /* AppDelegate.swift in Sources */, + DE1F9E9424A7ED7300A609B7 /* OctoPrintClientDelegate.swift in Sources */, + DE1F9E8624A7DCC600A609B7 /* EnclosureOutput.swift in Sources */, + DE1F9E8024A72D5200A609B7 /* IPPlug.swift in Sources */, + DE1F9E9D24A7F29800A609B7 /* ComplicationContentType.swift in Sources */, + DE1F9EA624A829F300A609B7 /* OctoPrintClient.swift in Sources */, + DE1F9EC524ABFB6600A609B7 /* Extensions.swift in Sources */, + DE1F9EC824AEEF7E00A609B7 /* PreferencesViewController.swift in Sources */, + DE1F9E8524A7DCC200A609B7 /* EnclosureInput.swift in Sources */, + DE1F9EC024A9CB0700A609B7 /* CameraImageView.swift in Sources */, + DE1F9EBB24A87E3B00A609B7 /* MjpegStreamingController.swift in Sources */, + DE1F9EA324A81CC700A609B7 /* SharedPersistentContainer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2291,6 +2470,14 @@ name = Localizable.strings; sourceTree = ""; }; + DE1F9E7024A7299C00A609B7 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DEF8D00424B58E4A0027E2A9 /* en */, + ); + name = Main.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -2799,6 +2986,55 @@ }; name = Release; }; + DE1F9E7524A7299C00A609B7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Mac"; + CODE_SIGN_ENTITLEMENTS = "OctoPod Mac/OctoPod_Mac.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = QCEM4H639D; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = "OctoPod Mac/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.OctoPod.OctoPod-Mac"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + DE1F9E7624A7299C00A609B7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Mac"; + CODE_SIGN_ENTITLEMENTS = "OctoPod Mac/OctoPod_Mac.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = QCEM4H639D; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = "OctoPod Mac/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.OctoPod.OctoPod-Mac"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2883,6 +3119,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DE1F9E7724A7299C00A609B7 /* Build configuration list for PBXNativeTarget "OctoPod Mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE1F9E7524A7299C00A609B7 /* Debug */, + DE1F9E7624A7299C00A609B7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -2920,6 +3165,11 @@ package = 3D2BEA6C248B328400EF6859 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + DE1F9E8D24A7DEDB00A609B7 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = 3D2BEA6C248B328400EF6859 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/OctoPod/Info.plist b/OctoPod/Info.plist index 2540ba0c..6dcdff88 100644 --- a/OctoPod/Info.plist +++ b/OctoPod/Info.plist @@ -8,6 +8,10 @@ OctoPod CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundleIcons~ipad + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion diff --git a/OctoPod/Model/Printer.swift b/OctoPod/Model/Printer.swift index dd6f788f..577f3db8 100644 --- a/OctoPod/Model/Printer.swift +++ b/OctoPod/Model/Printer.swift @@ -89,7 +89,9 @@ class Printer: NSManagedObject { @NSManaged public var enclosureInputs: Set? @NSManaged public var enclosureOutputs: Set? - + /// whether to ignore SSL cert validation + @NSManaged var ignoreSSLCertValidationError: Bool + func getStreamPath() -> String { if let path = streamUrl { return path diff --git a/OctoPod/Model/PrinterManager.swift b/OctoPod/Model/PrinterManager.swift index de0bb0c4..890407a3 100644 --- a/OctoPod/Model/PrinterManager.swift +++ b/OctoPod/Model/PrinterManager.swift @@ -1,5 +1,10 @@ import Foundation +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit import UIKit +#else +// all other platforms meaning macOS +#endif import CoreData // Manager of persistent printer information (OctoPrint servers) that are stored in the iPhone @@ -108,7 +113,13 @@ class PrinterManager { printer.iCloudUpdate = iCloudUpdate printer.sdSupport = true // Assume that printer supports SD card. Will get updated later with actual value + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit printer.cameraOrientation = Int16(UIImage.Orientation.up.rawValue) // Assume no flips or rotations for camera. Will get updated later with actual value + #else + // all other platforms meaning macOS + #endif + printer.invertX = false // Assume control of X axis is not inverted. Will get updated later with actual value printer.invertY = false // Assume control of Y axis is not inverted. Will get updated later with actual value diff --git a/OctoPod/OctoPod.xcdatamodeld/OctoPod v10.xcdatamodel/contents b/OctoPod/OctoPod.xcdatamodeld/OctoPod v10.xcdatamodel/contents index 7ece91e9..457123d2 100644 --- a/OctoPod/OctoPod.xcdatamodeld/OctoPod v10.xcdatamodel/contents +++ b/OctoPod/OctoPod.xcdatamodeld/OctoPod v10.xcdatamodel/contents @@ -29,6 +29,7 @@ + @@ -63,6 +64,6 @@ - + \ No newline at end of file diff --git a/OctoPod/OctoPrint/HTTPClient.swift b/OctoPod/OctoPrint/HTTPClient.swift index fd402ed3..c7d7abfc 100644 --- a/OctoPod/OctoPrint/HTTPClient.swift +++ b/OctoPod/OctoPrint/HTTPClient.swift @@ -1,5 +1,5 @@ import Foundation -import UIKit // Used for network indicator in the UI +//import UIKit // Used for network indicator in the UI // Basic HTTP Client that offers support for basic HTTP verbs. // Authentication (user/password) is supported in case a reverse proxy diff --git a/OctoPod/OctoPrint/OctoPrintClient.swift b/OctoPod/OctoPrint/OctoPrintClient.swift index 36898f33..177bd3b2 100644 --- a/OctoPod/OctoPrint/OctoPrintClient.swift +++ b/OctoPod/OctoPrint/OctoPrintClient.swift @@ -1,5 +1,10 @@ import Foundation +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit import UIKit +#else +// all other platforms meaning macOS +#endif /// OctoPrint client that exposes the REST API described /// here: http://docs.octoprint.org/en/master/api/index.html @@ -21,15 +26,17 @@ class OctoPrintClient: WebSocketClientDelegate, AppConfigurationDelegate { var octoPrintSettingsDelegates: Array = Array() var printerProfilesDelegates: Array = Array() var octoPrintPluginsDelegates: Array = Array() - + var appConfiguration: AppConfiguration { - get { - let appDelegate = UIApplication.shared.delegate as! AppDelegate - return appDelegate.appConfiguration - } - set(configuration) { - configuration.delegates.append(self) - } + + get { +// let appDelegate = UIApplication.shared.delegate as! AppDelegate +// return appDelegate.appConfiguration + return AppDelegate().appConfiguration + } + set(configuration) { + configuration.delegates.append(self) + } } // Remember last CurrentStateEvent that was reported from OctoPrint (via websockets) @@ -756,8 +763,10 @@ class OctoPrintClient: WebSocketClientDelegate, AppConfigurationDelegate { } } } - + if let webcam = json["webcam"] as? NSDictionary { + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit if let flipH = webcam["flipH"] as? Bool, let flipV = webcam["flipV"] as? Bool, let rotate90 = webcam["rotate90"] as? Bool { let newOrientation = calculateImageOrientation(flipH: flipH, flipV: flipV, rotate90: rotate90) if printer.cameraOrientation != newOrientation.rawValue { @@ -772,6 +781,10 @@ class OctoPrintClient: WebSocketClientDelegate, AppConfigurationDelegate { } } } + #else + // all other platforms meaning macOS + #endif + if let streamUrl = webcam["streamUrl"] as? String { if printer.streamUrl != streamUrl { // Update path to camera hosted by OctoPrint @@ -1241,7 +1254,8 @@ class OctoPrintClient: WebSocketClientDelegate, AppConfigurationDelegate { } } } - + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit fileprivate func calculateImageOrientation(flipH: Bool, flipV: Bool, rotate90: Bool) -> UIImage.Orientation { if !flipH && !flipV && !rotate90 { // No flips selected @@ -1269,6 +1283,10 @@ class OctoPrintClient: WebSocketClientDelegate, AppConfigurationDelegate { return UIImage.Orientation.right } } + #else + // all other platforms meaning macOS + #endif + // MARK: - Private - Printer Profile functions diff --git a/OctoPod/OctoPrint/OctoPrintSettingsDelegate.swift b/OctoPod/OctoPrint/OctoPrintSettingsDelegate.swift index 5ad7364a..38d76115 100644 --- a/OctoPod/OctoPrint/OctoPrintSettingsDelegate.swift +++ b/OctoPod/OctoPrint/OctoPrintSettingsDelegate.swift @@ -1,12 +1,24 @@ import Foundation +import Starscream +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit import UIKit +#else +// all other platforms meaning macOS +#endif /// Listener that reacts to changes in OctoPrint Settings (/api/settings) /// This is done via the OctoPrint admin console protocol OctoPrintSettingsDelegate: class { /// Notification that orientation of the camera hosted by OctoPrint has changed + + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit func cameraOrientationChanged(newOrientation: UIImage.Orientation) + #else + // all other platforms meaning macOS + #endif /// Notification that path to camera hosted by OctoPrint has changed func cameraPathChanged(streamUrl: String) @@ -58,9 +70,15 @@ protocol OctoPrintSettingsDelegate: class { // Make everything optional so implementors of this protocol are not forced to implement everything extension OctoPrintSettingsDelegate { - + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit func cameraOrientationChanged(newOrientation: UIImage.Orientation) { } + #else + // all other platforms meaning macOS + #endif + + func cameraPathChanged(streamUrl: String) { } diff --git a/OctoPod/OctoPrint/WebSocketClient.swift b/OctoPod/OctoPrint/WebSocketClient.swift index a934b410..93f150f0 100644 --- a/OctoPod/OctoPrint/WebSocketClient.swift +++ b/OctoPod/OctoPrint/WebSocketClient.swift @@ -1,7 +1,12 @@ import Foundation import Starscream +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit import UIKit - +#else +// all other platforms meaning macOS +import Cocoa +#endif // Classic websocket client that connects to "/sockjs/websocket" // To receive socket events and received messages, create a WebSocketClientDelegate // and add it as a delegate of this WebSocketClient @@ -378,8 +383,16 @@ class WebSocketClient : NSObject, WebSocketAdvancedDelegate { self.socket = WebSocket(request: self.socketRequest!) // Configure if SSL certificate validation is disabled or not DispatchQueue.main.async { + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit let appDelegate = UIApplication.shared.delegate as! AppDelegate self.socket?.disableSSLCertValidation = appDelegate.appConfiguration.certValidationDisabled() + #else + // all other platforms meaning macOS + let printerManager = (NSApp.delegate as! AppDelegate).printerManager! + let ignoreSSLCertValidation = printerManager.getDefaultPrinter()?.ignoreSSLCertValidationError + self.socket?.disableSSLCertValidation = ignoreSSLCertValidation ?? false + #endif } } diff --git a/OctoPod/UIUtils.swift b/OctoPod/UIUtils.swift index 105c6f2c..8f913691 100644 --- a/OctoPod/UIUtils.swift +++ b/OctoPod/UIUtils.swift @@ -1,8 +1,16 @@ import Foundation +import UserNotifications +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit import UIKit +#else +// all other platforms meaning macOS +import Cocoa +#endif class UIUtils { - + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit /// Caller may not be running in Main thread static func showAlert(presenter: UIViewController, title: String, message: String, done: (() -> Void)?) { // We are not always on the main thread so present dialog on main thread to prevent crashes @@ -17,7 +25,20 @@ class UIUtils { } } } + #else + // all other platforms meaning macOS + static func showAlert(title: String, message: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.runModal() + } + #endif + #if canImport(UIKit) + // iOS, tvOS, and watchOS – use UIKit /// Caller MUST be running in Main thread static func showConfirm(presenter: UIViewController, message: String, yes: @escaping (UIAlertAction) -> Void, no: @escaping (UIAlertAction) -> Void) { let alert = UIAlertController(title: NSLocalizedString("Confirm", comment: ""), message: message, preferredStyle: .alert) @@ -28,6 +49,41 @@ class UIUtils { // Nothing to do here } } + #else + // all other platforms meaning macOS + static func showConfirm(title: String, message: String) -> Bool { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .warning + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + return alert.runModal() == .alertFirstButtonReturn + } + #endif + @available(OSX 10.14, *) + static func notifyUser(title: String, message: String) { + let content = UNMutableNotificationContent() + content.title = title + content.body = message + content.sound = UNNotificationSound.default + content.categoryIdentifier = "alarm" + // Configure the recurring date. + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false) + + // Create the request + let uuidString = UUID().uuidString + let request = UNNotificationRequest(identifier: uuidString, + content: content, trigger: trigger) + + // Schedule the request with the system. + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.add(request) { (error) in + if error != nil { + print(error.debugDescription) + } + } + } static func calculateCameraHeightConstraints(screenHeight: CGFloat) -> (cameraHeight4_3ConstraintPortrait: CGFloat, cameraHeight4_3ConstraintLandscape: CGFloat, camera16_9HeightConstraintPortrait: CGFloat, cameral16_9HeightConstraintLandscape: CGFloat){ if screenHeight <= 568 { @@ -103,7 +159,15 @@ class UIUtils { formatter.allowedUnits = [ .day, .hour, .minute ] return formatter.string(from: duration)! } - + /// Converts number of seconds into a string that represents time (e.g. 23h 10m) + static func secondsToPrintTime(seconds: Int) -> String { + let duration = TimeInterval(seconds) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .brief + formatter.allowedUnits = [ .day, .hour, .minute, .second ] + formatter.zeroFormattingBehavior = [ .default ] + return formatter.string(from: duration)! + } /// Return estimated complection date based on number of estimated seconds to completion /// - parameter seconds: estimated number of seconds to complection static func secondsToETA(seconds: Int) -> String { @@ -134,8 +198,22 @@ class UIUtils { return "" } } + static func isValidURL(urlString: String) -> Bool { + + let urlRegEx = "(http|https)://((\\w)*|([0-9]*)|([-|_]|[\\.|/])*)+(:[0-9]+)?" + let urlTest = NSPredicate(format: "SELF MATCHES %@", urlRegEx) + var result = urlTest.evaluate(with: urlString) + if !result { + let ipv6RegEx = "(http|https)://(\\[)?(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(])?(:[0-9]+)?" + let ipv6Test = NSPredicate(format: "SELF MATCHES %@", ipv6RegEx) + result = ipv6Test.evaluate(with: urlString) + } + return result + } } +#if canImport(UIKit) +// iOS, tvOS, and watchOS – use UIKit extension UIImage { func resizeWithWidth(width: CGFloat) -> UIImage? { let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height))))) @@ -149,6 +227,9 @@ extension UIImage { return result } } +#else +// all other platforms meaning macOS +#endif extension Date { func timeAgoDisplay() -> String {