You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
15 KiB
480 lines
15 KiB
// Copyright © 2015 Abhishek Banthia |
|
|
|
import Cocoa |
|
import os.log |
|
|
|
struct DateFormat { |
|
static let twelveHour = "h:mm a" |
|
static let twelveHourWithSeconds = "h:mm:ss a" |
|
static let twentyFourHour = "HH:mm" |
|
static let twentyFourHourWithSeconds = "HH:mm:ss" |
|
} |
|
|
|
// Non-class type cannot conform to NSCoding! |
|
class TimezoneData: NSObject, NSCoding { |
|
enum SelectionType: Int { |
|
case city |
|
case timezone |
|
} |
|
|
|
enum DateDisplayType: Int { |
|
case panel |
|
case menu |
|
} |
|
|
|
enum TimezoneOverride: Int { |
|
case twelveHourFormat |
|
case twentyFourFormat |
|
case globalFormat |
|
} |
|
|
|
enum SecondsOverride: Int { |
|
case showSeconds |
|
case hideSeconds |
|
case globalFormat |
|
} |
|
|
|
var customLabel: String? |
|
var formattedAddress: String? |
|
var placeID: String? |
|
var timezoneID: String? = CLEmptyString |
|
var latitude: Double? |
|
var longitude: Double? |
|
var note: String? = CLEmptyString |
|
var nextUpdate: Date? = Date() |
|
var sunriseTime: Date? |
|
var sunsetTime: Date? |
|
var isFavourite: Int = 0 |
|
var isSunriseOrSunset = false |
|
var selectionType: SelectionType = .city |
|
var isSystemTimezone = false |
|
var overrideFormat: TimezoneOverride = .globalFormat |
|
var overrideSecondsFormat: SecondsOverride = .globalFormat |
|
|
|
override init() { |
|
selectionType = .timezone |
|
isFavourite = 0 |
|
note = CLEmptyString |
|
isSystemTimezone = false |
|
overrideFormat = .globalFormat |
|
overrideSecondsFormat = .globalFormat |
|
placeID = UUID().uuidString |
|
} |
|
|
|
init(with originalTimezone: CLTimezoneData) { |
|
customLabel = originalTimezone.customLabel |
|
formattedAddress = originalTimezone.formattedAddress |
|
placeID = originalTimezone.place_id |
|
timezoneID = originalTimezone.timezoneID |
|
|
|
if originalTimezone.latitude != nil { |
|
latitude = originalTimezone.latitude.doubleValue |
|
} |
|
|
|
if originalTimezone.longitude != nil { |
|
longitude = originalTimezone.longitude.doubleValue |
|
} |
|
|
|
note = originalTimezone.note |
|
nextUpdate = originalTimezone.nextUpdate |
|
sunriseTime = originalTimezone.sunriseTime |
|
sunsetTime = originalTimezone.sunsetTime |
|
isFavourite = originalTimezone.isFavourite.intValue |
|
isSunriseOrSunset = originalTimezone.sunriseOrSunset |
|
selectionType = originalTimezone.selectionType == CLSelection.citySelection ? .city : .timezone |
|
isSystemTimezone = originalTimezone.isSystemTimezone |
|
overrideFormat = .globalFormat |
|
overrideSecondsFormat = .globalFormat |
|
} |
|
|
|
init(with dictionary: [String: Any]) { |
|
if let label = dictionary[CLCustomLabel] as? String { |
|
customLabel = label |
|
} else { |
|
customLabel = nil |
|
} |
|
|
|
if let timezone = dictionary[CLTimezoneID] as? String { |
|
timezoneID = timezone |
|
} else { |
|
timezoneID = "Error" |
|
} |
|
|
|
if let lat = dictionary["latitude"] as? Double { |
|
latitude = lat |
|
} else { |
|
latitude = -0.0 |
|
} |
|
|
|
if let long = dictionary["longitude"] as? Double { |
|
longitude = long |
|
} else { |
|
longitude = -0.0 |
|
} |
|
|
|
if let placeIdentifier = dictionary[CLPlaceIdentifier] as? String { |
|
placeID = placeIdentifier |
|
} else { |
|
placeID = "Error" |
|
} |
|
|
|
if let address = dictionary[CLTimezoneName] as? String { |
|
formattedAddress = address |
|
} else { |
|
formattedAddress = "Error" |
|
} |
|
|
|
isFavourite = 0 |
|
|
|
selectionType = .city |
|
|
|
if let noteString = dictionary["note"] as? String { |
|
note = noteString |
|
} else { |
|
note = CLEmptyString |
|
} |
|
|
|
isSystemTimezone = false |
|
|
|
overrideFormat = .globalFormat |
|
overrideSecondsFormat = .globalFormat |
|
} |
|
|
|
required init?(coder aDecoder: NSCoder) { |
|
customLabel = aDecoder.decodeObject(forKey: "customLabel") as? String |
|
|
|
formattedAddress = aDecoder.decodeObject(forKey: "formattedAddress") as? String |
|
|
|
placeID = aDecoder.decodeObject(forKey: "place_id") as? String |
|
|
|
timezoneID = aDecoder.decodeObject(forKey: "timezoneID") as? String |
|
|
|
latitude = aDecoder.decodeObject(forKey: "latitude") as? Double |
|
|
|
longitude = aDecoder.decodeObject(forKey: "longitude") as? Double |
|
|
|
note = aDecoder.decodeObject(forKey: "note") as? String |
|
|
|
nextUpdate = aDecoder.decodeObject(forKey: "nextUpdate") as? Date |
|
|
|
sunriseTime = aDecoder.decodeObject(forKey: "sunriseTime") as? Date |
|
|
|
sunsetTime = aDecoder.decodeObject(forKey: "sunsetTime") as? Date |
|
|
|
isFavourite = aDecoder.decodeInteger(forKey: "isFavourite") |
|
|
|
let selection = aDecoder.decodeInteger(forKey: "selectionType") |
|
selectionType = SelectionType(rawValue: selection)! |
|
|
|
isSystemTimezone = aDecoder.decodeBool(forKey: "isSystemTimezone") |
|
|
|
let override = aDecoder.decodeInteger(forKey: "overrideFormat") |
|
overrideFormat = TimezoneOverride(rawValue: override)! |
|
|
|
let secondsOverride = aDecoder.decodeInteger(forKey: "secondsOverrideFormat") |
|
overrideSecondsFormat = SecondsOverride(rawValue: secondsOverride)! |
|
} |
|
|
|
class func customObject(from encodedData: Data?) -> TimezoneData? { |
|
guard let dataObject = encodedData else { |
|
return TimezoneData() |
|
} |
|
|
|
if let timezoneObject = NSKeyedUnarchiver.unarchiveObject(with: dataObject) as? TimezoneData { |
|
return timezoneObject |
|
} else if let originalTimezoneObject = NSKeyedUnarchiver.unarchiveObject(with: dataObject) as? CLTimezoneData { |
|
logOldModelUsage() |
|
return TimezoneData(with: originalTimezoneObject) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
private class func logOldModelUsage() { |
|
guard let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, |
|
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else { |
|
return |
|
} |
|
|
|
let operatingSystem = ProcessInfo.processInfo.operatingSystemVersion |
|
let osVersion = "\(operatingSystem.majorVersion).\(operatingSystem.minorVersion).\(operatingSystem.patchVersion)" |
|
let versionInfo = "Clocker \(shortVersion) (\(appVersion))" |
|
|
|
let feedbackInfo = [ |
|
AppFeedbackConstants.CLOperatingSystemVersion: osVersion, |
|
AppFeedbackConstants.CLClockerVersion: versionInfo, |
|
] |
|
|
|
Logger.log(object: feedbackInfo, for: "CLTimezoneData is still being used!") |
|
} |
|
|
|
/// Converts the Obj-C model objects into Swift |
|
class func convert() { |
|
if let timezones = DataStore.shared().retrieve(key: CLDefaultPreferenceKey) as? [Data], !timezones.isEmpty { |
|
let newModels = converter(timezones) |
|
|
|
if newModels.count == timezones.count { |
|
// Now point preferences to empty |
|
DataStore.shared().setTimezones([]) |
|
|
|
// Now point it to new models |
|
DataStore.shared().setTimezones(newModels) |
|
} |
|
} |
|
} |
|
|
|
private class func converter(_ timezones: [Data]) -> [Data] { |
|
var newModels: [TimezoneData] = [] |
|
|
|
for timezone in timezones { |
|
// Get the old (aka CLTimezoneData) model object |
|
let old = NSKeyedUnarchiver.unarchiveObject(with: timezone) |
|
if let oldModel = old as? CLTimezoneData { |
|
// Convert it to new model and add it |
|
Logger.info("We're still using old Objective-C models") |
|
let newTimezone = TimezoneData(with: oldModel) |
|
newModels.append(newTimezone) |
|
} else if let newModel = old as? TimezoneData { |
|
shouldOverrideSecondsFormatBugFix(model: newModel) |
|
newModels.append(newModel) |
|
} |
|
} |
|
|
|
if UserDefaults.standard.object(forKey: "shouldOverrideSecondsFormatBug") == nil { |
|
UserDefaults.standard.set("YES", forKey: "shouldOverrideSecondsFormatBug") |
|
} |
|
|
|
// Do the serialization |
|
let serializedModels = newModels.map { (place) -> Data in |
|
NSKeyedArchiver.archivedData(withRootObject: place) |
|
} |
|
|
|
return serializedModels |
|
} |
|
|
|
private class func shouldOverrideSecondsFormatBugFix(model: TimezoneData) { |
|
if UserDefaults.standard.object(forKey: "shouldOverrideSecondsFormatBug") == nil { |
|
model.setShouldOverrideSecondsFormat(2) |
|
} |
|
} |
|
|
|
func encode(with aCoder: NSCoder) { |
|
aCoder.encode(placeID, forKey: "place_id") |
|
|
|
aCoder.encode(formattedAddress, forKey: "formattedAddress") |
|
|
|
aCoder.encode(customLabel, forKey: "customLabel") |
|
|
|
aCoder.encode(timezoneID, forKey: "timezoneID") |
|
|
|
aCoder.encode(nextUpdate, forKey: "nextUpdate") |
|
|
|
aCoder.encode(latitude, forKey: "latitude") |
|
|
|
aCoder.encode(longitude, forKey: "longitude") |
|
|
|
aCoder.encode(isFavourite, forKey: "isFavourite") |
|
|
|
aCoder.encode(sunriseTime, forKey: "sunriseTime") |
|
|
|
aCoder.encode(sunsetTime, forKey: "sunsetTime") |
|
|
|
aCoder.encode(selectionType.rawValue, forKey: "selectionType") |
|
|
|
aCoder.encode(note, forKey: "note") |
|
|
|
aCoder.encode(isSystemTimezone, forKey: "isSystemTimezone") |
|
|
|
aCoder.encode(overrideFormat.rawValue, forKey: "overrideFormat") |
|
|
|
aCoder.encode(overrideSecondsFormat.rawValue, forKey: "secondsOverrideFormat") |
|
} |
|
|
|
func formattedTimezoneLabel() -> String { |
|
// First check if there's an user preferred custom label set |
|
if let label = customLabel, !label.isEmpty { |
|
return label |
|
} |
|
|
|
// No custom label, return the formatted address/timezone |
|
if let address = formattedAddress, !address.isEmpty { |
|
return address |
|
} |
|
|
|
// No formatted address, return the timezoneID |
|
if let timezone = timezoneID, !timezone.isEmpty { |
|
let hashSeperatedString = timezone.components(separatedBy: "/") |
|
|
|
// Return the second component! |
|
if let first = hashSeperatedString.first { |
|
return first |
|
} |
|
|
|
// Second component not available, return the whole thing! |
|
return timezone |
|
} |
|
|
|
// Return error |
|
return "Error" |
|
} |
|
|
|
func setLabel(_ label: String) { |
|
customLabel = !label.isEmpty ? label : CLEmptyString |
|
} |
|
|
|
func setShouldOverrideGlobalTimeFormat(_ shouldOverride: Int) { |
|
if shouldOverride == 0 { |
|
overrideFormat = .twelveHourFormat |
|
} else if shouldOverride == 1 { |
|
overrideFormat = .twentyFourFormat |
|
} else { |
|
overrideFormat = .globalFormat |
|
} |
|
} |
|
|
|
func setShouldOverrideSecondsFormat(_ shouldOverride: Int) { |
|
if shouldOverride == 0 { |
|
overrideSecondsFormat = .showSeconds |
|
} else if shouldOverride == 1 { |
|
overrideSecondsFormat = .hideSeconds |
|
} else { |
|
overrideSecondsFormat = .globalFormat |
|
} |
|
} |
|
|
|
func timezone() -> String { |
|
if isSystemTimezone { |
|
timezoneID = TimeZone.autoupdatingCurrent.identifier |
|
formattedAddress = TimeZone.autoupdatingCurrent.identifier |
|
return TimeZone.autoupdatingCurrent.identifier |
|
} |
|
|
|
if let timezone = timezoneID { |
|
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 |
|
} |
|
|
|
func timezoneFormat() -> String { |
|
let showSeconds = shouldShowSeconds() |
|
let isTwelveHourFormatSelected = DataStore.shared().shouldDisplay(.twelveHour) |
|
|
|
var timeFormat = DateFormat.twentyFourHour |
|
|
|
if showSeconds { |
|
if overrideFormat == .globalFormat { |
|
timeFormat = isTwelveHourFormatSelected ? DateFormat.twelveHourWithSeconds : DateFormat.twentyFourHourWithSeconds |
|
} else if overrideFormat == .twelveHourFormat { |
|
timeFormat = DateFormat.twelveHourWithSeconds |
|
} else { |
|
timeFormat = DateFormat.twentyFourHourWithSeconds |
|
} |
|
} else { |
|
if overrideFormat == .globalFormat { |
|
timeFormat = isTwelveHourFormatSelected ? DateFormat.twelveHour : DateFormat.twentyFourHour |
|
} else if overrideFormat == .twelveHourFormat { |
|
timeFormat = DateFormat.twelveHour |
|
} else { |
|
timeFormat = DateFormat.twentyFourHour |
|
} |
|
} |
|
|
|
return timeFormat |
|
} |
|
|
|
func shouldDisplayTwelveHourFormat() -> Bool { |
|
if overrideSecondsFormat == .globalFormat { |
|
return DataStore.shared().shouldDisplay(.twelveHour) |
|
} |
|
|
|
return overrideFormat == .twelveHourFormat |
|
} |
|
|
|
func shouldShowSeconds() -> Bool { |
|
if overrideSecondsFormat == .globalFormat { |
|
return DataStore.shared().shouldDisplay(.seconds) |
|
} |
|
|
|
return overrideSecondsFormat == .showSeconds |
|
} |
|
|
|
override var hash: Int { |
|
guard let placeIdentifier = placeID, let timezone = timezoneID else { |
|
return -1 |
|
} |
|
|
|
return placeIdentifier.hashValue ^ timezone.hashValue |
|
} |
|
|
|
static func == (lhs: TimezoneData, rhs: TimezoneData) -> Bool { |
|
return lhs.placeID == rhs.placeID |
|
} |
|
|
|
override func isEqual(to object: Any?) -> Bool { |
|
if let other = object as? TimezoneData { |
|
return placeID == other.placeID |
|
} |
|
return false |
|
} |
|
|
|
override func isEqual(_ object: Any?) -> Bool { |
|
guard let compared = object as? TimezoneData else { |
|
return false |
|
} |
|
|
|
// Plain timezones might have similar placeID. Adding another check for timezone identifier. |
|
return placeID == compared.placeID && timezoneID == compared.timezoneID |
|
} |
|
} |
|
|
|
extension TimezoneData { |
|
private func adjustStatusBarAppearance() { |
|
guard let statusItem = (NSApplication.shared.delegate as? AppDelegate)?.statusItemForPanel() else { |
|
return |
|
} |
|
|
|
statusItem.setupStatusItem() |
|
} |
|
} |
|
|
|
extension TimezoneData { |
|
override var description: String { |
|
return objectDescription() |
|
} |
|
|
|
override var debugDescription: String { |
|
return objectDescription() |
|
} |
|
|
|
private func objectDescription() -> String { |
|
let customString = """ |
|
TimezoneID: \(timezoneID ?? "Error") |
|
Formatted Address: \(formattedAddress ?? "Error") |
|
Custom Label: \(customLabel ?? "Error") |
|
Latitude: \(latitude ?? -0.0) |
|
Longitude: \(longitude ?? -0.0) |
|
Place Identifier: \(placeID ?? "Error") |
|
Is Favourite: \(isFavourite) |
|
Sunrise Time: \(sunriseTime?.debugDescription ?? "N/A") |
|
Sunset Time: \(sunsetTime?.debugDescription ?? "N/A") |
|
Selection Type: \(selectionType.rawValue) |
|
Note: \(note ?? "Error") |
|
Is System Timezone: \(isSystemTimezone) |
|
Override: \(overrideFormat) |
|
Seconds Override: \(overrideSecondsFormat) |
|
""" |
|
|
|
return customString |
|
} |
|
}
|
|
|