├── .DS_Store ├── .gitignore ├── Checkpoints.sketchplugin └── Contents │ └── Sketch │ ├── appcast │ ├── main.cocoascript │ ├── manifest.json │ └── utility.cocoascript └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/einancunlu/Checkpoints-Plugin-for-Sketch/8dfdb33fcbe5a17f0e5dbb900b52681f0883886e/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Checkpoints.sketchplugin/Contents/Sketch/appcast: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Checkpoints Plugin 5 | http://sparkle-project.org/files/sparkletestcast.xml 6 | Save important stages of your artboards in the blink of an eye, and then, move fast and break things. 7 | en 8 | 9 | Version 1.2 10 | 11 | 13 |
  • Sketch 3.8 fix.
  • 14 | 15 | ]]> 16 |
    17 | 18 |
    19 | 20 | Version 1.3 21 | 22 | 24 |
  • Sketch 4.5 update.
  • 25 | 26 | ]]> 27 |
    28 | 29 |
    30 |
    31 |
    32 | -------------------------------------------------------------------------------- /Checkpoints.sketchplugin/Contents/Sketch/main.cocoascript: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // Created by Emin İnanç Ünlü 5 | // 6 | 7 | 8 | /* 9 | The MIT License (MIT) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | 30 | 31 | @import 'utility.cocoascript' 32 | 33 | 34 | //-------------------------------------- 35 | // Global Variables 36 | //-------------------------------------- 37 | 38 | 39 | var doc, 40 | command, 41 | selection, 42 | scriptPath, 43 | scriptFolder, 44 | app = [NSApplication sharedApplication], 45 | defaults = [NSUserDefaults standardUserDefaults], 46 | 47 | kPluginDomain = "com.einancunlu.sketch-plugins.checkpoints", 48 | 49 | kLastUpdateCheckDayKey = kPluginDomain + ".lastUpdateCheckDayKey" 50 | kCheckpointGroupName = "Checkpoints", 51 | kCheckpointGroupKey = kPluginDomain + "checkpointGroup", 52 | kCheckpointNameKey = kPluginDomain + "checkpointName", 53 | kCheckpointGroupBGLayerName = "BG", 54 | kMaxCheckpointCountKey = kPluginDomain + "maxCheckpointCount", 55 | maxCheckpointCount = 3 56 | 57 | 58 | //-------------------------------------- 59 | // Menu Commands 60 | //-------------------------------------- 61 | 62 | 63 | function saveCheckpoint(context) { 64 | 65 | initCommand(context) 66 | maxCheckpointCount = [defaults objectForKey: kMaxCheckpointCountKey] || maxCheckpointCount 67 | 68 | // Create checkpoitns for all selected layers 69 | selection = context.selection 70 | if ([selection count] == 0) { 71 | [doc showMessage: "Select some layer(s) or artboard(s) first."] 72 | return 73 | } else { 74 | var successfulOperationCount = 0 75 | for (var i = 0; i < [selection count]; i++) { 76 | var selectedLayer = selection.objectAtIndex(i) 77 | var artboard = artboardOfLayer(selectedLayer) 78 | if (artboard) { 79 | var checkpointGroup = checkpointGroupForArtboard(artboard) 80 | newCheckpoint(checkpointGroup, artboard) 81 | checkpointGroup.setIsVisible(false) 82 | successfulOperationCount++ 83 | } 84 | } 85 | } 86 | 87 | // Give notification 88 | var failedOperationCount = [selection count] - successfulOperationCount 89 | if (failedOperationCount > 0) { 90 | [doc showMessage: "Failed to create checkpoints for: " + failedOperationCount + " artboards. For others, checkpoints are created succesfully."] 91 | } else { 92 | if (successfulOperationCount > 1) { 93 | [doc showMessage: "Checkpoints are created successfully."] 94 | } else { 95 | [doc showMessage: "Checkpoint is created successfully."] 96 | } 97 | } 98 | } 99 | 100 | function changeMaxCheckpointCount(context) { 101 | 102 | initCommand(context) 103 | maxCheckpointCount = [defaults objectForKey: kMaxCheckpointCountKey] || maxCheckpointCount 104 | 105 | var description = "Enter the max Checkpoint Count" 106 | var newCount = [doc askForUserInput: description initialValue: maxCheckpointCount] 107 | if (newCount) { 108 | if (isInt(newCount)) { 109 | saveObjectToUserDefaults(newCount, kMaxCheckpointCountKey) 110 | maxCheckpointCount = newCount 111 | [doc showMessage: "New max checkpoint count: " + maxCheckpointCount] 112 | } else { 113 | [doc showMessage: "Input should be an integer, please enter again."] 114 | } 115 | } else { 116 | [doc showMessage: "No input was taken."] 117 | } 118 | } 119 | 120 | function feedbackByMail(context) { 121 | 122 | initCommand(context) 123 | 124 | var encodedSubject = [NSString stringWithFormat:@"SUBJECT=%@", [@"Feedback on Checkpoints Plugin" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]], 125 | encodedBody = [NSString stringWithFormat:@"BODY=%@", [@"" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]], 126 | encodedTo = [[NSString stringWithFormat:@"apps.einancunlu", @"@gma", @"il.com"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], 127 | encodedURLString = [NSString stringWithFormat:@"mailto:%@?%@&%@", encodedTo, encodedSubject, encodedBody], 128 | mailtoURL = [NSURL URLWithString:encodedURLString] 129 | [[NSWorkspace sharedWorkspace] openURL:mailtoURL] 130 | } 131 | 132 | function feedbackByTwitter(context) { 133 | 134 | initCommand(context) 135 | 136 | var urlString = "https://twitter.com/einancunlu" 137 | [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: urlString]] 138 | } 139 | 140 | function initCommand(context) { 141 | 142 | doc = context.document 143 | command = context.command 144 | selection = context.selection 145 | scriptPath = context.scriptPath 146 | scriptFolder = [scriptPath stringByDeletingLastPathComponent] 147 | 148 | if (getSketchVersionNumber() < 450 && isTodayNewDay() && checkPluginUpdate()) { 149 | app.displayDialog_withTitle("Please redownload the plugin to install the new version.", "There is a new version of the plugin!") 150 | } 151 | } 152 | 153 | 154 | //-------------------------------------- 155 | // Helper Functions 156 | //-------------------------------------- 157 | 158 | 159 | function checkpointGroupForArtboard(artboard) { 160 | 161 | // Find parent group of checkpoints (checkpointGroup) 162 | var checkpointGroup 163 | var layers = artboard.layers() 164 | for (var i = 0; i < [layers count]; i++) { 165 | var layer = layers.objectAtIndex(i) 166 | if ([command valueForKey:kCheckpointGroupKey onLayer: layer]) { 167 | checkpointGroup = layer 168 | // Move to front 169 | checkpointGroup.removeFromParent() 170 | [artboard insertLayers: [checkpointGroup] atIndex: [layers count]] 171 | } 172 | } 173 | if (!checkpointGroup) { 174 | checkpointGroup = createCheckpointGroup(artboard) 175 | } 176 | return checkpointGroup 177 | } 178 | 179 | function newCheckpoint(checkpointGroup, artboard) { 180 | 181 | // Find layers to be saved 182 | var layers = [] 183 | var artboardLayers = artboard.layers() 184 | for (var i = 0; i < [artboardLayers count]; i++) { 185 | var layer = artboardLayers.objectAtIndex(i) 186 | if (![command valueForKey:kCheckpointGroupKey onLayer:layer]) { 187 | layers.push(layer.copy()) 188 | } 189 | } 190 | 191 | // Prepare the name of the checkpoint group 192 | var checkpointName = [command valueForKey:kCheckpointNameKey onLayer:checkpointGroup] 193 | if (checkpointName) { 194 | checkpointName++ 195 | [command setValue:checkpointName forKey:kCheckpointNameKey onLayer:checkpointGroup] 196 | checkpointName = checkpointName + "" 197 | } else { 198 | [command setValue:1 forKey:kCheckpointNameKey onLayer:checkpointGroup] 199 | checkpointName = "1" 200 | } 201 | checkpointName = addDateToName(checkpointName) 202 | 203 | prepareCheckpointsForUpdate(checkpointGroup) 204 | 205 | // Create and add the new checkpoint group 206 | var checkpoint = addGroup(checkpointName, checkpointGroup) 207 | var BGLayer = createBGLayer(checkpoint, artboard) 208 | checkpoint.addLayers(layers) 209 | cleanAllStylesAndSymbols(checkpoint) 210 | if (getSketchVersionNumber() >= 350) { 211 | checkpoint.resizeToFitChildrenWithOption(1) 212 | } else { 213 | checkpoint.resizeRoot(true) 214 | } 215 | } 216 | 217 | function prepareCheckpointsForUpdate(checkpointGroup) { 218 | 219 | // Hide all checkpoint groups and determine the extras to be deleted 220 | var checkpoints = checkpointGroup.layers() 221 | var checkpointCount = checkpoints.count() 222 | var extraCheckpointNumber = checkpointCount - maxCheckpointCount + 1 223 | var extraCheckpoints = [] 224 | for (var i = 0; i < checkpointCount; i++) { 225 | var checkpoint = checkpoints.objectAtIndex(i) 226 | if (i < extraCheckpointNumber) { 227 | extraCheckpoints.push(checkpoint) 228 | } else { 229 | checkpoint.setIsVisible(false) 230 | } 231 | } 232 | 233 | // Delete the extras 234 | for (var i = 0; i < extraCheckpoints.length; i++) { 235 | var checkpoint = extraCheckpoints[i] 236 | [checkpoint removeFromParent] 237 | } 238 | } 239 | 240 | function createCheckpointGroup(artboard) { 241 | 242 | var checkpointGroup = addGroup(kCheckpointGroupName, artboard) 243 | [command setValue:true forKey:kCheckpointGroupKey onLayer:checkpointGroup] 244 | return checkpointGroup 245 | } 246 | 247 | function createCheckpoint(checkpointGroup) { 248 | 249 | var BGLayer = createBGLayer(checkpointGroup, artboard) 250 | checkpointGroup.addLayers([BGLayer]) 251 | if (getSketchVersionNumber() >= 350) { 252 | checkpointGroup.resizeToFitChildrenWithOption(1) 253 | } else { 254 | checkpointGroup.resizeRoot(true) 255 | } 256 | return checkpointGroup 257 | } 258 | 259 | function createBGLayer(parent, referenceArtboard) { 260 | 261 | var BGLayer = addLayer(kCheckpointGroupBGLayerName, 'rectangle', parent) 262 | 263 | // Set size 264 | setLayerSizeEqualToReferenceLayer(BGLayer, referenceArtboard) 265 | 266 | // Set Style 267 | var fill 268 | if (getSketchVersionNumber() >= 380) { 269 | fill = BGLayer.style().addStylePartOfType(0) 270 | } else { 271 | var fills = BGLayer.style().fills() 272 | fill = [fills addNewStylePart] 273 | } 274 | fill.color = [referenceArtboard backgroundColor] 275 | 276 | return BGLayer 277 | } 278 | 279 | function cleanAllStylesAndSymbols(container) { 280 | 281 | var children = container.children() 282 | for (var i = 0; i < [children count]; i++) { 283 | var layer = children[i] 284 | if (getSketchVersionNumber() >= 370) { 285 | if ([layer class] === MSSymbolInstance) { 286 | detachSymbolInstance(layer) 287 | } else if (layer.style != undefined) { 288 | layer.style().setSharedObjectID(null) 289 | } 290 | } else { 291 | if ([layer class] === MSLayerGroup) { 292 | layer.setSharedObjectID(null) 293 | } else if (layer.style != undefined) { 294 | layer.style().setSharedObjectID(null) 295 | } 296 | } 297 | } 298 | doc.reloadInspector() 299 | } 300 | 301 | function detachSymbolInstance(layer) { 302 | 303 | var detachedGroup = layer.detachByReplacingWithGroup() 304 | var children = detachedGroup.children() 305 | for (var i = 0; i < [children count]; i++) { 306 | var childLayer = children.objectAtIndex(i) 307 | cleanAllStylesAndSymbols(childLayer) 308 | } 309 | } 310 | 311 | function addDateToName(name) { 312 | 313 | var formatter = [[NSDateFormatter alloc] init] 314 | [formatter setDateStyle: NSDateFormatterShortStyle] 315 | var now = [NSDate date] 316 | return name + " - " + [formatter stringFromDate: now] 317 | } 318 | 319 | function artboardOfLayer(layer) { 320 | 321 | if ([layer class] === MSArtboardGroup) return layer 322 | var parent = [layer parentGroup] 323 | if (parent) { 324 | if ([parent class] === MSArtboardGroup) { 325 | return parent 326 | } else { 327 | return artboardOfLayer(parent) 328 | } 329 | } else { 330 | return nil 331 | } 332 | } 333 | 334 | function isTodayNewDay() { 335 | 336 | var lastUpdateCheckDay = [defaults objectForKey: kLastUpdateCheckDayKey] 337 | 338 | var formatter = [[NSDateFormatter alloc] init] 339 | [formatter setDateStyle: NSDateFormatterShortStyle] 340 | var today = [formatter stringFromDate: [NSDate date]] 341 | saveObjectToUserDefaults(today, kLastUpdateCheckDayKey) 342 | 343 | if (lastUpdateCheckDay) { 344 | return lastUpdateCheckDay != today 345 | } else { 346 | return true 347 | } 348 | } 349 | 350 | -------------------------------------------------------------------------------- /Checkpoints.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Checkpoints", 3 | "description": "Save important stages of your artboards in the blink of an eye, and then, move fast and break things.", 4 | "author": "Emin Inanc Unlu", 5 | "homepage": "https://github.com/einancunlu/Checkpoints-Plugin-for-Sketch", 6 | "version": 1.2, 7 | "appcast" : "https://raw.githubusercontent.com/einancunlu/Checkpoints-Plugin-for-Sketch/master/Checkpoints.sketchplugin/Contents/Sketch/appcast", 8 | "compatibleVersion": 3.5, 9 | "identifier": "com.einancunlu.sketch-plugins.checkpoints", 10 | "commands": [ 11 | { 12 | "handler": "saveCheckpoint", 13 | "identifier": "saveCheckpoint", 14 | "name": "Save a Checkpoint", 15 | "shortcut" : "shift control option s", 16 | "script": "main.cocoascript" 17 | }, 18 | { 19 | "handler": "changeMaxCheckpointCount", 20 | "identifier": "changeMaxCheckpointCount", 21 | "name": "Change Max Checkpoint Count", 22 | "script": "main.cocoascript" 23 | }, 24 | { 25 | "handler": "feedbackByMail", 26 | "identifier": "feedbackByMail", 27 | "name": "Mail", 28 | "shortcut" : "", 29 | "script": "main.cocoascript" 30 | }, 31 | { 32 | "handler": "feedbackByTwitter", 33 | "identifier": "feedbackByTwitter", 34 | "name": "Twitter", 35 | "shortcut" : "", 36 | "script": "main.cocoascript" 37 | } 38 | ], 39 | "menu": { 40 | "title": "Checkpoints", 41 | "items": [ 42 | "saveCheckpoint", 43 | "changeMaxCheckpointCount", 44 | "-", 45 | { 46 | "title": "Feedback / Contact", 47 | "items": [ 48 | "feedbackByMail", 49 | "feedbackByTwitter" 50 | ] 51 | } 52 | ] 53 | } 54 | } -------------------------------------------------------------------------------- /Checkpoints.sketchplugin/Contents/Sketch/utility.cocoascript: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | The MIT License (MIT) 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 | */ 23 | 24 | 25 | function removeLayer(layer) { 26 | 27 | var parent = [layer parentGroup] 28 | if (parent)[parent removeLayer: layer] 29 | } 30 | 31 | function addLayer(name, type, parent) { 32 | 33 | var layer 34 | switch(type) { 35 | case "rectangle": 36 | var rectangleShape = MSRectangleShape.alloc().init() 37 | rectangleShape.frame = MSRect.rectWithRect(NSMakeRect(0, 0, 50, 50)) 38 | layer = MSShapeGroup.shapeWithPath(rectangleShape) 39 | parent.addLayers([layer]) 40 | break 41 | case "group": 42 | layer = [[MSLayerGroup alloc] init] 43 | [parent addLayers:[layer]] 44 | break 45 | default: 46 | break 47 | } 48 | if (name) [layer setName: name] 49 | return layer 50 | } 51 | 52 | function addGroup(name, parent) { 53 | 54 | return addLayer(name, 'group', parent) 55 | } 56 | 57 | function isInt(value) { 58 | 59 | return !isNaN(value) && 60 | parseInt(Number(value)) == value && 61 | !isNaN(parseInt(value, 10)) 62 | } 63 | 64 | function saveObjectToUserDefaults(object, key) { 65 | 66 | var configs = [NSMutableDictionary dictionary] 67 | [configs setObject: object forKey: key] 68 | [defaults registerDefaults: configs] 69 | [defaults synchronize] 70 | } 71 | 72 | function setLayerSizeEqualToReferenceLayer(layer, referenceLayer) { 73 | 74 | var rect = [referenceLayer absoluteRect] 75 | [[layer absoluteRect] setX: [rect x]] 76 | [[layer absoluteRect] setY: [rect y]] 77 | [[layer absoluteRect] setWidth: [rect width]] 78 | [[layer absoluteRect] setHeight: [rect height]] 79 | } 80 | 81 | function getSketchVersionNumber() { 82 | 83 | const version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] 84 | var versionNumber = version.stringByReplacingOccurrencesOfString_withString(".", "") + "" 85 | while(versionNumber.length != 3) { 86 | versionNumber += "0" 87 | } 88 | return parseInt(versionNumber) 89 | } 90 | 91 | function checkPluginUpdate() { 92 | 93 | var manifestFilePath = scriptFolder + "/manifest.json" 94 | var manifestJSON = getJSONFromFile(manifestFilePath) 95 | var isThereNewUpdate = false 96 | try { 97 | var response = getJSONFromURL('https://raw.githubusercontent.com/einancunlu/Checkpoints-Plugin-for-Sketch/master/Checkpoints.sketchplugin/Contents/Sketch/manifest.json') 98 | if (response && response.version) { 99 | if (response.version.toString() != manifestJSON.version.toString()) { 100 | isThereNewUpdate = true 101 | } 102 | } 103 | } catch (e) { 104 | log(e) 105 | return false 106 | } 107 | return isThereNewUpdate 108 | } 109 | 110 | 111 | //-------------------------------------- 112 | // JSON 113 | //-------------------------------------- 114 | 115 | 116 | function getJSONFromFile(filePath) { 117 | 118 | var data = [NSData dataWithContentsOfFile: filePath] 119 | return [NSJSONSerialization JSONObjectWithData: data options: 0 error: nil] 120 | } 121 | 122 | function getJSONFromURL(url) { 123 | 124 | var request = [NSURLRequest requestWithURL: [NSURL URLWithString:url]], 125 | response = [NSURLConnection sendSynchronousRequest: request returningResponse: nil error: nil], 126 | responseObj = [NSJSONSerialization JSONObjectWithData: response options: nil error: nil] 127 | return responseObj 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Checkpoints for Sketch 2 | 3 | Check out my [Medium post](https://medium.com/@einancunlu/checkpoints-plugin-for-sketch-482c135f0186#.i2lawplk5) for the details. 4 | 5 | # Changelog 6 | 7 | ## v1.3 8 | - Sketch 4.5 fix. 9 | 10 | ## v1.2 11 | - Sketch 3.8 fix. 12 | 13 | ## v1.1 14 | - Sketch 3.7 fix. 15 | 16 | # Installation 17 | 18 | ### Manuel: 19 | Double click the `Checkpoints.sketchplugin` file to install. 20 | 21 | ### Sketch Toolbox: 22 | 1. [Download the Sketch Toolbox plugin manager app](http://sketchtoolbox.com) and install. 23 | 2. Search `Checkpoints ` in the app and install. 24 | 25 | # Contact 26 | 27 | [Twitter](https://twitter.com/einancunlu). 28 | 29 | # License 30 | 31 | The MIT License (MIT). --------------------------------------------------------------------------------