├── .gitignore ├── LICENSE ├── Macboard.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── saumya.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── Macboard.xcscheme └── xcuserdata │ └── saumya.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Macboard ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── mac1024.png │ │ ├── mac128.png │ │ ├── mac16.png │ │ ├── mac256.png │ │ ├── mac32.png │ │ ├── mac512.png │ │ └── mac64.png │ └── Contents.json ├── Extensions │ ├── Bool+Comparable.swift │ ├── Color+Hex.swift │ ├── Date+TimeAgo.swift │ ├── Defaults++.swift │ ├── KeyboardShortcuts++.swift │ ├── Settings++.swift │ ├── String++.swift │ └── View++.swift ├── Helpers │ ├── DataHelper.swift │ ├── Functions.swift │ └── ViewHelper.swift ├── Images │ └── noImage.jpeg ├── Info.plist ├── Macboard.entitlements ├── MacboardApp.swift ├── Models │ └── DataModel │ │ ├── ClipboardItem.xcdatamodeld │ │ ├── .xccurrentversion │ │ └── ClipboardItem.xcdatamodel │ │ │ └── contents │ │ └── PersistanceController.swift └── Views │ ├── Accessibility.swift │ ├── ClipboardItems.swift │ ├── DetailedView.swift │ ├── SettingsViews │ ├── AboutSettingsView.swift │ ├── GeneralSettingsView.swift │ ├── KeyboardSettingsView.swift │ └── StorageSettingsView.swift │ └── Updates.swift ├── MacboardTests └── MacboardTests.swift ├── README.md └── appcast.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Saumya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C506F0B92B5FBECF0031F4D2 /* DataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C506F0B82B5FBECF0031F4D2 /* DataHelper.swift */; }; 11 | C5091C7D2B5C4D8A0091896F /* MacboardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5091C7C2B5C4D8A0091896F /* MacboardApp.swift */; }; 12 | C5091C812B5C4D8B0091896F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5091C802B5C4D8B0091896F /* Assets.xcassets */; }; 13 | C5230DCF2B6B6E7900B6E8F7 /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5230DCE2B6B6E7900B6E8F7 /* View++.swift */; }; 14 | C53410782B8F354E007C55EA /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = C53410772B8F354E007C55EA /* Defaults */; }; 15 | C534107A2B8F3B0E007C55EA /* Defaults++.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53410792B8F3B0E007C55EA /* Defaults++.swift */; }; 16 | C534107C2B8F48BF007C55EA /* StorageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534107B2B8F48BF007C55EA /* StorageSettingsView.swift */; }; 17 | C534107E2B8F6340007C55EA /* KeyboardSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534107D2B8F6340007C55EA /* KeyboardSettingsView.swift */; }; 18 | C53410802B8F63CB007C55EA /* KeyboardShortcuts++.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534107F2B8F63CB007C55EA /* KeyboardShortcuts++.swift */; }; 19 | C553CEDC2B5C57BB00EC82F1 /* ClipboardItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C553CEDB2B5C57BB00EC82F1 /* ClipboardItems.swift */; }; 20 | C55671C32B985CAC007744A1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = C55671C22B985CAC007744A1 /* Sparkle */; }; 21 | C5628C912BA463C300F32F5B /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5628C902BA463C300F32F5B /* Color+Hex.swift */; }; 22 | C56E675E2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C56E675C2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodeld */; }; 23 | C57241EA2B67CF5900703101 /* MacboardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57241E92B67CF5900703101 /* MacboardTests.swift */; }; 24 | C57800942B7C7BF900B40EEC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57800932B7C7BF900B40EEC /* AppDelegate.swift */; }; 25 | C57800972B7C7DE100B40EEC /* PopupView in Frameworks */ = {isa = PBXBuildFile; productRef = C57800962B7C7DE100B40EEC /* PopupView */; }; 26 | C57F29472B9AB03600228B6D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57F29462B9AB03600228B6D /* Accessibility.swift */; }; 27 | C57F29492B9ABD7E00228B6D /* Updates.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57F29482B9ABD7E00228B6D /* Updates.swift */; }; 28 | C58C169D2B8E051F00BDBD7A /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = C58C169C2B8E051F00BDBD7A /* LaunchAtLogin */; }; 29 | C58C16A12B8E224400BDBD7A /* Settings in Frameworks */ = {isa = PBXBuildFile; productRef = C58C16A02B8E224400BDBD7A /* Settings */; }; 30 | C5A1D2742B8F2899007943B2 /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A1D2732B8F2899007943B2 /* AboutSettingsView.swift */; }; 31 | C5A1D2762B8F296B007943B2 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A1D2752B8F296B007943B2 /* GeneralSettingsView.swift */; }; 32 | C5A5BC932B62692000104D06 /* String++.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A5BC922B62692000104D06 /* String++.swift */; }; 33 | C5A5BC982B626A8800104D06 /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A5BC972B626A8800104D06 /* Date+TimeAgo.swift */; }; 34 | C5A5BC9A2B626EEC00104D06 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A5BC992B626EEC00104D06 /* Functions.swift */; }; 35 | C5B396042B908B0E00CD3F51 /* Sauce in Frameworks */ = {isa = PBXBuildFile; productRef = C5B396032B908B0E00CD3F51 /* Sauce */; }; 36 | C5C5F0252B8EDCAB00903601 /* Settings++.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C5F0242B8EDCAB00903601 /* Settings++.swift */; }; 37 | C5CEB1492B6BB8D600495EE3 /* PersistanceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5CEB1482B6BB8D600495EE3 /* PersistanceController.swift */; }; 38 | C5D534AF2B604F8E00074920 /* Bool+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D534AE2B604F8E00074920 /* Bool+Comparable.swift */; }; 39 | C5D534B12B604FE000074920 /* ViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D534B02B604FE000074920 /* ViewHelper.swift */; }; 40 | C5DBE0DB2B62493000418163 /* DetailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5DBE0DA2B62493000418163 /* DetailedView.swift */; }; 41 | C5E1CD8C2B8F1C8D0009EAE1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = C5E1CD8B2B8F1C8D0009EAE1 /* KeyboardShortcuts */; }; 42 | /* End PBXBuildFile section */ 43 | 44 | /* Begin PBXContainerItemProxy section */ 45 | C57241EB2B67CF5900703101 /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = C5091C712B5C4D8A0091896F /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = C5091C782B5C4D8A0091896F; 50 | remoteInfo = Macboard; 51 | }; 52 | /* End PBXContainerItemProxy section */ 53 | 54 | /* Begin PBXFileReference section */ 55 | C506F0B82B5FBECF0031F4D2 /* DataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataHelper.swift; sourceTree = ""; }; 56 | C5091C792B5C4D8A0091896F /* Macboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Macboard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | C5091C7C2B5C4D8A0091896F /* MacboardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacboardApp.swift; sourceTree = ""; }; 58 | C5091C802B5C4D8B0091896F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 59 | C5091C852B5C4D8B0091896F /* Macboard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Macboard.entitlements; sourceTree = ""; }; 60 | C5230DCE2B6B6E7900B6E8F7 /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; 61 | C53410792B8F3B0E007C55EA /* Defaults++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults++.swift"; sourceTree = ""; }; 62 | C534107B2B8F48BF007C55EA /* StorageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSettingsView.swift; sourceTree = ""; }; 63 | C534107D2B8F6340007C55EA /* KeyboardSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSettingsView.swift; sourceTree = ""; }; 64 | C534107F2B8F63CB007C55EA /* KeyboardShortcuts++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcuts++.swift"; sourceTree = ""; }; 65 | C553CEDB2B5C57BB00EC82F1 /* ClipboardItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardItems.swift; sourceTree = ""; }; 66 | C5628C902BA463C300F32F5B /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; 67 | C567F32A2B8505A600822FCB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 68 | C56E675D2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ClipboardItem.xcdatamodel; sourceTree = ""; }; 69 | C57241E72B67CF5900703101 /* MacboardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacboardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | C57241E92B67CF5900703101 /* MacboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacboardTests.swift; sourceTree = ""; }; 71 | C57800932B7C7BF900B40EEC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 72 | C57F29462B9AB03600228B6D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; 73 | C57F29482B9ABD7E00228B6D /* Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updates.swift; sourceTree = ""; }; 74 | C5918ACE2B93287400B20031 /* ClipboardItem2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ClipboardItem2.xcdatamodel; sourceTree = ""; }; 75 | C5A1D2732B8F2899007943B2 /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = ""; }; 76 | C5A1D2752B8F296B007943B2 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; 77 | C5A5BC922B62692000104D06 /* String++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String++.swift"; sourceTree = ""; }; 78 | C5A5BC972B626A8800104D06 /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; 79 | C5A5BC992B626EEC00104D06 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 80 | C5C5F0242B8EDCAB00903601 /* Settings++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings++.swift"; sourceTree = ""; }; 81 | C5CEB1482B6BB8D600495EE3 /* PersistanceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistanceController.swift; sourceTree = ""; }; 82 | C5D534AE2B604F8E00074920 /* Bool+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+Comparable.swift"; sourceTree = ""; }; 83 | C5D534B02B604FE000074920 /* ViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHelper.swift; sourceTree = ""; }; 84 | C5DBE0DA2B62493000418163 /* DetailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedView.swift; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | C5091C762B5C4D8A0091896F /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | C55671C32B985CAC007744A1 /* Sparkle in Frameworks */, 93 | C5B396042B908B0E00CD3F51 /* Sauce in Frameworks */, 94 | C53410782B8F354E007C55EA /* Defaults in Frameworks */, 95 | C58C16A12B8E224400BDBD7A /* Settings in Frameworks */, 96 | C57800972B7C7DE100B40EEC /* PopupView in Frameworks */, 97 | C5E1CD8C2B8F1C8D0009EAE1 /* KeyboardShortcuts in Frameworks */, 98 | C58C169D2B8E051F00BDBD7A /* LaunchAtLogin in Frameworks */, 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | C57241E42B67CF5900703101 /* Frameworks */ = { 103 | isa = PBXFrameworksBuildPhase; 104 | buildActionMask = 2147483647; 105 | files = ( 106 | ); 107 | runOnlyForDeploymentPostprocessing = 0; 108 | }; 109 | /* End PBXFrameworksBuildPhase section */ 110 | 111 | /* Begin PBXGroup section */ 112 | C5091C702B5C4D8A0091896F = { 113 | isa = PBXGroup; 114 | children = ( 115 | C5091C7B2B5C4D8A0091896F /* Macboard */, 116 | C57241E82B67CF5900703101 /* MacboardTests */, 117 | C5091C7A2B5C4D8A0091896F /* Products */, 118 | ); 119 | sourceTree = ""; 120 | }; 121 | C5091C7A2B5C4D8A0091896F /* Products */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | C5091C792B5C4D8A0091896F /* Macboard.app */, 125 | C57241E72B67CF5900703101 /* MacboardTests.xctest */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | C5091C7B2B5C4D8A0091896F /* Macboard */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | C567F32A2B8505A600822FCB /* Info.plist */, 134 | C56E675A2B6B9B4100AE7CE7 /* Models */, 135 | C5A5BC962B6269DE00104D06 /* Helpers */, 136 | C5A5BC912B6268CC00104D06 /* Extensions */, 137 | C57800922B7C7BBC00B40EEC /* Views */, 138 | C5091C7C2B5C4D8A0091896F /* MacboardApp.swift */, 139 | C57800932B7C7BF900B40EEC /* AppDelegate.swift */, 140 | C5091C802B5C4D8B0091896F /* Assets.xcassets */, 141 | C5091C852B5C4D8B0091896F /* Macboard.entitlements */, 142 | ); 143 | path = Macboard; 144 | sourceTree = ""; 145 | }; 146 | C56E675A2B6B9B4100AE7CE7 /* Models */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | C56E675B2B6B9B5900AE7CE7 /* DataModel */, 150 | ); 151 | path = Models; 152 | sourceTree = ""; 153 | }; 154 | C56E675B2B6B9B5900AE7CE7 /* DataModel */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | C56E675C2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodeld */, 158 | C5CEB1482B6BB8D600495EE3 /* PersistanceController.swift */, 159 | ); 160 | path = DataModel; 161 | sourceTree = ""; 162 | }; 163 | C57241E82B67CF5900703101 /* MacboardTests */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | C57241E92B67CF5900703101 /* MacboardTests.swift */, 167 | ); 168 | path = MacboardTests; 169 | sourceTree = ""; 170 | }; 171 | C57800922B7C7BBC00B40EEC /* Views */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | C5A1D2722B8F287A007943B2 /* SettingsViews */, 175 | C553CEDB2B5C57BB00EC82F1 /* ClipboardItems.swift */, 176 | C5DBE0DA2B62493000418163 /* DetailedView.swift */, 177 | C57F29462B9AB03600228B6D /* Accessibility.swift */, 178 | C57F29482B9ABD7E00228B6D /* Updates.swift */, 179 | ); 180 | path = Views; 181 | sourceTree = ""; 182 | }; 183 | C5A1D2722B8F287A007943B2 /* SettingsViews */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | C5A1D2732B8F2899007943B2 /* AboutSettingsView.swift */, 187 | C5A1D2752B8F296B007943B2 /* GeneralSettingsView.swift */, 188 | C534107B2B8F48BF007C55EA /* StorageSettingsView.swift */, 189 | C534107D2B8F6340007C55EA /* KeyboardSettingsView.swift */, 190 | ); 191 | path = SettingsViews; 192 | sourceTree = ""; 193 | }; 194 | C5A5BC912B6268CC00104D06 /* Extensions */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | C5D534AE2B604F8E00074920 /* Bool+Comparable.swift */, 198 | C5A5BC922B62692000104D06 /* String++.swift */, 199 | C5A5BC972B626A8800104D06 /* Date+TimeAgo.swift */, 200 | C5230DCE2B6B6E7900B6E8F7 /* View++.swift */, 201 | C5C5F0242B8EDCAB00903601 /* Settings++.swift */, 202 | C53410792B8F3B0E007C55EA /* Defaults++.swift */, 203 | C534107F2B8F63CB007C55EA /* KeyboardShortcuts++.swift */, 204 | C5628C902BA463C300F32F5B /* Color+Hex.swift */, 205 | ); 206 | path = Extensions; 207 | sourceTree = ""; 208 | }; 209 | C5A5BC962B6269DE00104D06 /* Helpers */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | C5D534B02B604FE000074920 /* ViewHelper.swift */, 213 | C506F0B82B5FBECF0031F4D2 /* DataHelper.swift */, 214 | C5A5BC992B626EEC00104D06 /* Functions.swift */, 215 | ); 216 | path = Helpers; 217 | sourceTree = ""; 218 | }; 219 | /* End PBXGroup section */ 220 | 221 | /* Begin PBXNativeTarget section */ 222 | C5091C782B5C4D8A0091896F /* Macboard */ = { 223 | isa = PBXNativeTarget; 224 | buildConfigurationList = C5091C882B5C4D8B0091896F /* Build configuration list for PBXNativeTarget "Macboard" */; 225 | buildPhases = ( 226 | C5091C752B5C4D8A0091896F /* Sources */, 227 | C5091C762B5C4D8A0091896F /* Frameworks */, 228 | C5091C772B5C4D8A0091896F /* Resources */, 229 | C58C169E2B8E05E900BDBD7A /* Copy "Launch at Login Helper" */, 230 | ); 231 | buildRules = ( 232 | ); 233 | dependencies = ( 234 | ); 235 | name = Macboard; 236 | packageProductDependencies = ( 237 | C57800962B7C7DE100B40EEC /* PopupView */, 238 | C58C169C2B8E051F00BDBD7A /* LaunchAtLogin */, 239 | C58C16A02B8E224400BDBD7A /* Settings */, 240 | C5E1CD8B2B8F1C8D0009EAE1 /* KeyboardShortcuts */, 241 | C53410772B8F354E007C55EA /* Defaults */, 242 | C5B396032B908B0E00CD3F51 /* Sauce */, 243 | C55671C22B985CAC007744A1 /* Sparkle */, 244 | ); 245 | productName = Macboard; 246 | productReference = C5091C792B5C4D8A0091896F /* Macboard.app */; 247 | productType = "com.apple.product-type.application"; 248 | }; 249 | C57241E62B67CF5900703101 /* MacboardTests */ = { 250 | isa = PBXNativeTarget; 251 | buildConfigurationList = C57241EF2B67CF5900703101 /* Build configuration list for PBXNativeTarget "MacboardTests" */; 252 | buildPhases = ( 253 | C57241E32B67CF5900703101 /* Sources */, 254 | C57241E42B67CF5900703101 /* Frameworks */, 255 | C57241E52B67CF5900703101 /* Resources */, 256 | ); 257 | buildRules = ( 258 | ); 259 | dependencies = ( 260 | C57241EC2B67CF5900703101 /* PBXTargetDependency */, 261 | ); 262 | name = MacboardTests; 263 | productName = MacboardTests; 264 | productReference = C57241E72B67CF5900703101 /* MacboardTests.xctest */; 265 | productType = "com.apple.product-type.bundle.unit-test"; 266 | }; 267 | /* End PBXNativeTarget section */ 268 | 269 | /* Begin PBXProject section */ 270 | C5091C712B5C4D8A0091896F /* Project object */ = { 271 | isa = PBXProject; 272 | attributes = { 273 | BuildIndependentTargetsInParallel = 1; 274 | LastSwiftUpdateCheck = 1520; 275 | LastUpgradeCheck = 1520; 276 | TargetAttributes = { 277 | C5091C782B5C4D8A0091896F = { 278 | CreatedOnToolsVersion = 15.2; 279 | }; 280 | C57241E62B67CF5900703101 = { 281 | CreatedOnToolsVersion = 15.2; 282 | TestTargetID = C5091C782B5C4D8A0091896F; 283 | }; 284 | }; 285 | }; 286 | buildConfigurationList = C5091C742B5C4D8A0091896F /* Build configuration list for PBXProject "Macboard" */; 287 | compatibilityVersion = "Xcode 14.0"; 288 | developmentRegion = en; 289 | hasScannedForEncodings = 0; 290 | knownRegions = ( 291 | en, 292 | Base, 293 | ); 294 | mainGroup = C5091C702B5C4D8A0091896F; 295 | packageReferences = ( 296 | C57800952B7C7DE100B40EEC /* XCRemoteSwiftPackageReference "PopupView" */, 297 | C58C169B2B8E051F00BDBD7A /* XCRemoteSwiftPackageReference "LaunchAtLogin" */, 298 | C58C169F2B8E224400BDBD7A /* XCRemoteSwiftPackageReference "Settings" */, 299 | C5E1CD8A2B8F1C8D0009EAE1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 300 | C53410762B8F354E007C55EA /* XCRemoteSwiftPackageReference "Defaults" */, 301 | C5B396022B908B0E00CD3F51 /* XCRemoteSwiftPackageReference "Sauce" */, 302 | C55671C12B985CAC007744A1 /* XCRemoteSwiftPackageReference "Sparkle" */, 303 | ); 304 | productRefGroup = C5091C7A2B5C4D8A0091896F /* Products */; 305 | projectDirPath = ""; 306 | projectRoot = ""; 307 | targets = ( 308 | C5091C782B5C4D8A0091896F /* Macboard */, 309 | C57241E62B67CF5900703101 /* MacboardTests */, 310 | ); 311 | }; 312 | /* End PBXProject section */ 313 | 314 | /* Begin PBXResourcesBuildPhase section */ 315 | C5091C772B5C4D8A0091896F /* Resources */ = { 316 | isa = PBXResourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | C5091C812B5C4D8B0091896F /* Assets.xcassets in Resources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | C57241E52B67CF5900703101 /* Resources */ = { 324 | isa = PBXResourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | }; 330 | /* End PBXResourcesBuildPhase section */ 331 | 332 | /* Begin PBXShellScriptBuildPhase section */ 333 | C58C169E2B8E05E900BDBD7A /* Copy "Launch at Login Helper" */ = { 334 | isa = PBXShellScriptBuildPhase; 335 | alwaysOutOfDate = 1; 336 | buildActionMask = 2147483647; 337 | files = ( 338 | ); 339 | inputFileListPaths = ( 340 | ); 341 | inputPaths = ( 342 | ); 343 | name = "Copy \"Launch at Login Helper\""; 344 | outputFileListPaths = ( 345 | ); 346 | outputPaths = ( 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | shellPath = /bin/sh; 350 | shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n"; 351 | }; 352 | /* End PBXShellScriptBuildPhase section */ 353 | 354 | /* Begin PBXSourcesBuildPhase section */ 355 | C5091C752B5C4D8A0091896F /* Sources */ = { 356 | isa = PBXSourcesBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | C5628C912BA463C300F32F5B /* Color+Hex.swift in Sources */, 360 | C5D534B12B604FE000074920 /* ViewHelper.swift in Sources */, 361 | C57F29472B9AB03600228B6D /* Accessibility.swift in Sources */, 362 | C553CEDC2B5C57BB00EC82F1 /* ClipboardItems.swift in Sources */, 363 | C506F0B92B5FBECF0031F4D2 /* DataHelper.swift in Sources */, 364 | C5A5BC9A2B626EEC00104D06 /* Functions.swift in Sources */, 365 | C56E675E2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodeld in Sources */, 366 | C534107E2B8F6340007C55EA /* KeyboardSettingsView.swift in Sources */, 367 | C5A5BC932B62692000104D06 /* String++.swift in Sources */, 368 | C5CEB1492B6BB8D600495EE3 /* PersistanceController.swift in Sources */, 369 | C5D534AF2B604F8E00074920 /* Bool+Comparable.swift in Sources */, 370 | C57800942B7C7BF900B40EEC /* AppDelegate.swift in Sources */, 371 | C53410802B8F63CB007C55EA /* KeyboardShortcuts++.swift in Sources */, 372 | C534107A2B8F3B0E007C55EA /* Defaults++.swift in Sources */, 373 | C5A1D2742B8F2899007943B2 /* AboutSettingsView.swift in Sources */, 374 | C534107C2B8F48BF007C55EA /* StorageSettingsView.swift in Sources */, 375 | C5C5F0252B8EDCAB00903601 /* Settings++.swift in Sources */, 376 | C57F29492B9ABD7E00228B6D /* Updates.swift in Sources */, 377 | C5DBE0DB2B62493000418163 /* DetailedView.swift in Sources */, 378 | C5230DCF2B6B6E7900B6E8F7 /* View++.swift in Sources */, 379 | C5A5BC982B626A8800104D06 /* Date+TimeAgo.swift in Sources */, 380 | C5A1D2762B8F296B007943B2 /* GeneralSettingsView.swift in Sources */, 381 | C5091C7D2B5C4D8A0091896F /* MacboardApp.swift in Sources */, 382 | ); 383 | runOnlyForDeploymentPostprocessing = 0; 384 | }; 385 | C57241E32B67CF5900703101 /* Sources */ = { 386 | isa = PBXSourcesBuildPhase; 387 | buildActionMask = 2147483647; 388 | files = ( 389 | C57241EA2B67CF5900703101 /* MacboardTests.swift in Sources */, 390 | ); 391 | runOnlyForDeploymentPostprocessing = 0; 392 | }; 393 | /* End PBXSourcesBuildPhase section */ 394 | 395 | /* Begin PBXTargetDependency section */ 396 | C57241EC2B67CF5900703101 /* PBXTargetDependency */ = { 397 | isa = PBXTargetDependency; 398 | target = C5091C782B5C4D8A0091896F /* Macboard */; 399 | targetProxy = C57241EB2B67CF5900703101 /* PBXContainerItemProxy */; 400 | }; 401 | /* End PBXTargetDependency section */ 402 | 403 | /* Begin XCBuildConfiguration section */ 404 | C5091C862B5C4D8B0091896F /* Debug */ = { 405 | isa = XCBuildConfiguration; 406 | buildSettings = { 407 | ALWAYS_SEARCH_USER_PATHS = NO; 408 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 409 | CLANG_ANALYZER_NONNULL = YES; 410 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 411 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 412 | CLANG_ENABLE_MODULES = YES; 413 | CLANG_ENABLE_OBJC_ARC = YES; 414 | CLANG_ENABLE_OBJC_WEAK = YES; 415 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 416 | CLANG_WARN_BOOL_CONVERSION = YES; 417 | CLANG_WARN_COMMA = YES; 418 | CLANG_WARN_CONSTANT_CONVERSION = YES; 419 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 420 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 421 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 422 | CLANG_WARN_EMPTY_BODY = YES; 423 | CLANG_WARN_ENUM_CONVERSION = YES; 424 | CLANG_WARN_INFINITE_RECURSION = YES; 425 | CLANG_WARN_INT_CONVERSION = YES; 426 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 427 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 428 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 429 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 430 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 431 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 432 | CLANG_WARN_STRICT_PROTOTYPES = YES; 433 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 434 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 435 | CLANG_WARN_UNREACHABLE_CODE = YES; 436 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 437 | COPY_PHASE_STRIP = NO; 438 | DEBUG_INFORMATION_FORMAT = dwarf; 439 | ENABLE_STRICT_OBJC_MSGSEND = YES; 440 | ENABLE_TESTABILITY = YES; 441 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 442 | GCC_C_LANGUAGE_STANDARD = gnu17; 443 | GCC_DYNAMIC_NO_PIC = NO; 444 | GCC_NO_COMMON_BLOCKS = YES; 445 | GCC_OPTIMIZATION_LEVEL = 0; 446 | GCC_PREPROCESSOR_DEFINITIONS = ( 447 | "DEBUG=1", 448 | "$(inherited)", 449 | ); 450 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 451 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 452 | GCC_WARN_UNDECLARED_SELECTOR = YES; 453 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 454 | GCC_WARN_UNUSED_FUNCTION = YES; 455 | GCC_WARN_UNUSED_VARIABLE = YES; 456 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 457 | MACOSX_DEPLOYMENT_TARGET = 12.0; 458 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 459 | MTL_FAST_MATH = YES; 460 | ONLY_ACTIVE_ARCH = YES; 461 | SDKROOT = macosx; 462 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 463 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 464 | }; 465 | name = Debug; 466 | }; 467 | C5091C872B5C4D8B0091896F /* Release */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | ALWAYS_SEARCH_USER_PATHS = NO; 471 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 472 | CLANG_ANALYZER_NONNULL = YES; 473 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 474 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 475 | CLANG_ENABLE_MODULES = YES; 476 | CLANG_ENABLE_OBJC_ARC = YES; 477 | CLANG_ENABLE_OBJC_WEAK = YES; 478 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 479 | CLANG_WARN_BOOL_CONVERSION = YES; 480 | CLANG_WARN_COMMA = YES; 481 | CLANG_WARN_CONSTANT_CONVERSION = YES; 482 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 483 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 484 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 485 | CLANG_WARN_EMPTY_BODY = YES; 486 | CLANG_WARN_ENUM_CONVERSION = YES; 487 | CLANG_WARN_INFINITE_RECURSION = YES; 488 | CLANG_WARN_INT_CONVERSION = YES; 489 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 490 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 491 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 492 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 493 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 494 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 495 | CLANG_WARN_STRICT_PROTOTYPES = YES; 496 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 497 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 498 | CLANG_WARN_UNREACHABLE_CODE = YES; 499 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 500 | COPY_PHASE_STRIP = NO; 501 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 502 | ENABLE_NS_ASSERTIONS = NO; 503 | ENABLE_STRICT_OBJC_MSGSEND = YES; 504 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 505 | GCC_C_LANGUAGE_STANDARD = gnu17; 506 | GCC_NO_COMMON_BLOCKS = YES; 507 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 508 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 509 | GCC_WARN_UNDECLARED_SELECTOR = YES; 510 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 511 | GCC_WARN_UNUSED_FUNCTION = YES; 512 | GCC_WARN_UNUSED_VARIABLE = YES; 513 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 514 | MACOSX_DEPLOYMENT_TARGET = 12.0; 515 | MTL_ENABLE_DEBUG_INFO = NO; 516 | MTL_FAST_MATH = YES; 517 | SDKROOT = macosx; 518 | SWIFT_COMPILATION_MODE = wholemodule; 519 | }; 520 | name = Release; 521 | }; 522 | C5091C892B5C4D8B0091896F /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 526 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 527 | CODE_SIGN_ENTITLEMENTS = Macboard/Macboard.entitlements; 528 | CODE_SIGN_IDENTITY = "Apple Development"; 529 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 530 | CODE_SIGN_STYLE = Automatic; 531 | COMBINE_HIDPI_IMAGES = YES; 532 | CURRENT_PROJECT_VERSION = 1.6; 533 | DEVELOPMENT_TEAM = J4NR76XL6C; 534 | ENABLE_HARDENED_RUNTIME = NO; 535 | ENABLE_PREVIEWS = YES; 536 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 537 | GENERATE_INFOPLIST_FILE = YES; 538 | INFOPLIST_FILE = Macboard/Info.plist; 539 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 540 | INFOPLIST_KEY_LSUIElement = YES; 541 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Saumya • 2024 - All rights reserved"; 542 | LD_RUNPATH_SEARCH_PATHS = ( 543 | "$(inherited)", 544 | "@executable_path/../Frameworks", 545 | ); 546 | MACOSX_DEPLOYMENT_TARGET = 12.0; 547 | MARKETING_VERSION = 1.6; 548 | PRODUCT_BUNDLE_IDENTIFIER = com.saumya.Macboard; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | PROVISIONING_PROFILE_SPECIFIER = ""; 551 | SWIFT_EMIT_LOC_STRINGS = YES; 552 | SWIFT_VERSION = 5.0; 553 | }; 554 | name = Debug; 555 | }; 556 | C5091C8A2B5C4D8B0091896F /* Release */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 560 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 561 | CODE_SIGN_ENTITLEMENTS = Macboard/Macboard.entitlements; 562 | CODE_SIGN_IDENTITY = "Apple Development"; 563 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 564 | CODE_SIGN_STYLE = Automatic; 565 | COMBINE_HIDPI_IMAGES = YES; 566 | CURRENT_PROJECT_VERSION = 1.6; 567 | DEVELOPMENT_TEAM = J4NR76XL6C; 568 | ENABLE_HARDENED_RUNTIME = NO; 569 | ENABLE_PREVIEWS = YES; 570 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 571 | GENERATE_INFOPLIST_FILE = YES; 572 | INFOPLIST_FILE = Macboard/Info.plist; 573 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; 574 | INFOPLIST_KEY_LSUIElement = YES; 575 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Saumya • 2024 - All rights reserved"; 576 | LD_RUNPATH_SEARCH_PATHS = ( 577 | "$(inherited)", 578 | "@executable_path/../Frameworks", 579 | ); 580 | MACOSX_DEPLOYMENT_TARGET = 12.0; 581 | MARKETING_VERSION = 1.6; 582 | PRODUCT_BUNDLE_IDENTIFIER = com.saumya.Macboard; 583 | PRODUCT_NAME = "$(TARGET_NAME)"; 584 | PROVISIONING_PROFILE_SPECIFIER = ""; 585 | SWIFT_EMIT_LOC_STRINGS = YES; 586 | SWIFT_VERSION = 5.0; 587 | }; 588 | name = Release; 589 | }; 590 | C57241ED2B67CF5900703101 /* Debug */ = { 591 | isa = XCBuildConfiguration; 592 | buildSettings = { 593 | BUNDLE_LOADER = "$(TEST_HOST)"; 594 | CODE_SIGN_STYLE = Automatic; 595 | CURRENT_PROJECT_VERSION = 1; 596 | GENERATE_INFOPLIST_FILE = YES; 597 | INFOPLIST_KEY_LSUIElement = YES; 598 | MACOSX_DEPLOYMENT_TARGET = 14.0; 599 | MARKETING_VERSION = 1.0; 600 | PRODUCT_BUNDLE_IDENTIFIER = com.saumya.MacboardTests; 601 | PRODUCT_NAME = "$(TARGET_NAME)"; 602 | SWIFT_EMIT_LOC_STRINGS = NO; 603 | SWIFT_VERSION = 5.0; 604 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Macboard.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Macboard"; 605 | }; 606 | name = Debug; 607 | }; 608 | C57241EE2B67CF5900703101 /* Release */ = { 609 | isa = XCBuildConfiguration; 610 | buildSettings = { 611 | BUNDLE_LOADER = "$(TEST_HOST)"; 612 | CODE_SIGN_STYLE = Automatic; 613 | CURRENT_PROJECT_VERSION = 1; 614 | GENERATE_INFOPLIST_FILE = YES; 615 | INFOPLIST_KEY_LSUIElement = YES; 616 | MACOSX_DEPLOYMENT_TARGET = 14.0; 617 | MARKETING_VERSION = 1.0; 618 | PRODUCT_BUNDLE_IDENTIFIER = com.saumya.MacboardTests; 619 | PRODUCT_NAME = "$(TARGET_NAME)"; 620 | SWIFT_EMIT_LOC_STRINGS = NO; 621 | SWIFT_VERSION = 5.0; 622 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Macboard.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Macboard"; 623 | }; 624 | name = Release; 625 | }; 626 | /* End XCBuildConfiguration section */ 627 | 628 | /* Begin XCConfigurationList section */ 629 | C5091C742B5C4D8A0091896F /* Build configuration list for PBXProject "Macboard" */ = { 630 | isa = XCConfigurationList; 631 | buildConfigurations = ( 632 | C5091C862B5C4D8B0091896F /* Debug */, 633 | C5091C872B5C4D8B0091896F /* Release */, 634 | ); 635 | defaultConfigurationIsVisible = 0; 636 | defaultConfigurationName = Release; 637 | }; 638 | C5091C882B5C4D8B0091896F /* Build configuration list for PBXNativeTarget "Macboard" */ = { 639 | isa = XCConfigurationList; 640 | buildConfigurations = ( 641 | C5091C892B5C4D8B0091896F /* Debug */, 642 | C5091C8A2B5C4D8B0091896F /* Release */, 643 | ); 644 | defaultConfigurationIsVisible = 0; 645 | defaultConfigurationName = Release; 646 | }; 647 | C57241EF2B67CF5900703101 /* Build configuration list for PBXNativeTarget "MacboardTests" */ = { 648 | isa = XCConfigurationList; 649 | buildConfigurations = ( 650 | C57241ED2B67CF5900703101 /* Debug */, 651 | C57241EE2B67CF5900703101 /* Release */, 652 | ); 653 | defaultConfigurationIsVisible = 0; 654 | defaultConfigurationName = Release; 655 | }; 656 | /* End XCConfigurationList section */ 657 | 658 | /* Begin XCRemoteSwiftPackageReference section */ 659 | C53410762B8F354E007C55EA /* XCRemoteSwiftPackageReference "Defaults" */ = { 660 | isa = XCRemoteSwiftPackageReference; 661 | repositoryURL = "https://github.com/sindresorhus/Defaults"; 662 | requirement = { 663 | branch = main; 664 | kind = branch; 665 | }; 666 | }; 667 | C55671C12B985CAC007744A1 /* XCRemoteSwiftPackageReference "Sparkle" */ = { 668 | isa = XCRemoteSwiftPackageReference; 669 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 670 | requirement = { 671 | kind = upToNextMajorVersion; 672 | minimumVersion = 2.5.2; 673 | }; 674 | }; 675 | C57800952B7C7DE100B40EEC /* XCRemoteSwiftPackageReference "PopupView" */ = { 676 | isa = XCRemoteSwiftPackageReference; 677 | repositoryURL = "https://github.com/exyte/PopupView"; 678 | requirement = { 679 | kind = upToNextMajorVersion; 680 | minimumVersion = 2.8.5; 681 | }; 682 | }; 683 | C58C169B2B8E051F00BDBD7A /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = { 684 | isa = XCRemoteSwiftPackageReference; 685 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin"; 686 | requirement = { 687 | branch = main; 688 | kind = branch; 689 | }; 690 | }; 691 | C58C169F2B8E224400BDBD7A /* XCRemoteSwiftPackageReference "Settings" */ = { 692 | isa = XCRemoteSwiftPackageReference; 693 | repositoryURL = "https://github.com/sindresorhus/Settings"; 694 | requirement = { 695 | kind = upToNextMajorVersion; 696 | minimumVersion = 3.1.0; 697 | }; 698 | }; 699 | C5B396022B908B0E00CD3F51 /* XCRemoteSwiftPackageReference "Sauce" */ = { 700 | isa = XCRemoteSwiftPackageReference; 701 | repositoryURL = "https://github.com/Clipy/Sauce.git"; 702 | requirement = { 703 | branch = master; 704 | kind = branch; 705 | }; 706 | }; 707 | C5E1CD8A2B8F1C8D0009EAE1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 708 | isa = XCRemoteSwiftPackageReference; 709 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 710 | requirement = { 711 | kind = upToNextMajorVersion; 712 | minimumVersion = 2.0.0; 713 | }; 714 | }; 715 | /* End XCRemoteSwiftPackageReference section */ 716 | 717 | /* Begin XCSwiftPackageProductDependency section */ 718 | C53410772B8F354E007C55EA /* Defaults */ = { 719 | isa = XCSwiftPackageProductDependency; 720 | package = C53410762B8F354E007C55EA /* XCRemoteSwiftPackageReference "Defaults" */; 721 | productName = Defaults; 722 | }; 723 | C55671C22B985CAC007744A1 /* Sparkle */ = { 724 | isa = XCSwiftPackageProductDependency; 725 | package = C55671C12B985CAC007744A1 /* XCRemoteSwiftPackageReference "Sparkle" */; 726 | productName = Sparkle; 727 | }; 728 | C57800962B7C7DE100B40EEC /* PopupView */ = { 729 | isa = XCSwiftPackageProductDependency; 730 | package = C57800952B7C7DE100B40EEC /* XCRemoteSwiftPackageReference "PopupView" */; 731 | productName = PopupView; 732 | }; 733 | C58C169C2B8E051F00BDBD7A /* LaunchAtLogin */ = { 734 | isa = XCSwiftPackageProductDependency; 735 | package = C58C169B2B8E051F00BDBD7A /* XCRemoteSwiftPackageReference "LaunchAtLogin" */; 736 | productName = LaunchAtLogin; 737 | }; 738 | C58C16A02B8E224400BDBD7A /* Settings */ = { 739 | isa = XCSwiftPackageProductDependency; 740 | package = C58C169F2B8E224400BDBD7A /* XCRemoteSwiftPackageReference "Settings" */; 741 | productName = Settings; 742 | }; 743 | C5B396032B908B0E00CD3F51 /* Sauce */ = { 744 | isa = XCSwiftPackageProductDependency; 745 | package = C5B396022B908B0E00CD3F51 /* XCRemoteSwiftPackageReference "Sauce" */; 746 | productName = Sauce; 747 | }; 748 | C5E1CD8B2B8F1C8D0009EAE1 /* KeyboardShortcuts */ = { 749 | isa = XCSwiftPackageProductDependency; 750 | package = C5E1CD8A2B8F1C8D0009EAE1 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 751 | productName = KeyboardShortcuts; 752 | }; 753 | /* End XCSwiftPackageProductDependency section */ 754 | 755 | /* Begin XCVersionGroup section */ 756 | C56E675C2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodeld */ = { 757 | isa = XCVersionGroup; 758 | children = ( 759 | C5918ACE2B93287400B20031 /* ClipboardItem2.xcdatamodel */, 760 | C56E675D2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodel */, 761 | ); 762 | currentVersion = C56E675D2B6B9BC800AE7CE7 /* ClipboardItem.xcdatamodel */; 763 | path = ClipboardItem.xcdatamodeld; 764 | sourceTree = ""; 765 | versionGroupType = wrapper.xcdatamodel; 766 | }; 767 | /* End XCVersionGroup section */ 768 | }; 769 | rootObject = C5091C712B5C4D8A0091896F /* Project object */; 770 | } 771 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "defaults", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/sindresorhus/Defaults", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "38925e3cfacf3fb89a81a35b1cd44fd5a5b7e0fa" 10 | } 11 | }, 12 | { 13 | "identity" : "keyboardshortcuts", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 16 | "state" : { 17 | "revision" : "09e4a10ed6b65b3a40fe07b6bf0c84809313efc4", 18 | "version" : "2.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "launchatlogin", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/sindresorhus/LaunchAtLogin", 25 | "state" : { 26 | "branch" : "main", 27 | "revision" : "d811817ce35d74872a1170c851cab243a2b8b559" 28 | } 29 | }, 30 | { 31 | "identity" : "popupview", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/exyte/PopupView", 34 | "state" : { 35 | "revision" : "41ba4821ca576a937bf0c10e37b18563313c9b6a", 36 | "version" : "2.8.5" 37 | } 38 | }, 39 | { 40 | "identity" : "sauce", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/Clipy/Sauce.git", 43 | "state" : { 44 | "branch" : "master", 45 | "revision" : "4fa210cc79207a9570b5620119f887fbdea99a06" 46 | } 47 | }, 48 | { 49 | "identity" : "settings", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/sindresorhus/Settings", 52 | "state" : { 53 | "revision" : "2f4f65eed252198be383aa0d9058bbaf8f740aa5", 54 | "version" : "3.1.0" 55 | } 56 | }, 57 | { 58 | "identity" : "sparkle", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/sparkle-project/Sparkle", 61 | "state" : { 62 | "revision" : "47d3d90aee3c52b6f61d04ceae426e607df62347", 63 | "version" : "2.5.2" 64 | } 65 | } 66 | ], 67 | "version" : 2 68 | } 69 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/project.xcworkspace/xcuserdata/saumya.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard.xcodeproj/project.xcworkspace/xcuserdata/saumya.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Macboard.xcodeproj/xcshareddata/xcschemes/Macboard.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 57 | 63 | 64 | 65 | 66 | 72 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/xcuserdata/saumya.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Macboard.xcodeproj/xcuserdata/saumya.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Macboard.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | C5091C782B5C4D8A0091896F 16 | 17 | primary 18 | 19 | 20 | C57241E62B67CF5900703101 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Macboard/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | import KeyboardShortcuts 4 | import Settings 5 | import Defaults 6 | import Sparkle 7 | import UserNotifications 8 | 9 | let UPDATE_NOTIFICATION_IDENTIFIER = "UpdateCheck" 10 | 11 | class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate { 12 | 13 | @IBOutlet var updaterController: SPUStandardUpdaterController! 14 | 15 | private var statusItem: NSStatusItem! 16 | var popover: NSPopover! 17 | var didShowObserver: AnyObject? 18 | var didCloseObserver: AnyObject? 19 | var popoverFocused: Bool = false 20 | 21 | let GeneralSettingsViewController: () -> SettingsPane = { 22 | let paneView = Settings.Pane( 23 | identifier: .general, 24 | title: "General", 25 | toolbarIcon: NSImage(systemSymbolName: "gearshape", accessibilityDescription: "General Settings")! 26 | ) { 27 | GeneralSettingsView() 28 | } 29 | 30 | return Settings.PaneHostingController(pane: paneView) 31 | } 32 | let StorageSettingsViewController: () -> SettingsPane = { 33 | let paneView = Settings.Pane( 34 | identifier: .storage, 35 | title: "Storage", 36 | toolbarIcon: NSImage(systemSymbolName: "externaldrive", accessibilityDescription: "Storage Settings")! 37 | ) { 38 | StorageSettingsView() 39 | } 40 | 41 | return Settings.PaneHostingController(pane: paneView) 42 | } 43 | let KeyboardSettingsViewController: () -> SettingsPane = { 44 | let paneView = Settings.Pane( 45 | identifier: .keyboard, 46 | title: "Keyboard", 47 | toolbarIcon: NSImage(systemSymbolName: "command", accessibilityDescription: "Keyboard Settings")! 48 | ) { 49 | KeyboardSettingsView() 50 | } 51 | 52 | return Settings.PaneHostingController(pane: paneView) 53 | } 54 | let AboutSettingsViewController: () -> SettingsPane = { 55 | let paneView = Settings.Pane( 56 | identifier: .about, 57 | title: "About", 58 | toolbarIcon: NSImage(systemSymbolName: "info.circle", accessibilityDescription: "About Macboard")! 59 | ) { 60 | AboutSettingsView() 61 | } 62 | 63 | return Settings.PaneHostingController(pane: paneView) 64 | } 65 | 66 | @MainActor func applicationDidFinishLaunching(_ notification: Notification) { 67 | let rootView = ClipboardItemListView(appDelegate: self).environment(\.managedObjectContext, PersistanceController.shared.container.viewContext) 68 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 69 | 70 | if let statusButton = statusItem.button { 71 | statusButton.image = NSImage(systemSymbolName: Defaults[.menubarIcon].rawValue, accessibilityDescription: "Macboard") 72 | statusButton.action = #selector(togglePopover) 73 | } 74 | 75 | KeyboardShortcuts.onKeyUp(for: .toggleMacboard) { [self] in 76 | self.togglePopover() 77 | } 78 | self.popover = NSPopover() 79 | self.popover.contentSize = NSSize(width: 700, height: 500) 80 | self.popover.behavior = .transient 81 | self.popover.contentViewController = NSHostingController(rootView: rootView) 82 | didCloseObserver = NotificationCenter.default.addObserver(forName: NSPopover.didCloseNotification, object: nil, queue: .main) { [weak self] _ in 83 | self?.popoverDidClose() 84 | } 85 | didShowObserver = NotificationCenter.default.addObserver(forName: NSPopover.didShowNotification, object: popover, queue: .main) { [weak self] _ in 86 | self?.popoverDidAppear() 87 | } 88 | 89 | UNUserNotificationCenter.current().delegate = self 90 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 91 | if !self.popover.isShown { 92 | self.togglePopover() 93 | } 94 | } 95 | } 96 | 97 | func updater(_ updater: SPUUpdater, willScheduleUpdateCheckAfterDelay delay: TimeInterval) { 98 | UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { granted, error in 99 | } 100 | } 101 | 102 | var supportsGentleScheduledUpdateReminders: Bool { 103 | return true 104 | } 105 | 106 | func standardUserDriverWillHandleShowingUpdate(_ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { 107 | NSApp.setActivationPolicy(.regular) 108 | 109 | if !state.userInitiated { 110 | NSApp.dockTile.badgeLabel = "1" 111 | 112 | do { 113 | let content = UNMutableNotificationContent() 114 | content.title = "A new update is available" 115 | content.body = "Version \(update.displayVersionString) is now available" 116 | 117 | let request = UNNotificationRequest(identifier: UPDATE_NOTIFICATION_IDENTIFIER, content: content, trigger: nil) 118 | 119 | UNUserNotificationCenter.current().add(request) 120 | } 121 | } 122 | } 123 | 124 | func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { 125 | NSApp.dockTile.badgeLabel = "" 126 | 127 | UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [UPDATE_NOTIFICATION_IDENTIFIER]) 128 | } 129 | 130 | func standardUserDriverWillFinishUpdateSession() { 131 | NSApp.setActivationPolicy(.accessory) 132 | } 133 | 134 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 135 | if response.notification.request.identifier == UPDATE_NOTIFICATION_IDENTIFIER && response.actionIdentifier == UNNotificationDefaultActionIdentifier { 136 | updaterController.checkForUpdates(nil) 137 | } 138 | 139 | completionHandler() 140 | } 141 | 142 | func applicationWillTerminate(_ notification: Notification) { 143 | if let didCloseObserver = didCloseObserver { 144 | NotificationCenter.default.removeObserver(didCloseObserver) 145 | } 146 | } 147 | 148 | func popoverDidAppear() { 149 | popoverFocused = true 150 | } 151 | 152 | func popoverDidClose() { 153 | popoverFocused = false 154 | } 155 | 156 | @objc func togglePopover() { 157 | if let button = statusItem.button { 158 | if popover.isShown { 159 | self.popover.performClose(nil) 160 | NSApp.hide(nil) 161 | } else { 162 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) 163 | NSApp.activate(ignoringOtherApps: true) 164 | } 165 | } 166 | } 167 | 168 | @objc func openSettings() { 169 | SettingsWindowController( 170 | panes: [ 171 | GeneralSettingsViewController(), 172 | StorageSettingsViewController(), 173 | KeyboardSettingsViewController(), 174 | AboutSettingsViewController() 175 | ], 176 | style: .toolbarItems, 177 | animated: true, 178 | hidesToolbarForSingleItem: true 179 | ).show() 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /Macboard/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 | -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "mac32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "mac32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "mac64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "mac128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "mac256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "mac256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "mac512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "mac512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "mac1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac1024.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac128.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac16.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac256.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac32.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac512.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/AppIcon.appiconset/mac64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Assets.xcassets/AppIcon.appiconset/mac64.png -------------------------------------------------------------------------------- /Macboard/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Macboard/Extensions/Bool+Comparable.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | 4 | extension Bool: Comparable { 5 | public static func <(lhs: Self, rhs: Self) -> Bool { 6 | !lhs && rhs 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Macboard/Extensions/Color+Hex.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color { 4 | init?(hex: String) { 5 | var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) 6 | hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") 7 | 8 | var rgb: UInt64 = 0 9 | 10 | var r: CGFloat = 0.0 11 | var g: CGFloat = 0.0 12 | var b: CGFloat = 0.0 13 | var a: CGFloat = 1.0 14 | 15 | let length = hexSanitized.count 16 | 17 | guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil } 18 | 19 | if length == 6 { 20 | r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 21 | g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 22 | b = CGFloat(rgb & 0x0000FF) / 255.0 23 | 24 | } else if length == 8 { 25 | r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 26 | g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 27 | b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 28 | a = CGFloat(rgb & 0x000000FF) / 255.0 29 | 30 | } else { 31 | return nil 32 | } 33 | 34 | self.init(red: r, green: g, blue: b, opacity: a) 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Macboard/Extensions/Date+TimeAgo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | func timeAgoDisplay() -> String { 5 | 6 | let calendar = Calendar.current 7 | let minuteAgo = calendar.date(byAdding: .minute, value: -1, to: Date())! 8 | let hourAgo = calendar.date(byAdding: .hour, value: -1, to: Date())! 9 | let dayAgo = calendar.date(byAdding: .day, value: -1, to: Date())! 10 | let weekAgo = calendar.date(byAdding: .day, value: -7, to: Date())! 11 | 12 | if minuteAgo < self { 13 | let diff = Calendar.current.dateComponents([.second], from: self, to: Date()).second ?? 0 14 | return "\(diff) sec ago" 15 | } else if hourAgo < self { 16 | let diff = Calendar.current.dateComponents([.minute], from: self, to: Date()).minute ?? 0 17 | return "\(diff) min ago" 18 | } else if dayAgo < self { 19 | let diff = Calendar.current.dateComponents([.hour], from: self, to: Date()).hour ?? 0 20 | return "\(diff) hrs ago" 21 | } else if weekAgo < self { 22 | let diff = Calendar.current.dateComponents([.day], from: self, to: Date()).day ?? 0 23 | return "\(diff) days ago" 24 | } 25 | let diff = Calendar.current.dateComponents([.weekOfYear], from: self, to: Date()).weekOfYear ?? 0 26 | return "\(diff) weeks ago" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Macboard/Extensions/Defaults++.swift: -------------------------------------------------------------------------------- 1 | import Defaults 2 | 3 | extension Defaults.Keys { 4 | static let autoUpdate = Key("autoUpdate", default: false) 5 | static let showSearchbar = Key("showSearchbar", default: true) 6 | static let showUrlMetadata = Key("showUrlMetadata", default: true) 7 | static let maxItems = Key("maxItems", default: 200) 8 | static let allowedTypes = Key<[String]>("allowedTypes", default: ["Text", "Image", "File"]) 9 | static let menubarIcon = Key("menubarIcon", default: .normal) 10 | static let searchType = Key("searchType", default: .insensitive) 11 | static let clearPins = Key("clearPins", default: false) 12 | } 13 | -------------------------------------------------------------------------------- /Macboard/Extensions/KeyboardShortcuts++.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | import KeyboardShortcuts 4 | import Carbon 5 | import Sauce 6 | 7 | extension KeyboardShortcuts.Name { 8 | static let toggleMacboard = Self("toggleMacboard", default: .init(.v, modifiers: [.shift, .command])) 9 | static let clearClipboard = Self("clearClipboard", default: .init(.delete, modifiers: [.command])) 10 | static let paste = Self("paste", default: .init(.return, modifiers: [])) 11 | static let copyAndHide = Self("copyAndHide", default: .init(.return, modifiers: [.option])) 12 | static let togglePin = Self("togglePin", default: .init(.p, modifiers: [.command])) 13 | static let deleteItem = Self("deleteItem", default: .init(.delete, modifiers: [])) 14 | } 15 | -------------------------------------------------------------------------------- /Macboard/Extensions/Settings++.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Settings 3 | import enum Settings.Settings 4 | 5 | extension Settings.PaneIdentifier { 6 | static let general = Self("general") 7 | static let storage = Self("storage") 8 | static let keyboard = Self("keyboard") 9 | static let about = Self("about") 10 | } 11 | 12 | public extension SettingsWindowController { 13 | override func keyDown(with event: NSEvent) { 14 | if event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .command, let key = event.charactersIgnoringModifiers { 15 | if key == "w" { 16 | self.close() 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Macboard/Extensions/String++.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var isValidURL: Bool { 5 | if self.hasPrefix("file://") || self.hasPrefix("/") { 6 | return false 7 | } 8 | let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) 9 | if let match = detector.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) { 10 | return match.range.length == self.utf16.count 11 | } else { 12 | return false 13 | } 14 | } 15 | } 16 | 17 | extension String { 18 | var isNum: Bool { 19 | return !isEmpty && rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil 20 | } 21 | } 22 | 23 | extension String { 24 | var isValidColor: Bool { 25 | guard let regex = try? NSRegularExpression(pattern: "^#(?:[0-9a-fA-F]{2}){3,4}$") else { return false } 26 | let range = NSRange(location: 0, length: self.utf16.count) 27 | return regex.firstMatch(in: self, options: [], range: range) != nil 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Macboard/Extensions/View++.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | import Combine 4 | import KeyboardShortcuts 5 | 6 | public struct ChangeObserver: ViewModifier { 7 | public init(newValue: V, action: @escaping (V) -> Void) { 8 | self.newValue = newValue 9 | self.newAction = action 10 | } 11 | 12 | private typealias Action = (V) -> Void 13 | 14 | private let newValue: V 15 | private let newAction: Action 16 | 17 | @State private var state: (V, Action)? 18 | 19 | public func body(content: Content) -> some View { 20 | if #available(macOS 14, *) { 21 | assertionFailure("Please don't use this ViewModifer directly and use the `onChange(of:perform:)` modifier instead.") 22 | } 23 | return content 24 | .onAppear() 25 | .onReceive(Just(newValue)) { newValue in 26 | if let (currentValue, action) = state, newValue != currentValue { 27 | action(newValue) 28 | } 29 | state = (newValue, newAction) 30 | } 31 | } 32 | } 33 | 34 | extension View { 35 | @_disfavoredOverload 36 | @ViewBuilder public func onChange(of value: V, perform action: @escaping (V) -> Void) -> some View where V: Equatable { 37 | if #available(macOS 14, *) { 38 | onChange(of: value, perform: action) 39 | } else { 40 | modifier(ChangeObserver(newValue: value, action: action)) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Macboard/Helpers/DataHelper.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import Defaults 4 | 5 | enum MenubarIcon: String, CaseIterable, Identifiable, Defaults.Serializable { 6 | case normal = "doc.on.clipboard" 7 | case fill = "doc.on.clipboard.fill" 8 | case clip = "paperclip" 9 | case scissors = "scissors" 10 | 11 | var id: Self { self } 12 | } 13 | 14 | enum SearchType: String, CaseIterable, Identifiable, Defaults.Serializable { 15 | case sensitive = "Case Sensitive" 16 | case insensitive = "Case Insensitive" 17 | 18 | var id: Self { self } 19 | } 20 | 21 | struct Metadata { 22 | let key: String 23 | let value: String 24 | } 25 | 26 | struct CoreDataManager { 27 | func addToClipboard(content: String? = nil, 28 | imageData: Data? = nil, 29 | fileURL: URL? = nil, 30 | contentType: String, 31 | sourceApp: String, 32 | context: NSManagedObjectContext) { 33 | 34 | let newItem = ClipboardItem(context: context) 35 | newItem.id = UUID() 36 | newItem.createdAt = Date.now 37 | newItem.content = content 38 | newItem.imageData = imageData 39 | newItem.contentType = contentType 40 | newItem.sourceApp = sourceApp 41 | 42 | PersistanceController.shared.save() 43 | } 44 | 45 | func isReCopied(item: ClipboardItem) { 46 | item.createdAt = Date.now 47 | 48 | PersistanceController.shared.save() 49 | } 50 | 51 | func deleteItem(item: ClipboardItem) { 52 | guard let context = item.managedObjectContext else { return } 53 | 54 | context.delete(item) 55 | PersistanceController.shared.save() 56 | } 57 | 58 | func togglePin(for item: ClipboardItem) { 59 | item.isPinned.toggle() 60 | 61 | PersistanceController.shared.save() 62 | } 63 | 64 | func clearClipboard(clearPins: Bool) { 65 | let context = PersistanceController.shared.container.viewContext 66 | do { 67 | let fetchRequest = ClipboardItem.fetchRequest() 68 | let items = try context.fetch(fetchRequest) 69 | for item in items { 70 | if item.isPinned && !clearPins { 71 | continue 72 | } else { 73 | context.delete(item) 74 | } 75 | } 76 | } catch { 77 | print("Failed to clear the clipboard") 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Macboard/Helpers/Functions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | import KeyboardShortcuts 4 | import Sauce 5 | 6 | func dataToImage(_ value: Data) -> (Image, String) { 7 | let image = NSImage(data: value) ?? NSImage() 8 | return (Image(nsImage: image), image.name() ?? "Image") 9 | } 10 | 11 | func copyToClipboard(_ item: ClipboardItem) { 12 | NSPasteboard.general.clearContents() 13 | if item.contentType == "Image" { 14 | NSPasteboard.general.setData(item.imageData!, forType: .tiff) 15 | } else if item.contentType == "File" { 16 | if let fileURL = URL(string: item.content!) { 17 | NSPasteboard.general.writeObjects([fileURL as NSPasteboardWriting]) 18 | } 19 | } else { 20 | NSPasteboard.general.setString(item.content!, forType: .string) 21 | } 22 | } 23 | 24 | func shortcutToText(_ shortcut: KeyboardShortcuts.Shortcut) -> String { 25 | var description = "" 26 | let modifierFlags = shortcut.modifiers 27 | if modifierFlags.contains(.command) { 28 | description += "⌘ " 29 | } 30 | 31 | if modifierFlags.contains(.shift) { 32 | description += "⇧ " 33 | } 34 | 35 | if modifierFlags.contains(.option) { 36 | description += "⌥ " 37 | } 38 | 39 | if modifierFlags.contains(.control) { 40 | description += "⌃ " 41 | } 42 | 43 | if modifierFlags.contains(.capsLock) { 44 | description += "⇪ " 45 | } 46 | 47 | if modifierFlags.contains(.numericPad) { 48 | description += "⇒ " 49 | } 50 | if let char = Sauce.shared.character(for: shortcut.carbonKeyCode, carbonModifiers: shortcut.carbonModifiers) { 51 | description += char.uppercased() 52 | } 53 | return description 54 | } 55 | 56 | func relaunch(afterDelay seconds: TimeInterval = 0.5) -> Never { 57 | let task = Process() 58 | task.launchPath = "/bin/sh" 59 | task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""] 60 | task.launch() 61 | 62 | NSApp.terminate(nil) 63 | exit(0) 64 | } 65 | -------------------------------------------------------------------------------- /Macboard/Helpers/ViewHelper.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | 4 | struct LinkButtonStyle: ButtonStyle { 5 | 6 | @State var hover: Bool = false 7 | 8 | func makeBody(configuration: Configuration) -> some View { 9 | configuration.label 10 | .contentShape(Rectangle()) 11 | .onHover { isHovering in 12 | self.hover = isHovering 13 | DispatchQueue.main.async { 14 | if self.hover { 15 | NSCursor.pointingHand.push() 16 | } else { 17 | NSCursor.pop() 18 | } 19 | } 20 | } 21 | } 22 | } 23 | 24 | struct ItemButtonStyle: ButtonStyle { 25 | 26 | func makeBody(configuration: Configuration) -> some View { 27 | withAnimation(.easeInOut) { 28 | configuration.label 29 | .contentShape(Rectangle()) 30 | .ignoresSafeArea() 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | struct ToastView: View { 38 | @Environment(\.colorScheme) var colorScheme: ColorScheme 39 | let message: String 40 | 41 | var body: some View { 42 | HStack { 43 | Image(systemName: "checkmark.circle.fill") 44 | .padding(.all, 8) 45 | .background(.green) 46 | Text(message) 47 | .font(.subheadline) 48 | .foregroundColor(Color.green) 49 | .cornerRadius(10) 50 | .frame(maxWidth: .infinity) 51 | } 52 | .background(colorScheme == .light ? .white.opacity(0.9) : .black.opacity(0.7)) 53 | } 54 | } 55 | 56 | 57 | class MetadataViewModel: ObservableObject { 58 | @Published var metadata: [Metadata] = [] 59 | 60 | func fetchMetadata(_ string: String) { 61 | guard let url = URL(string: string) else { 62 | return 63 | } 64 | metadata.removeAll() 65 | let task = URLSession.shared.dataTask(with: url) { data, response, error in 66 | guard let data = data, error == nil else { 67 | print("Error fetching webpage:", error ?? "Unknown error") 68 | return 69 | } 70 | 71 | if let htmlString = String(data: data, encoding: .utf8) { 72 | DispatchQueue.main.async { 73 | self.parseMetadata(from: htmlString) 74 | } 75 | } 76 | } 77 | task.resume() 78 | } 79 | 80 | private func parseMetadata(from htmlString: String) { 81 | do { 82 | let regexTitle = try NSRegularExpression(pattern: "]*?property=['\"]og:title['\"][^>]*?content=['\"]([^'\"]*)['\"]", options: .caseInsensitive) 83 | let titleMatches = regexTitle.matches(in: htmlString, options: [], range: NSRange(location: 0, length: htmlString.utf16.count)) 84 | 85 | let title = titleMatches.compactMap { match -> String? in 86 | guard match.numberOfRanges == 2 else { return nil } 87 | let valueRange = match.range(at: 1) 88 | if let value = Range(valueRange, in: htmlString) { 89 | return String(htmlString[value]) 90 | } 91 | return nil 92 | }.first ?? "Title Not Found" 93 | 94 | let regexDescription = try NSRegularExpression(pattern: "]*?property=['\"]og:description['\"][^>]*?content=['\"]([^'\"]*)['\"]", options: .caseInsensitive) 95 | let descriptionMatches = regexDescription.matches(in: htmlString, options: [], range: NSRange(location: 0, length: htmlString.utf16.count)) 96 | 97 | let description = descriptionMatches.compactMap { match -> String? in 98 | guard match.numberOfRanges == 2 else { return nil } 99 | let valueRange = match.range(at: 1) 100 | if let value = Range(valueRange, in: htmlString) { 101 | return String(htmlString[value]) 102 | } 103 | return nil 104 | }.first ?? "Description Not Found" 105 | 106 | let regexImage = try NSRegularExpression(pattern: "]*?property=['\"]og:image['\"][^>]*?content=['\"]([^'\"]*)['\"]", options: .caseInsensitive) 107 | let imageMatches = regexImage.matches(in: htmlString, options: [], range: NSRange(location: 0, length: htmlString.utf16.count)) 108 | 109 | let imageUrl = imageMatches.compactMap { match -> String? in 110 | guard match.numberOfRanges == 2 else { return nil } 111 | let valueRange = match.range(at: 1) 112 | if let value = Range(valueRange, in: htmlString) { 113 | return String(htmlString[value]) 114 | } 115 | return nil 116 | }.first ?? "Image URL Not Found" 117 | 118 | metadata.append(Metadata(key: "Title", value: title)) 119 | metadata.append(Metadata(key: "Description", value: description)) 120 | metadata.append(Metadata(key: "Image", value: imageUrl)) 121 | 122 | } catch { 123 | print("Error parsing HTML:", error) 124 | } 125 | } 126 | } 127 | 128 | 129 | struct RemoteImage: View { 130 | let url: URL 131 | @State private var imageData: Data? 132 | 133 | var body: some View { 134 | Group { 135 | if let imageData = imageData, let nsImage = NSImage(data: imageData) { 136 | Image(nsImage: nsImage) 137 | .resizable() 138 | .aspectRatio(contentMode: .fill) 139 | .scaledToFit() 140 | .frame(maxWidth: .infinity, alignment: .center) 141 | } else { 142 | Image(systemName: "photo.fill") 143 | .resizable() 144 | .aspectRatio(contentMode: .fill) 145 | .scaledToFit() 146 | .frame(maxWidth: .infinity, alignment: .center) 147 | } 148 | } 149 | .onAppear { 150 | fetchImage() 151 | } 152 | } 153 | 154 | private func fetchImage() { 155 | URLSession.shared.dataTask(with: url) { data, response, error in 156 | guard let data = data, error == nil else { 157 | print("Error fetching image:", error ?? "Unknown error") 158 | return 159 | } 160 | 161 | DispatchQueue.main.async { 162 | imageData = data 163 | } 164 | }.resume() 165 | } 166 | } 167 | 168 | 169 | struct CustomSplitView: View { 170 | let master: Master 171 | let detail: Detail 172 | 173 | init(@ViewBuilder master: () -> Master, @ViewBuilder detail: () -> Detail) { 174 | self.master = master() 175 | self.detail = detail() 176 | } 177 | 178 | var body: some View { 179 | HStack(spacing: 0) { 180 | VStack { 181 | master 182 | } 183 | .frame(width: 300) 184 | 185 | Divider() 186 | 187 | VStack { 188 | detail 189 | } 190 | .frame(width: 400) 191 | } 192 | } 193 | } 194 | 195 | 196 | struct CustomDivider: View { 197 | 198 | var body: some View { 199 | Text("|") 200 | .font(.largeTitle) 201 | .opacity(0.3) 202 | .padding(.top, -7) 203 | .padding(.leading, -1) 204 | .padding(.trailing, 1) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Macboard/Images/noImage.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saumsy/Macboard/623ab24b860ae1c434aa3beb105fa7e9473ab17f/Macboard/Images/noImage.jpeg -------------------------------------------------------------------------------- /Macboard/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SUEnableDownloaderService 6 | 7 | SUEnableInstallerLauncherService 8 | 9 | SUFeedURL 10 | https://raw.githubusercontent.com/27Saumya/Macboard/master/appcast.xml 11 | SUPublicEDKey 12 | NbVx5U5iXsOSE3lJ2ketLmcA1No80xpaEwf4QHrbAvw= 13 | 14 | 15 | -------------------------------------------------------------------------------- /Macboard/Macboard.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.temporary-exception.mach-lookup.global-name 6 | 7 | $(PRODUCT_BUNDLE_IDENTIFIER)-spks 8 | $(PRODUCT_BUNDLE_IDENTIFIER)-spki 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Macboard/MacboardApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftData 3 | import Cocoa 4 | 5 | @main 6 | struct MacboardApp: App { 7 | 8 | @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate 9 | 10 | var body: some Scene { 11 | Settings { 12 | EmptyView() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Macboard/Models/DataModel/ClipboardItem.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | ClipboardItem.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Macboard/Models/DataModel/ClipboardItem.xcdatamodeld/ClipboardItem.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Macboard/Models/DataModel/PersistanceController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreData 3 | 4 | struct PersistanceController { 5 | 6 | static let shared = PersistanceController() 7 | 8 | let container: NSPersistentContainer 9 | 10 | init() { 11 | self.container = NSPersistentContainer(name: "ClipboardItem") 12 | 13 | container.loadPersistentStores { desc, error in 14 | if let error = error as NSError? { 15 | fatalError("Error loading container: \(error), \(error.userInfo)") 16 | } 17 | } 18 | } 19 | 20 | func save() { 21 | let context = container.viewContext 22 | 23 | guard context.hasChanges else { return } 24 | 25 | do { 26 | try context.save() 27 | } catch { 28 | print("Failed to save the data") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Macboard/Views/Accessibility.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | struct Accessibility { 4 | private static var alert: NSAlert { 5 | var settingsName = "System Settings" 6 | var settingsPane = "Privacy & Security settings" 7 | if #unavailable(macOS 13.0) { 8 | settingsName = "System Preferences" 9 | settingsPane = "Security & Privacy preferences" 10 | } 11 | let alert = NSAlert() 12 | alert.alertStyle = .warning 13 | alert.messageText = "\"Macboard\" would like to automate your keyboard using accessibility features" 14 | alert.addButton(withTitle: "Deny") 15 | alert.addButton(withTitle: "Open \(settingsName)") 16 | alert.icon = NSImage(named: "NSSecurity") 17 | 18 | alert.informativeText = "Grant access to this application in \(settingsPane), located in \(settingsName).\n\nClick the \"+\" button, select Macboard and enable access by toggling the button next to it" 19 | 20 | return alert 21 | } 22 | 23 | private static var allowed: Bool { AXIsProcessTrustedWithOptions(nil) } 24 | private static let url = URL( 25 | string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" 26 | ) 27 | 28 | static func check() { 29 | guard !allowed else { return } 30 | DispatchQueue.main.async { 31 | if alert.runModal() == NSApplication.ModalResponse.alertSecondButtonReturn, 32 | let url = url { 33 | NSWorkspace.shared.open(url) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Macboard/Views/ClipboardItems.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Cocoa 3 | import PopupView 4 | import Defaults 5 | import KeyboardShortcuts 6 | import Sauce 7 | 8 | struct ClipboardItemListView: View { 9 | 10 | @Environment(\.managedObjectContext) private var context 11 | @Environment(\.colorScheme) var colorScheme: ColorScheme 12 | 13 | @Default(.showSearchbar) var showSearchbar 14 | @Default(.allowedTypes) var allowedTypes 15 | @Default(.maxItems) var maxItems 16 | @Default(.searchType) var searchType 17 | @Default(.clearPins) var clearPins 18 | 19 | @StateObject var viewModel = MetadataViewModel() 20 | 21 | @State private var clipboardChangeTimer: Timer? 22 | @State private var selectedItem: ClipboardItem? 23 | @State private var showToast: Bool = false 24 | @State private var toastMessage: String = "" 25 | @State private var toastPosition: CGPoint = .zero 26 | @State private var isShowingConfirmationDialog = false 27 | @State private var searchText = "" 28 | var query: Binding { 29 | Binding { 30 | searchText 31 | } set: { newValue in 32 | searchText = newValue 33 | clipboardItems.nsPredicate = newValue.isEmpty 34 | ? nil 35 | : NSPredicate(format: "content CONTAINS\(searchType == .insensitive ? "[cd]" : "") %@", newValue) 36 | } 37 | } 38 | 39 | @FetchRequest(sortDescriptors: [SortDescriptor(\.isPinned, order: .reverse), 40 | SortDescriptor(\.createdAt, order: .reverse)]) 41 | var clipboardItems: FetchedResults 42 | @FetchRequest(sortDescriptors: [SortDescriptor(\.isPinned, order: .reverse), 43 | SortDescriptor(\.createdAt, order: .reverse)]) 44 | var rawClipboardItems: FetchedResults 45 | 46 | let dataManager = CoreDataManager() 47 | let appDelegate: AppDelegate 48 | 49 | let clearClipboardShortcut = KeyboardShortcuts.Name("clearClipboard").shortcut ?? KeyboardShortcuts.Shortcut(.delete, modifiers: [.command]) 50 | let pasteShortcut = KeyboardShortcuts.Name("paste").shortcut ?? KeyboardShortcuts.Shortcut(.return, modifiers: []) 51 | let copyAndHideShortcut = KeyboardShortcuts.Name("copyAndHide").shortcut ?? KeyboardShortcuts.Shortcut(.return, modifiers: [.option]) 52 | let togglePinShortcut = KeyboardShortcuts.Name("togglePin").shortcut ?? KeyboardShortcuts.Shortcut(.p, modifiers: [.command]) 53 | let deleteItemShortcut = KeyboardShortcuts.Name("deleteItem").shortcut ?? KeyboardShortcuts.Shortcut(.delete, modifiers: []) 54 | 55 | var body: some View { 56 | if #available(macOS 13.0, *) { 57 | NavigationSplitView { 58 | List { 59 | ForEach(clipboardItems) { item in 60 | HStack { 61 | if item.contentType == "Text" { 62 | NavigationLink { 63 | DetailedView(clipboardItem: item, vm: viewModel, selectedItem: $selectedItem) 64 | .onAppear { 65 | selectedItem = item 66 | if item.content!.isValidURL { 67 | viewModel.fetchMetadata(item.content!) 68 | } 69 | } 70 | .onChange(of: item) { newItem in 71 | selectedItem = newItem 72 | if newItem.contentType == "Text" { 73 | if newItem.content!.isValidURL { 74 | viewModel.fetchMetadata(newItem.content!) 75 | } 76 | } 77 | } 78 | 79 | } label: { 80 | HStack { 81 | Image(systemName: item.content!.isValidURL ? "link.circle.fill" : (item.content!.isValidColor ? "circle.hexagongrid.fill" : "doc.plaintext.fill")) 82 | Text(item.content!) 83 | .lineLimit(1) 84 | if item.content!.isValidColor { 85 | Circle() 86 | .fill(Color(hex: item.content!) ?? .accentColor) 87 | .frame(width: 12, height: 12) 88 | } 89 | } 90 | } 91 | 92 | } else { 93 | NavigationLink { 94 | DetailedView(clipboardItem: item, vm: viewModel, selectedItem: $selectedItem) 95 | .onAppear { 96 | selectedItem = item 97 | } 98 | .onChange(of: item) { newItem in 99 | selectedItem = newItem 100 | } 101 | } label: { 102 | HStack{ 103 | if item.contentType == "Image" { 104 | Image(systemName: "photo.fill") 105 | Text(dataToImage(item.imageData!).1) 106 | .lineLimit(1) 107 | dataToImage(item.imageData!).0 108 | .resizable() 109 | .frame(width: 14, height: 14) 110 | } else { 111 | Image(systemName: "doc.fill") 112 | Text(item.content!) 113 | .lineLimit(1) 114 | } 115 | } 116 | } 117 | } 118 | 119 | Spacer() 120 | 121 | Button(action: { 122 | withAnimation { 123 | dataManager.togglePin(for: item) 124 | showToast(item.isPinned ? "Pinned" : "Unpinned") 125 | } 126 | }) { 127 | Image(systemName: item.isPinned ? "pin.fill" : "pin") 128 | } 129 | .buttonStyle(LinkButtonStyle()) 130 | 131 | Button(action: { 132 | withAnimation { 133 | dataManager.deleteItem(item: item) 134 | showToast("Removed from Clipboard") 135 | if selectedItem != nil { 136 | if item == selectedItem! { 137 | selectedItem = nil 138 | } 139 | } 140 | } 141 | }) { 142 | Image(systemName: "trash") 143 | } 144 | .buttonStyle(LinkButtonStyle()) 145 | 146 | Button(action: { 147 | withAnimation { 148 | copyToClipboard(item) 149 | showToast("Copied to Clipboard") 150 | } 151 | }) { 152 | Image(systemName: "doc.on.doc") 153 | } 154 | .buttonStyle(LinkButtonStyle()) 155 | } 156 | .contextMenu { 157 | if item.contentType == "File" { 158 | Button { 159 | let fileURL = URL(fileURLWithPath: item.content!.replacingOccurrences(of: "file://", with: "")) 160 | NSWorkspace.shared.activateFileViewerSelecting([fileURL]) 161 | } label: { 162 | Image(systemName: "folder.fill") 163 | Text("Show in Finder") 164 | } 165 | } 166 | } 167 | } 168 | 169 | } 170 | .listStyle(SidebarListStyle()) 171 | .navigationTitle("Clipboard History") 172 | .searchable(text: query, placement: showSearchbar ? .sidebar : .toolbar, prompt: "type to search...") 173 | .popup(isPresented: $showToast) { 174 | ToastView(message: toastMessage) 175 | } customize: { 176 | $0 177 | .type(.toast) 178 | .position(.bottom) 179 | .animation(.easeInOut) 180 | .closeOnTapOutside(true) 181 | .autohideIn(1.25) 182 | } 183 | .onAppear { 184 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 185 | if keyboardShortcutsHandler13(event) { 186 | return nil 187 | } 188 | return event 189 | } 190 | clipboardChangeTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [self] _ in 191 | checkClipboard(context: context) 192 | } 193 | } 194 | 195 | Divider() 196 | .background(colorScheme == .light ? .black : .white) 197 | .opacity(0.5) 198 | 199 | Button(action: { 200 | withAnimation { 201 | isShowingConfirmationDialog = true 202 | } 203 | }) { 204 | HStack { 205 | Text("Clear Clipboard") 206 | .padding(.leading, 8) 207 | Spacer() 208 | if let shortcut = KeyboardShortcuts.Name("clearClipboard").shortcut { 209 | Text(shortcutToText(shortcut)) 210 | .opacity(0.8) 211 | .padding(.trailing, 4) 212 | } else { 213 | Text("⌘ ⌫") 214 | .opacity(0.8) 215 | .padding(.trailing, 4) 216 | } 217 | } 218 | } 219 | .buttonStyle(ItemButtonStyle()) 220 | .frame(maxWidth: .infinity, minHeight: 15, idealHeight: 15, maxHeight: 15) 221 | .confirmationDialog("Are you sure you want to clear your clipboard history?", 222 | isPresented: $isShowingConfirmationDialog) { 223 | Button("Yes") { 224 | withAnimation { 225 | dataManager.clearClipboard(clearPins: clearPins) 226 | selectedItem = nil 227 | showToast("Cleard the clipboard history") 228 | } 229 | } 230 | Button("No", role: .destructive) { } 231 | } 232 | 233 | Divider() 234 | 235 | Button { 236 | appDelegate.openSettings() 237 | } label: { 238 | HStack { 239 | Text("Settings...") 240 | .padding(.leading, 8) 241 | Spacer() 242 | Text("⌘ ,") 243 | .opacity(0.8) 244 | .padding(.trailing, 4) 245 | } 246 | } 247 | .buttonStyle(ItemButtonStyle()) 248 | .background(.clear) 249 | .padding(.top, -8) 250 | .frame(maxWidth: .infinity, minHeight: 18, idealHeight: 18, maxHeight: 18) 251 | .keyboardShortcut(",") 252 | 253 | .frame(minWidth: 300, idealWidth: 350) 254 | 255 | } detail: { 256 | Text("Select an item to get its detailed view") 257 | .bold() 258 | .padding() 259 | } 260 | } else { 261 | CustomSplitView { 262 | List { 263 | ForEach(clipboardItems) { item in 264 | HStack { 265 | if item.contentType == "Text" { 266 | Button { 267 | selectedItem = item 268 | } label: { 269 | HStack { 270 | Image(systemName: item.content!.isValidURL ? "link.circle.fill" : (item.content!.isValidColor ? "circle.hexagongrid.fill" : "doc.plaintext.fill")) 271 | Text(item.content!) 272 | .lineLimit(1) 273 | if item.content!.isValidColor { 274 | Circle() 275 | .fill(Color(hex: item.content!) ?? .accentColor) 276 | .frame(width: 12, height: 12) 277 | } 278 | } 279 | } 280 | .buttonStyle(ItemButtonStyle()) 281 | 282 | } else { 283 | Button { 284 | selectedItem = item 285 | } label: { 286 | HStack{ 287 | if item.contentType == "Image" { 288 | Image(systemName: "photo.fill") 289 | Text(dataToImage(item.imageData!).1) 290 | .lineLimit(1) 291 | dataToImage(item.imageData!).0 292 | .resizable() 293 | .frame(width: 14, height: 14) 294 | } else { 295 | Image(systemName: "doc.fill") 296 | Text(item.content!) 297 | .lineLimit(1) 298 | } 299 | } 300 | } 301 | .buttonStyle(ItemButtonStyle()) 302 | } 303 | 304 | Spacer() 305 | 306 | Button(action: { 307 | withAnimation { 308 | dataManager.togglePin(for: item) 309 | showToast(item.isPinned ? "Pinned" : "Unpinned") 310 | } 311 | }) { 312 | Image(systemName: item.isPinned ? "pin.fill" : "pin") 313 | } 314 | .buttonStyle(LinkButtonStyle()) 315 | 316 | Button(action: { 317 | withAnimation { 318 | dataManager.deleteItem(item: item) 319 | showToast("Removed from Clipboard") 320 | if selectedItem != nil { 321 | if item == selectedItem! { 322 | selectedItem = nil 323 | } 324 | } 325 | } 326 | }) { 327 | Image(systemName: "trash") 328 | } 329 | .buttonStyle(LinkButtonStyle()) 330 | 331 | Button(action: { 332 | withAnimation { 333 | copyToClipboard(item) 334 | showToast("Copied to Clipboard") 335 | } 336 | }) { 337 | Image(systemName: "doc.on.doc") 338 | } 339 | .buttonStyle(LinkButtonStyle()) 340 | } 341 | .background(selectedItem != nil && selectedItem == item ? Color.accentColor : Color.clear) 342 | .contextMenu { 343 | if item.contentType == "File" { 344 | Button { 345 | let fileURL = URL(fileURLWithPath: item.content!.replacingOccurrences(of: "file://", with: "")) 346 | NSWorkspace.shared.activateFileViewerSelecting([fileURL]) 347 | } label: { 348 | Image(systemName: "folder.fill") 349 | Text("Show in Finder") 350 | } 351 | } 352 | } 353 | } 354 | 355 | } 356 | .listStyle(SidebarListStyle()) 357 | .navigationTitle("Clipboard History") 358 | .searchable(text: query, placement: showSearchbar ? .sidebar : .toolbar, prompt: "type to search...") 359 | .popup(isPresented: $showToast) { 360 | ToastView(message: toastMessage) 361 | } customize: { 362 | $0 363 | .type(.toast) 364 | .position(.bottom) 365 | .animation(.easeInOut) 366 | .closeOnTapOutside(true) 367 | .autohideIn(1.25) 368 | } 369 | .onAppear { 370 | NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in 371 | if keyboardShortcutsHandler12(event) { 372 | return nil 373 | } 374 | return event 375 | } 376 | clipboardChangeTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [self] _ in 377 | checkClipboard(context: context) 378 | } 379 | } 380 | 381 | Divider() 382 | .background(colorScheme == .light ? .black : .white) 383 | .opacity(0.5) 384 | 385 | Button(action: { 386 | withAnimation { 387 | isShowingConfirmationDialog = true 388 | } 389 | }) { 390 | HStack { 391 | Text("Clear Clipboard") 392 | .padding(.leading, 8) 393 | Spacer() 394 | if let shortcut = KeyboardShortcuts.Name("clearClipboard").shortcut { 395 | Text(shortcutToText(shortcut)) 396 | .opacity(0.8) 397 | .padding(.trailing, 4) 398 | } else { 399 | Text("⌘ ⌫") 400 | .opacity(0.8) 401 | .padding(.trailing, 4) 402 | } 403 | } 404 | } 405 | .buttonStyle(ItemButtonStyle()) 406 | .frame(maxWidth: .infinity, minHeight: 15, idealHeight: 15, maxHeight: 15) 407 | .confirmationDialog("Are you sure you want to clear your clipboard history?", 408 | isPresented: $isShowingConfirmationDialog) { 409 | Button("Yes") { 410 | withAnimation { 411 | dataManager.clearClipboard(clearPins: clearPins) 412 | selectedItem = nil 413 | showToast("Cleard the clipboard history") 414 | } 415 | } 416 | Button("No", role: .destructive) { } 417 | } 418 | 419 | Divider() 420 | 421 | Button { 422 | appDelegate.openSettings() 423 | } label: { 424 | HStack { 425 | Text("Settings...") 426 | .padding(.leading, 8) 427 | Spacer() 428 | Text("⌘ ,") 429 | .opacity(0.8) 430 | .padding(.trailing, 4) 431 | } 432 | } 433 | .buttonStyle(ItemButtonStyle()) 434 | .background(.clear) 435 | .padding(.top, -8) 436 | .frame(maxWidth: .infinity, minHeight: 18, idealHeight: 18, maxHeight: 18) 437 | .keyboardShortcut(",") 438 | 439 | } detail: { 440 | if let selectedItem = selectedItem { 441 | DetailedView(clipboardItem: selectedItem, vm: viewModel, selectedItem: $selectedItem) 442 | .onAppear { 443 | if selectedItem.contentType == "Text" { 444 | if selectedItem.content!.isValidURL { 445 | viewModel.fetchMetadata(selectedItem.content!) 446 | } 447 | } 448 | } 449 | .onChange(of: selectedItem) { newItem in 450 | if newItem.contentType == "Text" { 451 | if newItem.content!.isValidURL { 452 | viewModel.fetchMetadata(newItem.content!) 453 | } 454 | } 455 | } 456 | } else { 457 | Text("Select an item to get its detailed view") 458 | .bold() 459 | .padding() 460 | } 461 | } 462 | } 463 | } 464 | 465 | func checkClipboard(context: NSManagedObjectContext) { 466 | let contentType = clipboardContentType().0 467 | if contentType == nil { 468 | return 469 | } 470 | if rawClipboardItems.count > maxItems && maxItems != 0 { 471 | let itemsToRemoveCount = rawClipboardItems.count - maxItems 472 | for _ in 0.. (String?, NSPasteboard.PasteboardType?) { 554 | let image_types: [NSPasteboard.PasteboardType] = [.png, .tiff] 555 | let _type = NSPasteboard.general.types?.first 556 | if _type != nil { 557 | if image_types.contains(_type!) { 558 | return ("Image", _type!) 559 | } else if _type == .fileURL { 560 | return ("File", nil) 561 | } else { 562 | return ("Text", nil) 563 | } 564 | } else { 565 | return (nil, nil) 566 | } 567 | } 568 | 569 | func showToast(_ message: String) { 570 | toastMessage = message 571 | showToast = true 572 | } 573 | 574 | func keyboardShortcutsHandler13(_ event: NSEvent) -> Bool { 575 | if !appDelegate.popoverFocused { 576 | return false 577 | } 578 | 579 | if isShowingConfirmationDialog { 580 | return false 581 | } 582 | 583 | if let responder = NSApplication.shared.keyWindow?.firstResponder { 584 | if responder.className.contains("SearchTextView") { 585 | return false 586 | } 587 | } 588 | 589 | guard let shortcut = KeyboardShortcuts.Shortcut(event: event) else { return false } 590 | 591 | switch shortcut { 592 | 593 | case clearClipboardShortcut: 594 | withAnimation { 595 | isShowingConfirmationDialog = true 596 | } 597 | return true 598 | 599 | case pasteShortcut: 600 | if selectedItem != nil { 601 | withAnimation { 602 | copyToClipboard(selectedItem!) 603 | appDelegate.togglePopover() 604 | Accessibility.check() 605 | 606 | DispatchQueue.main.async { 607 | let vCode = Sauce.shared.keyCode(for: .v) 608 | let source = CGEventSource(stateID: .combinedSessionState) 609 | source?.setLocalEventsFilterDuringSuppressionState([.permitLocalMouseEvents, .permitSystemDefinedEvents], 610 | state: .eventSuppressionStateSuppressionInterval) 611 | let keyVDown = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: true) 612 | let keyVUp = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: false) 613 | keyVDown?.flags = .maskCommand 614 | keyVUp?.flags = .maskCommand 615 | keyVDown?.post(tap: .cgAnnotatedSessionEventTap) 616 | keyVUp?.post(tap: .cgAnnotatedSessionEventTap) 617 | } 618 | } 619 | return true 620 | } else { 621 | return false 622 | } 623 | 624 | case copyAndHideShortcut: 625 | if selectedItem != nil { 626 | withAnimation { 627 | copyToClipboard(selectedItem!) 628 | appDelegate.togglePopover() 629 | } 630 | return true 631 | } else { 632 | return false 633 | } 634 | 635 | case togglePinShortcut: 636 | if selectedItem != nil { 637 | withAnimation { 638 | dataManager.togglePin(for: selectedItem!) 639 | showToast(selectedItem!.isPinned ? "Pinned" : "Unpinned") 640 | } 641 | return true 642 | } else { 643 | return false 644 | } 645 | 646 | case deleteItemShortcut: 647 | if selectedItem != nil { 648 | withAnimation { 649 | dataManager.deleteItem(item: selectedItem!) 650 | showToast("Removed from clipboard") 651 | selectedItem = nil 652 | } 653 | return true 654 | } else { 655 | return false 656 | } 657 | 658 | default: 659 | return false 660 | } 661 | } 662 | 663 | 664 | func keyboardShortcutsHandler12(_ event: NSEvent) -> Bool { 665 | if !appDelegate.popoverFocused { 666 | return false 667 | } 668 | 669 | if isShowingConfirmationDialog { 670 | return false 671 | } 672 | 673 | if let responder = NSApplication.shared.keyWindow?.firstResponder { 674 | if responder.className.contains("SearchTextView") { 675 | return false 676 | } 677 | } 678 | 679 | let upArrowKey = KeyboardShortcuts.Shortcut(.upArrow, modifiers: []) 680 | let downArrowKey = KeyboardShortcuts.Shortcut(.downArrow, modifiers: []) 681 | 682 | guard let shortcut = KeyboardShortcuts.Shortcut(event: event) else { return false } 683 | 684 | switch shortcut { 685 | 686 | case clearClipboardShortcut: 687 | withAnimation { 688 | isShowingConfirmationDialog = true 689 | } 690 | return true 691 | 692 | case pasteShortcut: 693 | if selectedItem != nil { 694 | withAnimation { 695 | copyToClipboard(selectedItem!) 696 | appDelegate.togglePopover() 697 | Accessibility.check() 698 | 699 | DispatchQueue.main.async { 700 | let vCode = Sauce.shared.keyCode(for: .v) 701 | let source = CGEventSource(stateID: .combinedSessionState) 702 | source?.setLocalEventsFilterDuringSuppressionState([.permitLocalMouseEvents, .permitSystemDefinedEvents], 703 | state: .eventSuppressionStateSuppressionInterval) 704 | let keyVDown = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: true) 705 | let keyVUp = CGEvent(keyboardEventSource: source, virtualKey: vCode, keyDown: false) 706 | keyVDown?.flags = .maskCommand 707 | keyVUp?.flags = .maskCommand 708 | keyVDown?.post(tap: .cgAnnotatedSessionEventTap) 709 | keyVUp?.post(tap: .cgAnnotatedSessionEventTap) 710 | } 711 | } 712 | return true 713 | } else { 714 | return false 715 | } 716 | 717 | case copyAndHideShortcut: 718 | if selectedItem != nil { 719 | withAnimation { 720 | copyToClipboard(selectedItem!) 721 | appDelegate.togglePopover() 722 | } 723 | return true 724 | } else { 725 | return false 726 | } 727 | 728 | case togglePinShortcut: 729 | if selectedItem != nil { 730 | withAnimation { 731 | dataManager.togglePin(for: selectedItem!) 732 | showToast(selectedItem!.isPinned ? "Pinned" : "Unpinned") 733 | } 734 | return true 735 | } else { 736 | return false 737 | } 738 | 739 | case deleteItemShortcut: 740 | if selectedItem != nil { 741 | withAnimation { 742 | dataManager.deleteItem(item: selectedItem!) 743 | showToast("Removed from clipboard") 744 | selectedItem = nil 745 | } 746 | return true 747 | } else { 748 | return false 749 | } 750 | 751 | case upArrowKey: 752 | if selectedItem != nil { 753 | let selectedItemIndex = clipboardItems.firstIndex(of: selectedItem!)! 754 | if selectedItemIndex-1 != -1 { 755 | let nextItem = clipboardItems[selectedItemIndex-1] 756 | selectedItem = nextItem 757 | } 758 | } else { 759 | selectedItem = clipboardItems.first! 760 | } 761 | return true 762 | 763 | case downArrowKey: 764 | if selectedItem != nil { 765 | let selectedItemIndex = clipboardItems.firstIndex(of: selectedItem!)! 766 | if selectedItemIndex+1 != clipboardItems.count { 767 | let nextItem = clipboardItems[selectedItemIndex+1] 768 | selectedItem = nextItem 769 | } 770 | } else { 771 | selectedItem = clipboardItems.first! 772 | } 773 | return true 774 | 775 | default: 776 | return false 777 | } 778 | } 779 | } 780 | -------------------------------------------------------------------------------- /Macboard/Views/DetailedView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Defaults 3 | import KeyboardShortcuts 4 | 5 | struct DetailedView: View { 6 | 7 | let clipboardItem: ClipboardItem 8 | 9 | @Default(.showUrlMetadata) var showUrlMetadata 10 | 11 | @ObservedObject var vm: MetadataViewModel 12 | @Binding var selectedItem: ClipboardItem? 13 | 14 | @State private var hover: Bool = false 15 | 16 | var body: some View { 17 | if selectedItem != nil { 18 | VStack { 19 | if clipboardItem.contentType == "Text" || clipboardItem.contentType == "File" { 20 | List { 21 | Section { 22 | if clipboardItem.contentType == "Text" && clipboardItem.content!.isValidURL && showUrlMetadata { 23 | let imageURLString = vm.metadata.first(where: {$0.key == "Image"})?.value 24 | if imageURLString != nil { 25 | if imageURLString != "Not Found" { 26 | let imageURL = URL(string: imageURLString!)! 27 | RemoteImage(url: imageURL) 28 | } else { 29 | Image(systemName: "photo.fill") 30 | .resizable() 31 | .aspectRatio(contentMode: .fill) 32 | .scaledToFit() 33 | .frame(maxWidth: .infinity, alignment: .center) 34 | } 35 | } else { 36 | Image(systemName: "photo.fill") 37 | .resizable() 38 | .aspectRatio(contentMode: .fill) 39 | .scaledToFit() 40 | .frame(maxWidth: .infinity, alignment: .center) 41 | } 42 | 43 | } else { 44 | if clipboardItem.contentType == "Text" && clipboardItem.content!.isValidURL { 45 | Link(clipboardItem.content!, destination: URL(string: clipboardItem.content!)!) 46 | .textFieldStyle(.roundedBorder) 47 | .textSelection(.enabled) 48 | .onHover(perform: { isHovering in 49 | self.hover = isHovering 50 | DispatchQueue.main.async { 51 | if self.hover { 52 | NSCursor.pointingHand.push() 53 | } else { 54 | NSCursor.pop() 55 | } 56 | } 57 | }) 58 | } else { 59 | Text(clipboardItem.content!) 60 | .textSelection(.enabled) 61 | .onHover(perform: { isHovering in 62 | self.hover = isHovering 63 | DispatchQueue.main.async { 64 | if self.hover { 65 | NSCursor.iBeam.push() 66 | } else { 67 | NSCursor.pop() 68 | } 69 | } 70 | }) 71 | } 72 | } 73 | } header: { 74 | HStack { 75 | if clipboardItem.content!.isValidURL && showUrlMetadata { 76 | Image(systemName: "photo.fill") 77 | Text("Meta Image") 78 | } else if clipboardItem.contentType == "File" { 79 | Image(systemName: "doc.circle.fill") 80 | Text("Complete File Path") 81 | } else { 82 | Image(systemName: "doc.plaintext.fill") 83 | Text("Complete Text") 84 | } 85 | } 86 | } 87 | 88 | if clipboardItem.contentType == "Text" && clipboardItem.content!.isValidURL && showUrlMetadata { 89 | Section { 90 | HStack { 91 | Image(systemName: "person.badge.clock.fill") 92 | Text("Copied:") 93 | Spacer() 94 | Text(clipboardItem.createdAt!.timeAgoDisplay()) 95 | } 96 | 97 | HStack { 98 | Image(systemName: "link") 99 | Text("URL:") 100 | Spacer() 101 | Link(clipboardItem.content!, destination: URL(string: clipboardItem.content!)!) 102 | .textFieldStyle(.roundedBorder) 103 | .textSelection(.enabled) 104 | .onHover(perform: { isHovering in 105 | self.hover = isHovering 106 | DispatchQueue.main.async { 107 | if self.hover { 108 | NSCursor.pointingHand.push() 109 | } else { 110 | NSCursor.pop() 111 | } 112 | } 113 | }) 114 | } 115 | 116 | if let title = vm.metadata.first(where: {$0.key == "Title"})?.value { 117 | HStack { 118 | Image(systemName: "pencil") 119 | Text("Title:") 120 | Spacer() 121 | Text(title) 122 | } 123 | } 124 | 125 | if let description = vm.metadata.first(where: {$0.key == "Description"})?.value { 126 | HStack { 127 | Image(systemName: "note.text") 128 | Text("Description:") 129 | Spacer() 130 | Text(description) 131 | } 132 | } 133 | 134 | HStack { 135 | Image(systemName: "app.badge.checkmark.fill") 136 | Text("Source App:") 137 | Spacer() 138 | Text(clipboardItem.sourceApp ?? "Unknown") 139 | } 140 | 141 | } header: { 142 | HStack { 143 | Image(systemName: "info.circle.fill") 144 | Text("URL Details") 145 | } 146 | } 147 | 148 | } else { 149 | Section { 150 | HStack { 151 | Image(systemName: "person.badge.clock.fill") 152 | Text("Copied:") 153 | Spacer() 154 | Text(clipboardItem.createdAt!.timeAgoDisplay()) 155 | } 156 | 157 | HStack { 158 | Image(systemName: "note.text") 159 | Text("Type:") 160 | Spacer() 161 | if clipboardItem.contentType == "File" { 162 | Text("File") 163 | } else if clipboardItem.content!.contains("\n") { 164 | Text("Multi-line Text") 165 | } else if clipboardItem.content!.isNum { 166 | Text("Number") 167 | } else if clipboardItem.content!.isValidColor { 168 | Text("Color") 169 | } else { 170 | Text("Text") 171 | } 172 | } 173 | 174 | HStack { 175 | Image(systemName: "app.badge.checkmark.fill") 176 | Text("Source App:") 177 | Spacer() 178 | Text(clipboardItem.sourceApp ?? "Unknown") 179 | } 180 | 181 | } header: { 182 | HStack { 183 | Image(systemName: "info.circle.fill") 184 | Text("Details") 185 | } 186 | } 187 | } 188 | } 189 | .padding(.bottom, -8) 190 | 191 | } else { 192 | List { 193 | Section { 194 | let image = dataToImage(clipboardItem.imageData!) 195 | image.0 196 | .resizable() 197 | .aspectRatio(contentMode: .fill) 198 | .scaledToFit() 199 | .frame(maxWidth: .infinity, alignment: .center) 200 | } header: { 201 | HStack { 202 | Image(systemName: "photo.fill") 203 | Text("Image") 204 | } 205 | } 206 | 207 | Section { 208 | HStack { 209 | Image(systemName: "person.badge.clock.fill") 210 | Text("Copied:") 211 | Spacer() 212 | Text(clipboardItem.createdAt!.timeAgoDisplay()) 213 | } 214 | 215 | HStack { 216 | Image(systemName: "photo.fill") 217 | Text("Type:") 218 | Spacer() 219 | Text("TIFF Image") 220 | } 221 | 222 | HStack { 223 | Image(systemName: "app.badge.checkmark.fill") 224 | Text("Source App:") 225 | Spacer() 226 | Text(clipboardItem.sourceApp ?? "Unknown") 227 | } 228 | 229 | } header: { 230 | HStack { 231 | Image(systemName: "info.circle.fill") 232 | Text("Details") 233 | } 234 | } 235 | } 236 | .padding(.bottom, -8) 237 | } 238 | 239 | Divider() 240 | 241 | HStack { 242 | Text("Paste:") 243 | .font(.footnote) 244 | if let shortcut = KeyboardShortcuts.Name("paste").shortcut { 245 | Text(shortcutToText(shortcut)) 246 | .font(.footnote) 247 | .opacity(0.8) 248 | } else { 249 | Text("↩") 250 | .font(.footnote) 251 | .opacity(0.8) 252 | } 253 | CustomDivider() 254 | Text("Copy & Hide:") 255 | .font(.footnote) 256 | if let shortcut = KeyboardShortcuts.Name("copyAndHide").shortcut { 257 | Text(shortcutToText(shortcut)) 258 | .font(.footnote) 259 | .opacity(0.8) 260 | } else { 261 | Text("⌘ ↩") 262 | .font(.footnote) 263 | .opacity(0.8) 264 | } 265 | CustomDivider() 266 | Text(clipboardItem.isPinned ? "Unpin:" : "Pin:") 267 | .font(.footnote) 268 | if let shortcut = KeyboardShortcuts.Name("togglePin").shortcut { 269 | Text(shortcutToText(shortcut)) 270 | .font(.footnote) 271 | .opacity(0.8) 272 | } else { 273 | Text("⌘ P") 274 | .font(.footnote) 275 | .opacity(0.8) 276 | } 277 | CustomDivider() 278 | Text("Delete:") 279 | .font(.footnote) 280 | if let shortcut = KeyboardShortcuts.Name("deleteItem").shortcut { 281 | Text(shortcutToText(shortcut)) 282 | .font(.footnote) 283 | .opacity(0.8) 284 | } else { 285 | Text("⌫") 286 | .font(.footnote) 287 | .opacity(0.8) 288 | } 289 | } 290 | .padding(.top, -8) 291 | .frame(maxWidth: .infinity, minHeight: 18, idealHeight: 18, maxHeight: 18) 292 | } 293 | } else { 294 | Text("Select an item to get its detailed view") 295 | .bold() 296 | .padding() 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /Macboard/Views/SettingsViews/AboutSettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Settings 3 | 4 | struct AboutSettingsView: View { 5 | var body: some View { 6 | Settings.Container(contentWidth: 300) { 7 | Settings.Section(title: "", verticalAlignment: .center) { 8 | VStack(alignment: .center) { 9 | Image(nsImage: NSImage(named: "AppIcon") ?? NSImage()) 10 | .resizable() 11 | .scaledToFit() 12 | .frame(width: 50, height: 50) 13 | VStack(alignment: .leading) { 14 | Text("Macboard") 15 | .font(.headline) 16 | .frame(maxWidth: .infinity, alignment: .center) 17 | Text("- a minimalistic clipboard manager for macOS") 18 | .padding(.top, 4) 19 | .frame(maxWidth: .infinity, alignment: .center) 20 | } 21 | Divider() 22 | HStack { 23 | Button { 24 | if let url = URL(string: "https://twitter.com/saums27") { 25 | NSWorkspace.shared.open(url) 26 | } 27 | } label: { 28 | Text("Developer") 29 | } 30 | Button { 31 | if let url = URL(string: "https://saumya.lol/macboard") { 32 | NSWorkspace.shared.open(url) 33 | } 34 | } label: { 35 | Text("Website") 36 | } 37 | Button { 38 | if let url = URL(string: "https://github.com/27Saumya") { 39 | NSWorkspace.shared.open(url) 40 | } 41 | } label: { 42 | Text("Github") 43 | } 44 | } 45 | } 46 | .frame(maxWidth: .infinity, alignment: .center) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Macboard/Views/SettingsViews/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Settings 3 | import LaunchAtLogin 4 | import Defaults 5 | 6 | struct GeneralSettingsView: View { 7 | 8 | @Default(.showSearchbar) var showSearchbar 9 | @Default(.showUrlMetadata) var showUrlMetadata 10 | @Default(.searchType) var searchType 11 | @Default(.menubarIcon) var menubarIcon 12 | 13 | var body: some View { 14 | Settings.Container(contentWidth: 300) { 15 | Settings.Section(title: "") { 16 | VStack(alignment: .leading) { 17 | LaunchAtLogin.Toggle() 18 | CheckForUpdatesView(updaterViewController: UpdaterViewController()) 19 | Divider() 20 | .padding(.vertical, 2) 21 | Toggle("Show search bar", isOn: $showSearchbar) 22 | Toggle("Show URL metadata", isOn: $showUrlMetadata) 23 | Picker(selection: $searchType, label: Text("Search")) { 24 | Text("Case Sensitive") 25 | .tag(SearchType.sensitive) 26 | Text("Case Insensitive") 27 | .tag(SearchType.insensitive) 28 | } 29 | .frame(width: 180) 30 | Picker(selection: $menubarIcon, label: Text("Menu bar icon")) { 31 | Image(systemName: "doc.on.clipboard") 32 | .tag(MenubarIcon.normal) 33 | Image(systemName: "doc.on.clipboard.fill") 34 | .tag(MenubarIcon.fill) 35 | Image(systemName: "paperclip") 36 | .tag(MenubarIcon.clip) 37 | Image(systemName: "scissors") 38 | .tag(MenubarIcon.scissors) 39 | } 40 | .frame(width: 150) 41 | Text("Icon changes require a re-launch to get reflected") 42 | .padding(.top, 4) 43 | .opacity(0.8) 44 | .font(.footnote) 45 | Button { 46 | relaunch() 47 | } label: { 48 | Text("Relaunch Now") 49 | .font(.footnote) 50 | } 51 | .padding(.top, 1) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Macboard/Views/SettingsViews/KeyboardSettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Settings 3 | import KeyboardShortcuts 4 | 5 | struct KeyboardSettingsView: View { 6 | 7 | var body: some View { 8 | Settings.Container(contentWidth: 450) { 9 | Settings.Section(title: "") { 10 | VStack(alignment: .center) { 11 | Form { 12 | HStack { 13 | KeyboardShortcuts.Recorder("Toggle Macboard:", name: .toggleMacboard) 14 | Button { 15 | KeyboardShortcuts.Name("toggleMacboard").shortcut = KeyboardShortcuts.Shortcut(.v, modifiers: [.shift, .command]) 16 | } label: { 17 | Text("Reset") 18 | .font(.footnote) 19 | } 20 | } 21 | .padding(.leading, -12) 22 | } 23 | .frame(maxWidth: .infinity, alignment: .center) 24 | Divider() 25 | .padding(.vertical, 4) 26 | Form { 27 | HStack { 28 | KeyboardShortcuts.Recorder("Clear Clipboard:", name: .clearClipboard) 29 | Button { 30 | KeyboardShortcuts.Name("clearClipboard").shortcut = KeyboardShortcuts.Shortcut(.delete, modifiers: [.command]) 31 | } label: { 32 | Text("Reset") 33 | .font(.footnote) 34 | } 35 | } 36 | HStack { 37 | KeyboardShortcuts.Recorder("Paste:", name: .paste) 38 | Button { 39 | KeyboardShortcuts.Name("paste").shortcut = KeyboardShortcuts.Shortcut(.return, modifiers: []) 40 | } label: { 41 | Text("Reset") 42 | .font(.footnote) 43 | } 44 | } 45 | HStack { 46 | KeyboardShortcuts.Recorder("Copy & Hide:", name: .copyAndHide) 47 | Button { 48 | KeyboardShortcuts.Name("copyAndHide").shortcut = KeyboardShortcuts.Shortcut(.return, modifiers: [.option]) 49 | } label: { 50 | Text("Reset") 51 | .font(.footnote) 52 | } 53 | } 54 | HStack { 55 | KeyboardShortcuts.Recorder("Toggle Pin:", name: .togglePin) 56 | Button { 57 | KeyboardShortcuts.Name("togglePin").shortcut = KeyboardShortcuts.Shortcut(.p, modifiers: [.command]) 58 | } label: { 59 | Text("Reset") 60 | .font(.footnote) 61 | } 62 | } 63 | HStack { 64 | KeyboardShortcuts.Recorder("Delete Item:", name: .deleteItem) 65 | Button { 66 | KeyboardShortcuts.Name("deleteItem").shortcut = KeyboardShortcuts.Shortcut(.delete, modifiers: []) 67 | } label: { 68 | Text("Reset") 69 | .font(.footnote) 70 | } 71 | } 72 | } 73 | Text("Custom keyboard shortcuts require a re-launch to get reflected") 74 | .padding(.top, 6) 75 | .opacity(0.8) 76 | .font(.footnote) 77 | Button { 78 | relaunch() 79 | } label: { 80 | Text("Relaunch Now") 81 | .font(.footnote) 82 | } 83 | .padding(.top, 2) 84 | .frame(maxWidth: .infinity, alignment: .center) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Macboard/Views/SettingsViews/StorageSettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Settings 3 | import Defaults 4 | 5 | struct StorageSettingsView: View { 6 | 7 | @Default(.allowedTypes) var allowedTypes 8 | @Default(.maxItems) var maxItems 9 | @Default(.clearPins) var clearPins 10 | 11 | @State var textAllowed: Bool = Bool() 12 | @State var imageAllowed: Bool = Bool() 13 | @State var fileAllowed: Bool = Bool() 14 | 15 | var body: some View { 16 | Settings.Container(contentWidth: 350) { 17 | Settings.Section(title: "") { 18 | VStack(alignment: .leading) { 19 | Toggle("Clear pins while clearing clipboard", isOn: $clearPins) 20 | Divider() 21 | .padding(.vertical, 2) 22 | Toggle("Save Text", isOn: $textAllowed) 23 | .onAppear { 24 | textAllowed = allowedTypes.contains("Text") 25 | } 26 | .onChange(of: textAllowed) { newValue in 27 | if newValue == true { 28 | if !allowedTypes.contains("Text") { 29 | allowedTypes.append("Text") 30 | } 31 | } else { 32 | if let index = allowedTypes.firstIndex(of: "Text") { 33 | allowedTypes.remove(at: index) 34 | } 35 | } 36 | } 37 | Toggle("Save Images", isOn: $imageAllowed) 38 | .onAppear { 39 | imageAllowed = allowedTypes.contains("Image") 40 | } 41 | .onChange(of: imageAllowed) { newValue in 42 | if newValue == true { 43 | if !allowedTypes.contains("Image") { 44 | allowedTypes.append("Image") 45 | } 46 | } else { 47 | if let index = allowedTypes.firstIndex(of: "Image") { 48 | allowedTypes.remove(at: index) 49 | } 50 | } 51 | } 52 | Toggle("Save Files", isOn: $fileAllowed) 53 | .onAppear { 54 | fileAllowed = allowedTypes.contains("File") 55 | } 56 | .onChange(of: fileAllowed) { newValue in 57 | if newValue == true { 58 | if !allowedTypes.contains("File") { 59 | allowedTypes.append("File") 60 | } 61 | } else { 62 | if let index = allowedTypes.firstIndex(of: "File") { 63 | allowedTypes.remove(at: index) 64 | } 65 | } 66 | } 67 | Text("Customise what type of content should be saved and displayed") 68 | .font(.footnote) 69 | .opacity(0.7) 70 | .padding(.top, 2) 71 | Divider() 72 | .padding(.top, 2) 73 | .padding(.bottom, 6) 74 | HStack { 75 | Text("Maximum Items") 76 | TextField("", value: $maxItems, formatter: NumberFormatter()) 77 | .frame(width: 75) 78 | Stepper(value: $maxItems) { 79 | 80 | } 81 | } 82 | Text("0 for unlimited") 83 | .font(.footnote) 84 | .opacity(0.7) 85 | .frame(maxWidth: .infinity, alignment: .center) 86 | .padding(.leading, -51) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /Macboard/Views/Updates.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Sparkle 3 | import Defaults 4 | 5 | 6 | final class UpdaterViewController: ObservableObject { 7 | let updaterController: SPUStandardUpdaterController 8 | 9 | @Published var canCheckForUpdates = false 10 | 11 | init() { 12 | updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 13 | 14 | updaterController.updater.publisher(for: \.canCheckForUpdates) 15 | .assign(to: &$canCheckForUpdates) 16 | } 17 | 18 | func toggleAutoUpdates(_ value: Bool) { 19 | updaterController.updater.automaticallyChecksForUpdates = value 20 | } 21 | 22 | func checkForUpdates() { 23 | updaterController.checkForUpdates(nil) 24 | } 25 | } 26 | 27 | struct CheckForUpdatesView: View { 28 | 29 | @ObservedObject var updaterViewController: UpdaterViewController 30 | 31 | @Default(.autoUpdate) var autoUpdate 32 | 33 | var body: some View { 34 | Toggle("Automatically check for updates", isOn: $autoUpdate) 35 | .onChange(of: autoUpdate) { newValue in 36 | updaterViewController.toggleAutoUpdates(newValue) 37 | } 38 | Button { 39 | updaterViewController.checkForUpdates() 40 | } label: { 41 | Text("Check Now") 42 | .font(.footnote) 43 | } 44 | .disabled(!updaterViewController.canCheckForUpdates) 45 | .padding(.top, 1) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MacboardTests/MacboardTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacboardTests.swift 3 | // MacboardTests 4 | // 5 | // Created by Saumya Patel on 29/01/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Macboard 10 | 11 | final class MacboardTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | func testContentClipboardItem() throws { 37 | // let clipboardItem = ClipboardItem(content: "test", isFavourite: false, contentType: .text) 38 | } 39 | 40 | func testImageClipboardItem() throws { 41 | // let data = Data() 42 | // let clipboardItem = ClipboardItem(imageData: data, isFavourite: false, contentType: .text) 43 | } 44 | 45 | func testDataToImage() throws { 46 | let data = Data() 47 | let image = dataToImage(data) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Logo 2 | 3 | # Macboard 4 | ![Downloads)](https://img.shields.io/github/downloads/27Saumya/Macboard/total?label=Downloads%20(Github%20Releases)&style=for-the-badge&color=%23a1ada4&link=https%3A%2F%2Fgithub.com%2F27Saumya%2FMacboard%2Freleases%2Flatest) 5 | 6 | Boost your productivity within seconds! 7 | 8 | ![demo](https://github.com/27Saumya/Macboard/assets/64534496/75c45f5e-98fd-4552-8ae3-7dc137ad920f) 9 | 10 | 11 | ## Table Of Contents 12 | - [Overview](#overview) 13 | - [Installation](#installation) 14 | - [Features](#features) 15 | - [Keyboard Shortcuts](#keyboard-shortcuts) 16 | 17 | ### Overview 18 | 19 | Macboard is a minimalistic, blazingly fast and lightweight clipboard manager for MacOS 20 |
21 | It works on macOS Monterey 12.0 or higher 22 | 23 | ### Installation 24 | 25 | - Install the `Macboard.dmg` disk image from [Github Releases](https://github.com/27Saumya/Macboard/releases). 26 | - Open the `Macboard.dmg` file and drag the Macboard icon into your `Applications/` folder. 27 | - Open the `Macboard` app now, you might face a warning regarding `Unidentified Developer!`, click on `Open Anyway`/manually grant Macboard permissions, (as the app isn't notarized because a code signing certificate is too expensive 😭 and this application is **free** and **open source**) and you're good to go! 28 | 29 | ### Features 30 | 31 | - Compact, user-friendly, clean UI 32 | - Quick filtering with a search bar 33 | - Completely customisable settings 34 | - Compatible with system theme 35 | - Images support 36 | - URL metadata preview 37 | - Hyperlinks highlighting 38 | - Pins 39 | - Complete keyboard control 40 | 41 | ### Keyboard Shortcuts 42 | All the keyboard shortcuts within Macboard are completely customisable! 43 | Here are the default ones: 44 | 45 | - Toggle Macboard -> `⇧ ⌘ V` 46 | - Clear Clipboard -> `⌘ ⌫` 47 | - Paste the selected item -> `↩` 48 | - Copy the selected item & hide Macboard -> `⌥ ↩` 49 | - Pin the selected item -> `⌘ P` 50 | - Delete the selected item -> `⌫` 51 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Macboard 5 | https://raw.githubusercontent.com/27Saumya/Macboard/master/appcast.xml 6 | 7 | 1.6 8 | 9 | 11 |
  • Added support for macOS Sequoia 15.0+
  • 12 |
  • Fixed hex color previews causing the app to crash
  • 13 |
  • Fixed some minor bugs and refactored code for best performance
  • 14 | 15 | ]]> 16 |
    17 | Wed, 15 Jan 2025 14:12:08 +0530 18 | 1.6 19 | 1.6 20 | 12.0 21 | 22 |
    23 |
    24 |
    --------------------------------------------------------------------------------