// // AppDelegate.swift // LunaMac // // Created by Claire Davis on 9/21/22. // Copyright © 2022 A Better Geek. All rights reserved. // /* ====== TO DO ======= * auto-update at a set interval * make help menu item work (show the window) * fix alignment of status bar icon (it's too high) */ import Cocoa import Foundation import CoreFoundation @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { // static array of possible lunar values let phaseArray:[String:[String:NSString]] = [ "new" : [ "nice" : "New Moon", "start" : "0", "end" : "1", "icon" : "🌑" ], "waxc" : [ "nice" : "Waxing Crescent", "start" : "1", "end" : "6.38264692644", "icon" : "🌒" ], "firstq" : [ "nice" : "First Quarter", "start" : "6.38264692644", "end" : "8.38264692644", "icon" : "🌓" ], "waxg" : [ "nice" : "Waxing Gibbous", "start" : "8.38264692644", "end" : "13.76529385288", "icon" : "🌔" ], "full" : [ "nice" : "Full Moon", "start" : "13.76529385288", "end" : "15.76529385288", "icon" : "🌕" ], "wang" : [ "nice" : "Waning Gibbous", "start" : "15.76529385288", "end" : "21.14794077932", "icon" : "🌖" ], "lastq" : [ "nice" : "Last Quarter", "start" : "21.14794077932", "end" : "23.14794077932", "icon" : "🌗" ], "wanc" : [ "nice" : "Waning Crescent", "start" : "23.14794077932", "end" : "28.53058770576", "icon" : "🌘" ], "new2" : [ "nice" : "New Moon", "start" : "28.53058770576", "end" : "29.53058770576", "icon" : "🌑" ], "default" : [ "nice" : "Failed to calculate.", "icon" : "⨂" ] ] @IBAction func clickClick(_ sender: NSButtonCell) { // show changelog showLog() } // get system-wide menu bar let statusBar = NSStatusBar.system // make sure applet stays in memory var statusBarItem: NSStatusItem? // create a menu for the icon let statusBarMenu = NSMenu(title: "LunaMac Menu") // create empty timer for event loop var lunaTimer = Timer() // is the info box open? var infoOpen = false // is the changelog open? var logOpen = false // user settings let defaults = UserDefaults.standard func applicationDidFinishLaunching(_ aNotification: Notification) { // set icon size to square statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) // build the icon and menu updateIcon() // listen for theme changes (dark and light) themeListener() // start the the timer for keeping the icon updated automatically startTimer() } // start the timer for auto-updating the icon func startTimer() { lunaTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(updateIcon), userInfo: nil, repeats: true) RunLoop.current.add(lunaTimer, forMode: RunLoop.Mode.common) } @objc func menuWillOpen(_ menu: NSMenu) { lunaTimer.invalidate() } @objc func menuDidClose(_ menu: NSMenu) { startTimer() } @objc func buildMenu(key: String = "default") { statusBarMenu.removeAllItems() statusBarMenu.delegate = self // filename suffix /* -dark dark mode -alt-dark dark mode inverted -emoji emoji */ var suffix = "" // if key is new2, rename to new var key2 = key.contains("2") ? "new" : key // if iconPref is set to emoji if (defaults.string(forKey: "iconPref") == "emoji") { suffix += "-emoji" } // otherwise use monochrome icons else { // check for dark mode and set the right image file if #available(OSX 10.14, *) { let darkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") if darkMode == "Dark" { // if theme is set to dark alt make adjustments if (defaults.string(forKey: "iconPref") == "darkalt") { switch (key2) { case "new" : key2 = "full" break case "full" : key2 = "new" break case "firstq" : key2 = "lastq" break case "lastq" : key2 = "firstq" break case "wanc", "wang", "waxc", "waxg" : suffix += "-alt" break default : break } } suffix += "-dark" } } } // set image filename let imgName = key2 + suffix + ".png" statusBarItem?.button?.image = NSImage(named: imgName) // add the menu to the menu bar icon statusBarItem?.menu = statusBarMenu let topTxt = phaseArray["default"]?["nice"] as String? ?? "Failed to calculate." // add menu items to the menu if (!topTxt.isEmpty) { statusBarMenu.addItem( withTitle: phaseArray[key]?["nice"] as String? ?? topTxt, action: nil, keyEquivalent: "" ) } statusBarMenu.addItem(NSMenuItem.separator()) statusBarMenu.addItem( withTitle: "Update moon phase", action: #selector(AppDelegate.updateIcon), keyEquivalent: "" ) statusBarMenu.addItem(NSMenuItem.separator()) // create menu item to hold submenu let prefMenu = NSMenu(title: "Prefs") // default let optDefault = NSMenuItem( title: "Default", action: #selector(setPref), keyEquivalent: "" ) // alternate dark let optDarkAlt = NSMenuItem( title: "Alternate Dark", action: #selector(setPref), keyEquivalent: "" ) // emoji let optEmoji = NSMenuItem( title: "Emoji", action: #selector(setPref), keyEquivalent: "" ) // make sure the right menu item is checked switch (defaults.string(forKey: "iconPref")) { case "default" : // standard icons optDefault.state = NSControl.StateValue.on break case "darkalt" : // inverted dark mode icons optDarkAlt.state = NSControl.StateValue.on break case "emoji" : // emoji icons optEmoji.state = NSControl.StateValue.on break default : // default to standard icons optDefault.state = NSControl.StateValue.on break } prefMenu.addItem(optDefault) prefMenu.addItem(optDarkAlt) prefMenu.addItem(optEmoji) let prefMenuItem = NSMenuItem( title: "Icon Style", action: nil, keyEquivalent: "" ) statusBarMenu.addItem(prefMenuItem) statusBarMenu.setSubmenu(prefMenu, for: prefMenuItem) statusBarMenu.addItem( withTitle: "About LunaMac...", action: #selector(AppDelegate.showAbout), keyEquivalent: "" ) statusBarMenu.addItem( withTitle: "Quit LunaMac", action: #selector(AppDelegate.quitApp), keyEquivalent: "" ) } @objc func setPref(sender: NSMenuItem) { // sender.title tells us what to do switch (sender.title) { case "Default" : defaults.set("default", forKey: "iconPref") break case "Alternate Dark" : defaults.set("darkalt", forKey: "iconPref") break case "Emoji" : defaults.set("emoji", forKey: "iconPref") break default: break } // rebuild the menu updateIcon() } @objc func showAbout() { if (!infoOpen) { // create infobox window object let aboutWin = NSWindowController(windowNibName: "InfoBox") aboutWin.loadWindow() } NSApp.activate(ignoringOtherApps: true) } @objc func showLog() { if (!logOpen) { // create changelog window object // this creates duplicate windows let logWin = NSWindowController(windowNibName: "Changelog") logWin.loadWindow() } NSApp.activate(ignoringOtherApps: true) } // update menu bar icon @objc func updateIcon() { // current system time in UTC let sysDate = Date() // get baseline date // let baseDateStr = "2000-01-06 18:14:00 +0000" // create date object from baseDate // date components var bdc = DateComponents() bdc.year = 2000 bdc.month = 1 bdc.day = 6 bdc.hour = 18 bdc.minute = 14 // create date object let ucal = Calendar(identifier: .gregorian) let baseDate = ucal.date(from: bdc) // days in a lunar cycle let lunarDays = 29.53058770576 // seconds in a lunar cycle let lunarSecs = lunarDays * (24 * 60 * 60) // calculate seconds between sysDate and baseDate // if baseDate is nil, default to current system time let totalSecs = sysDate.timeIntervalSince(baseDate ?? Date()) if (totalSecs.sign != .minus) { // a positive number is valid // % calculates seconds of current cycle let currSecs = totalSecs.truncatingRemainder(dividingBy: lunarSecs) let currFrac = currSecs / lunarSecs let currDays = currFrac * lunarDays var newArr = [String:[String:NSString]]() newArr = phaseArray.filter{ findPhase( start: $0.value["start"]?.doubleValue ?? 0, end: $0.value["end"]?.doubleValue ?? 0, curr: currDays ) } // we want newArr[0].value which is [String:NSString] let theKey = newArr.first?.key ?? "default" buildMenu(key: theKey) } else { // if dateDiff is negative, something went wrong. buildMenu() } } @objc func findPhase(start: Double, end: Double, curr: Double) -> Bool { if ((curr >= start) && (curr <= end)) { return true } else { return false } } // quit application @objc func quitApp() { NSApp.terminate(self) } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } func themeListener() { DistributedNotificationCenter.default.addObserver( self, selector: #selector(updateIcon), name: .AppleInterfaceThemeChangedNotification, object: nil ) } } extension Notification.Name { static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification") }