├── .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: ![Alt text](../assets/cleanshot-screenshot-examples.png) 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 | ![example](assets/app-store-hover-share-menu-expanded.png) 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. --------------------------------------------------------------------------------