diff --git a/.swiftlint.yml b/.swiftlint.yml index 1efd31d..3c7b649 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,18 +1,15 @@ disabled_rules: # rule identifiers to exclude from running - - colon - - comma - - control_statement - - line_length - type_body_length - - file_length - - nesting - - function_body_length + # - file_length + # - nesting opt_in_rules: # some rules are only opt-in - empty_count # included: # paths to include during linting. `--path` is ignored if present. # - Clocker excluded: # paths to ignore during linting. Takes precedence over `included`. - Clocker/Dependencies + - Clocker/ClockerUnitTests + - Clocker/ClockerUITests # - Pods # - Source/ExcludedFolder # - Source/ExcludedFile.swift @@ -51,4 +48,4 @@ identifier_name: # - id # - URL # - GlobalAPIKey -reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown) \ No newline at end of file +reporter: "xcode" \ No newline at end of file diff --git a/Clocker/AppDelegate.swift b/Clocker/AppDelegate.swift index 6ff477d..025631a 100644 --- a/Clocker/AppDelegate.swift +++ b/Clocker/AppDelegate.swift @@ -2,7 +2,7 @@ import Cocoa -open class AppDelegate : NSObject, NSApplicationDelegate { +open class AppDelegate: NSObject, NSApplicationDelegate { lazy private var floatingWindow: FloatingWindowController = FloatingWindowController.shared() lazy private var panelController: PanelController = PanelController.shared() @@ -13,7 +13,7 @@ open class AppDelegate : NSObject, NSApplicationDelegate { panelObserver?.invalidate() } - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { if let path = keyPath, path == "values.globalPing" { @@ -98,7 +98,8 @@ open class AppDelegate : NSObject, NSApplicationDelegate { }() private func showOnboardingFlow() { - let shouldLaunchOnboarding = (DataStore.shared().retrieve(key: CLShowOnboardingFlow) == nil && DataStore.shared().timezones().isEmpty) || (ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument)) + let shouldLaunchOnboarding = (DataStore.shared().retrieve(key: CLShowOnboardingFlow) == nil && DataStore.shared().timezones().isEmpty) + || (ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument)) shouldLaunchOnboarding ? controller?.launch() : continueUsually() } @@ -238,11 +239,17 @@ open class AppDelegate : NSObject, NSApplicationDelegate { } } + let informativeText = """ + Clocker must be run from the Applications folder in order to work properly. + Please quit Clocker, move it to the Applications folder, and relaunch. + Current folder: \(applicationDirectory)" + """ + // Clocker is installed out of Applications directory // This breaks start at login! Time to show an alert and terminate showAlert(message: "Move Clocker to the Applications folder", - informativeText: "Clocker must be run from the Applications folder in order to work properly.\n\nPlease quit Clocker, move it to the Applications folder, and relaunch. Current folder: \(applicationDirectory)", - buttonTitle: "Quit") + informativeText: informativeText, + buttonTitle: "Quit") // Terminate NSApp.terminate(nil) diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index a9cea2f..4c8b01f 100644 --- a/Clocker/Events and Reminders/CalendarHandler.swift +++ b/Clocker/Events and Reminders/CalendarHandler.swift @@ -228,23 +228,11 @@ extension EventCenter { } nextDate = autoupdatingCalendar.startOfDay(for: nextDate) - // Make a customized struct - let isStartDate = autoupdatingCalendar.isDate(date, inSameDayAs: event.startDate) && (event.endDate.compare(date) == .orderedDescending) - let isEndDate = autoupdatingCalendar.isDate(date, inSameDayAs: event.endDate) && (event.startDate.compare(date) == .orderedAscending) - let isAllDay = event.isAllDay || (event.startDate.compare(date) == .orderedAscending && event.endDate.compare(nextDate) == .orderedSame) - let isSingleDay = event.isAllDay && (event.startDate.compare(date) == .orderedSame && event.endDate.compare(nextDate) == .orderedSame) - - let eventInfo = EventInfo(event: event, - isStartDate: isStartDate, - isEndDate: isEndDate, - isAllDay: isAllDay, - isSingleDay: isSingleDay) - if eventsForDateMapper[date] == nil { eventsForDateMapper[date] = [] } - eventsForDateMapper[date]?.append(eventInfo) + eventsForDateMapper[date]?.append(generateEventInfo(for: event, date, nextDate)) date = nextDate } @@ -265,6 +253,21 @@ extension EventCenter { filterEvents() } + + private func generateEventInfo(for event: EKEvent, _ date: Date, _ nextDate: Date) -> EventInfo { + // Make a customized struct + let isStartDate = autoupdatingCalendar.isDate(date, inSameDayAs: event.startDate) && (event.endDate.compare(date) == .orderedDescending) + let isEndDate = autoupdatingCalendar.isDate(date, inSameDayAs: event.endDate) && (event.startDate.compare(date) == .orderedAscending) + let isAllDay = event.isAllDay || (event.startDate.compare(date) == .orderedAscending && event.endDate.compare(nextDate) == .orderedSame) + let isSingleDay = event.isAllDay && (event.startDate.compare(date) == .orderedSame && event.endDate.compare(nextDate) == .orderedSame) + + let eventInfo = EventInfo(event: event, + isStartDate: isStartDate, + isEndDate: isEndDate, + isAllDay: isAllDay, + isSingleDay: isSingleDay) + return eventInfo + } } struct CalendarInfo { diff --git a/Clocker/Onboarding/OnboardingParentViewController.swift b/Clocker/Onboarding/OnboardingParentViewController.swift index 9144335..682e8c6 100644 --- a/Clocker/Onboarding/OnboardingParentViewController.swift +++ b/Clocker/Onboarding/OnboardingParentViewController.swift @@ -26,15 +26,15 @@ class OnboardingParentViewController: NSViewController { @IBOutlet private var backButton: NSButton! @IBOutlet private var positiveButton: NSButton! - private lazy var welcomeVC: WelcomeViewController? = (storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.welcomeIdentifier) as? WelcomeViewController) + private lazy var welcomeVC = (storyboard?.instantiateController(withIdentifier: .welcomeIdentifier) as? WelcomeViewController) - private lazy var permissionsVC: OnboardingPermissionsViewController? = (storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.onboardingPermissionsIdentifier) as? OnboardingPermissionsViewController) + private lazy var permissionsVC = (storyboard?.instantiateController(withIdentifier: .onboardingPermissionsIdentifier) as? OnboardingPermissionsViewController) - private lazy var startAtLoginVC: StartAtLoginViewController? = (storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.startAtLoginIdentifier) as? StartAtLoginViewController) + private lazy var startAtLoginVC = (storyboard?.instantiateController(withIdentifier: .startAtLoginIdentifier) as? StartAtLoginViewController) - private lazy var onboardingSearchVC: OnboardingSearchController? = (self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.onboardingSearchIdentifier) as? OnboardingSearchController) + private lazy var onboardingSearchVC = (storyboard?.instantiateController(withIdentifier: .onboardingSearchIdentifier) as? OnboardingSearchController) - private lazy var finalOnboardingVC: FinalOnboardingViewController? = (self.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.finalOnboardingIdentifier) as? FinalOnboardingViewController) + private lazy var finalOnboardingVC = (storyboard?.instantiateController(withIdentifier: .finalOnboardingIdentifier) as? FinalOnboardingViewController) override func viewDidLoad() { super.viewDidLoad() @@ -96,84 +96,102 @@ class OnboardingParentViewController: NSViewController { @IBAction func continueOnboarding(_: NSButton) { if positiveButton.tag == OnboardingType.welcome.rawValue { - guard let fromViewController = welcomeVC, let toViewController = permissionsVC else { - assertionFailure() - return - } - - addChildIfNeccessary(toViewController) - - transition(from: fromViewController, - to: toViewController, - options: .slideLeft) { - self.positiveButton.tag = OnboardingType.permissions.rawValue - self.positiveButton.title = "Continue" - self.backButton.isHidden = false - } - + navigateToPermissions() } else if positiveButton.tag == OnboardingType.permissions.rawValue { - guard let fromViewController = permissionsVC, let toViewController = startAtLoginVC else { - assertionFailure() - return - } - - addChildIfNeccessary(toViewController) - - transition(from: fromViewController, - to: toViewController, - options: .slideLeft) { - self.backButton.tag = OnboardingType.permissions.rawValue - self.positiveButton.tag = OnboardingType.launchAtLogin.rawValue - self.positiveButton.title = "Open Clocker At Login" - self.negativeButton.isHidden = false - } + navigateToStartAtLogin() } else if positiveButton.tag == OnboardingType.launchAtLogin.rawValue { - guard let fromViewController = startAtLoginVC, let toViewController = onboardingSearchVC else { - assertionFailure() - return - } - - addChildIfNeccessary(toViewController) - - shouldStartAtLogin(true) - - transition(from: fromViewController, - to: toViewController, - options: .slideLeft) { - self.backButton.tag = OnboardingType.launchAtLogin.rawValue - self.positiveButton.tag = OnboardingType.search.rawValue - self.positiveButton.title = "Continue" - self.negativeButton.isHidden = true - } + navigateToSearch() } else if positiveButton.tag == OnboardingType.search.rawValue { - guard let fromViewController = onboardingSearchVC, let toViewController = finalOnboardingVC else { - assertionFailure() - return - } + navigateToFinalStage() + } else { + performFinalStepsBeforeFinishing() + } + } - addChildIfNeccessary(toViewController) + private func navigateToPermissions() { + guard let fromViewController = welcomeVC, let toViewController = permissionsVC else { + assertionFailure() + return + } - transition(from: fromViewController, - to: toViewController, - options: .slideLeft) { - self.backButton.tag = OnboardingType.search.rawValue - self.positiveButton.tag = OnboardingType.final.rawValue - self.positiveButton.title = "Launch Clocker" - } + addChildIfNeccessary(toViewController) - } else { + transition(from: fromViewController, + to: toViewController, + options: .slideLeft) { + self.positiveButton.tag = OnboardingType.permissions.rawValue + self.positiveButton.title = "Continue" + self.backButton.isHidden = false + } + } - self.positiveButton.tag = OnboardingType.complete.rawValue + private func navigateToStartAtLogin() { + guard let fromViewController = permissionsVC, let toViewController = startAtLoginVC else { + assertionFailure() + return + } - // Install the menubar option! - let appDelegate = NSApplication.shared.delegate as? AppDelegate - appDelegate?.continueUsually() + addChildIfNeccessary(toViewController) - view.window?.close() + transition(from: fromViewController, + to: toViewController, + options: .slideLeft) { + self.backButton.tag = OnboardingType.permissions.rawValue + self.positiveButton.tag = OnboardingType.launchAtLogin.rawValue + self.positiveButton.title = "Open Clocker At Login" + self.negativeButton.isHidden = false + } + } - if ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument) == false { - UserDefaults.standard.set(true, forKey: CLShowOnboardingFlow) - } + private func navigateToSearch() { + guard let fromViewController = startAtLoginVC, let toViewController = onboardingSearchVC else { + assertionFailure() + return + } + + addChildIfNeccessary(toViewController) + + shouldStartAtLogin(true) + + transition(from: fromViewController, + to: toViewController, + options: .slideLeft) { + self.backButton.tag = OnboardingType.launchAtLogin.rawValue + self.positiveButton.tag = OnboardingType.search.rawValue + self.positiveButton.title = "Continue" + self.negativeButton.isHidden = true + } + } + + private func navigateToFinalStage() { + guard let fromViewController = onboardingSearchVC, let toViewController = finalOnboardingVC else { + assertionFailure() + return + } + + addChildIfNeccessary(toViewController) + + transition(from: fromViewController, + to: toViewController, + options: .slideLeft) { + self.backButton.tag = OnboardingType.search.rawValue + self.positiveButton.tag = OnboardingType.final.rawValue + self.positiveButton.title = "Launch Clocker" + } + + } + + private func performFinalStepsBeforeFinishing() { + self.positiveButton.tag = OnboardingType.complete.rawValue + + // Install the menubar option! + let appDelegate = NSApplication.shared.delegate as? AppDelegate + appDelegate?.continueUsually() + + view.window?.close() + + if ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument) == false { + UserDefaults.standard.set(true, forKey: CLShowOnboardingFlow) } } @@ -185,64 +203,78 @@ class OnboardingParentViewController: NSViewController { @IBAction func back(_: Any) { if backButton.tag == OnboardingType.welcome.rawValue { - guard let fromViewController = permissionsVC, let toViewController = welcomeVC else { - assertionFailure() - return - } - - transition(from: fromViewController, - to: toViewController, - options: .slideRight) { - self.positiveButton.tag = OnboardingType.welcome.rawValue - self.backButton.isHidden = true - self.positiveButton.title = "Get Started" - } + goBackToWelcomeScreen() } else if backButton.tag == OnboardingType.permissions.rawValue { - // We're on StartAtLogin VC and we have to go back to Permissions - - guard let fromViewController = startAtLoginVC, let toViewController = permissionsVC else { - assertionFailure() - return - } - - transition(from: fromViewController, - to: toViewController, - options: .slideRight) { - self.positiveButton.tag = OnboardingType.permissions.rawValue - self.backButton.tag = OnboardingType.welcome.rawValue - self.negativeButton.isHidden = true - self.positiveButton.title = "Continue" - } + goBackToPermissions() } else if backButton.tag == OnboardingType.launchAtLogin.rawValue { - guard let fromViewController = onboardingSearchVC, let toViewController = startAtLoginVC else { - assertionFailure() - return - } - - transition(from: fromViewController, - to: toViewController, - options: .slideRight) { - self.positiveButton.tag = OnboardingType.launchAtLogin.rawValue - self.backButton.tag = OnboardingType.permissions.rawValue - self.positiveButton.title = "Open Clocker At Login" - self.negativeButton.isHidden = false - } + goBackToStartAtLogin() } else if backButton.tag == OnboardingType.search.rawValue { + goBackToSearch() + } + } + + private func goBackToSearch() { + guard let fromViewController = finalOnboardingVC, let toViewController = onboardingSearchVC else { + assertionFailure() + return + } + + transition(from: fromViewController, + to: toViewController, + options: .slideRight) { + self.positiveButton.tag = OnboardingType.search.rawValue + self.backButton.tag = OnboardingType.launchAtLogin.rawValue + self.positiveButton.title = "Continue" + self.negativeButton.isHidden = true + } + } + + private func goBackToStartAtLogin() { + guard let fromViewController = onboardingSearchVC, let toViewController = startAtLoginVC else { + assertionFailure() + return + } + + transition(from: fromViewController, + to: toViewController, + options: .slideRight) { + self.positiveButton.tag = OnboardingType.launchAtLogin.rawValue + self.backButton.tag = OnboardingType.permissions.rawValue + self.positiveButton.title = "Open Clocker At Login" + self.negativeButton.isHidden = false + } + } + + private func goBackToPermissions() { + // We're on StartAtLogin VC and we have to go back to Permissions + + guard let fromViewController = startAtLoginVC, let toViewController = permissionsVC else { + assertionFailure() + return + } - guard let fromViewController = finalOnboardingVC, let toViewController = onboardingSearchVC else { - assertionFailure() - return - } + transition(from: fromViewController, + to: toViewController, + options: .slideRight) { + self.positiveButton.tag = OnboardingType.permissions.rawValue + self.backButton.tag = OnboardingType.welcome.rawValue + self.negativeButton.isHidden = true + self.positiveButton.title = "Continue" + } + } - transition(from: fromViewController, - to: toViewController, - options: .slideRight) { - self.positiveButton.tag = OnboardingType.search.rawValue - self.backButton.tag = OnboardingType.launchAtLogin.rawValue - self.positiveButton.title = "Continue" - self.negativeButton.isHidden = true - } + private func goBackToWelcomeScreen() { + guard let fromViewController = permissionsVC, let toViewController = welcomeVC else { + assertionFailure() + return + } + transition(from: fromViewController, + to: toViewController, + options: .slideRight) { + self.positiveButton.tag = OnboardingType.welcome.rawValue + self.backButton.isHidden = true + self.positiveButton.title = "Get Started" } } diff --git a/Clocker/Onboarding/OnboardingSearchController.swift b/Clocker/Onboarding/OnboardingSearchController.swift index 72b4c40..8d31ab7 100644 --- a/Clocker/Onboarding/OnboardingSearchController.swift +++ b/Clocker/Onboarding/OnboardingSearchController.swift @@ -200,7 +200,13 @@ class OnboardingSearchController: NSViewController { } } - private var placeholders: [String] = ["New York", "Los Angeles", "Chicago", "Moscow", "Tokyo", "Istanbul", "Beijing", "Shanghai", "Sao Paulo", "Cairo", "Mexico City", "London", "Seoul", "Copenhagen", "Tel Aviv", "Bern", "San Francisco", "Los Angeles", "Sydney NSW", "Berlin"] + private var placeholders: [String] = ["New York", "Los Angeles", "Chicago", + "Moscow", "Tokyo", "Istanbul", + "Beijing", "Shanghai", "Sao Paulo", + "Cairo", "Mexico City", "London", + "Seoul", "Copenhagen", "Tel Aviv", + "Bern", "San Francisco", "Los Angeles", + "Sydney NSW", "Berlin"] private func setup() { appName.stringValue = "Quick Add Locations" @@ -278,12 +284,7 @@ class OnboardingSearchController: NSViewController { self.results = [] if let errorPresent = error { - if errorPresent.localizedDescription == PreferencesConstants.offlineErrorMessage { - self.setInfoLabel(PreferencesConstants.noInternetConnectivityError) - } else { - self.setInfoLabel(PreferencesConstants.tryAgainMessage) - } - + self.presentErrorMessage(errorPresent.localizedDescription) setupForError() return } @@ -302,23 +303,7 @@ class OnboardingSearchController: NSViewController { return } - for result in searchResults!.results { - let location = result.geometry.location - let latitude = location.lat - let longitude = location.lng - let formattedAddress = result.formattedAddress - - let totalPackage = [ - "latitude": latitude, - "longitude": longitude, - CLTimezoneName: formattedAddress, - CLCustomLabel: formattedAddress, - CLTimezoneID: CLEmptyString, - CLPlaceIdentifier: result.placeId - ] as [String: Any] - - self.results.append(TimezoneData(with: totalPackage)) - } + self.appendResultsToFilteredArray(searchResults!.results) self.setInfoLabel(CLEmptyString) @@ -327,6 +312,34 @@ class OnboardingSearchController: NSViewController { }) } + private func presentErrorMessage(_ errorMessage: String) { + if errorMessage == PreferencesConstants.offlineErrorMessage { + self.setInfoLabel(PreferencesConstants.noInternetConnectivityError) + } else { + self.setInfoLabel(PreferencesConstants.tryAgainMessage) + } + } + + private func appendResultsToFilteredArray(_ results: [SearchResult.Result]) { + results.forEach { + let location = $0.geometry.location + let latitude = location.lat + let longitude = location.lng + let formattedAddress = $0.formattedAddress + + let totalPackage = [ + "latitude": latitude, + "longitude": longitude, + CLTimezoneName: formattedAddress, + CLCustomLabel: formattedAddress, + CLTimezoneID: CLEmptyString, + CLPlaceIdentifier: $0.placeId + ] as [String: Any] + + self.results.append(TimezoneData(with: totalPackage)) + } + } + // Extracting this out for tests private func decode(from data: Data) -> SearchResult? { let jsonDecoder = JSONDecoder() diff --git a/Clocker/Overall App/NetworkManager.swift b/Clocker/Overall App/NetworkManager.swift index 22d8950..09d3dd1 100644 --- a/Clocker/Overall App/NetworkManager.swift +++ b/Clocker/Overall App/NetworkManager.swift @@ -10,15 +10,23 @@ class NetworkManager: NSObject { }() static let internalServerError: NSError = { + let localizedError = """ + There was a problem retrieving your information. Please try again later. + If the problem continues please contact App Support. + """ let userInfoDictionary: [String: Any] = [NSLocalizedDescriptionKey: "Internal Error", - NSLocalizedFailureReasonErrorKey: "There was a problem retrieving your information. Please try again later. If the problem continues please contact App Support."] + NSLocalizedFailureReasonErrorKey: localizedError] let error = NSError(domain: "APIError", code: 100, userInfo: userInfoDictionary) return error }() static let unableToGenerateURL: NSError = { + let localizedError = """ + There was a problem searching the location. Please try again later. + If the problem continues please contact App Support. + """ let userInfoDictionary: [String: Any] = [NSLocalizedDescriptionKey: "Unable to generate URL", - NSLocalizedFailureReasonErrorKey: "There was a problem searching the location. Please try again later. If the problem continues please contact App Support."] + NSLocalizedFailureReasonErrorKey: localizedError] let error = NSError(domain: "APIError", code: 100, userInfo: userInfoDictionary) return error }() diff --git a/Clocker/Overall App/Themer.swift b/Clocker/Overall App/Themer.swift index 931810a..f0dc3bc 100644 --- a/Clocker/Overall App/Themer.swift +++ b/Clocker/Overall App/Themer.swift @@ -432,6 +432,8 @@ extension Themer { } } - return themeIndex == .light ? NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) : NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0) + return themeIndex == .light ? + NSColor(deviceRed: 241.0 / 255.0, green: 241.0 / 255.0, blue: 241.0 / 255.0, alpha: 1.0) : + NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0) } } diff --git a/Clocker/Panel/Data Layer/TimezoneDataOperations.swift b/Clocker/Panel/Data Layer/TimezoneDataOperations.swift index c40ed11..eab3734 100644 --- a/Clocker/Panel/Data Layer/TimezoneDataOperations.swift +++ b/Clocker/Panel/Data Layer/TimezoneDataOperations.swift @@ -168,7 +168,7 @@ extension TimezoneDataOperations { return "\(todaysDate(with: sliderValue))\(timeDifference())" } - let errorDictionary: [String: Any] = ["Timezone" : dataObject.timezone(), + let errorDictionary: [String: Any] = ["Timezone": dataObject.timezone(), "Current Locale": Locale.autoupdatingCurrent.identifier, "Slider Value": sliderValue, "Today's Date": Date()] diff --git a/Clocker/Panel/Notes Popover/NotesPopover.swift b/Clocker/Panel/Notes Popover/NotesPopover.swift index f706dbb..9534e1b 100644 --- a/Clocker/Panel/Notes Popover/NotesPopover.swift +++ b/Clocker/Panel/Notes Popover/NotesPopover.swift @@ -278,34 +278,24 @@ class NotesPopover: NSViewController { private func insertTimezoneInDefaultPreferences() { guard let model = dataObject, var timezones = timezoneObjects else { return } - let encodedObject = NSKeyedArchiver.archivedData(withRootObject: model) - timezones[currentRow] = encodedObject - DataStore.shared().setTimezones(timezones) } private func updateMenubarTitles() { guard let model = dataObject, model.isFavourite == 1, var timezones = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data] else { return } - let menubarIndex = timezones.firstIndex { (menubarLocation) -> Bool in - if let convertedObject = TimezoneData.customObject(from: menubarLocation) { return convertedObject.isEqual(dataObject) } - return false } if let index = menubarIndex { - let encodedObject = NSKeyedArchiver.archivedData(withRootObject: model) - timezones[index] = encodedObject - UserDefaults.standard.set(timezones, forKey: CLMenubarFavorites) - } } @@ -370,7 +360,6 @@ class NotesPopover: NSViewController { if eventCenter.reminderAccessNotDetermined() { eventCenter.requestAccess(to: .reminder, completionHandler: { granted in - if granted { OperationQueue.main.addOperation { self.createReminder() @@ -402,7 +391,6 @@ class NotesPopover: NSViewController { private func createReminder() { guard let model = dataObject else { return } - if setReminderCheckbox.state == .on { let eventCenter = EventCenter.sharedCenter() let alertIndex = alertPopupButton.indexOfSelectedItem @@ -507,29 +495,19 @@ class NotesPopover: NSViewController { } setInitialReminderTime() - updateTimeFormat() - updateSecondsFormat() } private func updateTimeFormat() { - if dataObject?.overrideFormat.rawValue == 0 { - timeFormatControl.setSelected(true, forSegment: 0) - } else if dataObject?.overrideFormat.rawValue == 1 { - timeFormatControl.setSelected(true, forSegment: 1) - } else { - timeFormatControl.setSelected(true, forSegment: 2) + if let overrideFormat = dataObject?.overrideFormat.rawValue { + timeFormatControl.setSelected(true, forSegment: overrideFormat) } } private func updateSecondsFormat() { - if dataObject?.overrideSecondsFormat.rawValue == 0 { - secondsFormatControl.setSelected(true, forSegment: 0) - } else if dataObject?.overrideSecondsFormat.rawValue == 1 { - secondsFormatControl.setSelected(true, forSegment: 1) - } else { - secondsFormatControl.setSelected(true, forSegment: 2) + if let overrideFormat = dataObject?.overrideSecondsFormat.rawValue { + secondsFormatControl.setSelected(true, forSegment: overrideFormat) } } @@ -549,13 +527,10 @@ extension NotesPopover: NSTextFieldDelegate { // We need to do a couple of things if the customLabel is updated // 1. Update the userDefaults // 2. Check if the timezone is displayed in the menubar; if so, update the model - guard let model = dataObject else { return } - model.setLabel(customLabel.stringValue) insertTimezoneInDefaultPreferences() - updateMenubarTitles() NotificationCenter.default.post(name: NSNotification.Name.customLabelChanged, diff --git a/Clocker/Panel/ParentPanelController.swift b/Clocker/Panel/ParentPanelController.swift index f4b7413..21c63c6 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -536,7 +536,9 @@ class ParentPanelController: NSWindowController { stride(from: 0, to: preferences.count, by: 1).forEach { let current = preferences[$0] - if $0 < mainTableView.numberOfRows, let cellView = mainTableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? TimezoneCellView, let model = TimezoneData.customObject(from: current) { + if $0 < mainTableView.numberOfRows, + let cellView = mainTableView.view(atColumn: 0, row: $0, makeIfNecessary: false) as? TimezoneCellView, + let model = TimezoneData.customObject(from: current) { if let futureSliderCell = futureSlider.cell as? CustomSliderCell, futureSliderCell.tracking == true { return } diff --git a/Clocker/Panel/UI/BackgroundPanelView.swift b/Clocker/Panel/UI/BackgroundPanelView.swift index c6c63fc..02f0956 100644 --- a/Clocker/Panel/UI/BackgroundPanelView.swift +++ b/Clocker/Panel/UI/BackgroundPanelView.swift @@ -40,7 +40,10 @@ class BackgroundPanelView: NSView { let xOrdinate = arrowMidX - BackgroundPanelConstants.kArrowHeight - curveOffset let yOrdinate = frame.height - BackgroundPanelConstants.kArrowHeight - BackgroundPanelConstants.kBorderWidth arrowPath.move(to: NSPoint(x: xOrdinate, y: yOrdinate)) - arrowPath.relativeCurve(to: NSPoint(x: BackgroundPanelConstants.kArrowHeight + curveOffset, y: BackgroundPanelConstants.kBorderWidth), controlPoint1: NSPoint(x: curveOffset, y: 0), controlPoint2: NSPoint(x: BackgroundPanelConstants.kArrowHeight, y: BackgroundPanelConstants.kArrowHeight)) + arrowPath.relativeCurve(to: NSPoint(x: BackgroundPanelConstants.kArrowHeight + curveOffset, + y: BackgroundPanelConstants.kBorderWidth), + controlPoint1: NSPoint(x: curveOffset, y: 0), + controlPoint2: NSPoint(x: BackgroundPanelConstants.kArrowHeight, y: BackgroundPanelConstants.kArrowHeight)) } Themer.shared().mainBackgroundColor().setFill() diff --git a/Clocker/Preferences/Appearance/AppearanceViewController.swift b/Clocker/Preferences/Appearance/AppearanceViewController.swift index e237d0c..de7d5bc 100644 --- a/Clocker/Preferences/Appearance/AppearanceViewController.swift +++ b/Clocker/Preferences/Appearance/AppearanceViewController.swift @@ -112,7 +112,11 @@ class AppearanceViewController: ParentViewController { menubarDisplayOptionsLabel.stringValue = "Menubar Display Options" menubarModeLabel.stringValue = "Menubar Mode" - [headerLabel, timeFormatLabel, panelTheme, dayDisplayOptionsLabel, showSliderLabel, showSecondsLabel, showSunriseLabel, largerTextLabel, futureSliderRangeLabel, includeDayLabel, includeDateLabel, includePlaceLabel, menubarDisplayOptionsLabel, appDisplayLabel, menubarModeLabel].forEach { + [headerLabel, timeFormatLabel, panelTheme, + dayDisplayOptionsLabel, showSliderLabel, showSecondsLabel, + showSunriseLabel, largerTextLabel, futureSliderRangeLabel, + includeDayLabel, includeDateLabel, includePlaceLabel, + menubarDisplayOptionsLabel, appDisplayLabel, menubarModeLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() } } diff --git a/Clocker/Preferences/Calendar/CalendarViewController.swift b/Clocker/Preferences/Calendar/CalendarViewController.swift index 85ad0fe..93df181 100644 --- a/Clocker/Preferences/Calendar/CalendarViewController.swift +++ b/Clocker/Preferences/Calendar/CalendarViewController.swift @@ -224,7 +224,9 @@ class CalendarViewController: ParentViewController { showEventsFromLabel.stringValue = "Show events from" truncateAccessoryLabel.stringValue = "If meeting title is \"Meeting with Neel\" and truncate length is set to 5, text in menubar will appear as \"Meeti...\"" - [headerLabel, upcomingEventView, allDayMeetingsLabel, showNextMeetingLabel, nextMeetingAccessoryLabel, truncateTextLabel, showEventsFromLabel, charactersField, truncateAccessoryLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() } + [headerLabel, upcomingEventView, allDayMeetingsLabel, + showNextMeetingLabel, nextMeetingAccessoryLabel, truncateTextLabel, + showEventsFromLabel, charactersField, truncateAccessoryLabel].forEach { $0?.textColor = Themer.shared().mainTextColor() } } } @@ -253,12 +255,14 @@ extension CalendarViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - if let currentSource = calendars[row] as? String, let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sourceCellView"), owner: self) as? SourceTableViewCell { + if let currentSource = calendars[row] as? String, + let message = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "sourceCellView"), owner: self) as? SourceTableViewCell { message.sourceName.stringValue = currentSource return message } - if let currentSource = calendars[row] as? CalendarInfo, let calendarCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "calendarCellView"), owner: self) as? CalendarTableViewCell { + if let currentSource = calendars[row] as? CalendarInfo, + let calendarCell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "calendarCellView"), owner: self) as? CalendarTableViewCell { calendarCell.calendarName.stringValue = currentSource.calendar.title calendarCell.calendarSelected.state = currentSource.selected ? NSControl.StateValue.on : NSControl.StateValue.off calendarCell.calendarSelected.target = self diff --git a/Clocker/Preferences/General/PreferencesViewController.swift b/Clocker/Preferences/General/PreferencesViewController.swift index 9ca74e7..c0d5a15 100644 --- a/Clocker/Preferences/General/PreferencesViewController.swift +++ b/Clocker/Preferences/General/PreferencesViewController.swift @@ -479,7 +479,10 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate UserDefaults.standard.set(filteredMenubars, forKey: CLMenubarFavorites) - if let appDelegate = NSApplication.shared.delegate as? AppDelegate, let menubarFavourites = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data], menubarFavourites.isEmpty, DataStore.shared().shouldDisplay(.showMeetingInMenubar) == false { + if let appDelegate = NSApplication.shared.delegate as? AppDelegate, + let menubarFavourites = DataStore.shared().retrieve(key: CLMenubarFavorites) as? [Data], + menubarFavourites.isEmpty, + DataStore.shared().shouldDisplay(.showMeetingInMenubar) == false { appDelegate.invalidateMenubarTimer(true) } @@ -532,10 +535,15 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate // Time to display the alert. NSApplication.shared.activate(ignoringOtherApps: true) + let infoText = """ + Multiple timezones occupy space and if macOS determines Clocker is occupying too much space, it'll hide Clocker entirely! + Enable Menubar Compact Mode to fit in more timezones in less space. + """ + let alert = NSAlert() alert.showsSuppressionButton = true alert.messageText = "More than one location added to the menubar 😅" - alert.informativeText = "Multiple timezones occupy space and if macOS determines Clocker is occupying too much space, it'll hide Clocker entirely! Enable Menubar Compact Mode to fit in more timezones in less space." + alert.informativeText = infoText alert.addButton(withTitle: "Enable Compact Mode") alert.addButton(withTitle: "Cancel") @@ -626,13 +634,21 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate } if tableColumn.identifier.rawValue == "formattedAddress" { - return arePlacesSortedInAscendingOrder ? object1.formattedAddress! > object2.formattedAddress! : object1.formattedAddress! < object2.formattedAddress! + return arePlacesSortedInAscendingOrder ? + object1.formattedAddress! > object2.formattedAddress! : + object1.formattedAddress! < object2.formattedAddress! } else { - return arePlacesSortedInAscendingOrder ? object1.customLabel! > object2.customLabel! : object1.customLabel! < object2.customLabel! + return arePlacesSortedInAscendingOrder ? + object1.customLabel! > object2.customLabel! : + object1.customLabel! < object2.customLabel! } } - arePlacesSortedInAscendingOrder ? timezoneTableView.setIndicatorImage(NSImage(named: NSImage.Name("NSDescendingSortIndicator"))!, in: tableColumn) : timezoneTableView.setIndicatorImage(NSImage(named: NSImage.Name("NSAscendingSortIndicator"))!, in: tableColumn) + let indicatorImage = arePlacesSortedInAscendingOrder ? + NSImage(named: NSImage.Name("NSDescendingSortIndicator"))! : + NSImage(named: NSImage.Name("NSAscendingSortIndicator"))! + + timezoneTableView.setIndicatorImage(indicatorImage, in: tableColumn) arePlacesSortedInAscendingOrder.toggle() @@ -645,7 +661,7 @@ extension PreferencesViewController: NSTableViewDataSource, NSTableViewDelegate extension PreferencesViewController { @objc private func search() { - var searchString = searchField.stringValue + let searchString = searchField.stringValue if searchString.isEmpty { dataTask?.cancel() @@ -657,8 +673,6 @@ extension PreferencesViewController { dataTask?.cancel() } - let userPreferredLanguage = Locale.preferredLanguages.first ?? "en-US" - OperationQueue.main.addOperation { if self.availableTimezoneTableView.isHidden { self.availableTimezoneTableView.isHidden = false @@ -675,26 +689,14 @@ extension PreferencesViewController { self.placeholderLabel.placeholderString = "Searching for \(searchString)" - let words = searchString.components(separatedBy: CharacterSet.whitespacesAndNewlines) - - searchString = words.joined(separator: CLEmptyString) - - let urlString = "https://maps.googleapis.com/maps/api/geocode/json?address=\(searchString)&key=\(CLGeocodingKey)&language=\(userPreferredLanguage)" - - self.dataTask = NetworkManager.task(with: urlString, + self.dataTask = NetworkManager.task(with: self.generateSearchURL(), completionHandler: { [weak self] response, error in guard let `self` = self else { return } OperationQueue.main.addOperation { if let errorPresent = error { - if errorPresent.localizedDescription == PreferencesConstants.offlineErrorMessage { - self.placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError - } else { - self.placeholderLabel.placeholderString = PreferencesConstants.tryAgainMessage - } - - self.isActivityInProgress = false + self.presentError(errorPresent.localizedDescription) return } @@ -711,35 +713,61 @@ extension PreferencesViewController { return } - for result in searchResults!.results { - let location = result.geometry.location - let latitude = location.lat - let longitude = location.lng - let formattedAddress = result.formattedAddress - - let totalPackage = [ - "latitude": latitude, - "longitude": longitude, - CLTimezoneName: formattedAddress, - CLCustomLabel: formattedAddress, - CLTimezoneID: CLEmptyString, - CLPlaceIdentifier: result.placeId - ] as [String: Any] - - self.filteredArray.append(TimezoneData(with: totalPackage)) - } + self.appendResultsToFilteredArray(searchResults!.results) + self.prepareUIForPresentingResults() + } - self.placeholderLabel.placeholderString = CLEmptyString + }) + } + } - self.isActivityInProgress = false + private func generateSearchURL() -> String { + let userPreferredLanguage = Locale.preferredLanguages.first ?? "en-US" - self.availableTimezoneTableView.reloadData() - } + var searchString = searchField.stringValue + let words = searchString.components(separatedBy: CharacterSet.whitespacesAndNewlines) + searchString = words.joined(separator: CLEmptyString) - }) + let url = "https://maps.googleapis.com/maps/api/geocode/json?address=\(searchString)&key=\(CLGeocodingKey)&language=\(userPreferredLanguage)" + return url + } + + private func presentError(_ errorMessage: String) { + if errorMessage == PreferencesConstants.offlineErrorMessage { + self.placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError + } else { + self.placeholderLabel.placeholderString = PreferencesConstants.tryAgainMessage + } + + self.isActivityInProgress = false + } + + private func appendResultsToFilteredArray(_ results: [SearchResult.Result]) { + results.forEach { + let location = $0.geometry.location + let latitude = location.lat + let longitude = location.lng + let formattedAddress = $0.formattedAddress + + let totalPackage = [ + "latitude": latitude, + "longitude": longitude, + CLTimezoneName: formattedAddress, + CLCustomLabel: formattedAddress, + CLTimezoneID: CLEmptyString, + CLPlaceIdentifier: $0.placeId + ] as [String: Any] + + self.filteredArray.append(TimezoneData(with: totalPackage)) } } + private func prepareUIForPresentingResults() { + self.placeholderLabel.placeholderString = CLEmptyString + self.isActivityInProgress = false + self.availableTimezoneTableView.reloadData() + } + // Extracting this out for tests private func decode(from data: Data) -> SearchResult? { let jsonDecoder = JSONDecoder() @@ -802,34 +830,8 @@ extension PreferencesViewController { if error == nil, let json = response, let timezone = self.decodeTimezone(from: json) { if self.availableTimezoneTableView.selectedRow >= 0 && self.availableTimezoneTableView.selectedRow < self.filteredArray.count { - guard let dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow] as? TimezoneData else { - assertionFailure("Data was unexpectedly nil") - return - } - - var filteredAddress = "Error" - - if let address = dataObject.formattedAddress { - filteredAddress = address.filteredName() - } - - let newTimeZone = [ - CLTimezoneID: timezone.timeZoneId, - CLTimezoneName: filteredAddress, - CLPlaceIdentifier: dataObject.placeID!, - "latitude": dataObject.latitude!, - "longitude": dataObject.longitude!, - "nextUpdate": CLEmptyString, - CLCustomLabel: filteredAddress - ] as [String: Any] - - let timezoneObject = TimezoneData(with: newTimeZone) - let operationsObject = TimezoneDataOperations(with: timezoneObject) - operationsObject.saveObject() - - Logger.log(object: ["PlaceName": filteredAddress, "Timezone": timezone.timeZoneId], for: "Filtered Address") + self.installTimezone(timezone) } - self.updateViewState() } else { OperationQueue.main.addOperation { @@ -846,6 +848,35 @@ extension PreferencesViewController { } } + private func installTimezone(_ timezone: Timezone) { + guard let dataObject = self.filteredArray[self.availableTimezoneTableView.selectedRow] as? TimezoneData else { + assertionFailure("Data was unexpectedly nil") + return + } + + var filteredAddress = "Error" + + if let address = dataObject.formattedAddress { + filteredAddress = address.filteredName() + } + + let newTimeZone = [ + CLTimezoneID: timezone.timeZoneId, + CLTimezoneName: filteredAddress, + CLPlaceIdentifier: dataObject.placeID!, + "latitude": dataObject.latitude!, + "longitude": dataObject.longitude!, + "nextUpdate": CLEmptyString, + CLCustomLabel: filteredAddress + ] as [String: Any] + + let timezoneObject = TimezoneData(with: newTimeZone) + let operationsObject = TimezoneDataOperations(with: timezoneObject) + operationsObject.saveObject() + + Logger.log(object: ["PlaceName": filteredAddress, "Timezone": timezone.timeZoneId], for: "Filtered Address") + } + private func resetStateAndShowDisconnectedMessage() { OperationQueue.main.addOperation { self.showMessage() @@ -969,55 +1000,59 @@ extension PreferencesViewController { } } else { - let data = TimezoneData() - data.setLabel(CLEmptyString) + cleanupAfterInstallingTimezone() + } + } - if searchField.stringValue.isEmpty == false { - if timezoneFilteredArray.count <= availableTimezoneTableView.selectedRow { - return - } + private func cleanupAfterInstallingTimezone() { + let data = TimezoneData() + data.setLabel(CLEmptyString) - let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow] + if searchField.stringValue.isEmpty == false { + if timezoneFilteredArray.count <= availableTimezoneTableView.selectedRow { + return + } - let metaInfo = metadata(for: currentSelection) - data.timezoneID = metaInfo.0 - data.formattedAddress = metaInfo.1 + let currentSelection = timezoneFilteredArray[availableTimezoneTableView.selectedRow] - } else { - let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow] + let metaInfo = metadata(for: currentSelection) + data.timezoneID = metaInfo.0 + data.formattedAddress = metaInfo.1 - let metaInfo = metadata(for: currentSelection) - data.timezoneID = metaInfo.0 - data.formattedAddress = metaInfo.1 - } + } else { + let currentSelection = timezoneArray[availableTimezoneTableView.selectedRow] - data.selectionType = .timezone + let metaInfo = metadata(for: currentSelection) + data.timezoneID = metaInfo.0 + data.formattedAddress = metaInfo.1 + } - let operationObject = TimezoneDataOperations(with: data) - operationObject.saveObject() + data.selectionType = .timezone - timezoneFilteredArray = [] + let operationObject = TimezoneDataOperations(with: data) + operationObject.saveObject() - timezoneArray = [] + timezoneFilteredArray = [] - availableTimezoneTableView.reloadData() + timezoneArray = [] - refreshTimezoneTableView() + availableTimezoneTableView.reloadData() + + refreshTimezoneTableView() - refreshMainTable() + refreshMainTable() - timezonePanel.close() + timezonePanel.close() - placeholderLabel.placeholderString = CLEmptyString + placeholderLabel.placeholderString = CLEmptyString - searchField.stringValue = CLEmptyString + searchField.stringValue = CLEmptyString - searchField.placeholderString = "Enter a city, state or country name" + searchField.placeholderString = "Enter a city, state or country name" - availableTimezoneTableView.isHidden = false + availableTimezoneTableView.isHidden = false - isActivityInProgress = false - } + isActivityInProgress = false } private func metadata(for selection: String) -> (String, String) { diff --git a/Clocker/Preferences/OneWindowController.swift b/Clocker/Preferences/OneWindowController.swift index 3a6cb9d..1acecb5 100644 --- a/Clocker/Preferences/OneWindowController.swift +++ b/Clocker/Preferences/OneWindowController.swift @@ -35,7 +35,7 @@ class OneWindowController: NSWindowController { NSAnimationContext.runAnimationGroup({ (context) in context.duration = 1 - context.timingFunction = CAMediaTimingFunction(name:CAMediaTimingFunctionName.easeOut) + context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) self.window?.animator().backgroundColor = Themer.shared().mainBackgroundColor() }) @@ -60,8 +60,8 @@ class OneWindowController: NSWindowController { } class func shared() -> OneWindowController { - if (sharedWindow == nil) { - let prefStoryboard = NSStoryboard.init(name: "Preferences", bundle: nil) + if sharedWindow == nil { + let prefStoryboard = NSStoryboard.init(name: "Preferences", bundle: nil) sharedWindow = prefStoryboard.instantiateInitialController() as? OneWindowController } return sharedWindow @@ -95,7 +95,7 @@ class OneWindowController: NSWindowController { tabViewController.tabViewItems.forEach { (tabViewItem) in let identity = (tabViewItem.identifier as? String) ?? "" - if (identifierTOImageMapping[identity] != nil) { + if identifierTOImageMapping[identity] != nil { tabViewItem.image = identifierTOImageMapping[identity] } }