diff --git a/Clocker/Icons/New Icons/NewIcon-1024.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-1024.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-1024.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-1024.png diff --git a/Clocker/Icons/New Icons/NewIcon-128.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-128.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-128.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-128.png diff --git a/Clocker/Icons/New Icons/NewIcon-16.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-16.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-16.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-16.png diff --git a/Clocker/Icons/New Icons/NewIcon-256.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-256.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-256.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-256.png diff --git a/Clocker/Icons/New Icons/NewIcon-32.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-32.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-32.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-32.png diff --git a/Clocker/Icons/New Icons/NewIcon-512.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-512.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-512.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-512.png diff --git a/Clocker/Icons/New Icons/NewIcon-64.png b/Archive/New Icons/Older Icons/New Icons/NewIcon-64.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon-64.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon-64.png diff --git a/Clocker/Icons/New Icons/NewIcon.png b/Archive/New Icons/Older Icons/New Icons/NewIcon.png similarity index 100% rename from Clocker/Icons/New Icons/NewIcon.png rename to Archive/New Icons/Older Icons/New Icons/NewIcon.png diff --git a/Clocker/Icons/New Icons/NewIcon.xcf b/Archive/New Icons/Older Icons/New Icons/NewIcon.xcf similarity index 100% rename from Clocker/Icons/New Icons/NewIcon.xcf rename to Archive/New Icons/Older Icons/New Icons/NewIcon.xcf diff --git a/Clocker/Icons/Old Icons/ClockerIcon-1024.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-1024.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-1024.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-1024.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon-16.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-16.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-16.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-16.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon-256.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-256.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-256.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-256.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon-32.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-32.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-32.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-32.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon-512.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-512.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-512.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-512.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon-64.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon-64.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon-64.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon-64.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon.png b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon.png similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon.png rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon.png diff --git a/Clocker/Icons/Old Icons/ClockerIcon.xcf b/Archive/New Icons/Older Icons/Old Icons/ClockerIcon.xcf similarity index 100% rename from Clocker/Icons/Old Icons/ClockerIcon.xcf rename to Archive/New Icons/Older Icons/Old Icons/ClockerIcon.xcf diff --git a/Clocker/Icons/Old Icons/Icon.png b/Archive/New Icons/Older Icons/Old Icons/Icon.png similarity index 100% rename from Clocker/Icons/Old Icons/Icon.png rename to Archive/New Icons/Older Icons/Old Icons/Icon.png diff --git a/Clocker/AppDelegate.swift b/Clocker/AppDelegate.swift index c56f7e1..52ecf02 100644 --- a/Clocker/AppDelegate.swift +++ b/Clocker/AppDelegate.swift @@ -110,12 +110,12 @@ open class AppDelegate: NSObject, NSApplicationDelegate { if controller != nil { controller = nil } - + // Check if another instance of the app is already running. If so, then stop this one. checkIfAppIsAlreadyOpen() // Install the menubar item! - statusBarHandler = StatusItemHandler() + statusBarHandler = StatusItemHandler(with: DataStore.shared()) if ProcessInfo.processInfo.arguments.contains(CLUITestingLaunchArgument) { FirebaseApp.configure() diff --git a/Clocker/Clocker.xcodeproj/project.pbxproj b/Clocker/Clocker.xcodeproj/project.pbxproj index 0a8668a..dedaf64 100755 --- a/Clocker/Clocker.xcodeproj/project.pbxproj +++ b/Clocker/Clocker.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 35B2FEC0259A186F005DA84D /* StartupKit in Frameworks */ = {isa = PBXBuildFile; productRef = 35B2FEBF259A186F005DA84D /* StartupKit */; }; 35B2FEDD259A2291005DA84D /* CoreLoggerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 35B2FEDC259A2291005DA84D /* CoreLoggerKit */; }; 35B2FEF1259A2DB1005DA84D /* CoreModelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 35B2FEF0259A2DB1005DA84D /* CoreModelKit */; }; + 35BD9A572807580800077443 /* EventInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35BD9A562807580800077443 /* EventInfoTests.swift */; }; 35C36EF122595F14002FA5C6 /* OnboardingPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C36EE822595F13002FA5C6 /* OnboardingPermissionsViewController.swift */; }; 35C36EF222595F14002FA5C6 /* OnboardingWelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C36EE922595F13002FA5C6 /* OnboardingWelcomeViewController.swift */; }; 35C36EF322595F14002FA5C6 /* WelcomeView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 35C36EEA22595F13002FA5C6 /* WelcomeView.xib */; }; @@ -298,6 +299,7 @@ 35B2FEB1259A1649005DA84D /* StartupKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = StartupKit; sourceTree = ""; }; 35B2FED4259A2244005DA84D /* CoreLoggerKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CoreLoggerKit; sourceTree = ""; }; 35B2FEE4259A2C25005DA84D /* CoreModelKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CoreModelKit; sourceTree = ""; }; + 35BD9A562807580800077443 /* EventInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventInfoTests.swift; sourceTree = ""; }; 35C11E2024873A550031F18C /* VersionUpdateHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateHandler.swift; sourceTree = ""; }; 35C36EE822595F13002FA5C6 /* OnboardingPermissionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionsViewController.swift; sourceTree = ""; }; 35C36EE922595F13002FA5C6 /* OnboardingWelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingWelcomeViewController.swift; sourceTree = ""; }; @@ -881,6 +883,7 @@ 35584D1927F0B64E006E3EAD /* AppDelegateTests.swift */, 35D23E3627F27E2E00C6DD55 /* ReviewControllerTests.swift */, 35621CFB27F66C1900926D5C /* SearchDataSourceTests.swift */, + 35BD9A562807580800077443 /* EventInfoTests.swift */, ); path = ClockerUnitTests; sourceTree = ""; @@ -1266,6 +1269,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 35BD9A572807580800077443 /* EventInfoTests.swift in Sources */, 35584D1427EF8EB5006E3EAD /* ThemerTests.swift in Sources */, 35D23E3727F27E2E00C6DD55 /* ReviewControllerTests.swift in Sources */, 35584D1A27F0B64E006E3EAD /* AppDelegateTests.swift in Sources */, diff --git a/Clocker/ClockerUnitTests/ClockerUnitTests.swift b/Clocker/ClockerUnitTests/ClockerUnitTests.swift index ab0b26d..2f7e12f 100644 --- a/Clocker/ClockerUnitTests/ClockerUnitTests.swift +++ b/Clocker/ClockerUnitTests/ClockerUnitTests.swift @@ -406,7 +406,7 @@ class ClockerUnitTests: XCTestCase { func testToasty() { let view = NSView(frame: CGRect.zero) view.makeToast("Hello, this is a toast") - XCTAssertEqual(view.subviews.first?.accessibilityIdentifier(), "ToastView") + XCTAssertEqual(view.subviews.first?.accessibilityIdentifier(), "ToastView") let toastExpectation = expectation(description: "Toast View should hide after 1 second") let result = XCTWaiter.wait(for: [toastExpectation], timeout: 1.5) // Set 1.5 seconds here for a little leeway if result == XCTWaiter.Result.timedOut { diff --git a/Clocker/ClockerUnitTests/EventInfoTests.swift b/Clocker/ClockerUnitTests/EventInfoTests.swift new file mode 100644 index 0000000..cc116c0 --- /dev/null +++ b/Clocker/ClockerUnitTests/EventInfoTests.swift @@ -0,0 +1,79 @@ +// Copyright © 2015 Abhishek Banthia + +@testable import Clocker +import EventKit +import XCTest + +class EventInfoTests: XCTestCase { + private let eventStore = EKEventStore() + + func testMetadataForUpcomingEventHappeningInFiveMinutes() throws { + let futureChunk = TimeChunk(seconds: 10, minutes: 5, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(futureChunk) + + let mockEventInfo = EventInfo(event: mockEvent, + isAllDay: false, + meetingURL: nil, + attendeStatus: .accepted) + XCTAssert(mockEventInfo.metadataForMeeting() == "in 5m", + "Metadata for meeting: \(mockEventInfo.metadataForMeeting()) doesn't match expectation") + } + + func testMetadataForUpcomingEventHappeningInTenSeconds() throws { + let futureChunk = TimeChunk(seconds: 10, minutes: 0, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(futureChunk) + + let mockEventInfo = EventInfo(event: mockEvent, + isAllDay: false, + meetingURL: nil, + attendeStatus: .accepted) + XCTAssert(mockEventInfo.metadataForMeeting() == "in <1m", + "Metadata for meeting: \(mockEventInfo.metadataForMeeting()) doesn't match expectation") + } + + func testMetadataForEventPastTwoMinutes() throws { + let pastChunk = TimeChunk(seconds: 10, minutes: 2, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().subtract(pastChunk) + + let mockEventInfo = EventInfo(event: mockEvent, + isAllDay: false, + meetingURL: nil, + attendeStatus: .accepted) + XCTAssert(mockEventInfo.metadataForMeeting() == "started +2m.", + "Metadata for meeting: \(mockEventInfo.metadataForMeeting()) doesn't match expectation") + } + + func testMetadataForEventPastTenMinutes() throws { + let pastChunk = TimeChunk(seconds: 10, minutes: 10, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().subtract(pastChunk) + + let mockEventInfo = EventInfo(event: mockEvent, + isAllDay: false, + meetingURL: nil, + attendeStatus: .accepted) + XCTAssert(mockEventInfo.metadataForMeeting() == "Error", + "Metadata for meeting: \(mockEventInfo.metadataForMeeting()) doesn't match expectation") + } + + func testMetadataForEventHappeningTomorrow() throws { + let pastChunk = TimeChunk(seconds: 10, minutes: 0, hours: 25, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(pastChunk) + + let mockEventInfo = EventInfo(event: mockEvent, + isAllDay: false, + meetingURL: nil, + attendeStatus: .accepted) + XCTAssert(mockEventInfo.metadataForMeeting() == "in 25h", + "Metadata for meeting: \(mockEventInfo.metadataForMeeting()) doesn't match expectation") + } +} diff --git a/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift b/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift index 1eae0a7..5cc24ee 100644 --- a/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift +++ b/Clocker/ClockerUnitTests/StandardMenubarHandlerTests.swift @@ -1,11 +1,14 @@ // Copyright © 2015 Abhishek Banthia import CoreModelKit +import EventKit import XCTest @testable import Clocker class StandardMenubarHandlerTests: XCTestCase { + private let eventStore = EKEventStore() + private let mumbai = ["customLabel": "Ghar", "formattedAddress": "Mumbai", "place_id": "ChIJwe1EZjDG5zsRaYxkjY_tpF0", @@ -113,4 +116,68 @@ class StandardMenubarHandlerTests: XCTestCase { let menubarHandler = MenubarTitleProvider(with: store) XCTAssertNotNil(menubarHandler.titleForMenubar()) } + + func testFormattedUpcomingEvent() { + let store = makeMockStore() + + let futureChunk = TimeChunk(seconds: 10, minutes: 10, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(futureChunk) + + let menubarHandler = MenubarTitleProvider(with: store) + XCTAssert(menubarHandler.format(event: mockEvent) == "Mock Title in 10m", + "Suffix \(menubarHandler.format(event: mockEvent)) doesn't match expectation") + } + + func testUpcomingEventHappeningWithinOneMinute() { + let store = makeMockStore() + + let futureChunk = TimeChunk(seconds: 10, minutes: 1, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(futureChunk) + + let menubarHandler = MenubarTitleProvider(with: store) + XCTAssert(menubarHandler.format(event: mockEvent) == "Mock Title in 1m", + "Suffix \(menubarHandler.format(event: mockEvent)) doesn't match expectation") + } + + func testUpcomingEventHappeningWithinSeconds() { + let store = makeMockStore() + + let futureChunk = TimeChunk(seconds: 10, minutes: 0, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Mock Title" + mockEvent.startDate = Date().add(futureChunk) + + let menubarHandler = MenubarTitleProvider(with: store) + XCTAssert(menubarHandler.format(event: mockEvent) == "Mock Title starts now.", + "Suffix \(menubarHandler.format(event: mockEvent)) doesn't match expectation") + } + + func testEmptyUpcomingEvent() { + let store = makeMockStore() + + let futureChunk = TimeChunk(seconds: 10, minutes: 0, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.startDate = Date().add(futureChunk) + + let menubarHandler = MenubarTitleProvider(with: store) + XCTAssert(menubarHandler.format(event: mockEvent) == CLEmptyString, + "Suffix \(menubarHandler.format(event: mockEvent)) doesn't match expectation") + } + + func testLongUpcomingEvent() { + let store = makeMockStore() + + let futureChunk = TimeChunk(seconds: 10, minutes: 0, hours: 0, days: 0, weeks: 0, months: 0, years: 0) + let mockEvent = EKEvent(eventStore: eventStore) + mockEvent.title = "Really long calendar event title that longer than the longest name" + mockEvent.startDate = Date().add(futureChunk) + + let menubarHandler = MenubarTitleProvider(with: store) + XCTAssert(menubarHandler.format(event: mockEvent) == "Really long calendar event tit... starts now.", + "Suffix \(menubarHandler.format(event: mockEvent)) doesn't match expectation") + } } diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index 98d0d6b..bc1e8a2 100644 --- a/Clocker/Events and Reminders/CalendarHandler.swift +++ b/Clocker/Events and Reminders/CalendarHandler.swift @@ -116,40 +116,6 @@ extension EventCenter { return (formattedTitle, menubarText) } - func format(event: EKEvent) -> String { - guard let truncateLength = DataStore.shared().retrieve(key: CLTruncateTextLength) as? NSNumber, let eventTitle = event.title else { - return CLEmptyString - } - - let seconds = event.startDate.timeIntervalSinceNow - - var menubarText: String = CLEmptyString - - if eventTitle.count > truncateLength.intValue { - let truncateIndex = eventTitle.index(eventTitle.startIndex, offsetBy: truncateLength.intValue) - let truncatedTitle = String(eventTitle[.. 2 { - let suffix = String(format: " in %0.f mins", minutes) - menubarText.append(suffix) - } else if minutes == 1 { - let suffix = String(format: " in %0.f min", minutes) - menubarText.append(suffix) - } else { - menubarText.append(" starts now.") - } - - return menubarText - } - func nextOccuring(_: [EventInfo]) -> EventInfo? { if calendarAccessDenied() || calendarAccessNotDetermined() { return nil @@ -362,17 +328,11 @@ extension EventCenter { 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 eventParticipationStatus = attendingStatusForUser(event) let meetingURL = retrieveMeetingURL(event) let eventInfo = EventInfo(event: event, - isStartDate: isStartDate, - isEndDate: isEndDate, isAllDay: isAllDay, - isSingleDay: isSingleDay, meetingURL: meetingURL, attendeStatus: eventParticipationStatus) return eventInfo @@ -481,10 +441,7 @@ struct CalendarInfo { struct EventInfo { let event: EKEvent - let isStartDate: Bool - let isEndDate: Bool let isAllDay: Bool - let isSingleDay: Bool let meetingURL: URL? let attendeStatus: EKParticipantStatus @@ -492,13 +449,13 @@ struct EventInfo { let timeIntervalSinceNowForMeeting = event.startDate.timeIntervalSinceNow if timeIntervalSinceNowForMeeting < 0, timeIntervalSinceNowForMeeting > -300 { return "started +\(event.startDate.shortTimeAgoSinceNow)." - } else if event.startDate.isToday { + } else if event.startDate.isToday, timeIntervalSinceNowForMeeting > 0 { let timeSince = Date().timeAgo(since: event.startDate).lowercased() let withoutAn = timeSince.replacingOccurrences(of: "an", with: CLEmptyString) let withoutAgo = withoutAn.replacingOccurrences(of: "ago", with: CLEmptyString) // If the user has not turned on seconds granularity for one of the timezones, // we return "in 12 seconds" which looks weird. - return withoutAgo.contains("seconds") ? "starts in <1m" : "in \(withoutAgo.lowercased())" + return withoutAgo.contains("seconds") ? "in <1m" : "in \(withoutAgo.lowercased())".trimmingCharacters(in: .whitespaces) } else if event.startDate.isTomorrow { let hoursUntil = event.startDate.hoursUntil return "in \(hoursUntil)h" diff --git a/Clocker/Onboarding/OnboardingParentViewController.swift b/Clocker/Onboarding/OnboardingParentViewController.swift index e88424d..1772011 100644 --- a/Clocker/Onboarding/OnboardingParentViewController.swift +++ b/Clocker/Onboarding/OnboardingParentViewController.swift @@ -223,7 +223,7 @@ class OnboardingParentViewController: NSViewController { if ProcessInfo.processInfo.arguments.contains(CLOnboaringTestsLaunchArgument) == false { UserDefaults.standard.set(true, forKey: CLShowOnboardingFlow) } - + // Install the menubar option! let appDelegate = NSApplication.shared.delegate as? AppDelegate appDelegate?.continueUsually() diff --git a/Clocker/Onboarding/OnboardingSearchController.swift b/Clocker/Onboarding/OnboardingSearchController.swift index be1eb12..b119d23 100644 --- a/Clocker/Onboarding/OnboardingSearchController.swift +++ b/Clocker/Onboarding/OnboardingSearchController.swift @@ -40,7 +40,7 @@ class OnboardingSearchController: NSViewController { super.viewDidLoad() view.wantsLayer = true - + resultsTableView.isHidden = true resultsTableView.delegate = self resultsTableView.setAccessibility("ResultsTableView") @@ -69,12 +69,12 @@ class OnboardingSearchController: NSViewController { setupUndoButton() } - + override func viewWillAppear() { super.viewWillAppear() searchResultsDataSource = SearchDataSource(with: searchBar, location: .onboarding) } - + override func viewWillDisappear() { super.viewWillDisappear() searchResultsDataSource = nil diff --git a/Clocker/Overall App/AppKit + Additions.swift b/Clocker/Overall App/AppKit + Additions.swift index ad78e09..d40c044 100644 --- a/Clocker/Overall App/AppKit + Additions.swift +++ b/Clocker/Overall App/AppKit + Additions.swift @@ -21,7 +21,7 @@ extension NSTextField { } extension NSFont { - func size(_ string: String, _ width: Double, attributes: [NSAttributedString.Key: AnyObject]) -> CGSize { + func size(for string: String, width: Double, attributes: [NSAttributedString.Key: AnyObject]) -> CGSize { let size = CGSize(width: width, height: Double.greatestFiniteMagnitude) diff --git a/Clocker/Overall App/DataStore.swift b/Clocker/Overall App/DataStore.swift index e837bff..2cdb2eb 100644 --- a/Clocker/Overall App/DataStore.swift +++ b/Clocker/Overall App/DataStore.swift @@ -71,11 +71,11 @@ class DataStore: NSObject { let cloudTimezones = ubiquitousStore?.object(forKey: CLDefaultPreferenceKey) as? [Data] let cloudLastUpdateDate = (ubiquitousStore?.object(forKey: CLUbiquitousStoreLastUpdateKey) as? Date) ?? Date() let defaultsLastUpdateDate = (ubiquitousStore?.object(forKey: CLUserDefaultsLastUpdateKey) as? Date) ?? Date() - + if cloudTimezones == currentTimezones { Logger.info("Ubiquitous Store timezones aren't equal to current timezones") } - + if defaultsLastUpdateDate.isLaterThanOrEqual(to: cloudLastUpdateDate) { Logger.info("Ubiquitous Store is stale as compared to User Defaults") } diff --git a/Clocker/Panel/PanelController.swift b/Clocker/Panel/PanelController.swift index 6e3fac3..89519f3 100644 --- a/Clocker/Panel/PanelController.swift +++ b/Clocker/Panel/PanelController.swift @@ -74,7 +74,7 @@ class PanelController: ParentPanelController { super.dismissRowActions() updateDefaultPreferences() - + setupUpcomingEventViewCollectionViewIfNeccesary() if DataStore.shared().timezones().isEmpty || DataStore.shared().shouldDisplay(.futureSlider) == false { diff --git a/Clocker/Panel/ParentPanelController.swift b/Clocker/Panel/ParentPanelController.swift index 535180c..c48700c 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -1151,7 +1151,7 @@ extension ParentPanelController: AppFeedbackWindowControllerDelegate { func appFeedbackWindowWillClose() { feedbackWindow = nil } - + func appFeedbackWindoEntryPoint() -> String { return "parent_panel_controller" } diff --git a/Clocker/Preferences/About/AboutViewController.swift b/Clocker/Preferences/About/AboutViewController.swift index afc91fb..b6ac392 100644 --- a/Clocker/Preferences/About/AboutViewController.swift +++ b/Clocker/Preferences/About/AboutViewController.swift @@ -157,7 +157,7 @@ extension AboutViewController: AppFeedbackWindowControllerDelegate { func appFeedbackWindowWillClose() { feedbackWindow = nil } - + func appFeedbackWindoEntryPoint() -> String { return "about_view_controller" } diff --git a/Clocker/Preferences/App Feedback/AppFeedbackWindowController.swift b/Clocker/Preferences/App Feedback/AppFeedbackWindowController.swift index 152c315..16f8781 100644 --- a/Clocker/Preferences/App Feedback/AppFeedbackWindowController.swift +++ b/Clocker/Preferences/App Feedback/AppFeedbackWindowController.swift @@ -210,7 +210,7 @@ class AppFeedbackWindowController: NSWindowController { AppFeedbackConstants.CLClockerVersion: versionInfo, AppFeedbackConstants.CLAppFeedbackDateProperty: todaysDate(), AppFeedbackConstants.CLAppFeedbackUserPreferences: generateUserPreferences(), - AppFeedbackConstants.CLFeedbackEntryPoint: appFeedbackWindowDelegate?.appFeedbackWindoEntryPoint() ?? "Error" + AppFeedbackConstants.CLFeedbackEntryPoint: appFeedbackWindowDelegate?.appFeedbackWindoEntryPoint() ?? "Error", ] } @@ -228,7 +228,7 @@ class AppFeedbackWindowController: NSWindowController { #if DEBUG Logger.info("Sending a feedback in Debug builds will lead to a no-op") #endif - + guard let identifier = serialNumber else { assertionFailure("Serial Identifier was unexpectedly nil") return diff --git a/Clocker/Preferences/Appearance/AppearanceViewController.swift b/Clocker/Preferences/Appearance/AppearanceViewController.swift index 2c0f97b..679fb54 100644 --- a/Clocker/Preferences/Appearance/AppearanceViewController.swift +++ b/Clocker/Preferences/Appearance/AppearanceViewController.swift @@ -136,7 +136,7 @@ class AppearanceViewController: ParentViewController { // True is Menubar Only and False is Menubar + Dock let appDisplayOptions = DataStore.shared().shouldDisplay(.appDisplayOptions) appDisplayControl.setSelected(true, forSegment: appDisplayOptions ? 0 : 1) - + // Set the Sync value from NSUbiqutousKeyValueStore let syncEnabled = NSUbiquitousKeyValueStore.default.bool(forKey: CLEnableSyncKey) syncSegementedControl.setSelected(true, forSegment: syncEnabled ? 0 : 1) diff --git a/Clocker/Preferences/Calendar/CalendarViewController.swift b/Clocker/Preferences/Calendar/CalendarViewController.swift index 109c105..2571868 100644 --- a/Clocker/Preferences/Calendar/CalendarViewController.swift +++ b/Clocker/Preferences/Calendar/CalendarViewController.swift @@ -5,7 +5,6 @@ import CoreLoggerKit import EventKit class ClockerTextBackgroundView: NSView { - override func awakeFromNib() { wantsLayer = true layer?.cornerRadius = 8.0 @@ -16,12 +15,12 @@ class ClockerTextBackgroundView: NSView { name: .themeDidChangeNotification, object: nil) } - + override func updateLayer() { super.updateLayer() layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor } - + @objc func updateBackgroundColor() { layer?.backgroundColor = Themer.shared().textBackgroundColor().cgColor } diff --git a/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift b/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift index 2134f14..d8a5f89 100644 --- a/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift +++ b/Clocker/Preferences/Menu Bar/MenubarTitleProvider.swift @@ -60,11 +60,41 @@ class MenubarTitleProvider: NSObject { continue } - return EventCenter.sharedCenter().format(event: event) + return format(event: event) } } } return nil } + + internal func format(event: EKEvent) -> String { + guard let truncateLength = store.retrieve(key: CLTruncateTextLength) as? NSNumber, let eventTitle = event.title, event.title.isEmpty == false else { + return CLEmptyString + } + + let seconds = event.startDate.timeIntervalSinceNow + + var menubarText: String = CLEmptyString + + if eventTitle.count > truncateLength.intValue { + let truncateIndex = eventTitle.index(eventTitle.startIndex, offsetBy: truncateLength.intValue) + let truncatedTitle = String(eventTitle[..= 1 { + let suffix = String(format: " in %0.fm", minutes) + menubarText.append(suffix) + } else { + menubarText.append(" starts now.") + } + + return menubarText + } } diff --git a/Clocker/Preferences/Menu Bar/StatusContainerView.swift b/Clocker/Preferences/Menu Bar/StatusContainerView.swift index b4fee8b..6de08d5 100644 --- a/Clocker/Preferences/Menu Bar/StatusContainerView.swift +++ b/Clocker/Preferences/Menu Bar/StatusContainerView.swift @@ -4,33 +4,11 @@ import Cocoa import CoreLoggerKit import CoreModelKit -func bufferCalculatedWidth() -> Int { +func compactWidth(for timezone: TimezoneData, with store: DataStore) -> Int { var totalWidth = 55 + let timeFormat = timezone.timezoneFormat(store.timezoneFormat()) - if DataStore.shared().shouldShowDayInMenubar() { - totalWidth += 12 - } - - if DataStore.shared().isBufferRequiredForTwelveHourFormats() { - totalWidth += 20 - } - - if DataStore.shared().shouldShowDateInMenubar() { - totalWidth += 20 - } - - if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { - totalWidth += 100 - } - - return totalWidth -} - -func compactWidth(for timezone: TimezoneData) -> Int { - var totalWidth = 55 - let timeFormat = timezone.timezoneFormat(DataStore.shared().timezoneFormat()) - - if DataStore.shared().shouldShowDayInMenubar() { + if store.shouldShowDayInMenubar() { totalWidth += 12 } @@ -46,12 +24,12 @@ func compactWidth(for timezone: TimezoneData) -> Int { totalWidth += 0 } - if timezone.shouldShowSeconds(DataStore.shared().timezoneFormat()) { + if timezone.shouldShowSeconds(store.timezoneFormat()) { // Slight buffer needed when the Menubar supplementary text was Mon 9:27:58 AM totalWidth += 15 } - if DataStore.shared().shouldShowDateInMenubar() { + if store.shouldShowDateInMenubar() { totalWidth += 20 } @@ -71,6 +49,7 @@ protocol StatusItemViewConforming { class StatusContainerView: NSView { private var previousX: Int = 0 + private let store: DataStore override func awakeFromNib() { super.awakeFromNib() @@ -78,7 +57,13 @@ class StatusContainerView: NSView { layer?.backgroundColor = NSColor.clear.cgColor } - init(with timezones: [Data], showUpcomingEventView: Bool) { + init(with timezones: [Data], + store: DataStore, + showUpcomingEventView: Bool, + bufferContainerWidth: Int) + { + self.store = store + func addSubviews() { if showUpcomingEventView, let events = EventCenter.sharedCenter().eventsForDate[NSCalendar.autoupdatingCurrent.startOfDay(for: Date())], @@ -110,16 +95,20 @@ class StatusContainerView: NSView { var compressedWidth = timezones.reduce(0.0) { result, timezone -> CGFloat in if let timezoneObject = TimezoneData.customObject(from: timezone) { - let precalculatedWidth = Double(compactWidth(for: timezoneObject)) + let precalculatedWidth = Double(compactWidth(for: timezoneObject, with: store)) let operationObject = TimezoneDataOperations(with: timezoneObject) - let calculatedSubtitleSize = compactModeTimeFont.size(operationObject.compactMenuSubtitle(), precalculatedWidth, attributes: timeBasedAttributes) - let calculatedTitleSize = compactModeTimeFont.size(operationObject.compactMenuTitle(), precalculatedWidth, attributes: timeBasedAttributes) - let showSeconds = timezoneObject.shouldShowSeconds(DataStore.shared().timezoneFormat()) + let calculatedSubtitleSize = compactModeTimeFont.size(for: operationObject.compactMenuSubtitle(), + width: precalculatedWidth, + attributes: timeBasedAttributes) + let calculatedTitleSize = compactModeTimeFont.size(for: operationObject.compactMenuTitle(), + width: precalculatedWidth, + attributes: timeBasedAttributes) + let showSeconds = timezoneObject.shouldShowSeconds(store.timezoneFormat()) let secondsBuffer: CGFloat = showSeconds ? 7 : 0 return result + max(calculatedTitleSize.width, calculatedSubtitleSize.width) + bufferWidth + secondsBuffer } - return result + CGFloat(bufferCalculatedWidth()) + return result + CGFloat(bufferContainerWidth) } if showUpcomingEventView { @@ -127,7 +116,7 @@ class StatusContainerView: NSView { } let calculatedWidth = min(compressedWidth, - CGFloat(timezones.count * bufferCalculatedWidth())) + CGFloat(timezones.count * bufferContainerWidth)) return calculatedWidth } @@ -170,9 +159,12 @@ class StatusContainerView: NSView { ] let operation = TimezoneDataOperations(with: timezone) - let bestSize = compactModeTimeFont.size(operation.compactMenuSubtitle(), - Double(compactWidth(for: timezone)), attributes: timeBasedAttributes) - let bestTitleSize = compactModeTimeFont.size(operation.compactMenuTitle(), Double(compactWidth(for: timezone)), attributes: timeBasedAttributes) + let bestSize = compactModeTimeFont.size(for: operation.compactMenuSubtitle(), + width: Double(compactWidth(for: timezone, with: store)), + attributes: timeBasedAttributes) + let bestTitleSize = compactModeTimeFont.size(for: operation.compactMenuTitle(), + width: Double(compactWidth(for: timezone, with: store)), + attributes: timeBasedAttributes) return Int(max(bestSize.width, bestTitleSize.width) + bufferWidth) } @@ -191,11 +183,11 @@ class StatusContainerView: NSView { NSAttributedString.Key.paragraphStyle: defaultParagraphStyle, ] - let bestSize = compactModeTimeFont.size(eventInfo.metadataForMeeting(), - 55, // Default for a location based status view + let bestSize = compactModeTimeFont.size(for: eventInfo.metadataForMeeting(), + width: 55, // Default for a location based status view attributes: timeBasedAttributes) - let bestTitleSize = compactModeTimeFont.size(eventInfo.event.title, - 70, // Little more buffer since meeting titles tend to be longer + let bestTitleSize = compactModeTimeFont.size(for: eventInfo.event.title, + width: 70, // Little more buffer since meeting titles tend to be longer attributes: timeBasedAttributes) return Int(max(bestSize.width, bestTitleSize.width) + bufferWidth) diff --git a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift index f3055bb..95d7e74 100644 --- a/Clocker/Preferences/Menu Bar/StatusItemHandler.swift +++ b/Clocker/Preferences/Menu Bar/StatusItemHandler.swift @@ -22,7 +22,7 @@ class StatusItemHandler: NSObject { return statusItem }() - private var menubarTitleHandler = MenubarTitleProvider(with: DataStore.shared()) + private lazy var menubarTitleHandler = MenubarTitleProvider(with: self.store) private var statusContainerView: StatusContainerView? @@ -32,6 +32,8 @@ class StatusItemHandler: NSObject { private var userNotificationsDidChangeNotif: NSObjectProtocol? + private let store: DataStore + // Current State might be set twice when the user first launches an app. // First, when StatusItemHandler() is instantiated in AppDelegate // Second, when AppDelegate.fetchLocalTimezone() is called triggering a customLabel didSet. @@ -63,7 +65,8 @@ class StatusItemHandler: NSObject { } } - override init() { + init(with dataStore: DataStore) { + store = dataStore super.init() setupStatusItem() @@ -74,10 +77,10 @@ class StatusItemHandler: NSObject { // Let's figure out the initial menubar state var menubarState = MenubarState.icon - let shouldTextBeDisplayed = DataStore.shared().menubarTimezones()?.isEmpty ?? true + let shouldTextBeDisplayed = store.menubarTimezones()?.isEmpty ?? true - if !shouldTextBeDisplayed || DataStore.shared().shouldDisplay(.showMeetingInMenubar) { - if DataStore.shared().shouldDisplay(.menubarCompactMode) { + if !shouldTextBeDisplayed || store.shouldDisplay(.showMeetingInMenubar) { + if store.shouldDisplay(.menubarCompactMode) { menubarState = .compactText } else { menubarState = .standardText @@ -138,14 +141,16 @@ class StatusItemHandler: NSObject { private func constructCompactView(with upcomingEventView: Bool = false) { statusContainerView = nil - let menubarTimezones = DataStore.shared().menubarTimezones() ?? [] + let menubarTimezones = store.menubarTimezones() ?? [] if menubarTimezones.isEmpty { currentState = .icon return } statusContainerView = StatusContainerView(with: menubarTimezones, - showUpcomingEventView: upcomingEventView) + store: store, + showUpcomingEventView: upcomingEventView, + bufferContainerWidth: bufferCalculatedWidth()) statusItem.view = statusContainerView statusItem.view?.window?.backgroundColor = NSColor.clear } @@ -155,7 +160,7 @@ class StatusItemHandler: NSObject { // Our icon is template, so it changes automatically; so is our standard status bar text // Only need to handle the compact mode! @objc func respondToInterfaceStyleChange() { - if DataStore.shared().shouldDisplay(.menubarCompactMode) { + if store.shouldDisplay(.menubarCompactMode) { updateCompactMenubar() } } @@ -199,11 +204,11 @@ class StatusItemHandler: NSObject { } private func shouldDisplaySecondsInMenubar() -> Bool { - let syncedTimezones = DataStore.shared().menubarTimezones() ?? [] + let syncedTimezones = store.menubarTimezones() ?? [] let timezonesSupportingSeconds = syncedTimezones.filter { data in if let timezoneObj = TimezoneData.customObject(from: data) { - return timezoneObj.shouldShowSeconds(DataStore.shared().timezoneFormat()) + return timezoneObj.shouldShowSeconds(store.timezoneFormat()) } return false } @@ -213,7 +218,7 @@ class StatusItemHandler: NSObject { private func calculateFireDate() -> Date? { let shouldDisplaySeconds = shouldDisplaySecondsInMenubar() - let menubarFavourites = DataStore.shared().menubarTimezones() + let menubarFavourites = store.menubarTimezones() if !units.contains(.second), shouldDisplaySeconds { units.insert(.second) @@ -298,9 +303,9 @@ class StatusItemHandler: NSObject { // Check if user is not showing // 1. Timezones // 2. Upcoming Event - let menubarFavourites = DataStore.shared().menubarTimezones() ?? [] + let menubarFavourites = store.menubarTimezones() ?? [] - if menubarFavourites.isEmpty, DataStore.shared().shouldDisplay(.showMeetingInMenubar) == false { + if menubarFavourites.isEmpty, store.shouldDisplay(.showMeetingInMenubar) == false { Logger.info("Invalidating menubar timer!") invalidation() @@ -347,7 +352,7 @@ class StatusItemHandler: NSObject { if let menubarTitle = menubarTitleHandler.titleForMenubar() { menubarText = menubarTitle - } else if DataStore.shared().shouldDisplay(.showMeetingInMenubar) { + } else if store.shouldDisplay(.showMeetingInMenubar) { // Don't have any meeting to show } else { // We have no favourites to display and no meetings to show. @@ -383,4 +388,26 @@ class StatusItemHandler: NSObject { }) return upcomingEventView } + + private func bufferCalculatedWidth() -> Int { + var totalWidth = 55 + + if store.shouldShowDayInMenubar() { + totalWidth += 12 + } + + if store.isBufferRequiredForTwelveHourFormats() { + totalWidth += 20 + } + + if store.shouldShowDateInMenubar() { + totalWidth += 20 + } + + if store.shouldDisplay(.showMeetingInMenubar) { + totalWidth += 100 + } + + return totalWidth + } } diff --git a/Clocker/fastlane/Appfile b/Clocker/fastlane/Appfile deleted file mode 100644 index 22588ac..0000000 --- a/Clocker/fastlane/Appfile +++ /dev/null @@ -1,7 +0,0 @@ -app_identifier "com.abhishek.Clocker" # The bundle identifier of your app -apple_id "abhishekbanthia1712@gmail.com" # Your Apple email address - -team_id "[[DEV_PORTAL_TEAM_ID]]" # Developer Portal Team ID - -# you can even provide different app identifiers, Apple IDs and team names per lane: -# More information: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md diff --git a/Clocker/fastlane/Fastfile b/Clocker/fastlane/Fastfile deleted file mode 100644 index 7be0285..0000000 --- a/Clocker/fastlane/Fastfile +++ /dev/null @@ -1,73 +0,0 @@ -# Customise this file, documentation can be found here: -# https://github.com/fastlane/fastlane/tree/master/fastlane/docs -# All available actions: https://docs.fastlane.tools/actions -# can also be listed using the `fastlane actions` command - -# Change the syntax highlighting to Ruby -# All lines starting with a # are ignored when running `fastlane` - -# If you want to automatically update fastlane if a new version is available: -# update_fastlane - -# This is the minimum version number required. -# Update this, if you use features of a newer version -fastlane_version "2.36.0" - -default_platform :ios - -platform :ios do - before_all do - # ENV["SLACK_URL"] = "https://hooks.slack.com/services/..." - - - end - - desc "Runs all the tests" - lane :test do - scan - end - - desc "Submit a new Beta Build to Apple TestFlight" - desc "This will also make sure the profile is up to date" - lane :beta do - # match(type: "appstore") # more information: https://codesigning.guide - gym # Build your app - more options available - pilot - - # sh "your_script.sh" - # You can also use other beta testing services here (run `fastlane actions`) - end - - desc "Deploy a new version to the App Store" - lane :release do - # match(type: "appstore") - # snapshot - gym # Build your app - more options available - deliver(force: true) - # frameit - end - - # You can define as many lanes as you want - - after_all do |lane| - # This block is called, only if the executed lane was successful - - # slack( - # message: "Successfully deployed new App Update." - # ) - end - - error do |lane, exception| - # slack( - # message: exception.message, - # success: false - # ) - end -end - - -# More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md -# All available actions: https://docs.fastlane.tools/actions - -# fastlane reports which actions are used. No personal data is recorded. -# Learn more at https://github.com/fastlane/fastlane#metrics diff --git a/Clocker/fastlane/screenshots/Screenshot1 2.png b/Clocker/fastlane/screenshots/Screenshot1 2.png deleted file mode 100644 index 62dc67f..0000000 Binary files a/Clocker/fastlane/screenshots/Screenshot1 2.png and /dev/null differ diff --git a/Clocker/fastlane/screenshots/Screenshot1.png b/Clocker/fastlane/screenshots/Screenshot1.png deleted file mode 100644 index 9fdb841..0000000 Binary files a/Clocker/fastlane/screenshots/Screenshot1.png and /dev/null differ diff --git a/Clocker/fastlane/screenshots/Screenshot2.png b/Clocker/fastlane/screenshots/Screenshot2.png deleted file mode 100644 index ddd2a47..0000000 Binary files a/Clocker/fastlane/screenshots/Screenshot2.png and /dev/null differ diff --git a/Clocker/fastlane/screenshots/Screenshot3 2.png b/Clocker/fastlane/screenshots/Screenshot3 2.png deleted file mode 100644 index a213674..0000000 Binary files a/Clocker/fastlane/screenshots/Screenshot3 2.png and /dev/null differ diff --git a/Clocker/fastlane/screenshots/Screenshot3.png b/Clocker/fastlane/screenshots/Screenshot3.png deleted file mode 100644 index 5fd2565..0000000 Binary files a/Clocker/fastlane/screenshots/Screenshot3.png and /dev/null differ diff --git a/Clocker/fastlane/test_output/report.html b/Clocker/fastlane/test_output/report.html deleted file mode 100644 index 33fb08e..0000000 --- a/Clocker/fastlane/test_output/report.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - Test Results | xcpretty - - - - -
-
-

Test Results

-
-
-
-

3 tests

- -
-
- AllFailingPassing -
-
-
-
- - -
-
-

ClockerTests

-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
- -

0.154s

- -

testAddingPlaceToUserDefaults

- -

0.002s

- -

testDeletingPlaceFromUserDefaults

- -

0.022s

- -

testIfReminderIsCreated

-
-
- -
- - - diff --git a/Clocker/fastlane/test_output/report.junit b/Clocker/fastlane/test_output/report.junit deleted file mode 100644 index 03c12a5..0000000 --- a/Clocker/fastlane/test_output/report.junit +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file