diff --git a/Clocker/AppDelegate.swift b/Clocker/AppDelegate.swift index 653736f..cfcac83 100644 --- a/Clocker/AppDelegate.swift +++ b/Clocker/AppDelegate.swift @@ -53,9 +53,6 @@ open class AppDelegate: NSObject, NSApplicationDelegate { AppDefaults.initialize() - // For users, still on the old timezones, only migrate timezonezes once setClass has been called - migrateOverridenTimezones() - // Check if we can show the onboarding flow! showOnboardingFlowIfEligible() @@ -68,28 +65,6 @@ open class AppDelegate: NSObject, NSApplicationDelegate { #endif } - private func migrateOverridenTimezones() { - let defaults = UserDefaults.standard - if let shortCircuit = defaults.object(forKey: "MigrateIndividualTimezoneFormat") as? Bool, shortCircuit == true { - return - } - - let timezones = DataStore.shared().timezones() - var migratedTimezones: [Data] = [] - - for encodedTimezone in timezones { - if let timezoneObject = TimezoneData.customObject(from: encodedTimezone) { - timezoneObject.setShouldOverrideGlobalTimeFormat(0) - migratedTimezones.append(NSKeyedArchiver.archivedData(withRootObject: timezoneObject)) - } - } - - if migratedTimezones.count > 0 { - defaults.set(migratedTimezones, forKey: CLDefaultPreferenceKey) - defaults.set(true, forKey: "MigrateIndividualTimezoneFormat") - } - } - public func applicationDockMenu(_: NSApplication) -> NSMenu? { let menu = NSMenu(title: "Quick Access") @@ -279,12 +254,8 @@ open class AppDelegate: NSObject, NSApplicationDelegate { NSApp.activate(ignoringOtherApps: true) } - open func setupFloatingWindow() { - showFloatingWindow() - } - - open func closeFloatingWindow() { - floatingWindow.window?.close() + open func setupFloatingWindow(_ hide: Bool) { + hide ? floatingWindow.window?.close() : showFloatingWindow() } func statusItemForPanel() -> StatusItemHandler { diff --git a/Clocker/Clocker.xcodeproj/project.pbxproj b/Clocker/Clocker.xcodeproj/project.pbxproj index 5f827e9..44bec66 100755 --- a/Clocker/Clocker.xcodeproj/project.pbxproj +++ b/Clocker/Clocker.xcodeproj/project.pbxproj @@ -136,7 +136,6 @@ 9A3169C11D2CC5AA0079FDF8 /* com.abhishek.ClockerHelper.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A3169C01D2CC5AA0079FDF8 /* com.abhishek.ClockerHelper.plist */; }; 9A43792A1BEC230A00F4E27F /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A4379291BEC230A00F4E27F /* libc++.tbd */; }; 9A56DB801C1CFB73004CE6AF /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A56DB7D1C1CFB73004CE6AF /* MainMenu.xib */; }; - 9A5951BD1C1D0A8D009C17AA /* CommonStrings.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A5951BB1C1D0A8D009C17AA /* CommonStrings.m */; }; 9A5E75E4204CC39700119939 /* ShortcutRecorder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A86E2BC1CE04F1600547EE7 /* ShortcutRecorder.framework */; }; 9A5E75E5204CC39700119939 /* ShortcutRecorder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A86E2BC1CE04F1600547EE7 /* ShortcutRecorder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9A5E75E8204CC39700119939 /* PTHotKey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A86E2BE1CE04F1600547EE7 /* PTHotKey.framework */; }; @@ -386,8 +385,6 @@ 9A4DC4E82337F5D600F03FA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; 9A4DC4E92337F5D600F03FA4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; 9A56DB7D1C1CFB73004CE6AF /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MainMenu.xib; path = Clocker/MainMenu.xib; sourceTree = ""; }; - 9A5951BB1C1D0A8D009C17AA /* CommonStrings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CommonStrings.m; path = Clocker/Utilities/CommonStrings.m; sourceTree = ""; }; - 9A5951BC1C1D0A8D009C17AA /* CommonStrings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CommonStrings.h; path = Clocker/Utilities/CommonStrings.h; sourceTree = ""; }; 9A5B1A8D1BECDFB700A77C68 /* Clocker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = Clocker.entitlements; path = Clocker/Clocker.entitlements; sourceTree = ""; }; 9A5E6B9F1CAF71C1006E7C5C /* libicucore.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libicucore.tbd; path = usr/lib/libicucore.tbd; sourceTree = SDKROOT; }; 9A6D93361CF3E82F005A8690 /* CoreImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreImage.framework; path = System/Library/Frameworks/CoreImage.framework; sourceTree = SDKROOT; }; @@ -727,8 +724,6 @@ isa = PBXGroup; children = ( 9A3169C01D2CC5AA0079FDF8 /* com.abhishek.ClockerHelper.plist */, - 9A5951BB1C1D0A8D009C17AA /* CommonStrings.m */, - 9A5951BC1C1D0A8D009C17AA /* CommonStrings.h */, 9A13BAE21CA882FA007C6CBE /* InfoPlist.strings */, 9A13BAEC1CA88A76007C6CBE /* Localizable.strings */, C2CCCD8120619C4C00F2DFC2 /* LocationController.swift */, @@ -1316,7 +1311,6 @@ 35C36F452259D892002FA5C6 /* Strings.swift in Sources */, 35C36EF722595F14002FA5C6 /* FinalOnboardingViewController.swift in Sources */, 35C36FA12259ED6D002FA5C6 /* EventCenter.swift in Sources */, - 9A5951BD1C1D0A8D009C17AA /* CommonStrings.m in Sources */, 357391872507277500D30819 /* TimeMarkerViewItem.swift in Sources */, 35C36F782259E1D0002FA5C6 /* Foundation + Additions.swift in Sources */, 35C36F16225961DA002FA5C6 /* Date+Inits.swift in Sources */, diff --git a/Clocker/Clocker/Utilities/CommonStrings.h b/Clocker/Clocker/Utilities/CommonStrings.h deleted file mode 100644 index baef60a..0000000 --- a/Clocker/Clocker/Utilities/CommonStrings.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright © 2015 Abhishek Banthia - -#import - -@interface CommonStrings : NSObject - -extern NSString *const CLEmptyString; -extern NSString *const CLDefaultPreferenceKey; -extern NSString *const CLTimezoneName; -extern NSString *const CLCustomLabel; -extern NSString *const CL24hourFormatSelectedKey; -extern NSString *const CLDragSessionKey; -extern NSString *const CLTimezoneID; -extern NSString *const CLPlaceIdentifier; -extern NSString *const CLRelativeDateKey; -extern NSString *const CLThemeKey; -extern NSString *const CLShowDayInMenu; -extern NSString *const CLShowDateInMenu; -extern NSString *const CLShowPlaceInMenu; -extern NSString *const CLDisplayFutureSliderKey; -extern NSString *const CLStartAtLogin; -extern NSString *const CLShowAppInForeground; -extern NSString *const CLSunriseSunsetTime; -extern NSString *const CLLocationSearchURL; -extern NSString *const CLShowSecondsInMenubar; -extern NSString *const CLUserFontSizePreference; -extern NSString *const CLShowUpcomingEventView; -extern NSString *const CLShowAllDayEventsInUpcomingView; -extern NSString *const CLShowMeetingInMenubar; -extern NSString *const CLTruncateTextLength; -extern NSString *const CLFutureSliderRange; -extern NSString *const CLSelectedCalendars; - -@end diff --git a/Clocker/Clocker/Utilities/CommonStrings.m b/Clocker/Clocker/Utilities/CommonStrings.m deleted file mode 100644 index f62f20c..0000000 --- a/Clocker/Clocker/Utilities/CommonStrings.m +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2015 Abhishek Banthia - -#import "CommonStrings.h" - -@implementation CommonStrings - -NSString *const CLEmptyString = @""; -NSString *const CLDefaultPreferenceKey = @"defaultPreferences"; -NSString *const CLTimezoneName = @"formattedAddress"; -NSString *const CLPlaceIdentifier = @"place_id"; -NSString *const CLTimezoneID = @"timezoneID"; -NSString *const CLCustomLabel = @"customLabel"; -NSString *const CL24hourFormatSelectedKey = @"is24HourFormatSelected"; -NSString *const CLDragSessionKey = @"public.text"; -NSString *const CLRelativeDateKey = @"relativeDate"; -NSString *const CLThemeKey = @"defaultTheme"; -NSString *const CLDisplayFutureSliderKey = @"displayFutureSlider"; -NSString *const CLShowDayInMenu = @"showDay"; -NSString *const CLShowDateInMenu = @"showDate"; -NSString *const CLShowPlaceInMenu = @"showPlaceName"; -NSString *const CLStartAtLogin = @"startAtLogin"; -NSString *const CLShowAppInForeground = @"displayAppAsForegroundApp"; -NSString *const CLSunriseSunsetTime = @"showSunriseSetTime"; -NSString *const CLShowSecondsInMenubar = @"showSeconds"; -NSString *const CLUserFontSizePreference = @"userFontSize"; -NSString *const CLShowUpcomingEventView = @"ShowUpcomingEventView"; -NSString *const CLFutureSliderRange = @"sliderDayRange"; -NSString *const CLShowAllDayEventsInUpcomingView = @"showAllDayEventsInUpcomingView"; -NSString *const CLShowMeetingInMenubar = @"showMeetingInfoInMenubar"; -NSString *const CLTruncateTextLength = @"truncateTextLength"; -NSString *const CLSelectedCalendars = @"SelectedCalendars"; - -@end diff --git a/Clocker/ClockerUITests/ClockerUITests.m b/Clocker/ClockerUITests/ClockerUITests.m index c33a74c..4335207 100644 --- a/Clocker/ClockerUITests/ClockerUITests.m +++ b/Clocker/ClockerUITests/ClockerUITests.m @@ -1,7 +1,6 @@ // Copyright © 2015 Abhishek Banthia #import -#import "CommonStrings.h" @interface ClockerUITests : XCTestCase diff --git a/Clocker/ClockerUnitTests/ClockerUnitTests.swift b/Clocker/ClockerUnitTests/ClockerUnitTests.swift index 77dd55b..13cf265 100644 --- a/Clocker/ClockerUnitTests/ClockerUnitTests.swift +++ b/Clocker/ClockerUnitTests/ClockerUnitTests.swift @@ -94,27 +94,54 @@ class ClockerUnitTests: XCTestCase { func testAddingATimezoneToDefaults() { let timezoneData = TimezoneData(with: california) - - let defaults = UserDefaults.standard - let currentFavourites = (defaults.object(forKey: CLDefaultPreferenceKey) as? [Data]) ?? [] + let currentFavourites = DataStore.shared().timezones() let oldCount = currentFavourites.count let operationsObject = TimezoneDataOperations(with: timezoneData) operationsObject.saveObject() - let newDefaults = UserDefaults.standard.object(forKey: CLDefaultPreferenceKey) as? [Data] + let newDefaults = DataStore.shared().timezones() - XCTAssert(newDefaults != nil) - XCTAssert(newDefaults?.count == oldCount + 1) + XCTAssert(newDefaults.isEmpty == false) + XCTAssert(newDefaults.count == oldCount + 1) + } + + func testDecoding() { + let timezone1 = TimezoneData.customObject(from: nil) + XCTAssertNotNil(timezone1) + + let data = Data() + let timezone2 = TimezoneData.customObject(from: data) + XCTAssertNil(timezone2) + } + + func testDescription() { + let timezoneData = TimezoneData(with: california) + XCTAssertFalse(timezoneData.description.isEmpty) + XCTAssertFalse(timezoneData.debugDescription.isEmpty) + } + + func testHashing() { + let timezoneData = TimezoneData(with: california) + XCTAssert(timezoneData.hash != -1) + + timezoneData.placeID = nil + timezoneData.timezoneID = nil + XCTAssert(timezoneData.hash == -1) + } + + func testBadInputDictionaryForInitialization() { + let badInput: [String: Any] = ["customLabel": "", + "latitude": "41.2565369", + "longitude": "-95.9345034"] + let badTimezoneData = TimezoneData(with: badInput) + XCTAssertEqual(badTimezoneData.placeID, "Error") + XCTAssertEqual(badTimezoneData.timezoneID, "Error") + XCTAssertEqual(badTimezoneData.formattedAddress, "Error") } func testDeletingATimezone() { - let defaults = UserDefaults.standard - - guard var currentFavourites = defaults.object(forKey: CLDefaultPreferenceKey) as? [Data] else { - XCTFail("Default preferences aren't in the correct format") - return - } + var currentFavourites = DataStore.shared().timezones() // Check if timezone with test identifier is present. let filteredCount = currentFavourites.filter { let timezone = TimezoneData.customObject(from: $0) @@ -128,14 +155,14 @@ class ClockerUnitTests: XCTestCase { operationsObject.saveObject() } - let oldCount = (defaults.object(forKey: CLDefaultPreferenceKey) as? [Data])?.count ?? 0 + let oldCount = DataStore.shared().timezones().count currentFavourites = currentFavourites.filter { let timezone = TimezoneData.customObject(from: $0) return timezone?.placeID != "TestIdentifier" } - defaults.set(currentFavourites, forKey: CLDefaultPreferenceKey) + DataStore.shared().setTimezones(currentFavourites) XCTAssertTrue(currentFavourites.count == oldCount - 1) } @@ -143,10 +170,10 @@ class ClockerUnitTests: XCTestCase { // The below test might fail outside California or if DST is active! // CI is calibrated to be on LA timezone! func testTimeDifference() { - XCTAssertTrue(operations.timeDifference() == ", 10h 30m ahead", "Difference was unexpectedly: \(operations.timeDifference())") + XCTAssertTrue(operations.timeDifference() == ", 9h 30m ahead", "Difference was unexpectedly: \(operations.timeDifference())") XCTAssertTrue(californiaOperations.timeDifference() == ", 3h behind", "Difference was unexpectedly: \(californiaOperations.timeDifference())") XCTAssertTrue(floridaOperations.timeDifference() == "", "Difference was unexpectedly: \(floridaOperations.timeDifference())") - XCTAssertTrue(aucklandOperations.timeDifference() == ", 18h ahead", "Difference was unexpectedly: \(aucklandOperations.timeDifference())") + XCTAssertTrue(aucklandOperations.timeDifference() == ", 17h ahead", "Difference was unexpectedly: \(aucklandOperations.timeDifference())") XCTAssertTrue(omahaOperations.timeDifference() == ", an hour behind", "Difference was unexpectedly: \(omahaOperations.timeDifference())") } @@ -207,6 +234,10 @@ class ClockerUnitTests: XCTestCase { dataObject.setShouldOverrideGlobalTimeFormat(11) // 12-hour with preceding zero and seconds XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "hh:mm:ss") + + // Wrong input + dataObject.setShouldOverrideGlobalTimeFormat(0) // 12-hour with preceding zero and seconds + XCTAssertTrue(dataObject.timezoneFormat(88) == "h:mm a") } func testTimezoneFormatWithDefaultSetAs24HourFormat() { @@ -214,7 +245,8 @@ class ClockerUnitTests: XCTestCase { UserDefaults.standard.set(NSNumber(value: 1), forKey: CLSelectedTimeZoneFormatKey) // Set to 24-Hour Format dataObject.setShouldOverrideGlobalTimeFormat(0) - XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "HH:mm") + XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "HH:mm", + "Unexpected format returned: \(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()))") dataObject.setShouldOverrideGlobalTimeFormat(1) // 12-Hour Format XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "h:mm a") @@ -242,17 +274,73 @@ class ClockerUnitTests: XCTestCase { dataObject.setShouldOverrideGlobalTimeFormat(11) // 12-hour with preceding zero and seconds XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "hh:mm:ss") + + dataObject.setShouldOverrideGlobalTimeFormat(12) // 12-hour with preceding zero and seconds + XCTAssertTrue(dataObject.timezoneFormat(DataStore.shared().timezoneFormat()) == "epoch") + } + + func testSecondsDisplayForOverridenTimezone() { + let dataObject = TimezoneData(with: california) + UserDefaults.standard.set(NSNumber(value: 1), forKey: CLSelectedTimeZoneFormatKey) // Set to 24-Hour Format + + // Test default behaviour + let timezoneWithSecondsKeys = [4,5,8,11] + for timezoneKey in timezoneWithSecondsKeys { + dataObject.setShouldOverrideGlobalTimeFormat(timezoneKey) + XCTAssertTrue(dataObject.shouldShowSeconds(DataStore.shared().timezoneFormat())) + } + + let timezoneWithoutSecondsKeys = [1,2,7,10] + for timezoneKey in timezoneWithoutSecondsKeys { + dataObject.setShouldOverrideGlobalTimeFormat(timezoneKey) + XCTAssertFalse(dataObject.shouldShowSeconds(DataStore.shared().timezoneFormat())) + } + + // Test wrong override timezone key + let wrongTimezoneKey = 88 + dataObject.setShouldOverrideGlobalTimeFormat(wrongTimezoneKey) + XCTAssertFalse(dataObject.shouldShowSeconds(DataStore.shared().timezoneFormat())) + + // Test wrong global preference key + dataObject.setShouldOverrideGlobalTimeFormat(0) + XCTAssertFalse(dataObject.shouldShowSeconds(88)) + } + + func testTimezoneRetrieval() { + let dataObject = TimezoneData(with: mumbai) + let autoupdatingTimezone = TimeZone.autoupdatingCurrent.identifier + XCTAssertEqual(dataObject.timezone(), "Asia/Calcutta") + + // Unlikely + dataObject.timezoneID = nil + XCTAssertEqual(dataObject.timezone(), autoupdatingTimezone) + + dataObject.isSystemTimezone = true + XCTAssertEqual(dataObject.timezone(), autoupdatingTimezone) } func testFormattedLabel() { let dataObject = TimezoneData(with: mumbai) - XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Ghar", "Incorrect custom label returned by model.") + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Ghar", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") - dataObject.customLabel = nil - XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Mumbai", "Incorrect custom label returned by model.") + dataObject.setLabel("") + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Mumbai", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") dataObject.formattedAddress = nil - XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Asia", "Incorrect custom label returned by model.") + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Asia", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") + + dataObject.setLabel("Jogeshwari") + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Jogeshwari", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") + + // Unlikely scenario + dataObject.setLabel("") + dataObject.timezoneID = "GMT" + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "GMT", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") + + // Another unlikely scenario + dataObject.setLabel("") + dataObject.timezoneID = nil + XCTAssertTrue(dataObject.formattedTimezoneLabel() == "Error", "Incorrect custom label returned by model \(dataObject.formattedTimezoneLabel())") } func testEquality() { diff --git a/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift b/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift index 081072a..f93625e 100644 --- a/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift +++ b/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift @@ -16,7 +16,7 @@ class StandardMenubarHandlerTests: XCTestCase { func testValidStandardMenubarHandler_returnMenubarTitle() { // Wipe all timezones from UserDefaults - UserDefaults.standard.setValue(nil, forKey: CLDefaultPreferenceKey) + DataStore.shared().setTimezones(nil) // Save a menubar selected timezone let dataObject = TimezoneData(with: mumbai) @@ -43,7 +43,7 @@ class StandardMenubarHandlerTests: XCTestCase { func testUnfavouritedTimezone_returnEmptyMenubarTimezoneCount() { // Wipe all timezones from UserDefaults - UserDefaults.standard.setValue(nil, forKey: CLDefaultPreferenceKey) + DataStore.shared().setTimezones(nil) // Save a menubar selected timezone let dataObject = TimezoneData(with: mumbai) @@ -57,7 +57,7 @@ class StandardMenubarHandlerTests: XCTestCase { func testUnfavouritedTimezone_returnNilMenubarString() { // Wipe all timezones from UserDefaults - UserDefaults.standard.setValue(nil, forKey: CLDefaultPreferenceKey) + DataStore.shared().setTimezones(nil) let menubarHandler = MenubarHandler() let emptyMenubarString = menubarHandler.titleForMenubar() // Returns early because DataStore.menubarTimezones is nil diff --git a/Clocker/CoreModelKit/Sources/CoreModelKit/TimezoneData.swift b/Clocker/CoreModelKit/Sources/CoreModelKit/TimezoneData.swift index b58e3e1..0c0d6bf 100644 --- a/Clocker/CoreModelKit/Sources/CoreModelKit/TimezoneData.swift +++ b/Clocker/CoreModelKit/Sources/CoreModelKit/TimezoneData.swift @@ -198,7 +198,6 @@ public class TimezoneData: NSObject, NSCoding { } else if shouldOverride == 4 { overrideFormat = .twelveHourWithSeconds } else if shouldOverride == 5 { - print("Setting override format to five") overrideFormat = .twentyHourWithSeconds } else if shouldOverride == 7 { overrideFormat = .twelveHourPrecedingZero @@ -211,7 +210,7 @@ public class TimezoneData: NSObject, NSCoding { } else if shouldOverride == 12 { overrideFormat = .epochTime } else { - assertionFailure("Chosen a wrong timezone format") + Logger.info("Chosen a wrong timezone format: \(shouldOverride)") } } @@ -226,16 +225,6 @@ public class TimezoneData: NSObject, NSCoding { return timezone } - if let name = formattedAddress, let placeIdentifier = placeID, let timezoneIdentifier = timezoneID { - let errorDictionary = [ - "Formatted Address": name, - "Place Identifier": placeIdentifier, - "TimezoneID": timezoneIdentifier, - ] - - Logger.log(object: errorDictionary, for: "Error fetching timezone() in TimezoneData") - } - return TimeZone.autoupdatingCurrent.identifier } @@ -273,7 +262,9 @@ public class TimezoneData: NSObject, NSCoding { return formatInString.contains("ss") } - let formatInString = TimezoneData.values[NSNumber(integerLiteral: overrideFormat.rawValue)] ?? DateFormat.twelveHour + // We subtract 1 because the timezone format in the dropdown contains 1 extra row for "Respecting global preferences" + let key = NSNumber(integerLiteral: overrideFormat.rawValue - 1) + let formatInString = TimezoneData.values[key] ?? DateFormat.twelveHour return formatInString.contains("ss") } @@ -285,17 +276,6 @@ public class TimezoneData: NSObject, NSCoding { return placeIdentifier.hashValue ^ timezone.hashValue } - static func == (lhs: TimezoneData, rhs: TimezoneData) -> Bool { - return lhs.placeID == rhs.placeID - } - - public override func isEqual(to object: Any?) -> Bool { - if let other = object as? TimezoneData { - return placeID == other.placeID - } - return false - } - public override func isEqual(_ object: Any?) -> Bool { guard let compared = object as? TimezoneData else { return false @@ -317,17 +297,17 @@ public extension TimezoneData { private func objectDescription() -> String { let customString = """ - TimezoneID: \(timezoneID ?? "Error") + TimezoneID: \(String(describing: timezoneID)) Formatted Address: \(formattedAddress ?? "Error") Custom Label: \(customLabel ?? "Error") Latitude: \(latitude ?? -0.0) Longitude: \(longitude ?? -0.0) - Place Identifier: \(placeID ?? "Error") + Place Identifier: \(String(describing: placeID)) Is Favourite: \(isFavourite) - Sunrise Time: \(sunriseTime?.debugDescription ?? "N/A") - Sunset Time: \(sunsetTime?.debugDescription ?? "N/A") + Sunrise Time: \(String(describing: sunriseTime)) + Sunset Time: \(String(describing: sunsetTime)) Selection Type: \(selectionType.rawValue) - Note: \(note ?? "Error") + Note: \(String(describing: note)) Is System Timezone: \(isSystemTimezone) Override: \(overrideFormat) """ diff --git a/Clocker/CoreModelKit/Tests/CoreModelKitTests/TimezoneDataEqualityTests.swift b/Clocker/CoreModelKit/Tests/CoreModelKitTests/TimezoneDataEqualityTests.swift index a143140..4da2c46 100644 --- a/Clocker/CoreModelKit/Tests/CoreModelKitTests/TimezoneDataEqualityTests.swift +++ b/Clocker/CoreModelKit/Tests/CoreModelKitTests/TimezoneDataEqualityTests.swift @@ -14,7 +14,8 @@ class TimezoneDataEqualityTests: XCTestCase { timezone2.timezoneID = "Africa/Banjul" timezone2.formattedAddress = "SameLabel" - XCTAssertNotEqual(timezone1, timezone2) + XCTAssertFalse(timezone1 == timezone2) // Test == + XCTAssertNotEqual(timezone1, timezone2) // Test isEqual } func testEqualityWhenTimezonesLabelsDiffer() { @@ -26,6 +27,7 @@ class TimezoneDataEqualityTests: XCTestCase { timezone2.timezoneID = TimeZone.autoupdatingCurrent.identifier timezone2.formattedAddress = "DifferentLabel" + XCTAssertFalse(timezone1 == timezone2) XCTAssertNotEqual(timezone1, timezone2) } @@ -40,6 +42,7 @@ class TimezoneDataEqualityTests: XCTestCase { timezone2.timezoneID = TimeZone.autoupdatingCurrent.identifier timezone2.formattedAddress = "DifferentLabel" + XCTAssertTrue(timezone1 == timezone2) XCTAssertEqual(timezone1, timezone2) } @@ -54,6 +57,7 @@ class TimezoneDataEqualityTests: XCTestCase { timezone2.timezoneID = TimeZone.autoupdatingCurrent.identifier timezone2.formattedAddress = "DifferentLabel" + XCTAssertFalse(timezone1 == timezone2) XCTAssertNotEqual(timezone1, timezone2) } } diff --git a/Clocker/Overall App/AppDefaults.swift b/Clocker/Overall App/AppDefaults.swift index 85ed394..66fbdcd 100644 --- a/Clocker/Overall App/AppDefaults.swift +++ b/Clocker/Overall App/AppDefaults.swift @@ -20,8 +20,9 @@ class AppDefaults { private class func initializeDefaults() { let userDefaults = UserDefaults.standard + let dataStore = DataStore.shared() - let timezones = userDefaults.object(forKey: CLDefaultPreferenceKey) + let timezones = dataStore.timezones() let selectedCalendars = userDefaults.object(forKey: CLSelectedCalendars) // Now delete the old preferences @@ -30,7 +31,7 @@ class AppDefaults { // Register the usual suspects userDefaults.register(defaults: defaultsDictionary()) - userDefaults.set(timezones, forKey: CLDefaultPreferenceKey) + dataStore.setTimezones(timezones) userDefaults.set(selectedCalendars, forKey: CLSelectedCalendars) // Set the theme default as Light! @@ -39,7 +40,7 @@ class AppDefaults { // If we already have timezones to display in menubar, do nothing. // Else, we switch the menubar mode default to compact mode for new users if userDefaults.bool(forKey: CLDefaultMenubarMode) == false { - if let menubarFavourites = userDefaults.object(forKey: CLDefaultPreferenceKey) as? [Data], menubarFavourites.isEmpty == false { + if let menubarFavourites = dataStore.menubarTimezones(), menubarFavourites.isEmpty == false { userDefaults.set(1, forKey: CLMenubarCompactMode) } else { userDefaults.set(0, forKey: CLMenubarCompactMode) @@ -137,6 +138,7 @@ extension UserDefaults { func wipeIfNeccesary() { if let bundleID = Bundle.main.bundleIdentifier, object(forKey: "PreferencesHaveBeenWiped") == nil { + Logger.info("Wiping all user defaults") removePersistentDomain(forName: bundleID) set(true, forKey: "PreferencesHaveBeenWiped") } diff --git a/Clocker/Overall App/DataStore.swift b/Clocker/Overall App/DataStore.swift index c9efbc2..5cedc4b 100644 --- a/Clocker/Overall App/DataStore.swift +++ b/Clocker/Overall App/DataStore.swift @@ -40,12 +40,24 @@ class DataStore: NSObject { } func timezones() -> [Data] { + if let cloudPreferences = NSUbiquitousKeyValueStore().object(forKey: CLDefaultPreferenceKey) as? [Data] { + Logger.info("Returning preferences from NSUbiquitousKeyValueStore") + return cloudPreferences + } + guard let preferences = userDefaults.object(forKey: CLDefaultPreferenceKey) as? [Data] else { return [] } return preferences } + + func setTimezones(_ timezones: [Data]?) { + userDefaults.set(timezones, forKey: CLDefaultPreferenceKey) + // iCloud sync + NSUbiquitousKeyValueStore().set(timezones, forKey: CLDefaultPreferenceKey) + NSUbiquitousKeyValueStore().synchronize() + } func menubarTimezones() -> [Data]? { return timezones().filter { @@ -70,10 +82,6 @@ class DataStore: NSObject { return shouldDisplayDateInMenubar } - func setTimezones(_ timezones: [Data]) { - userDefaults.set(timezones, forKey: CLDefaultPreferenceKey) - } - func retrieve(key: String) -> Any? { return userDefaults.object(forKey: key) } @@ -81,10 +89,9 @@ class DataStore: NSObject { func addTimezone(_ timezone: TimezoneData) { let encodedTimezone = NSKeyedArchiver.archivedData(withRootObject: timezone) - var defaults: [Data] = (userDefaults.object(forKey: CLDefaultPreferenceKey) as? [Data]) ?? [] + var defaults: [Data] = timezones() defaults.append(encodedTimezone) - - userDefaults.set(defaults, forKey: CLDefaultPreferenceKey) + setTimezones(defaults) } func removeLastTimezone() { @@ -98,7 +105,7 @@ class DataStore: NSObject { Logger.log(object: [:], for: "Undo Action Executed during Onboarding") - userDefaults.set(currentLineup, forKey: CLDefaultPreferenceKey) + setTimezones(currentLineup) } private func shouldDisplayHelper(_ key: String) -> Bool { diff --git a/Clocker/Panel/FloatingWindowController.swift b/Clocker/Panel/FloatingWindowController.swift index fb6fc52..81112f4 100644 --- a/Clocker/Panel/FloatingWindowController.swift +++ b/Clocker/Panel/FloatingWindowController.swift @@ -49,7 +49,7 @@ class FloatingWindowController: ParentPanelController { } override func showNotesPopover(forRow row: Int, relativeTo positioningRect: NSRect, andButton target: NSButton!) -> Bool { - guard let popover = morePopover else { + guard let popover = additionalOptionsPopover else { return false } diff --git a/Clocker/Panel/PanelController.swift b/Clocker/Panel/PanelController.swift index 5464c4d..af798f7 100644 --- a/Clocker/Panel/PanelController.swift +++ b/Clocker/Panel/PanelController.swift @@ -302,7 +302,7 @@ class PanelController: ParentPanelController { NSAnimationContext.beginGrouping() NSAnimationContext.current.duration = 0.1 window?.animator().alphaValue = 0 - morePopover?.close() + additionalOptionsPopover?.close() NSAnimationContext.endGrouping() window?.orderOut(nil) @@ -329,11 +329,11 @@ class PanelController: ParentPanelController { } override func showNotesPopover(forRow row: Int, relativeTo positioningRect: NSRect, andButton target: NSButton!) -> Bool { - if morePopover == nil { - morePopover = NSPopover() + if additionalOptionsPopover == nil { + additionalOptionsPopover = NSPopover() } - guard let popover = morePopover else { + guard let popover = additionalOptionsPopover else { return false } diff --git a/Clocker/Panel/ParentPanelController.swift b/Clocker/Panel/ParentPanelController.swift index 3b2ae11..c688d9f 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -33,7 +33,7 @@ class ParentPanelController: NSWindowController { var previousPopoverRow: Int = -1 - var morePopover: NSPopover? + var additionalOptionsPopover: NSPopover? var datasource: TimezoneDataSource? @@ -153,36 +153,53 @@ class ParentPanelController: NSWindowController { override func awakeFromNib() { super.awakeFromNib() + // Setup table mainTableView.backgroundColor = NSColor.clear mainTableView.selectionHighlightStyle = .none mainTableView.enclosingScrollView?.hasVerticalScroller = false + if #available(OSX 11.0, *) { + mainTableView.style = .fullWidth + } + // Setup images let sharedThemer = Themer.shared() shutdownButton.image = sharedThemer.shutdownImage() preferencesButton.image = sharedThemer.preferenceImage() pinButton.image = sharedThemer.pinImage() sharingButton.image = sharedThemer.sharingImage() - if let upcomingView = upcomingEventContainerView { - upcomingView.setAccessibility("UpcomingEventView") - } - + // Setup KVO observers for user default changes setupObservers() updateReviewViewFontColor() + // Setup layers futureSliderView.wantsLayer = true reviewView.wantsLayer = true + // Setup notifications NotificationCenter.default.addObserver(self, selector: #selector(themeChanged), name: Notification.Name.themeDidChange, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(systemTimezoneDidChange), + name: NSNotification.Name.NSSystemTimeZoneDidChange, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(ubiquitousStoreDidChange), + name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, + object: nil) + // Setup upcoming events view + upcomingEventContainerView.setAccessibility("UpcomingEventView") determineUpcomingViewVisibility() + setupUpcomingEventViewCollectionViewIfNeccesary() + // Setup colors based on the curren theme themeChanged() + // UI adjustments based on user preferences if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false { futureSliderView.isHidden = true if modernContainerView != nil { @@ -203,22 +220,10 @@ class ParentPanelController: NSWindowController { } } + // More UI adjustments sharingButton.sendAction(on: .leftMouseDown) - adjustFutureSliderBasedOnPreferences() - - NotificationCenter.default.addObserver(self, - selector: #selector(timezoneGonnaChange), - name: NSNotification.Name.NSSystemTimeZoneDidChange, - object: nil) - - if #available(OSX 11.0, *) { - mainTableView.style = .fullWidth - } - setupModernSliderIfNeccessary() - setupUpcomingEventViewCollectionViewIfNeccesary() - if roundedDateView != nil { setupRoundedDateView() } @@ -231,7 +236,7 @@ class ParentPanelController: NSWindowController { roundedDateView.layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor } - @objc func timezoneGonnaChange() { + @objc func systemTimezoneDidChange() { OperationQueue.main.addOperation { /* let locationController = LocationController.sharedController() @@ -241,6 +246,14 @@ class ParentPanelController: NSWindowController { coordinates: nil) } } + + // Backing defaults changed + @objc func ubiquitousStoreDidChange() { + OperationQueue.main.addOperation { + self.mainTableView.reloadData() + self.setScrollViewConstraint() + } + } private func updateHomeObject(with customLabel: String, coordinates: CLLocationCoordinate2D?) { let timezones = DataStore.shared().timezones() @@ -366,7 +379,7 @@ class ParentPanelController: NSWindowController { override func windowDidLoad() { super.windowDidLoad() - morePopover = NSPopover() + additionalOptionsPopover = NSPopover() } func screenHeight() -> CGFloat { @@ -640,7 +653,7 @@ class ParentPanelController: NSWindowController { func showNotesPopover(forRow row: Int, relativeTo _: NSRect, andButton target: NSButton!) -> Bool { let defaults = DataStore.shared().timezones() - guard let popover = morePopover else { + guard let popover = additionalOptionsPopover else { assertionFailure("Data was unexpectedly nil") return false } @@ -773,9 +786,9 @@ class ParentPanelController: NSWindowController { close() if inverseSelection.isEqual(to: NSNumber(value: 1)) { - sharedDelegate.setupFloatingWindow() + sharedDelegate.setupFloatingWindow(false) } else { - sharedDelegate.closeFloatingWindow() + sharedDelegate.setupFloatingWindow(true) sharedDelegate.setPanelDefaults() } @@ -834,7 +847,7 @@ class ParentPanelController: NSWindowController { if notePopover != nil, let isShown = notePopover?.popover?.isShown, isShown { notePopover?.popover?.close() } - morePopover = nil + additionalOptionsPopover = nil } // MARK: Review @@ -987,6 +1000,12 @@ class ParentPanelController: NSWindowController { NSWorkspace.shared.open(sourceURL) } + + @objc func openFAQs() { + guard let sourceURL = URL(string: AboutUsConstants.FAQsLink) else { return } + + NSWorkspace.shared.open(sourceURL) + } @IBAction func showMoreOptions(_ sender: NSButton) { let menuItem = NSMenu(title: "More Options") @@ -1010,6 +1029,7 @@ class ParentPanelController: NSWindowController { clockerVersionInfo.isEnabled = false menuItem.addItem(openPreferences) menuItem.addItem(rateClocker) + menuItem.addItem(withTitle: "FAQs", action: #selector(openFAQs), keyEquivalent: "") menuItem.addItem(sendFeedback) menuItem.addItem(localizeClocker) menuItem.addItem(NSMenuItem.separator()) diff --git a/Clocker/Preferences/About/AboutViewController.swift b/Clocker/Preferences/About/AboutViewController.swift index f7de491..81be4ed 100644 --- a/Clocker/Preferences/About/AboutViewController.swift +++ b/Clocker/Preferences/About/AboutViewController.swift @@ -11,6 +11,7 @@ struct AboutUsConstants { static let TwitterFollowIntentLink = "https://twitter.com/intent/follow?screen_name=clocker_support" static let AppStoreLink = "macappstore://itunes.apple.com/us/app/clocker/id1056643111?action=write-review" static let CrowdInLocalizationLink = "https://crwd.in/clocker" + static let FAQsLink = "https://abhishekbanthia.com/clocker/faq" } class AboutViewController: ParentViewController { diff --git a/Clocker/Preferences/Appearance/AppearanceViewController.swift b/Clocker/Preferences/Appearance/AppearanceViewController.swift index b1d762e..8294fe1 100644 --- a/Clocker/Preferences/Appearance/AppearanceViewController.swift +++ b/Clocker/Preferences/Appearance/AppearanceViewController.swift @@ -189,7 +189,7 @@ class AppearanceViewController: ParentViewController { if let selectedFormat = sender.selectedItem?.title, selectedFormat.contains("ss") { - print("Seconds are contained") + Logger.info("Selected format contains timezone format") guard let panelController = PanelController.panel() else { return } panelController.pauseTimer() } diff --git a/Clocker/Preferences/General/PreferencesViewController.swift b/Clocker/Preferences/General/PreferencesViewController.swift index b1d362c..422c407 100644 --- a/Clocker/Preferences/General/PreferencesViewController.swift +++ b/Clocker/Preferences/General/PreferencesViewController.swift @@ -18,50 +18,39 @@ struct PreferencesConstants { static let hotKeyPathIdentifier = "values.globalPing" } -class TableHeaderViewCell: NSTableHeaderCell { - var backgroundColour: NSColor = NSColor.black { - didSet { - backgroundColor = backgroundColour - } - } +class PreferencesViewController: ParentViewController { - override init(textCell: String) { - super.init(textCell: textCell) - let attributedParagraphStyle = NSMutableParagraphStyle() - attributedParagraphStyle.alignment = .left - attributedStringValue = NSAttributedString(string: textCell, - attributes: [.foregroundColor: Themer.shared().mainTextColor(), - .font: NSFont(name: "Avenir", size: 14)!, - .paragraphStyle: attributedParagraphStyle]) - backgroundColor = Themer.shared().textBackgroundColor() - } + @IBOutlet private var placeholderLabel: NSTextField! + @IBOutlet private var timezoneTableView: NSTableView! + @IBOutlet private var availableTimezoneTableView: NSTableView! + @IBOutlet private var timezonePanel: Panelr! + @IBOutlet private var progressIndicator: NSProgressIndicator! + @IBOutlet private var addButton: NSButton! + @IBOutlet private var recorderControl: SRRecorderControl! + @IBOutlet private var closeButton: NSButton! - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + @IBOutlet private var timezoneSortButton: NSButton! + @IBOutlet private var timezoneNameSortButton: NSButton! + @IBOutlet private var labelSortButton: NSButton! + @IBOutlet private var deleteButton: NSButton! + @IBOutlet private var addTimezoneButton: NSButton! - override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { - super.draw(withFrame: cellFrame, in: controlView) - if !controlView.isHidden { - backgroundColor?.setFill() - cellFrame.fill() - drawInterior(withFrame: cellFrame, in: controlView) - } - } + @IBOutlet private var searchField: NSSearchField! + @IBOutlet private var messageLabel: NSTextField! - override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { - if !controlView.isHidden { - if let avenirFont = NSFont(name: "Avenir", size: 14) { - font = avenirFont - } - textColor = NSColor.white - let rect = titleRect(forBounds: cellFrame) - attributedStringValue.draw(in: rect) - } - } -} + @IBOutlet private var tableview: NSView! + @IBOutlet private var additionalSortOptions: NSView! + @IBOutlet var startAtLoginLabel: NSTextField! -class PreferencesViewController: ParentViewController { + @IBOutlet var startupCheckbox: NSButton! + + // Sorting + private var arePlacesSortedInAscendingOrder = false + private var arePlacesSortedInAscendingTimezoneOrder = false + private var isTimezoneSortOptionSelected = false + private var isTimezoneNameSortOptionSelected = false + private var isLabelOptionSelected = false + private var isActivityInProgress = false { didSet { OperationQueue.main.addOperation { @@ -76,6 +65,11 @@ class PreferencesViewController: ParentViewController { return DataStore.shared().timezones() } + private var themeDidChangeNotification: NSObjectProtocol? + // Selected Timezones Data Source + private var selectionsDataSource: PreferencesDataSource! + // Search Results Data Source Handler + private var searchResultsDataSource: SearchDataSource! private lazy var startupManager = StartupManager() private var dataTask: URLSessionDataTask? = .none @@ -94,43 +88,7 @@ class PreferencesViewController: ParentViewController { return apiKey }() - // Sorting - private var arePlacesSortedInAscendingOrder = false - private var arePlacesSortedInAscendingTimezoneOrder = false - private var isTimezoneSortOptionSelected = false - private var isTimezoneNameSortOptionSelected = false - private var isLabelOptionSelected = false - @IBOutlet private var placeholderLabel: NSTextField! - @IBOutlet private var timezoneTableView: NSTableView! - @IBOutlet private var availableTimezoneTableView: NSTableView! - @IBOutlet private var timezonePanel: Panelr! - @IBOutlet private var progressIndicator: NSProgressIndicator! - @IBOutlet private var addButton: NSButton! - @IBOutlet private var recorderControl: SRRecorderControl! - @IBOutlet private var closeButton: NSButton! - - @IBOutlet private var timezoneSortButton: NSButton! - @IBOutlet private var timezoneNameSortButton: NSButton! - @IBOutlet private var labelSortButton: NSButton! - @IBOutlet private var deleteButton: NSButton! - @IBOutlet private var addTimezoneButton: NSButton! - - @IBOutlet private var searchField: NSSearchField! - @IBOutlet private var messageLabel: NSTextField! - - @IBOutlet private var tableview: NSView! - @IBOutlet private var additionalSortOptions: NSView! - @IBOutlet var startAtLoginLabel: NSTextField! - - @IBOutlet var startupCheckbox: NSButton! - - private var themeDidChangeNotification: NSObjectProtocol? - - // Selected Timezones Data Source - private var selectionsDataSource: PreferencesDataSource! - // Search Results Data Source Handler - private var searchResultsDataSource: SearchDataSource! override func viewDidLoad() { super.viewDidLoad() @@ -530,12 +488,7 @@ extension PreferencesViewController { } private func presentError(_ errorMessage: String) { - if errorMessage == PreferencesConstants.offlineErrorMessage { - placeholderLabel.placeholderString = PreferencesConstants.noInternetConnectivityError - } else { - placeholderLabel.placeholderString = PreferencesConstants.tryAgainMessage - } - + placeholderLabel.placeholderString = errorMessage == PreferencesConstants.offlineErrorMessage ? PreferencesConstants.noInternetConnectivityError : PreferencesConstants.tryAgainMessage isActivityInProgress = false } diff --git a/Clocker/Preferences/Menu Bar/StatusContainerView.swift b/Clocker/Preferences/Menu Bar/StatusContainerView.swift index a7d12b9..66808f0 100644 --- a/Clocker/Preferences/Menu Bar/StatusContainerView.swift +++ b/Clocker/Preferences/Menu Bar/StatusContainerView.swift @@ -177,7 +177,13 @@ class StatusContainerView: NSView { if newWidth != frame.size.width, newWidth > frame.size.width + 2.0 { Logger.info("Correcting our width to \(newWidth) and the previous width was \(frame.size.width)") - frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: newWidth, height: frame.size.height) + // NSView move animation + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.2 + let newFrame = CGRect(x: frame.origin.x, y: frame.origin.y, width: newWidth, height: frame.size.height) + // The view will animate to the new origin + self.animator().frame = newFrame + }) {} } } } diff --git a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift index ca9dbf8..4c52168 100644 --- a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift +++ b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift @@ -150,7 +150,7 @@ class StatusItemHandler: NSObject { } private func retrieveSyncedMenubarTimezones() -> [Data] { - let defaultPreferences = DataStore.shared().retrieve(key: CLDefaultPreferenceKey) as? [Data] ?? [] + let defaultPreferences = DataStore.shared().timezones() let menubarTimezones = defaultPreferences.filter { data -> Bool in if let timezoneObj = TimezoneData.customObject(from: data) {