├── Capture
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon-16.png
│ │ ├── icon-32.png
│ │ ├── icon-128.png
│ │ ├── icon-16@2x.png
│ │ ├── icon-256.png
│ │ ├── icon-32@2x.png
│ │ ├── icon-512.png
│ │ ├── icon-128@2x.png
│ │ ├── icon-256@2x.png
│ │ ├── icon-512@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── BridgingHeader.h
├── Capture.entitlements
├── Controllers
│ ├── PrefsWindowController.swift
│ ├── CaptureWindowController.swift
│ ├── PrefsWindowController.xib
│ └── CaptureWindowController.xib
├── AppDelegate.swift
├── Info.plist
├── Extensions
│ ├── Shared
│ │ ├── Extensions-Geometry.swift
│ │ └── Extensions-Shared.swift
│ └── macOS
│ │ └── Extensions-macOS.swift
├── CaptureThing.swift
└── Base.lproj
│ └── MainMenu.xib
├── README.md
├── Capture.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── Podfile
├── Capture.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Podfile.lock
├── LICENSE
└── .gitignore
/Capture/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # capture-thing
2 |
3 | Please see [the accompanying blog post](https://tyler.io/capture-thing/) for details.
4 |
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-16.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-32.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-128.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-256.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-512.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tylerhall/capture-thing/main/Capture/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png
--------------------------------------------------------------------------------
/Capture.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Capture/BridgingHeader.h:
--------------------------------------------------------------------------------
1 | //
2 | // BridgingHeader.h
3 | // Capture
4 | //
5 | // Created by Tyler Hall on 4/19/21.
6 | //
7 |
8 | #ifndef BridgingHeader_h
9 | #define BridgingHeader_h
10 |
11 | #import
12 |
13 | #endif /* BridgingHeader_h */
14 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'Capture' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for Capture
9 | pod 'MASShortcut'
10 | end
11 |
--------------------------------------------------------------------------------
/Capture/Capture.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.automation.apple-events
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Capture.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Capture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Capture.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - MASShortcut (2.4.0)
3 |
4 | DEPENDENCIES:
5 | - MASShortcut
6 |
7 | SPEC REPOS:
8 | trunk:
9 | - MASShortcut
10 |
11 | SPEC CHECKSUMS:
12 | MASShortcut: d9e4909e878661cc42877cc9d6efbe638273ab57
13 |
14 | PODFILE CHECKSUM: 24a9bc608f460c195484110ae123abdace82d66e
15 |
16 | COCOAPODS: 1.11.2
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tyler Hall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Capture/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon-16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon-16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon-32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon-32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon-128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon-128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon-256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon-256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon-512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon-512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Capture/Controllers/PrefsWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrefsWindowController.swift
3 | // Capture
4 | //
5 | // Created by Tyler Hall on 4/19/21.
6 | //
7 |
8 | import AppKit
9 |
10 | let kShowShortcut = "kShowShortcut"
11 | let kCaptureScreenshot = "kCaptureScreenshot"
12 | let kCaptureActiveTab = "kCaptureActiveTab"
13 | let kCaptureAllTabs = "kCaptureAllTabs"
14 | let kCapturePath = "kCapturePath"
15 | let kBrowser = "kBrowser"
16 |
17 | class PrefsWindowController: NSWindowController {
18 |
19 | static let shared = PrefsWindowController(windowNibName: String(describing: PrefsWindowController.self))
20 |
21 | @IBOutlet weak var showShortcutKeyView: MASShortcutView!
22 | @IBOutlet weak var screenshotCheckbox: NSButton!
23 | @IBOutlet weak var activeBrowserTabCheckbox: NSButton!
24 | @IBOutlet weak var allBrowserTabsCheckbox: NSButton!
25 | @IBOutlet weak var captureFolderPathControl: NSPathControl!
26 |
27 | override func windowDidLoad() {
28 | super.windowDidLoad()
29 | showShortcutKeyView.associatedUserDefaultsKey = kShowShortcut
30 | captureFolderPathControl.url = UserDefaults.standard.url(forKey: kCapturePath)
31 | }
32 |
33 | @IBAction func activeBrowserCheckboxClicked(_ sender: AnyObject?) {
34 | if activeBrowserTabCheckbox.state == .on {
35 | allBrowserTabsCheckbox.state = .off
36 | UserDefaults.standard.set(false, forKey: kCaptureAllTabs)
37 | }
38 | }
39 |
40 | @IBAction func allBrowsersCheckboxClicked(_ sender: AnyObject?) {
41 | if allBrowserTabsCheckbox.state == .on {
42 | activeBrowserTabCheckbox.state = .off
43 | UserDefaults.standard.set(false, forKey: kCaptureActiveTab)
44 | }
45 | }
46 |
47 | @IBAction func chooseCaptureFolder(_ sender: AnyObject?) {
48 | let panel = NSOpenPanel()
49 | panel.canChooseFiles = false
50 | panel.canChooseDirectories = true
51 | panel.runModal()
52 | if let url = panel.url {
53 | captureFolderPathControl.url = url
54 | UserDefaults.standard.set(url, forKey: kCapturePath)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 | .DS_Store
92 | /Pods
93 |
--------------------------------------------------------------------------------
/Capture/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Capture
4 | //
5 | // Created by Tyler Hall on 4/18/21.
6 | //
7 |
8 | import Cocoa
9 |
10 | @main
11 | class AppDelegate: NSObject, NSApplicationDelegate {
12 |
13 | var isLaunching = true
14 |
15 | func applicationDidFinishLaunching(_ aNotification: Notification) {
16 | UserDefaults.standard.addObserver(self, forKeyPath: kShowShortcut, options: NSKeyValueObservingOptions.init(), context: nil)
17 | shortcutsDidChange()
18 |
19 | // Always show the prefs window until we have picked a capture folder...
20 | if UserDefaults.standard.url(forKey: kCapturePath) == nil {
21 | PrefsWindowController.shared.showWindow(nil)
22 | }
23 | }
24 |
25 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
26 | if keyPath == kShowShortcut {
27 | shortcutsDidChange()
28 | }
29 | }
30 |
31 | func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
32 | guard !isLaunching else {
33 | isLaunching = false
34 | return false
35 | }
36 |
37 | showCaptureWindow()
38 | return false
39 | }
40 |
41 | @objc func shortcutsDidChange() {
42 | MASShortcutBinder.shared()?.breakBinding(withDefaultsKey: kShowShortcut)
43 | MASShortcutBinder.shared()?.bindShortcut(withDefaultsKey: kShowShortcut, toAction: { [weak self] in
44 | self?.handleHotkey()
45 | })
46 | }
47 |
48 | @IBAction func showPrefs(_ sender: AnyObject?) {
49 | PrefsWindowController.shared.showWindow(nil)
50 | }
51 |
52 | @IBAction func newCapture(_ sender: AnyObject?) {
53 | showCaptureWindow()
54 | }
55 |
56 | @IBAction func openCaptureFolder(_ sender: AnyObject?) {
57 | if let url = CaptureThing.shared.currentFileURL?.deletingLastPathComponent() {
58 | NSWorkspace.shared.activateFileViewerSelecting([url])
59 | }
60 | }
61 |
62 | func handleHotkey() {
63 | let windowIsVisible = (CaptureWindowController.shared.window?.isVisible ?? false)
64 |
65 | if windowIsVisible && NSApp.isActive {
66 | CaptureWindowController.shared.takeQuickScreenshot()
67 | } else {
68 | showCaptureWindow()
69 | NSApp.activate(ignoringOtherApps: true)
70 | }
71 | }
72 |
73 | func showCaptureWindow() {
74 | // Always show the prefs window until we have picked a capture folder...
75 | guard UserDefaults.standard.url(forKey: kCapturePath) != nil else {
76 | PrefsWindowController.shared.showWindow(nil)
77 | return
78 | }
79 |
80 | CaptureWindowController.shared.showWindow(nil)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Capture/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0.0
21 | CFBundleVersion
22 | 2
23 | LSApplicationCategoryType
24 | public.app-category.productivity
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | NSAppleEventsUsageDescription
28 | Allow Capture to integrate with other applications.
29 | NSHumanReadableCopyright
30 | Copyright © 2021 Retina Studio. All rights reserved.
31 | NSPrincipalClass
32 | NSApplication
33 | NSServices
34 |
35 |
36 | NSKeyEquivalent
37 |
38 | default
39 | B
40 |
41 | NSMenuItem
42 |
43 | default
44 | Process with TextBuddy
45 |
46 | NSMessage
47 | processTextServiceHandler
48 | NSPortName
49 | TextBuddy
50 | NSRequiredContext
51 |
52 | NSSendTypes
53 |
54 | NSStringPboardType
55 | NSRTFPboardType
56 | NSRTFDPboardType
57 |
58 | NSServiceDescription
59 | Processes the selected text with TextBuddy.
60 |
61 |
62 | NSMenuItem
63 |
64 | default
65 | Send to TextBuddy
66 |
67 | NSMessage
68 | textServiceHandler
69 | NSPortName
70 | TextBuddy
71 | NSRequiredContext
72 |
73 | NSSendTypes
74 |
75 | NSStringPboardType
76 | NSRTFPboardType
77 | NSRTFDPboardType
78 |
79 | NSServiceDescription
80 | Sends the selected text to TextBuddy to be edited.
81 |
82 |
83 | NSMenuItem
84 |
85 | default
86 | Send to TextBuddy
87 |
88 | NSMessage
89 | fileServiceHandler
90 | NSPortName
91 | TextBuddy
92 | NSRequiredContext
93 |
94 | NSSendTypes
95 |
96 | NSURLPboardType
97 |
98 | NSServiceDescription
99 | Sends the selected file to TextBuddy to process.
100 |
101 |
102 | NSMainNibFile
103 | MainMenu
104 |
105 |
106 |
--------------------------------------------------------------------------------
/Capture/Extensions/Shared/Extensions-Geometry.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 |
3 | extension CGPoint {
4 |
5 | func scaled(_ size: CGSize) -> CGPoint {
6 | return CGPoint(x: x * size.width, y: y * size.height)
7 | }
8 |
9 | func offsetBy(_ dx: CGFloat, dy: CGFloat) -> CGPoint {
10 | return CGPoint(x: x + dx, y: y + dy)
11 | }
12 |
13 | func scaledAroundCenterPoint(_ scale: CGFloat, centerPoint: CGPoint) -> CGPoint {
14 | let xNew = (scale * (x - centerPoint.x)) + centerPoint.x
15 | let yNew = (scale * (y - centerPoint.y)) + centerPoint.y
16 | return CGPoint(x: xNew, y: yNew)
17 | }
18 |
19 | func rotatedAroundCenterPoint(_ radians: CGFloat, centerPoint: CGPoint) -> CGPoint {
20 | let s = sin(radians)
21 | let c = cos(radians)
22 |
23 | var px = self.x - centerPoint.x
24 | var py = self.y - centerPoint.y
25 |
26 | let newX = px * c - py * s
27 | let newY = px * s + py * c
28 |
29 | px = newX + centerPoint.x
30 | py = newY + centerPoint.y
31 |
32 | return CGPoint(x: px, y: py)
33 | }
34 | }
35 |
36 | extension CGFloat {
37 | var valueOrZero: CGFloat {
38 | return CGFloat.maximum(0, self)
39 | }
40 | }
41 |
42 | extension Array where Element == CGPoint {
43 |
44 | func pointsAdjacentTo(index: Int) -> (Int, Int)? {
45 | guard count >= 3 else { return nil }
46 |
47 | if index == 0 {
48 | return (count - 1, 1)
49 | } else if index == (count - 1) {
50 | return (index - 1, 0)
51 | } else {
52 | return (index - 1, index + 1)
53 | }
54 | }
55 |
56 | // This only works on arrays with 4 points
57 | func oppositeVertexIndex(vertexIndex: Int) -> Int {
58 | guard count == 4 else { fatalError() }
59 | switch vertexIndex {
60 | case 0:
61 | return 2
62 | case 1:
63 | return 3
64 | case 2:
65 | return 0
66 | case 3:
67 | return 1
68 | default:
69 | fatalError()
70 | }
71 | }
72 | }
73 |
74 | struct Line {
75 | var p1: CGPoint
76 | var p2: CGPoint
77 |
78 | var midPoint: CGPoint {
79 | return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
80 | }
81 |
82 | var length: CGFloat {
83 | return sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
84 | }
85 |
86 | var slope: CGFloat? {
87 | guard p2.x - p1.x != 0 else { return nil }
88 | return (p2.y - p1.y) / (p2.x - p1.x)
89 | }
90 |
91 | var yIntercept: CGFloat? {
92 | if let slope = slope {
93 | return p1.y - slope * p1.x
94 | }
95 | return nil
96 | }
97 |
98 | func pointAt(x: CGFloat) -> CGPoint? {
99 | if let slope = slope, let yIntercept = yIntercept {
100 | return CGPoint(x: x, y: slope * x + yIntercept)
101 | }
102 | return nil
103 | }
104 |
105 | func intercetionWith(line: Line) -> CGPoint? {
106 | // y = ax + c
107 | // y = bx + d
108 | // ax + c = bx + d
109 | // ax - bx = d - c
110 | // x = (d - c) / (a - b)
111 |
112 | guard let a = slope, let c = yIntercept else {
113 | return nil
114 | }
115 |
116 | guard let b = line.slope, let d = line.yIntercept else {
117 | return nil
118 | }
119 |
120 | // Check for parallel lines
121 | if a == b {
122 | return nil
123 | }
124 |
125 | if a - b == 0 {
126 | return nil
127 | }
128 |
129 | if d - c == 0 {
130 | return nil
131 | }
132 |
133 | let x = (d - c) / (a - b)
134 | let y = a * x + c
135 |
136 | let p = CGPoint(x: x, y: y)
137 | if hasXCoord(x: p.x) && hasYCoord(y: p.y) && line.hasXCoord(x: p.x) && line.hasYCoord(y: p.y) {
138 | return p
139 | }
140 |
141 | return nil
142 | }
143 |
144 | func hasXCoord(x: CGFloat) -> Bool {
145 | let xMin = min(p1.x, p2.x)
146 | let xMax = max(p1.x, p2.x)
147 | return (xMin <= x) && (x <= xMax)
148 | }
149 |
150 | func hasYCoord(y: CGFloat) -> Bool {
151 | let yMin = min(p1.y, p2.y)
152 | let yMax = max(p1.y, p2.y)
153 | return (yMin <= y) && (y <= yMax)
154 | }
155 |
156 | func hasPoint(point: CGPoint) -> Bool {
157 | return hasXCoord(x: point.x) && hasYCoord(y: point.y)
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Capture/Extensions/macOS/Extensions-macOS.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSEvent {
4 | var isRightClick: Bool {
5 | let rightClick = (self.type == .rightMouseDown)
6 | let controlClick = self.modifierFlags.contains(.control)
7 | return rightClick || controlClick
8 | }
9 | }
10 |
11 | extension NSFont {
12 | func withTraits(_ traits: NSFontDescriptor.SymbolicTraits) -> NSFont {
13 | let fd = fontDescriptor.withSymbolicTraits(traits)
14 | if let font = NSFont(descriptor: fd, size: pointSize) {
15 | return font
16 | } else {
17 | return self
18 | }
19 | }
20 |
21 | func italics() -> NSFont {
22 | return withTraits(.italic)
23 | }
24 |
25 | func bold() -> NSFont {
26 | return withTraits(.bold)
27 | }
28 |
29 | func boldItalics() -> NSFont {
30 | return withTraits([.bold, .italic])
31 | }
32 | }
33 |
34 | extension NSImage {
35 | func writeJPGToURL(_ url: URL, quality: Float = 0.85) -> Bool {
36 | let properties = [NSBitmapImageRep.PropertyKey.compressionFactor: quality]
37 | guard let imageData = self.tiffRepresentation else { return false }
38 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false }
39 | guard let fileData = imageRep.representation(using: .jpeg, properties: properties) else { return false }
40 |
41 | do {
42 | try fileData.write(to: url)
43 | } catch {
44 | return false
45 | }
46 |
47 | return true
48 | }
49 |
50 | func writePNGToURL(_ url: URL) -> Bool {
51 | guard let imageData = self.tiffRepresentation else { return false }
52 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false }
53 | guard let fileData = imageRep.representation(using: .png, properties: [:]) else { return false }
54 |
55 | do {
56 | try fileData.write(to: url)
57 | } catch {
58 | return false
59 | }
60 |
61 | return true
62 | }
63 |
64 | func tint(color: NSColor) -> NSImage {
65 | let image = self.copy() as! NSImage
66 | image.lockFocus()
67 |
68 | color.set()
69 |
70 | let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
71 | imageRect.fill(using: .sourceAtop)
72 |
73 | image.unlockFocus()
74 |
75 | return image
76 | }
77 |
78 | func scaleBy(factor: CGFloat = 0.5) -> NSImage {
79 | let newSize = NSMakeSize(size.width * factor, size.height * factor)
80 | let scaledImage = NSImage(size: newSize)
81 | scaledImage.lockFocus()
82 | draw(in: NSMakeRect(0, 0, newSize.width, newSize.height))
83 | scaledImage.unlockFocus()
84 | return scaledImage
85 | }
86 | }
87 |
88 | extension NSTableView {
89 | func reloadOnMainThread(_ complete: (() -> ())? = nil) {
90 | DispatchQueue.main.async {
91 | self.reloadData()
92 | complete?()
93 | }
94 | }
95 |
96 | func reloadMaintainingSelection(_ complete: (() -> ())? = nil) {
97 | let oldSelectedRowIndexes = selectedRowIndexes
98 | reloadOnMainThread {
99 | if oldSelectedRowIndexes.count == 0 {
100 | if self.numberOfRows > 0 {
101 | self.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
102 | }
103 | } else {
104 | self.selectRowIndexes(oldSelectedRowIndexes, byExtendingSelection: false)
105 | }
106 | }
107 | }
108 |
109 | func selectFirstPossibleRow() {
110 | for i in 0..= 0) && (selectedRow < numberOfRows) {
140 | return item(atRow: selectedRow)
141 | } else {
142 | return nil
143 | }
144 | }
145 | }
146 |
147 | var selectedView: NSView? {
148 | get {
149 | if (selectedRow >= 0) && (selectedRow < numberOfRows) {
150 | return view(atColumn: 0, row: self.selectedRow, makeIfNecessary: false)
151 | } else {
152 | return nil
153 | }
154 | }
155 | }
156 |
157 | func expandAll() {
158 | DispatchQueue.main.async {
159 | self.expandItem(nil, expandChildren: true)
160 | }
161 | }
162 | }
163 |
164 | extension NSView {
165 | func pinEdges(to other: NSView, offset: CGFloat = 0, animate: Bool = false) {
166 | if animate {
167 | animator().leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true
168 | } else {
169 | leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true
170 | trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
171 | topAnchor.constraint(equalTo: other.topAnchor).isActive = true
172 | bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
173 | }
174 | }
175 | }
176 |
177 | extension NSWindow {
178 | func toolbarHeight() -> CGFloat {
179 | if let windowFrameHeight = contentView?.frame.height {
180 | let contentLayoutRectHeight = contentLayoutRect.height
181 | let fullSizeContentViewNoContentAreaHeight = windowFrameHeight - contentLayoutRectHeight
182 | return fullSizeContentViewNoContentAreaHeight
183 | }
184 |
185 | return 0
186 | }
187 | }
188 |
189 | extension String {
190 | // I should really just stop using these and switch to one of the better, full-featured attributed string
191 | // libraries, but meh. This stuff works for now.
192 | func boldString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString {
193 | let attrStr = NSMutableAttributedString(string: self)
194 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self))
195 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self))
196 | return attrStr
197 | }
198 |
199 | func coloredAttributedString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString {
200 | let attrStr = NSMutableAttributedString(string: self)
201 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self))
202 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.systemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self))
203 | return attrStr
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Capture/Controllers/CaptureWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureWindowController.swift
3 | // Capture
4 | //
5 | // Created by Tyler Hall on 4/18/21.
6 | //
7 |
8 | import AppKit
9 | import CoreWLAN
10 |
11 | class CaptureWindowController: NSWindowController {
12 |
13 | static let shared = CaptureWindowController(windowNibName: String(describing: CaptureWindowController.self))
14 |
15 | @IBOutlet weak var summaryTextField: NSTextField!
16 | @IBOutlet weak var detailsTextView: NSTextView!
17 | @IBOutlet weak var dropView: DroppableView!
18 | @IBOutlet weak var attachmentsTableView: NSTableView!
19 | @IBOutlet weak var screenshotCheckbox: NSButton!
20 | @IBOutlet weak var activeBrowserTabCheckbox: NSButton!
21 | @IBOutlet weak var allBrowserTabsCheckbox: NSButton!
22 |
23 | var attachments = [URL]()
24 |
25 | override func windowDidLoad() {
26 | super.windowDidLoad()
27 |
28 | detailsTextView.font = NSFont.systemFont(ofSize: 16)
29 | detailsTextView.textContainerInset = NSSize(width: 10, height: 10)
30 |
31 | setupDragging()
32 | }
33 |
34 | override func showWindow(_ sender: Any?) {
35 | super.showWindow(sender)
36 | restoreDefaults()
37 | }
38 |
39 | func restoreDefaults() {
40 | summaryTextField.stringValue = ""
41 | detailsTextView.string = ""
42 | screenshotCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureScreenshot) ? .on : .off
43 | activeBrowserTabCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureActiveTab) ? .on : .off
44 | allBrowserTabsCheckbox.state = UserDefaults.standard.bool(forKey: kCaptureAllTabs) ? .on : .off
45 | attachments.removeAll()
46 | attachmentsTableView.reloadData()
47 | window?.makeFirstResponder(summaryTextField)
48 | }
49 |
50 | @IBAction func focusSummaryTextField(_ sender: AnyObject?) {
51 | window?.makeFirstResponder(summaryTextField)
52 | }
53 |
54 | @IBAction func checkScreenshot(_ sender: AnyObject?) {
55 | screenshotCheckbox.state = (screenshotCheckbox.state == .on) ? .off : .on
56 | }
57 |
58 | @IBAction func checkActiveBrowserTab(_ sender: AnyObject?) {
59 | if activeBrowserTabCheckbox.state == .on {
60 | activeBrowserTabCheckbox.state = .off
61 | } else {
62 | activeBrowserTabCheckbox.state = .on
63 | allBrowserTabsCheckbox.state = .off
64 | }
65 | }
66 |
67 | @IBAction func checkAllBrowserTabs(_ sender: AnyObject?) {
68 | if allBrowserTabsCheckbox.state == .on {
69 | allBrowserTabsCheckbox.state = .off
70 | } else {
71 | allBrowserTabsCheckbox.state = .on
72 | activeBrowserTabCheckbox.state = .off
73 | }
74 | }
75 |
76 | @IBAction func activeBrowserCheckboxClicked(_ sender: AnyObject?) {
77 | if activeBrowserTabCheckbox.state == .on {
78 | allBrowserTabsCheckbox.state = .off
79 | }
80 | }
81 |
82 | @IBAction func allBrowsersCheckboxClicked(_ sender: AnyObject?) {
83 | if allBrowserTabsCheckbox.state == .on {
84 | activeBrowserTabCheckbox.state = .off
85 | }
86 | }
87 |
88 | @IBAction func saveButtonClicked(_ sender: AnyObject?) {
89 | window?.close()
90 | NSApp.hide(nil)
91 |
92 | let settings = CaptureThing.Settings(summary: summaryTextField.stringValue,
93 | details: detailsTextView.string,
94 | takeScreenshot: (screenshotCheckbox.state == .on),
95 | activeBrowserTab: (activeBrowserTabCheckbox.state == .on),
96 | allBrowserTabs: (allBrowserTabsCheckbox.state == .on),
97 | attachments: attachments)
98 |
99 | // Delay to the next run loop so the window closes before screenshots are taken.
100 | DispatchQueue.main.async {
101 | CaptureThing.shared.capture(settings: settings)
102 | }
103 | }
104 |
105 | @IBAction func cancelButtonClicked(_ sender: AnyObject?) {
106 | window?.close()
107 | NSApp.hide(nil)
108 | }
109 |
110 | func takeQuickScreenshot() {
111 | window?.close()
112 | NSApp.hide(nil)
113 |
114 | let settings = CaptureThing.Settings(summary: "Quick Screenshot",
115 | details: "",
116 | takeScreenshot: true,
117 | activeBrowserTab: false,
118 | allBrowserTabs: false,
119 | attachments: [])
120 |
121 | // Delay to the next run loop so the window closes before screenshots are taken.
122 | DispatchQueue.main.async {
123 | CaptureThing.shared.capture(settings: settings)
124 | }
125 | }
126 | }
127 |
128 | protocol DragDelegate: AnyObject {
129 | func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
130 | func draggingExited(_ sender: NSDraggingInfo?)
131 | func draggingEnded(_ sender: NSDraggingInfo)
132 | func performDragOperation(_ sender: NSDraggingInfo) -> Bool
133 | }
134 |
135 | extension CaptureWindowController: DragDelegate {
136 |
137 | func setupDragging() {
138 | dropView.dragDelegate = self
139 | }
140 |
141 | func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
142 | return .copy
143 | }
144 |
145 | func draggingExited(_ sender: NSDraggingInfo?) {
146 |
147 | }
148 |
149 | func draggingEnded(_ sender: NSDraggingInfo) {
150 |
151 | }
152 |
153 | func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
154 | let supportedClasses = [NSFilePromiseReceiver.self, NSURL.self]
155 | let searchOptions: [NSPasteboard.ReadingOptionKey: Any] = [.urlReadingFileURLsOnly: true]
156 |
157 | sender.enumerateDraggingItems(options: [], for: nil, classes: supportedClasses, searchOptions: searchOptions) { [weak self] (draggingItem, _, _) in
158 | guard let self = self else { return }
159 | switch draggingItem.item {
160 | case let filePromiseReceiver as NSFilePromiseReceiver:
161 | filePromiseReceiver.receivePromisedFiles(atDestination: CaptureThing.shared.dropDestinationURL, options: [:],
162 | operationQueue: CaptureThing.shared.workQueue) { (fileURL, error) in
163 | if error == nil {
164 | self.attachments.append(fileURL)
165 | DispatchQueue.main.async { [weak self] in
166 | self?.attachmentsTableView.reloadData()
167 | }
168 | }
169 | }
170 | case let fileURL as URL:
171 | do {
172 | let newURL = CaptureThing.shared.dropDestinationURL.appendingPathComponent(fileURL.lastPathComponent)
173 | try FileManager.default.copyItem(at: fileURL, to: newURL)
174 | self.attachments.append(newURL)
175 | DispatchQueue.main.async { [weak self] in
176 | self?.attachmentsTableView.reloadData()
177 | }
178 | } catch {
179 | return
180 | }
181 | default:
182 | break
183 | }
184 | }
185 |
186 | return true
187 | }
188 | }
189 |
190 | extension CaptureWindowController: NSTableViewDataSource, NSTableViewDelegate {
191 |
192 | func numberOfRows(in tableView: NSTableView) -> Int {
193 | return attachments.count
194 | }
195 |
196 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
197 | guard attachments.indices.contains(row) else { return nil }
198 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("cell"), owner: nil) as? NSTableCellView else { return nil }
199 | cell.textField?.stringValue = attachments[row].lastPathComponent
200 | return cell
201 | }
202 | }
203 |
204 |
205 | class DroppableView: NSView {
206 | weak var dragDelegate: DragDelegate?
207 |
208 | override func awakeFromNib() {
209 | super.awakeFromNib()
210 | registerForDraggedTypes([.fileURL])
211 | }
212 |
213 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
214 | return dragDelegate?.draggingEntered(sender) ?? NSDragOperation.init()
215 | }
216 |
217 | override func draggingExited(_ sender: NSDraggingInfo?) {
218 | dragDelegate?.draggingExited(sender)
219 | }
220 |
221 | override func draggingEnded(_ sender: NSDraggingInfo) {
222 | dragDelegate?.draggingEnded(sender)
223 | }
224 |
225 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
226 | dragDelegate?.performDragOperation(sender) ?? false
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/Capture/CaptureThing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureThing.swift
3 | // Capture
4 | //
5 | // Created by Tyler Hall on 4/18/21.
6 | //
7 |
8 | import Foundation
9 | import CoreWLAN
10 | import CoreGraphics
11 | import AVFoundation
12 | import AppKit
13 |
14 | class CaptureThing {
15 |
16 | static let shared = CaptureThing()
17 |
18 | lazy var getAllTabsScriptSafari: NSAppleScript = {
19 | let script = """
20 | set outList to {}
21 | tell application "Safari"
22 | repeat with w in windows
23 | repeat with t in (tabs of w)
24 | set theTitle to name of t
25 | copy theTitle to end of outList
26 | set theURL to URL of t
27 | copy theURL to end of outList
28 | end repeat
29 | end repeat
30 | end tell
31 |
32 | return outList
33 | """
34 | let appleScript = NSAppleScript(source: script)
35 | appleScript?.compileAndReturnError(nil)
36 | return appleScript!
37 | }()
38 |
39 | lazy var getActiveTabsScriptSafari: NSAppleScript = {
40 | let script = """
41 | set outList to {}
42 | tell application "Safari"
43 | set theTitle to the name of current tab of first window
44 | copy theTitle to end of outList
45 | set theURL to the URL of current tab of first window
46 | copy theURL to end of outList
47 | end tell
48 |
49 | return outList
50 | """
51 | let appleScript = NSAppleScript(source: script)
52 | appleScript?.compileAndReturnError(nil)
53 | return appleScript!
54 | }()
55 |
56 | lazy var getAllTabsScriptChrome: NSAppleScript = {
57 | let script = """
58 | set outList to {}
59 |
60 | tell application "Google Chrome"
61 | repeat with w in windows
62 | repeat with t in (tabs of w)
63 | set theTitle to title of t
64 | copy theTitle to end of outList
65 | set theURL to URL of t
66 | copy theURL to end of outList
67 | end repeat
68 | end repeat
69 | end tell
70 |
71 | return outList
72 | """
73 | let appleScript = NSAppleScript(source: script)
74 | appleScript?.compileAndReturnError(nil)
75 | return appleScript!
76 | }()
77 |
78 | lazy var getActiveTabsScriptChrome: NSAppleScript = {
79 | let script = """
80 | set outList to {}
81 |
82 | tell application "Google Chrome"
83 | set theTitle to title of active tab of first window
84 | copy theTitle to end of outList
85 | set theURL to URL of active tab of first window
86 | copy theURL to end of outList
87 | end tell
88 |
89 | return outList
90 | """
91 | let appleScript = NSAppleScript(source: script)
92 | appleScript?.compileAndReturnError(nil)
93 | return appleScript!
94 | }()
95 |
96 | lazy var getAllTabsScriptBrave: NSAppleScript = {
97 | let script = """
98 | set outList to {}
99 |
100 | tell application "Brave Browser"
101 | repeat with w in windows
102 | repeat with t in (tabs of w)
103 | set theTitle to title of t
104 | copy theTitle to end of outList
105 | set theURL to URL of t
106 | copy theURL to end of outList
107 | end repeat
108 | end repeat
109 | end tell
110 |
111 | return outList
112 | """
113 | let appleScript = NSAppleScript(source: script)
114 | appleScript?.compileAndReturnError(nil)
115 | return appleScript!
116 | }()
117 |
118 | lazy var getActiveTabScriptBrave: NSAppleScript = {
119 | let script = """
120 | set outList to {}
121 |
122 | tell application "Brave Browser"
123 | set theTitle to title of active tab of first window
124 | copy theTitle to end of outList
125 | set theURL to URL of active tab of first window
126 | copy theURL to end of outList
127 | end tell
128 |
129 | return outList
130 | """
131 | let appleScript = NSAppleScript(source: script)
132 | appleScript?.compileAndReturnError(nil)
133 | return appleScript!
134 | }()
135 |
136 | enum Browser: Int {
137 | case Safari = 0
138 | case Brave = 1
139 | case Chrome = 2
140 | }
141 |
142 | struct Settings {
143 | var summary: String
144 | var details: String
145 | var takeScreenshot: Bool
146 | var activeBrowserTab: Bool
147 | var allBrowserTabs: Bool
148 | var attachments: [URL]
149 | }
150 |
151 | var hostname: String?
152 |
153 | var currentFileURL: URL? {
154 | guard let captureFolderURL = UserDefaults.standard.url(forKey: kCapturePath) else { return nil }
155 |
156 | let dfDir = DateFormatter()
157 | dfDir.dateFormat = "yyyy/MM/"
158 | let dateDir = dfDir.string(from: Date())
159 |
160 | let dfFilename = DateFormatter()
161 | dfFilename.dateFormat = "yyyy-MM-dd EEEE"
162 | let dateFilename = dfFilename.string(from: Date())
163 |
164 | var url = captureFolderURL.appendingPathComponent(dateDir)
165 |
166 | if !FileManager.default.fileExists(atPath: url.path) {
167 | do {
168 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
169 | } catch {
170 | return nil
171 | }
172 | }
173 |
174 | url.appendPathComponent(dateFilename)
175 | url.appendPathExtension("md")
176 | if !FileManager.default.fileExists(atPath: url.path) {
177 | do {
178 | try ("" as NSString).write(to: url, atomically: false, encoding: String.Encoding.utf8.rawValue)
179 | } catch {
180 | return nil
181 | }
182 | }
183 |
184 | return url
185 | }
186 |
187 | var attachmentsDir: URL? {
188 | guard let currentFileURL = currentFileURL else { return nil }
189 |
190 | var dir = currentFileURL.deletingLastPathComponent()
191 | dir.appendPathComponent("attachments")
192 |
193 | if !FileManager.default.fileExists(atPath: dir.path) {
194 | do {
195 | try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false, attributes: nil)
196 | } catch {
197 | return nil
198 | }
199 | }
200 |
201 | return dir
202 | }
203 |
204 | var dropDestinationURL: URL {
205 | let destinationURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString)
206 | try? FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
207 | return destinationURL
208 | }
209 |
210 | var workQueue: OperationQueue = {
211 | let providerQueue = OperationQueue()
212 | providerQueue.qualityOfService = .userInitiated
213 | return providerQueue
214 | }()
215 |
216 | init() {
217 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in
218 | self?.hostname = Host.current().name
219 | }
220 | }
221 | }
222 |
223 | extension CaptureThing {
224 |
225 | func capture(settings: Settings) {
226 | var outStr = makeHeader(summary: settings.summary, details: settings.details)
227 |
228 | if settings.takeScreenshot {
229 | outStr += makeScreenshot()
230 | }
231 |
232 | outStr += makeAttachments(attachments: settings.attachments)
233 |
234 | if settings.allBrowserTabs {
235 | outStr += makeAllBrowserTabs()
236 | } else if settings.activeBrowserTab {
237 | outStr += makeActiveBrowserTab()
238 | }
239 |
240 | outStr += "### Misc Info\n"
241 | outStr += makeTimestamp()
242 | outStr += makeWifi()
243 | outStr += makeHostname()
244 |
245 | outStr += "\n* * * * *\n\n"
246 |
247 | writeToFile(str: outStr)
248 | }
249 |
250 | func makeHeader(summary: String, details: String) -> String {
251 | let df = DateFormatter()
252 | df.dateStyle = .none
253 | df.timeStyle = .short
254 | let dateStr = df.string(from: Date())
255 |
256 | let trimmedSummary = summary.trimmingCharacters(in: .whitespacesAndNewlines)
257 |
258 | var outStr = "# \(dateStr)\n"
259 | outStr += trimmedSummary + "\n"
260 | outStr += "----------\n"
261 |
262 | if details.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
263 | outStr += details + "\n\n"
264 | }
265 |
266 | return outStr
267 | }
268 |
269 | func makeScreenshot() -> String {
270 | guard let attachmentsDir = attachmentsDir else { return "" }
271 |
272 | var outStr = ""
273 | if let filenames = takeScreenshots(folderURL: attachmentsDir), filenames.count > 0 {
274 | var i = 1
275 | for filename in filenames {
276 | if let escaped = filename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
277 | outStr += ")\n"
278 | i += 1
279 | }
280 | }
281 | outStr += "\n"
282 | }
283 | return outStr
284 | }
285 |
286 | func makeAttachments(attachments: [URL]) -> String {
287 | guard let attachmentsDir = attachmentsDir else { return "" }
288 | guard attachments.count > 0 else { return "" }
289 |
290 | let unixTimestamp = Int32(Date().timeIntervalSince1970)
291 | let sortingDF = DateFormatter()
292 | sortingDF.dateFormat = "yyyy-MM-dd"
293 | let sortingKey = sortingDF.string(from: Date())
294 |
295 | var outStr = "### Attachments\n"
296 | for srcURL in attachments {
297 | do {
298 | let fn = ("\(sortingKey) \(unixTimestamp)" + srcURL.lastPathComponent)
299 | if let escaped = fn.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
300 | var destURL = attachmentsDir
301 | destURL.appendPathComponent(fn)
302 | try FileManager.default.copyItem(at: srcURL, to: destURL)
303 | outStr += "* [\(srcURL.lastPathComponent)](attachments/\(escaped))\n"
304 | }
305 | } catch {
306 |
307 | }
308 | }
309 |
310 | outStr += "\n"
311 |
312 | return outStr
313 | }
314 |
315 | func makeWifi() -> String {
316 | if let ssid = CWWiFiClient.shared().interface()?.ssid() {
317 | return "*WiFi: \(ssid)*\n"
318 | } else {
319 | return ""
320 | }
321 | }
322 |
323 | func makeHostname() -> String {
324 | if let hostname = hostname {
325 | return "*Computer: \(hostname)*\n"
326 | } else {
327 | return ""
328 | }
329 | }
330 |
331 | func makeTimestamp() -> String {
332 | let df = ISO8601DateFormatter()
333 | let dateStr = df.string(from: Date())
334 | return "*Timestamp: \(dateStr)*\n"
335 | }
336 |
337 | func makeActiveBrowserTab() -> String {
338 | guard let browser = Browser(rawValue: UserDefaults.standard.integer(forKey: kBrowser)) else { return "" }
339 |
340 | var eventDescriptor: NSAppleEventDescriptor
341 | switch browser {
342 | case .Safari:
343 | eventDescriptor = getActiveTabsScriptSafari.executeAndReturnError(nil)
344 | case .Brave:
345 | eventDescriptor = getActiveTabScriptBrave.executeAndReturnError(nil)
346 | case .Chrome:
347 | eventDescriptor = getActiveTabsScriptChrome.executeAndReturnError(nil)
348 | }
349 |
350 | guard let listDescriptor = eventDescriptor.coerce(toDescriptorType: typeAEList) else { return "" }
351 | guard listDescriptor.numberOfItems > 0 else { return "" }
352 |
353 | var outStr = "### Active Browser Tab\n"
354 | for i in stride(from: 1, through: listDescriptor.numberOfItems, by: 2) {
355 | if let title = listDescriptor.atIndex(i)?.stringValue, let url = listDescriptor.atIndex(i + 1)?.stringValue {
356 | outStr += "* [\(title)](\(url))\n"
357 | }
358 | }
359 |
360 | outStr += "\n"
361 |
362 | return outStr
363 | }
364 |
365 | func makeAllBrowserTabs() -> String {
366 | guard let browser = Browser(rawValue: UserDefaults.standard.integer(forKey: kBrowser)) else { return "" }
367 |
368 | var eventDescriptor: NSAppleEventDescriptor
369 | switch browser {
370 | case .Safari:
371 | eventDescriptor = getAllTabsScriptSafari.executeAndReturnError(nil)
372 | case .Brave:
373 | eventDescriptor = getAllTabsScriptBrave.executeAndReturnError(nil)
374 | case .Chrome:
375 | eventDescriptor = getAllTabsScriptChrome.executeAndReturnError(nil)
376 | }
377 |
378 | guard let listDescriptor = eventDescriptor.coerce(toDescriptorType: typeAEList) else { return "" }
379 | guard listDescriptor.numberOfItems > 0 else { return "" }
380 |
381 | var outStr = "### All Browser Tabs\n"
382 | for i in stride(from: 1, through: listDescriptor.numberOfItems, by: 2) {
383 | if let title = listDescriptor.atIndex(i)?.stringValue, let url = listDescriptor.atIndex(i + 1)?.stringValue {
384 | outStr += "* [\(title)](\(url))\n"
385 | }
386 | }
387 |
388 | outStr += "\n"
389 |
390 | return outStr
391 | }
392 |
393 | func writeToFile(str: String) {
394 | guard let fileURL = currentFileURL else { return }
395 |
396 | do {
397 | let fileHandle = try FileHandle(forWritingTo: fileURL)
398 | try fileHandle.seekToEnd()
399 | if let data = str.data(using: .utf8) {
400 | fileHandle.write(data)
401 | }
402 | try fileHandle.close()
403 | } catch {
404 |
405 | }
406 | }
407 | }
408 |
409 | extension CaptureThing {
410 |
411 | func takeScreenshots(folderURL: URL) -> [String]? {
412 | let sortingDF = DateFormatter()
413 | sortingDF.dateFormat = "yyyy-MM-dd"
414 | let sortingKey = sortingDF.string(from: Date())
415 |
416 | var filenames = [String]()
417 |
418 | var displayCount: UInt32 = 0;
419 | var result = CGGetActiveDisplayList(0, nil, &displayCount)
420 | if (result != CGError.success) {
421 | print("error: \(result)")
422 | return nil
423 | }
424 |
425 | let allocated = Int(displayCount)
426 | let activeDisplays = UnsafeMutablePointer.allocate(capacity: allocated)
427 | result = CGGetActiveDisplayList(displayCount, activeDisplays, &displayCount)
428 |
429 | if (result != CGError.success) {
430 | print("error: \(result)")
431 | return nil
432 | }
433 |
434 | for i in 1...displayCount {
435 | let unixTimestamp = Int32(Date().timeIntervalSince1970)
436 |
437 | let baseFilename = "\(sortingKey) - Screenshot \(unixTimestamp)" + "_" + "\(i)"
438 | let filename = baseFilename + ".jpg"
439 |
440 | var fileURL = folderURL.appendingPathComponent(baseFilename)
441 | fileURL.appendPathExtension("jpg")
442 |
443 | let screenShot:CGImage = CGDisplayCreateImage(activeDisplays[Int(i - 1)])!
444 | let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
445 | let jpegData = bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])!
446 |
447 | do {
448 | try jpegData.write(to: fileURL, options: .atomic)
449 | filenames.append(filename)
450 | }
451 | catch {
452 | print("error: \(error)")
453 | }
454 | }
455 |
456 | return filenames
457 | }
458 | }
459 |
--------------------------------------------------------------------------------
/Capture/Controllers/PrefsWindowController.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 |
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 |
104 |
105 |
106 |
107 |
118 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
--------------------------------------------------------------------------------
/Capture/Extensions/Shared/Extensions-Shared.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CryptoKit
3 | import CommonCrypto
4 | import NaturalLanguage
5 |
6 | extension Array {
7 | func shuffled() -> [Element] {
8 | if count < 2 { return self }
9 | var list = self
10 | for i in 0..<(list.count - 1) {
11 | let j = Int(arc4random_uniform(UInt32(list.count - i))) + i
12 | if i != j {
13 | list.swapAt(i, j)
14 | }
15 | }
16 | return list
17 | }
18 |
19 | // Creates an array containing all combinations of two arrays.
20 | static func createAllCombinations(from lhs: Array, and rhs: Array) -> Array<(T, U)> {
21 | let result: [(T, U)] = lhs.reduce([]) { (accum, t) in
22 | let innerResult: [(T, U)] = rhs.reduce([]) { (innerAccum, u) in
23 | return innerAccum + [(t, u)]
24 | }
25 | return accum + innerResult
26 | }
27 | return result
28 | }
29 |
30 | mutating func move(from oldIndex: Index, to newIndex: Index) {
31 | // Don't work for free and use swap when indices are next to each other - this
32 | // won't rebuild array and will be super efficient.
33 | if oldIndex == newIndex { return }
34 | if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) }
35 | self.insert(self.remove(at: oldIndex), at: newIndex)
36 | }
37 | }
38 |
39 | extension Array where Element: Equatable {
40 | mutating func move(_ element: Element, to newIndex: Index) {
41 | if let oldIndex: Int = self.firstIndex(of: element) { self.move(from: oldIndex, to: newIndex) }
42 | }
43 | }
44 |
45 | extension Collection {
46 | subscript(optional i: Index) -> Iterator.Element? {
47 | return self.indices.contains(i) ? self[i] : nil
48 | }
49 | }
50 |
51 | extension Data {
52 | var hexString: String {
53 | let hexString = map { String(format: "%02.2hhx", $0) }.joined()
54 | return hexString
55 | }
56 | }
57 |
58 | extension Date {
59 | static var yesterday: Date { return Date().dayBefore }
60 | static var tomorrow: Date { return Date().dayAfter }
61 |
62 | var dayBefore: Date {
63 | return Calendar.current.date(byAdding: .day, value: -1, to: noon)!
64 | }
65 |
66 | var dayAfter: Date {
67 | return Calendar.current.date(byAdding: .day, value: 1, to: noon)!
68 | }
69 |
70 | var noon: Date {
71 | return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
72 | }
73 |
74 | // let d = Date() -> Mar 12, 2020 at 1:51 PM
75 | // d.stringify() -> "1584039099.486827"
76 | func stringify() -> String {
77 | return timeIntervalSince1970.stringValue()
78 | }
79 |
80 | // Date.unstringify("1584039099.486827") -> Mar 12, 2020 at 1:51 PM
81 | static func unstringify(_ ts: String) -> Date? {
82 | if let dbl = Double(ts) {
83 | return Date(timeIntervalSince1970: dbl)
84 | }
85 | return nil
86 | }
87 |
88 | func addMonth(n: Int) -> Date? {
89 | return Calendar.current.date(byAdding: .month, value: n, to: self)
90 | }
91 |
92 | func addDay(n: Int) -> Date? {
93 | return Calendar.current.date(byAdding: .day, value: n, to: self)
94 | }
95 |
96 | var month: Int {
97 | return Calendar.current.component(.month, from: self)
98 | }
99 |
100 | var year: Int {
101 | return Calendar.current.component(.year, from: self)
102 | }
103 |
104 | var day: Int {
105 | return Calendar.current.component(.day, from: self)
106 | }
107 |
108 | var startOfDay: Date {
109 | return Calendar.current.startOfDay(for: self)
110 | }
111 |
112 | var endOfDay: Date {
113 | var components = DateComponents()
114 | components.day = 1
115 | components.second = -1
116 | return Calendar.current.date(byAdding: components, to: startOfDay)!
117 | }
118 |
119 | var startOfMonth: Date {
120 | let components = Calendar.current.dateComponents([.year, .month], from: startOfDay)
121 | return Calendar.current.date(from: components)!
122 | }
123 |
124 | var endOfMonth: Date {
125 | var components = DateComponents()
126 | components.month = 1
127 | components.second = -1
128 | return Calendar.current.date(byAdding: components, to: startOfMonth)!
129 | }
130 |
131 | func numberOfDaysInMonth() -> Int {
132 | let range = Calendar.current.range(of: .day, in: .month, for: self)!
133 | return range.count
134 | }
135 |
136 | var isLastDayOfMonth: Bool {
137 | return dayAfter.month != month
138 | }
139 |
140 | var midnight: Date {
141 | return Calendar.current.startOfDay(for: self)
142 | }
143 |
144 | var startOfWeek: Date {
145 | return Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self))!
146 | }
147 | }
148 |
149 | extension Dictionary {
150 | // Combines self with another dictionary.
151 | mutating func merge(dict: [Key: Value]){
152 | for (k, v) in dict {
153 | updateValue(v, forKey: k)
154 | }
155 | }
156 | }
157 |
158 | extension Double {
159 | // It's dumb, but I swear I end up having to dump a number into some type
160 | // of storage that only accepts a String way more often than I care to think about.
161 | func stringValue() -> String {
162 | return String(format:"%f", self)
163 | }
164 |
165 | func formatAsCurrency() -> String {
166 | let nf = NumberFormatter()
167 | nf.numberStyle = .currency
168 | return nf.string(from: NSNumber(floatLiteral: self))!
169 | }
170 | }
171 |
172 | extension FileManager {
173 | // Given a basename such as "My Picture" and fileExtension "jpg",
174 | // it will produce a unique, seqential filename such as "My Picuture 1.jpg"
175 | // that does not exist in directoryURL. If the directory already contained
176 | // "My Picuture.jpg", "My Picuture 1.jpg", "My Picuture 2.jpg", this will
177 | // return "My Picuture 3.jpg".
178 | func uniqueFileURL(directoryURL: URL, basename: String, fileExtension: String?) -> URL {
179 | var fullPathURL = directoryURL.appendingPathComponent(basename)
180 | if let ext = fileExtension {
181 | fullPathURL = fullPathURL.appendingPathExtension(ext)
182 | }
183 |
184 | var i = 0
185 | while FileManager.default.fileExists(atPath: fullPathURL.path) {
186 | i += 1
187 | let newBasename = "\(basename) \(i)"
188 | fullPathURL = directoryURL.appendingPathComponent(newBasename)
189 | if let ext = fileExtension {
190 | fullPathURL = fullPathURL.appendingPathExtension(ext)
191 | }
192 | }
193 |
194 | return fullPathURL
195 | }
196 | }
197 |
198 | extension NSAttributedString {
199 | // Haphazard solution that returns the range of a line of text at a given position,
200 | // where a line is delimited by newlines.
201 | func rangeOfLineAtLocation(_ location: Int) -> NSRange {
202 | if string.character(location).isNewline {
203 | var start = location
204 | while(start > 0 && !string.character(start - 1).isNewline) {
205 | start -= 1
206 | }
207 | return NSMakeRange(start, location-start)
208 | }
209 |
210 | var start: Int = location
211 | while(start > 0 && !string.character(start - 1).isNewline) {
212 | start -= 1
213 | }
214 |
215 | var end = location
216 | while(end < string.count - 1 && !string.character(end + 1).isNewline) {
217 | end += 1
218 | }
219 |
220 | return NSMakeRange(start, end - start)
221 | }
222 |
223 | func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString {
224 | let modifiedString = NSMutableAttributedString(attributedString: self)
225 | modifiedString.trimCharactersInSet(charSet: charSet)
226 | return NSAttributedString(attributedString: modifiedString)
227 | }
228 | }
229 |
230 | extension NSMutableAttributedString {
231 | func trimCharactersInSet(charSet: CharacterSet) {
232 | var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet)
233 |
234 | // Trim leading characters from character set.
235 | while range.length != 0 && range.location == 0 {
236 | replaceCharacters(in: range, with: "")
237 | range = (string as NSString).rangeOfCharacter(from: charSet)
238 | }
239 |
240 | // Trim trailing characters from character set.
241 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
242 | while range.length != 0 && NSMaxRange(range) == length {
243 | replaceCharacters(in: range, with: "")
244 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
245 | }
246 | }
247 | }
248 |
249 | extension NSNotification.Name {
250 | func post(_ object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
251 | NotificationCenter.default.post(name: self, object: object, userInfo: userInfo)
252 | }
253 | }
254 |
255 | extension NSRange {
256 | init?(string: String, lowerBound: String.Index, upperBound: String.Index) {
257 | let utf16 = string.utf16
258 |
259 | if let lowerBound = lowerBound.samePosition(in: utf16), let upperBound = upperBound.samePosition(in: utf16) {
260 | let location = utf16.distance(from: utf16.startIndex, to: lowerBound)
261 | let length = utf16.distance(from: lowerBound, to: upperBound)
262 |
263 | self.init(location: location, length: length)
264 | }
265 | return nil
266 | }
267 |
268 | init?(range: Range, in string: String) {
269 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
270 | }
271 |
272 | init?(range: ClosedRange, in string: String) {
273 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
274 | }
275 | }
276 |
277 | extension String {
278 | // An incredibly lenient and forgiving way to get a numeric String
279 | // out of another string - typically one provided by the user.
280 | // You shouldn't rely on this for anything truly mission critical.
281 | func numberString() -> String? {
282 | let strippedStr = trimmingCharacters(in: .whitespacesAndNewlines)
283 | let isNegative = strippedStr.hasPrefix("-")
284 | let allowedCharSet = CharacterSet(charactersIn: ".,0123456789")
285 | let filteredStr = components(separatedBy: allowedCharSet.inverted).joined()
286 | if (count(of: ".") + count(of: ",")) > 1 { return nil }
287 | return (isNegative ? "-" : "") + filteredStr
288 | }
289 |
290 | // Number of times a character occurs within a string.
291 | func count(of needle: Character) -> Int {
292 | return reduce(0) {
293 | $1 == needle ? $0 + 1 : $0
294 | }
295 | }
296 |
297 | // Returns an array of substrings delimited by whitespace - and also
298 | // combines tokens inside matching quotes into a single token. I don't
299 | // claim this to be pefect in every edge case - but I haven't encountered
300 | // a bug yet 🤷♀️.
301 | // "My name is Tim Apple".tokenize() -> ["My", "name", "is", "Tim", "Apple"]
302 | // "I hope the \"SF Giants\" have a \"better season\" this year" -> ["I", "hope", "the", "SF Giants", "have", "a", "better season", "this", "year"]
303 | @available(OSX 10.15, iOS 13, *)
304 | func tokenize() -> [String] {
305 | enum State {
306 | case Normal
307 | case InsideAQuote
308 | }
309 |
310 | let theString = self.trimmingCharacters(in: .whitespacesAndNewlines)
311 |
312 | var tokens = [String]()
313 | var state = State.Normal
314 | let delimeters = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "\""))
315 | let quote = CharacterSet(charactersIn: "\"")
316 |
317 | let scanner = Scanner(string: theString)
318 | scanner.charactersToBeSkipped = .none
319 |
320 | while !scanner.isAtEnd {
321 | if state == .Normal {
322 | if let token = scanner.scanCharacters(from: delimeters.inverted) {
323 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines))
324 | } else if let delims = scanner.scanCharacters(from: delimeters) {
325 | if delims.hasSuffix("\"") {
326 | state = .InsideAQuote
327 | }
328 | }
329 | } else {
330 | if let token = scanner.scanCharacters(from: quote.inverted) {
331 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines))
332 | state = .Normal
333 | }
334 | }
335 | }
336 |
337 | return tokens
338 | }
339 |
340 | func number() -> NSNumber? {
341 | let amountStr = self.trimmingCharacters(in: .whitespacesAndNewlines)
342 |
343 | let nf = NumberFormatter()
344 |
345 | nf.numberStyle = .currency
346 | if let num = nf.number(from: amountStr) {
347 | return num
348 | }
349 |
350 | nf.numberStyle = .none
351 | if let num = nf.number(from: amountStr) {
352 | return num
353 | }
354 |
355 | return nil
356 | }
357 |
358 | func formatAsCurrency() -> String? {
359 | return number()?.doubleValue.formatAsCurrency()
360 | }
361 |
362 | func substring(to : Int) -> String {
363 | let toIndex = self.index(self.startIndex, offsetBy: to)
364 | return String(self[...toIndex])
365 | }
366 |
367 | func substring(from : Int) -> String {
368 | let fromIndex = self.index(self.startIndex, offsetBy: from)
369 | return String(self[fromIndex...])
370 | }
371 |
372 | func substring(_ r: Range) -> String {
373 | let fromIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
374 | let toIndex = self.index(self.startIndex, offsetBy: r.upperBound)
375 | let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex))
376 | return String(self[indexRange])
377 | }
378 |
379 | func character(_ at: Int) -> Character {
380 | return self[self.index(self.startIndex, offsetBy: at)]
381 | }
382 |
383 | func lastIndexOfCharacter(_ c: Character) -> Int? {
384 | guard let index = range(of: String(c), options: .backwards)?.lowerBound else { return nil }
385 | return distance(from: startIndex, to: index)
386 | }
387 |
388 | func deletingPrefix(_ prefix: String) -> String {
389 | guard self.hasPrefix(prefix) else { return self }
390 | return String(self.dropFirst(prefix.count))
391 | }
392 |
393 | func deletingSuffix(_ suffix: String) -> String {
394 | guard self.hasSuffix(suffix) else { return self }
395 | return String(self.dropLast(suffix.count))
396 | }
397 |
398 | func lowerTrimmed() -> String {
399 | return self.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
400 | }
401 |
402 | func keyVal() -> (String, String?)? {
403 | if let colonIndex = self.firstIndex(of: ":") {
404 | let key = self[.. [[String]] {
419 | guard let regex = try? NSRegularExpression(pattern: regex, options: [.dotMatchesLineSeparators]) else { return [] }
420 | let nsString = self as NSString
421 | let results = regex.matches(in: self, options: [.withoutAnchoringBounds], range: NSMakeRange(0, nsString.length))
422 | return results.map { result in
423 | (0.. String? {
433 | guard let leftRange = range(of: left) else { return nil }
434 | guard let rightRange = range(of: right, options: .backwards) else { return nil }
435 | guard leftRange.upperBound <= rightRange.lowerBound else { return nil }
436 |
437 | let sub = self[leftRange.upperBound...]
438 | let closestToLeftRange = sub.range(of: right)!
439 | return String(sub[..) -> NSRange {
443 | guard let lower = UTF16View.Index(range.lowerBound, within: utf16) else { return .init() }
444 | guard let upper = UTF16View.Index(range.upperBound, within: utf16) else { return .init() }
445 | return NSRange(location: utf16.distance(from: utf16.startIndex, to: lower), length: utf16.distance(from: lower, to: upper))
446 | }
447 |
448 | func lemmatize() -> [String] {
449 | let tagger = NSLinguisticTagger(tagSchemes: [.lemma], options: 0)
450 | tagger.string = self
451 | let range = NSMakeRange(0, self.utf16.count)
452 | let options: NSLinguisticTagger.Options = [.omitWhitespace, .omitPunctuation]
453 |
454 | var results = [String]()
455 | tagger.enumerateTags(in: range, unit: .word, scheme: .lemma, options: options) { (tag, tokenRange, stop) in
456 | if let lemma = tag?.rawValue {
457 | results.append(lemma)
458 | }
459 | }
460 |
461 | return (results.count > 0) ? [self] : results
462 | }
463 | }
464 |
465 | extension String {
466 | struct Summary {
467 | var characters = 0
468 | var nonWhitespaceCharacters = 0
469 | var words = 0
470 | var sentences = 0
471 | var lines = 0
472 | var averageReadingTime: TimeInterval = 0
473 | var aloudReadingTime: TimeInterval = 0
474 | }
475 |
476 | var summary: Summary {
477 | var s = Summary(characters: countCharacters(),
478 | nonWhitespaceCharacters: countNonWhitespaceCharacters(),
479 | words: countWords(),
480 | sentences: countSentences(),
481 | lines: countLines())
482 | s.averageReadingTime = calculateAverageReadingTime(words: s.words)
483 | s.aloudReadingTime = calculateAloudReadingTime(words: s.words)
484 | return s
485 | }
486 |
487 | // https://digest.bps.org.uk/2019/06/13/most-comprehensive-review-to-date-suggests-the-average-persons-reading-speed-is-slower-than-commonly-thought/
488 | func calculateAverageReadingTime(words: Int) -> TimeInterval {
489 | return TimeInterval(words) / 238.0 * 60.0
490 | }
491 |
492 | // https://www.visualthesaurus.com/cm/wc/seven-ways-to-write-a-better-speech/
493 | func calculateAloudReadingTime(words: Int) -> TimeInterval {
494 | return TimeInterval(words) / 125.0 * 60.0
495 | }
496 |
497 | func countCharacters() -> Int {
498 | return count
499 | }
500 |
501 | func countNonWhitespaceCharacters() -> Int {
502 | components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().count
503 | }
504 |
505 | func countWords() -> Int {
506 | let words = components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
507 | return words.count
508 | }
509 |
510 | func countSentences() -> Int {
511 | let s = self
512 | var r = [Range]()
513 | let t = s.linguisticTags(in: s.startIndex.. Int {
526 | return components(separatedBy: .newlines).count
527 | }
528 | }
529 |
530 | extension TimeInterval {
531 | // let foo: TimeInterval = 6227
532 | // foo.durationString() -> "2h"
533 | // foo.durationString(2) -> "1h 44m"
534 | // foo.durationString(3) -> "1h 43m 47s"
535 | func durationString() -> String {
536 | let formatter = DateComponentsFormatter()
537 | formatter.allowedUnits = [.day, .hour, .minute, .second]
538 | formatter.unitsStyle = .abbreviated
539 | formatter.maximumUnitCount = 1
540 | return formatter.string(from: self)!
541 | }
542 | }
543 |
544 | extension URL {
545 | var isAccessibleFile: Bool {
546 | var isDir: ObjCBool = false
547 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
548 | if !exists || isDir.boolValue {
549 | return false
550 | }
551 | return FileManager.default.isReadableFile(atPath: self.path)
552 | }
553 |
554 | var isAccessibleDirectory: Bool {
555 | var isDir: ObjCBool = false
556 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
557 | if !exists || !isDir.boolValue {
558 | return false
559 | }
560 | return FileManager.default.isReadableFile(atPath: self.path)
561 | }
562 |
563 | func dumbGET(_ results: ((Data?) -> ())? = nil) {
564 | DispatchQueue.global(qos: .userInitiated).async {
565 | do {
566 | let data = try Data(contentsOf: self)
567 | results?(data)
568 | } catch {
569 | results?(nil)
570 | }
571 | }
572 | }
573 | }
574 |
--------------------------------------------------------------------------------
/Capture.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 57C7AC60AA9E46BA1643DF39 /* Pods_Capture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */; };
11 | C636F6BD2747D53A00731E9F /* Extensions-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6B92747D53A00731E9F /* Extensions-macOS.swift */; };
12 | C636F6BE2747D53A00731E9F /* Extensions-Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */; };
13 | C636F6BF2747D53A00731E9F /* Extensions-Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */; };
14 | C636F6FD2747ED6A00731E9F /* PrefsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */; };
15 | C636F6FF2747ED8700731E9F /* PrefsWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */; };
16 | C6672AF927475FB000D616B6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672AF827475FB000D616B6 /* AppDelegate.swift */; };
17 | C6672AFB27475FB100D616B6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C6672AFA27475FB100D616B6 /* Assets.xcassets */; };
18 | C6672AFE27475FB100D616B6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6672AFC27475FB100D616B6 /* MainMenu.xib */; };
19 | C6672B0E2747605500D616B6 /* CaptureWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672B0D2747605500D616B6 /* CaptureWindowController.swift */; };
20 | C6672B102747607600D616B6 /* CaptureWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C6672B0F2747607600D616B6 /* CaptureWindowController.xib */; };
21 | C6672B12274767E700D616B6 /* CaptureThing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6672B11274767E700D616B6 /* CaptureThing.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXFileReference section */
25 | 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Capture.framework; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Capture.release.xcconfig"; path = "Target Support Files/Pods-Capture/Pods-Capture.release.xcconfig"; sourceTree = ""; };
27 | 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Capture.debug.xcconfig"; path = "Target Support Files/Pods-Capture/Pods-Capture.debug.xcconfig"; sourceTree = ""; };
28 | C636F6B92747D53A00731E9F /* Extensions-macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-macOS.swift"; sourceTree = ""; };
29 | C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Geometry.swift"; sourceTree = ""; };
30 | C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Shared.swift"; sourceTree = ""; };
31 | C636F6FA2747D7CA00731E9F /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; };
32 | C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsWindowController.swift; sourceTree = ""; };
33 | C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PrefsWindowController.xib; sourceTree = ""; };
34 | C6672AF527475FB000D616B6 /* Capture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Capture.app; sourceTree = BUILT_PRODUCTS_DIR; };
35 | C6672AF827475FB000D616B6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
36 | C6672AFA27475FB100D616B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
37 | C6672AFD27475FB100D616B6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
38 | C6672AFF27475FB100D616B6 /* Capture.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Capture.entitlements; sourceTree = ""; };
39 | C6672B0D2747605500D616B6 /* CaptureWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureWindowController.swift; sourceTree = ""; };
40 | C6672B0F2747607600D616B6 /* CaptureWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CaptureWindowController.xib; sourceTree = ""; };
41 | C6672B11274767E700D616B6 /* CaptureThing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureThing.swift; sourceTree = ""; };
42 | C6672B132747774D00D616B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
43 | /* End PBXFileReference section */
44 |
45 | /* Begin PBXFrameworksBuildPhase section */
46 | C6672AF227475FB000D616B6 /* Frameworks */ = {
47 | isa = PBXFrameworksBuildPhase;
48 | buildActionMask = 2147483647;
49 | files = (
50 | 57C7AC60AA9E46BA1643DF39 /* Pods_Capture.framework in Frameworks */,
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | /* End PBXFrameworksBuildPhase section */
55 |
56 | /* Begin PBXGroup section */
57 | 661301BB148B2A98123B6650 /* Pods */ = {
58 | isa = PBXGroup;
59 | children = (
60 | 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */,
61 | 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */,
62 | );
63 | path = Pods;
64 | sourceTree = "";
65 | };
66 | C24EC9F6EC71B867B6012736 /* Frameworks */ = {
67 | isa = PBXGroup;
68 | children = (
69 | 4AC90FC257387B5EC28985B9 /* Pods_Capture.framework */,
70 | );
71 | name = Frameworks;
72 | sourceTree = "";
73 | };
74 | C636F6B82747D53A00731E9F /* macOS */ = {
75 | isa = PBXGroup;
76 | children = (
77 | C636F6B92747D53A00731E9F /* Extensions-macOS.swift */,
78 | );
79 | path = macOS;
80 | sourceTree = "";
81 | };
82 | C636F6BA2747D53A00731E9F /* Shared */ = {
83 | isa = PBXGroup;
84 | children = (
85 | C636F6BB2747D53A00731E9F /* Extensions-Geometry.swift */,
86 | C636F6BC2747D53A00731E9F /* Extensions-Shared.swift */,
87 | );
88 | path = Shared;
89 | sourceTree = "";
90 | };
91 | C6672AEC27475FB000D616B6 = {
92 | isa = PBXGroup;
93 | children = (
94 | C6672AF727475FB000D616B6 /* Capture */,
95 | C6672AF627475FB000D616B6 /* Products */,
96 | 661301BB148B2A98123B6650 /* Pods */,
97 | C24EC9F6EC71B867B6012736 /* Frameworks */,
98 | );
99 | sourceTree = "";
100 | };
101 | C6672AF627475FB000D616B6 /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | C6672AF527475FB000D616B6 /* Capture.app */,
105 | );
106 | name = Products;
107 | sourceTree = "";
108 | };
109 | C6672AF727475FB000D616B6 /* Capture */ = {
110 | isa = PBXGroup;
111 | children = (
112 | C6672AF827475FB000D616B6 /* AppDelegate.swift */,
113 | C6672B11274767E700D616B6 /* CaptureThing.swift */,
114 | C6EC9C5A274BF1A1001765F9 /* Controllers */,
115 | C6EC9C5B274BF1B7001765F9 /* Extensions */,
116 | C6672AFA27475FB100D616B6 /* Assets.xcassets */,
117 | C6672AFC27475FB100D616B6 /* MainMenu.xib */,
118 | C6672AFF27475FB100D616B6 /* Capture.entitlements */,
119 | C6672B132747774D00D616B6 /* Info.plist */,
120 | C636F6FA2747D7CA00731E9F /* BridgingHeader.h */,
121 | );
122 | path = Capture;
123 | sourceTree = "";
124 | };
125 | C6EC9C5A274BF1A1001765F9 /* Controllers */ = {
126 | isa = PBXGroup;
127 | children = (
128 | C6672B0D2747605500D616B6 /* CaptureWindowController.swift */,
129 | C6672B0F2747607600D616B6 /* CaptureWindowController.xib */,
130 | C636F6FC2747ED6A00731E9F /* PrefsWindowController.swift */,
131 | C636F6FE2747ED8700731E9F /* PrefsWindowController.xib */,
132 | );
133 | path = Controllers;
134 | sourceTree = "";
135 | };
136 | C6EC9C5B274BF1B7001765F9 /* Extensions */ = {
137 | isa = PBXGroup;
138 | children = (
139 | C636F6B82747D53A00731E9F /* macOS */,
140 | C636F6BA2747D53A00731E9F /* Shared */,
141 | );
142 | path = Extensions;
143 | sourceTree = "";
144 | };
145 | /* End PBXGroup section */
146 |
147 | /* Begin PBXNativeTarget section */
148 | C6672AF427475FB000D616B6 /* Capture */ = {
149 | isa = PBXNativeTarget;
150 | buildConfigurationList = C6672B0227475FB100D616B6 /* Build configuration list for PBXNativeTarget "Capture" */;
151 | buildPhases = (
152 | 3A9AE67943FE16424E35D42C /* [CP] Check Pods Manifest.lock */,
153 | C6672AF127475FB000D616B6 /* Sources */,
154 | C6672AF227475FB000D616B6 /* Frameworks */,
155 | C6672AF327475FB000D616B6 /* Resources */,
156 | BFE9C85B6A0E6D8E40BE105D /* [CP] Embed Pods Frameworks */,
157 | );
158 | buildRules = (
159 | );
160 | dependencies = (
161 | );
162 | name = Capture;
163 | productName = Capture;
164 | productReference = C6672AF527475FB000D616B6 /* Capture.app */;
165 | productType = "com.apple.product-type.application";
166 | };
167 | /* End PBXNativeTarget section */
168 |
169 | /* Begin PBXProject section */
170 | C6672AED27475FB000D616B6 /* Project object */ = {
171 | isa = PBXProject;
172 | attributes = {
173 | BuildIndependentTargetsInParallel = 1;
174 | LastSwiftUpdateCheck = 1310;
175 | LastUpgradeCheck = 1310;
176 | TargetAttributes = {
177 | C6672AF427475FB000D616B6 = {
178 | CreatedOnToolsVersion = 13.1;
179 | };
180 | };
181 | };
182 | buildConfigurationList = C6672AF027475FB000D616B6 /* Build configuration list for PBXProject "Capture" */;
183 | compatibilityVersion = "Xcode 13.0";
184 | developmentRegion = en;
185 | hasScannedForEncodings = 0;
186 | knownRegions = (
187 | en,
188 | Base,
189 | de,
190 | "zh-Hans",
191 | ja,
192 | es,
193 | it,
194 | sv,
195 | cs,
196 | ko,
197 | "zh-Hant",
198 | pl,
199 | ru,
200 | fr,
201 | nl,
202 | pt,
203 | );
204 | mainGroup = C6672AEC27475FB000D616B6;
205 | productRefGroup = C6672AF627475FB000D616B6 /* Products */;
206 | projectDirPath = "";
207 | projectRoot = "";
208 | targets = (
209 | C6672AF427475FB000D616B6 /* Capture */,
210 | );
211 | };
212 | /* End PBXProject section */
213 |
214 | /* Begin PBXResourcesBuildPhase section */
215 | C6672AF327475FB000D616B6 /* Resources */ = {
216 | isa = PBXResourcesBuildPhase;
217 | buildActionMask = 2147483647;
218 | files = (
219 | C6672B102747607600D616B6 /* CaptureWindowController.xib in Resources */,
220 | C6672AFB27475FB100D616B6 /* Assets.xcassets in Resources */,
221 | C636F6FF2747ED8700731E9F /* PrefsWindowController.xib in Resources */,
222 | C6672AFE27475FB100D616B6 /* MainMenu.xib in Resources */,
223 | );
224 | runOnlyForDeploymentPostprocessing = 0;
225 | };
226 | /* End PBXResourcesBuildPhase section */
227 |
228 | /* Begin PBXShellScriptBuildPhase section */
229 | 3A9AE67943FE16424E35D42C /* [CP] Check Pods Manifest.lock */ = {
230 | isa = PBXShellScriptBuildPhase;
231 | buildActionMask = 2147483647;
232 | files = (
233 | );
234 | inputFileListPaths = (
235 | );
236 | inputPaths = (
237 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
238 | "${PODS_ROOT}/Manifest.lock",
239 | );
240 | name = "[CP] Check Pods Manifest.lock";
241 | outputFileListPaths = (
242 | );
243 | outputPaths = (
244 | "$(DERIVED_FILE_DIR)/Pods-Capture-checkManifestLockResult.txt",
245 | );
246 | runOnlyForDeploymentPostprocessing = 0;
247 | shellPath = /bin/sh;
248 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
249 | showEnvVarsInLog = 0;
250 | };
251 | BFE9C85B6A0E6D8E40BE105D /* [CP] Embed Pods Frameworks */ = {
252 | isa = PBXShellScriptBuildPhase;
253 | buildActionMask = 2147483647;
254 | files = (
255 | );
256 | inputFileListPaths = (
257 | "${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks-${CONFIGURATION}-input-files.xcfilelist",
258 | );
259 | name = "[CP] Embed Pods Frameworks";
260 | outputFileListPaths = (
261 | "${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks-${CONFIGURATION}-output-files.xcfilelist",
262 | );
263 | runOnlyForDeploymentPostprocessing = 0;
264 | shellPath = /bin/sh;
265 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Capture/Pods-Capture-frameworks.sh\"\n";
266 | showEnvVarsInLog = 0;
267 | };
268 | /* End PBXShellScriptBuildPhase section */
269 |
270 | /* Begin PBXSourcesBuildPhase section */
271 | C6672AF127475FB000D616B6 /* Sources */ = {
272 | isa = PBXSourcesBuildPhase;
273 | buildActionMask = 2147483647;
274 | files = (
275 | C6672AF927475FB000D616B6 /* AppDelegate.swift in Sources */,
276 | C636F6BD2747D53A00731E9F /* Extensions-macOS.swift in Sources */,
277 | C636F6FD2747ED6A00731E9F /* PrefsWindowController.swift in Sources */,
278 | C636F6BF2747D53A00731E9F /* Extensions-Shared.swift in Sources */,
279 | C636F6BE2747D53A00731E9F /* Extensions-Geometry.swift in Sources */,
280 | C6672B12274767E700D616B6 /* CaptureThing.swift in Sources */,
281 | C6672B0E2747605500D616B6 /* CaptureWindowController.swift in Sources */,
282 | );
283 | runOnlyForDeploymentPostprocessing = 0;
284 | };
285 | /* End PBXSourcesBuildPhase section */
286 |
287 | /* Begin PBXVariantGroup section */
288 | C6672AFC27475FB100D616B6 /* MainMenu.xib */ = {
289 | isa = PBXVariantGroup;
290 | children = (
291 | C6672AFD27475FB100D616B6 /* Base */,
292 | );
293 | name = MainMenu.xib;
294 | sourceTree = "";
295 | };
296 | /* End PBXVariantGroup section */
297 |
298 | /* Begin XCBuildConfiguration section */
299 | C6672B0027475FB100D616B6 /* Debug */ = {
300 | isa = XCBuildConfiguration;
301 | buildSettings = {
302 | ALWAYS_SEARCH_USER_PATHS = NO;
303 | CLANG_ANALYZER_NONNULL = YES;
304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
306 | CLANG_CXX_LIBRARY = "libc++";
307 | CLANG_ENABLE_MODULES = YES;
308 | CLANG_ENABLE_OBJC_ARC = YES;
309 | CLANG_ENABLE_OBJC_WEAK = YES;
310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
311 | CLANG_WARN_BOOL_CONVERSION = YES;
312 | CLANG_WARN_COMMA = YES;
313 | CLANG_WARN_CONSTANT_CONVERSION = YES;
314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
317 | CLANG_WARN_EMPTY_BODY = YES;
318 | CLANG_WARN_ENUM_CONVERSION = YES;
319 | CLANG_WARN_INFINITE_RECURSION = YES;
320 | CLANG_WARN_INT_CONVERSION = YES;
321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
325 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
327 | CLANG_WARN_STRICT_PROTOTYPES = YES;
328 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
329 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
330 | CLANG_WARN_UNREACHABLE_CODE = YES;
331 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
332 | COPY_PHASE_STRIP = NO;
333 | DEBUG_INFORMATION_FORMAT = dwarf;
334 | ENABLE_STRICT_OBJC_MSGSEND = YES;
335 | ENABLE_TESTABILITY = YES;
336 | GCC_C_LANGUAGE_STANDARD = gnu11;
337 | GCC_DYNAMIC_NO_PIC = NO;
338 | GCC_NO_COMMON_BLOCKS = YES;
339 | GCC_OPTIMIZATION_LEVEL = 0;
340 | GCC_PREPROCESSOR_DEFINITIONS = (
341 | "DEBUG=1",
342 | "$(inherited)",
343 | );
344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
346 | GCC_WARN_UNDECLARED_SELECTOR = YES;
347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
348 | GCC_WARN_UNUSED_FUNCTION = YES;
349 | GCC_WARN_UNUSED_VARIABLE = YES;
350 | MACOSX_DEPLOYMENT_TARGET = 12.0;
351 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
352 | MTL_FAST_MATH = YES;
353 | ONLY_ACTIVE_ARCH = YES;
354 | SDKROOT = macosx;
355 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
356 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
357 | };
358 | name = Debug;
359 | };
360 | C6672B0127475FB100D616B6 /* Release */ = {
361 | isa = XCBuildConfiguration;
362 | buildSettings = {
363 | ALWAYS_SEARCH_USER_PATHS = NO;
364 | CLANG_ANALYZER_NONNULL = YES;
365 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
367 | CLANG_CXX_LIBRARY = "libc++";
368 | CLANG_ENABLE_MODULES = YES;
369 | CLANG_ENABLE_OBJC_ARC = YES;
370 | CLANG_ENABLE_OBJC_WEAK = YES;
371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
372 | CLANG_WARN_BOOL_CONVERSION = YES;
373 | CLANG_WARN_COMMA = YES;
374 | CLANG_WARN_CONSTANT_CONVERSION = YES;
375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
377 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
378 | CLANG_WARN_EMPTY_BODY = YES;
379 | CLANG_WARN_ENUM_CONVERSION = YES;
380 | CLANG_WARN_INFINITE_RECURSION = YES;
381 | CLANG_WARN_INT_CONVERSION = YES;
382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
386 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
388 | CLANG_WARN_STRICT_PROTOTYPES = YES;
389 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
390 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
391 | CLANG_WARN_UNREACHABLE_CODE = YES;
392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
393 | COPY_PHASE_STRIP = NO;
394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
395 | ENABLE_NS_ASSERTIONS = NO;
396 | ENABLE_STRICT_OBJC_MSGSEND = YES;
397 | GCC_C_LANGUAGE_STANDARD = gnu11;
398 | GCC_NO_COMMON_BLOCKS = YES;
399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
401 | GCC_WARN_UNDECLARED_SELECTOR = YES;
402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
403 | GCC_WARN_UNUSED_FUNCTION = YES;
404 | GCC_WARN_UNUSED_VARIABLE = YES;
405 | MACOSX_DEPLOYMENT_TARGET = 12.0;
406 | MTL_ENABLE_DEBUG_INFO = NO;
407 | MTL_FAST_MATH = YES;
408 | SDKROOT = macosx;
409 | SWIFT_COMPILATION_MODE = wholemodule;
410 | SWIFT_OPTIMIZATION_LEVEL = "-O";
411 | };
412 | name = Release;
413 | };
414 | C6672B0327475FB100D616B6 /* Debug */ = {
415 | isa = XCBuildConfiguration;
416 | baseConfigurationReference = 9CF0C42B8EE8BB2B4E5C7006 /* Pods-Capture.debug.xcconfig */;
417 | buildSettings = {
418 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
419 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
420 | CODE_SIGN_ENTITLEMENTS = Capture/Capture.entitlements;
421 | CODE_SIGN_STYLE = Automatic;
422 | COMBINE_HIDPI_IMAGES = YES;
423 | CURRENT_PROJECT_VERSION = 2;
424 | DEVELOPMENT_TEAM = 3A6K89K388;
425 | ENABLE_HARDENED_RUNTIME = YES;
426 | GENERATE_INFOPLIST_FILE = NO;
427 | INFOPLIST_FILE = Capture/Info.plist;
428 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
429 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
430 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
431 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
432 | LD_RUNPATH_SEARCH_PATHS = (
433 | "$(inherited)",
434 | "@executable_path/../Frameworks",
435 | );
436 | MARKETING_VERSION = 1.0;
437 | PRODUCT_BUNDLE_IDENTIFIER = studio.retina.Capture;
438 | PRODUCT_NAME = "$(TARGET_NAME)";
439 | SWIFT_EMIT_LOC_STRINGS = YES;
440 | SWIFT_OBJC_BRIDGING_HEADER = Capture/BridgingHeader.h;
441 | SWIFT_VERSION = 5.0;
442 | };
443 | name = Debug;
444 | };
445 | C6672B0427475FB100D616B6 /* Release */ = {
446 | isa = XCBuildConfiguration;
447 | baseConfigurationReference = 6606ACB6A864F72E01DA4F21 /* Pods-Capture.release.xcconfig */;
448 | buildSettings = {
449 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
450 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
451 | CODE_SIGN_ENTITLEMENTS = Capture/Capture.entitlements;
452 | CODE_SIGN_STYLE = Automatic;
453 | COMBINE_HIDPI_IMAGES = YES;
454 | CURRENT_PROJECT_VERSION = 2;
455 | DEVELOPMENT_TEAM = 3A6K89K388;
456 | ENABLE_HARDENED_RUNTIME = YES;
457 | GENERATE_INFOPLIST_FILE = NO;
458 | INFOPLIST_FILE = Capture/Info.plist;
459 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
460 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
461 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
462 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
463 | LD_RUNPATH_SEARCH_PATHS = (
464 | "$(inherited)",
465 | "@executable_path/../Frameworks",
466 | );
467 | MARKETING_VERSION = 1.0;
468 | PRODUCT_BUNDLE_IDENTIFIER = studio.retina.Capture;
469 | PRODUCT_NAME = "$(TARGET_NAME)";
470 | SWIFT_EMIT_LOC_STRINGS = YES;
471 | SWIFT_OBJC_BRIDGING_HEADER = Capture/BridgingHeader.h;
472 | SWIFT_VERSION = 5.0;
473 | };
474 | name = Release;
475 | };
476 | /* End XCBuildConfiguration section */
477 |
478 | /* Begin XCConfigurationList section */
479 | C6672AF027475FB000D616B6 /* Build configuration list for PBXProject "Capture" */ = {
480 | isa = XCConfigurationList;
481 | buildConfigurations = (
482 | C6672B0027475FB100D616B6 /* Debug */,
483 | C6672B0127475FB100D616B6 /* Release */,
484 | );
485 | defaultConfigurationIsVisible = 0;
486 | defaultConfigurationName = Release;
487 | };
488 | C6672B0227475FB100D616B6 /* Build configuration list for PBXNativeTarget "Capture" */ = {
489 | isa = XCConfigurationList;
490 | buildConfigurations = (
491 | C6672B0327475FB100D616B6 /* Debug */,
492 | C6672B0427475FB100D616B6 /* Release */,
493 | );
494 | defaultConfigurationIsVisible = 0;
495 | defaultConfigurationName = Release;
496 | };
497 | /* End XCConfigurationList section */
498 | };
499 | rootObject = C6672AED27475FB000D616B6 /* Project object */;
500 | }
501 |
--------------------------------------------------------------------------------
/Capture/Controllers/CaptureWindowController.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 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
179 |
180 |
181 |
182 |
192 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
--------------------------------------------------------------------------------
/Capture/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
408 |
409 |
410 |
--------------------------------------------------------------------------------