├── LICENCE ├── README.md ├── User Flow Docs.sketchplugin └── Contents │ └── Sketch │ ├── export-for-user-flows.js │ ├── libs │ ├── constants.js │ ├── sandbox.js │ └── utils.js │ ├── manifest.json │ ├── populate-user-flows.js │ └── toggle-user-flow-metadata.js └── sketch.h /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2014 ribot 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketch User Flows 2 | This plugin exports every artboard in your design document and allows you to create a user flows document for clients by populating a template. 3 | 4 | - Export for User Flows (⌃⇧E) 5 | - Populate User Flows (⌃⇧P) 6 | 7 | # Installation 8 | 1. Download the repository using this [link](https://github.com/ribot/sketch-user-flow-docs/archive/master.zip) 9 | 2. Unzip the files from the ZIP 10 | 3. In Sketch 3, select `Plugins > Reveal Plugins Folder...` from the menu bar 11 | 4. Put the (downloaded, unzipped) folder in here 12 | 13 | # Setup 14 | In your visual design document... 15 | * **Pages** should be given a number and title separated by a space e.g.: 16 | * 01 Welcome 17 | * 02 Signin 18 | * **Artboards** should have a number and title separated by a space e.g.: 19 | * 02.1 Sign in 20 | * 02.2 Sign in success 21 | * 02.3 Sign in failure 22 | * To hide a page or artboard, start the name with an underscore (e.g.): 23 | * _01 Welcome 24 | * _02.1 Sign in 25 | * Add a text layer named `User Flow Description` to each artboard (screen) where you want a description in the user flow doc (you can hide this text layer if you like) 26 | 27 | # Usage 28 | 2. Run the **Export for User Flows** plugin `(⌃⇧E)` 29 | 2. Fill in the project name and version number 30 | 3. Create a new User Flow doc by going `File > New From Template > Ribot Visual Doc iPhone 6` 31 | 4. Run the **Populate User Flows** plugin `(⌃⇧P)` 32 | 5. Save your User Flow doc 33 | 34 | # Compatibility 35 | Tested on Sketch 3.3 and 3.4 36 | 37 | # Licence 38 | ``` 39 | Copyright 2014 ribot 40 | 41 | Licensed under the Apache License, Version 2.0 (the "License"); 42 | you may not use this file except in compliance with the License. 43 | You may obtain a copy of the License at 44 | 45 | http://www.apache.org/licenses/LICENSE-2.0 46 | 47 | Unless required by applicable law or agreed to in writing, software 48 | distributed under the License is distributed on an "AS IS" BASIS, 49 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 50 | See the License for the specific language governing permissions and 51 | limitations under the License. 52 | ``` 53 | -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/export-for-user-flows.js: -------------------------------------------------------------------------------- 1 | @import 'libs/sandbox.js'; 2 | @import 'libs/constants.js'; 3 | @import 'libs/utils.js'; 4 | 5 | // Globals 6 | var USER_FLOW_METADATA_LAYER_NAMEKEY = new RegExp(USER_FLOW_METADATA_LAYER_NAME); 7 | var doc; 8 | 9 | // Sandboxing 10 | function exportForUserFlows(context) { 11 | doc = context.document; 12 | 13 | var homeFolder = "/Users/" + NSUserName(); 14 | new AppSandbox().authorize(homeFolder, doExport); 15 | } 16 | 17 | // onRun handler 18 | function doExport() { 19 | // Set global 20 | if (doc.fileURL() == null) { 21 | doc.showMessage("Please save your document first."); 22 | return; 23 | } 24 | 25 | // Ask the user where they want to export to 26 | var openPanel = NSOpenPanel.openPanel() 27 | openPanel.setCanChooseDirectories(true) 28 | openPanel.setCanChooseFiles(false) 29 | openPanel.setCanCreateDirectories(true) 30 | openPanel.setTitle("Choose an folder to export to...") 31 | openPanel.setPrompt("Export") 32 | 33 | if (openPanel.runModal() == NSOKButton) { 34 | // Get the url of the folder the user selected 35 | var exportBaseUrl = openPanel.URL() 36 | 37 | // Create an object to store the JSON data in 38 | var jsonObject = {} 39 | var sectionsJson = {} 40 | 41 | // Loop through all the pages 42 | var pageLoop = doc.pages().objectEnumerator() 43 | while (page = pageLoop.nextObject()) { 44 | 45 | // Create an object to store the descriptions for each artboard in this page, using the page name as the key 46 | var section = parseName(page.name()); 47 | if (!section) { 48 | return; 49 | } 50 | 51 | section.screens = {}; 52 | 53 | // Switch to this page to avoid errors when exporting 54 | doc.setCurrentPage(page) 55 | 56 | // Hide screen descriptions 57 | hideScreenDescriptions(doc.currentPage().layers().array()) 58 | 59 | // Setup the URL for the page 60 | var pageDirectoryUrl = [exportBaseUrl URLByAppendingPathComponent:page.name() isDirectory:true] 61 | 62 | // Create the directory for this page to export to 63 | [[NSFileManager defaultManager] createDirectoryAtURL:pageDirectoryUrl withIntermediateDirectories:false attributes:null error:null] 64 | 65 | // Export each of this pages artboards to the page directory 66 | var artboardLoop = page.artboards().objectEnumerator() 67 | while (artboard = artboardLoop.nextObject()) { 68 | // Check that it's an artboard and not a slice 69 | if (artboard.class() == MSArtboardGroup.class()) { 70 | // Export the artboard to the correct file location 71 | var artboardPath = pageDirectoryUrl.URLByAppendingPathComponent(artboard.name() + FILE_EXTENSION).path() 72 | 73 | var sizes = artboard.exportOptions().sizes().array() 74 | var slices = _getSlices(artboard, sizes) 75 | while(slice = slices.nextObject()) { 76 | doc.saveArtboardOrSlice_toFile(slice, artboardPath); 77 | } 78 | 79 | var screen = getArtboardData(artboard); 80 | if (!screen) { 81 | return; 82 | } 83 | 84 | // Put the artboard description in the descriptions object 85 | section.screens[ artboard.name() ] = screen; 86 | } 87 | } 88 | 89 | // Add section to JSON 90 | sectionsJson[ page.name() ] = section; 91 | } 92 | 93 | // Add the sections 94 | jsonObject[ SECTIONS_JSON_KEY ] = sectionsJson; 95 | 96 | // Ask the user for the name of the project 97 | var projectName = [doc askForUserInput:"What is this project called?" initialValue:guessProjectName()] 98 | jsonObject[PROJECT_NAME_JSON_KEY] = new String(projectName) 99 | 100 | // Ask the user for the verson of the document 101 | var documentVersion = [doc askForUserInput:"What version of the document is this?" initialValue:guessDocumentVersion()] 102 | jsonObject[DOCUMENT_VERSION_JSON_KEY] = new String(documentVersion) 103 | 104 | // Convert the descriptions object to JSON 105 | var metadataFilePath = exportBaseUrl.URLByAppendingPathComponent(METADATA_FILE_NAME).path() 106 | var jsonString = [NSString stringWithFormat:"%@", JSON.stringify(jsonObject)]; 107 | print(jsonString); 108 | 109 | [jsonString writeToFile:metadataFilePath atomically:true encoding:NSUTF8StringEncoding error:nil]; 110 | 111 | doc.showMessage("All done!"); 112 | } 113 | } 114 | 115 | function guessProjectName() { 116 | return getFilenameWithoutExtension().componentsSeparatedByString("_").firstObject() 117 | } 118 | 119 | function guessDocumentVersion() { 120 | return "Version " + getFilenameWithoutExtension().componentsSeparatedByString("_").lastObject() 121 | } 122 | 123 | function getFilenameWithoutExtension() { 124 | var filename = doc.fileURL().lastPathComponent() 125 | return [filename stringByReplacingOccurrencesOfString:".sketch" withString:""] 126 | } 127 | 128 | function hideScreenDescriptions(layers) { 129 | processAllLayers(layers, function(layer) { 130 | if (USER_FLOW_METADATA_LAYER_NAMEKEY.test(layer.name())) { 131 | layer.setIsVisible(false) 132 | } 133 | }) 134 | } 135 | 136 | function _getSlices( artboard, sizes ) { 137 | if ( MSSliceMaker.slicesFromExportableLayer_sizes ) { 138 | return MSSliceMaker.slicesFromExportableLayer_sizes(artboard, sizes).objectEnumerator() 139 | } 140 | return MSSliceMaker.slicesFromExportableLayer_sizes_useIDForName(artboard, sizes, false).objectEnumerator() 141 | } 142 | 143 | function getArtboardData(artboard) { 144 | // Get the user flow description text layer, if there is one 145 | var artboardData = parseName(artboard.name()); 146 | if (!artboardData) { 147 | return; 148 | } 149 | 150 | // Get any metadata 151 | var possibleMetadataLayer = getChildLayerByName(artboard, USER_FLOW_METADATA_LAYER_NAME) 152 | if (possibleMetadataLayer) { 153 | 154 | // Get description 155 | var possibleDescriptionLayer = getChildLayerByName(possibleMetadataLayer, USER_FLOW_DESCRIPTION_LAYER_NAME) 156 | if (possibleDescriptionLayer) { 157 | artboardData.description = new String(possibleDescriptionLayer.stringValue()) 158 | } 159 | 160 | } 161 | 162 | var size = artboard.absoluteRect().rect().size; 163 | artboardData.size = { 164 | width: size.width * 1, 165 | height: size.height * 1 166 | }; 167 | 168 | return artboardData; 169 | } 170 | 171 | function parseName(name) { 172 | var original = new String(name); 173 | var nameParts = name.split(" "); 174 | if (nameParts.length === 1) { 175 | doc.showMessage('"' + name + '" is not formatted correctly. Please use a name such as "01 Home"'); 176 | return; 177 | } 178 | 179 | var tag = nameParts[ 0 ]; 180 | var displayTag = tag.replace(/\_/g, '.'); 181 | var status = null; 182 | 183 | var title = name.substringFromIndex(tag.length + 1); 184 | var displayTitle = title.replace(/\_/g, ' '); 185 | 186 | var data = { 187 | tag: displayTag, 188 | title: displayTitle, 189 | original: original, 190 | }; 191 | 192 | var statusMatch = /\[([\w\s]+)\]/.exec(title); 193 | if (statusMatch) { 194 | data.status = statusMatch[ 1 ].toUpperCase(); 195 | data.title = displayTitle.split('[')[ 0 ]; 196 | } 197 | 198 | var firstCharacter = tag.charAt(0); 199 | if (firstCharacter == "_" || firstCharacter == "-") { 200 | data.exclude = true; 201 | } 202 | 203 | return data; 204 | } 205 | -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/libs/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all the constants that we use for the plugin(s). 3 | **/ 4 | 5 | /** 6 | * 'Export' constants 7 | */ 8 | // The name of the User Flow Description text layer in the designs document 9 | var USER_FLOW_METADATA_LAYER_NAME = "#metadata"; 10 | // The name of the User Flow Description text layer in the designs document 11 | var USER_FLOW_DESCRIPTION_LAYER_NAME = "#description"; 12 | 13 | /** 14 | * 'Populate' constants 15 | */ 16 | // The name of the page in the template document to duplicate and populate 17 | var TEMPLATE_PAGE_NAME = "#template-page"; 18 | // The name of the header layer in the template document 19 | var HEADER_LAYER_NAME = "#template-header"; 20 | // The name of the project name text layer in the template header layer 21 | var PROJECT_NAME_LAYER_NAME = "#project-name"; 22 | // The name of the section name text layer in the template header layer 23 | var SECTION_NAME_LAYER_NAME = "#section-name"; 24 | // The name of the screens layer group in the template document 25 | var SCREENS_LAYER_GROUP_NAME = "#screens"; 26 | // The name of the screen number text layer in the screen layer group 27 | var SCREEN_NUMBER_LAYER_NAME = "#screen-number"; 28 | // The name of the screen heading text layer in the screen layer group 29 | var SCREEN_HEADING_LAYER_NAME = "#screen-title"; 30 | // The name of the screen description text layer in the screen layer group 31 | var SCREEN_DESCRIPTION_LAYER_NAME = "#screen-description"; 32 | // The name of the screen description text layer in the screen layer group 33 | var SCREEN_STATUS_LAYER_NAME = "#screen-status"; 34 | // The name of the screen placeholder image layer in the screen layer group 35 | var SCREEN_PLACEHOLDER_LAYER_NAME = "#screen-placeholder"; 36 | // The name of the screen placeholder image layer in the screen layer group 37 | var SCREEN_MASK_LAYER_NAME = "#screen-mask"; 38 | // The name of the cover page in the template document 39 | var COVER_PAGE_NAME = "#cover-page"; 40 | // The name of the header layer group on the cover page 41 | var COVER_HEADER_LAYER_GROUP_NAME = "#cover-header"; 42 | // The name of the project name layer on the cover page 43 | var COVER_PROJECT_NAME_LAYER_NAME = "#cover-client"; 44 | // The name of the document version layer on the cover page 45 | var COVER_DOCUMENT_VERSION_LAYER_NAME = "#cover-version"; 46 | // The name of the document title layer on the cover page 47 | var COVER_DOCUMENT_TITLE_LAYER_NAME = "#cover-title"; 48 | 49 | // The file extension to export the images with 50 | var FILE_EXTENSION = ".png"; 51 | // The filename to use for the descriptions file 52 | var METADATA_FILE_NAME = "metadata.json"; 53 | // The key used to reference the sections in the JSON file 54 | var SECTIONS_JSON_KEY = "sections"; 55 | // The key used to reference the screens in the JSON file 56 | var SCREENS_JSON_KEY = "screens"; 57 | // The key used to reference the project name in the JSON file 58 | var PROJECT_NAME_JSON_KEY = "projectName"; 59 | // The key used to reference the document version in the JSON file 60 | var DOCUMENT_VERSION_JSON_KEY = "documentVersion"; 61 | -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/libs/sandbox.js: -------------------------------------------------------------------------------- 1 | var environ = [[NSProcessInfo processInfo] environment], 2 | in_sandbox= (nil != [environ objectForKey:@"APP_SANDBOX_CONTAINER_ID"]) 3 | 4 | if(in_sandbox){ 5 | print("We’re sandboxed: here be dragons") 6 | } 7 | 8 | AppSandbox = function(){ } 9 | AppSandbox.prototype.authorize = function(path, callback){ 10 | log("AppSandbox.authorize("+path+")") 11 | var success = false 12 | 13 | if (in_sandbox) { 14 | var url = [[[NSURL fileURLWithPath:path] URLByStandardizingPath] URLByResolvingSymlinksInPath], 15 | allowedUrl = false 16 | 17 | // Key for bookmark data: 18 | var bd_key = this.key_for_url(url) 19 | 20 | // this.clear_key(bd_key) // For debug only, this clears the key we're looking for :P 21 | 22 | // Bookmark 23 | var bookmark = this.get_data_for_key(bd_key) 24 | if(!bookmark){ 25 | log("– No bookmark found, let's create one") 26 | var target = this.file_picker(url) 27 | bookmark = [target bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope 28 | includingResourceValuesForKeys:nil 29 | relativeToURL:nil 30 | error:{}] 31 | // Store bookmark 32 | this.set_data_for_key(bookmark,bd_key) 33 | } else { 34 | log("– Bookmark found") 35 | } 36 | log(" " + bookmark) 37 | 38 | // Thanks to @joethephish for this pointer (pun totally intended) 39 | var bookmarkDataIsStalePtr = MOPointer.alloc().init() 40 | var allowedURL = [NSURL URLByResolvingBookmarkData:bookmark 41 | options:NSURLBookmarkResolutionWithSecurityScope 42 | relativeToURL:nil 43 | bookmarkDataIsStale:bookmarkDataIsStalePtr 44 | error:{}] 45 | 46 | if(bookmarkDataIsStalePtr.value() != 0){ 47 | log("— Bookmark data is stale") 48 | log(bookmarkDataIsStalePtr.value()) 49 | } 50 | 51 | if(allowedURL) { 52 | success = true 53 | } 54 | } else { 55 | success = true 56 | } 57 | 58 | // [allowedUrl startAccessingSecurityScopedResource] 59 | callback.call(this,success) 60 | // [allowedUrl stopAccessingSecurityScopedResource] 61 | } 62 | AppSandbox.prototype.key_for_url = function(url){ 63 | return "bd_" + [url absoluteString] 64 | } 65 | AppSandbox.prototype.clear_key = function(key){ 66 | var def = [NSUserDefaults standardUserDefaults] 67 | [def setObject:nil forKey:key] 68 | } 69 | AppSandbox.prototype.file_picker = function(url){ 70 | // Panel 71 | var openPanel = [NSOpenPanel openPanel] 72 | 73 | [openPanel setTitle:"Sketch Authorization"] 74 | [openPanel setMessage:"Due to Apple's Sandboxing technology, Sketch needs your permission to write to this folder."]; 75 | [openPanel setPrompt:"Authorize"]; 76 | 77 | [openPanel setCanCreateDirectories:false] 78 | [openPanel setCanChooseFiles:true] 79 | [openPanel setCanChooseDirectories:true] 80 | [openPanel setAllowsMultipleSelection:false] 81 | [openPanel setShowsHiddenFiles:false] 82 | [openPanel setExtensionHidden:false] 83 | 84 | [openPanel setDirectoryURL:url] 85 | 86 | var openPanelButtonPressed = [openPanel runModal] 87 | if (openPanelButtonPressed == NSFileHandlingPanelOKButton) { 88 | allowedUrl = [openPanel URL] 89 | } 90 | return allowedUrl 91 | } 92 | 93 | AppSandbox.prototype.get_data_for_key = function(key){ 94 | var def = [NSUserDefaults standardUserDefaults] 95 | return [def objectForKey:key] 96 | } 97 | AppSandbox.prototype.set_data_for_key = function(data,key){ 98 | var defaults = [NSUserDefaults standardUserDefaults], 99 | default_values = [NSMutableDictionary dictionary] 100 | 101 | [default_values setObject:data forKey:key] 102 | [defaults registerDefaults:default_values] 103 | } -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/libs/utils.js: -------------------------------------------------------------------------------- 1 | // Fetch a layer inside the given layer group which has the name 2 | function getChildLayerByName(group, name) { 3 | var layers = group.layers(); 4 | var layerLoop = layers.array().objectEnumerator(); 5 | while ( childLayer = layerLoop.nextObject() ) { 6 | if (childLayer.name() == name) { 7 | return childLayer; 8 | } 9 | } 10 | return null; 11 | } 12 | 13 | function getPageByName(pageName) { 14 | var matchingPages = getPagesByName( pageName ) 15 | if ( matchingPages.length ) { 16 | return matchingPages[ 0 ] 17 | } else { 18 | return false 19 | } 20 | } 21 | 22 | function getPagesByName(pageName) { 23 | var pageLoop = doc.pages().objectEnumerator() 24 | var matchingPages = [] 25 | 26 | while (page = pageLoop.nextObject()) { 27 | if (page.name() == pageName) { 28 | matchingPages.push( page ) 29 | } 30 | } 31 | 32 | return matchingPages 33 | } 34 | 35 | function sortLikeFinder( a, b ) { 36 | a = a.name.lastPathComponent() 37 | b = b.name.lastPathComponent() 38 | var comparison = [a localizedStandardCompare:b]; 39 | 40 | return comparison; 41 | } 42 | 43 | 44 | function getFirstArtboard() { 45 | return doc.currentPage().artboards().firstObject() 46 | } 47 | 48 | function setTextOnChildLayerByName(group, name, text) { 49 | var layer = getChildLayerByName( group, name ) 50 | if ( layer ) { 51 | layer.setStringValue( text ) 52 | layer.adjustFrameToFit() 53 | } 54 | } 55 | 56 | function processAllLayers(layers, callback) { 57 | var abs = layers.objectEnumerator(); 58 | while ( layer = abs.nextObject() ) { 59 | // if ([layer isMemberOfClass:[MSLayerGroup class]]) { 60 | if ( layer.isMemberOfClass( MSLayerGroup.class() ) || layer.isMemberOfClass( MSArtboardGroup.class() ) ) { 61 | callback( layer ); 62 | // Process child layers/groups 63 | processAllLayers( layer.layers().array(), callback ); 64 | } 65 | else { 66 | callback( layer ); 67 | } 68 | } 69 | } 70 | 71 | function isFolder( folder ) { 72 | return folder.pathExtension() != ""; 73 | } 74 | -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "ribot", 3 | "commands" : [ 4 | { 5 | "script" : "toggle-user-flow-metadata.js", 6 | "handler" : "toggleUserFlowMetadata", 7 | "shortcut" : "cmd ctrl m", 8 | "name" : "1. Toggle User Flow Metadata", 9 | "identifier" : "toggleUserFlowMetadata" 10 | }, 11 | { 12 | "script" : "export-for-user-flows.js", 13 | "handler" : "exportForUserFlows", 14 | "shortcut" : "cmd ctrl e", 15 | "name" : "2. Export for User Flows", 16 | "identifier" : "exportForUserFlows" 17 | }, 18 | { 19 | "script" : "populate-user-flows.js", 20 | "handler" : "populateUserFlows", 21 | "shortcut" : "cmd ctrl p", 22 | "name" : "3. Populate User Flows", 23 | "identifier" : "populateUserFlows" 24 | }, 25 | { 26 | "script" : "populate-responsive-user-flows.js", 27 | "handler" : "populateResponsiveUserFlows", 28 | "shortcut" : "cmd ctrl option p", 29 | "name" : "4. Populate Responsive User Flows", 30 | "identifier" : "populateResponsiveUserFlows" 31 | } 32 | ], 33 | "menu": { 34 | "items": [ 35 | "toggleUserFlowMetadata", 36 | "exportForUserFlows", 37 | "populateUserFlows", 38 | "populateResponsiveUserFlows" 39 | ] 40 | }, 41 | "identifier" : "uk.co.ribot.sketch.user-flow-docs", 42 | "version" : "1.1", 43 | "description" : "Example Sketch Plugin using resources from within the bundle and a library script.", 44 | "authorEmail" : "rob@ribot.co.uk", 45 | "name" : "User Flow Docs" 46 | } 47 | -------------------------------------------------------------------------------- /User Flow Docs.sketchplugin/Contents/Sketch/populate-user-flows.js: -------------------------------------------------------------------------------- 1 | // @import 'libs/sandbox.js'; 2 | @import 'libs/constants.js'; 3 | @import 'libs/utils.js'; 4 | 5 | // Globals 6 | var doc; 7 | 8 | // onRun handler 9 | function populateUserFlows(context) { 10 | doc = context.document; 11 | 12 | var path = getExportsFolder(); 13 | 14 | // Get metadata into a JSON object 15 | var metadata = getMetadata(path); 16 | 17 | // Get the project name from the metadata 18 | // Get the document version from the metadata 19 | // Get the descriptions from the metadata 20 | var projectName = metadata[PROJECT_NAME_JSON_KEY]; 21 | var documentVersion = metadata[DOCUMENT_VERSION_JSON_KEY]; 22 | var screenData = metadata[SECTIONS_JSON_KEY]; 23 | 24 | // Select first template page 25 | var templatePage = getPageByName(TEMPLATE_PAGE_NAME); 26 | 27 | // Get number of screens per page 28 | var numberOfScreensPerPage = getScreensPerPageFromTemplate(templatePage); 29 | 30 | // Populate cover 31 | populateCover(projectName, documentVersion); 32 | 33 | // Populate screens 34 | populateScreens(path, templatePage, numberOfScreensPerPage, projectName, screenData); 35 | 36 | // Remove template 37 | removeTemplatePages(); 38 | 39 | // Let it snow... 40 | finish(); 41 | } 42 | 43 | function getMetadata(path) { 44 | var metadataFilePath = path.URLByAppendingPathComponent(METADATA_FILE_NAME); 45 | var metadataString = [NSString stringWithContentsOfURL:metadataFilePath encoding:NSUTF8StringEncoding error:null]; 46 | return JSON.parse(metadataString); 47 | } 48 | 49 | // Ask user to select exports folder 50 | function getExportsFolder() { 51 | // Set up 'File open' modal 52 | var openPanel = NSOpenPanel.openPanel(); 53 | openPanel.setCanChooseDirectories(true); 54 | openPanel.setCanChooseFiles(false); 55 | openPanel.setCanCreateDirectories(false); 56 | 57 | openPanel.setTitle("Choose your exports folder"); 58 | openPanel.setPrompt("Choose"); 59 | 60 | // Handle folder chosen 61 | if (openPanel.runModal() == NSOKButton) { 62 | return openPanel.URL(); 63 | } 64 | } 65 | 66 | // Get screens per page from Template > Artboard > Screens group 67 | function getScreensPerPageFromTemplate(templatePage) { 68 | var artboard = templatePage.artboards().firstObject(); 69 | 70 | // Get Screens group 71 | var screens = getChildLayerByName(artboard, SCREENS_LAYER_GROUP_NAME); 72 | 73 | // Get number of children in Screens group 74 | return screens.layers().count(); 75 | } 76 | 77 | 78 | // Get the 'Screens' group 79 | function getTemplateScreens() { 80 | return getChildLayerByName(getFirstArtboard(), SCREENS_LAYER_GROUP_NAME); 81 | } 82 | 83 | // Populate screens 84 | function populateScreens(path, templatePage, numberOfScreensPerPage, projectName, screenData) { 85 | var fileManager = NSFileManager.defaultManager(); 86 | var folders = fileManager.shallowSubpathsOfDirectoryAtURL(path); 87 | 88 | // Loop through folders 89 | var folderLoop = folders.objectEnumerator(); 90 | while (folder = folderLoop.nextObject()) { 91 | var key = folder.lastPathComponent(); 92 | var folderData = screenData[ key ]; 93 | 94 | // Check if it's a folder and if shouldIgnoreItem should be ignored 95 | if(isFolder(folder) || shouldIgnoreItem(folderData)) { 96 | return; 97 | } 98 | 99 | // Duplicate Templates page to start 100 | var pageCount = 0; 101 | var page = duplicateTemplatePage(templatePage); 102 | 103 | // Get folder name (for page title) 104 | var sectionDisplayName = folderData.original; 105 | 106 | // Get files for current folder 107 | var files = fileManager.shallowSubpathsOfDirectoryAtURL(folder); 108 | var numberOfFilesInFolder = files.count(); 109 | var screensInSectionCount = 0; 110 | var screenCount = 0; 111 | 112 | // Sort files like finder 113 | var sortedFiles = [] 114 | for (var i=0; i