├── .gitignore ├── OceanBar.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── terhechte.xcuserdatad │ └── xcschemes │ ├── OceanBar.xcscheme │ └── xcschememanagement.plist ├── OceanBar.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ └── OceanBar.xccheckout └── xcuserdata │ └── terhechte.xcuserdatad │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── OceanBar ├── BTAppDelegate.h ├── BTAppDelegate.m ├── BTDigitalOceanLogin.h ├── BTDigitalOceanLogin.m ├── BTOAuthLoginBase.h ├── BTOAuthLoginBase.m ├── BTOceanData.h ├── BTOceanData.m ├── BTSocialTextView.h ├── BTSocialTextView.m ├── BTStatusbarController.h ├── BTStatusbarController.m ├── Base.lproj │ └── MainMenu.xib ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-256.png │ │ ├── icon-32-1.png │ │ ├── icon-32.png │ │ ├── icon-512.png │ │ ├── icon-64.png │ │ ├── icon128x128.png │ │ ├── icon16x16.png │ │ ├── icon256x256.png │ │ └── icon512x512.png │ ├── closedLock.imageset │ │ ├── Contents.json │ │ ├── closedLock.png │ │ └── closedLock@2x.png │ ├── default.imageset │ │ ├── Contents.json │ │ ├── default.png │ │ └── default@2x.png │ ├── loading.imageset │ │ ├── Contents.json │ │ ├── loading.png │ │ └── loading@2x.png │ ├── newContent.imageset │ │ ├── Contents.json │ │ ├── newContent.png │ │ └── newContent@2x.png │ ├── notConnected.imageset │ │ ├── Contents.json │ │ ├── notConnected.png │ │ └── notConnected@2x.png │ ├── openLock.imageset │ │ ├── Contents.json │ │ ├── openLock.png │ │ └── openLock@2x.png │ ├── stateActive.imageset │ │ ├── Contents.json │ │ ├── stateActive.png │ │ └── stateActive@2x.png │ ├── stateInactive.imageset │ │ ├── Contents.json │ │ ├── stateInactive.png │ │ └── stateInactive@2x.png │ └── stateUpdated.imageset │ │ ├── Contents.json │ │ ├── stateUpdated.png │ │ └── stateUpdated@2x.png ├── NSString+BTOauthLogin.h ├── NSString+BTOauthLogin.m ├── OceanBar-Info.plist ├── OceanBar-Prefix.pch ├── OceanBar.entitlements ├── en.lproj │ ├── Credits.rtf │ └── InfoPlist.strings ├── kDigitalOceanClientKey.h └── main.m ├── OceanBarTests ├── OceanBarTests-Info.plist ├── OceanBarTests.m └── en.lproj │ └── InfoPlist.strings ├── Podfile ├── Podfile.lock ├── Readme.org ├── Resources ├── closedLock.png ├── closedLock@2x.png ├── default.png ├── default@2x.png ├── icon-1024.png ├── icon-256.png ├── icon-32.png ├── icon-512.png ├── icon-64.png ├── icon.icns ├── icon.png ├── loading.png ├── loading@2x.png ├── newContent.png ├── newContent@2x.png ├── notConnected.png ├── notConnected@2x.png ├── openLock.png ├── openLock@2x.png ├── stateActive.png ├── stateActive@2x.png ├── stateInactive.png ├── stateInactive@2x.png ├── stateUpdated.png └── stateUpdated@2x.png ├── screenshot1.jpeg └── screenshot2.jpeg /.gitignore: -------------------------------------------------------------------------------- 1 | Pods 2 | -------------------------------------------------------------------------------- /OceanBar.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4D31656F1B2C833E0035EA44 /* BTOAuthLoginBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D31656E1B2C833E0035EA44 /* BTOAuthLoginBase.m */; }; 11 | 4D3165711B2C83820035EA44 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3165701B2C83820035EA44 /* WebKit.framework */; }; 12 | 4D3165741B2C84090035EA44 /* NSString+BTOauthLogin.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D3165731B2C84090035EA44 /* NSString+BTOauthLogin.m */; }; 13 | 4D3165771B2C8A410035EA44 /* BTDigitalOceanLogin.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D3165761B2C8A410035EA44 /* BTDigitalOceanLogin.m */; }; 14 | 4DC520CF190E94CD00765BA2 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC520CE190E94CD00765BA2 /* Cocoa.framework */; }; 15 | 4DC520D9190E94CD00765BA2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4DC520D7190E94CD00765BA2 /* InfoPlist.strings */; }; 16 | 4DC520DB190E94CD00765BA2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC520DA190E94CD00765BA2 /* main.m */; }; 17 | 4DC520DF190E94CD00765BA2 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 4DC520DD190E94CD00765BA2 /* Credits.rtf */; }; 18 | 4DC520E2190E94CD00765BA2 /* BTAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC520E1190E94CD00765BA2 /* BTAppDelegate.m */; }; 19 | 4DC520E5190E94CD00765BA2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4DC520E3190E94CD00765BA2 /* MainMenu.xib */; }; 20 | 4DC520E7190E94CD00765BA2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4DC520E6190E94CD00765BA2 /* Images.xcassets */; }; 21 | 4DC520EF190E94CD00765BA2 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DC520CE190E94CD00765BA2 /* Cocoa.framework */; }; 22 | 4DC520F7190E94CE00765BA2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4DC520F5190E94CE00765BA2 /* InfoPlist.strings */; }; 23 | 4DC520F9190E94CE00765BA2 /* OceanBarTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC520F8190E94CE00765BA2 /* OceanBarTests.m */; }; 24 | 4DC52105190E95CD00765BA2 /* BTStatusbarController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC52104190E95CD00765BA2 /* BTStatusbarController.m */; }; 25 | 4DC52108190E9A5200765BA2 /* BTOceanData.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC52107190E9A5200765BA2 /* BTOceanData.m */; }; 26 | 4DC8BE69190EDFFF00B250CD /* BTSocialTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DC8BE68190EDFFF00B250CD /* BTSocialTextView.m */; }; 27 | A401856B37BC2CB8FDF3D4C7 /* libPods-OceanBar.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C7ABED945C5C23B16275D199 /* libPods-OceanBar.a */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 4DC520F0190E94CD00765BA2 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 4DC520C3190E94CD00765BA2 /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 4DC520CA190E94CD00765BA2; 36 | remoteInfo = OceanBar; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 4D31656D1B2C833E0035EA44 /* BTOAuthLoginBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTOAuthLoginBase.h; sourceTree = ""; }; 42 | 4D31656E1B2C833E0035EA44 /* BTOAuthLoginBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTOAuthLoginBase.m; sourceTree = ""; }; 43 | 4D3165701B2C83820035EA44 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 44 | 4D3165721B2C84090035EA44 /* NSString+BTOauthLogin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+BTOauthLogin.h"; sourceTree = ""; }; 45 | 4D3165731B2C84090035EA44 /* NSString+BTOauthLogin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+BTOauthLogin.m"; sourceTree = ""; }; 46 | 4D3165751B2C8A410035EA44 /* BTDigitalOceanLogin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTDigitalOceanLogin.h; sourceTree = ""; }; 47 | 4D3165761B2C8A410035EA44 /* BTDigitalOceanLogin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTDigitalOceanLogin.m; sourceTree = ""; }; 48 | 4D3165781B2C8A990035EA44 /* kDigitalOceanClientKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = kDigitalOceanClientKey.h; sourceTree = ""; }; 49 | 4DAE5FE619115C8800A7C1E0 /* OceanBar.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = OceanBar.entitlements; sourceTree = ""; }; 50 | 4DC520CB190E94CD00765BA2 /* OceanBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OceanBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 4DC520CE190E94CD00765BA2 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 52 | 4DC520D1190E94CD00765BA2 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 53 | 4DC520D2190E94CD00765BA2 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 54 | 4DC520D3190E94CD00765BA2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 55 | 4DC520D6190E94CD00765BA2 /* OceanBar-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OceanBar-Info.plist"; sourceTree = ""; }; 56 | 4DC520D8190E94CD00765BA2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 57 | 4DC520DA190E94CD00765BA2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 58 | 4DC520DC190E94CD00765BA2 /* OceanBar-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OceanBar-Prefix.pch"; sourceTree = ""; }; 59 | 4DC520DE190E94CD00765BA2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 60 | 4DC520E0190E94CD00765BA2 /* BTAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BTAppDelegate.h; sourceTree = ""; }; 61 | 4DC520E1190E94CD00765BA2 /* BTAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BTAppDelegate.m; sourceTree = ""; }; 62 | 4DC520E4190E94CD00765BA2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 63 | 4DC520E6190E94CD00765BA2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 64 | 4DC520EC190E94CD00765BA2 /* OceanBarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OceanBarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 4DC520F4190E94CD00765BA2 /* OceanBarTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OceanBarTests-Info.plist"; sourceTree = ""; }; 66 | 4DC520F6190E94CE00765BA2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 67 | 4DC520F8190E94CE00765BA2 /* OceanBarTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OceanBarTests.m; sourceTree = ""; }; 68 | 4DC52103190E95CD00765BA2 /* BTStatusbarController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTStatusbarController.h; sourceTree = ""; }; 69 | 4DC52104190E95CD00765BA2 /* BTStatusbarController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTStatusbarController.m; sourceTree = ""; }; 70 | 4DC52106190E9A5200765BA2 /* BTOceanData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTOceanData.h; sourceTree = ""; }; 71 | 4DC52107190E9A5200765BA2 /* BTOceanData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTOceanData.m; sourceTree = ""; }; 72 | 4DC5210B190E9D9500765BA2 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 73 | 4DC5210D190E9DE300765BA2 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 74 | 4DC8BE67190EDFFF00B250CD /* BTSocialTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BTSocialTextView.h; sourceTree = ""; }; 75 | 4DC8BE68190EDFFF00B250CD /* BTSocialTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BTSocialTextView.m; sourceTree = ""; }; 76 | B4BC6A1D51E9087127B4B820 /* Pods-OceanBar.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OceanBar.debug.xcconfig"; path = "Pods/Target Support Files/Pods-OceanBar/Pods-OceanBar.debug.xcconfig"; sourceTree = ""; }; 77 | C7ABED945C5C23B16275D199 /* libPods-OceanBar.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OceanBar.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | F8D6E753B496D428D360374E /* Pods-OceanBar.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OceanBar.release.xcconfig"; path = "Pods/Target Support Files/Pods-OceanBar/Pods-OceanBar.release.xcconfig"; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 4DC520C8190E94CD00765BA2 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | 4D3165711B2C83820035EA44 /* WebKit.framework in Frameworks */, 87 | 4DC520CF190E94CD00765BA2 /* Cocoa.framework in Frameworks */, 88 | A401856B37BC2CB8FDF3D4C7 /* libPods-OceanBar.a in Frameworks */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | 4DC520E9190E94CD00765BA2 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | 4DC520EF190E94CD00765BA2 /* Cocoa.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | /* End PBXFrameworksBuildPhase section */ 101 | 102 | /* Begin PBXGroup section */ 103 | 4DC520C2190E94CD00765BA2 = { 104 | isa = PBXGroup; 105 | children = ( 106 | 4DC520D4190E94CD00765BA2 /* OceanBar */, 107 | 4DC520F2190E94CD00765BA2 /* OceanBarTests */, 108 | 4DC520CD190E94CD00765BA2 /* Frameworks */, 109 | 4DC520CC190E94CD00765BA2 /* Products */, 110 | E02650916B3EEF9224DE8C56 /* Pods */, 111 | ); 112 | sourceTree = ""; 113 | }; 114 | 4DC520CC190E94CD00765BA2 /* Products */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 4DC520CB190E94CD00765BA2 /* OceanBar.app */, 118 | 4DC520EC190E94CD00765BA2 /* OceanBarTests.xctest */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 4DC520CD190E94CD00765BA2 /* Frameworks */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 4D3165701B2C83820035EA44 /* WebKit.framework */, 127 | 4DC5210D190E9DE300765BA2 /* Security.framework */, 128 | 4DC5210B190E9D9500765BA2 /* SystemConfiguration.framework */, 129 | 4DC520CE190E94CD00765BA2 /* Cocoa.framework */, 130 | 4DC520D0190E94CD00765BA2 /* Other Frameworks */, 131 | C7ABED945C5C23B16275D199 /* libPods-OceanBar.a */, 132 | ); 133 | name = Frameworks; 134 | sourceTree = ""; 135 | }; 136 | 4DC520D0190E94CD00765BA2 /* Other Frameworks */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 4DC520D1190E94CD00765BA2 /* AppKit.framework */, 140 | 4DC520D2190E94CD00765BA2 /* CoreData.framework */, 141 | 4DC520D3190E94CD00765BA2 /* Foundation.framework */, 142 | ); 143 | name = "Other Frameworks"; 144 | sourceTree = ""; 145 | }; 146 | 4DC520D4190E94CD00765BA2 /* OceanBar */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 4DAE5FE619115C8800A7C1E0 /* OceanBar.entitlements */, 150 | 4DC520E0190E94CD00765BA2 /* BTAppDelegate.h */, 151 | 4DC520E1190E94CD00765BA2 /* BTAppDelegate.m */, 152 | 4DC520E3190E94CD00765BA2 /* MainMenu.xib */, 153 | 4DC520E6190E94CD00765BA2 /* Images.xcassets */, 154 | 4DC520D5190E94CD00765BA2 /* Supporting Files */, 155 | 4DC52103190E95CD00765BA2 /* BTStatusbarController.h */, 156 | 4DC52104190E95CD00765BA2 /* BTStatusbarController.m */, 157 | 4DC52106190E9A5200765BA2 /* BTOceanData.h */, 158 | 4DC52107190E9A5200765BA2 /* BTOceanData.m */, 159 | 4DC8BE67190EDFFF00B250CD /* BTSocialTextView.h */, 160 | 4DC8BE68190EDFFF00B250CD /* BTSocialTextView.m */, 161 | 4D31656D1B2C833E0035EA44 /* BTOAuthLoginBase.h */, 162 | 4D31656E1B2C833E0035EA44 /* BTOAuthLoginBase.m */, 163 | 4D3165721B2C84090035EA44 /* NSString+BTOauthLogin.h */, 164 | 4D3165731B2C84090035EA44 /* NSString+BTOauthLogin.m */, 165 | 4D3165751B2C8A410035EA44 /* BTDigitalOceanLogin.h */, 166 | 4D3165761B2C8A410035EA44 /* BTDigitalOceanLogin.m */, 167 | 4D3165781B2C8A990035EA44 /* kDigitalOceanClientKey.h */, 168 | ); 169 | path = OceanBar; 170 | sourceTree = ""; 171 | }; 172 | 4DC520D5190E94CD00765BA2 /* Supporting Files */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | 4DC520D6190E94CD00765BA2 /* OceanBar-Info.plist */, 176 | 4DC520D7190E94CD00765BA2 /* InfoPlist.strings */, 177 | 4DC520DA190E94CD00765BA2 /* main.m */, 178 | 4DC520DC190E94CD00765BA2 /* OceanBar-Prefix.pch */, 179 | 4DC520DD190E94CD00765BA2 /* Credits.rtf */, 180 | ); 181 | name = "Supporting Files"; 182 | sourceTree = ""; 183 | }; 184 | 4DC520F2190E94CD00765BA2 /* OceanBarTests */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 4DC520F8190E94CE00765BA2 /* OceanBarTests.m */, 188 | 4DC520F3190E94CD00765BA2 /* Supporting Files */, 189 | ); 190 | path = OceanBarTests; 191 | sourceTree = ""; 192 | }; 193 | 4DC520F3190E94CD00765BA2 /* Supporting Files */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 4DC520F4190E94CD00765BA2 /* OceanBarTests-Info.plist */, 197 | 4DC520F5190E94CE00765BA2 /* InfoPlist.strings */, 198 | ); 199 | name = "Supporting Files"; 200 | sourceTree = ""; 201 | }; 202 | E02650916B3EEF9224DE8C56 /* Pods */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | B4BC6A1D51E9087127B4B820 /* Pods-OceanBar.debug.xcconfig */, 206 | F8D6E753B496D428D360374E /* Pods-OceanBar.release.xcconfig */, 207 | ); 208 | name = Pods; 209 | sourceTree = ""; 210 | }; 211 | /* End PBXGroup section */ 212 | 213 | /* Begin PBXNativeTarget section */ 214 | 4DC520CA190E94CD00765BA2 /* OceanBar */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = 4DC520FC190E94CE00765BA2 /* Build configuration list for PBXNativeTarget "OceanBar" */; 217 | buildPhases = ( 218 | 50C2AA3F55569C2CE0B4BFF1 /* 📦 Check Pods Manifest.lock */, 219 | 4DC520C7190E94CD00765BA2 /* Sources */, 220 | 4DC520C8190E94CD00765BA2 /* Frameworks */, 221 | 4DC520C9190E94CD00765BA2 /* Resources */, 222 | 5BABF2ED47F2277A82D2833D /* 📦 Embed Pods Frameworks */, 223 | 9954A0C291E16D5B236A323A /* 📦 Copy Pods Resources */, 224 | ); 225 | buildRules = ( 226 | ); 227 | dependencies = ( 228 | ); 229 | name = OceanBar; 230 | productName = OceanBar; 231 | productReference = 4DC520CB190E94CD00765BA2 /* OceanBar.app */; 232 | productType = "com.apple.product-type.application"; 233 | }; 234 | 4DC520EB190E94CD00765BA2 /* OceanBarTests */ = { 235 | isa = PBXNativeTarget; 236 | buildConfigurationList = 4DC520FF190E94CE00765BA2 /* Build configuration list for PBXNativeTarget "OceanBarTests" */; 237 | buildPhases = ( 238 | 4DC520E8190E94CD00765BA2 /* Sources */, 239 | 4DC520E9190E94CD00765BA2 /* Frameworks */, 240 | 4DC520EA190E94CD00765BA2 /* Resources */, 241 | ); 242 | buildRules = ( 243 | ); 244 | dependencies = ( 245 | 4DC520F1190E94CD00765BA2 /* PBXTargetDependency */, 246 | ); 247 | name = OceanBarTests; 248 | productName = OceanBarTests; 249 | productReference = 4DC520EC190E94CD00765BA2 /* OceanBarTests.xctest */; 250 | productType = "com.apple.product-type.bundle.unit-test"; 251 | }; 252 | /* End PBXNativeTarget section */ 253 | 254 | /* Begin PBXProject section */ 255 | 4DC520C3190E94CD00765BA2 /* Project object */ = { 256 | isa = PBXProject; 257 | attributes = { 258 | CLASSPREFIX = BT; 259 | LastUpgradeCheck = 0630; 260 | ORGANIZATIONNAME = "Benedikt Terhechte"; 261 | TargetAttributes = { 262 | 4DC520CA190E94CD00765BA2 = { 263 | DevelopmentTeam = 76VT9VZ6GK; 264 | SystemCapabilities = { 265 | com.apple.Sandbox = { 266 | enabled = 1; 267 | }; 268 | }; 269 | }; 270 | 4DC520EB190E94CD00765BA2 = { 271 | TestTargetID = 4DC520CA190E94CD00765BA2; 272 | }; 273 | }; 274 | }; 275 | buildConfigurationList = 4DC520C6190E94CD00765BA2 /* Build configuration list for PBXProject "OceanBar" */; 276 | compatibilityVersion = "Xcode 3.2"; 277 | developmentRegion = English; 278 | hasScannedForEncodings = 0; 279 | knownRegions = ( 280 | en, 281 | Base, 282 | ); 283 | mainGroup = 4DC520C2190E94CD00765BA2; 284 | productRefGroup = 4DC520CC190E94CD00765BA2 /* Products */; 285 | projectDirPath = ""; 286 | projectRoot = ""; 287 | targets = ( 288 | 4DC520CA190E94CD00765BA2 /* OceanBar */, 289 | 4DC520EB190E94CD00765BA2 /* OceanBarTests */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 4DC520C9190E94CD00765BA2 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 4DC520D9190E94CD00765BA2 /* InfoPlist.strings in Resources */, 300 | 4DC520E7190E94CD00765BA2 /* Images.xcassets in Resources */, 301 | 4DC520DF190E94CD00765BA2 /* Credits.rtf in Resources */, 302 | 4DC520E5190E94CD00765BA2 /* MainMenu.xib in Resources */, 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | 4DC520EA190E94CD00765BA2 /* Resources */ = { 307 | isa = PBXResourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | 4DC520F7190E94CE00765BA2 /* InfoPlist.strings in Resources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXResourcesBuildPhase section */ 315 | 316 | /* Begin PBXShellScriptBuildPhase section */ 317 | 50C2AA3F55569C2CE0B4BFF1 /* 📦 Check Pods Manifest.lock */ = { 318 | isa = PBXShellScriptBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | inputPaths = ( 323 | ); 324 | name = "📦 Check Pods Manifest.lock"; 325 | outputPaths = ( 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | shellPath = /bin/sh; 329 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 330 | showEnvVarsInLog = 0; 331 | }; 332 | 5BABF2ED47F2277A82D2833D /* 📦 Embed Pods Frameworks */ = { 333 | isa = PBXShellScriptBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | ); 337 | inputPaths = ( 338 | ); 339 | name = "📦 Embed Pods Frameworks"; 340 | outputPaths = ( 341 | ); 342 | runOnlyForDeploymentPostprocessing = 0; 343 | shellPath = /bin/sh; 344 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-OceanBar/Pods-OceanBar-frameworks.sh\"\n"; 345 | showEnvVarsInLog = 0; 346 | }; 347 | 9954A0C291E16D5B236A323A /* 📦 Copy Pods Resources */ = { 348 | isa = PBXShellScriptBuildPhase; 349 | buildActionMask = 2147483647; 350 | files = ( 351 | ); 352 | inputPaths = ( 353 | ); 354 | name = "📦 Copy Pods Resources"; 355 | outputPaths = ( 356 | ); 357 | runOnlyForDeploymentPostprocessing = 0; 358 | shellPath = /bin/sh; 359 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-OceanBar/Pods-OceanBar-resources.sh\"\n"; 360 | showEnvVarsInLog = 0; 361 | }; 362 | /* End PBXShellScriptBuildPhase section */ 363 | 364 | /* Begin PBXSourcesBuildPhase section */ 365 | 4DC520C7190E94CD00765BA2 /* Sources */ = { 366 | isa = PBXSourcesBuildPhase; 367 | buildActionMask = 2147483647; 368 | files = ( 369 | 4DC520E2190E94CD00765BA2 /* BTAppDelegate.m in Sources */, 370 | 4DC520DB190E94CD00765BA2 /* main.m in Sources */, 371 | 4DC8BE69190EDFFF00B250CD /* BTSocialTextView.m in Sources */, 372 | 4D3165771B2C8A410035EA44 /* BTDigitalOceanLogin.m in Sources */, 373 | 4DC52108190E9A5200765BA2 /* BTOceanData.m in Sources */, 374 | 4D31656F1B2C833E0035EA44 /* BTOAuthLoginBase.m in Sources */, 375 | 4DC52105190E95CD00765BA2 /* BTStatusbarController.m in Sources */, 376 | 4D3165741B2C84090035EA44 /* NSString+BTOauthLogin.m in Sources */, 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | 4DC520E8190E94CD00765BA2 /* Sources */ = { 381 | isa = PBXSourcesBuildPhase; 382 | buildActionMask = 2147483647; 383 | files = ( 384 | 4DC520F9190E94CE00765BA2 /* OceanBarTests.m in Sources */, 385 | ); 386 | runOnlyForDeploymentPostprocessing = 0; 387 | }; 388 | /* End PBXSourcesBuildPhase section */ 389 | 390 | /* Begin PBXTargetDependency section */ 391 | 4DC520F1190E94CD00765BA2 /* PBXTargetDependency */ = { 392 | isa = PBXTargetDependency; 393 | target = 4DC520CA190E94CD00765BA2 /* OceanBar */; 394 | targetProxy = 4DC520F0190E94CD00765BA2 /* PBXContainerItemProxy */; 395 | }; 396 | /* End PBXTargetDependency section */ 397 | 398 | /* Begin PBXVariantGroup section */ 399 | 4DC520D7190E94CD00765BA2 /* InfoPlist.strings */ = { 400 | isa = PBXVariantGroup; 401 | children = ( 402 | 4DC520D8190E94CD00765BA2 /* en */, 403 | ); 404 | name = InfoPlist.strings; 405 | sourceTree = ""; 406 | }; 407 | 4DC520DD190E94CD00765BA2 /* Credits.rtf */ = { 408 | isa = PBXVariantGroup; 409 | children = ( 410 | 4DC520DE190E94CD00765BA2 /* en */, 411 | ); 412 | name = Credits.rtf; 413 | sourceTree = ""; 414 | }; 415 | 4DC520E3190E94CD00765BA2 /* MainMenu.xib */ = { 416 | isa = PBXVariantGroup; 417 | children = ( 418 | 4DC520E4190E94CD00765BA2 /* Base */, 419 | ); 420 | name = MainMenu.xib; 421 | sourceTree = ""; 422 | }; 423 | 4DC520F5190E94CE00765BA2 /* InfoPlist.strings */ = { 424 | isa = PBXVariantGroup; 425 | children = ( 426 | 4DC520F6190E94CE00765BA2 /* en */, 427 | ); 428 | name = InfoPlist.strings; 429 | sourceTree = ""; 430 | }; 431 | /* End PBXVariantGroup section */ 432 | 433 | /* Begin XCBuildConfiguration section */ 434 | 4DC520FA190E94CE00765BA2 /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ALWAYS_SEARCH_USER_PATHS = NO; 438 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 439 | CLANG_CXX_LIBRARY = "libc++"; 440 | CLANG_ENABLE_MODULES = YES; 441 | CLANG_ENABLE_OBJC_ARC = YES; 442 | CLANG_WARN_BOOL_CONVERSION = YES; 443 | CLANG_WARN_CONSTANT_CONVERSION = YES; 444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INT_CONVERSION = YES; 448 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | COPY_PHASE_STRIP = NO; 451 | GCC_C_LANGUAGE_STANDARD = gnu99; 452 | GCC_DYNAMIC_NO_PIC = NO; 453 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 454 | GCC_OPTIMIZATION_LEVEL = 0; 455 | GCC_PREPROCESSOR_DEFINITIONS = ( 456 | "DEBUG=1", 457 | "$(inherited)", 458 | ); 459 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 460 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 461 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 462 | GCC_WARN_UNDECLARED_SELECTOR = YES; 463 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 464 | GCC_WARN_UNUSED_FUNCTION = YES; 465 | GCC_WARN_UNUSED_VARIABLE = YES; 466 | MACOSX_DEPLOYMENT_TARGET = 10.9; 467 | ONLY_ACTIVE_ARCH = YES; 468 | SDKROOT = macosx; 469 | }; 470 | name = Debug; 471 | }; 472 | 4DC520FB190E94CE00765BA2 /* Release */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ALWAYS_SEARCH_USER_PATHS = NO; 476 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 477 | CLANG_CXX_LIBRARY = "libc++"; 478 | CLANG_ENABLE_MODULES = YES; 479 | CLANG_ENABLE_OBJC_ARC = YES; 480 | CLANG_WARN_BOOL_CONVERSION = YES; 481 | CLANG_WARN_CONSTANT_CONVERSION = YES; 482 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 483 | CLANG_WARN_EMPTY_BODY = YES; 484 | CLANG_WARN_ENUM_CONVERSION = YES; 485 | CLANG_WARN_INT_CONVERSION = YES; 486 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 487 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 488 | COPY_PHASE_STRIP = YES; 489 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 490 | ENABLE_NS_ASSERTIONS = NO; 491 | GCC_C_LANGUAGE_STANDARD = gnu99; 492 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 493 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 494 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 495 | GCC_WARN_UNDECLARED_SELECTOR = YES; 496 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 497 | GCC_WARN_UNUSED_FUNCTION = YES; 498 | GCC_WARN_UNUSED_VARIABLE = YES; 499 | MACOSX_DEPLOYMENT_TARGET = 10.9; 500 | SDKROOT = macosx; 501 | }; 502 | name = Release; 503 | }; 504 | 4DC520FD190E94CE00765BA2 /* Debug */ = { 505 | isa = XCBuildConfiguration; 506 | baseConfigurationReference = B4BC6A1D51E9087127B4B820 /* Pods-OceanBar.debug.xcconfig */; 507 | buildSettings = { 508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 509 | CODE_SIGN_ENTITLEMENTS = OceanBar/OceanBar.entitlements; 510 | CODE_SIGN_IDENTITY = "Developer ID Application"; 511 | COMBINE_HIDPI_IMAGES = YES; 512 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 513 | GCC_PREFIX_HEADER = "OceanBar/OceanBar-Prefix.pch"; 514 | INFOPLIST_FILE = "OceanBar/OceanBar-Info.plist"; 515 | MACOSX_DEPLOYMENT_TARGET = 10.9; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | PROVISIONING_PROFILE = ""; 518 | SDKROOT = macosx; 519 | WRAPPER_EXTENSION = app; 520 | }; 521 | name = Debug; 522 | }; 523 | 4DC520FE190E94CE00765BA2 /* Release */ = { 524 | isa = XCBuildConfiguration; 525 | baseConfigurationReference = F8D6E753B496D428D360374E /* Pods-OceanBar.release.xcconfig */; 526 | buildSettings = { 527 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 528 | CODE_SIGN_ENTITLEMENTS = OceanBar/OceanBar.entitlements; 529 | CODE_SIGN_IDENTITY = "Developer ID Application"; 530 | COMBINE_HIDPI_IMAGES = YES; 531 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 532 | GCC_PREFIX_HEADER = "OceanBar/OceanBar-Prefix.pch"; 533 | INFOPLIST_FILE = "OceanBar/OceanBar-Info.plist"; 534 | MACOSX_DEPLOYMENT_TARGET = 10.9; 535 | PRODUCT_NAME = "$(TARGET_NAME)"; 536 | SDKROOT = macosx; 537 | WRAPPER_EXTENSION = app; 538 | }; 539 | name = Release; 540 | }; 541 | 4DC52100190E94CE00765BA2 /* Debug */ = { 542 | isa = XCBuildConfiguration; 543 | buildSettings = { 544 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/OceanBar.app/Contents/MacOS/OceanBar"; 545 | COMBINE_HIDPI_IMAGES = YES; 546 | FRAMEWORK_SEARCH_PATHS = ( 547 | "$(DEVELOPER_FRAMEWORKS_DIR)", 548 | "$(inherited)", 549 | ); 550 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 551 | GCC_PREFIX_HEADER = "OceanBar/OceanBar-Prefix.pch"; 552 | GCC_PREPROCESSOR_DEFINITIONS = ( 553 | "DEBUG=1", 554 | "$(inherited)", 555 | ); 556 | INFOPLIST_FILE = "OceanBarTests/OceanBarTests-Info.plist"; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | TEST_HOST = "$(BUNDLE_LOADER)"; 559 | WRAPPER_EXTENSION = xctest; 560 | }; 561 | name = Debug; 562 | }; 563 | 4DC52101190E94CE00765BA2 /* Release */ = { 564 | isa = XCBuildConfiguration; 565 | buildSettings = { 566 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/OceanBar.app/Contents/MacOS/OceanBar"; 567 | COMBINE_HIDPI_IMAGES = YES; 568 | FRAMEWORK_SEARCH_PATHS = ( 569 | "$(DEVELOPER_FRAMEWORKS_DIR)", 570 | "$(inherited)", 571 | ); 572 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 573 | GCC_PREFIX_HEADER = "OceanBar/OceanBar-Prefix.pch"; 574 | INFOPLIST_FILE = "OceanBarTests/OceanBarTests-Info.plist"; 575 | PRODUCT_NAME = "$(TARGET_NAME)"; 576 | TEST_HOST = "$(BUNDLE_LOADER)"; 577 | WRAPPER_EXTENSION = xctest; 578 | }; 579 | name = Release; 580 | }; 581 | /* End XCBuildConfiguration section */ 582 | 583 | /* Begin XCConfigurationList section */ 584 | 4DC520C6190E94CD00765BA2 /* Build configuration list for PBXProject "OceanBar" */ = { 585 | isa = XCConfigurationList; 586 | buildConfigurations = ( 587 | 4DC520FA190E94CE00765BA2 /* Debug */, 588 | 4DC520FB190E94CE00765BA2 /* Release */, 589 | ); 590 | defaultConfigurationIsVisible = 0; 591 | defaultConfigurationName = Release; 592 | }; 593 | 4DC520FC190E94CE00765BA2 /* Build configuration list for PBXNativeTarget "OceanBar" */ = { 594 | isa = XCConfigurationList; 595 | buildConfigurations = ( 596 | 4DC520FD190E94CE00765BA2 /* Debug */, 597 | 4DC520FE190E94CE00765BA2 /* Release */, 598 | ); 599 | defaultConfigurationIsVisible = 0; 600 | defaultConfigurationName = Release; 601 | }; 602 | 4DC520FF190E94CE00765BA2 /* Build configuration list for PBXNativeTarget "OceanBarTests" */ = { 603 | isa = XCConfigurationList; 604 | buildConfigurations = ( 605 | 4DC52100190E94CE00765BA2 /* Debug */, 606 | 4DC52101190E94CE00765BA2 /* Release */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | /* End XCConfigurationList section */ 612 | }; 613 | rootObject = 4DC520C3190E94CD00765BA2 /* Project object */; 614 | } 615 | -------------------------------------------------------------------------------- /OceanBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OceanBar.xcodeproj/xcuserdata/terhechte.xcuserdatad/xcschemes/OceanBar.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 61 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /OceanBar.xcodeproj/xcuserdata/terhechte.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | OceanBar.xcscheme 8 | 9 | orderHint 10 | 3 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 4DC520CA190E94CD00765BA2 16 | 17 | primary 18 | 19 | 20 | 4DC520EB190E94CD00765BA2 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /OceanBar.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /OceanBar.xcworkspace/xcshareddata/OceanBar.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | DA297EDE-D659-4C6D-81F9-EE11C61BAD09 9 | IDESourceControlProjectName 10 | OceanBar 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 0907E000CD62E05CEBE38BBCF58B1C3D85F935A1 14 | github.com:terhechte/OceanBar.git 15 | 16 | IDESourceControlProjectPath 17 | OceanBar.xcworkspace 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 0907E000CD62E05CEBE38BBCF58B1C3D85F935A1 21 | .. 22 | 23 | IDESourceControlProjectURL 24 | github.com:terhechte/OceanBar.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 0907E000CD62E05CEBE38BBCF58B1C3D85F935A1 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 0907E000CD62E05CEBE38BBCF58B1C3D85F935A1 36 | IDESourceControlWCCName 37 | OceanBar 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /OceanBar.xcworkspace/xcuserdata/terhechte.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 24 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /OceanBar/BTAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTAppDelegate.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BTDigitalOceanLogin.h" 11 | 12 | @interface BTAppDelegate : NSObject 13 | 14 | @property (weak) IBOutlet BTDigitalOceanLogin *digitalOceanLogin; 15 | @property (weak) IBOutlet NSButton *loginLogoutButton; 16 | @property (weak) IBOutlet NSTextField *credentialUsernameField; 17 | @property (weak) IBOutlet NSTextField *credentialPasswordField; 18 | @property (weak) IBOutlet NSWindow *credentialWindow; 19 | @property (weak) IBOutlet NSTextField *customTokenField; 20 | @property (retain) NSString *credentialLoginErrorMessage; 21 | 22 | @property (retain) NSString *currentUserName; 23 | 24 | @property (weak) IBOutlet NSWindow *window; 25 | @property (weak) IBOutlet NSTabView *preferencesTabView; 26 | 27 | - (IBAction)doLoginToDigitalOcean:(id)sender; 28 | 29 | - (IBAction)doEnterCustomToken:(id)sender; 30 | - (IBAction)doOpenCustomTokenWebsite:(id)sender; 31 | 32 | - (IBAction)credentialOk:(id)sender; 33 | - (IBAction)credentialCancel:(id)sender; 34 | - (IBAction)credentialSignup:(id)sender; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /OceanBar/BTAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTAppDelegate.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTAppDelegate.h" 10 | #import "BTStatusbarController.h" 11 | #import "BTSocialTextView.h" 12 | #import 13 | 14 | NSString * const kDigitalOceanAPILink = @"https://cloud.digitalocean.com/api_access"; 15 | 16 | @interface BTAppDelegate() 17 | @property (retain) BTStatusbarController *statusBarController; 18 | @end 19 | 20 | @implementation BTAppDelegate 21 | 22 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 23 | { 24 | NSUserDefaults *d = [NSUserDefaults standardUserDefaults]; 25 | 26 | NSDictionary *defaults = @{@"reloadInterval": @2, 27 | @"postNotificationCenter": @YES, 28 | @"hideFromDock": @YES, 29 | @"terminalEmulator": @"Terminal", 30 | @"terminalCommand": @"ssh root@{{IPADDRESS}}"}; 31 | 32 | [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; 33 | 34 | // Register the defaults, and set the start values 35 | if (![d boolForKey:@"hasPreferencesDefaults"]) { 36 | [d setBool:YES forKey:@"hasPreferencesDefaults"]; 37 | [[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:defaults]; 38 | } 39 | 40 | // if this is the first start, animate the user to fill in his values 41 | NSArray *accounts = [[NXOAuth2AccountStore sharedStore] accounts]; 42 | if (accounts.count == 0) { 43 | 44 | self.loginLogoutButton.title = NSLocalizedString(@"Login", @"In the preferences, the button"); 45 | 46 | // display the preferences window 47 | [self.window makeKeyAndOrderFront:self]; 48 | 49 | NSAlert *alert = 50 | [NSAlert alertWithMessageText:NSLocalizedString(@"Please sign in to Digital Ocean", @"First Start Popup") 51 | defaultButton:NSLocalizedString(@"Ok", @"First Start Popup") 52 | alternateButton:nil 53 | otherButton:nil 54 | informativeTextWithFormat:NSLocalizedString(@"Please sign in to Digital Ocean with your Digital Ocean User account", @"First Start Popup")]; 55 | 56 | [alert beginSheetModalForWindow:self.window 57 | modalDelegate:self 58 | didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) 59 | contextInfo:nil]; 60 | } else { 61 | NXOAuth2Account *ac = accounts.firstObject; 62 | self.currentUserName = [(NSDictionary*)ac.userData objectForKey:@"username"]; 63 | 64 | self.loginLogoutButton.title = NSLocalizedString(@"Logout", @"In the preferences, the button"); 65 | } 66 | 67 | self.statusBarController = [[BTStatusbarController alloc] init]; 68 | self.statusBarController.delegate = self; 69 | 70 | if (![d boolForKey:@"hideFromDock"]) { 71 | [self showDockIcon]; 72 | } 73 | } 74 | 75 | - (void) alertDidEnd:(NSAlert *) alert returnCode:(int) returnCode contextInfo:(int *) contextInfo { 76 | [self.preferencesTabView selectTabViewItemAtIndex:1]; 77 | [self doLoginToDigitalOcean:nil]; 78 | } 79 | 80 | 81 | - (IBAction)doLoginToDigitalOcean:(id)sender { 82 | NSArray *accounts = [[NXOAuth2AccountStore sharedStore] accounts]; 83 | if (accounts.count == 0) { 84 | // login 85 | [self.window beginSheet:self.credentialWindow 86 | completionHandler:nil]; 87 | } else { 88 | // logout 89 | for (id acc in accounts) { 90 | [[NXOAuth2AccountStore sharedStore] removeAccount:acc]; 91 | } 92 | 93 | self.currentUserName = @""; 94 | 95 | self.loginLogoutButton.title = NSLocalizedString(@"Login", @"In the preferences, the button"); 96 | 97 | [self.statusBarController reloadContents]; 98 | } 99 | } 100 | 101 | - (IBAction)credentialOk:(id)sender { 102 | self.credentialLoginErrorMessage = @""; 103 | 104 | NSString *username = self.credentialUsernameField.stringValue; 105 | NSString *password = self.credentialPasswordField.stringValue; 106 | 107 | if (username == nil || username.length == 0) { 108 | self.credentialLoginErrorMessage = NSLocalizedString(@"Error: Please enter a valid username", nil); 109 | return; 110 | } 111 | 112 | if (password == nil || password.length == 0) { 113 | self.credentialLoginErrorMessage = NSLocalizedString(@"Error: Please enter a valid password", nil); 114 | return; 115 | } 116 | 117 | [self.window endSheet:self.credentialWindow]; 118 | 119 | [self.digitalOceanLogin startLoginProcessWithUsername:username password:password]; 120 | } 121 | 122 | - (IBAction)credentialCancel:(id)sender { 123 | [self.window endSheet:self.credentialWindow]; 124 | } 125 | 126 | - (IBAction)credentialSignup:(id)sender { 127 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://cloud.digitalocean.com/registrations/new"]]; 128 | } 129 | 130 | - (IBAction)doEnterCustomToken:(id)sender { 131 | NSString *customToken = [self.customTokenField stringValue]; 132 | if (!customToken || [customToken length] == 0) { 133 | NSRunAlertPanel(@"Error", @"Please provide a valid custom token", @"Ok", nil, nil); 134 | return; 135 | } 136 | 137 | [[NSUserDefaults standardUserDefaults] setObject:customToken forKey:kCustomTokenKey]; 138 | 139 | [self.statusBarController reloadContents]; 140 | } 141 | 142 | - (IBAction)doOpenCustomTokenWebsite:(id)sender { 143 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://cloud.digitalocean.com/settings/api/tokens"]]; 144 | } 145 | 146 | //----------------------------------------------------------------------------- 147 | #pragma mark BTStatusBarControllerDelegate 148 | //----------------------------------------------------------------------------- 149 | 150 | - (void) openPreferences { 151 | [NSApp activateIgnoringOtherApps:YES]; 152 | [self.window makeKeyAndOrderFront:self]; 153 | NSString *customToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCustomTokenKey]; 154 | if (customToken) { 155 | [self.customTokenField setStringValue:customToken]; 156 | } 157 | } 158 | 159 | - (void) exit { 160 | [NSApp terminate:self]; 161 | } 162 | 163 | - (void) showDockIcon { 164 | // currently deactivated, there's nothing in the main menu that would 165 | // make this functionality any useful... 166 | return; 167 | //http://stackoverflow.com/questions/620841/how-to-hide-the-dock-icon 168 | ProcessSerialNumber psn = { 0, kCurrentProcess }; 169 | TransformProcessType(&psn, kProcessTransformToForegroundApplication); 170 | 171 | } 172 | 173 | #pragma mark - 174 | #pragma mark BTOAuthUserCredentialsProtocol 175 | 176 | - (void) loginManagerCredentialsWrong:(id)loginManager { 177 | self.credentialLoginErrorMessage = NSLocalizedString(@"Error: Please enter a valid password", nil); 178 | [self doLoginToDigitalOcean:nil]; 179 | } 180 | 181 | - (void) loginManagerLoginSuccess:(id)loginManager withNewAccount:(NXOAuth2Account*)account { 182 | // Set the user data as DO returns nil 183 | [account setUserData:@{@"username": self.credentialUsernameField.stringValue}]; 184 | 185 | self.currentUserName = self.credentialUsernameField.stringValue; 186 | 187 | [self.statusBarController reloadContents]; 188 | 189 | self.loginLogoutButton.title = NSLocalizedString(@"Logout", @"In the preferences, the button"); 190 | } 191 | 192 | - (void) loginManagerLoginFailure:(id)loginManager { 193 | 194 | } 195 | 196 | @end 197 | -------------------------------------------------------------------------------- /OceanBar/BTDigitalOceanLogin.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTDigitalOceanLogin.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTOAuthLoginBase.h" 10 | 11 | @interface BTDigitalOceanLogin : BTOAuthLoginBase { 12 | // Internal state we need to assess whether it works 13 | BOOL _wasInLogin; 14 | BOOL _wasInSession; 15 | BOOL _didShowSheet; 16 | } 17 | @end 18 | -------------------------------------------------------------------------------- /OceanBar/BTDigitalOceanLogin.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTDigitalOceanLogin.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTDigitalOceanLogin.h" 10 | #import "kDigitalOceanClientKey.h" 11 | #import "NSString+BTOauthLogin.h" 12 | 13 | @implementation BTDigitalOceanLogin 14 | 15 | - (NSString*) name { 16 | return @"DigitalOcean"; 17 | } 18 | 19 | - (NSString *) clientID { 20 | return kDigitalOceanClientId; 21 | } 22 | 23 | - (NSString *) clientSecret { 24 | return kDigitalOceanClientSecret; 25 | } 26 | 27 | - (NSURL *) authorizationURL { 28 | return [NSURL URLWithString:@"https://cloud.digitalocean.com/v1/oauth/authorize"]; 29 | } 30 | 31 | - (NSURL *) tokenURL { 32 | return [NSURL URLWithString:@"https://cloud.digitalocean.com/v1/oauth/token"]; 33 | } 34 | 35 | - (NSURL *) redirectURL { 36 | return [NSURL URLWithString:@"oceanbar://success"]; 37 | } 38 | 39 | - (void) addLoginParams:(NSMutableDictionary*)params { 40 | NSMutableString *randString = @"".mutableCopy; 41 | for (int i=0; i<=16; i++)[randString appendFormat:@"%c", 'a' + arc4random_uniform(20)]; 42 | [params setObject:randString.copy forKey:@"state"]; 43 | } 44 | - (NSSet*) desiredScope { 45 | return [NSSet setWithObjects:@"read", @"write", nil]; 46 | } 47 | 48 | - (BTOAuthURLAction) actionForURL:(NSURL*)url orCustomURL:(NSURL**)customURL 49 | error:(NSError**)error { 50 | /* Flow: 51 | 1. https://cloud.digitalocean.com/v1/oauth/authorize?client_id=297 is called. 52 | If the user is not logged in yet, there'll be a redirect to 53 | 2. https://cloud.digitalocean.com/login - the login form 54 | 3. There, the user submits, and comes to https://cloud.digitalocean.com/sessions 55 | 4. Finally, if the login is successful, forwarded back to 56 | 5. https://cloud.digitalocean.com/v1/oauth/authorize?client_id=297 to ask the user whether he wants to authorize the app 57 | 6. If the user replies with yes, it will first forward to 58 | 6.1 https://cloud.digitalocean.com/v1/oauth/authorize 59 | 6.2 and then forward to oceanbar://success?code=96731e38d0d857b2884c1970520b1af52fa99114c9724e913a0185a6de5ed7c4 60 | 7. If the user denies, it will first forward to 61 | 7.1 https://cloud.digitalocean.com/v1/oauth/authorize 62 | 7.2 and then forward to oceanbar://success?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request. 63 | 8. Any other links the user clicks, are to be loaded verbatim in a different window, there is a link to the app url: http://terhechte.github.io/OceanBar/ 64 | 9. There's a button to log out, which will do the following 65 | 9.1 https://cloud.digitalocean.com/logout 66 | 9.2 then https://cloud.digitalocean.com/ 67 | 9.3 and finally https://cloud.digitalocean.com/login again 68 | */ 69 | 70 | // Flow for Digital Ocean 71 | NSString *path = [url path]; 72 | NSString *host = [url host]; 73 | NSString *scheme = [url scheme]; 74 | 75 | if ([scheme isEqualToString:@"https"]) { 76 | if ([host isEqualToString:@"cloud.digitalocean.com"]) { 77 | if ([path isEqualToString:@"/v1/oauth/authorize"] && !_wasInLogin && !_wasInSession) { 78 | return BTOAuthFancyLoadURL; 79 | } else if ([path isEqualToString:@"/v1/oauth/authorize"] && _wasInLogin && _wasInSession && !_didShowSheet) { 80 | _didShowSheet = true; 81 | return BTOAuthFancyShowSheet; 82 | } else if ([path isEqualToString:@"/v1/oauth/authorize"] && _wasInLogin && _wasInSession && _didShowSheet) { 83 | // This is the the user denies or the user allows case, we just load it, and hide the sheet 84 | return BTOAuthFancyHideSheet; 85 | } else if ([path isEqualToString:@"/login"]) { 86 | if (_wasInSession && _wasInLogin) { 87 | // the user entered a wrong password, we already were here: 88 | _wasInLogin = false; 89 | _wasInSession = false; 90 | return BTOAuthFancyWrongCredentials; 91 | } 92 | _wasInLogin = true; 93 | return BTOAuthFancyInsertCredentials; 94 | } else if ([path isEqualToString:@"/sessions"]) { 95 | _wasInSession = true; 96 | return BTOAuthFancyLoadURL; 97 | } else if ([path isEqualToString:@"/logout"]) { 98 | _wasInSession = false; 99 | _wasInLogin = false; 100 | _didShowSheet = false; 101 | return BTOAuthFancyHideSheet; 102 | } 103 | } 104 | } else if ([scheme isEqualToString:@"oceanbar"]) { 105 | NSDictionary *components = [url.query dictionaryFromQueryComponents]; 106 | if (components[@"code"]) { 107 | // success 108 | 109 | // reset state 110 | _didShowSheet = false; 111 | _wasInLogin = false; 112 | _wasInSession = false; 113 | 114 | return BTOAuthFancyFinished; 115 | } else if (components[@"error"]) { 116 | // failure 117 | 118 | // reset state 119 | _didShowSheet = false; 120 | _wasInLogin = false; 121 | _wasInSession = false; 122 | 123 | return BTOAuthFancyError; 124 | } 125 | } 126 | 127 | return BTOAuthFancyLoadExternal; 128 | } 129 | 130 | - (BTOAuthURLType) pageTypeForDataSource:(WebDataSource*)dataSource { 131 | 132 | /*! 133 | @abstract Verify that string contains all strings in strings. 134 | @discussion The strings need to be in sequential order in the document 135 | */ 136 | BOOL (^stringContainsStrings)(NSArray *strings, NSString* string) = ^BOOL (NSArray *strings, NSString* string) { 137 | NSScanner *haystack = [NSScanner scannerWithString:string]; 138 | 139 | for (NSString *needle in strings) { 140 | NSString *readString = nil; 141 | if (![haystack scanUpToString:needle intoString:&readString]) { 142 | return false; 143 | } 144 | if (readString.length == string.length) { 145 | return false; 146 | } 147 | } 148 | 149 | return true; 150 | }; 151 | 152 | id representation = dataSource.representation; 153 | if (!representation) { 154 | return BTOAuthURLTypeDefault; 155 | } 156 | 157 | if (![representation canProvideDocumentSource]) { 158 | return BTOAuthURLTypeDefault; 159 | } 160 | 161 | NSString *htmlContent = [representation documentSource]; 162 | 163 | if (!htmlContent) { 164 | return BTOAuthURLTypeDefault; 165 | } 166 | 167 | // FIXME: Load these needles from a server, to quickly adopt to HTML changes. 168 | 169 | // Check if it is a login page 170 | if (stringContainsStrings(@[@"Log in to use your DigitalOcean
account with"], htmlContent)) 171 | return BTOAuthURLTypeLoginPage; 172 | 173 | // Check if it is an authorization page 174 | if (stringContainsStrings(@[@"Authorize Application", @"would like permission to access your account"], htmlContent)) 175 | return BTOAuthURLTypeApprovalPage; 176 | 177 | return BTOAuthURLTypeDefault; 178 | } 179 | 180 | - (void) fillCredentials:(NSString*)username password:(NSString*)password intoPage:(WebView*)webView { 181 | // FIXME: Load this javascript from a server, to quickly adopt to HTML changes 182 | // Insert Username and Password 183 | NSString *abc = [NSString stringWithFormat:@"document.getElementById(\"user_email\").value = \"%@\";\ 184 | document.getElementById(\"user_password\").value = \"%@\";\ 185 | document.getElementById(\"new_user\").submit();", username, password]; 186 | 187 | WebScriptObject *script = [webView windowScriptObject]; 188 | [script evaluateWebScript:abc]; 189 | } 190 | 191 | @end 192 | -------------------------------------------------------------------------------- /OceanBar/BTOAuthLoginBase.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTOAuthLoginBase.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | /*! 13 | @abstract Each time an URL is loaded, one of those actions can be performed 14 | */ 15 | typedef enum : NSUInteger { 16 | // Just load the URL 17 | BTOAuthFancyLoadURL, 18 | // Load the URL and show the approval sheet 19 | BTOAuthFancyShowSheet, 20 | // Load the URL and hide the approval sheet 21 | BTOAuthFancyHideSheet, 22 | // Load the URL and afterwards insert credentials. 23 | // This implies that the URL to be loaded is of BTOAuthURLType BTOAuthURLTypeLoginPage 24 | BTOAuthFancyInsertCredentials, 25 | // Load the URL with the informationt hat the credentials were wrong 26 | BTOAuthFancyWrongCredentials, 27 | // Ignore the URL, there was an error 28 | BTOAuthFancyError, 29 | // Ignore this URL, and instead load the provided *customURL 30 | BTOAuthFancyCustomURL, 31 | // This is a custom URL to be loaded in Safari 32 | BTOAuthFancyLoadExternal, 33 | // This URL contains the token, authentication is finished, we're done 34 | BTOAuthFancyFinished, 35 | } BTOAuthURLAction; 36 | 37 | /*! 38 | @abstract These are the possible types an URL can be 39 | */ 40 | typedef enum : NSUInteger { 41 | // A normal URL 42 | BTOAuthURLTypeDefault, 43 | // The URL to the "Login into Service" page 44 | BTOAuthURLTypeLoginPage, 45 | // The URL to the "Approve App ? for Service" page 46 | BTOAuthURLTypeApprovalPage 47 | } BTOAuthURLType; 48 | 49 | 50 | @protocol BTOAuthUserCredentialsProtocol 51 | @required 52 | /*! 53 | @abstract Will be called when the user-provided credentials for login proved to be wrong 54 | */ 55 | - (void) loginManagerCredentialsWrong:(id)loginManager; 56 | /*! 57 | @abstract Will be called when login & approval was successful 58 | */ 59 | - (void) loginManagerLoginSuccess:(id)loginManager withNewAccount:(id)account; 60 | /*! 61 | @abstract Will be called when login or approval failed 62 | */ 63 | - (void) loginManagerLoginFailure:(id)loginManager; 64 | @end 65 | 66 | /*! 67 | @abstract the OAuth Login Base Class 68 | @discussion This class itself does nothing. It has to be subclassed, and all appropriate methods have to be implemented. 69 | Then, the resulting class can be used for a fancy oauth flow where the username / password does happen in a constrained 70 | & well defined environment 71 | */ 72 | @interface BTOAuthLoginBase : NSObject 73 | 74 | #pragma mark - 75 | #pragma mark Public Interface 76 | 77 | /*! 78 | @abstract Once username and password have been gathered, start the login process with them 79 | */ 80 | - (void) startLoginProcessWithUsername:(NSString*)username password:(NSString*)password; 81 | 82 | #pragma mark - 83 | #pragma mark To be implemented by Subclasses 84 | 85 | /// The name of the service 86 | @property (readonly) NSString *name; 87 | 88 | /// The URL to be used for authorization 89 | @property (readonly) NSURL *authorizationURL; 90 | 91 | /// The Client ID 92 | @property (readonly) NSString *clientID; 93 | 94 | /// The Client Secret 95 | @property (readonly) NSString *clientSecret; 96 | 97 | /// The Token retrieval URL 98 | @property (readonly) NSURL *tokenURL; 99 | 100 | /// The Redirect URL 101 | @property (readonly) NSURL *redirectURL; 102 | 103 | - (void) addLoginParams:(NSMutableDictionary*)params; 104 | 105 | /*! 106 | @abstract Return the appropriate action for the URL before it is being loaded 107 | @discussion The Base Class will ask for each URL that is being loaded in the OAuth process what kind of action should be performed. 108 | This allows great flexibility in terms of how the flow should be implemented for a particular oauth provider 109 | @param url The URL that is about to be loaded 110 | @param customURL If the return type is BTOAuthFancyCustomURL then this is a NSURL that should be loaded instead of url 111 | @param error If there was an error, this will be filled 112 | @returns The type of action to be implemented 113 | */ 114 | - (BTOAuthURLAction) actionForURL:(NSURL*)url orCustomURL:(NSURL**)customURL 115 | error:(NSError**)error; 116 | 117 | /*! 118 | @abstract When a page is done loading, this will be called to figure out how to act on the loaded page 119 | @discussion A loaded page can either be the login page, the approval page, or anything else. For Login and Approval special actions are needed. This call allows the Subclass to figure out what kind of page it is 120 | @param dataSource The WebKit datasource that just finished loading 121 | @returns the URL type 122 | */ 123 | - (BTOAuthURLType) pageTypeForDataSource:(WebDataSource*)dataSource; 124 | 125 | /*! 126 | @abstract When the page is a login page, the subclass has to modify the HTML via Javascript to fill the form with the correct credentials *and then submit it* 127 | @discussion This is only needed if credentials are being entered in a seperate Window. One can also ignore that and for the login page also open a sheet. In that case this method can just do nothing 128 | @param username Username 129 | @param password Password 130 | @param webView The webview where the login form has been loaded 131 | */ 132 | - (void) fillCredentials:(NSString*)username password:(NSString*)password intoPage:(WebView*)webView; 133 | 134 | @end 135 | 136 | -------------------------------------------------------------------------------- /OceanBar/BTOAuthLoginBase.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTOAuthLoginBase.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTOAuthLoginBase.h" 10 | #import "NSString+BTOauthLogin.h" 11 | #import 12 | 13 | @interface BTOAuthLoginBase() { 14 | // if the webkit should, after finishing the next load, try to insert 15 | // credentials into the form 16 | BOOL _nextStepInsertCredentials; 17 | BOOL _nextStepShowApproval; 18 | 19 | // Tracking resource loading 20 | NSInteger _resourceCount; 21 | NSInteger _resourceFailedCount; 22 | NSInteger _resourceCompletedCount; 23 | } 24 | // This allows us to force a redirect to a specific URL by whitelisting it for the next request 25 | @property (retain) NSURL *nextRequestWhiteListURL; 26 | 27 | @property (retain) IBOutlet NSWindow *sheetParentWindow; 28 | 29 | @property (retain) NSString *credentialUsername; 30 | @property (retain) NSString *credentialPassword; 31 | 32 | @property (retain) IBOutlet NSWindow *approvalSheet; 33 | 34 | @property (retain) IBOutlet WebView *approvalWebView; 35 | 36 | @property (retain) IBOutlet NSProgressIndicator *approvalProgressIndicator; 37 | 38 | @property (weak) IBOutlet id delegate; 39 | 40 | @end 41 | 42 | @implementation BTOAuthLoginBase 43 | 44 | - (id) init { 45 | self = [super init]; 46 | if (self) { 47 | [[NXOAuth2AccountStore sharedStore] setClientID:[self clientID] 48 | secret:[self clientSecret] 49 | scope:[self desiredScope] 50 | authorizationURL:[self authorizationURL] 51 | tokenURL:[self tokenURL] 52 | redirectURL:[self redirectURL] 53 | keyChainGroup:@"com.stylemac.OceanBar" 54 | forAccountType:[self name]]; 55 | 56 | // allow the login to modify the configuration 57 | NSMutableDictionary *params = @{}.mutableCopy; 58 | [self addLoginParams:params]; 59 | 60 | if (params.count > 0) { 61 | NSMutableDictionary *config = [[NXOAuth2AccountStore sharedStore] 62 | configurationForAccountType:[self name]].mutableCopy; 63 | [[NXOAuth2AccountStore sharedStore] setConfiguration:config.copy forAccountType:[self name]]; 64 | } 65 | 66 | // Make sure we identify succesful loading 67 | [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification 68 | object:[NXOAuth2AccountStore sharedStore] 69 | queue:nil 70 | usingBlock:^(NSNotification *aNotification){ 71 | if (self.delegate && [self.delegate conformsToProtocol:@protocol(BTOAuthUserCredentialsProtocol)]) { 72 | [self.delegate loginManagerLoginSuccess:self withNewAccount:aNotification.userInfo[NXOAuth2AccountStoreNewAccountUserInfoKey]]; 73 | } 74 | }]; 75 | 76 | [[NSNotificationCenter defaultCenter] addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification 77 | object:[NXOAuth2AccountStore sharedStore] 78 | queue:nil 79 | usingBlock:^(NSNotification *aNotification){ 80 | if (self.delegate && [self.delegate conformsToProtocol:@protocol(BTOAuthUserCredentialsProtocol)]) { 81 | [self.delegate loginManagerLoginFailure:self]; 82 | } 83 | }]; 84 | 85 | } 86 | return self; 87 | } 88 | 89 | - (void) dealloc { 90 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 91 | } 92 | 93 | - (void) startLoginProcessWithUsername:(NSString*)username password:(NSString*)password { 94 | [self.approvalWebView setFrameLoadDelegate:self]; 95 | [self.approvalWebView setPolicyDelegate:self]; 96 | [self.approvalWebView setResourceLoadDelegate:self]; 97 | 98 | self.credentialUsername = username; 99 | self.credentialPassword = password; 100 | 101 | #ifdef DEBUG 102 | [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:@"WebKitDeveloperExtras"]; 103 | [[NSUserDefaults standardUserDefaults] synchronize]; 104 | #endif 105 | 106 | [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:[self name] 107 | withPreparedAuthorizationURLHandler:^(NSURL *preparedURL){ 108 | // Open a web view or similar 109 | [self.approvalWebView.mainFrame loadRequest:[NSURLRequest requestWithURL:preparedURL]]; 110 | }]; 111 | } 112 | 113 | - (void) displayApprovalSheet { 114 | [NSApp beginSheet:self.approvalSheet modalForWindow:self.sheetParentWindow modalDelegate:nil didEndSelector:NULL contextInfo:nil]; 115 | } 116 | 117 | - (void) updateResourceStatus { 118 | [self.approvalProgressIndicator setDoubleValue:(double)((float)(_resourceCompletedCount + _resourceFailedCount) / (float)_resourceCount) * 100.0]; 119 | } 120 | 121 | - (void) showApprovalProgressIndicator { 122 | [self.approvalProgressIndicator setHidden:false]; 123 | [self.approvalProgressIndicator setDoubleValue:0]; 124 | } 125 | 126 | - (void) hideApprovalProgressIndicator { 127 | [self.approvalProgressIndicator setHidden:true]; 128 | } 129 | 130 | #pragma mark - 131 | #pragma mark Implemented By Subclass 132 | 133 | - (void) addLoginParams:(NSMutableDictionary*)params { 134 | [NSException raise:NSGenericException format:@"%@: Not Implemented yet", NSStringFromSelector(_cmd)]; 135 | } 136 | 137 | - (NSSet*) desiredScope { 138 | [NSException raise:NSGenericException format:@"%@: Not Implemented yet", NSStringFromSelector(_cmd)]; 139 | return nil; 140 | } 141 | 142 | - (BTOAuthURLAction) actionForURL:(NSURL*)url orCustomURL:(NSURL**)customURL 143 | error:(NSError**)error { 144 | [NSException raise:NSGenericException format:@"%@: Not Implemented yet", NSStringFromSelector(_cmd)]; 145 | return BTOAuthFancyLoadExternal; 146 | } 147 | 148 | - (BTOAuthURLType) pageTypeForDataSource:(WebDataSource*)dataSource { 149 | [NSException raise:NSGenericException format:@"%@: Not Implemented yet", NSStringFromSelector(_cmd)]; 150 | return BTOAuthURLTypeDefault; 151 | } 152 | 153 | - (void) fillCredentials:(NSString*)username password:(NSString*)password intoPage:(WebView*)webView { 154 | [NSException raise:NSGenericException format:@"%@: Not Implemented yet", NSStringFromSelector(_cmd)]; 155 | } 156 | 157 | #pragma mark - 158 | #pragma mark ResourceLoadDelegate 159 | 160 | - (id)webView:(WebView *)sender identifierForInitialRequest:(NSURLRequest *)request fromDataSource:(WebDataSource *)dataSource { 161 | // Return some object that can be used to identify this resource 162 | return [NSNumber numberWithInteger:_resourceCount++]; 163 | } 164 | 165 | -(NSURLRequest *)webView:(WebView *)sender 166 | resource:(id)identifier 167 | willSendRequest:(NSURLRequest *)request 168 | redirectResponse:(NSURLResponse *)redirectResponse 169 | fromDataSource:(WebDataSource *)dataSource { 170 | // Update the status message 171 | [self updateResourceStatus]; 172 | return request; 173 | } 174 | 175 | -(void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource { 176 | _resourceFailedCount++; 177 | // Update the status message 178 | [self updateResourceStatus]; 179 | } 180 | 181 | -(void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource { 182 | _resourceCompletedCount++; 183 | // Update the status message 184 | [self updateResourceStatus]; 185 | } 186 | 187 | #pragma mark - 188 | #pragma mark FrameLoadDelegate 189 | 190 | - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame { 191 | // always hide the progress indicator again 192 | [self hideApprovalProgressIndicator]; 193 | 194 | BTOAuthURLType urlType = [self pageTypeForDataSource:frame.dataSource]; 195 | 196 | // If this is the login page, insert the credentials 197 | if (_nextStepInsertCredentials && urlType == BTOAuthURLTypeLoginPage) { 198 | [self fillCredentials:self.credentialUsername password:self.credentialPassword intoPage:self.approvalWebView]; 199 | 200 | _nextStepInsertCredentials = false; 201 | } 202 | 203 | // If this is the approval page, show the approval screen 204 | if (_nextStepShowApproval && urlType == BTOAuthURLTypeApprovalPage) { 205 | [self displayApprovalSheet]; 206 | _nextStepShowApproval = false; 207 | } 208 | } 209 | 210 | - (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation 211 | request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { 212 | 213 | NSURL *url = [request URL]; 214 | 215 | if ([url isEqualTo:self.nextRequestWhiteListURL]) { 216 | [listener use]; 217 | return; 218 | } 219 | 220 | NSError *error = nil; 221 | NSURL *customURL = nil; 222 | BTOAuthURLAction anAction = [self actionForURL:url orCustomURL:&customURL error:&error]; 223 | 224 | switch (anAction) { 225 | case BTOAuthFancyCustomURL: 226 | if (customURL) { 227 | self.nextRequestWhiteListURL = customURL; 228 | [listener ignore]; 229 | [frame loadRequest:[NSURLRequest requestWithURL:customURL]]; 230 | } else { 231 | [listener use]; 232 | } 233 | break; 234 | case BTOAuthFancyError: 235 | if (error) { 236 | [listener ignore]; 237 | [[NSAlert alertWithError:error] runModal]; 238 | } else { 239 | [listener use]; 240 | } 241 | break; 242 | case BTOAuthFancyHideSheet: 243 | [self.approvalSheet close]; 244 | [self.sheetParentWindow endSheet:self.approvalSheet]; 245 | [listener use]; 246 | break; 247 | case BTOAuthFancyLoadURL: 248 | [listener use]; 249 | break; 250 | case BTOAuthFancyShowSheet: { 251 | // we begin the sheet with a delay, so the listener instruction has time to load the new page 252 | [self showApprovalProgressIndicator]; 253 | _nextStepShowApproval = true; 254 | [listener use]; 255 | break; 256 | } 257 | case BTOAuthFancyWrongCredentials: 258 | if (self.delegate) { 259 | [self.delegate loginManagerCredentialsWrong:self]; 260 | } 261 | [listener ignore]; 262 | break; 263 | case BTOAuthFancyInsertCredentials: 264 | [self showApprovalProgressIndicator]; 265 | _nextStepInsertCredentials = true; 266 | [listener use]; 267 | break; 268 | case BTOAuthFancyLoadExternal: 269 | [[NSWorkspace sharedWorkspace] openURL:url]; 270 | [listener ignore]; 271 | break; 272 | case BTOAuthFancyFinished: 273 | [self hideApprovalProgressIndicator]; 274 | [[NXOAuth2AccountStore sharedStore] handleRedirectURL:url]; 275 | break; 276 | } 277 | 278 | } 279 | 280 | @end 281 | -------------------------------------------------------------------------------- /OceanBar/BTOceanData.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTOceanData.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void (^BTOceanDataAction)(id results); 12 | typedef void (^BTOceanDataError)(NSError *error); 13 | 14 | //----------------------------------------------------------------------------- 15 | #pragma mark Types 16 | //----------------------------------------------------------------------------- 17 | // Lightweight interfaces-types around the few properties 18 | // that the items contain 19 | 20 | @interface BTOceanDataItem : NSObject 21 | - (id) initWithDictionary:(NSDictionary *)otherDictionary; 22 | - (id) objectForKeyedSubscript:(id )key; 23 | - (NSNumber*) identifier; 24 | - (NSString*) name; 25 | - (NSString*) slug; 26 | @end 27 | 28 | @interface BTOceanDataImage : BTOceanDataItem 29 | - (NSString*) distribution; 30 | @end 31 | 32 | @interface BTOceanDataSize : BTOceanDataItem 33 | - (NSNumber*) memory; 34 | - (NSNumber*) disk; 35 | - (NSNumber*) cpu; 36 | - (NSNumber*) costPerHour; 37 | - (NSNumber*) costPerMonth; 38 | @end 39 | 40 | @interface BTOceanDataRegion : BTOceanDataItem 41 | @end 42 | 43 | @interface BTOceanDataDroplet : BTOceanDataItem 44 | @property (retain) BTOceanDataSize *size; 45 | @property (retain) BTOceanDataRegion *region; 46 | @property (retain) BTOceanDataImage *image; 47 | - (bool) isActive; 48 | - (bool) backupsActive; 49 | - (NSString*) publicIpAddresses; 50 | - (NSString*) privateIpAddresses; 51 | - (NSString*) ipAddress; 52 | - (NSString*) privateIpAddress; 53 | - (NSString*) kernelName; 54 | - (NSString*) features; 55 | - (bool) locked; 56 | - (NSString*) status; 57 | - (NSDate*) createdAt; 58 | @end 59 | 60 | @interface BTOceanData : NSObject 61 | @property (weak) id delegate; 62 | - (BTOceanDataDroplet*) dropletForID:(NSNumber*)dropletId; 63 | - (void) loadDropletsWithSuccess:(BTOceanDataAction)actionBlock failure:(BTOceanDataError)errorBlock; 64 | - (void) shutdownDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 65 | - (void) rebootDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 66 | - (void) powercycleDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 67 | - (void) powerOffDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 68 | - (void) powerOnDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 69 | - (void) destroyDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock; 70 | @end 71 | 72 | @protocol BTOceanDataDelegate 73 | - (void) oceanData:(BTOceanData*)data didFindChangedStateForDroplets:(NSArray*)droplets; 74 | @end 75 | 76 | -------------------------------------------------------------------------------- /OceanBar/BTOceanData.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTOceanData.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTOceanData.h" 10 | #import 11 | #import 12 | 13 | @interface BTOceanDataItem() { 14 | NSDictionary *_storage; 15 | } 16 | 17 | @end 18 | 19 | // small macro to convert NSNull or Nil to a default value 20 | #define defau(c, d) (c == [NSNull null] || c == nil ? d : c) 21 | // convert NSNull to nil 22 | #define nu2ni(c) (c == [NSNull null] ? nil : c) 23 | 24 | @implementation BTOceanDataItem 25 | 26 | - (id) initWithDictionary:(NSDictionary *)otherDictionary { 27 | self = [super init]; 28 | if (self) { 29 | _storage = otherDictionary; 30 | } 31 | return self; 32 | } 33 | 34 | - (NSDictionary*) data { 35 | return _storage; 36 | } 37 | 38 | - (id)objectForKeyedSubscript:(id )key { 39 | return nu2ni([_storage objectForKey:key]); 40 | } 41 | 42 | - (NSNumber*) identifier { 43 | return self[@"id"]; 44 | } 45 | 46 | - (NSString*) name { 47 | return defau(self[@"name"], @""); 48 | } 49 | 50 | - (NSString*) slug { 51 | return defau(self[@"slug"], @""); 52 | } 53 | 54 | @end 55 | 56 | @implementation BTOceanDataImage 57 | 58 | - (NSString*) distribution { 59 | return self[@"distribution"]; 60 | } 61 | 62 | @end 63 | 64 | @implementation BTOceanDataRegion 65 | - (NSNumber*) identifier { 66 | // Sizes don't return an id anymore, only a slug. 67 | // thankfully, NSString hash is used for string content identity within an OSX release, 68 | // which is sufficient for our temporary use here. 69 | return @(self.slug.hash); 70 | } 71 | @end 72 | 73 | @implementation BTOceanDataSize 74 | - (NSNumber*) identifier { 75 | // Sizes don't return an id anymore, only a slug. 76 | // thankfully, NSString hash is used for string content identity within an OSX release, 77 | // which is sufficient for our temporary use here. 78 | return @(self.slug.hash); 79 | } 80 | 81 | - (NSNumber*) memory { 82 | return self[@"memory"]; 83 | } 84 | - (NSNumber*) disk { 85 | return self[@"disk"]; 86 | } 87 | 88 | - (NSNumber*) cpu { 89 | return self[@"vcpus"]; 90 | } 91 | 92 | - (NSNumber*) costPerHour { 93 | return self[@"prize_hourly"]; 94 | } 95 | 96 | - (NSNumber*) costPerMonth { 97 | return self[@"price_monthly"]; 98 | } 99 | 100 | @end 101 | 102 | @implementation BTOceanDataDroplet 103 | 104 | - (bool) isActive { 105 | return [self[@"status"] isEqualToString:@"active"] || [self[@"status"] isEqualToString:@"new"]; 106 | } 107 | 108 | - (bool) backupsActive { 109 | return [self[@"backup_ids"] count] > 0; 110 | } 111 | 112 | - (NSString*) kernelName { 113 | return [self.data valueForKeyPath:@"kernel.name"]; 114 | } 115 | 116 | - (NSString*) features { 117 | return [(NSArray*)self[@"features"] componentsJoinedByString:@", "]; 118 | } 119 | 120 | - (NSArray*) queryAddressesFor:(NSString*)type { 121 | NSMutableArray *addresses = @[].mutableCopy; 122 | for (NSDictionary *item in [self.data valueForKeyPath:@"networks.v4"]) { 123 | if ([item[@"type"] isEqualToString:type]) 124 | [addresses addObject:item[@"ip_address"]]; 125 | } 126 | return addresses.copy; 127 | } 128 | 129 | - (NSString*) ipAddress { 130 | // return the first public IP 131 | return [self queryAddressesFor:@"public"].firstObject; 132 | } 133 | 134 | - (NSString*) privateIpAddress { 135 | // return the first public IP 136 | return [self queryAddressesFor:@"private"].firstObject; 137 | } 138 | 139 | - (NSString*) publicIpAddresses { 140 | return [[self queryAddressesFor:@"public"] componentsJoinedByString:@", "]; 141 | } 142 | 143 | - (NSString*) privateIpAddresses { 144 | return [[self queryAddressesFor:@"private"] componentsJoinedByString:@", "]; 145 | } 146 | 147 | - (bool) locked { 148 | return [self[@"locked"] boolValue]; 149 | } 150 | 151 | - (NSString*) status { 152 | return self[@"status"]; 153 | } 154 | 155 | - (NSDate*) createdAt { 156 | // 2013-01-01T09:30:00Z 157 | NSString *dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; 158 | NSDateFormatter *dateFormatter = 159 | [[NSDateFormatter alloc] init]; 160 | [dateFormatter setDateFormat:dateFormat]; 161 | return [dateFormatter dateFromString:self[@"created_at"]]; 162 | } 163 | 164 | @end 165 | 166 | @interface BTOceanData() 167 | { 168 | NSDictionary *_images; 169 | NSDictionary *_regions; 170 | NSDictionary *_sizes; 171 | NSDictionary *_droplets; 172 | } 173 | @end 174 | 175 | @implementation BTOceanData 176 | 177 | - (NSString*) dropletURLString { 178 | return $p(@"%@/droplets", DIGITALOCEAN_BASE_URL); 179 | } 180 | 181 | - (NSString*) regionURLString { 182 | return $p(@"%@/regions", DIGITALOCEAN_BASE_URL); 183 | } 184 | 185 | - (NSString*) imageURLString { 186 | return $p(@"%@/images", DIGITALOCEAN_BASE_URL); 187 | } 188 | 189 | - (NSString*) sizeURLString { 190 | return $p(@"%@/sizes", DIGITALOCEAN_BASE_URL); 191 | } 192 | 193 | - (BTOceanDataDroplet*) dropletForID:(NSNumber*)dropletId { 194 | return _droplets[dropletId]; 195 | } 196 | 197 | - (void) loadDropletsWithSuccess:(BTOceanDataAction)actionBlock failure:(BTOceanDataError)errorBlock { 198 | // Each time we reload, we also reload the available 199 | // Regions, Images, and Sizes 200 | 201 | AFHTTPRequestOperation *op1 = 202 | [self requestOperationFor:[self sizeURLString] 203 | createClass:@"BTOceanDataSize" 204 | propertyKey:@"sizes" 205 | successBlock:^(NSDictionary* results) { 206 | _sizes = results; 207 | } failBlock:^(NSError *error) { 208 | NSLog(@"Error: %@", error); 209 | errorBlock(error); 210 | }]; 211 | 212 | AFHTTPRequestOperation *op2 = 213 | [self requestOperationFor:[self regionURLString] 214 | createClass:@"BTOceanDataRegion" 215 | propertyKey:@"regions" 216 | successBlock:^(NSDictionary* results) { 217 | _regions = results; 218 | } failBlock:^(NSError *error) { 219 | NSLog(@"Error: %@", error); 220 | errorBlock(error); 221 | }]; 222 | 223 | AFHTTPRequestOperation *op3 = 224 | [self requestOperationFor:[self imageURLString] 225 | createClass:@"BTOceanDataImage" 226 | propertyKey:@"images" 227 | successBlock:^(NSDictionary* resultDict) { 228 | _images = resultDict; 229 | } failBlock:^(NSError *error) { 230 | NSLog(@"Error: %@", error); 231 | errorBlock(error); 232 | }]; 233 | 234 | AFHTTPRequestOperation *finalOp = 235 | [self requestOperationFor:[self dropletURLString] 236 | createClass:@"BTOceanDataDroplet" 237 | propertyKey:@"droplets" 238 | successBlock:^(NSDictionary* results) { 239 | 240 | // calculate if we have a changed state to previously 241 | // but only after the inital import. 242 | if (_droplets && 243 | ![[self comparisonDictionary:results] 244 | isEqualToDictionary:[self comparisonDictionary:_droplets]]) { 245 | // find the changes 246 | // TODO: Test for more changes than just activity? 247 | NSMutableArray *changedDroplets = @[].mutableCopy; 248 | for (BTOceanDataDroplet *droplet in results.allValues) { 249 | if (!_droplets[droplet.identifier]) { 250 | [changedDroplets addObject:@{@"change": @"new", @"droplet": droplet}]; 251 | [self postNotification:NSLocalizedString(@"New Droplet", @"If a new droplet appeared") 252 | subtitle:$p(NSLocalizedString(@"Found new droplet: %@", @"if a new droplet appeared, subtitle"), droplet.name)]; 253 | } else if (![_droplets[droplet.identifier] isActive] == droplet.isActive) { 254 | [changedDroplets addObject:@{@"change": @"active", @"droplet": droplet}]; 255 | if (droplet.isActive) { 256 | [self postNotification:NSLocalizedString(@"Droplet Activated", @"If a droplet activated") 257 | subtitle:$p(NSLocalizedString(@"Droplet %@ was actived", @"if a droplet acitvated, subtitle"), droplet.name)]; 258 | } else { 259 | [self postNotification:NSLocalizedString(@"Droplet Deactivated", @"If a droplet deactivated") 260 | subtitle:$p(NSLocalizedString(@"Droplet %@ was deactived", @"if a droplet deacitvated, subtitle"), droplet.name)]; 261 | } 262 | } 263 | } 264 | if (self.delegate && [self.delegate conformsToProtocol:@protocol(BTOceanDataDelegate)]) { 265 | [self.delegate oceanData:self didFindChangedStateForDroplets:changedDroplets.copy]; 266 | } 267 | } 268 | 269 | _droplets = results; 270 | 271 | // add regions and images 272 | for (BTOceanDataDroplet *droplet in [results allValues]) { 273 | droplet.region = _regions[droplet[@"region_id"]]; 274 | droplet.size = _sizes[droplet[@"size_id"]]; 275 | droplet.image = _images[droplet[@"image_id"]]; 276 | } 277 | actionBlock([results allValues]); 278 | } failBlock:^(NSError *error) { 279 | errorBlock(error); 280 | }]; 281 | 282 | [finalOp addDependency:op1]; 283 | [finalOp addDependency:op2]; 284 | [finalOp addDependency:op3]; 285 | 286 | NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init]; 287 | operationQueue.maxConcurrentOperationCount = 1; 288 | [operationQueue addOperations:@[op1, op2, op3, finalOp] 289 | waitUntilFinished:NO]; 290 | } 291 | 292 | - (void) rebootDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 293 | [self dropletAction:droplet verb:@"Reboot" request:@"reboot" finishAction:finishBlock]; 294 | } 295 | 296 | - (void) shutdownDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 297 | [self dropletAction:droplet verb:@"Shutdown" request:@"shutdown" finishAction:finishBlock]; 298 | } 299 | 300 | - (void) powercycleDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 301 | [self dropletAction:droplet verb:@"Power Cycle" request:@"power_cycle" finishAction:finishBlock]; 302 | } 303 | 304 | - (void) powerOffDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 305 | [self dropletAction:droplet verb:@"Power Off" request:@"power_off" finishAction:finishBlock]; 306 | } 307 | 308 | - (void) powerOnDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 309 | [self dropletAction:droplet verb:@"Power On" request:@"power_on" finishAction:finishBlock]; 310 | } 311 | 312 | - (void) destroyDroplet:(BTOceanDataDroplet*)droplet finishAction:(BTOceanDataAction)finishBlock { 313 | [self dropletAction:droplet verb:@"Destroy" request:@"destroy" finishAction:finishBlock]; 314 | } 315 | 316 | - (void) dropletAction:(BTOceanDataDroplet*)droplet verb:(NSString*)verb request:(NSString*)requestOp finishAction:(BTOceanDataAction)finishBlock { 317 | NSString *actionPath = $p(@"%@/droplets/%@/actions", DIGITALOCEAN_BASE_URL, droplet.identifier); 318 | 319 | NSArray *accounts = [[NXOAuth2AccountStore sharedStore] accounts]; 320 | if (accounts.count == 0)return; 321 | 322 | NXOAuth2Account *account = accounts.firstObject; 323 | 324 | 325 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:actionPath] 326 | cachePolicy:NSURLRequestReloadIgnoringCacheData 327 | timeoutInterval:10]; 328 | 329 | [request setHTTPMethod:@"POST"]; 330 | [request setValue:[NSString stringWithFormat:@"Bearer %@", account.accessToken.accessToken] forHTTPHeaderField:@"Authorization"]; 331 | [request setValue: @"application/json" forHTTPHeaderField:@"Content-Type"]; 332 | [request setHTTPBody: [[NSString stringWithFormat:@"{\"type\": \"%@\"}", requestOp] dataUsingEncoding:NSUTF8StringEncoding]]; 333 | 334 | AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 335 | op.responseSerializer = [AFJSONResponseSerializer serializer]; 336 | [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 337 | 338 | [self postNotification:NSLocalizedString(@"Success", @"notification for droplet action") 339 | subtitle:$p(NSLocalizedString(@"Droplet '%@' action successful: %@", @"Notification for droplet action"), 340 | droplet.name, verb)]; 341 | 342 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 343 | 344 | [self postNotification:NSLocalizedString(@"Failure", @"notification title for failed droplet action") 345 | subtitle:$p(NSLocalizedString(@"Droplet %@ failed at: %@", @"notification for failed droplet action"), 346 | droplet.name, verb)]; 347 | 348 | }]; 349 | [op start]; 350 | 351 | } 352 | 353 | - (AFHTTPRequestOperation*) requestOperationFor:(NSString*)urlString 354 | createClass:(NSString*)createClass 355 | propertyKey:(NSString*)propertyKey 356 | successBlock:(BTOceanDataAction)success 357 | failBlock:(BTOceanDataError)failBlock { 358 | 359 | NSString *customToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCustomTokenKey]; 360 | if (!customToken) { 361 | NSArray *accounts = [[NXOAuth2AccountStore sharedStore] accounts]; 362 | NXOAuth2Account *account = accounts.firstObject; 363 | customToken = account.accessToken.accessToken; 364 | } 365 | 366 | NSURL *URL = [NSURL URLWithString:urlString]; 367 | NSMutableURLRequest *request = [NSURLRequest requestWithURL:URL].mutableCopy; 368 | 369 | [request setValue:[NSString stringWithFormat:@"Bearer %@", customToken] forHTTPHeaderField:@"Authorization"]; 370 | 371 | AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request]; 372 | op.responseSerializer = [AFJSONResponseSerializer serializer]; 373 | [op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 374 | 375 | // Never know what we get back in JSON 376 | if (![responseObject isKindOfClass:[NSDictionary class]]) { 377 | failBlock(errorFor(1, $p(@"Error: Data is '%@ class'", [responseObject className]), @"OceanData")); 378 | return; 379 | } 380 | 381 | NSMutableDictionary *collector = [NSMutableDictionary dictionary]; 382 | for (NSDictionary *aDictionary in responseObject[propertyKey]) { 383 | BTOceanDataItem *item = [[NSClassFromString(createClass) alloc] initWithDictionary:aDictionary]; 384 | [collector setObject:item forKey:item.identifier]; 385 | } 386 | 387 | success(collector.copy); 388 | } failure:^(AFHTTPRequestOperation *operation, NSError *error) { 389 | failBlock(error); 390 | }]; 391 | 392 | return op; 393 | } 394 | 395 | - (void) postNotification:(NSString*)title subtitle:(NSString*)subtitle { 396 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 397 | if ([defaults boolForKey:@"postNotificationCenter"]) { 398 | NSUserNotification *notification = [[NSUserNotification alloc] init]; 399 | notification.title = title; 400 | notification.subtitle = subtitle; 401 | [[NSUserNotificationCenter defaultUserNotificationCenter] 402 | scheduleNotification:notification]; 403 | } 404 | } 405 | 406 | - (NSDictionary*) comparisonDictionary:(NSDictionary*)droplets { 407 | NSMutableDictionary *dx = @{}.mutableCopy; 408 | for (NSString* dropletID in [droplets allKeys]) { 409 | BTOceanDataDroplet *droplet = [droplets objectForKey:dropletID]; 410 | dx[droplet.identifier] = @(droplet.isActive); 411 | } 412 | return dx.copy; 413 | } 414 | 415 | @end 416 | -------------------------------------------------------------------------------- /OceanBar/BTSocialTextView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface BTSocialTextView : NSTextView 4 | 5 | - (void) addItem:(NSString*)item withValue:(NSString*)value; 6 | - (void) render; 7 | 8 | @end 9 | 10 | -------------------------------------------------------------------------------- /OceanBar/BTSocialTextView.m: -------------------------------------------------------------------------------- 1 | #import "BTSocialTextView.h" 2 | 3 | NSString * const kItemFontName = @"Lucida Grande"; 4 | NSString * const kValueFontName = @"Lucida Grande Bold"; 5 | static const NSUInteger kItemFontSize = 16; 6 | static const NSUInteger kLeftSideIndent = 5; 7 | static const NSUInteger kLineSpacing = 3; 8 | static const NSUInteger kTabStopWidth = 80; 9 | 10 | 11 | @interface BTSocialTextView() 12 | { 13 | NSMutableArray *_keyStorage; 14 | NSMutableArray *_valueStorage; 15 | NSTrackingArea *_trackingArea; 16 | } 17 | 18 | @end 19 | 20 | //-------------------------------------------------------------------------------------- 21 | #pragma mark - 22 | #pragma mark Main Class 23 | //-------------------------------------------------------------------------------------- 24 | 25 | @implementation BTSocialTextView 26 | 27 | - (id) initWithFrame:(NSRect)frameRect { 28 | self = [super initWithFrame:frameRect]; 29 | if (self) { 30 | _keyStorage = @[].mutableCopy; 31 | _valueStorage = @[].mutableCopy; 32 | 33 | [self setDrawsBackground:NO]; 34 | 35 | _trackingArea = [[NSTrackingArea alloc]initWithRect:[self bounds] options: (NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil]; 36 | [self addTrackingArea:_trackingArea]; 37 | 38 | //[super setTextContainerInset:NSMakeSize(1.0f, 10.0f)]; 39 | } 40 | return self; 41 | } 42 | 43 | - (NSPoint)textContainerOrigin { 44 | NSPoint origin = [super textContainerOrigin]; 45 | NSPoint newOrigin = NSMakePoint(origin.x, origin.y + 10); 46 | return newOrigin; 47 | } 48 | 49 | - (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)flag { 50 | // we don't want selection 51 | } 52 | 53 | -(void)drawInsertionPointInRect:(NSRect)aRect color:(NSColor *)aColor turnedOn:(BOOL)flag { 54 | // we don't want an insertion cursor 55 | } 56 | 57 | - (void)mouseMoved:(NSEvent *)event { 58 | // we want the default arrow cursor 59 | [[NSCursor arrowCursor] set]; 60 | } 61 | 62 | - (void)drawRect:(NSRect)rect { 63 | [[NSColor gridColor] setFill]; 64 | NSRectFill(rect); 65 | 66 | // draw the gray area for the keys 67 | NSRect drawRect = NSMakeRect(0, 0, kTabStopWidth - 2, self.bounds.size.height); 68 | [[NSColor controlHighlightColor] setFill]; 69 | NSRectFill(drawRect); 70 | 71 | // draw two 1px lines for top and bottom 72 | [[NSColor grayColor] setFill]; 73 | NSRectFill(NSMakeRect(0, 0, self.bounds.size.width, 1)); 74 | NSRectFill(NSMakeRect(0, self.bounds.size.height - 1, self.bounds.size.width, 1)); 75 | 76 | [super drawRect:rect]; 77 | } 78 | 79 | - (void)updateTrackingAreas { 80 | [super updateTrackingAreas]; 81 | [self removeTrackingArea:_trackingArea]; 82 | _trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options: (NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil]; 83 | [self addTrackingArea:_trackingArea]; 84 | } 85 | 86 | 87 | - (void)viewWillMoveToWindow:(NSWindow *)newWindow { 88 | [super viewWillMoveToWindow:newWindow]; 89 | 90 | // update our height to the minimum necessary 91 | NSRect usedRect = [self.layoutManager usedRectForTextContainer:self.textContainer]; 92 | [self setFrame:usedRect]; 93 | } 94 | 95 | //-------------------------------------------------------------------------------------- 96 | #pragma mark - 97 | #pragma mark Public Interface 98 | //-------------------------------------------------------------------------------------- 99 | 100 | - (void) addItem:(NSString*)item withValue:(NSString*)value { 101 | if (!item || !value) { 102 | NSLog(@"Null value: %@ / %@", item, value); 103 | return; 104 | } 105 | if ([item isKindOfClass:[NSNull class]] || [value isKindOfClass:[NSNull class]]) { 106 | NSLog(@"Null value: %@ / %@", item, value); 107 | return; 108 | } 109 | 110 | [_keyStorage addObject:item]; 111 | [_valueStorage addObject:value]; 112 | } 113 | 114 | - (void) render { 115 | [self setEditable:YES]; 116 | 117 | [self setString:@""]; 118 | 119 | for (NSString *key in _keyStorage.reverseObjectEnumerator.allObjects) { 120 | [self renderItem:key 121 | withValue:_valueStorage[[_keyStorage indexOfObject:key]]]; 122 | } 123 | 124 | [[[self enclosingScrollView] contentView] scrollToPoint:NSMakePoint(0, 0)]; 125 | 126 | [self setEditable:NO]; 127 | } 128 | 129 | //----------------------------------------------------------------------------- 130 | #pragma mark Private 131 | //----------------------------------------------------------------------------- 132 | 133 | - (void) renderItem:(NSString*)item withValue:(NSString*)value { 134 | 135 | NSDictionary *valueTextAttributes = 136 | @{NSFontAttributeName: [NSFont fontWithName: kValueFontName size: kItemFontSize], 137 | NSForegroundColorAttributeName: [NSColor blackColor]}; 138 | 139 | // by replacing breaks with breaktabs, we can keep the indent 140 | NSAttributedString *valueString = 141 | [[NSAttributedString alloc] 142 | initWithString:[value stringByReplacingOccurrencesOfString:@"\n" 143 | withString:@"\n\t"] 144 | attributes:valueTextAttributes]; 145 | 146 | NSFont *itemFont = [NSFont fontWithName:kItemFontName size:kItemFontSize]; 147 | 148 | NSString *tabbedItemFont = [item stringByAppendingString:@"\t"]; 149 | 150 | NSMutableAttributedString *mutableString = [valueString mutableCopy]; 151 | [mutableString insertAttributedString: 152 | [[NSAttributedString alloc] initWithString: 153 | [NSString stringWithFormat:@"%@", tabbedItemFont]] 154 | atIndex:0]; 155 | [mutableString appendAttributedString: 156 | [[NSAttributedString alloc] initWithString:@"\n\n"]]; 157 | 158 | NSMutableParagraphStyle *paragStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; 159 | //[paragStyle setAlignment:NSLeftTextAlignment]; // default, but just in case 160 | [paragStyle setLineSpacing:kLineSpacing]; 161 | 162 | // Define a tab stop to the right of the bullet glyph. 163 | NSTextTab *textTabFllwgBullet = [[NSTextTab alloc] initWithType:NSLeftTabStopType 164 | location:kTabStopWidth]; 165 | [paragStyle setTabStops:[NSArray arrayWithObject:textTabFllwgBullet]]; 166 | 167 | // Indent the first line up to where the bullet should appear. 168 | [paragStyle setFirstLineHeadIndent:kLeftSideIndent]; 169 | 170 | // Set the indentation for the wrapped lines to the same place as the tab stop. 171 | [paragStyle setHeadIndent:kTabStopWidth]; 172 | 173 | // set the attributes 174 | [mutableString setAttributes:@{NSParagraphStyleAttributeName: paragStyle, 175 | NSFontAttributeName: itemFont} 176 | range:NSMakeRange(0, mutableString.length)]; 177 | 178 | // Set the text color for the headline 179 | [mutableString setAttributes:@{NSFontAttributeName: itemFont, 180 | NSForegroundColorAttributeName: [NSColor darkGrayColor], 181 | NSParagraphStyleAttributeName: paragStyle} 182 | range:NSMakeRange(0, [tabbedItemFont length])]; 183 | 184 | [self insertText:mutableString]; 185 | } 186 | 187 | @end 188 | -------------------------------------------------------------------------------- /OceanBar/BTStatusbarController.h: -------------------------------------------------------------------------------- 1 | // 2 | // BTStatusbarController.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface BTStatusbarController : NSObject 12 | @property (weak) id delegate; 13 | - (void) reloadContents; 14 | @end 15 | 16 | @protocol BTStatusbarControllerDelegate 17 | - (void) openPreferences; 18 | - (void) exit; 19 | @end 20 | -------------------------------------------------------------------------------- /OceanBar/BTStatusbarController.m: -------------------------------------------------------------------------------- 1 | // 2 | // BTStatusbarController.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "BTStatusbarController.h" 10 | #import "BTOceanData.h" 11 | #import "BTSocialTextView.h" 12 | #import 13 | 14 | typedef void (^ConfirmationAction)(); 15 | 16 | const void* kApiCredentialContext = &kApiCredentialContext; 17 | 18 | ///----------------------------------------------------------------------------- 19 | #pragma mark Icon Setup 20 | //----------------------------------------------------------------------------- 21 | 22 | NSString * const kIconDefault = @"default"; 23 | NSString * const kIconNewContent = @"newContent"; 24 | NSString * const kIconLoading = @"loading"; 25 | NSString * const kIconError = @"notConnected"; 26 | 27 | NSString * const kIconActive = @"stateActive"; 28 | NSString * const kIconInactive = @"stateInactive"; 29 | NSString * const kIconUpdated = @"stateUpdated"; 30 | 31 | NSString * const kIconLocked = @"closedLock"; 32 | NSString * const kIconUnlocked = @"openLock"; 33 | 34 | // how many seconds do we wait before we reload after droplet actions like shutdown etc? 35 | const NSUInteger kReloadDelay = 10; 36 | 37 | @interface BTStatusbarController() { 38 | bool _loading; 39 | NSString *_loadError; 40 | NSArray *_updated; 41 | } 42 | @property (retain) NSStatusItem* mainStatusbarItem; 43 | @property (retain) NSTimer* reloadTimer; 44 | @property (retain) BTOceanData *oceanData; 45 | @end 46 | 47 | @implementation BTStatusbarController 48 | 49 | //----------------------------------------------------------------------------- 50 | #pragma mark Setup & Creation 51 | //----------------------------------------------------------------------------- 52 | 53 | - (id) init { 54 | self = [super init]; 55 | if (self) { 56 | self.oceanData = [[BTOceanData alloc] init]; 57 | self.oceanData.delegate = self; 58 | 59 | [self setupStatusbarItemWithDroplets:@[]]; 60 | 61 | [self reloadContents]; 62 | 63 | // any changes to the id / key have to trigger a reload 64 | NSUserDefaultsController *userDefaultsController = [NSUserDefaultsController sharedUserDefaultsController]; 65 | [userDefaultsController addObserver:self 66 | forKeyPath:@"values.doAPIKey" 67 | options:0 68 | context:&kApiCredentialContext]; 69 | [userDefaultsController addObserver:self 70 | forKeyPath:@"values.doAPISecret" 71 | options:0 72 | context:&kApiCredentialContext]; 73 | } 74 | return self; 75 | } 76 | 77 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 78 | { 79 | if (context == &kApiCredentialContext) { 80 | [self forceReload]; 81 | } else { 82 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 83 | } 84 | } 85 | 86 | - (void) forceReload { 87 | [self.reloadTimer invalidate]; 88 | self.reloadTimer = nil; 89 | [self reloadContents]; 90 | } 91 | 92 | - (void) setIconState { 93 | NSString *state = kIconDefault; 94 | 95 | if (_loading) 96 | state = kIconLoading; 97 | else if (_loadError) 98 | state = kIconError; 99 | else if (_updated) 100 | state = kIconNewContent; 101 | 102 | [self.mainStatusbarItem setImage:[NSImage imageNamed:state]]; 103 | } 104 | 105 | - (void) setupStatusbarItemWithDroplets:(NSArray*)droplets { 106 | if (!self.mainStatusbarItem) { 107 | self.mainStatusbarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; 108 | [self.mainStatusbarItem setEnabled: YES]; 109 | 110 | //let it highlight when the user activates it 111 | [self.mainStatusbarItem setHighlightMode:YES]; 112 | } 113 | 114 | [self.mainStatusbarItem setMenu: [self mainMenuForDroplets:droplets]]; 115 | 116 | [self setIconState]; 117 | 118 | [self.mainStatusbarItem.menu setDelegate:self]; 119 | } 120 | 121 | - (void) reloadContents { 122 | // ignore if the credentials aren't set yet 123 | NSArray *accounts = [[NXOAuth2AccountStore sharedStore] accounts]; 124 | if (accounts.count == 0) { 125 | _loadError = NSLocalizedString(@"Not logged in", @"Menu entry if we can't load"); 126 | [self setupStatusbarItemWithDroplets:@[]]; 127 | return; 128 | } 129 | 130 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 131 | 132 | // sometimes, loading takes time, simple indicator 133 | _loading = YES; 134 | 135 | [self.oceanData loadDropletsWithSuccess:^(NSArray *results) { 136 | _loading = NO; 137 | _loadError = nil; 138 | [self setupStatusbarItemWithDroplets:results]; 139 | } failure:^(NSError *error) { 140 | _loading = NO; 141 | 142 | _loadError = error.localizedDescription; 143 | 144 | [self setupStatusbarItemWithDroplets:@[]]; 145 | }]; 146 | 147 | // and trigger the next reload 148 | NSNumber *reloadInterval = [defaults objectForKey:@"reloadInterval"]; 149 | if (reloadInterval.intValue < 1)reloadInterval = @1; 150 | self.reloadTimer = [NSTimer scheduledTimerWithTimeInterval:reloadInterval.intValue * 60.0 151 | target:self 152 | selector:@selector(reloadContents) 153 | userInfo:nil 154 | repeats:NO]; 155 | } 156 | 157 | - (NSMenu*) mainMenuForDroplets:(NSArray*)droplets { 158 | NSMenu *statusMenu = [[NSMenu alloc] init]; 159 | 160 | if (droplets.count > 0) { 161 | for (BTOceanDataDroplet* droplet in droplets) { 162 | NSMenuItem *mainItem = [[NSMenuItem alloc] init]; 163 | [mainItem setTitle:droplet.name]; 164 | 165 | if (droplet.isActive) 166 | mainItem.image = [NSImage imageNamed:kIconActive]; 167 | else 168 | mainItem.image = [NSImage imageNamed:kIconInactive]; 169 | 170 | if ([_updated containsObject:droplet]) { 171 | mainItem.image = [NSImage imageNamed:kIconUpdated]; 172 | } 173 | 174 | NSMenu *subMenu = [self menuForDroplet:droplet]; 175 | [mainItem setSubmenu:subMenu]; 176 | 177 | [statusMenu addItem: mainItem]; 178 | } 179 | } else { 180 | if (_loading) { 181 | NSMenuItem *mainItem = [[NSMenuItem alloc] init]; 182 | [mainItem setTitle:NSLocalizedString(@"Loading...", @"If we're loading data. displayed as a menu entry")]; 183 | [statusMenu addItem:mainItem]; 184 | } 185 | if (_loadError) { 186 | NSMenuItem *mainItem = [[NSMenuItem alloc] init]; 187 | [mainItem setTitle:$p(@"Error: %@", _loadError)]; 188 | [statusMenu addItem:mainItem]; 189 | } 190 | // otherwise, the user probably didn't set it up, or we don't have any droplets... 191 | } 192 | 193 | [statusMenu addItem:[NSMenuItem separatorItem]]; 194 | 195 | NSMenuItem *prefItem = [[NSMenuItem alloc] 196 | initWithTitle:NSLocalizedString(@"Preferences", @"main menu") 197 | action:@selector(openPreferences:) 198 | keyEquivalent:@","]; 199 | prefItem.target = self; 200 | 201 | [statusMenu addItem:prefItem]; 202 | 203 | [statusMenu addItem:[NSMenuItem separatorItem]]; 204 | 205 | NSMenuItem *refreshItem = [[NSMenuItem alloc] 206 | initWithTitle:NSLocalizedString(@"Refresh", @"main menu") 207 | action:@selector(forceReload) 208 | keyEquivalent:@"r"]; 209 | refreshItem.target = self; 210 | [statusMenu addItem:refreshItem]; 211 | 212 | [statusMenu addItem:[NSMenuItem separatorItem]]; 213 | 214 | NSMenuItem *exitItem = [[NSMenuItem alloc] 215 | initWithTitle:NSLocalizedString(@"Quit", @"main menu") 216 | action:@selector(exit:) 217 | keyEquivalent:@"q"]; 218 | exitItem.target = self; 219 | 220 | [statusMenu addItem:exitItem]; 221 | 222 | return statusMenu; 223 | } 224 | 225 | - (NSMenu*) menuForDroplet:(BTOceanDataDroplet*) droplet { 226 | const NSUInteger kMenuWidth = 220; 227 | 228 | NSMenu *dropletMenu = [[NSMenu alloc] initWithTitle:droplet.name]; 229 | NSMenuItem *viewItem = [[NSMenuItem alloc] init]; 230 | 231 | // The first line is a simple custom view, too 232 | NSView *headlineView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, kMenuWidth, 35)]; 233 | 234 | // containing the current state as a led 235 | NSImageView *im = [[NSImageView alloc] initWithFrame:NSMakeRect(10, 9, 16, 16)]; 236 | if (droplet.isActive) 237 | im.image = [NSImage imageNamed:kIconActive]; 238 | else 239 | im.image = [NSImage imageNamed:kIconInactive]; 240 | [headlineView addSubview:im]; 241 | 242 | // and the current locked state 243 | NSImageView *imLocked = [[NSImageView alloc] initWithFrame:NSMakeRect(30, 9, 16, 16)]; 244 | if (droplet.locked) 245 | imLocked.image = [NSImage imageNamed:kIconLocked]; 246 | else 247 | imLocked.image = [NSImage imageNamed:kIconUnlocked]; 248 | [headlineView addSubview:imLocked]; 249 | 250 | // the label with the active status 251 | NSTextField *labelField = [[NSTextField alloc] initWithFrame: 252 | NSMakeRect(60, 5, 80, 20)]; 253 | [labelField setBordered:NO]; 254 | [labelField setEditable:NO]; 255 | [labelField setStringValue:droplet.status]; 256 | [labelField setBackgroundColor:[NSColor clearColor]]; 257 | 258 | // the open button 259 | if (droplet.isActive) { 260 | NSButton *openButton = [[NSButton alloc] 261 | initWithFrame:NSMakeRect(kMenuWidth - 70, 4, 60, 25)]; 262 | openButton.title = NSLocalizedString(@"Open...", @"info box"); 263 | [openButton setButtonType:NSMomentaryLightButton]; 264 | [openButton setBezelStyle:NSTexturedRoundedBezelStyle]; 265 | [openButton setTarget:self]; 266 | [openButton setAction:@selector(openDropletAction:)]; 267 | [openButton setTag: droplet.identifier.integerValue]; 268 | 269 | [headlineView addSubview:openButton]; 270 | } 271 | 272 | [headlineView addSubview:labelField]; 273 | 274 | NSMenuItem *activeitem = [[NSMenuItem alloc] init]; 275 | activeitem.view = headlineView; 276 | 277 | [dropletMenu addItem:activeitem]; 278 | 279 | BTSocialTextView *dropletTextView = [[BTSocialTextView alloc] 280 | initWithFrame:NSMakeRect(0, 0, kMenuWidth, 300)]; 281 | 282 | 283 | NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 284 | [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; 285 | [dateFormatter setDateStyle:NSDateFormatterShortStyle]; 286 | [dropletTextView addItem:NSLocalizedString(@"Created", @"info box") 287 | withValue:[dateFormatter stringFromDate:droplet.createdAt]]; 288 | 289 | [dropletTextView addItem:NSLocalizedString(@"Public IP", @"info box") withValue:droplet.publicIpAddresses]; 290 | if (droplet.privateIpAddresses && droplet.privateIpAddresses.length > 0) 291 | [dropletTextView addItem:NSLocalizedString(@"Private IP", @"info box") withValue:droplet.privateIpAddresses]; 292 | 293 | [dropletTextView addItem:NSLocalizedString(@"Kernel", @"info box") withValue:droplet.kernelName]; 294 | 295 | [dropletTextView addItem:NSLocalizedString(@"Features", @"info box") withValue:droplet.features]; 296 | 297 | // Create the Image String: 298 | if (droplet.image) { 299 | NSMutableString *imageString = [droplet.image.name mutableCopy]; 300 | if (droplet.image.distribution && droplet.image.distribution.length > 0) 301 | [imageString appendFormat:@"\n%@", droplet.image.distribution]; 302 | 303 | [dropletTextView addItem:NSLocalizedString(@"Image", @"info box") withValue:imageString.copy]; 304 | 305 | } else { 306 | [dropletTextView addItem:NSLocalizedString(@"Image", @"info box") withValue:NSLocalizedString(@"Image not found", @"info box if unknown image")]; 307 | } 308 | 309 | [dropletTextView addItem:NSLocalizedString(@"Region", @"info box") withValue:droplet.region.name]; 310 | 311 | // Create the Size String 312 | if (droplet.size) { 313 | NSMutableString *sizeString = [droplet.size.name mutableCopy]; 314 | NSArray *sizeName = @[NSLocalizedString(@"Memory", @"Info box size"), 315 | NSLocalizedString(@"Disk", @"Info box size"), 316 | NSLocalizedString(@"CPU", @"Info box size"), 317 | NSLocalizedString(@"Cost/Hour", @"Info box size"), 318 | NSLocalizedString(@"Cost/Month", @"Info Box size")]; 319 | 320 | NSArray *sizeKeys = @[droplet.size.memory, 321 | droplet.size.disk, 322 | droplet.size.cpu, 323 | droplet.size.costPerHour, 324 | droplet.size.costPerMonth]; 325 | for (int i=0; i 0) { 581 | _updated = @[]; 582 | for (NSDictionary* d in droplets) { 583 | _updated = [_updated arrayByAddingObject:d[@"droplet"]]; 584 | } 585 | [self setIconState]; 586 | } 587 | } 588 | 589 | //----------------------------------------------------------------------------- 590 | #pragma mark NSMenuDelegate 591 | //----------------------------------------------------------------------------- 592 | 593 | - (void)menuWillOpen:(NSMenu *)menu { 594 | // reset the updated flag 595 | _updated = nil; 596 | [self setIconState]; 597 | } 598 | 599 | //----------------------------------------------------------------------------- 600 | #pragma mark AppleScript 601 | //----------------------------------------------------------------------------- 602 | 603 | - (void) executeAppleScript:(NSString*)script { 604 | NSDictionary *errors = nil; 605 | NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script]; 606 | NSLog(@"%@", script); 607 | [appleScript executeAndReturnError:&errors]; 608 | if (errors) { 609 | NSLog(@"error: %@", errors); 610 | } 611 | } 612 | 613 | - (void) appleScriptOpenTerminalWithDroplet:(NSMenuItem*)dropletItem { 614 | BTOceanDataDroplet *droplet = dropletItem.representedObject; 615 | 616 | NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 617 | NSString *tc = [ud objectForKey:@"terminalCommand"]; 618 | if (!tc)tc = @"ssh root@{{IPADDRESS}}"; 619 | 620 | NSString *te = [ud objectForKey:@"terminalEmulator"]; 621 | 622 | tc = [tc stringByReplacingOccurrencesOfString:@"{{IPADDRESS}}" withString:droplet.ipAddress]; 623 | 624 | tc = [tc stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 625 | te = [te stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 626 | 627 | NSString *cmd = nil; 628 | if ([te isEqualToString:@"Terminal"]) { 629 | cmd = [self appleScript_terminal:tc]; 630 | } else if ([te isEqualToString:@"iTerm 2.0"]) { 631 | cmd = [self appleScript_iTerm2:tc]; 632 | } else if ([te isEqualToString:@"iTerm 2.9+"]) { 633 | cmd = [self appleScript_iTerm3:tc]; 634 | } 635 | 636 | [self executeAppleScript:cmd]; 637 | } 638 | 639 | - (NSString*) appleScript_terminal:(NSString*)cmd { 640 | NSString *basic = @"tell application \"Terminal\"\ndo script \"%@\"\nactivate\nend tell"; 641 | return [NSString stringWithFormat:basic, cmd]; 642 | } 643 | 644 | - (NSString*) appleScript_iTerm2:(NSString*)cmd { 645 | NSString *basic = @"tell application \"iTerm\"\nset myterm to (make new terminal)\ntell myterm\nset mysession to (make new session at the end of sessions)\ntell mysession\nexec command \"%@\"\nend tell\nend tell\nactivate\nend tell"; 646 | return [NSString stringWithFormat:basic, cmd]; 647 | } 648 | 649 | - (NSString*) appleScript_iTerm3:(NSString*)cmd { 650 | NSString *basic = @"tell application \"iTerm\"\ncreate window with default profile\ntell current session of current window\nwrite text \"%@\"\nend tell\nactivate\nend tell"; 651 | return [NSString stringWithFormat:basic, cmd]; 652 | } 653 | 654 | @end 655 | -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon-32-1.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-32-1.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon-64.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon128x128.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon16x16.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon256x256.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/AppIcon.appiconset/icon512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/AppIcon.appiconset/icon512x512.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/closedLock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "closedLock.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "closedLock@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/closedLock.imageset/closedLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/closedLock.imageset/closedLock.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/closedLock.imageset/closedLock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/closedLock.imageset/closedLock@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/default.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "default.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "default@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/default.imageset/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/default.imageset/default.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/default.imageset/default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/default.imageset/default@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/loading.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "loading.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "loading@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/loading.imageset/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/loading.imageset/loading.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/loading.imageset/loading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/loading.imageset/loading@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/newContent.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "newContent.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "newContent@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/newContent.imageset/newContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/newContent.imageset/newContent.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/newContent.imageset/newContent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/newContent.imageset/newContent@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/notConnected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "notConnected.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "notConnected@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/notConnected.imageset/notConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/notConnected.imageset/notConnected.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/notConnected.imageset/notConnected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/notConnected.imageset/notConnected@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/openLock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "openLock.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "openLock@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/openLock.imageset/openLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/openLock.imageset/openLock.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/openLock.imageset/openLock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/openLock.imageset/openLock@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateActive.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stateActive.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "stateActive@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateActive.imageset/stateActive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateActive.imageset/stateActive.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateActive.imageset/stateActive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateActive.imageset/stateActive@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateInactive.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stateInactive.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "stateInactive@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateInactive.imageset/stateInactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateInactive.imageset/stateInactive.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateInactive.imageset/stateInactive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateInactive.imageset/stateInactive@2x.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateUpdated.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stateUpdated.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "stateUpdated@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateUpdated.imageset/stateUpdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateUpdated.imageset/stateUpdated.png -------------------------------------------------------------------------------- /OceanBar/Images.xcassets/stateUpdated.imageset/stateUpdated@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/OceanBar/Images.xcassets/stateUpdated.imageset/stateUpdated@2x.png -------------------------------------------------------------------------------- /OceanBar/NSString+BTOauthLogin.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+BTOauthLogin.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (BTOauthLogin) 12 | - (NSString *)stringByDecodingURLFormat; 13 | - (NSMutableDictionary *)dictionaryFromQueryComponents; 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /OceanBar/NSString+BTOauthLogin.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+BTOauthLogin.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import "NSString+BTOauthLogin.h" 10 | 11 | @implementation NSString (BTOauthLogin) 12 | 13 | - (NSString *)stringByDecodingURLFormat 14 | { 15 | NSString *result = [self stringByReplacingOccurrencesOfString:@"+" withString:@" "]; 16 | result = [result stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 17 | return result; 18 | } 19 | 20 | - (NSMutableDictionary *)dictionaryFromQueryComponents 21 | { 22 | NSMutableDictionary *queryComponents = [NSMutableDictionary dictionary]; 23 | for(NSString *keyValuePairString in [self componentsSeparatedByString:@"&"]) 24 | { 25 | NSArray *keyValuePairArray = [keyValuePairString componentsSeparatedByString:@"="]; 26 | if ([keyValuePairArray count] < 2) continue; // Verify that there is at least one key, and at least one value. Ignore extra = signs 27 | NSString *key = [[keyValuePairArray objectAtIndex:0] stringByDecodingURLFormat]; 28 | NSString *value = [[keyValuePairArray objectAtIndex:1] stringByDecodingURLFormat]; 29 | NSMutableArray *results = [queryComponents objectForKey:key]; // URL spec says that multiple values are allowed per key 30 | if(!results) // First object 31 | { 32 | results = [NSMutableArray arrayWithCapacity:1]; 33 | [queryComponents setObject:results forKey:key]; 34 | } 35 | [results addObject:value]; 36 | } 37 | return queryComponents; 38 | } 39 | @end 40 | -------------------------------------------------------------------------------- /OceanBar/OceanBar-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.stylemac.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 30 23 | LSApplicationCategoryType 24 | public.app-category.developer-tools 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | Copyright © 2014 Benedikt Terhechte. All rights reserved. 36 | NSMainNibFile 37 | MainMenu 38 | NSPrincipalClass 39 | NSApplication 40 | 41 | 42 | -------------------------------------------------------------------------------- /OceanBar/OceanBar-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #import "BTOceanData.h" 10 | 11 | //----------------------------------------------------------------------------- 12 | #pragma mark Digital Ocean Setup 13 | //----------------------------------------------------------------------------- 14 | 15 | #define DIGITALOCEAN_BASE_URL @"https://api.digitalocean.com/v2" 16 | 17 | //----------------------------------------------------------------------------- 18 | #pragma mark Preferences User Defaults Keys 19 | //----------------------------------------------------------------------------- 20 | #define kCustomTokenKey @"customTokenKey" 21 | 22 | //----------------------------------------------------------------------------- 23 | #pragma mark Macros 24 | //----------------------------------------------------------------------------- 25 | 26 | // shorthand format string macro 27 | #define $p(...) [NSString stringWithFormat: __VA_ARGS__] 28 | 29 | //----------------------------------------------------------------------------- 30 | #pragma mark Static Functions 31 | //----------------------------------------------------------------------------- 32 | 33 | static inline NSError* errorFor(int code, NSString *message, NSString *domain) { 34 | NSError *underlyingError = [[NSError alloc] initWithDomain:NSCocoaErrorDomain 35 | code:errno userInfo:nil]; 36 | // Create and return the custom domain error. 37 | NSDictionary *errorDictionary = @{ NSLocalizedDescriptionKey : message, 38 | NSUnderlyingErrorKey : underlyingError}; 39 | 40 | NSError *anError = [[NSError alloc] initWithDomain:domain 41 | code:code 42 | userInfo:errorDictionary]; 43 | 44 | return anError; 45 | } 46 | 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /OceanBar/OceanBar.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.temporary-exception.apple-events 10 | 11 | com.apple.terminal 12 | com.googlecode.iterm2 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /OceanBar/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1347\cocoasubrtf570 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | \paperw11900\paperh16840\vieww9600\viewh8400\viewkind0 5 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 6 | 7 | \f0\b\fs24 \cf0 Engineering: 8 | \b0 \ 9 | Benedikt Terhechte\ 10 | \ 11 | 12 | \b Info: 13 | \b0 \ 14 | {\field{\*\fldinst{HYPERLINK "https://github.com/terhechte/OceanBar"}}{\fldrslt https://github.com/terhechte/OceanBar}}\ 15 | \ 16 | 17 | \b Frameworks Used:\ 18 | 19 | \b0 {\field{\*\fldinst{HYPERLINK "https://github.com/nxtbgthng/OAuth2Client"}}{\fldrslt https://github.com/nxtbgthng/OAuth2Client}}\ 20 | {\field{\*\fldinst{HYPERLINK "https://github.com/AFNetworking/AFNetworking"}}{\fldrslt https://github.com/AFNetworking/AFNetworking}}} -------------------------------------------------------------------------------- /OceanBar/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /OceanBar/kDigitalOceanClientKey.h: -------------------------------------------------------------------------------- 1 | // 2 | // kDigitalOceanClientKey.h 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 13/06/15. 6 | // Copyright (c) 2015 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #ifndef OceanBar_kDigitalOceanClientKey_h 10 | #define OceanBar_kDigitalOceanClientKey_h 11 | 12 | // Your Digital Ocean Client ID 13 | #define kDigitalOceanClientId @"CLIENT ID" 14 | 15 | // Your Digital Ocean Client Secret 16 | #define kDigitalOceanClientSecret @"CLIENT SECRET" 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /OceanBar/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // OceanBar 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) 12 | { 13 | return NSApplicationMain(argc, argv); 14 | } 15 | -------------------------------------------------------------------------------- /OceanBarTests/OceanBarTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.stylmac.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /OceanBarTests/OceanBarTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // OceanBarTests.m 3 | // OceanBarTests 4 | // 5 | // Created by Benedikt Terhechte on 28/04/14. 6 | // Copyright (c) 2014 Benedikt Terhechte. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface OceanBarTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation OceanBarTests 16 | 17 | - (void)setUp 18 | { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown 24 | { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample 30 | { 31 | XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /OceanBarTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.9' 2 | 3 | target 'OceanBar' do 4 | pod 'AFNetworking', '~> 2.0' 5 | pod 'NXOAuth2Client', '~> 1.2.8' 6 | end 7 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (2.6.3): 3 | - AFNetworking/NSURLConnection (= 2.6.3) 4 | - AFNetworking/NSURLSession (= 2.6.3) 5 | - AFNetworking/Reachability (= 2.6.3) 6 | - AFNetworking/Security (= 2.6.3) 7 | - AFNetworking/Serialization (= 2.6.3) 8 | - AFNetworking/UIKit (= 2.6.3) 9 | - AFNetworking/NSURLConnection (2.6.3): 10 | - AFNetworking/Reachability 11 | - AFNetworking/Security 12 | - AFNetworking/Serialization 13 | - AFNetworking/NSURLSession (2.6.3): 14 | - AFNetworking/Reachability 15 | - AFNetworking/Security 16 | - AFNetworking/Serialization 17 | - AFNetworking/Reachability (2.6.3) 18 | - AFNetworking/Security (2.6.3) 19 | - AFNetworking/Serialization (2.6.3) 20 | - NXOAuth2Client (1.2.8) 21 | 22 | DEPENDENCIES: 23 | - AFNetworking (~> 2.0) 24 | - NXOAuth2Client (~> 1.2.8) 25 | 26 | SPEC CHECKSUMS: 27 | AFNetworking: cb8d14a848e831097108418f5d49217339d4eb60 28 | NXOAuth2Client: 98c4d4c594edd2b289cfc34a8928653e1dab7adf 29 | 30 | PODFILE CHECKSUM: f7dac609fc9fcb1ae516daff458d1adc74a0b562 31 | 32 | COCOAPODS: 1.0.0.beta.6 33 | -------------------------------------------------------------------------------- /Readme.org: -------------------------------------------------------------------------------- 1 | * OceanBar 2 | ** A Mac OS X Digital Ocean Client 3 | 4 | This is a Mac OS X Client for the Digital Ocean API 5 | It is compatible with 10.9+ 6 | 7 | ** Features 8 | 9 | - Menubar App 10 | - List active and inactive machines 11 | - Power Cycle, Shutdown, Reboot, Power Off/On and Destroy machines 12 | - Open machines ip addresses in the browser on various ports. 13 | - Get notification for changes in the Notification Center 14 | - Define reload interval in the preferences 15 | - Prepared for localization 16 | - Open Terminal Window (Terminal.app/iTerm) for droplet 17 | - DigitalOcean API 2.0 18 | 19 | I know that there're by now other Digital Ocean Menubar apps for the Mac, but I had already half finished this one when the first other apps were released, and since I was quite happy with the data model I decided to just continue along the path and add some more functions. 20 | 21 | I'm planning to add more features to this app (as listed below) but for now it supports all the necessities required to use it as a good digital ocean menubar control. 22 | 23 | ** Screenshots 24 | 25 | [[screenshot1.jpeg]] 26 | 27 | [[screenshot2.jpeg]] 28 | 29 | ** Installation 30 | 31 | *** Download 32 | 33 | Simply Download the most [[https://github.com/terhechte/OceanBar/releases][recent binary from the "Releases" tab]]. 34 | 35 | *** Compile Yourself 36 | You'll need [[http://cocoapods.org/][CocoaPods]] to compile this project: 37 | 1. Checkout the repository 38 | 2. Jump into a terminal and go into the directory 39 | 3. Run 40 | #+BEGIN_SRC bash 41 | pod install 42 | #+END_SRC 43 | 4. Open 'OceanBar.xcworkspace' in Xcode 44 | 5. Open kDigitalOceanClientKey.h and enter your DigitalOcean Credentials 45 | 6. Compile, Run & Success 46 | 47 | * Todo 48 | - [x] Filter for additional droplet state: "new", which means - just created 49 | - [ ] Add functionality to snapshot machines 50 | - [ ] Add functionality to restore machines from snapshots 51 | - [ ] Add support for snapshot / image administration 52 | - [ ] See if it is possible to list backups / snapshots for machines. This would make it easier to destroy machines as we'd know that we have a snapshot from it. 53 | - [ ] Localize to other languages (any takers?) 54 | 55 | 56 | * License 57 | This code is licensed under the "Eclipse Public License 1.0 (EPL-1.0)" 58 | http://opensource.org/licenses/eclipse-1.0.php 59 | -------------------------------------------------------------------------------- /Resources/closedLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/closedLock.png -------------------------------------------------------------------------------- /Resources/closedLock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/closedLock@2x.png -------------------------------------------------------------------------------- /Resources/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/default.png -------------------------------------------------------------------------------- /Resources/default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/default@2x.png -------------------------------------------------------------------------------- /Resources/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon-1024.png -------------------------------------------------------------------------------- /Resources/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon-256.png -------------------------------------------------------------------------------- /Resources/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon-32.png -------------------------------------------------------------------------------- /Resources/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon-512.png -------------------------------------------------------------------------------- /Resources/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon-64.png -------------------------------------------------------------------------------- /Resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon.icns -------------------------------------------------------------------------------- /Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/icon.png -------------------------------------------------------------------------------- /Resources/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/loading.png -------------------------------------------------------------------------------- /Resources/loading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/loading@2x.png -------------------------------------------------------------------------------- /Resources/newContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/newContent.png -------------------------------------------------------------------------------- /Resources/newContent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/newContent@2x.png -------------------------------------------------------------------------------- /Resources/notConnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/notConnected.png -------------------------------------------------------------------------------- /Resources/notConnected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/notConnected@2x.png -------------------------------------------------------------------------------- /Resources/openLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/openLock.png -------------------------------------------------------------------------------- /Resources/openLock@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/openLock@2x.png -------------------------------------------------------------------------------- /Resources/stateActive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateActive.png -------------------------------------------------------------------------------- /Resources/stateActive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateActive@2x.png -------------------------------------------------------------------------------- /Resources/stateInactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateInactive.png -------------------------------------------------------------------------------- /Resources/stateInactive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateInactive@2x.png -------------------------------------------------------------------------------- /Resources/stateUpdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateUpdated.png -------------------------------------------------------------------------------- /Resources/stateUpdated@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/Resources/stateUpdated@2x.png -------------------------------------------------------------------------------- /screenshot1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/screenshot1.jpeg -------------------------------------------------------------------------------- /screenshot2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terhechte/OceanBar/eb2da3919a21af0c46a639d449dc44e019af66dc/screenshot2.jpeg --------------------------------------------------------------------------------