├── .gitignore
├── .vscode
├── build.sh
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── ShareShot copy2-Info.plist
├── ShareShot.xcodeproj
├── .xcodesamplecode.plist
├── project.pbxproj
├── project.xcworkspace
│ └── xcshareddata
│ │ └── WorkspaceSettings.xcsettings
└── xcshareddata
│ └── xcschemes
│ ├── ShareShot withoutsandbox.xcscheme
│ └── ShareShot.xcscheme
├── ShareShot
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Frame 6.png
│ │ ├── Frame 61024.png
│ │ ├── Frame 6128.png
│ │ ├── Frame 616.png
│ │ ├── Frame 6256.png
│ │ ├── Frame 6257.png
│ │ ├── Frame 632.png
│ │ ├── Frame 633.png
│ │ ├── Frame 664.png
│ │ └── Frame 7.png
│ ├── Contents.json
│ ├── ExampleImage.imageset
│ │ ├── CleanShot 2023-12-17 at 20.32.06@2x.png
│ │ └── Contents.json
│ ├── Logo.imageset
│ │ ├── Contents.json
│ │ └── Frame 6.png
│ └── LogoForStatusBarItem.imageset
│ │ ├── Contents.json
│ │ ├── Frame 6256.png
│ │ ├── Frame 6257.png
│ │ └── Frame 6258.png
├── Audio
│ └── Synth.aif
├── CaptureSample.entitlements
├── CaptureSampleApp.swift
├── CaptureStackView.swift
├── DragAndDrop.swift
├── HistoryStackPanel.swift
├── HistoryStackView.swift
├── Info.plist
├── KeyboardAndMouseEventMonitors.swift
├── OnboardingView.swift
├── Persistance.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Prorotypes
│ ├── AudioLevelsView.swift
│ ├── CapturePreview.swift
│ ├── ConfigurationView.swift
│ ├── FolderLink.swift
│ ├── ImageEditorWindow.swift
│ ├── MaterialView.swift
│ ├── PinScreenShotView.swift
│ └── iCloudURL.swift
├── SampleCodeFromCaptureExample
│ ├── AudioPlayer.swift
│ ├── CaptureEngine.swift
│ ├── ContentView.swift
│ ├── PowerMeter.swift
│ └── ScreenRecorder.swift
├── ScreenShotStatusBarView.swift
├── ScreenShotView.swift
├── ScreenshopPreviewWindow.swift
├── ScreenshotAreaSelectionNonactivatingPanel.swift
├── ScreenshotAreaSelectionView.swift
├── ScreenshotStackPanel.swift
├── ShareShot-Bridging-Header.h
├── ShowAndHideCursor.h
├── StatusBarView.swift
└── convertToSwiftUICoordinates.swift
├── assets
├── ScreenCapture.mov
├── app-store-1280x800, hover.png
├── app-store-hover-share-menu-expanded.png
└── cleanshot-screenshot-examples.png
├── documents
├── competition.md
├── implementation.md
├── marketing.md
└── todo.md
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # See LICENSE folder for this sample’s licensing information.
2 | #
3 | # Apple sample code gitignore configuration.
4 |
5 | # Don't include build artifacts when built from the command line
6 | build/
7 |
8 | # Finder
9 | .DS_Store
10 |
11 | # Xcode - User files
12 | xcuserdata/
13 |
14 | **/*.xcodeproj/project.xcworkspace/*
15 | !**/*.xcodeproj/project.xcworkspace/xcshareddata
16 |
17 | **/*.xcodeproj/project.xcworkspace/xcshareddata/*
18 | !**/*.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
19 |
20 | **/*.playground/playground.xcworkspace/*
21 | !**/*.playground/playground.xcworkspace/xcshareddata
22 |
23 | **/*.playground/playground.xcworkspace/xcshareddata/*
24 | !**/*.playground/playground.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
25 |
--------------------------------------------------------------------------------
/.vscode/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Change YOUR_SCHEME_NAME to your actual scheme name
4 | SCHEME=CaptureSample
5 |
6 | # Create a desired build output path using current directory /build folder
7 | BUILD_OUTPUT_PATH="$PWD/build/debug/"
8 |
9 | # Build macos application
10 | xcodebuild -scheme $SCHEME -configuration Debug CONFIGURATION_BUILD_DIR=$BUILD_OUTPUT_PATH
11 |
12 | # Get the path to the built application
13 | APP_PATH="$BUILD_OUTPUT_PATH/$SCHEME.app"
14 |
15 | echo "Built application path: $APP_PATH, provide this in the launch config"
16 |
17 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "sswg.swift-lang",
4 | "vadimcn.vscode-lldb",
5 | ]
6 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "lldb",
9 | "request": "launch",
10 | "name": "Debug",
11 | "program": "${workspaceFolder}/build/debug/CaptureSample.app",
12 | "args": [],
13 | "cwd": "${workspaceFolder}",
14 | "preLaunchTask": "build debug",
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "swift.sourcekit-lsp.serverArguments": [
3 | "--log-level",
4 | "debug",
5 | ]
6 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build debug",
6 | "type": "shell",
7 | "command": "./build.sh",
8 | "group": {
9 | "kind": "build",
10 | "isDefault": true
11 | },
12 | "presentation": {
13 | "reveal": "never"
14 | }
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/ShareShot copy2-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ShareShot.xcodeproj/.xcodesamplecode.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ShareShot.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8B5E62252B93DA1D00455929 /* FolderLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5E62242B93DA1D00455929 /* FolderLink.swift */; };
11 | 8B5E62272B93E78C00455929 /* ImageEditorWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5E62262B93E78C00455929 /* ImageEditorWindow.swift */; };
12 | 8B6934E32B495D29003BFDDF /* ScreenshotStackPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6934E22B495D29003BFDDF /* ScreenshotStackPanel.swift */; };
13 | 8B6E04CD2B4F4CB600DD5CDB /* ScreenShotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6E04CC2B4F4CB600DD5CDB /* ScreenShotView.swift */; };
14 | 8BB3A9842BAF7350002CA772 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BB3A9832BAF7350002CA772 /* OnboardingView.swift */; };
15 | 8BB3A9852BAF7350002CA772 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BB3A9832BAF7350002CA772 /* OnboardingView.swift */; };
16 | 8BB5A2F52B8E6EB300292962 /* CaptureSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A115922B8DED31003A69FF /* CaptureSampleApp.swift */; };
17 | 8BB5A2F62B8E6F4000292962 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2A115942B8DEDE1003A69FF /* Assets.xcassets */; };
18 | 8BBAE2842B4689260079CCD4 /* CaptureStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBAE2832B4689260079CCD4 /* CaptureStackView.swift */; };
19 | 8BEBDCFF2B8D3AD4006C6B96 /* ScreenshotAreaSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255C43E2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift */; };
20 | 8BEBDD012B8D3AD4006C6B96 /* CaptureStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBAE2832B4689260079CCD4 /* CaptureStackView.swift */; };
21 | 8BEBDD022B8D3AD4006C6B96 /* ScreenShotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6E04CC2B4F4CB600DD5CDB /* ScreenShotView.swift */; };
22 | 8BEBDD032B8D3AD4006C6B96 /* ScreenshotAreaSelectionNonactivatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D269967D2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift */; };
23 | 8BEBDD042B8D3AD4006C6B96 /* ScreenshotStackPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6934E22B495D29003BFDDF /* ScreenshotStackPanel.swift */; };
24 | 8BEBDD052B8D3AD4006C6B96 /* convertToSwiftUICoordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255C43C2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift */; };
25 | 8BEBDD072B8D3AD4006C6B96 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 8BEBDCFC2B8D3AD4006C6B96 /* HotKey */; };
26 | 8BEBDD082B8D3AD4006C6B96 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 8BEBDCFA2B8D3AD4006C6B96 /* LaunchAtLogin */; };
27 | 8BFDCEF92BCEADE500B7E84E /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFDCEF82BCEADE500B7E84E /* StatusBarView.swift */; };
28 | 8BFDCEFA2BCEADE500B7E84E /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFDCEF82BCEADE500B7E84E /* StatusBarView.swift */; };
29 | D21277F72BBBA020001C0953 /* Persistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21277F62BBBA020001C0953 /* Persistance.swift */; };
30 | D21277F82BBBA020001C0953 /* Persistance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21277F62BBBA020001C0953 /* Persistance.swift */; };
31 | D255C43D2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255C43C2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift */; };
32 | D255C43F2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D255C43E2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift */; };
33 | D26996772B2EAA7100CADFDC /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D26996762B2EAA7100CADFDC /* LaunchAtLogin */; };
34 | D269967A2B2EAB0600CADFDC /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = D26996792B2EAB0600CADFDC /* HotKey */; };
35 | D269967E2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D269967D2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift */; };
36 | D2A115932B8DED31003A69FF /* CaptureSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A115922B8DED31003A69FF /* CaptureSampleApp.swift */; };
37 | D2A115952B8DEDE1003A69FF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2A115942B8DEDE1003A69FF /* Assets.xcassets */; };
38 | D2C5BC6D2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5BC6C2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift */; };
39 | D2C5BC6E2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C5BC6C2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift */; };
40 | /* End PBXBuildFile section */
41 |
42 | /* Begin PBXFileReference section */
43 | 8B5E62242B93DA1D00455929 /* FolderLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderLink.swift; sourceTree = ""; };
44 | 8B5E62262B93E78C00455929 /* ImageEditorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEditorWindow.swift; sourceTree = ""; };
45 | 8B6934E22B495D29003BFDDF /* ScreenshotStackPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotStackPanel.swift; sourceTree = ""; };
46 | 8B6E04CC2B4F4CB600DD5CDB /* ScreenShotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShotView.swift; sourceTree = ""; };
47 | 8BB3A9832BAF7350002CA772 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; };
48 | 8BBAE2832B4689260079CCD4 /* CaptureStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureStackView.swift; sourceTree = ""; };
49 | 8BEBDD0F2B8D3AD4006C6B96 /* ShareShot withoutsandbox.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ShareShot withoutsandbox.app"; sourceTree = BUILT_PRODUCTS_DIR; };
50 | 8BFDCEF82BCEADE500B7E84E /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = ""; };
51 | 8D89E0E7125AFE6B4E2E6500 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
52 | C4B0DAA7276BA4460015082A /* ShareShot.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShareShot.app; sourceTree = BUILT_PRODUCTS_DIR; };
53 | D21277F62BBBA020001C0953 /* Persistance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistance.swift; sourceTree = ""; };
54 | D255C4362B30077000A0C7B2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
55 | D255C43C2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = convertToSwiftUICoordinates.swift; sourceTree = ""; };
56 | D255C43E2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotAreaSelectionView.swift; sourceTree = ""; };
57 | D266C99D2B2FB18B001D7B66 /* ShareShot-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ShareShot-Bridging-Header.h"; sourceTree = ""; };
58 | D266C9A32B2FB87A001D7B66 /* ShowAndHideCursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShowAndHideCursor.h; sourceTree = ""; };
59 | D269967D2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotAreaSelectionNonactivatingPanel.swift; sourceTree = ""; };
60 | D276E0542BACA390000C4C06 /* documents */ = {isa = PBXFileReference; lastKnownFileType = folder; path = documents; sourceTree = ""; };
61 | D2A115922B8DED31003A69FF /* CaptureSampleApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureSampleApp.swift; sourceTree = ""; };
62 | D2A115942B8DEDE1003A69FF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
63 | D2C5BC6C2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardAndMouseEventMonitors.swift; sourceTree = ""; };
64 | /* End PBXFileReference section */
65 |
66 | /* Begin PBXFrameworksBuildPhase section */
67 | 8BEBDD062B8D3AD4006C6B96 /* Frameworks */ = {
68 | isa = PBXFrameworksBuildPhase;
69 | buildActionMask = 2147483647;
70 | files = (
71 | 8BEBDD072B8D3AD4006C6B96 /* HotKey in Frameworks */,
72 | 8BEBDD082B8D3AD4006C6B96 /* LaunchAtLogin in Frameworks */,
73 | );
74 | runOnlyForDeploymentPostprocessing = 0;
75 | };
76 | C4B0DAA4276BA4460015082A /* Frameworks */ = {
77 | isa = PBXFrameworksBuildPhase;
78 | buildActionMask = 2147483647;
79 | files = (
80 | D269967A2B2EAB0600CADFDC /* HotKey in Frameworks */,
81 | D26996772B2EAA7100CADFDC /* LaunchAtLogin in Frameworks */,
82 | );
83 | runOnlyForDeploymentPostprocessing = 0;
84 | };
85 | /* End PBXFrameworksBuildPhase section */
86 |
87 | /* Begin PBXGroup section */
88 | C41D1B1D2814BD230033613F /* Prorotypes */ = {
89 | isa = PBXGroup;
90 | children = (
91 | 8B5E62242B93DA1D00455929 /* FolderLink.swift */,
92 | 8B5E62262B93E78C00455929 /* ImageEditorWindow.swift */,
93 | );
94 | path = Prorotypes;
95 | sourceTree = "";
96 | };
97 | C4B0DA9E276BA4460015082A = {
98 | isa = PBXGroup;
99 | children = (
100 | D276E0542BACA390000C4C06 /* documents */,
101 | 8D89E0E7125AFE6B4E2E6500 /* README.md */,
102 | C4B0DAA9276BA4460015082A /* ShareShot */,
103 | C4B0DAA8276BA4460015082A /* Products */,
104 | );
105 | sourceTree = "";
106 | };
107 | C4B0DAA8276BA4460015082A /* Products */ = {
108 | isa = PBXGroup;
109 | children = (
110 | C4B0DAA7276BA4460015082A /* ShareShot.app */,
111 | 8BEBDD0F2B8D3AD4006C6B96 /* ShareShot withoutsandbox.app */,
112 | );
113 | name = Products;
114 | sourceTree = "";
115 | };
116 | C4B0DAA9276BA4460015082A /* ShareShot */ = {
117 | isa = PBXGroup;
118 | children = (
119 | D2A115922B8DED31003A69FF /* CaptureSampleApp.swift */,
120 | D269967D2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift */,
121 | D255C43E2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift */,
122 | D2C5BC6C2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift */,
123 | 8B6934E22B495D29003BFDDF /* ScreenshotStackPanel.swift */,
124 | 8BFDCEF82BCEADE500B7E84E /* StatusBarView.swift */,
125 | 8BBAE2832B4689260079CCD4 /* CaptureStackView.swift */,
126 | 8B6E04CC2B4F4CB600DD5CDB /* ScreenShotView.swift */,
127 | 8BB3A9832BAF7350002CA772 /* OnboardingView.swift */,
128 | D255C43C2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift */,
129 | D21277F62BBBA020001C0953 /* Persistance.swift */,
130 | D266C9A32B2FB87A001D7B66 /* ShowAndHideCursor.h */,
131 | D266C99D2B2FB18B001D7B66 /* ShareShot-Bridging-Header.h */,
132 | D255C4362B30077000A0C7B2 /* Info.plist */,
133 | D2A115942B8DEDE1003A69FF /* Assets.xcassets */,
134 | C41D1B1D2814BD230033613F /* Prorotypes */,
135 | );
136 | path = ShareShot;
137 | sourceTree = "";
138 | };
139 | /* End PBXGroup section */
140 |
141 | /* Begin PBXNativeTarget section */
142 | 8BEBDCF92B8D3AD4006C6B96 /* ShareShot withoutsandbox */ = {
143 | isa = PBXNativeTarget;
144 | buildConfigurationList = 8BEBDD0C2B8D3AD4006C6B96 /* Build configuration list for PBXNativeTarget "ShareShot withoutsandbox" */;
145 | buildPhases = (
146 | 8BEBDCFE2B8D3AD4006C6B96 /* Sources */,
147 | 8BEBDD062B8D3AD4006C6B96 /* Frameworks */,
148 | 8BEBDD092B8D3AD4006C6B96 /* Resources */,
149 | );
150 | buildRules = (
151 | );
152 | dependencies = (
153 | );
154 | name = "ShareShot withoutsandbox";
155 | packageProductDependencies = (
156 | 8BEBDCFA2B8D3AD4006C6B96 /* LaunchAtLogin */,
157 | 8BEBDCFC2B8D3AD4006C6B96 /* HotKey */,
158 | );
159 | productName = CaptureIt;
160 | productReference = 8BEBDD0F2B8D3AD4006C6B96 /* ShareShot withoutsandbox.app */;
161 | productType = "com.apple.product-type.application";
162 | };
163 | C4B0DAA6276BA4460015082A /* ShareShot */ = {
164 | isa = PBXNativeTarget;
165 | buildConfigurationList = C4B0DAB6276BA4480015082A /* Build configuration list for PBXNativeTarget "ShareShot" */;
166 | buildPhases = (
167 | C4B0DAA3276BA4460015082A /* Sources */,
168 | C4B0DAA4276BA4460015082A /* Frameworks */,
169 | C4B0DAA5276BA4460015082A /* Resources */,
170 | );
171 | buildRules = (
172 | );
173 | dependencies = (
174 | );
175 | name = ShareShot;
176 | packageProductDependencies = (
177 | D26996762B2EAA7100CADFDC /* LaunchAtLogin */,
178 | D26996792B2EAB0600CADFDC /* HotKey */,
179 | );
180 | productName = CaptureIt;
181 | productReference = C4B0DAA7276BA4460015082A /* ShareShot.app */;
182 | productType = "com.apple.product-type.application";
183 | };
184 | /* End PBXNativeTarget section */
185 |
186 | /* Begin PBXProject section */
187 | C4B0DA9F276BA4460015082A /* Project object */ = {
188 | isa = PBXProject;
189 | attributes = {
190 | BuildIndependentTargetsInParallel = 1;
191 | LastSwiftUpdateCheck = 1330;
192 | LastUpgradeCheck = 1520;
193 | ORGANIZATIONNAME = Apple;
194 | TargetAttributes = {
195 | C4B0DAA6276BA4460015082A = {
196 | CreatedOnToolsVersion = 13.3;
197 | LastSwiftMigration = 1330;
198 | };
199 | };
200 | };
201 | buildConfigurationList = C4B0DAA2276BA4460015082A /* Build configuration list for PBXProject "ShareShot" */;
202 | compatibilityVersion = "Xcode 14.0";
203 | developmentRegion = en;
204 | hasScannedForEncodings = 0;
205 | knownRegions = (
206 | en,
207 | Base,
208 | );
209 | mainGroup = C4B0DA9E276BA4460015082A;
210 | packageReferences = (
211 | D26996752B2EAA7100CADFDC /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
212 | D26996782B2EAB0600CADFDC /* XCRemoteSwiftPackageReference "HotKey" */,
213 | );
214 | productRefGroup = C4B0DAA8276BA4460015082A /* Products */;
215 | projectDirPath = "";
216 | projectRoot = "";
217 | targets = (
218 | C4B0DAA6276BA4460015082A /* ShareShot */,
219 | 8BEBDCF92B8D3AD4006C6B96 /* ShareShot withoutsandbox */,
220 | );
221 | };
222 | /* End PBXProject section */
223 |
224 | /* Begin PBXResourcesBuildPhase section */
225 | 8BEBDD092B8D3AD4006C6B96 /* Resources */ = {
226 | isa = PBXResourcesBuildPhase;
227 | buildActionMask = 2147483647;
228 | files = (
229 | 8BB5A2F62B8E6F4000292962 /* Assets.xcassets in Resources */,
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | C4B0DAA5276BA4460015082A /* Resources */ = {
234 | isa = PBXResourcesBuildPhase;
235 | buildActionMask = 2147483647;
236 | files = (
237 | D2A115952B8DEDE1003A69FF /* Assets.xcassets in Resources */,
238 | );
239 | runOnlyForDeploymentPostprocessing = 0;
240 | };
241 | /* End PBXResourcesBuildPhase section */
242 |
243 | /* Begin PBXSourcesBuildPhase section */
244 | 8BEBDCFE2B8D3AD4006C6B96 /* Sources */ = {
245 | isa = PBXSourcesBuildPhase;
246 | buildActionMask = 2147483647;
247 | files = (
248 | 8BEBDCFF2B8D3AD4006C6B96 /* ScreenshotAreaSelectionView.swift in Sources */,
249 | 8BB5A2F52B8E6EB300292962 /* CaptureSampleApp.swift in Sources */,
250 | 8BEBDD012B8D3AD4006C6B96 /* CaptureStackView.swift in Sources */,
251 | D21277F82BBBA020001C0953 /* Persistance.swift in Sources */,
252 | 8BB3A9852BAF7350002CA772 /* OnboardingView.swift in Sources */,
253 | 8BEBDD022B8D3AD4006C6B96 /* ScreenShotView.swift in Sources */,
254 | 8BFDCEFA2BCEADE500B7E84E /* StatusBarView.swift in Sources */,
255 | 8BEBDD032B8D3AD4006C6B96 /* ScreenshotAreaSelectionNonactivatingPanel.swift in Sources */,
256 | D2C5BC6E2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift in Sources */,
257 | 8BEBDD042B8D3AD4006C6B96 /* ScreenshotStackPanel.swift in Sources */,
258 | 8BEBDD052B8D3AD4006C6B96 /* convertToSwiftUICoordinates.swift in Sources */,
259 | );
260 | runOnlyForDeploymentPostprocessing = 0;
261 | };
262 | C4B0DAA3276BA4460015082A /* Sources */ = {
263 | isa = PBXSourcesBuildPhase;
264 | buildActionMask = 2147483647;
265 | files = (
266 | D255C43F2B31D1D200A0C7B2 /* ScreenshotAreaSelectionView.swift in Sources */,
267 | D2A115932B8DED31003A69FF /* CaptureSampleApp.swift in Sources */,
268 | 8B5E62272B93E78C00455929 /* ImageEditorWindow.swift in Sources */,
269 | 8BBAE2842B4689260079CCD4 /* CaptureStackView.swift in Sources */,
270 | D2C5BC6D2BAA2F08004ACF8A /* KeyboardAndMouseEventMonitors.swift in Sources */,
271 | D21277F72BBBA020001C0953 /* Persistance.swift in Sources */,
272 | 8BFDCEF92BCEADE500B7E84E /* StatusBarView.swift in Sources */,
273 | 8B6E04CD2B4F4CB600DD5CDB /* ScreenShotView.swift in Sources */,
274 | D269967E2B2EB12700CADFDC /* ScreenshotAreaSelectionNonactivatingPanel.swift in Sources */,
275 | 8B5E62252B93DA1D00455929 /* FolderLink.swift in Sources */,
276 | 8B6934E32B495D29003BFDDF /* ScreenshotStackPanel.swift in Sources */,
277 | D255C43D2B31CE1300A0C7B2 /* convertToSwiftUICoordinates.swift in Sources */,
278 | 8BB3A9842BAF7350002CA772 /* OnboardingView.swift in Sources */,
279 | );
280 | runOnlyForDeploymentPostprocessing = 0;
281 | };
282 | /* End PBXSourcesBuildPhase section */
283 |
284 | /* Begin XCBuildConfiguration section */
285 | 8BEBDD0D2B8D3AD4006C6B96 /* Debug */ = {
286 | isa = XCBuildConfiguration;
287 | buildSettings = {
288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
290 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
291 | CLANG_ENABLE_MODULES = YES;
292 | CODE_SIGN_ENTITLEMENTS = ShareShot/CaptureSample.entitlements;
293 | CODE_SIGN_IDENTITY = "Apple Development";
294 | CODE_SIGN_STYLE = Automatic;
295 | COMBINE_HIDPI_IMAGES = YES;
296 | CURRENT_PROJECT_VERSION = 1;
297 | DEAD_CODE_STRIPPING = YES;
298 | DEVELOPMENT_ASSET_PATHS = "\"ShareShot/Preview Content\"";
299 | DEVELOPMENT_TEAM = A55978949T;
300 | ENABLE_HARDENED_RUNTIME = YES;
301 | ENABLE_PREVIEWS = YES;
302 | GENERATE_INFOPLIST_FILE = YES;
303 | INFOPLIST_FILE = "ShareShot copy2-Info.plist";
304 | INFOPLIST_KEY_CFBundleDisplayName = ShareScreenshot;
305 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
306 | INFOPLIST_KEY_LSUIElement = YES;
307 | INFOPLIST_KEY_NSCameraUsageDescription = "We need access to capture the screen";
308 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "We need access to the desktop to save files.";
309 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
310 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "We need access to capture the screen";
311 | LD_RUNPATH_SEARCH_PATHS = (
312 | "$(inherited)",
313 | "@executable_path/../Frameworks",
314 | );
315 | MACOSX_DEPLOYMENT_TARGET = 14.0;
316 | MARKETING_VERSION = 1.0;
317 | "OTHER_SWIFT_FLAGS[arch=*]" = "-DNOSANDBOX";
318 | PRODUCT_BUNDLE_IDENTIFIER = "com.bra1ndump.share-screenshot";
319 | PRODUCT_NAME = "$(TARGET_NAME)";
320 | PROVISIONING_PROFILE_SPECIFIER = "";
321 | SWIFT_EMIT_LOC_STRINGS = YES;
322 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ShareShot/ShareShot-Bridging-Header.h";
323 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
324 | SWIFT_VERSION = 5.0;
325 | };
326 | name = Debug;
327 | };
328 | 8BEBDD0E2B8D3AD4006C6B96 /* Release */ = {
329 | isa = XCBuildConfiguration;
330 | buildSettings = {
331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
332 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
333 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
334 | CLANG_ENABLE_MODULES = YES;
335 | CODE_SIGN_ENTITLEMENTS = ShareShot/CaptureSample.entitlements;
336 | CODE_SIGN_IDENTITY = "Apple Development";
337 | CODE_SIGN_STYLE = Automatic;
338 | COMBINE_HIDPI_IMAGES = YES;
339 | CURRENT_PROJECT_VERSION = 1;
340 | DEAD_CODE_STRIPPING = YES;
341 | DEVELOPMENT_ASSET_PATHS = "\"ShareShot/Preview Content\"";
342 | DEVELOPMENT_TEAM = A55978949T;
343 | ENABLE_HARDENED_RUNTIME = YES;
344 | ENABLE_PREVIEWS = YES;
345 | GENERATE_INFOPLIST_FILE = YES;
346 | INFOPLIST_FILE = "ShareShot copy2-Info.plist";
347 | INFOPLIST_KEY_CFBundleDisplayName = ShareScreenshot;
348 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
349 | INFOPLIST_KEY_LSUIElement = YES;
350 | INFOPLIST_KEY_NSCameraUsageDescription = "We need access to capture the screen";
351 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "We need access to the desktop to save files.";
352 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
353 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "We need access to capture the screen";
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/../Frameworks",
357 | );
358 | MACOSX_DEPLOYMENT_TARGET = 14.0;
359 | MARKETING_VERSION = 1.0;
360 | "OTHER_SWIFT_FLAGS[arch=*]" = "-DNOSANDBOX";
361 | PRODUCT_BUNDLE_IDENTIFIER = "com.bra1ndump.share-screenshot";
362 | PRODUCT_NAME = "$(TARGET_NAME)";
363 | PROVISIONING_PROFILE_SPECIFIER = "";
364 | SWIFT_EMIT_LOC_STRINGS = YES;
365 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ShareShot/ShareShot-Bridging-Header.h";
366 | SWIFT_VERSION = 5.0;
367 | };
368 | name = Release;
369 | };
370 | C4B0DAB4276BA4480015082A /* Debug */ = {
371 | isa = XCBuildConfiguration;
372 | buildSettings = {
373 | ALWAYS_SEARCH_USER_PATHS = NO;
374 | CLANG_ANALYZER_NONNULL = YES;
375 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
376 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
377 | CLANG_CXX_LIBRARY = "libc++";
378 | CLANG_ENABLE_MODULES = YES;
379 | CLANG_ENABLE_OBJC_ARC = YES;
380 | CLANG_ENABLE_OBJC_WEAK = YES;
381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
382 | CLANG_WARN_BOOL_CONVERSION = YES;
383 | CLANG_WARN_COMMA = YES;
384 | CLANG_WARN_CONSTANT_CONVERSION = YES;
385 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
387 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
388 | CLANG_WARN_EMPTY_BODY = YES;
389 | CLANG_WARN_ENUM_CONVERSION = YES;
390 | CLANG_WARN_INFINITE_RECURSION = YES;
391 | CLANG_WARN_INT_CONVERSION = YES;
392 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
393 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
396 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
398 | CLANG_WARN_STRICT_PROTOTYPES = YES;
399 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
400 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
401 | CLANG_WARN_UNREACHABLE_CODE = YES;
402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
403 | COPY_PHASE_STRIP = NO;
404 | DEAD_CODE_STRIPPING = YES;
405 | DEBUG_INFORMATION_FORMAT = dwarf;
406 | ENABLE_STRICT_OBJC_MSGSEND = YES;
407 | ENABLE_TESTABILITY = YES;
408 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
409 | GCC_C_LANGUAGE_STANDARD = gnu11;
410 | GCC_DYNAMIC_NO_PIC = NO;
411 | GCC_NO_COMMON_BLOCKS = YES;
412 | GCC_OPTIMIZATION_LEVEL = 0;
413 | GCC_PREPROCESSOR_DEFINITIONS = (
414 | "DEBUG=1",
415 | "$(inherited)",
416 | );
417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
419 | GCC_WARN_UNDECLARED_SELECTOR = YES;
420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
421 | GCC_WARN_UNUSED_FUNCTION = YES;
422 | GCC_WARN_UNUSED_VARIABLE = YES;
423 | MACOSX_DEPLOYMENT_TARGET = 14.0;
424 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
425 | MTL_FAST_MATH = YES;
426 | ONLY_ACTIVE_ARCH = YES;
427 | SDKROOT = macosx;
428 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
429 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
430 | };
431 | name = Debug;
432 | };
433 | C4B0DAB5276BA4480015082A /* Release */ = {
434 | isa = XCBuildConfiguration;
435 | buildSettings = {
436 | ALWAYS_SEARCH_USER_PATHS = NO;
437 | CLANG_ANALYZER_NONNULL = YES;
438 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
439 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
440 | CLANG_CXX_LIBRARY = "libc++";
441 | CLANG_ENABLE_MODULES = YES;
442 | CLANG_ENABLE_OBJC_ARC = YES;
443 | CLANG_ENABLE_OBJC_WEAK = YES;
444 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
445 | CLANG_WARN_BOOL_CONVERSION = YES;
446 | CLANG_WARN_COMMA = YES;
447 | CLANG_WARN_CONSTANT_CONVERSION = YES;
448 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
449 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
450 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
451 | CLANG_WARN_EMPTY_BODY = YES;
452 | CLANG_WARN_ENUM_CONVERSION = YES;
453 | CLANG_WARN_INFINITE_RECURSION = YES;
454 | CLANG_WARN_INT_CONVERSION = YES;
455 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
456 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
457 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
459 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
460 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
461 | CLANG_WARN_STRICT_PROTOTYPES = YES;
462 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
463 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
464 | CLANG_WARN_UNREACHABLE_CODE = YES;
465 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
466 | COPY_PHASE_STRIP = NO;
467 | DEAD_CODE_STRIPPING = YES;
468 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
469 | ENABLE_NS_ASSERTIONS = NO;
470 | ENABLE_STRICT_OBJC_MSGSEND = YES;
471 | ENABLE_USER_SCRIPT_SANDBOXING = NO;
472 | GCC_C_LANGUAGE_STANDARD = gnu11;
473 | GCC_NO_COMMON_BLOCKS = YES;
474 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
475 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
476 | GCC_WARN_UNDECLARED_SELECTOR = YES;
477 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
478 | GCC_WARN_UNUSED_FUNCTION = YES;
479 | GCC_WARN_UNUSED_VARIABLE = YES;
480 | MACOSX_DEPLOYMENT_TARGET = 14.0;
481 | MTL_ENABLE_DEBUG_INFO = NO;
482 | MTL_FAST_MATH = YES;
483 | SDKROOT = macosx;
484 | SWIFT_COMPILATION_MODE = wholemodule;
485 | SWIFT_OPTIMIZATION_LEVEL = "-O";
486 | };
487 | name = Release;
488 | };
489 | C4B0DAB7276BA4480015082A /* Debug */ = {
490 | isa = XCBuildConfiguration;
491 | buildSettings = {
492 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
493 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
494 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
495 | CLANG_ENABLE_MODULES = YES;
496 | CODE_SIGN_ENTITLEMENTS = ShareShot/CaptureSample.entitlements;
497 | CODE_SIGN_IDENTITY = "Apple Development";
498 | CODE_SIGN_STYLE = Automatic;
499 | COMBINE_HIDPI_IMAGES = YES;
500 | CURRENT_PROJECT_VERSION = 1;
501 | DEAD_CODE_STRIPPING = YES;
502 | DEVELOPMENT_TEAM = A55978949T;
503 | ENABLE_HARDENED_RUNTIME = YES;
504 | ENABLE_PREVIEWS = YES;
505 | GENERATE_INFOPLIST_FILE = YES;
506 | INFOPLIST_FILE = ShareShot/Info.plist;
507 | INFOPLIST_KEY_CFBundleDisplayName = ShareScreenshot;
508 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
509 | INFOPLIST_KEY_LSUIElement = YES;
510 | INFOPLIST_KEY_NSCameraUsageDescription = "We need access to capture the screen";
511 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "We need access to the desktop to save files.";
512 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
513 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "We need access to capture the screen";
514 | LD_RUNPATH_SEARCH_PATHS = (
515 | "$(inherited)",
516 | "@executable_path/../Frameworks",
517 | );
518 | MACOSX_DEPLOYMENT_TARGET = 14.0;
519 | MARKETING_VERSION = 1.0;
520 | OTHER_SWIFT_FLAGS = "";
521 | PRODUCT_BUNDLE_IDENTIFIER = "com.bra1ndump.share-screenshot";
522 | PRODUCT_NAME = "$(TARGET_NAME)";
523 | PROVISIONING_PROFILE_SPECIFIER = "";
524 | SWIFT_EMIT_LOC_STRINGS = YES;
525 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ShareShot/ShareShot-Bridging-Header.h";
526 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
527 | SWIFT_VERSION = 5.0;
528 | };
529 | name = Debug;
530 | };
531 | C4B0DAB8276BA4480015082A /* Release */ = {
532 | isa = XCBuildConfiguration;
533 | buildSettings = {
534 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
535 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
536 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
537 | CLANG_ENABLE_MODULES = YES;
538 | CODE_SIGN_ENTITLEMENTS = ShareShot/CaptureSample.entitlements;
539 | CODE_SIGN_IDENTITY = "Apple Development";
540 | CODE_SIGN_STYLE = Automatic;
541 | COMBINE_HIDPI_IMAGES = YES;
542 | CURRENT_PROJECT_VERSION = 1;
543 | DEAD_CODE_STRIPPING = YES;
544 | DEVELOPMENT_TEAM = A55978949T;
545 | ENABLE_HARDENED_RUNTIME = YES;
546 | ENABLE_PREVIEWS = YES;
547 | GENERATE_INFOPLIST_FILE = YES;
548 | INFOPLIST_FILE = ShareShot/Info.plist;
549 | INFOPLIST_KEY_CFBundleDisplayName = ShareScreenshot;
550 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
551 | INFOPLIST_KEY_LSUIElement = YES;
552 | INFOPLIST_KEY_NSCameraUsageDescription = "We need access to capture the screen";
553 | INFOPLIST_KEY_NSDesktopFolderUsageDescription = "We need access to the desktop to save files.";
554 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
555 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "We need access to capture the screen";
556 | LD_RUNPATH_SEARCH_PATHS = (
557 | "$(inherited)",
558 | "@executable_path/../Frameworks",
559 | );
560 | MACOSX_DEPLOYMENT_TARGET = 14.0;
561 | MARKETING_VERSION = 1.0;
562 | PRODUCT_BUNDLE_IDENTIFIER = "com.bra1ndump.share-screenshot";
563 | PRODUCT_NAME = "$(TARGET_NAME)";
564 | PROVISIONING_PROFILE_SPECIFIER = "";
565 | SWIFT_EMIT_LOC_STRINGS = YES;
566 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/ShareShot/ShareShot-Bridging-Header.h";
567 | SWIFT_VERSION = 5.0;
568 | };
569 | name = Release;
570 | };
571 | /* End XCBuildConfiguration section */
572 |
573 | /* Begin XCConfigurationList section */
574 | 8BEBDD0C2B8D3AD4006C6B96 /* Build configuration list for PBXNativeTarget "ShareShot withoutsandbox" */ = {
575 | isa = XCConfigurationList;
576 | buildConfigurations = (
577 | 8BEBDD0D2B8D3AD4006C6B96 /* Debug */,
578 | 8BEBDD0E2B8D3AD4006C6B96 /* Release */,
579 | );
580 | defaultConfigurationIsVisible = 0;
581 | defaultConfigurationName = Release;
582 | };
583 | C4B0DAA2276BA4460015082A /* Build configuration list for PBXProject "ShareShot" */ = {
584 | isa = XCConfigurationList;
585 | buildConfigurations = (
586 | C4B0DAB4276BA4480015082A /* Debug */,
587 | C4B0DAB5276BA4480015082A /* Release */,
588 | );
589 | defaultConfigurationIsVisible = 0;
590 | defaultConfigurationName = Release;
591 | };
592 | C4B0DAB6276BA4480015082A /* Build configuration list for PBXNativeTarget "ShareShot" */ = {
593 | isa = XCConfigurationList;
594 | buildConfigurations = (
595 | C4B0DAB7276BA4480015082A /* Debug */,
596 | C4B0DAB8276BA4480015082A /* Release */,
597 | );
598 | defaultConfigurationIsVisible = 0;
599 | defaultConfigurationName = Release;
600 | };
601 | /* End XCConfigurationList section */
602 |
603 | /* Begin XCRemoteSwiftPackageReference section */
604 | 8BEBDCFB2B8D3AD4006C6B96 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
605 | isa = XCRemoteSwiftPackageReference;
606 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
607 | requirement = {
608 | branch = main;
609 | kind = branch;
610 | };
611 | };
612 | 8BEBDCFD2B8D3AD4006C6B96 /* XCRemoteSwiftPackageReference "HotKey" */ = {
613 | isa = XCRemoteSwiftPackageReference;
614 | repositoryURL = "https://github.com/soffes/HotKey";
615 | requirement = {
616 | branch = main;
617 | kind = branch;
618 | };
619 | };
620 | D26996752B2EAA7100CADFDC /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
621 | isa = XCRemoteSwiftPackageReference;
622 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
623 | requirement = {
624 | branch = main;
625 | kind = branch;
626 | };
627 | };
628 | D26996782B2EAB0600CADFDC /* XCRemoteSwiftPackageReference "HotKey" */ = {
629 | isa = XCRemoteSwiftPackageReference;
630 | repositoryURL = "https://github.com/soffes/HotKey";
631 | requirement = {
632 | branch = main;
633 | kind = branch;
634 | };
635 | };
636 | /* End XCRemoteSwiftPackageReference section */
637 |
638 | /* Begin XCSwiftPackageProductDependency section */
639 | 8BEBDCFA2B8D3AD4006C6B96 /* LaunchAtLogin */ = {
640 | isa = XCSwiftPackageProductDependency;
641 | package = 8BEBDCFB2B8D3AD4006C6B96 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
642 | productName = LaunchAtLogin;
643 | };
644 | 8BEBDCFC2B8D3AD4006C6B96 /* HotKey */ = {
645 | isa = XCSwiftPackageProductDependency;
646 | package = 8BEBDCFD2B8D3AD4006C6B96 /* XCRemoteSwiftPackageReference "HotKey" */;
647 | productName = HotKey;
648 | };
649 | D26996762B2EAA7100CADFDC /* LaunchAtLogin */ = {
650 | isa = XCSwiftPackageProductDependency;
651 | package = D26996752B2EAA7100CADFDC /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
652 | productName = LaunchAtLogin;
653 | };
654 | D26996792B2EAB0600CADFDC /* HotKey */ = {
655 | isa = XCSwiftPackageProductDependency;
656 | package = D26996782B2EAB0600CADFDC /* XCRemoteSwiftPackageReference "HotKey" */;
657 | productName = HotKey;
658 | };
659 | /* End XCSwiftPackageProductDependency section */
660 | };
661 | rootObject = C4B0DA9F276BA4460015082A /* Project object */;
662 | }
663 |
--------------------------------------------------------------------------------
/ShareShot.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Latest
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ShareShot.xcodeproj/xcshareddata/xcschemes/ShareShot withoutsandbox.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ShareShot.xcodeproj/xcshareddata/xcschemes/ShareShot.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/ShareShot/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 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Frame 616.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "Frame 632.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Frame 633.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "Frame 664.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Frame 6128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "Frame 6257.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Frame 6256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "Frame 7.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Frame 6.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "Frame 61024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 61024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 61024.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6128.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 616.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 616.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6256.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 6257.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 632.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 632.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 633.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 633.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 664.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 664.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/AppIcon.appiconset/Frame 7.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/ExampleImage.imageset/CleanShot 2023-12-17 at 20.32.06@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/ExampleImage.imageset/CleanShot 2023-12-17 at 20.32.06@2x.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/ExampleImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "CleanShot 2023-12-17 at 20.32.06@2x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/Logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Frame 6.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/Logo.imageset/Frame 6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/Logo.imageset/Frame 6.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Frame 6256.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "light"
13 | }
14 | ],
15 | "filename" : "Frame 6257.png",
16 | "idiom" : "universal",
17 | "scale" : "1x"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "dark"
24 | }
25 | ],
26 | "filename" : "Frame 6258.png",
27 | "idiom" : "universal",
28 | "scale" : "1x"
29 | },
30 | {
31 | "idiom" : "universal",
32 | "scale" : "2x"
33 | },
34 | {
35 | "appearances" : [
36 | {
37 | "appearance" : "luminosity",
38 | "value" : "light"
39 | }
40 | ],
41 | "idiom" : "universal",
42 | "scale" : "2x"
43 | },
44 | {
45 | "appearances" : [
46 | {
47 | "appearance" : "luminosity",
48 | "value" : "dark"
49 | }
50 | ],
51 | "idiom" : "universal",
52 | "scale" : "2x"
53 | },
54 | {
55 | "idiom" : "universal",
56 | "scale" : "3x"
57 | },
58 | {
59 | "appearances" : [
60 | {
61 | "appearance" : "luminosity",
62 | "value" : "light"
63 | }
64 | ],
65 | "idiom" : "universal",
66 | "scale" : "3x"
67 | },
68 | {
69 | "appearances" : [
70 | {
71 | "appearance" : "luminosity",
72 | "value" : "dark"
73 | }
74 | ],
75 | "idiom" : "universal",
76 | "scale" : "3x"
77 | }
78 | ],
79 | "info" : {
80 | "author" : "xcode",
81 | "version" : 1
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6256.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6257.png
--------------------------------------------------------------------------------
/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6258.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Assets.xcassets/LogoForStatusBarItem.imageset/Frame 6258.png
--------------------------------------------------------------------------------
/ShareShot/Audio/Synth.aif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/ShareShot/Audio/Synth.aif
--------------------------------------------------------------------------------
/ShareShot/CaptureSample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.bra1ndump.share-screenshot
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 | com.apple.security.app-sandbox
16 |
17 | com.apple.security.automation.apple-events
18 |
19 | com.apple.security.files.user-selected.read-write
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ShareShot/CaptureSampleApp.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Abstract:
3 | The entry point into this app.
4 | */
5 | import Cocoa
6 | import HotKey
7 | import SwiftUI
8 | import ScreenCaptureKit
9 |
10 | // For the new @Observable macro
11 | import Observation
12 |
13 | @main
14 | struct ShareShotApp {
15 | @AppStorage("onboardingShown") static var onboardingShown = false
16 | static var appDelegate: AppDelegate?
17 |
18 | static func main() {
19 | let appDelegate = AppDelegate()
20 | ShareShotApp.appDelegate = appDelegate
21 | let application = NSApplication.shared
22 | application.delegate = appDelegate
23 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
24 | }
25 | }
26 |
27 | enum StackState {
28 | case inactive
29 | case userTookAScreenshot
30 | case userAskedToShowHistory
31 | }
32 |
33 | @Observable
34 | class StackModel {
35 | var state: StackState
36 | /// Images shown
37 | var images: [ImageData]
38 |
39 | init(state: StackState, images: [ImageData]) {
40 | self.state = state
41 | self.images = images
42 | }
43 | }
44 |
45 | /**
46 | * Showing onboarding
47 | * Idle
48 | * Taking new screenshot
49 | * (optionally some screenshots are already taken)
50 | * Showing stack of screenshots
51 | * (could be from history, could be after taking a screenshot)
52 | * - panel showing the view, array of images to show, a way to modify the array from inside the view
53 | */
54 |
55 | class AppDelegate: NSObject, NSApplicationDelegate {
56 | // Settings
57 | let maxScreenshotsToShowOnStack = 5
58 | let maxStoredHistory = 50
59 |
60 | /// Overlay that blocks user interaction without switching key window.
61 | /// - Removes standard cursor while tracking the mouse position
62 | /// - Adds a SwiftUI overlay that draws the area selection
63 | var overlayWindow: ScreenshotAreaSelectionNonactivatingPanel?
64 |
65 | /// Each screenshot is added to a stack on the bottom left
66 | var currentPreviewPanel: ScreenshotStackPanel?
67 |
68 | // Start out empty
69 | // https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro
70 | let stackModel = StackModel(state: .inactive, images: [])
71 |
72 | /// Menu bar
73 | var statusBarItem: NSStatusItem!
74 | var contextMenu: NSMenu = NSMenu()
75 | var popover: NSPopover!
76 | var eventMonitor: Any?
77 | // Hot Keys
78 | // Screenshot
79 | let cmdShiftSeven = HotKey(key: .seven, modifiers: [.command, .shift])
80 | // Show screenshot history
81 | let cmdShiftEight = HotKey(key: .eight, modifiers: [.command, .shift])
82 |
83 | func applicationDidFinishLaunching(_ notification: Notification) {
84 | setupStatusBarItem()
85 | requestAuthorizationForLoginItem()
86 |
87 | // TODO: We might want to ask for permissions before trying to screen record using CGRequestScreenCaptureAccess()
88 | // Probably should do this before allowing the user to proceed with the screenshot
89 | // That api is known to show settings only once: https://stackoverflow.com/questions/75617005/how-to-show-screen-recording-permission-programmatically-using-swiftui
90 | // so we might want to track if we shown this before, maybe show additional info to the user suggesting to go to settings
91 | // but do not open the area selection - no point
92 | let defaults = UserDefaults.standard
93 | if !defaults.bool(forKey: "HasLaunchedBefore") {
94 | showOnboardingView()
95 | } else {
96 | #if DEBUG
97 | // startScreenshot()
98 | #endif
99 | }
100 | defaults.set(true, forKey: "HasLaunchedBefore")
101 |
102 | cmdShiftSeven.keyDownHandler = { [weak self] in
103 | // Make sure the old window is dismissed
104 | self?.startScreenshot()
105 | }
106 |
107 | cmdShiftEight.keyDownHandler = { [weak self] in
108 | self?.showScreenshotHistoryStack()
109 | }
110 | let clickRecognizer = NSClickGestureRecognizer(target: self, action: #selector(handleClickOutsidePopover))
111 | NSApplication.shared.keyWindow?.contentView?.addGestureRecognizer(clickRecognizer)
112 | handleClickOutsidePopover(sender: clickRecognizer)
113 |
114 | }
115 |
116 | @objc
117 | func startScreenshot() {
118 | // Screenshot area selection already in progress
119 | guard overlayWindow == nil else {
120 | return
121 | }
122 |
123 | // This is expected to be visible if we take the second screenshot in a row
124 | // We will re-show this after the screenshot is complete - or we cancel (might not work right now)
125 | currentPreviewPanel?.orderOut(nil)
126 |
127 | // Configure and show screenshot area selection
128 | let screenRect = NSScreen.main?.frame ?? NSRect.zero
129 | let screenshotAreaSelectionNoninteractiveWindow = ScreenshotAreaSelectionNonactivatingPanel(contentRect: screenRect)
130 |
131 | screenshotAreaSelectionNoninteractiveWindow.onComplete = { [weak self] capturedImageData in
132 | // If image data is nil:
133 | // - We canceled by either clicking and doing a single pixel selection
134 | // - Or by pressing escape
135 | // Either way show the stack, unless its empty
136 |
137 | // Always destroy the screenshot area selection panel
138 | self?.overlayWindow = nil
139 |
140 | // New screenshot arrived, we did not just cancel
141 | guard let capturedImageData, let self else {
142 | return
143 | }
144 |
145 | // Persist new one
146 | let screenshotsDirectory = screenshotHistoryUrl()
147 | // Ensure directory exixts
148 | try? FileManager.default.createDirectory(at: screenshotsDirectory, withIntermediateDirectories: false)
149 |
150 | // Cleanup old screenshots
151 | deleteOldScreenshots()
152 |
153 | let newCapturedScreenshotPath = screenshotsDirectory.appendingPathComponent(dateTimeUniqueScreenshotFileName())
154 | do {
155 | try capturedImageData.write(to: newCapturedScreenshotPath)
156 | } catch {
157 | print("error saving screenshot to history", error, "at path: ", newCapturedScreenshotPath)
158 | }
159 |
160 | // Update model
161 | stackModel.images.insert(capturedImageData, at: 0)
162 |
163 | // Only push up to fixed number of screenshots
164 | if stackModel.images.count + 1 > maxScreenshotsToShowOnStack {
165 | stackModel.images.removeLast(stackModel.images.count + 1 - maxScreenshotsToShowOnStack )
166 | }
167 |
168 | // Create panel hosting the stack if not shown
169 | if currentPreviewPanel == nil {
170 | currentPreviewPanel = ScreenshotStackPanel(stackModelState: stackModel)
171 | }
172 |
173 | // Always activate the app
174 | //
175 | // Magic configuration to show the panel, co7mbined with the panel's configuration results in
176 | // the app not taking away focus from the current app, yet still appearing.
177 | // Some of the configuraiton might be discardable - further fiddling might reveal what.
178 | NSApp.activate(ignoringOtherApps: true)
179 | currentPreviewPanel?.orderFront(nil)
180 | currentPreviewPanel?.makeFirstResponder(self.currentPreviewPanel)
181 | }
182 |
183 | screenshotAreaSelectionNoninteractiveWindow.makeKeyAndOrderFront(nil)
184 | self.overlayWindow = screenshotAreaSelectionNoninteractiveWindow
185 | }
186 |
187 | /// Show history in the same panel as we normally show users
188 | @objc
189 | func showScreenshotHistoryStack() {
190 | // To avoid overflow show only last 44
191 | let last4Screenshots = lastNScreenshots(n: 4)
192 |
193 | // Mutate model in-place
194 | stackModel.images = last4Screenshots
195 | stackModel.state = .userAskedToShowHistory
196 |
197 | let newCapturePreview = ScreenshotStackPanel(stackModelState: stackModel)
198 | NSApp.activate(ignoringOtherApps: true)
199 | newCapturePreview.orderFront(nil)
200 | newCapturePreview.makeFirstResponder(newCapturePreview)
201 | }
202 |
203 | func allScreenshotUrlsMostRecentOneIsLast() -> [URL] {
204 | let screenshotsDirectory = screenshotHistoryUrl()
205 | let urls = try? FileManager.default.contentsOfDirectory(at: screenshotsDirectory, includingPropertiesForKeys: nil)
206 | let sortedUrls = (urls ?? []).sorted { $0.path < $1.path }
207 | return sortedUrls
208 | }
209 |
210 | func lastNScreenshots(n: Int) -> [ImageData] {
211 | let urls = allScreenshotUrlsMostRecentOneIsLast()
212 | let lastN = urls.suffix(n)
213 | return lastN.compactMap { try? Data(contentsOf: $0) }
214 | }
215 |
216 | func deleteOldScreenshots() {
217 | let urls = allScreenshotUrlsMostRecentOneIsLast()
218 |
219 | // Drop the urls to keep
220 | let urlsToDelete = urls.dropLast(maxStoredHistory)
221 |
222 | for url in urlsToDelete {
223 | print("removing old screenshot at ", url)
224 | try? FileManager.default.removeItem(at: url)
225 | }
226 | }
227 |
228 | @objc private func showOnboardingView() {
229 | // Create a new NSPanel
230 | let panel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: 500, height: 500),
231 | styleMask: [.titled, .closable, .resizable],
232 | backing: .buffered,
233 | defer: false)
234 |
235 | // Create an instance of the OnboardingView with a completion handler
236 | let onboardingView = OnboardingView(onComplete: { self.startScreenshot(); panel.close()})
237 |
238 | // Create an NSHostingController with the OnboardingView as its rootView
239 | _ = NSHostingController(rootView: onboardingView)
240 |
241 | // Set panel properties
242 | panel.isFloatingPanel = true
243 | panel.worksWhenModal = true
244 | panel.isOpaque = false
245 | panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
246 | panel.contentView = NSHostingView(rootView: onboardingView)
247 | panel.center() // Center the panel on the screen
248 | panel.level = .floating // Set the panel's level to floating
249 |
250 | // Make the panel key and order it front
251 | panel.makeKeyAndOrderFront(nil)
252 | }
253 |
254 | func setupStatusBarItem() {
255 | statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
256 | let statusBarItemLogo = NSImage(named: NSImage.Name("LogoForStatusBarItem"))!
257 | statusBarItemLogo.size = NSSize(width: 18, height: 18)
258 | statusBarItem.button?.image = statusBarItemLogo
259 | statusBarItem.button?.action = #selector(togglePopover(_:))
260 | popover = NSPopover()
261 | popover.contentViewController = NSHostingController(rootView: StatusBarView(startScreenshot: startScreenshot, quitApplication: quitApplication, history: showScreenshotHistoryStack, onboarding: showOnboardingView, lastScreenshots: lastNScreenshots(n: 5)))
262 | }
263 |
264 | @objc func togglePopover(_ sender: AnyObject?) {
265 | if popover.isShown {
266 | popover.performClose(sender)
267 | if let eventMonitor = eventMonitor {
268 | NSEvent.removeMonitor(eventMonitor)
269 | self.eventMonitor = nil
270 | }
271 | } else if let button = statusBarItem.button {
272 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
273 | popover.behavior = .applicationDefined
274 |
275 | eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: .leftMouseDown) { [weak self] event in
276 | if let strongSelf = self, strongSelf.popover.isShown {
277 | strongSelf.popover.performClose(sender)
278 | if let eventMonitor = strongSelf.eventMonitor {
279 | NSEvent.removeMonitor(eventMonitor)
280 | strongSelf.eventMonitor = nil
281 | }
282 | }
283 | }
284 | }
285 | }
286 |
287 | @objc func showScreenRecordingPermissionAlert() {
288 | let alert = NSAlert()
289 | alert.messageText = "Screen Recording Permission Required"
290 | alert.informativeText = "To use this app, please grant screen recording permission in System Preferences > Security & Privacy > Privacy > Screen Recording."
291 | alert.alertStyle = .warning
292 | alert.addButton(withTitle: "Open System Preferences")
293 | alert.addButton(withTitle: "Cancel")
294 | let response = alert.runModal()
295 | if response == .alertFirstButtonReturn {
296 | CGRequestScreenCaptureAccess()
297 | }
298 | }
299 |
300 | @objc func openGitHub() {
301 | if let url = URL(string: "https://github.com/bra1nDump/macos-share-shot") {
302 | NSWorkspace.shared.open(url)
303 | }
304 | }
305 |
306 | @objc func quitApplication() {
307 | NSApplication.shared.terminate(self)
308 | }
309 |
310 | func requestAuthorizationForLoginItem() {
311 | let helperBundleIdentifier = "com.bra1ndump.share-screenshot"
312 | guard let launcherAppURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: helperBundleIdentifier) else {
313 | return
314 | }
315 |
316 | // AppleScript command to add application to login items
317 | let scriptString = "tell application \"System Events\" to make login item at end with properties {path:\"\(launcherAppURL.path)\", hidden:false}"
318 |
319 | var error: NSDictionary?
320 | if let script = NSAppleScript(source: scriptString) {
321 | script.executeAndReturnError(&error)
322 | if let error = error {
323 | print("Error adding login item: \(error)")
324 | }
325 | }
326 | }
327 |
328 | @objc func handleClickOutsidePopover(sender: NSClickGestureRecognizer) {
329 | if popover.isShown {
330 | let location = sender.location(in: nil)
331 | if !popover.contentViewController!.view.frame.contains(location) {
332 | popover.performClose(sender)
333 | }
334 | }
335 | }
336 | // Implement any other necessary AppDelegate methods here
337 |
338 | }
339 |
340 |
--------------------------------------------------------------------------------
/ShareShot/CaptureStackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureStackView.swift
3 | // CaptureSample
4 | //
5 | // Created by Oleg Yakushin on 1/4/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 | import SwiftUI
9 | import AppKit
10 | import Foundation
11 | import CloudKit
12 |
13 | // SwiftUI view for displaying captured stack of images
14 | struct CaptureStackView: View {
15 | var model: StackModel
16 | @AppStorage("onboardingShown") var onboardingShown = true
17 | // Initialize with a StackModel
18 | init(model: StackModel) {
19 | self.model = model
20 | }
21 |
22 | var body: some View {
23 | VStack {
24 | if !model.images.isEmpty {
25 | ScrollView(showsIndicators: false) {
26 | VStack(spacing: 20) {
27 | ForEach(model.images.reversed(), id: \.self) { image in
28 | ScreenShotView(image: image, saveImage: saveImage, copyImage: copyToClipboard, deleteImage: deleteImage, saveToDesktopImage: saveImageToDesktop, shareImage: shareAction)
29 | .onTapGesture {
30 | // Open the image in Preview app upon tap
31 | openImageInPreview(image: NSImage(data: image)!)
32 | }
33 | .rotated()
34 |
35 | }
36 |
37 | if onboardingShown {
38 | OnboardingScreenshot()
39 | .onAppear {
40 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
41 | onboardingShown = false
42 | }
43 | }
44 | }
45 | CloseAllButton(action: { deleteAllImage() })
46 | .padding()
47 | .rotationEffect(.degrees(180))
48 |
49 | }
50 | }
51 | .rotationEffect(.degrees(180), anchor: .center)
52 | }
53 | }
54 | .padding(.bottom, 60)
55 | .padding(20)
56 | }
57 |
58 | // Generate actions for the screenshot view
59 |
60 | // Share action to share the image
61 | private func shareAction(_ imageData: ImageData) {
62 | guard let mainWindow = ShareShotApp.appDelegate?.currentPreviewPanel?.contentView?.subviews.first?.subviews.first?.subviews.first?.subviews.first?.subviews.first?.subviews[indexForImage(imageData)!] else {
63 | print("No windows available.")
64 | return
65 | }
66 | let sharingPicker = NSSharingServicePicker(items: [NSImage(data: imageData) as Any])
67 | sharingPicker.show(relativeTo: mainWindow.bounds, of: mainWindow, preferredEdge: .minX)
68 | }
69 |
70 | // Get the index of the image in the model
71 | private func indexForImage(_ imageData: ImageData) -> Int? {
72 | model.images.firstIndex(of: imageData)
73 | }
74 |
75 | // Copy the image to clipboard
76 | private func copyToClipboard(_ image: ImageData) {
77 | guard let nsImage = NSImage(data: image) else { return }
78 | let pasteboard = NSPasteboard.general
79 | pasteboard.clearContents()
80 | pasteboard.writeObjects([nsImage])
81 | deleteImage(image)
82 | }
83 |
84 | // Save the image locally
85 | private func saveImage(_ image: ImageData) {
86 | guard let nsImage = NSImage(data: image) else { return }
87 | let savePanel = NSSavePanel()
88 | let currentDate = Date()
89 | let formattedDate = DateFormatter.localizedString(from: currentDate, dateStyle: .short, timeStyle: .short)
90 | savePanel.nameFieldStringValue = "CaptureSample - \(formattedDate).png"
91 | savePanel.message = "Select a directory to save the image"
92 | savePanel.begin { response in
93 | if response == .OK, let url = savePanel.url {
94 | let folderManager = FolderManager()
95 | folderManager.loadFromUserDefaults()
96 | folderManager.addFolderLink(name: formattedDate, url: url)
97 | guard let tiffData = nsImage.tiffRepresentation,
98 | let bitmapImageRep = NSBitmapImageRep(data: tiffData),
99 | let imageData = bitmapImageRep.representation(using: .png, properties: [:]) else { return }
100 | do {
101 | try imageData.write(to: url)
102 | deleteImage(image)
103 | print("Image saved")
104 | } catch {
105 | print("Error saving image: \(error)")
106 | }
107 | #if SANDBOX
108 | folderManager.saveToUserDefaults()
109 | print(folderManager.getRecentFolders())
110 | #endif
111 | }
112 | }
113 | }
114 |
115 | // Save the image locally and return its URL
116 | private func saveImageLocally(_ image: ImageData) -> URL? {
117 | guard let nsImage = NSImage(data: image) else {
118 | print("Unable to convert ImageData to NSImage.")
119 | return nil
120 | }
121 | let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
122 | let fileName = "ShareShot_\(UUID().uuidString).png"
123 | guard let fileURL = documentsDirectory?.appendingPathComponent(fileName) else { return nil }
124 | guard let tiffData = nsImage.tiffRepresentation,
125 | let bitmapImageRep = NSBitmapImageRep(data: tiffData),
126 | let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else {
127 | print("Error converting image to PNG format.")
128 | return nil
129 | }
130 | do {
131 | try pngData.write(to: fileURL)
132 | return fileURL
133 | } catch {
134 | print("Error saving image locally: \(error)")
135 | return nil
136 | }
137 | }
138 | // Delete the image from the model
139 | private func deleteImage(_ image: ImageData) {
140 | model.images.removeAll(where: { $0 == image })
141 | }
142 |
143 | private func deleteAllImage() {
144 | model.images.removeAll()
145 | }
146 |
147 | // Open the image in Preview app
148 | private func openImageInPreview(image: NSImage) {
149 | let temporaryDirectoryURL = FileManager.default.temporaryDirectory
150 | let temporaryImageURL = temporaryDirectoryURL.appendingPathComponent("ShareShot.png")
151 | if let imageData = image.tiffRepresentation, let bitmapRep = NSBitmapImageRep(data: imageData) {
152 | if let pngData = bitmapRep.representation(using: .png, properties: [:]) {
153 | do {
154 | try pngData.write(to: temporaryImageURL)
155 | } catch {
156 | print("Failed to save temporary image: \(error)")
157 | return
158 | }
159 | }
160 | }
161 | NSWorkspace.shared.open(temporaryImageURL)
162 | }
163 |
164 | // Save the image to desktop (sandbox only)
165 | private func saveImageToDesktop(_ image: ImageData) {
166 | guard let desktopURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
167 | print("Unable to access desktop directory.")
168 | return
169 | }
170 | let fileName = dateTimeUniqueScreenshotFileName()
171 | let filePath = desktopURL.appendingPathComponent(fileName)
172 | saveImageAsPng(image: image, at: filePath)
173 | }
174 |
175 | // Generate a unique filename based on date and time
176 | private func dateTimeUniqueScreenshotFileName() -> String {
177 | let currentDate = Date()
178 | let formatter = DateFormatter()
179 | formatter.dateFormat = "yyyyMMdd_HHmmss"
180 | return "ShareShot_\(formatter.string(from: currentDate)).png"
181 | }
182 | }
183 |
184 | // MARK: View Extensions
185 |
186 | private extension View {
187 | func rotated() -> some View {
188 | self.rotationEffect(.degrees(180))
189 | }
190 | }
191 |
192 | private struct CloseAllButton: View {
193 | var action: () -> Void
194 |
195 | var body: some View {
196 | RoundedRectangle(cornerRadius: 15)
197 | .frame(width: 100, height: 40)
198 | .foregroundColor(.white.opacity(0.7))
199 | .overlay(
200 | Text("Close All")
201 | .font(.title2)
202 | .foregroundColor(.black)
203 | )
204 | .onTapGesture {
205 | action()
206 | }
207 | }
208 | }
209 |
210 | // View for onboarding screenshot example
211 | struct OnboardingScreenshot: View {
212 | var body: some View {
213 | RoundedRectangle(cornerRadius: 20)
214 | .frame(width: 201, height: 152)
215 | .foregroundColor(.gray.opacity(0.7))
216 | .overlay(
217 | Text("Use ⇧⌘7 for screenshot") // Display instructions for screenshot shortcut
218 | .bold()
219 | )
220 | .rotationEffect(.degrees(180))
221 | }
222 | }
223 |
224 |
--------------------------------------------------------------------------------
/ShareShot/DragAndDrop.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DragAndDrop.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 5/15/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Cocoa
11 |
12 | class DragDropView: NSView {
13 |
14 | // Convenience initializer to reduce redundancy
15 | override init(frame frameRect: NSRect) {
16 | super.init(frame: frameRect)
17 | setup()
18 | }
19 |
20 | required init?(coder: NSCoder) {
21 | super.init(coder: coder)
22 | setup()
23 | }
24 |
25 | // Setup function for common initialization tasks
26 | private func setup() {
27 | registerForDraggedTypes([.fileURL])
28 | }
29 |
30 | // Draw the view
31 | override func draw(_ dirtyRect: NSRect) {
32 | super.draw(dirtyRect)
33 | NSColor.white.setFill()
34 | dirtyRect.fill()
35 | }
36 |
37 | // Invoked when a drag enters the view
38 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
39 | return shouldAllowDrag(sender) ? .copy : []
40 | }
41 |
42 | // Invoked when a drag operation is performed
43 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
44 | guard let fileURL = getURL(from: sender) else { return false }
45 | print("Received file: \(fileURL.path)")
46 | return true
47 | }
48 |
49 | // Check if drag should be allowed
50 | private func shouldAllowDrag(_ draggingInfo: NSDraggingInfo) -> Bool {
51 | guard let types = draggingInfo.draggingPasteboard.types else { return false }
52 | return types.contains(.fileURL)
53 | }
54 |
55 | // Get the URL from dragging info
56 | private func getURL(from draggingInfo: NSDraggingInfo) -> URL? {
57 | let classes = [NSURL.self]
58 | let options: [NSPasteboard.ReadingOptionKey: Any] = [.urlReadingFileURLsOnly: true]
59 | return draggingInfo.draggingPasteboard.readObjects(forClasses: classes, options: options)?.first as? URL
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ShareShot/HistoryStackPanel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HistoryStackPanel.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 4/15/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | class HistoryStackPanel: NSPanel {
13 | init(stackModelState: StackModel) {
14 | let screenSize = NSScreen.main?.frame.size ?? CGSize(width: 300, height: 950)
15 | let panelWidth: CGFloat = 300
16 | let panelHeight: CGFloat = 950
17 | let panelOriginX = (screenSize.width - panelWidth) / 2 // Center horizontally
18 | let panelOriginY = screenSize.height - panelHeight // Align with the top of the screen
19 | let previewRect = NSRect(x: panelOriginX, y: panelOriginY, width: panelWidth, height: panelHeight)
20 | super.init(contentRect: previewRect, styleMask: [.borderless, .fullSizeContentView], backing: .buffered, defer: false)
21 | self.backgroundColor = NSColor.clear
22 | self.isFloatingPanel = true
23 | self.worksWhenModal = true
24 | self.isOpaque = false
25 | self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
26 | self.hidesOnDeactivate = false
27 | let hostingView = NSHostingView(rootView: HistoryStackView(model: stackModelState))
28 | hostingView.frame = previewRect
29 |
30 | if let contentView = self.contentView {
31 | contentView.addSubview(hostingView)
32 | } else {
33 | print("Warning: contentView is nil.")
34 | }
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/ShareShot/HistoryStackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HistoryStackView.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 4/15/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import AppKit
11 |
12 | // SwiftUI view for displaying history stack of images
13 | struct HistoryStackView: View {
14 | var model: StackModel
15 | // Initialize with a StackModel
16 | init(model: StackModel) {
17 | self.model = model
18 | }
19 |
20 | var body: some View {
21 | let capturedImages = model.images
22 | RoundedRectangle(cornerRadius: 10)
23 | .foregroundColor(.black.opacity(0.5))
24 | .overlay(
25 | ScrollView(.horizontal) {
26 | HStack {
27 | ForEach(capturedImages.reversed(), id: \.self) { image in
28 | // Custom view to display screenshot
29 | ScreenShotView(image: image, saveImage: {_ in }, copyImage: {_ in }, deleteImage: {_ in }, saveToDesktopImage: {_ in }, shareImage: {_ in }, saveToiCloud: {_ in })
30 | .rotationEffect(.degrees(180))
31 | }
32 | }
33 | }
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ShareShot/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | com.apple.security.app-sandbox
11 |
12 | com.apple.security.files.user-selected.read-write
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ShareShot/KeyboardAndMouseEventMonitors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAndMouseEventMonitors.swift
3 | // ShareShot
4 | //
5 | // Created by Kirill Dubovitskiy on 3/19/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import Carbon
11 |
12 | import Combine
13 |
14 | class KeyboardAndMouseEventMonitors: ObservableObject {
15 | private var globalMonitors: [Any?] = []
16 |
17 | func startMonitoringEvents(
18 | onMouseDown: @escaping (CGPoint) -> Void,
19 | onMouseMove: @escaping (CGPoint) -> Void,
20 | onMouseUp: @escaping (CGPoint) -> Void,
21 | onEscape: @escaping () -> Void
22 | ) {
23 | // Ensure no duplicate monitors
24 | stopMonitoringEvents()
25 |
26 | func adaptorEventToMousePosition(_ handler: @escaping (CGPoint) -> Void) -> (NSEvent) -> NSEvent {
27 | return { (event: NSEvent) in
28 | guard let window = event.window else { return event }
29 | let point = convertToSwiftUICoordinates(event.locationInWindow, in: window)
30 | handler(point)
31 |
32 | return event
33 | }
34 | }
35 |
36 | globalMonitors = [
37 | // This will only work when mouse is not down, we still need it for anchor placement stage
38 | NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved], handler: adaptorEventToMousePosition(onMouseMove)),
39 |
40 | // Drag will be emited while mouse is down
41 | // IDEA: right mouse dragged for capturing video / creating link by default
42 | NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDragged, .rightMouseDragged, .otherMouseDragged], handler: adaptorEventToMousePosition(onMouseMove)),
43 |
44 | NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown], handler: adaptorEventToMousePosition(onMouseDown)),
45 |
46 | NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp], handler: adaptorEventToMousePosition(onMouseUp)),
47 |
48 | // Catch Escape by watching all keyboard presses
49 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
50 | if Int(event.keyCode) == kVK_Escape {
51 | onEscape()
52 |
53 | // To avoid the beep
54 | return nil
55 | } else {
56 | return nil
57 | }
58 | },
59 | ]
60 | }
61 |
62 | func stopMonitoringEvents() {
63 | for monitor in globalMonitors {
64 | if let monitor {
65 | NSEvent.removeMonitor(monitor)
66 | }
67 | }
68 | globalMonitors.removeAll()
69 | }
70 |
71 | // Why is this not being called?!
72 | deinit {
73 | stopMonitoringEvents()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ShareShot/OnboardingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingView.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 3/23/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct OnboardingView: View {
12 | @State private var currentPage = 0
13 | @State private var backgroundColor: Color = .blue
14 | @State private var nextButtonColor: Color = .red
15 | let onComplete: () -> Void
16 |
17 | let screens: [OnboardingScreen] = [
18 | OnboardingScreen(imageName: "Logo",
19 | title: "Welcome to ShareShot!",
20 | description: "Let's guide you through a quick setup and tailor ShareShot to your preferences."),
21 | OnboardingScreen(imageName: "command",
22 | title: "Shortcut for Screenshots",
23 | description: "Use ⇧⌘7 to capture screenshots."),
24 | OnboardingScreen(imageName: "plus.square.on.square",
25 | title: "Drag and Drop",
26 | description: "Drag and drop options available in the status bar."),
27 | OnboardingScreen(imageName: "gear",
28 | title: "Set Your Preferences",
29 | description: "Customize settings from the status bar.")
30 | ]
31 |
32 | let colors: [Color] = [.red, .green, .blue, .orange, .yellow] // Example colors
33 |
34 | var body: some View {
35 | GeometryReader { geometry in
36 | ZStack {
37 | backgroundColor.edgesIgnoringSafeArea(.all)
38 |
39 | VStack {
40 | OnboardingScreenView(screen: screens[currentPage])
41 | .padding(.top, geometry.size.height * 0.1)
42 |
43 | Spacer()
44 |
45 | PageControl(numberOfPages: screens.count, currentPage: $currentPage)
46 | .padding(.bottom)
47 |
48 | Button(action: handleNextButton) {
49 | Label(currentPage == screens.count - 1 ? "Start" : "Next", systemImage: "arrow.right.circle.fill")
50 | .font(.headline)
51 | .padding()
52 | .foregroundColor(.white)
53 | .background(nextButtonColor)
54 | .clipShape(RoundedRectangle(cornerRadius: 10))
55 | .shadow(radius: 5)
56 | }
57 | .padding()
58 | }
59 | }
60 | }
61 | .frame(maxWidth: .infinity, maxHeight: .infinity)
62 | }
63 |
64 | private func handleNextButton() {
65 | withAnimation {
66 | if currentPage == screens.count - 1 {
67 | onComplete()
68 | } else {
69 | currentPage += 1
70 | backgroundColor = colors[currentPage % colors.count]
71 | nextButtonColor = colors[(currentPage + 1) % colors.count]
72 | }
73 | }
74 | }
75 | }
76 |
77 | struct PageControl: View {
78 | var numberOfPages: Int
79 | @Binding var currentPage: Int
80 |
81 | var body: some View {
82 | HStack {
83 | ForEach(0.. URL {
13 | FileManager.default.homeDirectoryForCurrentUser.appending(component: "screenshots")
14 | }
15 |
16 | func dateTimeUniqueScreenshotFileName() -> String {
17 | let currentDate = Date()
18 | let dateFormatter = DateFormatter()
19 | dateFormatter.dateFormat = "yy:MM:dd - HH:mm:ss"
20 | let formattedDate = dateFormatter.string(from: currentDate)
21 | let fileName = "ShareScreenshot_\(formattedDate).png"
22 | return fileName
23 | }
24 |
25 | func saveImageAsPng(image: ImageData, at fileURL: URL) {
26 | do {
27 | guard let nsImage = NSImage(data: image) else {
28 | print("Unable to convert ImageData to NSImage.")
29 | return
30 | }
31 |
32 | let imageData: Data
33 | if let tiffData = nsImage.tiffRepresentation,
34 | let bitmapImageRep = NSBitmapImageRep(data: tiffData) {
35 | imageData = bitmapImageRep.representation(using: .png, properties: [:]) ?? Data()
36 | } else {
37 | print("Error converting image to PNG format.")
38 | return
39 | }
40 |
41 | try imageData.write(to: fileURL)
42 | print("Image saved at \(fileURL.absoluteString)")
43 | } catch {
44 | print("Error saving image: \(error)")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ShareShot/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/AudioLevelsView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A view that renders an audio level meter.
6 | */
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct AudioLevelsView: NSViewRepresentable {
12 |
13 | @StateObject var audioLevelsProvider: AudioLevelsProvider
14 |
15 | func makeNSView(context: Context) -> NSLevelIndicator {
16 | let levelIndicator = NSLevelIndicator(frame: .zero)
17 | levelIndicator.minValue = 0
18 | levelIndicator.maxValue = 10
19 | levelIndicator.warningValue = 6
20 | levelIndicator.criticalValue = 8
21 | levelIndicator.levelIndicatorStyle = .continuousCapacity
22 | levelIndicator.heightAnchor.constraint(equalToConstant: 5).isActive = true
23 | return levelIndicator
24 | }
25 |
26 | func updateNSView(_ levelMeter: NSLevelIndicator, context: Context) {
27 | levelMeter.floatValue = audioLevelsProvider.audioLevels.level * 10
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/CapturePreview.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A view that renders a video frame.
6 | */
7 |
8 | import SwiftUI
9 |
10 | struct CapturePreview: NSViewRepresentable {
11 |
12 | // A layer that renders the video contents.
13 | private let contentLayer = CALayer()
14 |
15 | init() {
16 | contentLayer.contentsGravity = .resizeAspect
17 | }
18 |
19 | func makeNSView(context: Context) -> CaptureVideoPreview {
20 | CaptureVideoPreview(layer: contentLayer)
21 | }
22 |
23 | // The view isn't updatable. Updates to the layer's content are done in outputFrame(frame:).
24 | func updateNSView(_ nsView: CaptureVideoPreview, context: Context) {}
25 |
26 | class CaptureVideoPreview: NSView {
27 | // Create the preview with the video layer as the backing layer.
28 | init(layer: CALayer) {
29 | super.init(frame: .zero)
30 | // Make this a layer-hosting view. First set the layer, then set wantsLayer to true.
31 | self.layer = layer
32 | wantsLayer = true
33 | }
34 |
35 | required init?(coder: NSCoder) {
36 | fatalError("init(coder:) has not been implemented")
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/ConfigurationView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A view that provides the UI to configure screen capture.
6 | */
7 |
8 | import SwiftUI
9 | import ScreenCaptureKit
10 |
11 | import LaunchAtLogin
12 |
13 | /// The app's configuration user interface.
14 | struct ConfigurationView: View {
15 |
16 | private let sectionSpacing: CGFloat = 20
17 | private let verticalLabelSpacing: CGFloat = 8
18 |
19 | private let alignmentOffset: CGFloat = 10
20 |
21 | @StateObject private var audioPlayer = AudioPlayer()
22 | @ObservedObject var screenRecorder: ScreenRecorder
23 | @Binding var userStopped: Bool
24 |
25 | var body: some View {
26 | VStack {
27 | Form {
28 | HeaderView("Video")
29 | .padding(EdgeInsets(top: 0, leading: 0, bottom: 1, trailing: 0))
30 |
31 | // A group that hides view labels.
32 | Group {
33 | VStack(alignment: .leading, spacing: verticalLabelSpacing) {
34 | Text("Capture Type")
35 | Picker("Capture", selection: $screenRecorder.captureType) {
36 | Text("Display")
37 | .tag(ScreenRecorder.CaptureType.display)
38 | Text("Window")
39 | .tag(ScreenRecorder.CaptureType.window)
40 | }
41 | }
42 |
43 | VStack(alignment: .leading, spacing: verticalLabelSpacing) {
44 | Text("Screen Content")
45 | switch screenRecorder.captureType {
46 | case .display:
47 | Picker("Display", selection: $screenRecorder.selectedDisplay) {
48 | ForEach(screenRecorder.availableDisplays, id: \.self) { display in
49 | Text(display.displayName)
50 | .tag(SCDisplay?.some(display))
51 | }
52 | }
53 |
54 | case .window:
55 | Picker("Window", selection: $screenRecorder.selectedWindow) {
56 | ForEach(screenRecorder.availableWindows, id: \.self) { window in
57 | Text(window.displayName)
58 | .tag(SCWindow?.some(window))
59 | }
60 | }
61 | }
62 | }
63 | }
64 | .labelsHidden()
65 |
66 | LaunchAtLogin.Toggle("Launch at login 🦄")
67 |
68 | Toggle("Exclude sample app from stream", isOn: $screenRecorder.isAppExcluded)
69 | .disabled(screenRecorder.captureType == .window)
70 | .onChange(of: screenRecorder.isAppExcluded) { _ in
71 | // Capturing app audio is only possible when the sample is included in the stream.
72 | // Ensure the audio stops playing if the user enables the "Exclude app from stream" checkbox.
73 | if screenRecorder.isAppExcluded {
74 | audioPlayer.stop()
75 | }
76 | }
77 |
78 | // Add some space between the Video and Audio sections.
79 | Spacer()
80 | .frame(height: 20)
81 |
82 | HeaderView("Audio")
83 |
84 | Toggle("Capture audio", isOn: $screenRecorder.isAudioCaptureEnabled)
85 | Toggle("Exclude app audio", isOn: $screenRecorder.isAppAudioExcluded)
86 | .disabled(screenRecorder.isAppExcluded)
87 | AudioLevelsView(audioLevelsProvider: screenRecorder.audioLevelsProvider)
88 | Button {
89 | if !audioPlayer.isPlaying {
90 | audioPlayer.play()
91 | } else {
92 | audioPlayer.stop()
93 | }
94 | } label: {
95 | Text("\(!audioPlayer.isPlaying ? "Play" : "Stop") App Audio")
96 | }
97 | .disabled(screenRecorder.isAppExcluded)
98 | Spacer()
99 | }
100 | .padding()
101 |
102 | Spacer()
103 | HStack {
104 | Button {
105 | Task { await screenRecorder.start() }
106 | // Fades the paused screen out.
107 | withAnimation(Animation.easeOut(duration: 0.25)) {
108 | userStopped = false
109 | }
110 | } label: {
111 | Text("Start Capture")
112 | }
113 | .disabled(screenRecorder.isRunning)
114 | Button {
115 | Task { await screenRecorder.stop() }
116 | // Fades the paused screen in.
117 | withAnimation(Animation.easeOut(duration: 0.25)) {
118 | userStopped = true
119 | }
120 |
121 | } label: {
122 | Text("Stop Capture")
123 | }
124 | .disabled(!screenRecorder.isRunning)
125 | }
126 | .frame(maxWidth: .infinity, minHeight: 60)
127 | }
128 | .background(MaterialView())
129 | }
130 | }
131 |
132 | /// A view that displays a styled header for the Video and Audio sections.
133 | struct HeaderView: View {
134 |
135 | private let title: String
136 | private let alignmentOffset: CGFloat = 10.0
137 |
138 | init(_ title: String) {
139 | self.title = title
140 | }
141 |
142 | var body: some View {
143 | Text(title)
144 | .font(.headline)
145 | .foregroundColor(.secondary)
146 | .alignmentGuide(.leading) { _ in alignmentOffset }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/FolderLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FolderLink.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 3/3/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Structure to represent a folder link
12 | struct FolderLink: Codable {
13 | var name: String
14 | var url: URL
15 | }
16 |
17 | // Manager class for handling recent folders
18 | class FolderManager {
19 | // Array to store recent folder links
20 | private var recentFolders: [FolderLink] = []
21 | // Maximum number of recent folders to keep
22 | private let maxRecentFoldersCount = 3
23 |
24 | // Add a folder link to recent folders
25 | func addFolderLink(name: String, url: URL) {
26 | let newLink = FolderLink(name: name, url: url)
27 | // Check if the folder already exists, remove it before adding to keep it unique
28 | if let existingIndex = recentFolders.firstIndex(where: { $0.url == url }) {
29 | recentFolders.remove(at: existingIndex)
30 | }
31 | // Insert the new folder link at the beginning of the array
32 | recentFolders.insert(newLink, at: 0)
33 | // Remove the last folder link if the count exceeds the maximum
34 | if recentFolders.count > maxRecentFoldersCount {
35 | recentFolders.removeLast()
36 | }
37 | // Save the recent folders to UserDefaults
38 | saveToUserDefaults()
39 | }
40 |
41 | // Retrieve the recent folders
42 | func getRecentFolders() -> [FolderLink] {
43 | return recentFolders
44 | }
45 |
46 | // Save recent folders to UserDefaults
47 | func saveToUserDefaults() {
48 | do {
49 | let encoder = JSONEncoder()
50 | let data = try encoder.encode(recentFolders)
51 | UserDefaults.standard.set(data, forKey: "recentFolders")
52 | } catch {
53 | print("Failed to save recent folders to UserDefaults: \(error)")
54 | }
55 | }
56 |
57 | // Load recent folders from UserDefaults
58 | func loadFromUserDefaults() {
59 | if let data = UserDefaults.standard.data(forKey: "recentFolders") {
60 | do {
61 | let decoder = JSONDecoder()
62 | recentFolders = try decoder.decode([FolderLink].self, from: data)
63 | } catch {
64 | print("Failed to load recent folders from UserDefaults: \(error)")
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/ImageEditorWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageEditorWindow.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 3/3/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Cocoa
11 | import UniformTypeIdentifiers
12 |
13 | // Window controller for image editing
14 | class ImageEditorWindowController: NSWindowController, NSWindowDelegate {
15 | private var imageView: NSImageView!
16 | var openImage: ((ImageData) -> Void)?
17 |
18 | // Convenience initializer to set up the window
19 | convenience init() {
20 | let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
21 | styleMask: [.closable, .resizable, .miniaturizable, .fullSizeContentView],
22 | backing: .buffered,
23 | defer: false)
24 | self.init(window: window)
25 |
26 | window.title = "Image Editor"
27 | window.delegate = self
28 | window.center()
29 |
30 | setupUI()
31 | }
32 |
33 | // Setup UI elements within the window
34 | private func setupUI() {
35 | imageView = NSImageView(frame: NSRect(x: 20, y: 20, width: 400, height: 300))
36 | imageView.imageScaling = .scaleProportionallyUpOrDown
37 | imageView.imageAlignment = .alignCenter
38 | window?.contentView?.addSubview(imageView)
39 |
40 | let openButton = NSButton(title: "Open Image", target: self, action: #selector(openImage(_:)))
41 | openButton.frame = NSRect(x: 440, y: 250, width: 140, height: 30)
42 | window?.contentView?.addSubview(openButton)
43 |
44 | let saveButton = NSButton(title: "Save Image", target: self, action: #selector(saveImage(_:)))
45 | saveButton.frame = NSRect(x: 440, y: 200, width: 140, height: 30)
46 | window?.contentView?.addSubview(saveButton)
47 | }
48 |
49 | // Action method to open an image
50 | @objc private func openImage(_ sender: Any) {
51 | let openPanel = NSOpenPanel()
52 | let allowedTypes: [UTType] = [.jpeg, .png]
53 | openPanel.allowedContentTypes = allowedTypes
54 |
55 | openPanel.begin { response in
56 | if response == .OK, let imageURL = openPanel.url {
57 | let image = NSImage(contentsOf: imageURL)
58 | self.imageView.image = image
59 | }
60 | }
61 | }
62 |
63 | // Action method to save the edited image
64 | @objc private func saveImage(_ sender: Any) {
65 | guard let image = imageView.image else {
66 | return
67 | }
68 |
69 | let savePanel = NSSavePanel()
70 | let allowedTypes: [UTType] = [.jpeg, .png]
71 | savePanel.allowedContentTypes = allowedTypes
72 | savePanel.begin { response in
73 |
74 | if response == .OK, let saveURL = savePanel.url {
75 | guard let data = image.tiffRepresentation else {
76 | // Handle the case when there is an issue with image representation
77 | return
78 | }
79 |
80 | do {
81 | try data.write(to: saveURL, options: .atomic)
82 | } catch {
83 | // Handle the error while saving
84 | print("Error saving image: \(error)")
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/MaterialView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A wrapper view around NSVisualEffectView.
6 | */
7 |
8 | import SwiftUI
9 |
10 | struct MaterialView: NSViewRepresentable {
11 |
12 | func makeNSView(context: Context) -> NSVisualEffectView {
13 | let view = NSVisualEffectView()
14 | view.blendingMode = .behindWindow
15 | return view
16 | }
17 |
18 | func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
19 | }
20 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/PinScreenShotView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PinScreenShotView.swift
3 | // CaptureSample
4 | //
5 | // Created by Oleg Yakushin on 1/21/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct PinScreenShotView: View {
12 | var body: some View {
13 | Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
14 | }
15 | }
16 |
17 | #Preview {
18 | PinScreenShotView()
19 | }
20 |
--------------------------------------------------------------------------------
/ShareShot/Prorotypes/iCloudURL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // iCloudURL.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 4/7/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
--------------------------------------------------------------------------------
/ShareShot/SampleCodeFromCaptureExample/AudioPlayer.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | An object that holds an AVAudioPlayer that plays an AIFF file.
6 | */
7 |
8 | import Foundation
9 | import AVFoundation
10 |
11 | class AudioPlayer: ObservableObject {
12 |
13 | let audioPlayer: AVAudioPlayer
14 |
15 | @Published var isPlaying = false
16 |
17 | init() {
18 | guard let url = Bundle.main.url(forResource: "Synth", withExtension: "aif") else {
19 | fatalError("Couldn't find Synth.aif in the app bundle.")
20 | }
21 | audioPlayer = try! AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.aiff.rawValue)
22 | audioPlayer.numberOfLoops = -1 // Loop indefinitely.
23 | audioPlayer.prepareToPlay()
24 | }
25 |
26 | func play() {
27 | audioPlayer.play()
28 | isPlaying = true
29 | }
30 |
31 | func stop() {
32 | audioPlayer.stop()
33 | isPlaying = false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ShareShot/SampleCodeFromCaptureExample/CaptureEngine.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | An object that captures a stream of captured sample buffers containing screen and audio content.
6 | */
7 |
8 | import Foundation
9 | import AVFAudio
10 | import ScreenCaptureKit
11 | import OSLog
12 | import Combine
13 |
14 | /// A structure that contains the video data to render.
15 | struct CapturedFrame {
16 | static let invalid = CapturedFrame(surface: nil, contentRect: .zero, contentScale: 0, scaleFactor: 0)
17 |
18 | let surface: IOSurface?
19 | let contentRect: CGRect
20 | let contentScale: CGFloat
21 | let scaleFactor: CGFloat
22 | var size: CGSize { contentRect.size }
23 | }
24 |
25 | /// An object that wraps an instance of `SCStream`, and returns its results as an `AsyncThrowingStream`.
26 | class CaptureEngine: NSObject, @unchecked Sendable {
27 |
28 | private let logger = Logger()
29 |
30 | private var stream: SCStream?
31 | private let videoSampleBufferQueue = DispatchQueue(label: "com.example.apple-samplecode.VideoSampleBufferQueue")
32 | private let audioSampleBufferQueue = DispatchQueue(label: "com.example.apple-samplecode.AudioSampleBufferQueue")
33 |
34 | // Performs average and peak power calculations on the audio samples.
35 | private let powerMeter = PowerMeter()
36 | var audioLevels: AudioLevels { powerMeter.levels }
37 |
38 | // Store the the startCapture continuation, so that you can cancel it when you call stopCapture().
39 | private var continuation: AsyncThrowingStream.Continuation?
40 |
41 | /// - Tag: StartCapture
42 | func startCapture(configuration: SCStreamConfiguration, filter: SCContentFilter) -> AsyncThrowingStream {
43 | AsyncThrowingStream { continuation in
44 | // The stream output object.
45 | let streamOutput = CaptureEngineStreamOutput(continuation: continuation)
46 | streamOutput.capturedFrameHandler = { continuation.yield($0) }
47 | streamOutput.pcmBufferHandler = { self.powerMeter.process(buffer: $0) }
48 |
49 | do {
50 | stream = SCStream(filter: filter, configuration: configuration, delegate: streamOutput)
51 |
52 | // Add a stream output to capture screen content.
53 | try stream?.addStreamOutput(streamOutput, type: .screen, sampleHandlerQueue: videoSampleBufferQueue)
54 | try stream?.addStreamOutput(streamOutput, type: .audio, sampleHandlerQueue: audioSampleBufferQueue)
55 | stream?.startCapture()
56 | } catch {
57 | continuation.finish(throwing: error)
58 | }
59 | }
60 | }
61 |
62 | func stopCapture() async {
63 | do {
64 | try await stream?.stopCapture()
65 | continuation?.finish()
66 | } catch {
67 | continuation?.finish(throwing: error)
68 | }
69 | powerMeter.processSilence()
70 | }
71 |
72 | /// - Tag: UpdateStreamConfiguration
73 | func update(configuration: SCStreamConfiguration, filter: SCContentFilter) async {
74 | do {
75 | try await stream?.updateConfiguration(configuration)
76 | try await stream?.updateContentFilter(filter)
77 | } catch {
78 | logger.error("Failed to update the stream session: \(String(describing: error))")
79 | }
80 | }
81 | }
82 |
83 | /// A class that handles output from an SCStream, and handles stream errors.
84 | private class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDelegate {
85 |
86 | var pcmBufferHandler: ((AVAudioPCMBuffer) -> Void)?
87 | var capturedFrameHandler: ((CapturedFrame) -> Void)?
88 |
89 | // Store the the startCapture continuation, so you can cancel it if an error occurs.
90 | private var continuation: AsyncThrowingStream.Continuation?
91 |
92 | init(continuation: AsyncThrowingStream.Continuation?) {
93 | self.continuation = continuation
94 | }
95 |
96 | /// - Tag: DidOutputSampleBuffer
97 | func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) {
98 |
99 | // Return early if the sample buffer is invalid.
100 | guard sampleBuffer.isValid else { return }
101 |
102 | // Determine which type of data the sample buffer contains.
103 | switch outputType {
104 | case .screen:
105 | // Create a CapturedFrame structure for a video sample buffer.
106 | guard let frame = createFrame(for: sampleBuffer) else { return }
107 | capturedFrameHandler?(frame)
108 | case .audio:
109 | // Create an AVAudioPCMBuffer from an audio sample buffer.
110 | guard let samples = createPCMBuffer(for: sampleBuffer) else { return }
111 | pcmBufferHandler?(samples)
112 | @unknown default:
113 | fatalError("Encountered unknown stream output type: \(outputType)")
114 | }
115 | }
116 |
117 | /// Create a `CapturedFrame` for the video sample buffer.
118 | private func createFrame(for sampleBuffer: CMSampleBuffer) -> CapturedFrame? {
119 |
120 | // Retrieve the array of metadata attachments from the sample buffer.
121 | guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,
122 | createIfNecessary: false) as? [[SCStreamFrameInfo: Any]],
123 | let attachments = attachmentsArray.first else { return nil }
124 |
125 | // Validate the status of the frame. If it isn't `.complete`, return nil.
126 | guard let statusRawValue = attachments[SCStreamFrameInfo.status] as? Int,
127 | let status = SCFrameStatus(rawValue: statusRawValue),
128 | status == .complete else { return nil }
129 |
130 | // Get the pixel buffer that contains the image data.
131 | guard let pixelBuffer = sampleBuffer.imageBuffer else { return nil }
132 |
133 | // Get the backing IOSurface.
134 | guard let surfaceRef = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else { return nil }
135 | let surface = unsafeBitCast(surfaceRef, to: IOSurface.self)
136 |
137 | // Retrieve the content rectangle, scale, and scale factor.
138 | guard let contentRectDict = attachments[.contentRect],
139 | let contentRect = CGRect(dictionaryRepresentation: contentRectDict as! CFDictionary),
140 | let contentScale = attachments[.contentScale] as? CGFloat,
141 | let scaleFactor = attachments[.scaleFactor] as? CGFloat else { return nil }
142 |
143 | // Create a new frame with the relevant data.
144 | let frame = CapturedFrame(surface: surface,
145 | contentRect: contentRect,
146 | contentScale: contentScale,
147 | scaleFactor: scaleFactor)
148 | return frame
149 | }
150 |
151 | // Creates an AVAudioPCMBuffer instance on which to perform an average and peak audio level calculation.
152 | private func createPCMBuffer(for sampleBuffer: CMSampleBuffer) -> AVAudioPCMBuffer? {
153 | var ablPointer: UnsafePointer?
154 | try? sampleBuffer.withAudioBufferList { audioBufferList, blockBuffer in
155 | ablPointer = audioBufferList.unsafePointer
156 | }
157 | guard let audioBufferList = ablPointer,
158 | let absd = sampleBuffer.formatDescription?.audioStreamBasicDescription,
159 | let format = AVAudioFormat(standardFormatWithSampleRate: absd.mSampleRate, channels: absd.mChannelsPerFrame) else { return nil }
160 | return AVAudioPCMBuffer(pcmFormat: format, bufferListNoCopy: audioBufferList)
161 | }
162 |
163 | func stream(_ stream: SCStream, didStopWithError error: Error) {
164 | continuation?.finish(throwing: error)
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/ShareShot/SampleCodeFromCaptureExample/ContentView.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | The app's main view.
6 | */
7 |
8 | import SwiftUI
9 | import ScreenCaptureKit
10 | import OSLog
11 | import Combine
12 |
13 | import LaunchAtLogin
14 | import HotKey
15 |
16 | struct ContentView: View {
17 | @State var userStopped = false
18 | @State var disableInput = false
19 | @State var isUnauthorized = false
20 |
21 | @StateObject var screenRecorder = ScreenRecorder()
22 |
23 | var body: some View {
24 | HSplitView {
25 |
26 | // TODO: Put the screenshot here for debugging? I mean why not just place it in the bottom left?
27 | screenRecorder.capturePreview
28 | .frame(maxWidth: .infinity, maxHeight: .infinity)
29 | .aspectRatio(screenRecorder.contentSize, contentMode: .fit)
30 | .padding(8)
31 | .overlay {
32 | if userStopped {
33 | Image(systemName: "nosign")
34 | .font(.system(size: 250, weight: .bold))
35 | .foregroundColor(Color(white: 0.3, opacity: 1.0))
36 | .frame(maxWidth: .infinity, maxHeight: .infinity)
37 | .background(Color(white: 0.0, opacity: 0.5))
38 | }
39 | }
40 | }
41 | .overlay {
42 | if isUnauthorized {
43 | VStack() {
44 | Spacer()
45 | VStack {
46 | Text("No screen recording permission.")
47 | .font(.largeTitle)
48 | .padding(.top)
49 | Text("Open System Settings and go to Privacy & Security > Screen Recording to grant permission.")
50 | .font(.title2)
51 | .padding(.bottom)
52 | }
53 | .frame(maxWidth: .infinity)
54 | .background(.red)
55 |
56 | }
57 | }
58 | }
59 | .navigationTitle("Screen Capture Sample")
60 | .onAppear {
61 |
62 |
63 | Task {
64 | if await screenRecorder.canRecord {
65 | // await screenRecorder.start()
66 | } else {
67 | isUnauthorized = true
68 | disableInput = true
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
75 | struct ContentView_Previews: PreviewProvider {
76 | static var previews: some View {
77 | ContentView()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ShareShot/SampleCodeFromCaptureExample/PowerMeter.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | An object that calculates the average and peak power levels for the captured audio samples.
6 | */
7 |
8 | import Foundation
9 | import AVFoundation
10 | import Accelerate
11 |
12 | struct AudioLevels {
13 | static let zero = AudioLevels(level: 0, peakLevel: 0)
14 | let level: Float
15 | let peakLevel: Float
16 | }
17 |
18 | // The protocol for the object that provides peak and average power levels to adopt.
19 | protocol AudioLevelProvider {
20 | var levels: AudioLevels { get }
21 | }
22 |
23 | class PowerMeter: AudioLevelProvider {
24 | private let kMinLevel: Float = 0.000_000_01 // -160 dB
25 |
26 | private struct PowerLevels {
27 | let average: Float
28 | let peak: Float
29 | }
30 |
31 | private var values = [PowerLevels]()
32 |
33 | private var meterTableAverage = MeterTable()
34 | private var meterTablePeak = MeterTable()
35 |
36 | var levels: AudioLevels {
37 | if values.isEmpty { return AudioLevels(level: 0.0, peakLevel: 0.0) }
38 | return AudioLevels(level: meterTableAverage.valueForPower(values[0].average),
39 | peakLevel: meterTablePeak.valueForPower(values[0].peak))
40 | }
41 |
42 | func processSilence() {
43 | if values.isEmpty { return }
44 | values = []
45 | }
46 |
47 | // Calculates the average (rms) and peak level of each channel in the PCM buffer and caches data.
48 | func process(buffer: AVAudioPCMBuffer) {
49 | var powerLevels = [PowerLevels]()
50 | let channelCount = Int(buffer.format.channelCount)
51 | let length = vDSP_Length(buffer.frameLength)
52 |
53 | if let floatData = buffer.floatChannelData {
54 | for channel in 0.., strideFrames: Int, length: vDSP_Length) -> PowerLevels {
82 | var max: Float = 0.0
83 | vDSP_maxv(data, strideFrames, &max, length)
84 | if max < kMinLevel {
85 | max = kMinLevel
86 | }
87 |
88 | var rms: Float = 0.0
89 | vDSP_rmsqv(data, strideFrames, &rms, length)
90 | if rms < kMinLevel {
91 | rms = kMinLevel
92 | }
93 |
94 | return PowerLevels(average: 20.0 * log10(rms), peak: 20.0 * log10(max))
95 | }
96 | }
97 |
98 | private struct MeterTable {
99 |
100 | // The decibel value of the minimum displayed amplitude.
101 | private let kMinDB: Float = -60.0
102 |
103 | // The table needs to be large enough so that there are no large gaps in the response.
104 | private let tableSize = 300
105 |
106 | private let scaleFactor: Float
107 | private var meterTable = [Float]()
108 |
109 | init() {
110 | let dbResolution = kMinDB / Float(tableSize - 1)
111 | scaleFactor = 1.0 / dbResolution
112 |
113 | // This controls the curvature of the response.
114 | // 2.0 is the square root, 3.0 is the cube root.
115 | let root: Float = 2.0
116 |
117 | let rroot = 1.0 / root
118 | let minAmp = dbToAmp(dBValue: kMinDB)
119 | let ampRange = 1.0 - minAmp
120 | let invAmpRange = 1.0 / ampRange
121 |
122 | for index in 0.. Float {
131 | return powf(10.0, 0.05 * dBValue)
132 | }
133 |
134 | func valueForPower(_ power: Float) -> Float {
135 | if power < kMinDB {
136 | return 0.0
137 | } else if power >= 0.0 {
138 | return 1.0
139 | } else {
140 | let index = Int(power) * Int(scaleFactor)
141 | return meterTable[index]
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/ShareShot/SampleCodeFromCaptureExample/ScreenRecorder.swift:
--------------------------------------------------------------------------------
1 | /*
2 | See LICENSE folder for this sample’s licensing information.
3 |
4 | Abstract:
5 | A model object that provides the interface to capture screen content and system audio.
6 | */
7 |
8 | import Foundation
9 | import ScreenCaptureKit
10 | import Combine
11 | import OSLog
12 | import SwiftUI
13 |
14 | /// A provider of audio levels from the captured samples.
15 | class AudioLevelsProvider: ObservableObject {
16 | @Published var audioLevels = AudioLevels.zero
17 | }
18 |
19 | @MainActor
20 | class ScreenRecorder: ObservableObject {
21 |
22 | /// The supported capture types.
23 | enum CaptureType {
24 | case display
25 | case window
26 | }
27 |
28 | private let logger = Logger()
29 |
30 | @Published var isRunning = false
31 |
32 | // MARK: - Video Properties
33 | @Published var captureType: CaptureType = .display {
34 | didSet { updateEngine() }
35 | }
36 |
37 | @Published var selectedDisplay: SCDisplay? {
38 | didSet { updateEngine() }
39 | }
40 |
41 | @Published var selectedWindow: SCWindow? {
42 | didSet { updateEngine() }
43 | }
44 |
45 | @Published var isAppExcluded = true {
46 | didSet { updateEngine() }
47 | }
48 |
49 | @Published var contentSize = CGSize(width: 1, height: 1)
50 | private var scaleFactor: Int { Int(NSScreen.main?.backingScaleFactor ?? 2) }
51 |
52 | /// A view that renders the screen content.
53 | lazy var capturePreview: CapturePreview = {
54 | CapturePreview()
55 | }()
56 |
57 | private var availableApps = [SCRunningApplication]()
58 | @Published private(set) var availableDisplays = [SCDisplay]()
59 | @Published private(set) var availableWindows = [SCWindow]()
60 |
61 | // MARK: - Audio Properties
62 | @Published var isAudioCaptureEnabled = true {
63 | didSet {
64 | updateEngine()
65 | if isAudioCaptureEnabled {
66 | startAudioMetering()
67 | } else {
68 | stopAudioMetering()
69 | }
70 | }
71 | }
72 | @Published var isAppAudioExcluded = false { didSet { updateEngine() } }
73 | @Published private(set) var audioLevelsProvider = AudioLevelsProvider()
74 | // A value that specifies how often to retrieve calculated audio levels.
75 | private let audioLevelRefreshRate: TimeInterval = 0.1
76 | private var audioMeterCancellable: AnyCancellable?
77 |
78 | // The object that manages the SCStream.
79 | private let captureEngine = CaptureEngine()
80 |
81 | private var isSetup = false
82 |
83 | // Combine subscribers.
84 | private var subscriptions = Set()
85 |
86 | var canRecord: Bool {
87 | get async {
88 | do {
89 | // If the app doesn't have Screen Recording permission, this call generates an exception.
90 | try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
91 | return true
92 | } catch {
93 | return false
94 | }
95 | }
96 | }
97 |
98 | func monitorAvailableContent() async {
99 | guard !isSetup else { return }
100 | // Refresh the lists of capturable content.
101 | await self.refreshAvailableContent()
102 | Timer.publish(every: 3, on: .main, in: .common).autoconnect().sink { [weak self] _ in
103 | guard let self = self else { return }
104 | Task {
105 | await self.refreshAvailableContent()
106 | }
107 | }
108 | .store(in: &subscriptions)
109 | }
110 |
111 | /// Starts capturing screen content.
112 | func start() async {
113 | // Exit early if already running.
114 | guard !isRunning else { return }
115 |
116 | if !isSetup {
117 | // Starting polling for available screen content.
118 | await monitorAvailableContent()
119 | isSetup = true
120 | }
121 |
122 | // If the user enables audio capture, start monitoring the audio stream.
123 | if isAudioCaptureEnabled {
124 | startAudioMetering()
125 | }
126 |
127 | do {
128 | let config = streamConfiguration
129 | let filter = contentFilter
130 | // Update the running state.
131 | isRunning = true
132 | // Start the stream and await new video frames.
133 | for try await frame in captureEngine.startCapture(configuration: config, filter: filter) {
134 | capturePreview.updateFrame(frame)
135 | if contentSize != frame.size {
136 | // Update the content size if it changed.
137 | contentSize = frame.size
138 | }
139 | }
140 | } catch {
141 | logger.error("\(error.localizedDescription)")
142 | // Unable to start the stream. Set the running state to false.
143 | isRunning = false
144 | }
145 | }
146 |
147 | /// Stops capturing screen content.
148 | func stop() async {
149 | guard isRunning else { return }
150 | await captureEngine.stopCapture()
151 | stopAudioMetering()
152 | isRunning = false
153 | }
154 |
155 | private func startAudioMetering() {
156 | audioMeterCancellable = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect().sink { [weak self] _ in
157 | guard let self = self else { return }
158 | self.audioLevelsProvider.audioLevels = self.captureEngine.audioLevels
159 | }
160 | }
161 |
162 | private func stopAudioMetering() {
163 | audioMeterCancellable?.cancel()
164 | audioLevelsProvider.audioLevels = AudioLevels.zero
165 | }
166 |
167 | /// - Tag: UpdateCaptureConfig
168 | private func updateEngine() {
169 | guard isRunning else { return }
170 | Task {
171 | await captureEngine.update(configuration: streamConfiguration, filter: contentFilter)
172 | }
173 | }
174 |
175 | /// - Tag: UpdateFilter
176 | private var contentFilter: SCContentFilter {
177 | let filter: SCContentFilter
178 | switch captureType {
179 | case .display:
180 | guard let display = selectedDisplay else { fatalError("No display selected.") }
181 | var excludedApps = [SCRunningApplication]()
182 | // If a user chooses to exclude the app from the stream,
183 | // exclude it by matching its bundle identifier.
184 | if isAppExcluded {
185 | excludedApps = availableApps.filter { app in
186 | Bundle.main.bundleIdentifier == app.bundleIdentifier
187 | }
188 | }
189 | // Create a content filter with excluded apps.
190 | filter = SCContentFilter(display: display,
191 | excludingApplications: excludedApps,
192 | exceptingWindows: [])
193 | case .window:
194 | guard let window = selectedWindow else { fatalError("No window selected.") }
195 |
196 | // Create a content filter that includes a single window.
197 | filter = SCContentFilter(desktopIndependentWindow: window)
198 | }
199 | return filter
200 | }
201 |
202 | private var streamConfiguration: SCStreamConfiguration {
203 |
204 | let streamConfig = SCStreamConfiguration()
205 |
206 | // Configure audio capture.
207 | streamConfig.capturesAudio = isAudioCaptureEnabled
208 | streamConfig.excludesCurrentProcessAudio = isAppAudioExcluded
209 |
210 | // Configure the display content width and height.
211 | if captureType == .display, let display = selectedDisplay {
212 | streamConfig.width = display.width * scaleFactor
213 | streamConfig.height = display.height * scaleFactor
214 | }
215 |
216 | // Configure the window content width and height.
217 | if captureType == .window, let window = selectedWindow {
218 | streamConfig.width = Int(window.frame.width) * 2
219 | streamConfig.height = Int(window.frame.height) * 2
220 | }
221 |
222 | // Set the capture interval at 60 fps.
223 | streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: 60)
224 |
225 | // Increase the depth of the frame queue to ensure high fps at the expense of increasing
226 | // the memory footprint of WindowServer.
227 | streamConfig.queueDepth = 5
228 |
229 | return streamConfig
230 | }
231 |
232 | /// - Tag: GetAvailableContent
233 | private func refreshAvailableContent() async {
234 | do {
235 | // Retrieve the available screen content to capture.
236 | let availableContent = try await SCShareableContent.excludingDesktopWindows(false,
237 | onScreenWindowsOnly: true)
238 | availableDisplays = availableContent.displays
239 |
240 | let windows = filterWindows(availableContent.windows)
241 | if windows != availableWindows {
242 | availableWindows = windows
243 | }
244 | availableApps = availableContent.applications
245 |
246 | if selectedDisplay == nil {
247 | selectedDisplay = availableDisplays.first
248 | }
249 | if selectedWindow == nil {
250 | selectedWindow = availableWindows.first
251 | }
252 | } catch {
253 | logger.error("Failed to get the shareable content: \(error.localizedDescription)")
254 | }
255 | }
256 |
257 | private func filterWindows(_ windows: [SCWindow]) -> [SCWindow] {
258 | windows
259 | // Sort the windows by app name.
260 | .sorted { $0.owningApplication?.applicationName ?? "" < $1.owningApplication?.applicationName ?? "" }
261 | // Remove windows that don't have an associated .app bundle.
262 | .filter { $0.owningApplication != nil && $0.owningApplication?.applicationName != "" }
263 | // Remove this app's window from the list.
264 | .filter { $0.owningApplication?.bundleIdentifier != Bundle.main.bundleIdentifier }
265 | }
266 | }
267 |
268 | extension SCWindow {
269 | var displayName: String {
270 | switch (owningApplication, title) {
271 | case (.some(let application), .some(let title)):
272 | return "\(application.applicationName): \(title)"
273 | case (.none, .some(let title)):
274 | return title
275 | case (.some(let application), .none):
276 | return "\(application.applicationName): \(windowID)"
277 | default:
278 | return ""
279 | }
280 | }
281 | }
282 |
283 | extension SCDisplay {
284 | var displayName: String {
285 | "Display: \(width) x \(height)"
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/ShareShot/ScreenShotStatusBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenShotStatusBarView.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 4/18/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ScreenShotStatusBarView: View {
12 | var image: ImageData
13 | var body: some View {
14 | RoundedRectangle(cornerRadius: 10) // Container for the screenshot view
15 | .frame(width: 100, height: 75)
16 | .foregroundColor(.clear)
17 | .overlay(
18 | Group {
19 | // Check if NSImage can be created from image data
20 | if NSImage(data: image) != nil {
21 | // Display the image
22 | Image(nsImage: NSImage(data: image)!)
23 | .resizable()
24 | .aspectRatio(contentMode: .fill)
25 | .frame(width: 100, height: 75)
26 | .background(Color.clear)
27 | .cornerRadius(10)
28 | .cornerRadius(20)
29 | // Enable drag and drop functionality
30 | .onDrag {
31 | NSItemProvider(object: NSImage(data: image)!)
32 | }
33 | } else {
34 | // Display message for invalid image
35 | Text("Invalid Image")
36 | }
37 | }
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ShareShot/ScreenShotView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenShotView.swift
3 | // CaptureSample
4 | //
5 | // Created by Oleg Yakushin on 1/11/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import AppKit
11 |
12 | // Main view for displaying a screenshot with action buttons
13 | struct ScreenShotView: View {
14 | var image: Data
15 | @State private var fileURL: URL?
16 | @State private var isHovered = false
17 | var saveImage: ((Data) -> Void)
18 | var copyImage: ((Data) -> Void)
19 | var deleteImage: ((Data) -> Void)
20 | var saveToDesktopImage: ((Data) -> Void)
21 | var shareImage: ((Data) -> Void)
22 |
23 | var body: some View {
24 | RoundedRectangle(cornerRadius: 20)
25 | .frame(width: 201, height: 152)
26 | .foregroundColor(.clear)
27 | .overlay(
28 | Group {
29 | // Check if NSImage can be created from image data
30 | if let nsImage = NSImage(data: image) {
31 | Image(nsImage: nsImage)
32 | .resizable()
33 | .aspectRatio(contentMode: .fill)
34 | .frame(width: 200, height: 150)
35 | .background(Color.clear)
36 | .cornerRadius(20)
37 | // Enable drag and drop functionality
38 | .onDrag {
39 | let url = saveImageToTemporaryDirectory(image: nsImage)
40 | return url != nil ? NSItemProvider(contentsOf: url!)! : NSItemProvider(object: image as NSData as! NSItemProviderWriting)
41 | }
42 | // Overlay to show border when not hovered
43 | .overlay(
44 | RoundedRectangle(cornerRadius: 20)
45 | .stroke(Color.gray, lineWidth: 1)
46 | .opacity(!isHovered ? 1.0 : 0.0)
47 | )
48 | // Overlay to show border when hovered
49 | .overlay(
50 | RoundedRectangle(cornerRadius: 20)
51 | .stroke(Color.white, lineWidth: 1)
52 | .opacity(isHovered ? 1.0 : 0.0)
53 | .overlay(
54 | RoundedRectangle(cornerRadius: 10)
55 | .fill(Color.clear)
56 | .frame(width: 195, height: 145)
57 | .overlay(
58 | ZStack {
59 | VStack {
60 | HStack {
61 | CircleButton(systemName: "xmark", action: deleteImage, image: image)
62 | Spacer()
63 | CircleButton(systemName: "square.and.arrow.up", action: shareImage, image: image)
64 | }
65 | Spacer()
66 | }
67 | .padding(7)
68 | VStack(spacing: 15) {
69 | TextButton(text: "Copy", action: copyImage, image: image)
70 | // Conditionally show button based on a flag
71 | #if NOSANDBOX
72 | TextButton(text: "Save to Desktop", action: saveToDesktopImage, image: image)
73 | #endif
74 | TextButton(text: "Save as", action: saveImage, image: image)
75 | }
76 | }
77 | .opacity(isHovered ? 1.0 : 0.0)
78 | )
79 | )
80 | )
81 | // Track hover state
82 | .onHover { hovering in
83 | isHovered = hovering
84 | }
85 | } else {
86 | // Display message for invalid image
87 | Text("Invalid Image")
88 | }
89 | }
90 | )
91 | }
92 |
93 | // Function to save the image to a temporary directory and return the URL
94 | func saveImageToTemporaryDirectory(image: NSImage) -> URL? {
95 | let temporaryDirectory = FileManager.default.temporaryDirectory
96 | let fileURL = temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png")
97 |
98 | guard let data = image.tiffRepresentation,
99 | let bitmap = NSBitmapImageRep(data: data),
100 | let pngData = bitmap.representation(using: .png, properties: [:]) else {
101 | return nil
102 | }
103 |
104 | do {
105 | try pngData.write(to: fileURL)
106 | return fileURL
107 | } catch {
108 | print("Failed to save image to temporary directory: \(error)")
109 | return nil
110 | }
111 | }
112 | }
113 |
114 | // View for small circular buttons overlaying the screenshot preview
115 | struct CircleButton: View {
116 | let systemName: String
117 | let action: ((Data) -> Void)
118 | var image: Data
119 | var body: some View {
120 | Circle()
121 | .frame(width: 25, height: 25)
122 | .foregroundColor(.white)
123 | .overlay(
124 | Image(systemName: systemName)
125 | .foregroundColor(.black)
126 | )
127 | .onTapGesture {
128 | action(image)
129 | }
130 | }
131 | }
132 |
133 | // View for text buttons overlaying the screenshot preview
134 | struct TextButton: View {
135 | let text: String
136 | let action: ((Data) -> Void)
137 | var image: Data
138 | var body: some View {
139 | RoundedRectangle(cornerRadius: 20)
140 | .frame(width: 110, height: 30)
141 | .foregroundColor(.white)
142 | .overlay(
143 | Text(text)
144 | .foregroundColor(.black)
145 | )
146 | .onTapGesture {
147 | action(image)
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/ShareShot/ScreenshopPreviewWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenshopPreviewWindow.swift
3 | // CaptureSample
4 | //
5 | // Created by Oleg Yakushin on 1/16/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Cocoa
12 |
13 | class ScreenshotPreview: NSWindow {
14 | init(imageData: [ImageData]) {
15 | let windowRect = NSRect(x: 0, y: 0, width: 300, height: 950)
16 | super.init(contentRect: windowRect, styleMask: [.titled, .closable, .resizable, .fullSizeContentView], backing: .buffered, defer: false)
17 |
18 | self.isReleasedWhenClosed = false
19 | self.center()
20 | self.title = "Screenshot Preview"
21 | self.contentView = NSHostingView(rootView: CaptureStackView(capturedImages: imageData))
22 | self.contentView?.frame = windowRect
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 | }
29 |
30 | class AlwaysOnTopWindowController: NSWindowController {
31 | override func windowDidLoad() {
32 | super.windowDidLoad()
33 |
34 | self.window?.level = .floating
35 |
36 | let screenshotPreviewWindow = ScreenshotPreviewWindow(imageData: /* ImageData */)
37 | screenshotPreviewWindow.makeKeyAndOrderFront(nil)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ShareShot/ScreenshotAreaSelectionNonactivatingPanel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenshotAreaSelectionNonactivatingPanel.swift
3 | // CaptureSample
4 | //
5 | // Created by Kirill Dubovitskiy on 12/16/23.
6 | // Copyright © 2023 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Cocoa
12 |
13 | // This class took me 2 days to get somewhat right - the flags are obviously very important.
14 | //
15 | // It accomplishes the following behavior:
16 | // - From anywhere I will be able to use a keyboard shortcut to create a overlay over my entire screen
17 | // - It will block interactions with existing applications while the user is selecting the range to screenshot
18 | // - We will use this overly to draw the cursor and the selection rectangle (delegated to the
19 | //
20 | // The tricky parts:
21 | // - We want the application not to take focus, so everything else on screen remains exacly the same
22 | // - It allows to get keyboard events
23 | //
24 | // Most of this was eye balled and copied from pixel picker / Maccy projects
25 | class ScreenshotAreaSelectionNonactivatingPanel: NSPanel {
26 | override var canBecomeKey: Bool {
27 | get { return true }
28 | }
29 | override var canBecomeMain: Bool {
30 | return true
31 | }
32 |
33 | var onComplete: ((Data?) -> Void)?
34 |
35 | // Initializer for OverlayPanel
36 | init(contentRect: NSRect) {
37 | // Style mask passed here is key! Changing it later will not have the same effect!
38 | super.init(contentRect: contentRect, styleMask: .nonactivatingPanel, backing: .buffered, defer: true)
39 |
40 | // Not quite sure what it does, sounds like it makes this float over other models
41 | self.isFloatingPanel = true
42 |
43 | // How does the window behave across collections (I assume this means ctrl + up, spaces managment)
44 | // We might need to further update the styleMask above to get the right combination, but good enough for now
45 | self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .fullScreenAuxiliary]
46 |
47 | // Special behavior for models
48 | self.worksWhenModal = true
49 |
50 | // Track the mouse
51 | self.acceptsMouseMovedEvents = true
52 | self.ignoresMouseEvents = false
53 | self.backgroundColor = .clear
54 |
55 | let nsHostingContentView = NSHostingView(rootView: ScreenshotAreaSelectionView(
56 | initialMousePosition: convertToSwiftUICoordinates(NSEvent.mouseLocation, in: self),
57 | onComplete: { imageData in
58 | // If image data is nil - still call on complete for proper cleanup of the panel
59 | self.onComplete?(imageData)
60 |
61 | self.contentView = nil
62 | self.close()
63 |
64 | cShowCursor()
65 | }
66 | ))
67 | self.contentView = nsHostingContentView
68 | // Additional window setup
69 | makeKeyAndOrderFront(self)
70 |
71 | cHideCursor()
72 | }
73 |
74 | override func keyDown(with event: NSEvent) {
75 | if event.keyCode == 53 { // Escape key code
76 | self.close()
77 | } else {
78 | super.keyDown(with: event)
79 | }
80 | }
81 |
82 | override func mouseDown(with event: NSEvent) {
83 | if !self.frame.contains(event.locationInWindow) {
84 | self.close()
85 | } else {
86 | super.mouseDown(with: event)
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ShareShot/ScreenshotAreaSelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenshotAreaSelectionView.swift
3 | // CaptureSample
4 | //
5 | // Created by Kirill Dubovitskiy on 12/19/23.
6 | // Copyright © 2023 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import ScreenCaptureKit
11 | import AVFoundation
12 |
13 | /**
14 | This view is
15 | */
16 | struct ScreenshotAreaSelectionView: View {
17 |
18 | private enum CaptureOverlayState {
19 | case placingAnchor(currentVirtualCursorPosition: CGPoint)
20 | // Starts out the same as anchor
21 | case selectingFrame(anchorPoint: CGPoint, virtualCursorPosition: CGPoint)
22 |
23 | // No need to keep track of the frame
24 | case capturingInProgress
25 | }
26 |
27 | let onComplete: (_ imageData: Data?) -> Void
28 | @ObservedObject private var eventMonitors = KeyboardAndMouseEventMonitors()
29 | @State private var state: CaptureOverlayState
30 |
31 | var isDebugging = false
32 |
33 | init(initialMousePosition: CGPoint, onComplete: @escaping (_: Data?) -> Void) {
34 | self.onComplete = onComplete
35 | self.state = .placingAnchor(currentVirtualCursorPosition: initialMousePosition)
36 | }
37 |
38 | func printWhenDebugging(_ items: Any...) {
39 | if isDebugging {
40 | // well shiiit Pass array to variadic function is not yet supported :/
41 | // https://github.com/apple/swift/issues/42750
42 | for item in items {
43 | print(item, terminator: "")
44 | }
45 | print()
46 | }
47 | }
48 |
49 | var body: some View {
50 | GeometryReader { geometry in
51 | createOverlayView(geometry: geometry)
52 | .onDisappear {
53 | printWhenDebugging("on dissapear")
54 | eventMonitors.stopMonitoringEvents()
55 | }
56 | .onAppear {
57 | printWhenDebugging("on appear")
58 |
59 | // We might be able to move this up to the view init code
60 | eventMonitors.startMonitoringEvents(
61 | onMouseDown: { point in
62 | printWhenDebugging("mouse down")
63 |
64 | let isPlacingAnchor = switch state { case .placingAnchor(_): true; default: false }
65 | assert(isPlacingAnchor, "Mouse down detected, but state is not .placingAnchor")
66 |
67 | // Capture the initial point and transition to placingAnchor state
68 | state = .selectingFrame(anchorPoint: point, virtualCursorPosition: point)
69 | },
70 | onMouseMove: { point in
71 | // The state might need to move as I think this might not be releasing the window when escape is clicked
72 | printWhenDebugging("on move")
73 |
74 | switch state {
75 | case .placingAnchor(_):
76 | printWhenDebugging("state == placingAnchor")
77 |
78 | // Update crosshair position
79 | state = .placingAnchor(currentVirtualCursorPosition: point)
80 | case .selectingFrame(let anchorPoint, _):
81 | printWhenDebugging("state == selectingFrame")
82 |
83 | // Update cursor position
84 | state = .selectingFrame(anchorPoint: anchorPoint, virtualCursorPosition: point)
85 | case .capturingInProgress:
86 | printWhenDebugging("state == capturing; ignore mouse movement")
87 | break
88 | }
89 | },
90 | onMouseUp: { point in
91 | printWhenDebugging("mouse up")
92 |
93 | // Have to be in selectingFrame state
94 | guard case .selectingFrame(let anchorPoint, let virtualCursorPosition) = state else {
95 | print("WARNING: Mouse up should only happen in selectingFrame state")
96 | onComplete(nil)
97 | return
98 | }
99 |
100 | let frame = Self.toFrame(anchorPoint: anchorPoint, virtualCursorPosition: virtualCursorPosition)
101 |
102 | // Frame has to not be empty
103 | guard frame.size.width != 0 && frame.size.height != 0 else {
104 | print("WARNING: Mouse up should only happen in selectingFrame state")
105 | onComplete(nil)
106 | return
107 | }
108 |
109 | // Mark as in progress - the capture process is async
110 | state = .capturingInProgress
111 |
112 | Task(priority: .userInitiated) {
113 | // Add a delay of 1 second (adjust as needed)
114 | await Task.sleep(1 * 1_000_000_000)
115 |
116 | if let screenshot = await captureScreenshot(display: (NSScreen.main?.displayID)!, rect: frame) {
117 | onComplete(screenshot)
118 | } else {
119 | print("WARNING: Capture failed! Still dismissing screenshot view")
120 | onComplete(nil)
121 | }
122 | }
123 | },
124 | onEscape: {
125 | // Manually release monitors to release the view - otherwise the monitors hold on to reference to the Window (somehow) I am assuming and the window does not get ordered out
126 | eventMonitors.stopMonitoringEvents()
127 | onComplete(nil)
128 | }
129 | )
130 | }
131 | }
132 | }
133 |
134 | @ViewBuilder
135 | private func createOverlayView(geometry: GeometryProxy) -> some View {
136 | switch state {
137 | case .placingAnchor(let currentVirtualCursorPosition):
138 | createCrosshairView(center: currentVirtualCursorPosition)
139 | case .selectingFrame(let anchorPoint, let virtualCursorPosition):
140 | createSelectionRectangle(anchor: anchorPoint, currentPoint: virtualCursorPosition)
141 | createCrosshairView(center: virtualCursorPosition)
142 | case .capturingInProgress:
143 | EmptyView()
144 | }
145 | }
146 |
147 | private func createCrosshairView(center: CGPoint) -> some View {
148 | Path { path in
149 | path.move(to: CGPoint(x: center.x - 8, y: center.y))
150 | path.addLine(to: CGPoint(x: center.x + 8, y: center.y))
151 | path.move(to: CGPoint(x: center.x, y: center.y - 8))
152 | path.addLine(to: CGPoint(x: center.x, y: center.y + 8))
153 | }
154 | .stroke(Color.blue, lineWidth: 1)
155 | }
156 |
157 | private func createSelectionRectangle(anchor: CGPoint, currentPoint: CGPoint) -> some View {
158 | printWhenDebugging("anchor \(anchor) current: \(currentPoint)")
159 |
160 | let frame = Self.toFrame(anchorPoint: anchor, virtualCursorPosition: currentPoint)
161 |
162 | // Create a rectangle view based on anchor and currentPoint
163 | return Rectangle()
164 | .fill(Color.blue.opacity(0.2))
165 | .frame(width: frame.width, height: frame.height)
166 | .position(x: frame.midX, y: frame.midY)
167 | }
168 |
169 | func getShareableContent() async -> SCDisplay? {
170 | let availableContent = try? await SCShareableContent.current
171 |
172 | guard let availableContent = availableContent,
173 | let display = availableContent.displays.first else {
174 | return nil
175 | }
176 |
177 | return display
178 | }
179 |
180 | private func captureScreenshot(display: CGDirectDisplayID, rect: CGRect) async -> Data? {
181 | if #available(macOS 14.0, *) {
182 | guard let display = await getShareableContent() else {
183 | return nil
184 | }
185 |
186 | let filter = SCContentFilter(display: display, excludingWindows: [])
187 |
188 | // Note: Configuring height / width reduced the quality somehow
189 | let config = SCStreamConfiguration()
190 | config.showsCursor = false
191 | config.captureResolution = .best
192 |
193 | // Source.
194 | // Essential that the rect and the content filter have matching coordinate spaces.
195 | // Currently it just kinda works... Kinda because its only for a single screen :D
196 | config.sourceRect = rect
197 |
198 | // Destination
199 | // We are upsampling due to retina. Not sure at which point things break, but if we don't upsample, the final image will have
200 | // number of pixels matching logical pixels captured, not the real retina physical pixels.
201 | // We should probably be multiplying by the display scale factor physicalPixelDensityPerLogicalPixel
202 |
203 | // Configure size aka bounds
204 | config.width = Int(rect.width * 4)
205 | config.height = Int(rect.height * 4)
206 |
207 | // Configure where inside the bounds to place content
208 | config.destinationRect = CGRect(x: 0, y: 0, width: rect.width * 4, height: rect.height * 4)
209 |
210 |
211 | guard let cgImage = try? await SCScreenshotManager.captureImage(
212 | contentFilter: filter,
213 | configuration: config
214 | ) else {
215 | return nil
216 | }
217 |
218 | return NSImage(cgImage: cgImage, size: rect.size).tiffRepresentation
219 | } else {
220 | guard let cgImage = CGDisplayCreateImage(display, rect: rect) else {
221 | return nil
222 | }
223 | let capturedImage = NSImage(cgImage: cgImage, size: rect.size)
224 | guard capturedImage.isValid else {
225 | return nil
226 | }
227 |
228 | return capturedImage.tiffRepresentation
229 | }
230 | }
231 |
232 | static private func toFrame(anchorPoint: CGPoint, virtualCursorPosition: CGPoint) -> CGRect {
233 | CGRect(x: min(anchorPoint.x, virtualCursorPosition.x),
234 | y: min(anchorPoint.y, virtualCursorPosition.y),
235 | width: abs(anchorPoint.x - virtualCursorPosition.x),
236 | height: abs(anchorPoint.y - virtualCursorPosition.y))
237 | }
238 | }
239 |
240 | // Extension to get CGDirectDisplayID from NSScreen
241 | extension NSScreen {
242 | var displayID: CGDirectDisplayID? {
243 | guard let screen = self.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else {
244 | return nil
245 | }
246 | return CGDirectDisplayID(truncating: screen)
247 | }
248 | }
249 |
250 | // Function to get all available displays
251 | func getAllDisplays() -> [NSScreen] {
252 | return NSScreen.screens
253 | }
254 |
255 | func getScreenContainingPoint(point: NSPoint) -> NSScreen? {
256 | return NSScreen.screens.first(where: { NSMouseInRect(point, $0.frame, false) })
257 | }
258 |
259 | typealias ImageData = Data
260 |
261 |
--------------------------------------------------------------------------------
/ShareShot/ScreenshotStackPanel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScreenshotStackPanel.swift
3 | // CaptureSample
4 | //
5 | // Created by Oleg Yakushin on 1/6/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | class ScreenshotStackPanel: NSPanel {
12 | init(stackModelState: StackModel) {
13 | // Get the screen size, safely handling the optional value
14 | let screenFrame = NSScreen.main?.frame ?? NSRect.zero
15 | let panelWidth = min(300, screenFrame.width * 0.8)
16 | let panelHeight = min(950, screenFrame.height * 0.95)
17 | let previewRect = NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight)
18 |
19 | // Initialize the panel with the specified parameters
20 | super.init(contentRect: previewRect, styleMask: .borderless, backing: .buffered, defer: false)
21 |
22 | // Configure the panel properties
23 | self.backgroundColor = NSColor.clear
24 | self.isFloatingPanel = true
25 | self.worksWhenModal = true
26 | self.isOpaque = false
27 | self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
28 | self.hidesOnDeactivate = false
29 |
30 | // Create an NSHostingView with the root SwiftUI view
31 | let hostingView = NSHostingView(rootView: CaptureStackView(model: stackModelState))
32 | hostingView.frame = previewRect
33 |
34 | // Add the hostingView to the panel's contentView
35 | if let contentView = self.contentView {
36 | contentView.addSubview(hostingView)
37 | } else {
38 | // Warning if contentView is nil
39 | print("Warning: contentView is nil.")
40 | }
41 | }
42 |
43 | // Required initializer with fatalError, as it is not used
44 | required init?(coder: NSCoder) {
45 | fatalError("init(coder:) has not been implemented")
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/ShareShot/ShareShot-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // ShareShot-Bridging-Header.h
3 | // CaptureSample
4 | //
5 | // Created by Kirill Dubovitskiy on 12/17/23.
6 | // Copyright © 2023 Apple. All rights reserved.
7 | //
8 |
9 | #ifndef ShareShot_Bridging_Header_h
10 | #define ShareShot_Bridging_Header_h
11 |
12 | #import "ShowAndHideCursor.h"
13 |
14 | #endif /* ShareShot_Bridging_Header_h */
15 |
--------------------------------------------------------------------------------
/ShareShot/ShowAndHideCursor.h:
--------------------------------------------------------------------------------
1 | //
2 | // ShowAndHideCursor.h
3 | // CaptureSample
4 | //
5 | // Created by Kirill Dubovitskiy on 12/17/23.
6 | // Copyright © 2023 Apple. All rights reserved.
7 | //
8 |
9 | #ifndef ShowAndHideCursor_h
10 | #define ShowAndHideCursor_h
11 |
12 | #import
13 | #import
14 | #import
15 |
16 | // Source: https://stackoverflow.com/a/3939241/5278310
17 | // And https://github.com/acheronfail/pixel-picker/blob/fae1ec38c938d625b5122aa5cbc497c9ef6effc1/Pixel%20Picker/ShowAndHideCursor.swift
18 | //
19 | // Related
20 | // https://developer.apple.com/documentation/coregraphics/1454426-cgeventtapcreate
21 |
22 | void cHideCursor(void) {
23 | void CGSSetConnectionProperty(int, int, CFStringRef, CFBooleanRef);
24 | int _CGSDefaultConnection(void);
25 | CFStringRef propertyString;
26 |
27 | // Hack to make background cursor setting work
28 | propertyString = CFStringCreateWithCString(NULL, "SetsCursorInBackground", kCFStringEncodingUTF8);
29 | CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue);
30 | CFRelease(propertyString);
31 | // Hide the cursor
32 | CGDisplayHideCursor(kCGDirectMainDisplay);
33 | }
34 |
35 | void cShowCursor(void) {
36 | CGDisplayShowCursor(kCGDirectMainDisplay);
37 | }
38 |
39 |
40 | #endif /* ShowAndHideCursor_h */
41 |
42 |
--------------------------------------------------------------------------------
/ShareShot/StatusBarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusBarView.swift
3 | // ShareShot
4 | //
5 | // Created by Oleg Yakushin on 4/16/24.
6 | // Copyright © 2024 Apple. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct StatusBarView: View {
12 | var startScreenshot: () -> Void
13 | var quitApplication: () -> Void
14 | var history: () -> Void
15 | var onboarding: () -> Void
16 | var lastScreenshots: [Data]
17 |
18 | private let imageSize: CGSize = CGSize(width: 100, height: 75)
19 |
20 | var body: some View {
21 | VStack {
22 | ForEach(lastScreenshots, id: \.self) { imageData in
23 | ScreenShotView(image: imageData, saveImage: {_ in }, copyImage: {_ in }, deleteImage: {_ in }, saveToDesktopImage: {_ in }, shareImage: {_ in })
24 | .onTapGesture {
25 | openImageInPreview(image: NSImage(data: imageData)!)
26 | }
27 | }
28 | Button(action: startScreenshot) {
29 | Label("Screenshot", systemImage: "camera")
30 | }
31 | Button(action: history) {
32 | Label("History", systemImage: "tray.full")
33 | }
34 | Button(action: onboarding) {
35 | Label("Onboarding", systemImage: "info.circle")
36 | }
37 | Button(action: quitApplication) {
38 | Label("Quit", systemImage: "power")
39 | }
40 | }
41 | .padding()
42 | }
43 | private func deleteImage(_ image: ImageData) {
44 | }
45 |
46 | // Open the image in Preview app
47 | private func openImageInPreview(image: NSImage) {
48 | let temporaryDirectoryURL = FileManager.default.temporaryDirectory
49 | let temporaryImageURL = temporaryDirectoryURL.appendingPathComponent("ShareShot.png")
50 | if let imageData = image.tiffRepresentation, let bitmapRep = NSBitmapImageRep(data: imageData) {
51 | if let pngData = bitmapRep.representation(using: .png, properties: [:]) {
52 | do {
53 | try pngData.write(to: temporaryImageURL)
54 | } catch {
55 | print("Failed to save temporary image: \(error)")
56 | return
57 | }
58 | }
59 | }
60 | NSWorkspace.shared.open(temporaryImageURL)
61 | }
62 |
63 | // Save the image to desktop (sandbox only)
64 | private func saveImageToDesktop(_ image: ImageData) {
65 | guard let desktopURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
66 | print("Unable to access desktop directory.")
67 | return
68 | }
69 | let fileName = dateTimeUniqueScreenshotFileName()
70 | let filePath = desktopURL.appendingPathComponent(fileName)
71 | saveImageAsPng(image: image, at: filePath)
72 | }
73 |
74 | // Generate a unique filename based on date and time
75 | private func dateTimeUniqueScreenshotFileName() -> String {
76 | let currentDate = Date()
77 | let formatter = DateFormatter()
78 | formatter.dateFormat = "yyyyMMdd_HHmmss"
79 | return "ShareShot_\(formatter.string(from: currentDate)).png"
80 | }
81 |
82 | }
83 |
84 | struct ScreenShotStatusBarView: View {
85 | var imageData: Data
86 | var size: CGSize
87 |
88 | var body: some View {
89 | RoundedRectangle(cornerRadius: 10)
90 | .frame(width: size.width, height: size.height)
91 | .foregroundColor(.clear)
92 | .overlay(
93 | Group {
94 | if let nsImage = NSImage(data: imageData) {
95 | ScreenShotView(image: imageData, saveImage: {_ in }, copyImage: {_ in }, deleteImage: {_ in }, saveToDesktopImage: {_ in }, shareImage: {_ in })
96 | .onTapGesture { copyToClipboard(nsImage) }
97 | } else {
98 | Text("Invalid Image")
99 | .frame(width: size.width, height: size.height)
100 | .background(Color.clear)
101 | .cornerRadius(10)
102 | }
103 | }
104 | )
105 | }
106 |
107 | private func copyToClipboard(_ image: NSImage) {
108 | let pasteboard = NSPasteboard.general
109 | pasteboard.clearContents()
110 | pasteboard.writeObjects([image])
111 | }
112 |
113 | // Function to save the image to a temporary directory and return the URL
114 | func saveImageToTemporaryDirectory(image: NSImage) -> URL? {
115 | let temporaryDirectory = FileManager.default.temporaryDirectory
116 | let fileURL = temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png")
117 |
118 | guard let data = image.tiffRepresentation,
119 | let bitmap = NSBitmapImageRep(data: data),
120 | let pngData = bitmap.representation(using: .png, properties: [:]) else {
121 | return nil
122 | }
123 |
124 | do {
125 | try pngData.write(to: fileURL)
126 | return fileURL
127 | } catch {
128 | print("Failed to save image to temporary directory: \(error)")
129 | return nil
130 | }
131 | }
132 |
133 | func isReceivingURL() -> Bool {
134 | return true
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/ShareShot/convertToSwiftUICoordinates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConvertFromFlippedMacOSCooridnateSystem.swift
3 | // CaptureSample
4 | //
5 | // Created by Kirill Dubovitskiy on 12/19/23.
6 | // Copyright © 2023 Apple. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AppKit
11 |
12 | func convertToSwiftUICoordinates(_ point: CGPoint, in window: NSWindow) -> CGPoint {
13 | // Get the height of the window
14 | let windowHeight = window.frame.height
15 |
16 | // Flip the Y-coordinate
17 | let newY = windowHeight - point.y
18 |
19 | // Return the adjusted point
20 | return CGPoint(x: point.x, y: newY)
21 | }
22 |
--------------------------------------------------------------------------------
/assets/ScreenCapture.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/assets/ScreenCapture.mov
--------------------------------------------------------------------------------
/assets/app-store-1280x800, hover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/assets/app-store-1280x800, hover.png
--------------------------------------------------------------------------------
/assets/app-store-hover-share-menu-expanded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/assets/app-store-hover-share-menu-expanded.png
--------------------------------------------------------------------------------
/assets/cleanshot-screenshot-examples.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bra1nDump/macos-share-screenshot/d419ebf845dd235419edeb256036ca0f5ae4ea39/assets/cleanshot-screenshot-examples.png
--------------------------------------------------------------------------------
/documents/competition.md:
--------------------------------------------------------------------------------
1 | # Clean Shot X
2 | Demo: https://youtu.be/FZbICrBKWIU
3 |
4 | # Ideas to differentiate
5 | - Distribute on app store - distribution
6 | - Price - cleanshot is 30$ - one time, no free tier
7 | - We can do free tier with up to 100 screenshots (this is what Awesome Screenshot does)
8 | - We can do .99$ / month for upload to cloud feature
9 | - 1GB of S3 storage is 0.023$ / month, so give up to 5GB of storage with basic plan
10 | - Open source - so like Maccy - go after github clout instead of money. This being fully in SwiftUI could have a good reception and likely contributors.
11 | - Allow you to configure upload to your own url for ... god knows for what
12 | - Custom domains is actually a nice feature from CleanShot - but thats more enterprisey - and they will always have more features, so we should not compete on that front
13 | - As part of sending to other things - maybe we can have quick actions. For example I mostly send screnshots to Isaiah on Telegram. This is too custom though ... Maybe if integrated with shortcuts
14 | - I was exploring a cute angle of customizing the screeshotting experience - like having a pacman like icon and animate the process of creating the screenshot.
15 | - Target audience - ?? maybe developers .. as usual lol
16 |
17 | # Pricing
18 |
19 | ## Basic: 29 one-time payment
20 | The Mac app — yours to keep, forever
21 | - You will receive a license key to activate the app.
22 | - One year of updates
23 | - Stay up to date with new features and improvements.
24 | - 1 GB of Cloud storage
25 | - Upload your captures and instantly get a shareable link.
26 |
27 | ## Pro: 8 per user/mo, billed annually or $10/mo, billed monthly
28 | - Access to the Mac app for all users
29 | - You will activate the app via Cloud account.
30 | - Always get the latest version of CleanShot
31 | - Stay up to date with new features and improvements.
32 | - Unlimited Cloud storage
33 | - Upload your captures and instantly get a shareable link.
34 | - Custom domain & branding
35 | - Add your own domain and logo and use it for sharing.
36 | - Advanced security features
37 | - Self destruct control, password protected links.
38 | - Advanced team features
39 | - Effortless team management, SSO login.
40 |
41 | ## Packaged with Setapp - https://setapp.com/how-it-works
42 | Neat idea too
43 | 10$ / month for all apps
44 |
45 | # Conclusion - There is lower hanging fruit -
--------------------------------------------------------------------------------
/documents/implementation.md:
--------------------------------------------------------------------------------
1 | # Detailed functionality and implementation notes
2 |
3 | ## Upon startup
4 | - [library added, user not prompted though] Prompt user to launch at login (startup item)
5 | - [done] Add a keybinding to (Shift + Cmd + 7) to start selecting an area for screenshot
6 | - [Later] Remove stock keybindings for screenshoting on macOS in settings
7 | - How to find the settings
8 | - https://share.cleanshot.com/y0jMlvfP
9 | - https://share.cleanshot.com/ynnnL0mm
10 |
11 | ## When user uses the keyboard shortcut
12 | - [Done] We present an invisible window over the entire screen, hide the mouse an show a selection rectangle
13 | - If the user clicks escape - we dismiss the window and do nothing
14 | - Once the area is selected - we just kind of show the area and thats it
15 | - [done] We take the screenshot of the selected area and show it in the bottom left corner of the screen
16 | - Originally the project I started with was this tutorial by apple on how to use screen capture api https://developer.apple.com/documentation/screencapturekit/capturing_screen_content_in_macos
17 | - Most of the code from that is in this project, just inactive under /SampleCodeFromCaptureExample
18 | - You most likely will use this API to capture a screenshot: https://developer.apple.com/documentation/screencapturekit/scscreenshotmanager
19 | - You might run into some issues with coordinate systems in swiftui vs appkit, not matching - use ChatGPT + google + talk to me if you run into issues. There is a function `convertToSwiftUICoordinates` that deals with the issue.
20 | - To start off - just add the single screeshot to the same window - bottom left corner. Escape will dismiss similarly.
21 |
22 | ## Window behavior
23 | The rule of thumb:
24 | - we should not interfere with anything that the user is doing
25 | - the screenshot stack should stay visible until the user explicitly dismisses it
26 | - the ideal experience is clean shot - our differentiation from them will be in the shortcuts, and probably making this open source
27 |
28 | - The screen / space should not switch due to us taking the screenshot.
29 | - The screen we are taking a picture of (where the blue are window appears) should follow the mouse. Alternatively we can place the area on every single screen, we should think about which one is easier to implement. Basically if I invoke the shortcut while one screen is in focus, I should be able to move to another screen and take a screenshot there
30 | - When we move the mouse to a different screen, the screenshot stack should follow
31 |
32 | ## Stack of Screenshot Cards
33 | How it looks like: 
34 |
35 | - Have a sticky stack of screenshots on the left bottom that stays there until explicitly dismissed by the user, see examples of cleanshot
36 | - The difficult thing here is to make it sticky, but not take focus from other applications. I think we can use the same exact approach as we do with the window for screenshot selection. We can just create a similar window to that one with roughly the same configurations, but only show on the left column of the screen. The windows will never be active at the same time.
37 | - As you take more screenshots, they should be pushed on top of the stack
38 | - Restore mouse pointer
39 | - Make sure its on a different window - we don't want to keep blocking the user's view
40 | - You can drag the screenshot to drop it into another applications like telegram, gmail, etc.
41 | - As you hover the mouse over the screenshot in the stack
42 | - Top left shows a close button - it will remove the screenshot from the stack
43 | - Center shows quick actions
44 | - [done] copy
45 | - save
46 | - [done] save to arbitrary folder using the system picker
47 | - save to desktop shortcut
48 | - [innovation] allow users to configure shortcuts to save to specific folders
49 | - [innovation] send to chat gpt (paird with a chrome extension)
50 | - [innovation] share through google drive
51 | - We would need to use the API, this needs more research
52 | - The screenshot can be immediately edited with annotations - arrows, shapes - the usual annotation tooling functionality. Cleanshot also has it
53 | - Edit button will be added top right on the screenshot card
54 |
55 | ## Shortcut actions
56 | - The value add is to be able to do things you often do. For example when I save a screenshot I sometimes want to save it to desktop, other times I want to save it to a specific folder. I want to be able to add shortcuts to locations where save them.
57 | - Automatically name the screenshot - We can do this with vision API
58 | - Re-impelement share menu on macos. I want as a quick action to have send to Isaiah, Steve, Max etc.
59 | - Telegram
60 | - Move share menu to top overlay where copy / save is
61 | - https://talk.automators.fm/t/sending-a-message-from-shortcuts-to-telegram/13551
62 | - Macos for instance does not have telegram shortcut. So accessibility integration becomes even more important. Technically we can record any action on the screen and replay it.
63 | - Deep links don't support opening a chat with a specific user and adding attachments to it. https://core.telegram.org/api/links
64 | - Create a PR to telegram MacOS app
65 | - [Dead 8 years] telegram-cli https://www.omgubuntu.co.uk/2016/10/use-telegram-cli-in-terminal-ubuntu
66 | - [Gucii] https://github.com/xtrime-ru/TelegramApiServer
67 | - But would have to ask the user to login ... thats a lot to ask lol. Maybe if we are fully open source? Still deeplinks would be preferred
68 | - Upload to drive
69 | - Sales point - security, don't trust us, trust google / apple
70 | - Icloud - save to some folder
71 | - Oleg to check if we can create a link
72 | - Slack - probably more important :D
73 | - slack://user?team={TEAM_ID}&id={USER_ID}
74 | - https://api.slack.com/reference/deep-linking
75 | - Teams
76 | - Gmail
77 | - https://stackoverflow.com/a/8852679
78 | - We can upload to google drive and then send a link to the file in the body easily, or just use share google drive API ... but they have to be a google user I think
79 |
80 | - Sending to my contacts on telegram for example, would be another benefit. Basically invoking a shortcut action on the screenshot.
81 | - https://talk.automators.fm/t/sending-a-message-from-shortcuts-to-telegram/13551
82 | - Macos for instance does not have telegram shortcut. So accessibility integration becomes even more important. Technically we can record any action on the screen and replay it.
83 |
84 | ## Annotations
85 |
86 | ### Libraries that already implement drawing on the screen we can use
87 | - https://github.com/maxchuquimia/quickdraw
88 | - The best one - supports circles, arroes, squares
89 | - UIKit :(
90 | - Is not ment to be used as a library, adopting it by copying over relevant code will take effort (3 days?)
91 | - Can be used as a reference
92 | - [notes] Render happens nicely here: https://github.com/maxchuquimia/quickdraw/blob/b9732eeef42869c88927a640d8c128affd3c19f4/QuickDraw/Views/DrawingView/DrawingView.swift#L105
93 | - Majority of the code seems to be bookkeeping as always :D, tool selection, coordinate translation, mouse tracking, etc.
94 | - Was rejected from AppStore repeatedly :(
95 |
96 | - Telegram drawing contest https://github.com/Serfodi/MediaEditing/tree/main
97 |
98 | - SwiftUI 10+ stars, not researched further https://github.com/gahntpo/DrawingApp-Youtube-tutorial/tree/main
99 | - SwiftUI 16 stars, simple, but better looking than below https://github.com/gahntpo/DrawingApp
100 | - SwiftUI 26 stars, very simple drawing https://github.com/Ytemiloluwa/DrawingApp
101 |
102 | - [Later] Automatically name the screenshot - We can do this with vision API
103 | - [Later] Automatically add annotations to the screenshot - We can do this with vision API
104 | - Well this doesn't work for shit with vision / dale API https://chat.openai.com/c/786492b8-c66e-4193-84bd-daa30562b9b1 (private link because image conversations sharing are not supported yet)
105 | - Automatically remove background, do segmetation
106 |
107 | - The fact that I'm struggling to come up with things I do often with screenshots is not a good sign. I might be coming up with a fake problem.
108 |
--------------------------------------------------------------------------------
/documents/marketing.md:
--------------------------------------------------------------------------------
1 | # App Store
2 |
3 | ## Screenshots - 1 is required:
4 | Screenshots dimensions should be: 1280 x 800px, 1440 x 900px, 2560 x 1600px or 2880 x 1800px
5 |
6 | ## Promotional text
7 | Cmd+Shift+4, select area, save to desktop, drag from there to the app where you are trying to send the image. But you need to send 3 images, so you need to do this 3 times. Sound familiar?
8 |
9 | Share Screenshot features:
10 | - Keeps a stack of your recent screenshots
11 | - Allows to share them in 2 clicks to any app, contact
12 | - Allows drag and drop to any app
13 |
14 | To use Share Screenshot after installation:
15 | Press Cmd+Shift+7 to start selecting an area. The rest is intuitive!
16 |
17 | ## App Reivew Information
18 |
19 | Run the app, press Cmd+Shift+7 to take a screenshot.
20 | First time you do this you will be asked to allow screen recording.
21 | Once you do, after re-launching the app you will be able to take a screenshot by pressing down and dragging, similar to Cmd+Shift+4 native screenshot tool.
22 | You can take multiple screenshots and they will stack on top of each other.
--------------------------------------------------------------------------------
/documents/todo.md:
--------------------------------------------------------------------------------
1 | Lets continue working with Sandbox for now.
2 |
3 | # TODO Oleg
4 | - will also take care of the reactivity to changes in capture history
5 | - Make draggable
6 | - Try doing draggable using paths instead of data - or in addition to data
7 | - Lets try avoiding dropping to old school draggable if possible. If you think thats the only way, send evidence you found to Kirill so we can double check
8 | - Get the first test flight out
9 |
10 |
11 | # TODO Kirill
12 | - Test flight - Oleg will let Kirill know when ready to test, Kirill will deploy
13 |
14 | - Kirill is now writing all screenshots to the app's container /Data/screenshots. Can we try opening preview and pointing to the matching screenshot file? I hope this will allow us to allow preview to directly modify the file in place and read that data once the user hits save in Preview. Unlikely though. tmp is probably also within sandbox and Preview is declining to write to that path.
15 |
16 | - Promote
17 |
18 | # Later
19 | - Multi-display fix [later, Oleg does not have 2 displays to test with at this time]
20 | - Have the preview follow the cursor (like on cleanshot)
21 | - Be able to screenshot on any display (not just the main / current one)
22 |
23 | - Compression
24 | - ? Does cleanshot x already have this?
25 |
26 | - iCloud fix link or abandon for now
27 | - Might not be possible
28 |
29 | We are storing captured images in two places it seems - here and ScreenshotStackPanel
30 | We should have a shared model on app delegate level that the ScreenshotStackPanel will
31 | also consume / mutate
32 |
33 | - Save to cloud
34 | - ICloud - research
35 | - [impossible] https://stackoverflow.com/a/27934969/5278310
36 | - [Later] Google drive - research
37 | - If user is not logged in into iCloud - show error the user
38 |
39 | - Analytics PostHog - 1 million events free
40 |
41 | # Distribution
42 | - Open source
43 | - Brew
44 | - https://www.irradiatedsoftware.com/help/accessibility/index.php
45 | - Similar websites for distribution (there is one that cleanshot is bundled with)
46 |
47 | - Create user stories / flows
48 | - Create an example 'story' of how the user would interact with the app and how they use screenshots
49 |
50 | # Done
51 | - Implement Cmd+Shift+8 to show history - last 5 probably?
52 | - Show history in the menu bar - maybe even make draggable?
53 | - NSMenuItem.view can be set and this can be a draggable view
54 |
55 | - Show error that failed to save
56 | - Simplify or decouple the screenshot view - its currently massive with too many stacks and overlays
57 | - First one should be how to create a screenshot, and should appear in the stack and show the key keyboard combination, or say to select from the menu bar
58 | - Exponential backoff showing the hint above the screenshot
59 | - Drag and drop does not work for all things
60 | - Add show onboarding button as one of the menu items
61 | - Fix: share menu appears connected to our invisible stack view, not the screenshot being shared
62 | - Onboarding. Similar to cleanshot x
63 | - Fix: Panel for onboarding sometimes disappears
64 | - Add login item to menu bar - needed to launch the app on startup
65 | - Ditch the separate history panel for now
66 | - Maybe "close all" translucent button when multiple screenshots are shown?
67 | - Update screenshot history
68 | - close all should simply close all individual screenshots in stack, not just collapse
69 | - refactor to have swiftUI in the entire status bar
70 |
71 | # Archive
72 |
73 | - Quick actions [no-sandbox]
74 | - Share menu
75 | - [verdict] the easiest way to
76 | - List of 'blessed apps' - telegram, slack, gmail (web app, we would need google drive integration to add attachments), etc.
77 | - [impossible]
78 | - Edit the list manually
79 | - Research - can we extract individual items from the list of apps that support sharing?
80 | - Re-implement - see if anyone did this?
81 | - Save to common folders (for example icloud?)
82 | - Can we get path where we ended up saving the file?
83 | - Persistence for the list of folder user likes to use
84 | - They should be able to edit / pin them manually
85 |
86 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Why Share Screenshot?
2 |
3 | Cmd+Shift+4, select area, save to desktop, drag from there to the app where you are trying to send the image. But you need to send 3 images, so you need to do this 3 times. Sound familiar?
4 |
5 | CleanShot is a great product, but we think sharing / save / upload shortcuts can be further improved. Aaand it costs $30.
6 |
7 | 
8 |
9 | ## Features
10 |
11 | - Free
12 | - Keeps a stack of recent screenshots and avoid going back and forth between apps
13 | - Share screenshots in 2 clicks to any app, contact
14 | - Create iCloud link with 1 click
15 | - Drag and drop to any app
16 |
17 | ## How to install
18 |
19 | We have submitted the app to the app store, but it is not yet approved. As of now you can only build it from source.
20 |
21 | ## How to use after installation
22 |
23 | - Run the app, press Cmd+Shift+7 to take a screenshot.
24 | - First time you do this you will be asked to allow screen recording.
25 | - Once you do, after re-launching the app you will be able to take a screenshot by pressing down and dragging, similar to Cmd+Shift+4 native screenshot tool.
26 | - You can take multiple screenshots and they will stack on top of each other.
--------------------------------------------------------------------------------