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.

489 lines
16 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"
static let twelveHourWithZero = "hh:mm a"
static let twelveHourWithZeroSeconds = "hh:mm:ss a"
static let twelveHourWithoutSuffix = "hh:mm"
static let twelveHourWithoutSuffixAndSeconds = "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 globalFormat = 0
4 years ago
case twelveHourFormat = 1
case twentyFourFormat = 2
case twelveHourWithSeconds = 4
case twentyHourWithSeconds = 5
case twelveHourPrecedingZero = 7
case twelveHourPrecedingZeroSeconds = 8
case twelveHourWithoutSuffix = 10
case twelveHourWithoutSuffixAndSeconds = 11
}
6 years ago
static let values = [
NSNumber(integerLiteral: 0): DateFormat.twelveHour,
NSNumber(integerLiteral: 1): DateFormat.twentyFourHour,
// Seconds
NSNumber(integerLiteral: 2): DateFormat.twelveHourWithSeconds,
NSNumber(integerLiteral: 3): DateFormat.twentyFourHourWithSeconds,
// Preceding Zero
NSNumber(integerLiteral: 4): DateFormat.twelveHourWithZero,
NSNumber(integerLiteral: 5): DateFormat.twelveHourWithZeroSeconds,
// Suffix
NSNumber(integerLiteral: 6): DateFormat.twelveHourWithoutSuffix,
NSNumber(integerLiteral: 7): DateFormat.twelveHourWithoutSuffixAndSeconds,
]
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
override init() {
selectionType = .timezone
isFavourite = 0
note = CLEmptyString
isSystemTimezone = false
overrideFormat = .globalFormat
4 years ago
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
}
6 years ago
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
}
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)!
}
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
}
6 years ago
private class func logOldModelUsage() {
guard let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String,
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else {
6 years ago
return
}
6 years ago
let operatingSystem = ProcessInfo.processInfo.operatingSystemVersion
let osVersion = "\(operatingSystem.majorVersion).\(operatingSystem.minorVersion).\(operatingSystem.patchVersion)"
let versionInfo = "Clocker \(shortVersion) (\(appVersion))"
6 years ago
let feedbackInfo = [
AppFeedbackConstants.CLOperatingSystemVersion: osVersion,
6 years ago
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 {
if UserDefaults.standard.object(forKey: "migrateOverrideFormat") == nil {
print("Resetting Global Format")
newModel.setShouldOverrideGlobalTimeFormat(0)
}
newModels.append(newModel)
}
}
if UserDefaults.standard.object(forKey: "migrateOverrideFormat") == nil {
UserDefaults.standard.set("YES", forKey: "migrateOverrideFormat")
}
// Do the serialization
let serializedModels = newModels.map { (place) -> Data in
6 years ago
NSKeyedArchiver.archivedData(withRootObject: place)
}
return serializedModels
}
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")
}
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 = .globalFormat
} else if shouldOverride == 1 {
overrideFormat = .twelveHourFormat
} else if shouldOverride == 2 {
overrideFormat = .twentyFourFormat
} else if shouldOverride == 4 {
overrideFormat = .twelveHourWithSeconds
} else if shouldOverride == 5 {
4 years ago
print("Setting override format to five")
overrideFormat = .twentyHourWithSeconds
} else if shouldOverride == 7 {
overrideFormat = .twelveHourPrecedingZero
} else if shouldOverride == 8 {
overrideFormat = .twelveHourPrecedingZeroSeconds
} else if shouldOverride == 10 {
overrideFormat = .twelveHourWithoutSuffix
} else if shouldOverride == 11 {
overrideFormat = .twelveHourWithoutSuffixAndSeconds
4 years ago
} else {
assertionFailure("Chosen a wrong timezone format")
}
}
6 years ago
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,
6 years ago
"TimezoneID": timezoneIdentifier,
]
Logger.log(object: errorDictionary, for: "Error fetching timezone() in TimezoneData")
}
return TimeZone.autoupdatingCurrent.identifier
}
func timezoneFormat() -> String {
let chosenDefault = DataStore.shared().timezoneFormat()
let timeFormat = TimezoneData.values[chosenDefault] ?? DateFormat.twelveHour
if overrideFormat == .globalFormat {
return timeFormat
} else if overrideFormat == .twelveHourFormat {
return DateFormat.twelveHour
} else if overrideFormat == .twentyFourFormat {
return DateFormat.twentyFourHour
} else if overrideFormat == .twelveHourWithSeconds {
return DateFormat.twelveHourWithSeconds
} else if overrideFormat == .twentyHourWithSeconds {
return DateFormat.twentyFourHourWithSeconds
} else if overrideFormat == .twelveHourPrecedingZero {
return DateFormat.twelveHourWithZero
} else if overrideFormat == .twelveHourPrecedingZeroSeconds {
return DateFormat.twelveHourWithZeroSeconds
} else if overrideFormat == .twelveHourWithoutSuffix {
return DateFormat.twelveHourWithoutSuffix
} else if overrideFormat == .twelveHourWithoutSuffixAndSeconds {
return DateFormat.twelveHourWithoutSuffixAndSeconds
}
return timeFormat
}
6 years ago
func shouldShowSeconds() -> Bool {
if overrideFormat == .globalFormat {
let currentFormat = DataStore.shared().timezoneFormat()
let formatInString = TimezoneData.values[currentFormat] ?? DateFormat.twelveHour
return formatInString.contains("ss")
}
let formatInString = TimezoneData.values[NSNumber(integerLiteral: overrideFormat.rawValue)] ?? DateFormat.twelveHour
return formatInString.contains("ss")
}
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
}
6 years ago
6 years ago
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
}
4 years ago
// 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)
"""
return customString
}
}