├── README.md
├── ScreenTime
├── Assets.xcassets
│ ├── Contents.json
│ ├── ScreenTime.imageset
│ │ ├── ScreenTime.png
│ │ ├── ScreenTime@2x.png
│ │ └── Contents.json
│ ├── ScreenTimePaused.imageset
│ │ ├── ScreenTimePaused.png
│ │ ├── ScreenTimePaused@2x.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── NSImageExtension.swift
├── Info.plist
├── NSDateExtension.swift
├── LaunchServicesHelper.swift
├── ScreenShooter.swift
├── Base.lproj
│ └── MainMenu.xib
├── MovieMaker.swift
├── Consolidator.swift
└── AppDelegate.swift
├── ScreenTime.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcuserdata
│ │ └── nst.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
├── xcuserdata
│ └── nst.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── project.pbxproj
├── ScreenTimeTests
├── Info.plist
└── ScreenTimeTests.swift
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 | # ScreenTime
2 |
3 | homepage: [http://seriot.ch/screentime/](http://seriot.ch/screentime/)
4 |
--------------------------------------------------------------------------------
/ScreenTime/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nst/ScreenTime/master/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime.png
--------------------------------------------------------------------------------
/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nst/ScreenTime/master/ScreenTime/Assets.xcassets/ScreenTime.imageset/ScreenTime@2x.png
--------------------------------------------------------------------------------
/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nst/ScreenTime/master/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused.png
--------------------------------------------------------------------------------
/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nst/ScreenTime/master/ScreenTime/Assets.xcassets/ScreenTimePaused.imageset/ScreenTimePaused@2x.png
--------------------------------------------------------------------------------
/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/master/ScreenTime.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/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/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.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/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 |
--------------------------------------------------------------------------------
/ScreenTimeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/ScreenTime.xcodeproj/xcuserdata/nst.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
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 |
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/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/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/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.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 |
--------------------------------------------------------------------------------