diff --git a/Harbor.xcodeproj/project.pbxproj b/Harbor.xcodeproj/project.pbxproj index 614ad1d..17f868b 100644 --- a/Harbor.xcodeproj/project.pbxproj +++ b/Harbor.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 31CEEC161D3826BD0023A5AC /* NSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CEEC151D3826BD0023A5AC /* NSButton.swift */; }; 4C86D9FC1CE50142008E91E4 /* codeshipLogo_blue.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C86D9FA1CE50142008E91E4 /* codeshipLogo_blue.png */; }; 4C86D9FD1CE50142008E91E4 /* codeshipLogo_blue@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C86D9FB1CE50142008E91E4 /* codeshipLogo_blue@2x.png */; }; + 721620151D401E4A00EE239C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 721620141D401E4A00EE239C /* Credits.rtf */; }; CDD5638F36C28F1CBED22C30 /* Pods_Harbor_HarborTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1EF75FA103814CE2B8C146C9 /* Pods_Harbor_HarborTests.framework */; }; D01093BC1BB5B3D7002D5794 /* SettingsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01093BB1BB5B3D7002D5794 /* SettingsSpec.swift */; }; D01093BF1BB5D932002D5794 /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01093BE1BB5D932002D5794 /* NotificationCenter.swift */; }; @@ -161,6 +162,7 @@ 6A44CEAA7EBDF79EBEA74B01 /* Pods-Harbor-HarborTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Harbor-HarborTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Harbor-HarborTests/Pods-Harbor-HarborTests.release.xcconfig"; sourceTree = ""; }; 6AB17BFC533903194B2259FA /* Pods-HarborTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HarborTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-HarborTests/Pods-HarborTests.release.xcconfig"; sourceTree = ""; }; 7150961101FB11B6275D20F3 /* Pods_HarborTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HarborTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 721620141D401E4A00EE239C /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 9F06F0036A0C8CA9A3D11BA6 /* Pods-Harbor.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Harbor.release.xcconfig"; path = "Pods/Target Support Files/Pods-Harbor/Pods-Harbor.release.xcconfig"; sourceTree = ""; }; C0CEA4AAFB6873D19A822129 /* Pods-Harbor.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Harbor.test.xcconfig"; path = "Pods/Target Support Files/Pods-Harbor/Pods-Harbor.test.xcconfig"; sourceTree = ""; }; D01093BB1BB5B3D7002D5794 /* SettingsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SettingsSpec.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -324,6 +326,14 @@ name = View; sourceTree = ""; }; + 721620101D401E0000EE239C /* Resources */ = { + isa = PBXGroup; + children = ( + 721620141D401E4A00EE239C /* Credits.rtf */, + ); + name = Resources; + sourceTree = ""; + }; A338C7D8E749FE47B694D06D /* Pods */ = { isa = PBXGroup; children = ( @@ -441,6 +451,7 @@ D0772F0A1B3C437800031FB9 /* Harbor */ = { isa = PBXGroup; children = ( + 721620101D401E0000EE239C /* Resources */, 10FA200E1C933E1B00A642A7 /* Application.swift */, D0772F0B1B3C437800031FB9 /* AppDelegate.swift */, D08EAB3F1B98D5D5009564CE /* Views */, @@ -655,6 +666,7 @@ 4C86D9FD1CE50142008E91E4 /* codeshipLogo_blue@2x.png in Resources */, D08EAB3B1B98A60A009564CE /* codeshipLogo_red.png in Resources */, D08EAB391B98A60A009564CE /* codeshipLogo_green.png in Resources */, + 721620151D401E4A00EE239C /* Credits.rtf in Resources */, D08EAB321B97531A009564CE /* MainMenu.xib in Resources */, D0B1777A1B9651BD0055ECC6 /* codeshipLogo_black@2x.png in Resources */, D0B177791B9651BD0055ECC6 /* codeshipLogo_black.png in Resources */, diff --git a/Harbor/BuildView.xib b/Harbor/BuildView.xib index ef68665..9153a63 100644 --- a/Harbor/BuildView.xib +++ b/Harbor/BuildView.xib @@ -1,7 +1,7 @@ - + - + diff --git a/Harbor/Credits.rtf b/Harbor/Credits.rtf new file mode 100644 index 0000000..30ff2bf --- /dev/null +++ b/Harbor/Credits.rtf @@ -0,0 +1,13 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1404\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\margl1440\margr1440\vieww9000\viewh8400\viewkind0 +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 + +\f0\fs24 \cf0 Brought to you by your friends at {\field{\*\fldinst{HYPERLINK "http://devmynd.com"}}{\fldrslt DevMynd}} in Chicago and San Francisco. +\b \ +\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 +\cf0 \ +Contributors +\b0 : Erin Hochstatter, Ty Cobb, \ +Michael Crismali, Joe Hirn, Ifu Aniemeka, Erick Arias & Eryan Cobham} \ No newline at end of file diff --git a/Harbor/Info.plist b/Harbor/Info.plist index bfa3204..fd61390 100644 --- a/Harbor/Info.plist +++ b/Harbor/Info.plist @@ -17,17 +17,15 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.1 + 0.1.2 CFBundleSignature ???? - CFBundleVersion - 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement NSHumanReadableCopyright - Copyright © 2015 DevMynd. All rights reserved. + Copyright © 2016 DevMynd. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass diff --git a/Harbor/PreferencesPaneWindowController.xib b/Harbor/PreferencesPaneWindowController.xib index 4385a88..fe36769 100644 --- a/Harbor/PreferencesPaneWindowController.xib +++ b/Harbor/PreferencesPaneWindowController.xib @@ -1,5 +1,5 @@ - + @@ -22,7 +22,7 @@ - + @@ -30,8 +30,8 @@ - - + + @@ -49,8 +49,8 @@ - - + + @@ -59,11 +59,13 @@ + - + + @@ -71,8 +73,8 @@ - - + + @@ -90,8 +92,8 @@ - - + + @@ -100,27 +102,17 @@ - - + + - - + + - - + @@ -149,8 +141,11 @@ - + + + + @@ -185,8 +184,8 @@ - - + + @@ -194,6 +193,11 @@ + + + + + @@ -209,8 +213,8 @@ - + + + + - - - - - + + + + + - - + diff --git a/Harbor/PreferencesPresenter.swift b/Harbor/PreferencesPresenter.swift index e4abe93..e5ac9df 100644 --- a/Harbor/PreferencesPresenter.swift +++ b/Harbor/PreferencesPresenter.swift @@ -11,7 +11,7 @@ class PreferencesPresenter : Presenter { // // MARK: Properties private var apiKey: String = "" - private var refreshRate: Double = 60.0 + private var refreshRate: Int = 60 private(set) var launchOnLogin: Bool = true private var allProjects: [Project] private var apiKeyError: String = "" @@ -95,7 +95,7 @@ class PreferencesPresenter : Presenter { } func updateRefreshRate(refreshRate: String) { - self.refreshRate = (refreshRate as NSString).doubleValue + self.refreshRate = (refreshRate as NSString).integerValue validateRefreshRate(refreshRate) setNeedsRefresh() } @@ -174,11 +174,11 @@ class PreferencesPresenter : Presenter { } private func validateRefreshRate(value: String) { - let doubleValue = Double(value) + let refreshValue = Double(value) - if doubleValue == nil { + if refreshValue == nil { refreshRateError = "must be a number" - } else if !(5 ... 600 ~= doubleValue!) { + } else if !(5.0...600.0 ~= refreshValue!) { refreshRateError = "must be between 5 and 600 seconds" } else { refreshRateError = "" diff --git a/Harbor/Settings.swift b/Harbor/Settings.swift index 6030408..b57ce9e 100644 --- a/Harbor/Settings.swift +++ b/Harbor/Settings.swift @@ -38,9 +38,9 @@ class Settings: SettingsType { } } - var refreshRate: Double { + var refreshRate: Int { didSet { - defaults.setDouble(refreshRate, forKey: Key.RefreshRate) + defaults.setInteger(refreshRate, forKey: Key.RefreshRate) postNotification(.RefreshRate) } } @@ -62,6 +62,7 @@ class Settings: SettingsType { } var isFirstRun: Bool + private let defaultRefreshRate: Int = 60 init(defaults: UserDefaults, keychain: Keychain, notificationCenter: NotificationCenter) { self.defaults = defaults @@ -69,7 +70,7 @@ class Settings: SettingsType { self.notificationCenter = notificationCenter apiKey = keychain.stringForKey(Key.ApiKey) ?? "" - refreshRate = defaults.doubleForKey(Key.RefreshRate) + refreshRate = (defaults.integerForKey(Key.RefreshRate) > 0) ? defaults.integerForKey(Key.RefreshRate) : defaultRefreshRate disabledProjectIds = defaults.objectForKey(Key.DisabledProjects) as? [Int] ?? [Int]() isFirstRun = !defaults.boolForKey(Key.HasLaunched) launchOnLogin = isFirstRun ? true : defaults.boolForKey(Key.LaunchOnLogin) diff --git a/Harbor/SettingsType.swift b/Harbor/SettingsType.swift index 7ab209a..ef34b39 100644 --- a/Harbor/SettingsType.swift +++ b/Harbor/SettingsType.swift @@ -3,7 +3,7 @@ import Foundation protocol SettingsType { var apiKey: String { get set } - var refreshRate: Double { get set } + var refreshRate: Int { get set } var disabledProjectIds: [Int] { get set } var launchOnLogin: Bool { get set } var isFirstRun: Bool { get set } diff --git a/Harbor/TimerCoordinator.swift b/Harbor/TimerCoordinator.swift index 4eaef34..4cb2320 100644 --- a/Harbor/TimerCoordinator.swift +++ b/Harbor/TimerCoordinator.swift @@ -40,12 +40,12 @@ class TimerCoordinator: NSObject, TimerCoordinatorType { // // MARK: Helpers - private func setupTimer(refreshRate: Double) -> NSTimer? { + private func setupTimer(refreshRate: Int) -> NSTimer? { // cancel current timer if necessary stopTimer() - if !refreshRate.isZero { - currentTimer = NSTimer(timeInterval: refreshRate, target: self, selector:#selector(TimerCoordinator.handleUpdateTimer(_:)), userInfo: nil, repeats: true) + if refreshRate != 0 { + currentTimer = NSTimer(timeInterval: convertIntToDouble(refreshRate), target: self, selector:#selector(TimerCoordinator.handleUpdateTimer(_:)), userInfo: nil, repeats: true) runLoop.addTimer(currentTimer!, forMode: NSDefaultRunLoopMode) } @@ -57,4 +57,8 @@ class TimerCoordinator: NSObject, TimerCoordinatorType { projectsInteractor.refreshProjects() } } + + private func convertIntToDouble(integer: Int) -> Double { + return Double(integer) + } } \ No newline at end of file diff --git a/Harbor/UserDefaults.swift b/Harbor/UserDefaults.swift index 7eaf9b1..25117a2 100644 --- a/Harbor/UserDefaults.swift +++ b/Harbor/UserDefaults.swift @@ -4,8 +4,8 @@ protocol UserDefaults { func setObject(object: AnyObject?, forKey key: CustomStringConvertible) func objectForKey(key: CustomStringConvertible) -> AnyObject? - func setDouble(double: Double, forKey key: CustomStringConvertible) - func doubleForKey(key: CustomStringConvertible) -> Double + func setInteger(integer: Int, forKey key: CustomStringConvertible) + func integerForKey(key: CustomStringConvertible) -> Int func setBool(bool: Bool, forKey key: CustomStringConvertible) func boolForKey(key: CustomStringConvertible) -> Bool @@ -22,12 +22,17 @@ extension NSUserDefaults : UserDefaults { return self.objectForKey(key.description) } - func setDouble(double: Double, forKey key: CustomStringConvertible) { - self.setDouble(double, forKey: key.description) + func setInteger(integer: Int, forKey key: CustomStringConvertible) { + if let currentObject = objectForKey(key.description) { + if currentObject is Double { + removeObjectForKey(key.description) + } + } + self.setInteger(integer, forKey: key.description) } - func doubleForKey(key: CustomStringConvertible) -> Double { - return self.doubleForKey(key.description) + func integerForKey(key: CustomStringConvertible) -> Int { + return self.integerForKey(key.description) } func setBool(bool: Bool, forKey key: CustomStringConvertible) { diff --git a/HarborTests/MockPreferencesView.swift b/HarborTests/MockPreferencesView.swift index 7ab5682..a0a6c04 100644 --- a/HarborTests/MockPreferencesView.swift +++ b/HarborTests/MockPreferencesView.swift @@ -1,18 +1,13 @@ @testable import Harbor class MockPreferencesView : PreferencesView { - init() { - - } - + var refreshRateError: String? var apiKey: String? func updateProjects(projects: [Project]) { - } func updateRefreshRate(refreshRate: String) { - } func updateApiKey(apiKey: String) { @@ -20,14 +15,12 @@ class MockPreferencesView : PreferencesView { } func updateLaunchOnLogin(launchOnLogin: Bool) { - } func updateApiKeyError(errorMessage: String) { - } func updateRefreshRateError(errorMessage: String) { - + refreshRateError = errorMessage } } diff --git a/HarborTests/MockSettings.swift b/HarborTests/MockSettings.swift index f695c96..cd15928 100644 --- a/HarborTests/MockSettings.swift +++ b/HarborTests/MockSettings.swift @@ -3,7 +3,7 @@ import Foundation class MockSettings: SettingsType { var apiKey: String = "" - var refreshRate: Double = 0.0 + var refreshRate: Int = 0 var disabledProjectIds: [Int] = [] var launchOnLogin: Bool = false var isFirstRun: Bool = false diff --git a/HarborTests/MockUserDefaults.swift b/HarborTests/MockUserDefaults.swift index a4d301c..e22e18c 100644 --- a/HarborTests/MockUserDefaults.swift +++ b/HarborTests/MockUserDefaults.swift @@ -4,8 +4,8 @@ class MockUserDefaults : UserDefaults { enum Method : MethodType { case SetObject case ObjectForKey - case SetDouble - case DoubleForKey + case SetInteger + case IntegerForKey } var invocation: Invocation? @@ -20,14 +20,14 @@ class MockUserDefaults : UserDefaults { return lastValue as! AnyObject? } - func setDouble(double: Double, forKey key: CustomStringConvertible) { - invocation = Invocation(.SetDouble, double) + func setInteger(integer: Int, forKey key: CustomStringConvertible) { + invocation = Invocation(.SetInteger, integer) } - func doubleForKey(key: CustomStringConvertible) -> Double { + func integerForKey(key: CustomStringConvertible) -> Int { let lastValue = invocation?.value - invocation = Invocation(.DoubleForKey, lastValue) - return lastValue as? Double ?? 0.0 + invocation = Invocation(.IntegerForKey, lastValue) + return lastValue as? Int ?? 0 } func setBool(bool: Bool, forKey key: CustomStringConvertible) { diff --git a/HarborTests/PreferencesPresenterSpec.swift b/HarborTests/PreferencesPresenterSpec.swift index 123c72f..a61f87f 100644 --- a/HarborTests/PreferencesPresenterSpec.swift +++ b/HarborTests/PreferencesPresenterSpec.swift @@ -85,10 +85,40 @@ class PreferencesPresenterSpec: HarborSpec { override func spec() { } } + describe("#updateRefreshRate") { + context("when the refresh rate is valid") { + it("renders no error message") { + subject.updateRefreshRate("5.01") + expect(view.refreshRateError) == "" + } + } + + context("when the refresh rate is not a number") { + it("renders the the numeric validation error") { + subject.updateRefreshRate("asdf") + expect(view.refreshRateError) == "must be a number" + } + } + + context("when the refresh rate is less than 5") { + it("renders the range validation error") { + subject.updateRefreshRate("4.9") + expect(view.refreshRateError) == "must be between 5 and 600 seconds" + } + } + + context("when the refresh rate is greater than 600") { + it("renders the range validation error") { + subject.updateRefreshRate("600.1") + expect(view.refreshRateError) == "must be between 5 and 600 seconds" + } + } + } + describe("#savePreferences") { beforeEach { subject.updateApiKey("abc123") - subject.updateRefreshRate("10.0") + subject.updateRefreshRate("10") subject.updateLaunchOnLogin(true) subject.setNeedsRefresh() subject.savePreferences() @@ -96,7 +126,7 @@ class PreferencesPresenterSpec: HarborSpec { override func spec() { it("updates settings"){ expect(settings.apiKey) == "abc123" - expect(settings.refreshRate) == Double("10.0") + expect(settings.refreshRate) == Int("10") expect(settings.launchOnLogin) == true expect(subject.needsRefresh).to(beFalse()) } diff --git a/HarborTests/SettingsSpec.swift b/HarborTests/SettingsSpec.swift index 84866de..ebfd0d2 100644 --- a/HarborTests/SettingsSpec.swift +++ b/HarborTests/SettingsSpec.swift @@ -31,12 +31,19 @@ class SettingsSpec: HarborSpec { } it("retrieves the correct refresh rate"){ - let refreshRate = 60.0 + let refreshRate = 90 example.subject.refreshRate = refreshRate let local = example.rebuild { $0.defaults = example.defaults } expect(local.subject.refreshRate).to(equal(refreshRate)) } + + it("provides a default refresh rate if it has not been set"){ + let refreshRate = 60 + + let local = example.rebuild { $0.defaults = example.defaults } + expect(local.subject.refreshRate).to(equal(refreshRate)) + } it("retrieves the correct disabled project ids"){ let disabledProjectIds = [1, 2, 3, 4] @@ -49,10 +56,10 @@ class SettingsSpec: HarborSpec { describe("setting") { describe("the refresh rate") { - let value = 60.0 + let value = 60 it("updates user defaults with the given rate"){ - let invocation = Invocations.defaults(.SetDouble, VerifierOf(value)) + let invocation = Invocations.defaults(.SetInteger, VerifierOf(value)) example.subject.refreshRate = value expect(example.defaults.invocation).to(match(invocation)) diff --git a/HarborTests/TimerCoordinatorSpec.swift b/HarborTests/TimerCoordinatorSpec.swift index 1a2e93e..7c9c20a 100644 --- a/HarborTests/TimerCoordinatorSpec.swift +++ b/HarborTests/TimerCoordinatorSpec.swift @@ -30,11 +30,11 @@ class TimerCoordinatorSpec: HarborSpec { describe("when starting a timer") { beforeEach { - example.settings.refreshRate = 60.0 + example.settings.refreshRate = 60 } it("should not create a timer when the refresh rate is 0.0") { - example.settings.refreshRate = 0.0 + example.settings.refreshRate = 0 let timer = example.subject.startTimer() expect(timer).to(beNil()) @@ -42,8 +42,9 @@ class TimerCoordinatorSpec: HarborSpec { it("creates a timer") { let timer = example.subject.startTimer() + let refreshRateAsDouble = Double(example.settings.refreshRate) expect(timer).toNot(beNil()) - expect(timer!.timeInterval).to(equal(example.settings.refreshRate)) + expect(timer!.timeInterval).to(equal(refreshRateAsDouble)) } it("adds the timer to the run loop") { diff --git a/README.md b/README.md index ad7f4ea..454335b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Devmynd](https://www.devmynd.com/wp-content/uploads/2016/07/logo-horizontal.jpg "Devmynd") + # Harbor Monitor your codeship builds from the comfort of a convenient OSX status bar application. @@ -16,8 +18,14 @@ You can also download Harbor from the app store. ### Setup Go to [Codeship] (http://www.codeship.io) (if you haven't already, you'll need to set up an account). Click on your profile picture. Under **My Account**, go to the **Account Settings** page. Copy your API Key. -Click on the Harbor app and select **Set Preferences**. Paste your API key in the field labeled 'Codeship API Key'. Set the refresh rate (which is measured in seconds) and hit 'Save'. -You're good to go! :D +Click on the Harbor app and select **Set Preferences**. Paste your API key in the field labeled 'Codeship API Key'. Set the refresh rate (which is measured in seconds) and hit 'Save'. Your projects will be automatically fetched from Codeship. +
+![Harbor Settings](https://www.devmynd.com/wp-content/uploads/2016/07/harbor_settings.jpg "Harbor Settings") +
+Click the Harbor icon from the menu bar. You will see a list of your Codeship Projects. Projects with a green Harbor icon are passing, while red means they are failing. A black Harbor icon means that a project is currently building. If you hover over a project you will see the your latest commits. +
+![Harbor Menu](https://www.devmynd.com/wp-content/uploads/2016/07/harbor_menu.jpg "Harbor Menu") +
### License Copyright (c) 2016 DevMynd Software.