├── MakeExportable.sketchplugin └── Contents │ ├── Resources │ └── icon.png │ └── Sketch │ ├── manifest.json │ └── script.js └── README.md /MakeExportable.sketchplugin/Contents/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abynim/sketch-exportable/9abfd2981a3fa38f92ad81e35650568e8b135a75/MakeExportable.sketchplugin/Contents/Resources/icon.png -------------------------------------------------------------------------------- /MakeExportable.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Aby Nimbalkar", 3 | "commands" : [ 4 | { 5 | "script" : "script.js", 6 | "handler" : "saveAsPreset", 7 | "name" : "New Preset", 8 | "identifier" : "saveAsPreset" 9 | }, 10 | { 11 | "script" : "script.js", 12 | "handler" : "restorePresets", 13 | "name" : "Restore Presets", 14 | "identifier" : "restorePresets" 15 | }, 16 | { 17 | "script" : "script.js", 18 | "handler" : "importPresets", 19 | "name" : "Import Presets", 20 | "identifier" : "importPresets" 21 | }, 22 | { 23 | "script" : "script.js", 24 | "handler" : "exportPresets", 25 | "name" : "Export Presets", 26 | "identifier" : "exportPresets" 27 | }, 28 | { 29 | "script" : "script.js", 30 | "handler" : "deletePresets", 31 | "name" : "Delete Presets", 32 | "identifier" : "deletePresets" 33 | }, 34 | { 35 | "script" : "script.js", 36 | "handler" : "clearFormats", 37 | "name" : "Remove Formats for Selection", 38 | "identifier" : "removeAllFormats" 39 | } 40 | ], 41 | "bundleVersion" : 1, 42 | "menu" : { 43 | "items" : [ 44 | "-", 45 | "removeAllFormats", 46 | "-", 47 | { 48 | "title" : "Configure", 49 | "items" : [ 50 | "saveAsPreset", 51 | "-", 52 | "importPresets", 53 | "exportPresets", 54 | "-", 55 | "restorePresets", 56 | "deletePresets" 57 | ] 58 | } 59 | ] 60 | }, 61 | "compatibleVersion" : 40, 62 | "identifier" : "com.abynim.sketchplugins.exportable", 63 | "version" : "1.0", 64 | "description" : "Export options for Sketch layers.", 65 | "homepage" : "https:\/\/github.com\/abynim\/exportable", 66 | "authorEmail" : "abynimbalkar@gmail.com", 67 | "name" : "Make Exportable" 68 | } -------------------------------------------------------------------------------- /MakeExportable.sketchplugin/Contents/Sketch/script.js: -------------------------------------------------------------------------------- 1 | var kPluginDomain = "com.abynim.sketchplugins.exportable"; 2 | var kSavedPresetsKey = "com.abynim.sketchplugins.savedPresets"; 3 | 4 | var iconImage; 5 | 6 | var saveAsPreset = function(context) { 7 | 8 | parseContext(context); 9 | 10 | var layer = context.selection.firstObject(); 11 | if (!layer) { 12 | showAlert("No Selection", "To save Export Presets, select a layer that has all the export options you wish to include in the preset, then run the plugin again."); 13 | return; 14 | } 15 | 16 | var exportOptions = layer.exportOptions().exportFormats(); 17 | if (exportOptions.count() == 0) { 18 | showAlert("No Export Options Defined", "To save Export Presets, select a layer that has all the export options you wish to include in the preset, then run the plugin again."); 19 | return; 20 | } 21 | 22 | var settingsWindow = getAlertWindow(); 23 | settingsWindow.addButtonWithTitle("Save Preset"); 24 | settingsWindow.addButtonWithTitle("Cancel"); 25 | 26 | settingsWindow.setMessageText(context.command.name()); 27 | settingsWindow.setInformativeText("Save the export options from the selected layer as a preset for future use."); 28 | 29 | settingsWindow.addTextLabelWithValue("Preset Name:"); 30 | var presetNameField = NSTextField.alloc().initWithFrame(NSMakeRect(0,0,300,23)); 31 | presetNameField.setPlaceholderString("For iOS, For Android, etc.") 32 | settingsWindow.addAccessoryView(presetNameField); 33 | 34 | settingsWindow.addTextLabelWithValue("Shortcut:"); 35 | var shortcutField = NSTextField.alloc().initWithFrame(NSMakeRect(0,0,300,23)); 36 | shortcutField.setPlaceholderString("Ex: cmd shift y"); 37 | settingsWindow.addAccessoryView(shortcutField); 38 | 39 | presetNameField.setNextKeyView(shortcutField); 40 | settingsWindow.alert().window().setInitialFirstResponder(presetNameField); 41 | 42 | var response = settingsWindow.runModal(); 43 | if (response == "1000") { 44 | 45 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(), 46 | manifest = getJSON(manifestPath, true), 47 | commands = manifest.commands, 48 | commandsCount = commands.count(), 49 | presetName = presetNameField.stringValue(), 50 | presetShortcut = shortcutField.stringValue(), 51 | presetID = NSUUID.UUID().UUIDString(), 52 | presetExists = false, 53 | absoluteSize = layer.absoluteInfluenceRect().size, 54 | existingPresetIndex, existingPresetID, 55 | command; 56 | 57 | for (var i = 0; i < commandsCount; i++) { 58 | command = commands[i]; 59 | if (command.name == presetName) { 60 | presetExists = true; 61 | existingPresetIndex = i; 62 | existingPresetID = command.identifier; 63 | break; 64 | } 65 | } 66 | 67 | if (presetExists) { 68 | var overwriteResponse = showAlert("A preset named '" + presetName + "' already exists.", "Would you like to overwrite it?", "Cancel", "Overwrite"); 69 | if (overwriteResponse != "1001") { 70 | return; 71 | } 72 | 73 | manifest.commands.splice(existingPresetIndex, 1); 74 | manifest.menu.items.removeObject(existingPresetID); 75 | } 76 | 77 | var newCommand = { 78 | script : "script.js", 79 | handler : "applyPreset", 80 | name : presetName, 81 | identifier : presetID, 82 | shortcut : presetShortcut 83 | } 84 | manifest.commands.push(newCommand); 85 | manifest.menu.items.splice(-4, 0, presetID); 86 | 87 | var savedConfig = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey), 88 | optionsCount = exportOptions.count(), 89 | config = { presets : {} }, 90 | newPreset = { presetName : presetName, shortcut : presetShortcut, exportFormats : [] }, 91 | formatObj, formatOption, visualScaleType, actualSize, actualScale; 92 | 93 | if (savedConfig) { 94 | for (var pID in savedConfig.presets) { 95 | config.presets[pID] = savedConfig.presets[pID]; 96 | } 97 | } 98 | 99 | if (presetExists) { 100 | delete config.presets[existingPresetID]; 101 | } 102 | 103 | for (var i = 0; i < optionsCount; i++) { 104 | formatOption = exportOptions.objectAtIndex(i); 105 | visualScaleType = formatOption.visibleScaleType(); 106 | actualScale = formatOption.scale(); 107 | actualSize = visualScaleType == 1 ? Math.round(absoluteSize.width*actualScale) : visualScaleType == 2 ? Math.round(absoluteSize.height*actualScale) : 1; 108 | formatObj = { 109 | scale : actualScale, 110 | size : actualSize, 111 | name : formatOption.name(), 112 | fileFormat : formatOption.fileFormat(), 113 | visibleScaleType : visualScaleType 114 | } 115 | newPreset.exportFormats.push(formatObj); 116 | } 117 | config.presets[presetID] = newPreset; 118 | 119 | NSUserDefaults.standardUserDefaults().setObject_forKey(config, kSavedPresetsKey); 120 | saveJSON(manifest, manifestPath); 121 | 122 | AppController.sharedInstance().pluginManager().reloadPlugins(); 123 | context.document.showMessage(presetName + " : Preset Saved."); 124 | 125 | } 126 | } 127 | 128 | var restorePresets = function(context) { 129 | parseContext(context); 130 | var savedConfig = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey); 131 | 132 | if (!savedConfig) { 133 | showAlert("No presets found.", "Use this command only after you've updated to a new version of the plugin and your saved presets no longer show up."); 134 | return; 135 | } 136 | 137 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(), 138 | manifest = getJSON(manifestPath, true), 139 | commands = manifest.commands, 140 | presets = savedConfig.presets, 141 | numPresets = 0, 142 | preset, newCommand; 143 | 144 | for (var pID in presets) { 145 | numPresets++; 146 | if (manifest.menu.items.containsObject(pID)) { 147 | continue; 148 | } 149 | preset = presets[pID]; 150 | newCommand = { 151 | script : "script.js", 152 | handler : "applyPreset", 153 | name : preset.presetName, 154 | identifier : pID, 155 | shortcut : preset.shortcut 156 | } 157 | manifest.commands.push(newCommand); 158 | manifest.menu.items.splice(-4, 0, pID); 159 | } 160 | saveJSON(manifest, manifestPath); 161 | 162 | AppController.sharedInstance().pluginManager().reloadPlugins(); 163 | context.document.showMessage(numPresets + " Preset" + (numPresets == 1 ? "" : "s") + " Restored"); 164 | } 165 | 166 | var deletePresets = function(context) { 167 | 168 | parseContext(context); 169 | 170 | var savedConfig = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey); 171 | 172 | if (!savedConfig || savedConfig.presets.count() == 0) { 173 | showAlert("No presets found.", "There are no presets to delete."); 174 | return; 175 | } 176 | 177 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(), 178 | manifest = getJSON(manifestPath, true), 179 | predicate = NSPredicate.predicateWithFormat("handler == 'applyPreset'"), 180 | commands = manifest.commands.filteredArrayUsingPredicate(predicate), 181 | commandsCount = commands.count(), 182 | settingsWindow = getAlertWindow(), 183 | command, checkbox; 184 | 185 | settingsWindow.addButtonWithTitle("Delete"); 186 | settingsWindow.addButtonWithTitle("Cancel"); 187 | 188 | settingsWindow.setMessageText(context.command.name()); 189 | settingsWindow.setInformativeText("Select the presets you wish to delete. This cannot be undone, so please be sure."); 190 | 191 | for (var i = 0; i < commandsCount; i++) { 192 | command = commands[i]; 193 | checkbox = NSButton.alloc().initWithFrame(NSMakeRect(0,0,300, 23)); 194 | checkbox.setState(NSOffState); 195 | checkbox.setButtonType(NSSwitchButton); 196 | checkbox.setBezelStyle(0); 197 | checkbox.setTitle(command.name); 198 | settingsWindow.addAccessoryView(checkbox); 199 | } 200 | 201 | var response = settingsWindow.runModal(); 202 | if (response == "1000") { 203 | 204 | var config = { presets : {} }, 205 | numDeleted = 0, 206 | commandIndex; 207 | 208 | if (savedConfig) { 209 | for (var pID in savedConfig.presets) { 210 | config.presets[pID] = savedConfig.presets[pID]; 211 | } 212 | } 213 | 214 | for (var i = 0; i < commandsCount; i++) { 215 | checkbox = settingsWindow.viewAtIndex(i); 216 | if (checkbox.state() == NSOnState) { 217 | command = commands[i]; 218 | commandIndex = manifest.commands.indexOfObject(command); 219 | manifest.commands.splice(commandIndex, 1); 220 | manifest.menu.items.removeObject(command.identifier); 221 | 222 | delete config.presets[command.identifier]; 223 | numDeleted++; 224 | } 225 | } 226 | 227 | NSUserDefaults.standardUserDefaults().setObject_forKey(config, kSavedPresetsKey); 228 | saveJSON(manifest, manifestPath); 229 | 230 | AppController.sharedInstance().pluginManager().reloadPlugins(); 231 | context.document.showMessage(numDeleted + " Preset" + (numDeleted == 1 ? "" : "s") + " Deleted."); 232 | } 233 | } 234 | 235 | var applyPreset = function(context) { 236 | 237 | parseContext(context); 238 | 239 | var selection = context.selection; 240 | if (selection.count() == 0) { 241 | showAlert("No Selection", "Select one or more layers to make exportable."); 242 | return; 243 | } 244 | 245 | var loop = selection.objectEnumerator(), 246 | presets = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey).presets, 247 | presetID = context.command.identifier(), 248 | selectedPreset = presets[presetID].exportFormats, 249 | presetOptionsCount = selectedPreset.count(), 250 | formatOptions, 251 | formatOption, presetOption, exportOptions, layer, absoluteSize, actualScale; 252 | 253 | 254 | context.document.currentPage().deselectAllLayers(); 255 | 256 | while (layer = loop.nextObject()) { 257 | exportOptions = layer.exportOptions(); 258 | exportOptions.removeAllExportFormats(); 259 | 260 | absoluteSize = layer.absoluteInfluenceRect().size; 261 | formatOptions = []; 262 | for (var i = 0; i < presetOptionsCount; i++) { 263 | presetOption = selectedPreset[i]; 264 | 265 | actualScale = presetOption.visibleScaleType == 1 ? presetOption.size/absoluteSize.width : presetOption.visibleScaleType == 2 ? presetOption.size/absoluteSize.height : presetOption.scale; 266 | formatOption = MSExportFormat.formatWithScale_name_fileFormat(actualScale, presetOption.name, presetOption.fileFormat); 267 | 268 | formatOption.setVisibleScaleType(presetOption.visibleScaleType); 269 | formatOptions.push(formatOption); 270 | } 271 | 272 | exportOptions.addExportFormats(formatOptions); 273 | layer.select_byExpandingSelection(true, true); 274 | } 275 | } 276 | 277 | var importPresets = function(context) { 278 | 279 | parseContext(context); 280 | 281 | var openPanel = NSOpenPanel.openPanel(); 282 | openPanel.setCanChooseDirectories(false); 283 | openPanel.setAllowsMultipleSelection(true); 284 | openPanel.setMessage("Select a .sketchexportpreset file."); 285 | openPanel.setAllowedFileTypes(["sketchexportpreset"]); 286 | 287 | var response = openPanel.runModal(); 288 | if (response == 1) { 289 | 290 | var URLs = openPanel.URLs(), 291 | URLsCount = URLs.count(), 292 | savedConfig = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey), 293 | manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(), 294 | manifest = getJSON(manifestPath, true), 295 | commands = manifest.commands, 296 | commandsCount = commands.count(), 297 | existingPresetNames = [], 298 | presetsToOverwrite = [], 299 | presetsCount = 0, 300 | config = { presets : {} }, 301 | importedPresets, command, newCommand; 302 | 303 | if (savedConfig) { 304 | for (var pID in savedConfig.presets) { 305 | config.presets[pID] = savedConfig.presets[pID]; 306 | } 307 | } 308 | 309 | for (var i = 0; i < commandsCount; i++) { 310 | command = commands[i]; 311 | existingPresetNames.push(command.name); 312 | } 313 | 314 | for (var i = 0; i < URLsCount; i++) { 315 | importedPresets = getJSON(URLs[i].path(), false).presets; 316 | 317 | for (var pID in importedPresets) { 318 | if (manifest.menu.items.containsObject(pID)) { 319 | continue; 320 | } 321 | preset = importedPresets[pID]; 322 | if (existingPresetNames.indexOf(preset.presetName) != -1) { 323 | presetsToOverwrite.push(preset); 324 | continue; 325 | } 326 | 327 | newCommand = { 328 | script : "script.js", 329 | handler : "applyPreset", 330 | name : preset.presetName, 331 | identifier : pID, 332 | shortcut : preset.shortcut 333 | } 334 | 335 | config.presets[pID] = preset; 336 | 337 | manifest.commands.push(newCommand); 338 | manifest.menu.items.splice(-4, 0, pID); 339 | 340 | presetsCount++; 341 | } 342 | } 343 | 344 | var overwriteCount = presetsToOverwrite.length; 345 | if (overwriteCount) { 346 | var overwriteResponse = showAlert(overwriteCount + " presets already exists.", "Would you like to overwrite them?", "Skip", "Overwrite"); 347 | if (overwriteResponse == "1000") { 348 | 349 | for (var i = 0; i < overwriteCount; i++) { 350 | preset = presetsToOverwrite[i]; 351 | 352 | newCommand = { 353 | script : "script.js", 354 | handler : "applyPreset", 355 | name : preset.presetName, 356 | identifier : pID, 357 | shortcut : preset.shortcut 358 | } 359 | 360 | config.presets[pID] = preset; 361 | 362 | manifest.commands.push(newCommand); 363 | manifest.menu.items.splice(-4, 0, pID); 364 | 365 | presetsCount++; 366 | } 367 | 368 | } 369 | } 370 | 371 | NSUserDefaults.standardUserDefaults().setObject_forKey(config, kSavedPresetsKey); 372 | saveJSON(manifest, manifestPath); 373 | 374 | AppController.sharedInstance().pluginManager().reloadPlugins(); 375 | context.document.showMessage(presetsCount + " Preset" + (presetsCount == 1 ? "" : "s") + " Imported."); 376 | 377 | } 378 | } 379 | 380 | var exportPresets = function(context) { 381 | 382 | parseContext(context); 383 | 384 | var savedConfig = NSUserDefaults.standardUserDefaults().objectForKey(kSavedPresetsKey); 385 | 386 | if (!savedConfig || savedConfig.presets.count() == 0) { 387 | showAlert("No presets found.", "Define your presets first, then export them to share with your team or to import them on another Mac."); 388 | return; 389 | } 390 | 391 | var savePanel = NSSavePanel.savePanel(); 392 | savePanel.setExtensionHidden(false); 393 | savePanel.setAllowedFileTypes(["sketchexportpreset"]); 394 | savePanel.setNameFieldStringValue("Export Format Presets"); 395 | 396 | var response = savePanel.runModal(); 397 | if (response == 1) { 398 | var filePath = savePanel.URL().path(); 399 | saveJSON(savedConfig, filePath); 400 | 401 | NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath(filePath, filePath.stringByDeletingLastPathComponent()); 402 | } 403 | } 404 | 405 | var clearFormats = function(context) { 406 | 407 | var selection = context.selection, 408 | loop = selection.objectEnumerator(), 409 | layer; 410 | 411 | context.document.currentPage().deselectAllLayers(); 412 | 413 | while (layer = loop.nextObject()) { 414 | layer.exportOptions().removeAllExportFormats(); 415 | layer.select_byExpandingSelection(true, true); 416 | } 417 | } 418 | 419 | var showAlert = function(message, info, primaryButtonText, secondaryButtonText) { 420 | var alert = getAlertWindow(); 421 | alert.setMessageText(message); 422 | alert.setInformativeText(info); 423 | if (typeof primaryButtonText !== 'undefined') { 424 | alert.addButtonWithTitle(primaryButtonText); 425 | } 426 | if (typeof secondaryButtonText !== 'undefined') { 427 | alert.addButtonWithTitle(secondaryButtonText); 428 | } 429 | return alert.runModal(); 430 | } 431 | 432 | var getJSON = function(filePath, mutable) { 433 | var data = NSData.dataWithContentsOfFile(filePath), 434 | options = mutable == true ? NSJSONReadingMutableContainers : 0; 435 | return NSJSONSerialization.JSONObjectWithData_options_error(data, options, nil); 436 | } 437 | 438 | var saveJSON = function(obj, filePath) { 439 | var data = NSJSONSerialization.dataWithJSONObject_options_error(obj, NSJSONWritingPrettyPrinted, nil), 440 | dataAsString = NSString.alloc().initWithData_encoding(data, NSUTF8StringEncoding); 441 | return dataAsString.writeToFile_atomically_encoding_error(filePath, true, NSUTF8StringEncoding, nil); 442 | } 443 | 444 | var parseContext = function(context) { 445 | iconImage = NSImage.alloc().initByReferencingFile(context.plugin.urlForResourceNamed("icon.png").path()); 446 | } 447 | 448 | var getAlertWindow = function() { 449 | var alert = COSAlertWindow.new(); 450 | if (iconImage) { 451 | alert.setIcon(iconImage); 452 | } 453 | return alert; 454 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Make Exportable 2 | Export option presets for Sketch layers. 3 | Icon 4 | 5 | ##Install the plugin 6 | [Download](https://github.com/abynim/sketch-exportable/archive/master.zip) and extract the contents of this repository. Then double-click the `MakeExportable.sketchplugin` bundle to install the plugin. 7 | 8 | --- 9 | 10 | ##Usage 11 | When you first install the plugin it will contain no presets. You must set it up with the presets that you need, based on the scale you design at and the devices for which you wish to export assets. 12 | 13 | ###Create a New Preset 14 | Select a layer and manually make it exportable in the Inspector. Add all the formats, sizes and suffixes you need. 15 | 16 | New Preset Formats 17 | 18 | With the same layer selected, run the `Make Exportable` > `Configure` > `New Preset` plugin command. 19 | 20 | New Preset Menu 21 | 22 | Give the preset a name. Optionally also set a shortcut. Shortcuts are defined by using a combination of modifiers ( `cmd`, `control`, `shift`, `option`) and any other key. For example, `cmd shift y`. Remember to check if a shortcut is already being used by a different plugin or by Sketch itself. 23 | 24 | New Preset Options 25 | 26 | When you save the preset, you will see it as a menu item in the plugins menu. Next time you need to add these export settings to a layer, just select it and trigger the command. 27 | 28 | New Preset Defined 29 | 30 | 31 | ###Other Configuration Options 32 | 33 | Other Config Options 34 | 35 | 1. **Import and Export** - To transfer your presets to another Mac or to share them with your team, you can `Export` them to a file. Then `Import` the file on the destination Mac. Easy-peasy. 36 | 2. **Restore Presets** - When you update the plugin with future versions, your presets will be removed from the plugin menu. Not to worry, they are saved independently. Run a `Restore` to see them in the plugins menu again. 37 | 3. **Delete Presets** - This lets you delete an existing preset from the menu. Duh! 38 | 39 | ###Remove Formats for Selection 40 | Run this command to remove all export formats from one or more selected layers. 41 | 42 | --- 43 | 44 | ## Share 45 | If this plugin saved you a few minutes of mundane work, do spend a second to Tweet about it or share it on Facebook. 46 | --------------------------------------------------------------------------------