├── .gitignore ├── README.md ├── ScreenTime.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── nst.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── nst.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── ScreenTime ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── ScreenTime.imageset │ │ ├── Contents.json │ │ ├── ScreenTime.png │ │ └── ScreenTime@2x.png │ └── ScreenTimePaused.imageset │ │ ├── Contents.json │ │ ├── ScreenTimePaused.png │ │ └── ScreenTimePaused@2x.png ├── Base.lproj │ └── MainMenu.xib ├── Consolidator.swift ├── Info.plist ├── LaunchServicesHelper.swift ├── MovieMaker.swift ├── NSDateExtension.swift ├── NSImageExtension.swift └── ScreenShooter.swift └── ScreenTimeTests ├── Info.plist └── ScreenTimeTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | #Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenTime 2 | 3 | homepage: [http://seriot.ch/screentime/](http://seriot.ch/screentime/) 4 | -------------------------------------------------------------------------------- /ScreenTime.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 033688EA1C410FE300FE23CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033688E91C410FE300FE23CB /* AppDelegate.swift */; }; 11 | 033688EC1C410FE300FE23CB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 033688EB1C410FE300FE23CB /* Assets.xcassets */; }; 12 | 033688EF1C410FE300FE23CB /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 033688ED1C410FE300FE23CB /* MainMenu.xib */; }; 13 | 033688FA1C410FE300FE23CB /* ScreenTimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033688F91C410FE300FE23CB /* ScreenTimeTests.swift */; }; 14 | 033689071C42A3E500FE23CB /* ScreenShooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033689061C42A3E500FE23CB /* ScreenShooter.swift */; }; 15 | 033689091C42A56500FE23CB /* NSDateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033689081C42A56500FE23CB /* NSDateExtension.swift */; }; 16 | 0336890B1C42F74500FE23CB /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336890A1C42F74500FE23CB /* NSImageExtension.swift */; }; 17 | 0336890D1C43100F00FE23CB /* Consolidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336890C1C43100F00FE23CB /* Consolidator.swift */; }; 18 | 0336890F1C431AAB00FE23CB /* MovieMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336890E1C431AAB00FE23CB /* MovieMaker.swift */; }; 19 | 033689111C49A52700FE23CB /* LaunchServicesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033689101C49A52700FE23CB /* LaunchServicesHelper.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 033688F61C410FE300FE23CB /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 033688DE1C410FE300FE23CB /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 033688E51C410FE300FE23CB; 28 | remoteInfo = ScreenTime; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 033688E61C410FE300FE23CB /* ScreenTime.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScreenTime.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 033688E91C410FE300FE23CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 033688EB1C410FE300FE23CB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | 033688EE1C410FE300FE23CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 37 | 033688F01C410FE300FE23CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | 033688F51C410FE300FE23CB /* ScreenTimeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScreenTimeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 033688F91C410FE300FE23CB /* ScreenTimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTimeTests.swift; sourceTree = ""; }; 40 | 033688FB1C410FE300FE23CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 033689061C42A3E500FE23CB /* ScreenShooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShooter.swift; sourceTree = ""; }; 42 | 033689081C42A56500FE23CB /* NSDateExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateExtension.swift; sourceTree = ""; }; 43 | 0336890A1C42F74500FE23CB /* NSImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = ""; }; 44 | 0336890C1C43100F00FE23CB /* Consolidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Consolidator.swift; sourceTree = ""; }; 45 | 0336890E1C431AAB00FE23CB /* MovieMaker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieMaker.swift; sourceTree = ""; }; 46 | 033689101C49A52700FE23CB /* LaunchServicesHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchServicesHelper.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 033688E31C410FE300FE23CB /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | 033688F21C410FE300FE23CB /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 033688DD1C410FE300FE23CB = { 68 | isa = PBXGroup; 69 | children = ( 70 | 033688E81C410FE300FE23CB /* ScreenTime */, 71 | 033688F81C410FE300FE23CB /* ScreenTimeTests */, 72 | 033688E71C410FE300FE23CB /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 033688E71C410FE300FE23CB /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 033688E61C410FE300FE23CB /* ScreenTime.app */, 80 | 033688F51C410FE300FE23CB /* ScreenTimeTests.xctest */, 81 | ); 82 | name = Products; 83 | sourceTree = ""; 84 | }; 85 | 033688E81C410FE300FE23CB /* ScreenTime */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 0336890A1C42F74500FE23CB /* NSImageExtension.swift */, 89 | 033689081C42A56500FE23CB /* NSDateExtension.swift */, 90 | 0336890E1C431AAB00FE23CB /* MovieMaker.swift */, 91 | 0336890C1C43100F00FE23CB /* Consolidator.swift */, 92 | 033689101C49A52700FE23CB /* LaunchServicesHelper.swift */, 93 | 033689061C42A3E500FE23CB /* ScreenShooter.swift */, 94 | 033688E91C410FE300FE23CB /* AppDelegate.swift */, 95 | 033688EB1C410FE300FE23CB /* Assets.xcassets */, 96 | 033688ED1C410FE300FE23CB /* MainMenu.xib */, 97 | 033688F01C410FE300FE23CB /* Info.plist */, 98 | ); 99 | path = ScreenTime; 100 | sourceTree = ""; 101 | }; 102 | 033688F81C410FE300FE23CB /* ScreenTimeTests */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 033688F91C410FE300FE23CB /* ScreenTimeTests.swift */, 106 | 033688FB1C410FE300FE23CB /* Info.plist */, 107 | ); 108 | path = ScreenTimeTests; 109 | sourceTree = ""; 110 | }; 111 | /* End PBXGroup section */ 112 | 113 | /* Begin PBXNativeTarget section */ 114 | 033688E51C410FE300FE23CB /* ScreenTime */ = { 115 | isa = PBXNativeTarget; 116 | buildConfigurationList = 033688FE1C410FE300FE23CB /* Build configuration list for PBXNativeTarget "ScreenTime" */; 117 | buildPhases = ( 118 | 033688E21C410FE300FE23CB /* Sources */, 119 | 033688E31C410FE300FE23CB /* Frameworks */, 120 | 033688E41C410FE300FE23CB /* Resources */, 121 | ); 122 | buildRules = ( 123 | ); 124 | dependencies = ( 125 | ); 126 | name = ScreenTime; 127 | productName = ScreenTime; 128 | productReference = 033688E61C410FE300FE23CB /* ScreenTime.app */; 129 | productType = "com.apple.product-type.application"; 130 | }; 131 | 033688F41C410FE300FE23CB /* ScreenTimeTests */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 033689011C410FE300FE23CB /* Build configuration list for PBXNativeTarget "ScreenTimeTests" */; 134 | buildPhases = ( 135 | 033688F11C410FE300FE23CB /* Sources */, 136 | 033688F21C410FE300FE23CB /* Frameworks */, 137 | 033688F31C410FE300FE23CB /* Resources */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | 033688F71C410FE300FE23CB /* PBXTargetDependency */, 143 | ); 144 | name = ScreenTimeTests; 145 | productName = ScreenTimeTests; 146 | productReference = 033688F51C410FE300FE23CB /* ScreenTimeTests.xctest */; 147 | productType = "com.apple.product-type.bundle.unit-test"; 148 | }; 149 | /* End PBXNativeTarget section */ 150 | 151 | /* Begin PBXProject section */ 152 | 033688DE1C410FE300FE23CB /* Project object */ = { 153 | isa = PBXProject; 154 | attributes = { 155 | LastSwiftUpdateCheck = 0720; 156 | LastUpgradeCheck = 0720; 157 | ORGANIZATIONNAME = "Nicolas Seriot"; 158 | TargetAttributes = { 159 | 033688E51C410FE300FE23CB = { 160 | CreatedOnToolsVersion = 7.2; 161 | LastSwiftMigration = 0800; 162 | }; 163 | 033688F41C410FE300FE23CB = { 164 | CreatedOnToolsVersion = 7.2; 165 | LastSwiftMigration = 0820; 166 | TestTargetID = 033688E51C410FE300FE23CB; 167 | }; 168 | }; 169 | }; 170 | buildConfigurationList = 033688E11C410FE300FE23CB /* Build configuration list for PBXProject "ScreenTime" */; 171 | compatibilityVersion = "Xcode 3.2"; 172 | developmentRegion = English; 173 | hasScannedForEncodings = 0; 174 | knownRegions = ( 175 | en, 176 | Base, 177 | ); 178 | mainGroup = 033688DD1C410FE300FE23CB; 179 | productRefGroup = 033688E71C410FE300FE23CB /* Products */; 180 | projectDirPath = ""; 181 | projectRoot = ""; 182 | targets = ( 183 | 033688E51C410FE300FE23CB /* ScreenTime */, 184 | 033688F41C410FE300FE23CB /* ScreenTimeTests */, 185 | ); 186 | }; 187 | /* End PBXProject section */ 188 | 189 | /* Begin PBXResourcesBuildPhase section */ 190 | 033688E41C410FE300FE23CB /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | 033688EC1C410FE300FE23CB /* Assets.xcassets in Resources */, 195 | 033688EF1C410FE300FE23CB /* MainMenu.xib in Resources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | 033688F31C410FE300FE23CB /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXResourcesBuildPhase section */ 207 | 208 | /* Begin PBXSourcesBuildPhase section */ 209 | 033688E21C410FE300FE23CB /* Sources */ = { 210 | isa = PBXSourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | 033689091C42A56500FE23CB /* NSDateExtension.swift in Sources */, 214 | 033688EA1C410FE300FE23CB /* AppDelegate.swift in Sources */, 215 | 0336890D1C43100F00FE23CB /* Consolidator.swift in Sources */, 216 | 033689111C49A52700FE23CB /* LaunchServicesHelper.swift in Sources */, 217 | 0336890F1C431AAB00FE23CB /* MovieMaker.swift in Sources */, 218 | 0336890B1C42F74500FE23CB /* NSImageExtension.swift in Sources */, 219 | 033689071C42A3E500FE23CB /* ScreenShooter.swift in Sources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | 033688F11C410FE300FE23CB /* Sources */ = { 224 | isa = PBXSourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 033688FA1C410FE300FE23CB /* ScreenTimeTests.swift in Sources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | /* End PBXSourcesBuildPhase section */ 232 | 233 | /* Begin PBXTargetDependency section */ 234 | 033688F71C410FE300FE23CB /* PBXTargetDependency */ = { 235 | isa = PBXTargetDependency; 236 | target = 033688E51C410FE300FE23CB /* ScreenTime */; 237 | targetProxy = 033688F61C410FE300FE23CB /* PBXContainerItemProxy */; 238 | }; 239 | /* End PBXTargetDependency section */ 240 | 241 | /* Begin PBXVariantGroup section */ 242 | 033688ED1C410FE300FE23CB /* MainMenu.xib */ = { 243 | isa = PBXVariantGroup; 244 | children = ( 245 | 033688EE1C410FE300FE23CB /* Base */, 246 | ); 247 | name = MainMenu.xib; 248 | sourceTree = ""; 249 | }; 250 | /* End PBXVariantGroup section */ 251 | 252 | /* Begin XCBuildConfiguration section */ 253 | 033688FC1C410FE300FE23CB /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 258 | CLANG_CXX_LIBRARY = "libc++"; 259 | CLANG_ENABLE_MODULES = YES; 260 | CLANG_ENABLE_OBJC_ARC = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_CONSTANT_CONVERSION = YES; 263 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 264 | CLANG_WARN_EMPTY_BODY = YES; 265 | CLANG_WARN_ENUM_CONVERSION = YES; 266 | CLANG_WARN_INT_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_UNREACHABLE_CODE = YES; 269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 270 | CODE_SIGN_IDENTITY = "-"; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_DYNAMIC_NO_PIC = NO; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_OPTIMIZATION_LEVEL = 0; 279 | GCC_PREPROCESSOR_DEFINITIONS = ( 280 | "DEBUG=1", 281 | "$(inherited)", 282 | ); 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | MACOSX_DEPLOYMENT_TARGET = 10.11; 290 | MTL_ENABLE_DEBUG_INFO = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = macosx; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | SWIFT_VERSION = 4.0; 295 | }; 296 | name = Debug; 297 | }; 298 | 033688FD1C410FE300FE23CB /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 303 | CLANG_CXX_LIBRARY = "libc++"; 304 | CLANG_ENABLE_MODULES = YES; 305 | CLANG_ENABLE_OBJC_ARC = YES; 306 | CLANG_WARN_BOOL_CONVERSION = YES; 307 | CLANG_WARN_CONSTANT_CONVERSION = YES; 308 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 309 | CLANG_WARN_EMPTY_BODY = YES; 310 | CLANG_WARN_ENUM_CONVERSION = YES; 311 | CLANG_WARN_INT_CONVERSION = YES; 312 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 313 | CLANG_WARN_UNREACHABLE_CODE = YES; 314 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 315 | CODE_SIGN_IDENTITY = "-"; 316 | COPY_PHASE_STRIP = NO; 317 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 318 | ENABLE_NS_ASSERTIONS = NO; 319 | ENABLE_STRICT_OBJC_MSGSEND = YES; 320 | GCC_C_LANGUAGE_STANDARD = gnu99; 321 | GCC_NO_COMMON_BLOCKS = YES; 322 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 323 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 324 | GCC_WARN_UNDECLARED_SELECTOR = YES; 325 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 326 | GCC_WARN_UNUSED_FUNCTION = YES; 327 | GCC_WARN_UNUSED_VARIABLE = YES; 328 | MACOSX_DEPLOYMENT_TARGET = 10.11; 329 | MTL_ENABLE_DEBUG_INFO = NO; 330 | SDKROOT = macosx; 331 | SWIFT_VERSION = 4.0; 332 | }; 333 | name = Release; 334 | }; 335 | 033688FF1C410FE300FE23CB /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | COMBINE_HIDPI_IMAGES = YES; 340 | INFOPLIST_FILE = ScreenTime/Info.plist; 341 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 342 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.ScreenTime; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | }; 345 | name = Debug; 346 | }; 347 | 033689001C410FE300FE23CB /* Release */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 351 | COMBINE_HIDPI_IMAGES = YES; 352 | INFOPLIST_FILE = ScreenTime/Info.plist; 353 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 354 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.ScreenTime; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | }; 357 | name = Release; 358 | }; 359 | 033689021C410FE300FE23CB /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | BUNDLE_LOADER = "$(TEST_HOST)"; 363 | COMBINE_HIDPI_IMAGES = YES; 364 | INFOPLIST_FILE = ScreenTimeTests/Info.plist; 365 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 366 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.ScreenTimeTests; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ScreenTime.app/Contents/MacOS/ScreenTime"; 369 | }; 370 | name = Debug; 371 | }; 372 | 033689031C410FE300FE23CB /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | BUNDLE_LOADER = "$(TEST_HOST)"; 376 | COMBINE_HIDPI_IMAGES = YES; 377 | INFOPLIST_FILE = ScreenTimeTests/Info.plist; 378 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 379 | PRODUCT_BUNDLE_IDENTIFIER = ch.seriot.ScreenTimeTests; 380 | PRODUCT_NAME = "$(TARGET_NAME)"; 381 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ScreenTime.app/Contents/MacOS/ScreenTime"; 382 | }; 383 | name = Release; 384 | }; 385 | /* End XCBuildConfiguration section */ 386 | 387 | /* Begin XCConfigurationList section */ 388 | 033688E11C410FE300FE23CB /* Build configuration list for PBXProject "ScreenTime" */ = { 389 | isa = XCConfigurationList; 390 | buildConfigurations = ( 391 | 033688FC1C410FE300FE23CB /* Debug */, 392 | 033688FD1C410FE300FE23CB /* Release */, 393 | ); 394 | defaultConfigurationIsVisible = 0; 395 | defaultConfigurationName = Release; 396 | }; 397 | 033688FE1C410FE300FE23CB /* Build configuration list for PBXNativeTarget "ScreenTime" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | 033688FF1C410FE300FE23CB /* Debug */, 401 | 033689001C410FE300FE23CB /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | 033689011C410FE300FE23CB /* Build configuration list for PBXNativeTarget "ScreenTimeTests" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 033689021C410FE300FE23CB /* Debug */, 410 | 033689031C410FE300FE23CB /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | /* End XCConfigurationList section */ 416 | }; 417 | rootObject = 033688DE1C410FE300FE23CB /* Project object */; 418 | } 419 | -------------------------------------------------------------------------------- /ScreenTime.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScreenTime.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/ScreenTime/fba237a62fed469f01b218dce26ff64e64e8c3f2/ScreenTime.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ScreenTime.xcodeproj/xcuserdata/nst.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ScreenTime.xcodeproj/xcuserdata/nst.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ScreenTime.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 033688E51C410FE300FE23CB 16 | 17 | primary 18 | 19 | 20 | 033688F41C410FE300FE23CB 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ScreenTime/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 09/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 11 | switch (lhs, rhs) { 12 | case let (l?, r?): 13 | return l < r 14 | case (nil, _?): 15 | return true 16 | default: 17 | return false 18 | } 19 | } 20 | 21 | 22 | @NSApplicationMain 23 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { 24 | 25 | @IBOutlet weak var window: NSWindow! 26 | 27 | @IBOutlet weak var menu : NSMenu! 28 | @IBOutlet weak var versionMenuItem : NSMenuItem! 29 | @IBOutlet weak var skipScreensaverMenuItem : NSMenuItem! 30 | @IBOutlet weak var startAtLoginMenuItem : NSMenuItem! 31 | @IBOutlet weak var pauseCaptureMenuItem : NSMenuItem! 32 | @IBOutlet weak var historyDepthMenuItem : NSMenuItem! 33 | @IBOutlet weak var historyContentsMenuItem : NSMenuItem! 34 | @IBOutlet weak var historyDepthView : NSView! 35 | @IBOutlet weak var historyDepthSlider : NSSlider! 36 | @IBOutlet weak var historyDepthTextField : NSTextField! 37 | 38 | var dirPath : String 39 | var statusItem : NSStatusItem 40 | var timer: Timer 41 | var screenShooter : ScreenShooter 42 | 43 | static var historyDays = [1,7,30,90,360, 0] // zero for never 44 | 45 | override init() { 46 | self.dirPath = ("~/Library/ScreenTime" as NSString).expandingTildeInPath 47 | 48 | // 49 | 50 | self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 51 | 52 | // 53 | 54 | self.timer = Timer() // this instance won't be used 55 | 56 | self.screenShooter = ScreenShooter(path:dirPath)! 57 | 58 | super.init() 59 | 60 | let dirExists = self.ensureThatDirectoryExistsByCreatingOneIfNeeded(self.dirPath) 61 | 62 | guard dirExists else { 63 | print("-- cannot create \(self.dirPath)") 64 | 65 | let alert = NSAlert() 66 | alert.messageText = "ScreenTime cannot run" 67 | alert.informativeText = "Please create the ~/Library/ScreenTime/ directory"; 68 | alert.addButton(withTitle: "OK") 69 | alert.alertStyle = .critical 70 | 71 | let modalResponse = alert.runModal() 72 | 73 | if modalResponse == .alertFirstButtonReturn { 74 | NSApplication.shared.terminate(self) 75 | return 76 | } 77 | return 78 | } 79 | } 80 | 81 | func applicationDidFinishLaunching(_ aNotification: Notification) { 82 | // Insert code here to initialize your application 83 | 84 | let defaults = ["SecondsBetweenScreenshots":60, "FramesPerSecond":2] 85 | UserDefaults.standard.register(defaults: defaults) 86 | 87 | /**/ 88 | 89 | let currentVersionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")! 90 | 91 | let imageName = NSImage.Name(rawValue: "ScreenTime") 92 | let iconImage = NSImage(named: imageName) 93 | iconImage?.isTemplate = true 94 | 95 | self.statusItem.image = iconImage 96 | self.statusItem.highlightMode = true 97 | self.statusItem.toolTip = "ScreenTime \(currentVersionString)" 98 | self.statusItem.menu = self.menu; 99 | 100 | self.versionMenuItem.title = "Version \(currentVersionString)" 101 | 102 | self.historyDepthSlider.target = self 103 | self.historyDepthSlider.action = #selector(AppDelegate.historySliderDidMove(_:)) 104 | 105 | self.historyDepthSlider.allowsTickMarkValuesOnly = true 106 | self.historyDepthSlider.maxValue = Double(type(of: self).historyDays.count - 1) 107 | self.historyDepthSlider.numberOfTickMarks = type(of: self).historyDays.count 108 | 109 | self.updateHistoryDepthLabelDescription() 110 | self.updateHistoryDepthSliderPosition() 111 | 112 | self.historyDepthMenuItem.view = self.historyDepthView 113 | 114 | self.menu.delegate = self 115 | 116 | /**/ 117 | 118 | self.startTimer() 119 | 120 | /**/ 121 | 122 | self.updateStartAtLaunchMenuItemState() 123 | self.updateSkipScreensaverMenuItemState() 124 | self.updatePauseCaptureMenuItemState() 125 | } 126 | 127 | func applicationWillTerminate(_ aNotification: Notification) { 128 | // Insert code here to tear down your application 129 | 130 | self.stopTimer() 131 | } 132 | 133 | func ensureThatDirectoryExistsByCreatingOneIfNeeded(_ path : String) -> Bool { 134 | 135 | let fm = FileManager.default 136 | var isDir : ObjCBool = false 137 | let fileExists = fm.fileExists(atPath: path, isDirectory:&isDir) 138 | if fileExists { 139 | return isDir.boolValue 140 | } 141 | 142 | // create file 143 | 144 | do { 145 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 146 | print("-- created", path); 147 | return true 148 | } catch { 149 | print("-- error, cannot create \(path)", error) 150 | return false 151 | } 152 | } 153 | 154 | func startTimer() { 155 | print("-- startTimer") 156 | 157 | let optionalScreenShooter = ScreenShooter(path:dirPath) 158 | 159 | guard let existingScreenShooter = optionalScreenShooter else { 160 | 161 | let alert = NSAlert() 162 | alert.messageText = "ScreenTime cannot run" 163 | alert.informativeText = "Cannot use ~/Library/ScreenTime/ for screenshots"; 164 | alert.addButton(withTitle: "OK") 165 | alert.alertStyle = .critical 166 | 167 | let modalResponse = alert.runModal() 168 | 169 | if modalResponse == .alertFirstButtonReturn { 170 | NSApplication.shared.terminate(self) 171 | } 172 | 173 | return 174 | } 175 | 176 | self.screenShooter = existingScreenShooter 177 | screenShooter.makeScreenshotsAndConsolidate(nil) 178 | 179 | let timeInterval = UserDefaults.standard.integer(forKey: "SecondsBetweenScreenshots") 180 | 181 | self.timer.invalidate() 182 | self.timer = Timer.scheduledTimer( 183 | timeInterval: TimeInterval(timeInterval), 184 | target: screenShooter, 185 | selector: #selector(ScreenShooter.makeScreenshotsAndConsolidate(_:)), 186 | userInfo: nil, 187 | repeats: true) 188 | timer.tolerance = 10 189 | 190 | self.checkForUpdates() 191 | } 192 | 193 | func stopTimer() { 194 | print("-- stopTimer") 195 | 196 | timer.invalidate() 197 | } 198 | 199 | func updateStartAtLaunchMenuItemState() { 200 | let startAtLogin = LaunchServicesHelper().applicationIsInStartUpItems 201 | startAtLoginMenuItem.state = startAtLogin ? .on : .off; 202 | } 203 | 204 | func updateSkipScreensaverMenuItemState() { 205 | let skipScreensaver = UserDefaults.standard.bool(forKey: "SkipScreensaver") 206 | skipScreensaverMenuItem.state = skipScreensaver ? .on : .off 207 | } 208 | 209 | func updatePauseCaptureMenuItemState() { 210 | let captureIsPaused = self.timer.isValid == false 211 | pauseCaptureMenuItem.state = captureIsPaused ? .on : .off 212 | } 213 | 214 | @IBAction func about(_ sender:NSControl) { 215 | if let url = URL(string:"http://seriot.ch/screentime/") { 216 | NSWorkspace.shared.open(url) 217 | } 218 | } 219 | 220 | @IBAction func openFolder(_ sender:NSControl) { 221 | NSWorkspace.shared.openFile(dirPath) 222 | } 223 | 224 | @IBAction func toggleSkipScreensaver(_ sender:NSControl) { 225 | let skipScreensaver = UserDefaults.standard.bool(forKey: "SkipScreensaver") 226 | 227 | UserDefaults.standard.set(!skipScreensaver, forKey: "SkipScreensaver"); 228 | 229 | UserDefaults.standard.synchronize() 230 | 231 | self.updateSkipScreensaverMenuItemState() 232 | } 233 | 234 | @IBAction func toggleStartAtLogin(_ sender:NSControl) { 235 | LaunchServicesHelper().toggleLaunchAtStartup() 236 | 237 | self.updateStartAtLaunchMenuItemState() 238 | } 239 | 240 | @IBAction func togglePause(_ sender:NSControl) { 241 | let captureWasPaused = self.timer.isValid == false 242 | 243 | if captureWasPaused { 244 | self.startTimer() 245 | } else { 246 | self.stopTimer() 247 | } 248 | 249 | let imageName = NSImage.Name(rawValue: captureWasPaused ? "ScreenTime" : "ScreenTimePaused") 250 | if let iconImage = NSImage(named: imageName) { 251 | iconImage.isTemplate = true 252 | self.statusItem.image = iconImage 253 | } else { 254 | print("-- Error: cannot get image named \(imageName)") 255 | } 256 | 257 | self.updatePauseCaptureMenuItemState() 258 | } 259 | 260 | @IBAction func quit(_ sender:NSControl) { 261 | NSApplication.shared.terminate(self) 262 | } 263 | 264 | @IBAction func historySliderDidMove(_ slider:NSSlider) { 265 | 266 | let sliderValue = slider.integerValue 267 | print("-- \(sliderValue)") 268 | 269 | let s = AppDelegate.historyPeriodDescriptionForSliderValue(sliderValue) 270 | 271 | self.historyDepthTextField.stringValue = s 272 | 273 | let numberOfDays = AppDelegate.historyNumberOfDaysForSliderValue(sliderValue) 274 | 275 | UserDefaults.standard.set(numberOfDays, forKey: "HistoryToKeepInDays") 276 | } 277 | 278 | func checkForUpdates() { 279 | 280 | let url = URL(string:"http://www.seriot.ch/screentime/screentime.json")! 281 | 282 | let config = URLSessionConfiguration.default 283 | config.requestCachePolicy = .reloadIgnoringLocalCacheData 284 | config.urlCache = nil 285 | 286 | let session = URLSession.init(configuration: config) 287 | 288 | session.dataTask(with: url, completionHandler: { (optionalData, response, error) -> Void in 289 | 290 | DispatchQueue.main.async(execute: { 291 | 292 | guard let data = optionalData, 293 | let optionalDict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String:AnyObject], 294 | let d = optionalDict, 295 | let latestVersionString = d["latest_version_string"] as? String, 296 | let latestVersionURL = d["latest_version_url"] as? String 297 | else { 298 | return 299 | } 300 | 301 | print("-- latestVersionString: \(latestVersionString)") 302 | print("-- latestVersionURL: \(latestVersionURL)") 303 | 304 | let currentVersionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String 305 | 306 | let needsUpdate = currentVersionString < latestVersionString 307 | 308 | print("-- needsUpdate: \(needsUpdate)") 309 | if needsUpdate == false { return } 310 | 311 | let alert = NSAlert() 312 | alert.messageText = "ScreenTime \(latestVersionString) is Available" 313 | alert.informativeText = "Please download it and replace the current version."; 314 | alert.addButton(withTitle: "Download") 315 | alert.addButton(withTitle: "Cancel") 316 | alert.alertStyle = .critical 317 | 318 | let modalResponse = alert.runModal() 319 | 320 | if modalResponse == .alertFirstButtonReturn { 321 | if let downloadURL = URL(string:latestVersionURL) { 322 | NSWorkspace.shared.open(downloadURL) 323 | } 324 | } 325 | 326 | }) 327 | }) .resume() 328 | } 329 | 330 | class func sliderValueForNumberOfDays(_ numberOfDays:Int) -> Int { 331 | 332 | if let i = self.historyDays.index(of: numberOfDays) { 333 | return i 334 | } 335 | 336 | return 0 337 | } 338 | 339 | class func historyNumberOfDaysForSliderValue(_ value:Int) -> Int { 340 | 341 | if value >= self.historyDays.count { 342 | return 0 343 | } 344 | 345 | return self.historyDays[value] 346 | } 347 | 348 | class func historyPeriodDescriptionForSliderValue(_ value:Int) -> String { 349 | let i = self.historyNumberOfDaysForSliderValue(value) 350 | 351 | if i == 0 { return "Never" } 352 | if i == 1 { return "1 day" } 353 | 354 | return "\(i) days" 355 | } 356 | 357 | func updateHistoryDepthLabelDescription() { 358 | let numberOfDays = UserDefaults.standard.integer(forKey: "HistoryToKeepInDays") 359 | 360 | let sliderValue = AppDelegate.sliderValueForNumberOfDays(numberOfDays) 361 | 362 | let s = AppDelegate.historyPeriodDescriptionForSliderValue(sliderValue) 363 | 364 | historyDepthTextField.stringValue = s 365 | } 366 | 367 | func updateHistoryDepthSliderPosition() { 368 | let numberOfDays = UserDefaults.standard.integer(forKey: "HistoryToKeepInDays") 369 | historyDepthSlider.integerValue = AppDelegate.sliderValueForNumberOfDays(numberOfDays) 370 | } 371 | 372 | // MARK: - NSMenuDelegate 373 | 374 | func menuWillOpen(_ menu:NSMenu) { 375 | 376 | self.updateStartAtLaunchMenuItemState() 377 | 378 | guard let e = NSApp.currentEvent else { print("-- no event"); return } 379 | 380 | let modifierFlags = e.modifierFlags 381 | 382 | let optionKeyIsPressed = modifierFlags.contains(.option) 383 | let commandKeyIsPressed = modifierFlags.contains(.command) 384 | 385 | if(optionKeyIsPressed && commandKeyIsPressed) { 386 | screenShooter.makeScreenshotsAndConsolidate(nil) 387 | } 388 | self.versionMenuItem.isHidden = optionKeyIsPressed == false; 389 | 390 | // build history contents according to file system's contents 391 | 392 | guard let subMenu = self.historyContentsMenuItem.submenu else { return } 393 | subMenu.removeAllItems() 394 | 395 | let namesAndPaths = Consolidator.movies(dirPath).sorted(by: { $0.path > $1.path }) 396 | 397 | for (n,p) in namesAndPaths { 398 | var title = Consolidator.timestampInFilename(n) 399 | if let s = Date.srt_prettyDateFromShortTimestamp(title) { 400 | title = s 401 | } else if let s = Date.srt_prettyDateFromMediumTimestamp(title) { 402 | title = s 403 | } else if let s = Date.srt_prettyDateFromLongTimestamp(title) { 404 | title = s 405 | } 406 | let historyItem = subMenu.addItem(withTitle: title, action: #selector(AppDelegate.historyItemAction(_:)), keyEquivalent: "") 407 | historyItem.representedObject = p 408 | } 409 | } 410 | 411 | @objc 412 | func historyItemAction(_ menuItem:NSMenuItem) { 413 | if let path = menuItem.representedObject as? String { 414 | print("-- open \(path)") 415 | NSWorkspace().openFile(path) 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTime.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ScreenTime.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ScreenTime@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 | } -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/ScreenTime/fba237a62fed469f01b218dce26ff64e64e8c3f2/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime.png -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/ScreenTime/fba237a62fed469f01b218dce26ff64e64e8c3f2/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime@2x.png -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ScreenTimePaused.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "ScreenTimePaused@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 | } -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/ScreenTime/fba237a62fed469f01b218dce26ff64e64e8c3f2/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused.png -------------------------------------------------------------------------------- /ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/ScreenTime/fba237a62fed469f01b218dce26ff64e64e8c3f2/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused@2x.png -------------------------------------------------------------------------------- /ScreenTime/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /ScreenTime/Consolidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Consolidator.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 10/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class Consolidator { 12 | 13 | var dirPath : String 14 | 15 | class func removeFiles(_ paths:[String]) { 16 | for path in paths { 17 | do { 18 | try FileManager.default.removeItem(atPath: path) 19 | } catch { 20 | print("-- could not remove \(path), error \(error)") 21 | } 22 | } 23 | } 24 | 25 | class func timestampInFilename(_ filename:String) -> String { 26 | let timestampAndDisplayID = ((filename as NSString).lastPathComponent as NSString).deletingPathExtension.components(separatedBy: "_") 27 | return timestampAndDisplayID[0] 28 | } 29 | 30 | class func writeMovieFromJpgPaths(_ dirPath:String, jpgPaths:[String], movieName:String, displayIDString:NSString, fps:Int, completionHandler:@escaping (String) -> ()) { 31 | 32 | if jpgPaths.isEmpty { 33 | print("-- no screenshots to turn into movie") 34 | return 35 | } 36 | 37 | if fps <= 0 { 38 | print("-- fps must be > 0") 39 | return 40 | } 41 | 42 | // write movie 43 | 44 | let filename = "\(movieName)_\(displayIDString).mov" 45 | let moviePath = (dirPath as NSString).appendingPathComponent(filename) 46 | 47 | let fm = FileManager.default 48 | let fileExists = fm.fileExists(atPath: moviePath) 49 | if fileExists { 50 | do { 51 | try fm.removeItem(atPath: moviePath) 52 | } catch { 53 | print("-- can't remove \(moviePath), error: \(error)") 54 | } 55 | } 56 | 57 | let firstImage = NSImage(contentsOfFile: jpgPaths.first!) 58 | guard let existingFirstImage = firstImage else { 59 | print("-- cannot get first image at \(jpgPaths.first!)") 60 | return 61 | } 62 | 63 | guard let movieMaker = MovieMaker(path: moviePath, frameSize: existingFirstImage.size, fps: UInt(fps)) else { return } 64 | 65 | for jpgPath in jpgPaths { 66 | 67 | guard let image = NSImage(contentsOfFile:jpgPath) else { 68 | continue 69 | } 70 | 71 | let timestamp = timestampInFilename(jpgPath) 72 | 73 | var formattedDate = timestamp 74 | 75 | if let s = Date.srt_prettyDateFromLongTimestamp(timestamp) { 76 | formattedDate = s 77 | } 78 | 79 | _ = movieMaker.appendImageFromDrawing({ (context) -> () in 80 | 81 | // draw image 82 | let rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) 83 | image.draw(in: rect) 84 | 85 | // draw string frame 86 | let STRING_RECT_ORIGIN_X : CGFloat = rect.size.width - 320 87 | let STRING_RECT_ORIGIN_Y : CGFloat = 32 88 | let STRING_RECT_WIDTH : CGFloat = 300 89 | let STRING_RECT_HEIGHT : CGFloat = 54 90 | let stringRect : CGRect = CGRect(x: STRING_RECT_ORIGIN_X, y: STRING_RECT_ORIGIN_Y, width: STRING_RECT_WIDTH, height: STRING_RECT_HEIGHT); 91 | 92 | NSColor.white.setFill() 93 | NSColor.black.setStroke() 94 | stringRect.fill() 95 | NSBezierPath.stroke(stringRect) 96 | 97 | // draw string 98 | let font = NSFont(name:"Courier", size:24)! 99 | let attributes : [NSAttributedStringKey:AnyObject] = [.font:font, .foregroundColor:NSColor.blue] 100 | 101 | let s = NSAttributedString(string: (formattedDate as NSString).lastPathComponent, attributes: attributes) 102 | s.draw(at: CGPoint(x: STRING_RECT_ORIGIN_X + 16, y: STRING_RECT_ORIGIN_Y + 16)) 103 | }) 104 | } 105 | 106 | movieMaker.endWritingMovieWithWithCompletionHandler({ (path) -> () in 107 | DispatchQueue.main.async(execute: { 108 | completionHandler(path) 109 | }) 110 | }) 111 | } 112 | 113 | /* 114 | consolidate past assets 115 | 116 | for each past day 117 | ...make hour movie from day images 118 | ...make day movie from hour movies 119 | 120 | for each today past hour 121 | ...make hour movie 122 | */ 123 | 124 | init(dirPath:String) { 125 | self.dirPath = dirPath 126 | } 127 | 128 | static let dateFormatter: DateFormatter = { 129 | let df = DateFormatter() 130 | df.dateFormat = "yyyyMMdd" 131 | return df 132 | }() 133 | 134 | class func filterFilename( 135 | _ paths:[String], 136 | dirPath:String, 137 | withExt ext:String, 138 | timestampLength:Int, 139 | beforeString:String, 140 | groupedByPrefixOfLength groupPrefixLength:Int) -> [[String]] { 141 | 142 | let filteredPaths = paths.filter { 143 | let p = ($0 as NSString) 144 | if p.pathExtension.lowercased() != ext.lowercased() { return false } 145 | let filename = (p.lastPathComponent as NSString).deletingPathExtension 146 | let components = filename.components(separatedBy: "_") 147 | if components.count != 2 { return false } 148 | let timestamp = components[0] 149 | if timestamp.lengthOfBytes(using: String.Encoding.utf8) != timestampLength { return false } 150 | return beforeString.compare(filename) == .orderedDescending 151 | } 152 | 153 | // 154 | 155 | var groupDictionary = [String:[String]]() 156 | 157 | for path in filteredPaths { 158 | 159 | let lastPathComponent = ((path as NSString).lastPathComponent as NSString) 160 | let prefix = lastPathComponent.substring(to: groupPrefixLength) // timestamp 161 | let components = lastPathComponent.deletingPathExtension.components(separatedBy: "_") 162 | guard components.count == 2 else { 163 | print("-- unexpected lastPathComponent: \(lastPathComponent)") 164 | continue 165 | } 166 | let suffix = components[1] 167 | 168 | let key = "\(prefix)_\(suffix)" 169 | 170 | if groupDictionary[key] == nil { groupDictionary[key] = [String]() } 171 | 172 | let fullPath = (dirPath as NSString).appendingPathComponent(path) 173 | groupDictionary[key]?.append(fullPath) 174 | } 175 | 176 | let sortedKeys = groupDictionary.keys.sorted() 177 | 178 | var groups = [[String]]() 179 | 180 | for key in sortedKeys { 181 | if let group = groupDictionary[key] { 182 | groups.append( group ) 183 | } 184 | } 185 | 186 | return groups 187 | } 188 | 189 | // hour movies -> day movies 190 | func consolidateHourMoviesIntoDayMovies() throws { 191 | 192 | let today = (Date().srt_timestamp() as NSString).substring(to: 8) 193 | let filenames = try FileManager.default.contentsOfDirectory(atPath: dirPath) 194 | 195 | let hourMoviesArrays = Consolidator.filterFilename( 196 | filenames, 197 | dirPath: dirPath, 198 | withExt: "mov", 199 | timestampLength: 10, 200 | beforeString: today, 201 | groupedByPrefixOfLength: 8) 202 | 203 | for hourMovies in hourMoviesArrays { 204 | 205 | guard hourMovies.count > 0 else { continue } 206 | 207 | let timestampAndDisplayID = ((hourMovies.first! as NSString).lastPathComponent as NSString).deletingPathExtension.components(separatedBy: "_") 208 | 209 | guard timestampAndDisplayID.count == 2 else { 210 | print("-- unexpected path: \(timestampAndDisplayID)") 211 | continue 212 | } 213 | 214 | let timestamp = timestampAndDisplayID[0] 215 | let displayID = timestampAndDisplayID[1] 216 | 217 | let day = (timestamp as NSString).substring(to: 8) 218 | 219 | let filename = "\(day)_\(displayID).mov" 220 | 221 | let outPath = (dirPath as NSString).appendingPathComponent(filename) 222 | 223 | print("-- merging into \(outPath): \(hourMovies)") 224 | 225 | do { 226 | try MovieMaker.mergeMovies(hourMovies, outPath: outPath, completionHandler: { (path) -> () in 227 | Consolidator.removeFiles(hourMovies) 228 | }) 229 | } catch { 230 | print("-- could not merge movies, \(error)") 231 | } 232 | } 233 | } 234 | 235 | // screenshots -> hour movies 236 | func consolidateScreenshotsIntoHourMovies() throws { 237 | 238 | let todayHour = (Date().srt_timestamp() as NSString).substring(to: 10) 239 | let filenames = try FileManager.default.contentsOfDirectory(atPath: dirPath) 240 | 241 | let hourImagesArrays = Consolidator.filterFilename( 242 | filenames, 243 | dirPath: dirPath, 244 | withExt: "jpg", 245 | timestampLength: 14, 246 | beforeString: todayHour, 247 | groupedByPrefixOfLength: 10) 248 | 249 | for hourImages in hourImagesArrays { 250 | 251 | guard hourImages.count > 0 else { continue } 252 | 253 | let timestampAndDisplayID = ((hourImages.first! as NSString).lastPathComponent as NSString).deletingPathExtension.components(separatedBy: "_") 254 | 255 | if timestampAndDisplayID.count != 2 { 256 | print("-- unexpected path format: \(timestampAndDisplayID)") 257 | continue 258 | } 259 | 260 | let timestamp = timestampAndDisplayID[0] 261 | let displayID = timestampAndDisplayID[1] 262 | 263 | let filename = (timestamp as NSString).substring(to: 10) 264 | 265 | let fps : Int = UserDefaults.standard.integer(forKey: "FramesPerSecond") 266 | 267 | Consolidator.writeMovieFromJpgPaths( 268 | dirPath, 269 | jpgPaths: hourImages, 270 | movieName: filename, 271 | displayIDString: displayID as NSString, 272 | fps: fps, 273 | completionHandler: { (path) -> () in 274 | Consolidator.removeFiles(hourImages) 275 | }) 276 | } 277 | } 278 | 279 | class func movies(_ dirPath:String) -> [(prettyName:String, path:String)] { 280 | 281 | let fm = FileManager.default 282 | 283 | do { 284 | let contents = try fm.contentsOfDirectory(atPath: dirPath) 285 | 286 | return contents 287 | .filter({ ($0 as NSString).pathExtension == "mov" }) 288 | .map({ (prettyName:$0, path:"\(dirPath)/\($0)") }) 289 | } catch { 290 | return [] 291 | } 292 | } 293 | 294 | class func dateOfDayForFilename(_ filename:String) -> Date? { 295 | 296 | guard filename.lengthOfBytes(using: String.Encoding.utf8) >= 8 else { return nil } 297 | 298 | let s = (filename as NSString).substring(to: 8) 299 | 300 | return self.dateFormatter.date(from: s) 301 | } 302 | 303 | class func daysBetweenDates(date1 d1:Date, date2 d2:Date) -> Int { 304 | 305 | var (date1, date2) = (d1, d2) 306 | 307 | if date1.compare(date2) == .orderedDescending { 308 | (date1, date2) = (date2, date1) 309 | } 310 | 311 | let calendar = Calendar.current 312 | let components = (calendar as NSCalendar).components(.day, from: date1, to: date2, options: []) 313 | return components.day! 314 | } 315 | 316 | func removeFilesOlderThanNumberOfDays(_ historyToKeepInDays:Int) throws { 317 | 318 | guard historyToKeepInDays > 0 else { return } 319 | 320 | let fm = FileManager.default 321 | 322 | let contents = try fm.contentsOfDirectory(atPath: dirPath) 323 | 324 | let now = Date() 325 | 326 | for filename in contents { 327 | 328 | guard let date = Consolidator.dateOfDayForFilename(filename) else { continue } 329 | 330 | let fileAgeInDays = Consolidator.daysBetweenDates(date1: date, date2: now) 331 | 332 | if fileAgeInDays > historyToKeepInDays { 333 | let path = (dirPath as NSString).appendingPathComponent(filename) 334 | print("-- removing file with age in days: \(fileAgeInDays), \(path)") 335 | 336 | do { 337 | try fm.removeItem(atPath: path) 338 | } catch { 339 | print("-- cannot remove \(path), \(error)") 340 | } 341 | } 342 | 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /ScreenTime/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | ch.seriot.$(PRODUCT_NAME:rfc1034identifier) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.7 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | LSUIElement 28 | 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | seriot.ch 34 | 35 | NSIncludesSubdomains 36 | 37 | NSTemporaryExceptionAllowsInsecureHTTPLoads 38 | 39 | 40 | 41 | 42 | NSHumanReadableCopyright 43 | Copyright © 2015 Nicolas Seriot. All rights reserved. 44 | NSMainNibFile 45 | MainMenu 46 | NSPrincipalClass 47 | NSApplication 48 | 49 | 50 | -------------------------------------------------------------------------------- /ScreenTime/LaunchServicesHelper.swift: -------------------------------------------------------------------------------- 1 | /* This file is part of mac2imgur. 2 | * 3 | * mac2imgur is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | 8 | * mac2imgur is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | 13 | * You should have received a copy of the GNU General Public License 14 | * along with mac2imgur. If not, see . 15 | */ 16 | 17 | import Foundation 18 | 19 | // Refined version of http://stackoverflow.com/a/27442962 20 | class LaunchServicesHelper { 21 | 22 | let applicationURL = URL(fileURLWithPath: Bundle.main.bundlePath) 23 | 24 | var applicationIsInStartUpItems: Bool { 25 | return itemReferencesInLoginItems.existingItem != nil 26 | } 27 | 28 | var itemReferencesInLoginItems: (existingItem: LSSharedFileListItem?, lastItem: LSSharedFileListItem?) { 29 | let itemURL = UnsafeMutablePointer?>.allocate(capacity: 1) 30 | let loginItemsList = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() 31 | // Can't cast directly from CFArray to Swift Array, so the CFArray needs to be bridged to a NSArray first 32 | let loginItemsListSnapshot: NSArray = LSSharedFileListCopySnapshot(loginItemsList, nil).takeRetainedValue() 33 | if let loginItems = loginItemsListSnapshot as? [LSSharedFileListItem] { 34 | for loginItem in loginItems { 35 | if LSSharedFileListItemResolve(loginItem, 0, itemURL, nil) == noErr { 36 | if let URL = itemURL.pointee?.takeRetainedValue() { 37 | // Check whether the item is for this application 38 | if URL as URL == applicationURL { 39 | return (loginItem, loginItems.last) 40 | } 41 | } 42 | } 43 | } 44 | // The application was not found in the startup list 45 | return (nil, loginItems.last ?? kLSSharedFileListItemBeforeFirst.takeRetainedValue()) 46 | } 47 | return (nil, nil) 48 | } 49 | 50 | func toggleLaunchAtStartup() { 51 | let itemReferences = itemReferencesInLoginItems 52 | let loginItemsList = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() 53 | if let existingItem = itemReferences.existingItem { 54 | // Remove application from login items 55 | LSSharedFileListItemRemove(loginItemsList, existingItem) 56 | } else { 57 | // Add application to login items 58 | LSSharedFileListInsertItemURL(loginItemsList, itemReferences.lastItem, nil, nil, applicationURL as CFURL!, nil, nil) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ScreenTime/MovieMaker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieMaker.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 08/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | // inspired by Erica Sadun's MovieMaker class 10 | // https://github.com/erica/useful-things/tree/master/useful%20pack/Movie%20Maker 11 | 12 | import AppKit 13 | import AVFoundation 14 | 15 | open class MovieMaker { 16 | 17 | fileprivate var height : Int 18 | fileprivate var width : Int 19 | fileprivate var framesPerSecond : UInt? 20 | fileprivate var frameCount : UInt? 21 | 22 | fileprivate var writer : AVAssetWriter! 23 | fileprivate var input : AVAssetWriterInput? 24 | fileprivate var adaptor : AVAssetWriterInputPixelBufferAdaptor? 25 | 26 | public init?(path:String, frameSize:CGSize, fps:UInt) { 27 | 28 | self.height = Int(frameSize.height) 29 | self.width = Int(frameSize.width) 30 | 31 | guard fps > 0 else { 32 | print("-- error: frames per second must be a positive integer") 33 | return 34 | } 35 | 36 | let dm = FileManager.default 37 | 38 | if dm.fileExists(atPath: path) { 39 | let globallyUniqueString = ProcessInfo.processInfo.globallyUniqueString 40 | let newPath = path + "_\(globallyUniqueString)" 41 | 42 | do { 43 | try dm.moveItem(atPath: path, toPath: newPath) 44 | } catch { 45 | print("-- cannot move \(path) to \(newPath)") 46 | return nil 47 | } 48 | } 49 | 50 | self.framesPerSecond = fps 51 | 52 | self.frameCount = 0; 53 | 54 | // Create Movie URL 55 | let movieURL = URL(fileURLWithPath: path) 56 | 57 | // Create Asset Writer 58 | do { 59 | self.writer = try AVAssetWriter(outputURL: movieURL, fileType: .mov) 60 | } catch { 61 | print("-- error: cannot create asset writer, \(error)") 62 | return nil 63 | } 64 | 65 | // Create Input 66 | var videoSettings = [String:AnyObject]() 67 | videoSettings[AVVideoCodecKey] = AVVideoCodecH264 as AnyObject? 68 | videoSettings[AVVideoWidthKey] = width as AnyObject? 69 | videoSettings[AVVideoHeightKey] = height as AnyObject? 70 | 71 | self.input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) 72 | 73 | self.writer.add(input!) 74 | 75 | // Build adapter 76 | self.adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input!, sourcePixelBufferAttributes: nil) 77 | 78 | guard writer.startWriting() else { 79 | print("-- cannot start writing") 80 | return nil 81 | } 82 | 83 | writer.startSession(atSourceTime: kCMTimeZero) 84 | } 85 | 86 | //@objc(mergeMovieAtPaths:intoPath:completionHandler:error:) 87 | open class func mergeMovies(_ inPath:[String], outPath:String, completionHandler:@escaping ((_ path:String) -> ())) throws { 88 | 89 | let composition = AVMutableComposition() 90 | 91 | guard let composedTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { 92 | throw NSError(domain: "ScreenTime", code: 0, userInfo: [NSLocalizedDescriptionKey:"Cannot add video track"]) 93 | } 94 | 95 | var time = kCMTimeZero 96 | 97 | try inPath.forEach { (inPath) -> () in 98 | 99 | let fileURL = URL(fileURLWithPath: inPath) 100 | let asset = AVURLAsset(url: fileURL) 101 | let videoTracks = asset.tracks(withMediaType: .video) 102 | 103 | guard let firstTrack = videoTracks.first else { 104 | print("-- no first track in \(inPath)") 105 | return 106 | } 107 | 108 | let timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration) 109 | 110 | try composedTrack.insertTimeRange(timeRange, of: firstTrack, at: time) 111 | 112 | time = CMTimeAdd(time, asset.duration); 113 | } 114 | 115 | /**/ 116 | 117 | let fm = FileManager.default 118 | 119 | if fm.fileExists(atPath: outPath) { 120 | try fm.removeItem(atPath: outPath) 121 | } 122 | 123 | guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleProRes422LPCM) else { return } 124 | 125 | exporter.outputURL = URL(fileURLWithPath: outPath) 126 | exporter.outputFileType = .mov 127 | exporter.shouldOptimizeForNetworkUse = true 128 | 129 | exporter.exportAsynchronously(completionHandler: { () -> Void in 130 | /* 131 | AVAssetExportSessionStatusUnknown, 132 | AVAssetExportSessionStatusWaiting, 133 | AVAssetExportSessionStatusExporting, 134 | AVAssetExportSessionStatusCompleted, 135 | AVAssetExportSessionStatusFailed, 136 | AVAssetExportSessionStatusCancelled 137 | */ 138 | 139 | switch(exporter.status) { 140 | case .completed: 141 | DispatchQueue.main.sync { 142 | print("-- mergeMovies export completed \(outPath)") 143 | completionHandler(outPath) 144 | } 145 | default: 146 | DispatchQueue.main.sync { 147 | print(exporter.status) 148 | } 149 | } 150 | }) 151 | } 152 | 153 | open func appendImage(_ image:NSImage) -> Bool { 154 | 155 | return self.appendImageFromDrawing({ [unowned self] (context) -> () in 156 | let rect = CGRect(x:0, y:0, width:CGFloat(self.width), height:CGFloat(self.height)) 157 | NSColor.black.set() 158 | rect.fill() 159 | image.draw(in:rect) 160 | }) 161 | } 162 | 163 | open func appendImageFromDrawing(_ drawingBlock: (_ context:CGContext) -> ()) -> Bool { 164 | 165 | guard let pixelBufferRef = self.createPixelBufferFromDrawing(drawingBlock) else { 166 | return false 167 | } 168 | 169 | guard let existingInput = input else { 170 | return false 171 | } 172 | 173 | while(existingInput.isReadyForMoreMediaData == false) {} 174 | 175 | guard let existingFrameCount = self.frameCount else { return false } 176 | guard let existimeFramesPerSecond = self.framesPerSecond else { return false } 177 | 178 | guard let existingAdaptor = self.adaptor else { return false } 179 | 180 | let cmTime = CMTimeMake(Int64(existingFrameCount), Int32(existimeFramesPerSecond)) 181 | let success = existingAdaptor.append(pixelBufferRef, withPresentationTime: cmTime) 182 | 183 | if success == false { 184 | print("-- error writing frame \(self.frameCount!)") 185 | return false 186 | } 187 | 188 | self.frameCount! += 1 189 | 190 | return success 191 | } 192 | 193 | open func endWritingMovieWithWithCompletionHandler(_ completionHandler:@escaping (_ path:String) -> ()) { 194 | // frameCount++; 195 | 196 | guard let existingInput = self.input else { return } 197 | 198 | existingInput.markAsFinished() 199 | 200 | // [_writer endSessionAtSourceTime:CMTimeMake(frameCount, (int32_t) framesPerSecond)]; 201 | 202 | self.writer.finishWriting { () -> Void in 203 | let path = self.writer.outputURL.path 204 | print("-- wrote", path) 205 | self.input = nil 206 | self.adaptor = nil 207 | completionHandler(path) 208 | } 209 | } 210 | 211 | fileprivate func createPixelBuffer() -> CVPixelBuffer? { 212 | 213 | // Create Pixel Buffer 214 | let pixelBufferOptions : NSDictionary = [kCVPixelBufferCGImageCompatibilityKey as NSString:true, kCVPixelBufferCGBitmapContextCompatibilityKey as NSString:true]; 215 | 216 | var pixelBuffer : CVPixelBuffer? = nil; 217 | 218 | let status : CVReturn = CVPixelBufferCreate(kCFAllocatorDefault, 219 | self.width, 220 | self.height, 221 | kCVPixelFormatType_32ARGB, 222 | pixelBufferOptions as NSDictionary, 223 | &pixelBuffer) 224 | 225 | if (status != kCVReturnSuccess) { 226 | print("-- error creating pixel buffer") 227 | return nil 228 | } 229 | 230 | return pixelBuffer 231 | } 232 | 233 | fileprivate func createPixelBufferFromDrawing(_ contextDrawingBlock: (_ context:CGContext) -> ()) -> CVPixelBuffer? { 234 | 235 | guard let pixelBufferRef = self.createPixelBuffer() else { return nil } 236 | 237 | CVPixelBufferLockBaseAddress(pixelBufferRef, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) 238 | let pixelData = CVPixelBufferGetBaseAddress(pixelBufferRef) 239 | let RGBColorSpace = CGColorSpaceCreateDeviceRGB() 240 | 241 | guard let context = CGContext( 242 | data: pixelData, 243 | width: self.width, 244 | height: self.height, 245 | bitsPerComponent: 8, 246 | bytesPerRow: 4 * self.width, 247 | space: RGBColorSpace, 248 | bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) else { 249 | print("-- error creating bitmap context") 250 | CVPixelBufferUnlockBaseAddress(pixelBufferRef, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) 251 | return nil 252 | } 253 | 254 | // Perform drawing 255 | NSGraphicsContext.saveGraphicsState() 256 | let gc = NSGraphicsContext(cgContext: context, flipped: false) 257 | 258 | NSGraphicsContext.current = gc 259 | 260 | contextDrawingBlock(context) 261 | 262 | NSGraphicsContext.restoreGraphicsState() 263 | 264 | CVPixelBufferUnlockBaseAddress(pixelBufferRef, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))); 265 | 266 | return pixelBufferRef; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /ScreenTime/NSDateExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateExtension.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 10/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | 13 | fileprivate static var srt_longTimestampDateFormatter : DateFormatter { 14 | let df = DateFormatter() 15 | df.dateFormat = "yyyyMMddHHmmss" 16 | return df 17 | } 18 | 19 | fileprivate static var srt_mediumTimestampDateFormatter : DateFormatter { 20 | let df = DateFormatter() 21 | df.dateFormat = "yyyyMMddHH" 22 | return df 23 | } 24 | 25 | fileprivate static var srt_shortTimestampDateFormatter : DateFormatter { 26 | let df = DateFormatter() 27 | df.dateFormat = "yyyyMMdd" 28 | return df 29 | } 30 | 31 | fileprivate static var srt_prettyDateMediumFormatter : DateFormatter { 32 | let df = DateFormatter() 33 | df.dateFormat = "yyyy-MM-dd HH:00" 34 | return df 35 | } 36 | 37 | fileprivate static var srt_prettyDateLongFormatter : DateFormatter { 38 | let df = DateFormatter() 39 | df.dateFormat = "yyyy-MM-dd HH:mm:ss" 40 | return df 41 | } 42 | 43 | fileprivate static var srt_prettyDateShortFormatter : DateFormatter { 44 | let df = DateFormatter() 45 | df.dateFormat = "yyyy-MM-dd E" 46 | return df 47 | } 48 | 49 | func srt_timestamp() -> String { 50 | return Date.srt_longTimestampDateFormatter.string(from: self) 51 | } 52 | 53 | static func srt_prettyDateFromShortTimestamp(_ timestamp:String) -> String? { 54 | guard let date = Date.srt_shortTimestampDateFormatter.date(from: timestamp) else { 55 | print("-- cannot convert short timestamp \(timestamp) into short date string") 56 | return nil 57 | } 58 | return Date.srt_prettyDateShortFormatter.string(from: date) 59 | } 60 | 61 | static func srt_prettyDateFromMediumTimestamp(_ timestamp:String) -> String? { 62 | guard let date = Date.srt_mediumTimestampDateFormatter.date(from: timestamp) else { 63 | print("-- cannot convert medium timestamp \(timestamp) into medium date string") 64 | return nil 65 | } 66 | return Date.srt_prettyDateMediumFormatter.string(from: date) 67 | } 68 | 69 | static func srt_prettyDateFromLongTimestamp(_ timestamp:String) -> String? { 70 | guard let date = Date.srt_longTimestampDateFormatter.date(from: timestamp) else { 71 | print("-- cannot convert long timestamp \(timestamp) into long date string") 72 | return nil 73 | } 74 | return Date.srt_prettyDateLongFormatter.string(from: date) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ScreenTime/NSImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImageExtension.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 10/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | extension NSImage { 12 | func srt_writeAsJpeg(_ path:String) -> Bool { 13 | guard let imageData = self.tiffRepresentation else { return false } 14 | let bitmapRep = NSBitmapImageRep(data: imageData) 15 | guard let jpegData = bitmapRep?.representation(using:.jpeg, properties: [.compressionFactor : 0.8]) else { return false } 16 | do { 17 | try jpegData.write(to: URL(fileURLWithPath:path)) 18 | } catch { 19 | print("-- can't write, error", error) 20 | return false 21 | } 22 | return true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ScreenTime/ScreenShooter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenShooter.swift 3 | // ScreenTime 4 | // 5 | // Created by nst on 10/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class ScreenShooter { 12 | 13 | var directoryPath : String 14 | 15 | init?(path:String) { 16 | 17 | let fm = FileManager.default 18 | 19 | var isDir : ObjCBool = false 20 | let fileExists = fm.fileExists(atPath: path, isDirectory: &isDir) 21 | if fileExists == false || isDir.boolValue == false { 22 | do { 23 | try fm.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 24 | print("-- created", path); 25 | } catch { 26 | print("-- error, cannot create", path, error) 27 | directoryPath = "" 28 | return nil 29 | } 30 | } 31 | 32 | self.directoryPath = path; 33 | } 34 | 35 | func takeScreenshot(_ displayID:CGDirectDisplayID) -> NSImage? { 36 | guard let imageRef = CGDisplayCreateImage(displayID) else { return nil } 37 | return NSImage(cgImage: imageRef, size: NSZeroSize) 38 | } 39 | 40 | func writeScreenshot(_ image:NSImage, displayIDForFilename:String) -> Bool { 41 | 42 | let timestamp = Date().srt_timestamp() 43 | let filename = timestamp + "_\(displayIDForFilename).jpg" 44 | 45 | let path = (directoryPath as NSString).appendingPathComponent(filename) 46 | 47 | let success = image.srt_writeAsJpeg(path) 48 | 49 | if(success) { 50 | print("-- write", path) 51 | } else { 52 | print("-- can't write", path) 53 | } 54 | 55 | return success 56 | } 57 | 58 | func isRunningScreensaver() -> Bool { 59 | let runningApplications = NSWorkspace.shared.runningApplications 60 | 61 | for app in runningApplications { 62 | if let bundlerIdentifier = app.bundleIdentifier { 63 | if bundlerIdentifier.hasPrefix("com.apple.ScreenSaver") { 64 | return true 65 | } 66 | } 67 | } 68 | return false 69 | } 70 | 71 | @objc 72 | func makeScreenshotsAndConsolidate(_ timer:Timer?) { 73 | if UserDefaults.standard.bool(forKey: "SkipScreensaver") && self.isRunningScreensaver() { 74 | return 75 | } 76 | 77 | if UserDefaults.standard.bool(forKey: "PauseCapture") { 78 | print("-- capture pause prevented screenshot"); 79 | return 80 | } 81 | 82 | let MAX_DISPLAYS : UInt32 = 16 83 | 84 | var displayCount: UInt32 = 0; 85 | let result = CGGetActiveDisplayList(0, nil, &displayCount) 86 | if result != .success { 87 | print("-- can't get active display list, error: \(result)") 88 | return 89 | } 90 | 91 | let allocated = Int(displayCount) 92 | let activeDisplays = UnsafeMutablePointer.allocate(capacity: allocated) 93 | 94 | let status : CGError = CGGetActiveDisplayList(MAX_DISPLAYS, activeDisplays, &displayCount) 95 | if status != .success { 96 | print("-- cannot get active display list, error \(status)") 97 | } 98 | 99 | for i in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ScreenTimeTests/ScreenTimeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenTimeTests.swift 3 | // ScreenTimeTests 4 | // 5 | // Created by nst on 09/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ScreenTime 11 | 12 | class ScreenTimeTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | 28 | let filenames = [ 29 | "20150601000000_111.jpg", 30 | "20150601000100_111.jpg", 31 | "20150601000000_123.jpg", 32 | "20150601000100_123.jpg", 33 | "20150615000000_111.jpg", 34 | "2015061510_111.mov", 35 | "2015061511_111.mov", 36 | "2015080110_111.mov"] 37 | 38 | let jpgGroups = Consolidator.filterFilename(filenames, 39 | dirPath: "/tmp", 40 | withExt: "jpg", 41 | timestampLength: 14, 42 | beforeString: "20150615", 43 | groupedByPrefixOfLength: 10) 44 | 45 | let expectedJPGs = [ 46 | ["/tmp/20150601000000_111.jpg", "/tmp/20150601000100_111.jpg"], 47 | ["/tmp/20150601000000_123.jpg", "/tmp/20150601000100_123.jpg"] 48 | ] 49 | 50 | XCTAssertEqual(jpgGroups.count, expectedJPGs.count) 51 | 52 | for (i,o1) in jpgGroups.enumerated() { 53 | let o2 = expectedJPGs[i] 54 | XCTAssertEqual(o1, o2) 55 | } 56 | 57 | /**/ 58 | 59 | let movGroups = Consolidator.filterFilename(filenames, 60 | dirPath: "/tmp", 61 | withExt: "mov", 62 | timestampLength: 10, 63 | beforeString: "20150701", 64 | groupedByPrefixOfLength: 8) 65 | 66 | let expectedMOVs = [ 67 | ["/tmp/2015061510_111.mov", "/tmp/2015061511_111.mov"] 68 | ] 69 | 70 | XCTAssertEqual(movGroups.count, expectedMOVs.count) 71 | 72 | for (i,o1) in movGroups.enumerated() { 73 | let o2 = expectedMOVs[i] 74 | XCTAssertEqual(o1, o2) 75 | } 76 | } 77 | 78 | func testPerformanceExample() { 79 | // This is an example of a performance test case. 80 | self.measure { 81 | // Put the code you want to measure the time of here. 82 | } 83 | } 84 | 85 | } 86 | --------------------------------------------------------------------------------