├── .appcast.xml ├── LICENSE ├── README.md └── prott.sketchplugin └── Contents ├── Resources ├── logo.png └── logo@2x.png └── Sketch ├── core.cocoascript ├── logout.cocoascript ├── manifest.json ├── sync-all-artboards.cocoascript └── sync-selected-artboards.cocoascript /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sketch Plugin 5 | https://raw.githubusercontent.com/goodpatch/Prott-Sketch-Plugin/tree/master/prott.sketchplugin/Contents/Sketch/appcast.xml 6 | Sync Sketch artboards to Prott like a boss. 7 | en 8 | 9 | Version 2.0.9 10 | 11 | 13 |
  • Fix appcast.xml path
  • 14 | 15 | ]]> 16 |
    17 | 18 |
    19 | 20 | Version 2.0.8 21 | 22 | 24 |
  • Update the link for plugin documentation
  • 25 | 26 | ]]> 27 |
    28 | 29 |
    30 | 31 | Version 2.0.7 32 | 33 | 35 |
  • Support plugin update system
  • 36 | 37 | ]]> 38 |
    39 | 40 |
    41 | 42 | Version 2.0.6 43 | 44 | 46 |
  • We fixed some bugs.
  • 47 | 48 | ]]> 49 |
    50 | 51 |
    52 |
    53 |
    -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Goodpatch Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prott-sketch-plugin 2 | 3 | Sync Sketch artboards to Prott like a boss. 4 | 5 | [Details here.](https://docs.prottapp.com/article/156-how-to-use-sketch-plugin) 6 | -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodpatch/Prott-Sketch-Plugin/adcb1314887d4f2205aea58c8138851c9792ab4e/prott.sketchplugin/Contents/Resources/logo.png -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Resources/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodpatch/Prott-Sketch-Plugin/adcb1314887d4f2205aea58c8138851c9792ab4e/prott.sketchplugin/Contents/Resources/logo@2x.png -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Sketch/core.cocoascript: -------------------------------------------------------------------------------- 1 | /*The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Goodpatch, Inc. 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 | var prott = { 24 | "definations": { 25 | "pluginVersion": "Version 2.0.9", 26 | "apiBase": "https://prottapp.com/", 27 | "apiSignin": "users/sign_in.json", 28 | "apiSignout": "users/sign_out.json", 29 | "apiProjects": "api/sketch_app/projects.json", 30 | "apiScreens": "api/sketch_app/screens.json", 31 | "uploadToAppBase": "/Library/Application Support/com.goodpatch.prott.editor/toApp/", 32 | "uploadToPluginBase": "/Library/Application Support/com.goodpatch.prott.editor/toPlugin/", 33 | "ratioItems": [ 34 | {"name": "0.5x", "value": "0.5"}, 35 | {"name": "1x", "value": "1"}, 36 | {"name": "1.5x", "value": "1.5"}, 37 | {"name": "2x", "value": "2"}, 38 | {"name": "3x", "value": "3"}, 39 | {"name": "4x", "value": "4"} 40 | ] 41 | }, 42 | 43 | "getUnarchivedObjectFromData": function(data){ 44 | return [NSKeyedUnarchiver unarchiveObjectWithData:data] 45 | }, 46 | 47 | "setArchivedObjectForData": function(data){ 48 | return [NSKeyedArchiver archivedDataWithRootObject:data] 49 | }, 50 | 51 | "getSavedValueFromKey": function(key){ 52 | return [[NSUserDefaults standardUserDefaults] objectForKey:key] 53 | }, 54 | 55 | "saveValueForKey": function(value, key){ 56 | [[NSUserDefaults standardUserDefaults] setObject:value forKey:key] 57 | [[NSUserDefaults standardUserDefaults] synchronize] 58 | }, 59 | 60 | "isUpdated": function(){ 61 | var version = prott.getSavedValueFromKey("currentVersion") 62 | if (version == nil) { return true } 63 | return ![version isEqualToString: prott.definations.pluginVersion] 64 | }, 65 | 66 | "getSavedCookie": function(){ 67 | var cookieSaved = prott.getSavedValueFromKey("cookie") 68 | return prott.getUnarchivedObjectFromData(cookieSaved) 69 | }, 70 | 71 | "getSavedprojects": function(){ 72 | var projectsSaved = prott.getSavedValueFromKey("projects") 73 | return prott.getUnarchivedObjectFromData(projectsSaved) 74 | }, 75 | 76 | "showMessage": function(message, context){ 77 | var doc = context.document 78 | [doc showMessage:message] 79 | }, 80 | 81 | "showAlert": function(message, context){ 82 | var alert = NSAlert.alloc().init() 83 | var plugin = context.plugin 84 | var imageFilePath=[plugin urlForResourceNamed:"logo.png"] 85 | var imageData = [NSData dataWithContentsOfURL:imageFilePath] 86 | var image = NSImage.alloc().initWithData(imageData) 87 | alert.setIcon(image) 88 | alert.setMessageText(message) 89 | alert.addButtonWithTitle("OK") 90 | alert.runModal() 91 | }, 92 | 93 | "showLoginDialog": function(context){ 94 | var accessoryView = NSView.alloc().initWithFrame(NSMakeRect(0.0, 0.0, 260.0, 50.0)) 95 | 96 | var emailInputField = NSTextField.alloc().initWithFrame(NSMakeRect(0.0, 30.0, 260.0, 20.0)) 97 | emailInputField.cell().setPlaceholderString("Email") 98 | accessoryView.addSubview(emailInputField) 99 | 100 | var passwordInputField = NSSecureTextField.alloc().initWithFrame(NSMakeRect(0.0, 0.0, 260.0, 20.0)) 101 | passwordInputField.cell().setPlaceholderString("Password") 102 | accessoryView.addSubview(passwordInputField) 103 | 104 | var alert = NSAlert.alloc().init() 105 | alert.addButtonWithTitle("Login") 106 | alert.addButtonWithTitle("Cancel") 107 | alert.setAccessoryView(accessoryView) 108 | alert.setMessageText("Log into Prott") 109 | 110 | [[alert window] setInitialFirstResponder:emailInputField] 111 | [emailInputField setNextKeyView:passwordInputField] 112 | 113 | var plugin = context.plugin 114 | var imageFilePath=[plugin urlForResourceNamed:"logo.png"] 115 | var imageData = [NSData dataWithContentsOfURL:imageFilePath] 116 | var image = NSImage.alloc().initWithData(imageData) 117 | alert.setIcon(image) 118 | 119 | var responseCode = alert.runModal() 120 | return [responseCode, emailInputField.stringValue(), passwordInputField.stringValue()] 121 | }, 122 | 123 | "showOptionDialog": function(context, isAllArtboardSync){ 124 | var optionWindow = NSWindow.alloc().init() 125 | if(isAllArtboardSync){ 126 | [optionWindow setTitle:"Sync All Artboards to Prott"] 127 | }else{ 128 | [optionWindow setTitle:"Sync Selected Artboards to Prott"] 129 | } 130 | [optionWindow setFrame:NSMakeRect(0, 0, 523, 236) display:true] 131 | 132 | var accessoryView = NSView.alloc().initWithFrame(NSMakeRect(0.0, 0.0, 503.0, 256.0)) 133 | var plugin = context.plugin 134 | var imageFilePath=[plugin urlForResourceNamed:"logo@2x.png"] 135 | var imageData = [NSData dataWithContentsOfURL:imageFilePath] 136 | var image = NSImage.alloc().initWithData(imageData) 137 | var imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(20.0, 122.0, 80.0, 80.0)] 138 | [imageView setImage: image] 139 | accessoryView.addSubview(imageView) 140 | 141 | var syncOptionsBox = NSBox.alloc().initWithFrame(NSMakeRect(117.0, 46.0, 400.0, 166.0)) 142 | [syncOptionsBox setTitle:""] 143 | accessoryView.addSubview(syncOptionsBox) 144 | 145 | var projectsListButton = NSPopUpButton.alloc().initWithFrame(NSMakeRect(134.0, 112.0, 240.0, 26.0)) 146 | [projectsListButton setNeedsDisplay:true] 147 | var projects = prott.getSavedprojects() 148 | var projectsList = [] 149 | var menu = NSMenu.alloc().init() 150 | var separateFlg = false 151 | 152 | if (prott.getSavedValueFromKey("UploadPlatform") == 1) { 153 | projectsList = prott.getAppProjectList() 154 | menu = prott.getAppProjectListMenu() 155 | } else { 156 | for(organzation in projects){ 157 | if (projects[organzation].projects.count() == 0) { 158 | continue 159 | } 160 | if (separateFlg) { 161 | projectsList.push({name: "separate", id: nil}) 162 | [menu addItem:[NSMenuItem separatorItem]] 163 | }else{ 164 | separateFlg = true 165 | } 166 | 167 | var accountMenuItem = [[NSMenuItem alloc] initWithTitle:projects[organzation].name action:nil keyEquivalent:""] 168 | [accountMenuItem setEnabled:false] 169 | [menu addItem:accountMenuItem] 170 | projectsList.push({name: projects[organzation].name, id: nil}) 171 | 172 | for(i = 0; i < projects[organzation].projects.count(); i++){ 173 | var projectMenuItem = [[NSMenuItem alloc] initWithTitle:projects[organzation].projects[i].name action:nil keyEquivalent:""] 174 | [projectMenuItem setIndentationLevel:1] 175 | [menu addItem:projectMenuItem] 176 | projectsList.push({name: projects[organzation].projects[i].name, id: projects[organzation].projects[i].id}) 177 | } 178 | } 179 | } 180 | 181 | [menu setAutoenablesItems:false] 182 | [projectsListButton setMenu:menu] 183 | if(prott.getSavedValueFromKey("projectToSyncIndex")){ 184 | [projectsListButton selectItemAtIndex:prott.getSavedValueFromKey("projectToSyncIndex")] 185 | } else { 186 | [projectsListButton selectItemAtIndex:1] 187 | } 188 | syncOptionsBox.addSubview(projectsListButton) 189 | 190 | var selectProjectsLabel = NSTextField.alloc().initWithFrame(NSMakeRect(12.0, 108.0, 110.0, 26.0)) 191 | [selectProjectsLabel setEditable:false] 192 | [selectProjectsLabel setBordered:false] 193 | [selectProjectsLabel setDrawsBackground:false] 194 | [selectProjectsLabel setStringValue:"Select project:"] 195 | [selectProjectsLabel setAlignment:1] 196 | syncOptionsBox.addSubview(selectProjectsLabel) 197 | 198 | var ratioLabel = NSTextField.alloc().initWithFrame(NSMakeRect(12.0, 48.0, 110.0, 26.0)) 199 | [ratioLabel setEditable:false] 200 | [ratioLabel setBordered:false] 201 | [ratioLabel setDrawsBackground:false] 202 | [ratioLabel setStringValue:"Image ratio:"] 203 | [ratioLabel setAlignment:1] 204 | syncOptionsBox.addSubview(ratioLabel) 205 | 206 | var ratioList = NSPopUpButton.alloc().initWithFrame(NSMakeRect(134.0, 52.0, 80.0, 26.0)) 207 | for(var i in prott.definations.ratioItems){ 208 | var menuItem = [[NSMenuItem alloc] initWithTitle:prott.definations.ratioItems[i].name action:nil keyEquivalent:""] 209 | [[ratioList menu] addItem:menuItem] 210 | } 211 | if(prott.getSavedValueFromKey("ratioIndex")){ 212 | [ratioList selectItemAtIndex:prott.getSavedValueFromKey("ratioIndex")] 213 | }else{ 214 | [ratioList selectItemAtIndex:1] 215 | } 216 | syncOptionsBox.addSubview(ratioList) 217 | 218 | var syncOptionsLine = NSBox.alloc().initWithFrame(NSMakeRect(12.0, 35.0, 360.0, 4.0)) 219 | [syncOptionsLine setTitle:""] 220 | syncOptionsBox.addSubview(syncOptionsLine) 221 | 222 | var accountLabel = NSTextField.alloc().initWithFrame(NSMakeRect(12.0, 0.0, 110.0, 26.0)) 223 | [accountLabel setEditable:false] 224 | [accountLabel setBordered:false] 225 | [accountLabel setDrawsBackground:false] 226 | [accountLabel setStringValue:"Account:"] 227 | [accountLabel setAlignment:1] 228 | syncOptionsBox.addSubview(accountLabel) 229 | 230 | var accountInfo = NSTextField.alloc().initWithFrame(NSMakeRect(136.0, 0.0, 240.0, 26.0)) 231 | [accountInfo setEditable:false] 232 | [accountInfo setBordered:false] 233 | [accountInfo setDrawsBackground:false] 234 | 235 | // NSAppKitVersionNumber10_10 = 1343 236 | if (NSAppKitVersionNumber >= 1343) { 237 | [accountInfo setLineBreakMode:NSLineBreakByTruncatingTail] 238 | } 239 | [accountInfo setStringValue:prott.getSavedValueFromKey("accountInfo")] 240 | syncOptionsBox.addSubview(accountInfo) 241 | 242 | var versionLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0.0, 8.0, 200.0, 26.0)) 243 | [versionLabel setEditable:false] 244 | [versionLabel setBordered:false] 245 | if (NSAppKitVersionNumber >= 1343) { 246 | [versionLabel setTextColor:[NSColor grayColor]] 247 | } 248 | [versionLabel setDrawsBackground:false] 249 | [versionLabel setStringValue:prott.definations.pluginVersion] 250 | [versionLabel setAlignment:1] 251 | accessoryView.addSubview(versionLabel) 252 | 253 | var cancelButton = NSButton.alloc().initWithFrame(NSMakeRect(293.0, 10.0, 110.0, 26.0)) 254 | [cancelButton setTitle:"Cancel"] 255 | [cancelButton setBezelStyle:NSRoundedBezelStyle] 256 | [cancelButton setCOSJSTargetFunction:function(sender) { 257 | if(projectsList[0] != nil) { 258 | if(projectsList[[projectsListButton indexOfSelectedItem]].id){ 259 | prott.saveValueForKey(projectsList[[projectsListButton indexOfSelectedItem]].id, "projectToSync") 260 | prott.saveValueForKey([projectsListButton indexOfSelectedItem], "projectToSyncIndex") 261 | prott.saveValueForKey(prott.definations.ratioItems[[ratioList indexOfSelectedItem]].value, "ratio") 262 | prott.saveValueForKey([ratioList indexOfSelectedItem], "ratioIndex") 263 | } 264 | } 265 | 266 | [[NSApplication sharedApplication] stopModal] 267 | [optionWindow orderOut:self] 268 | [projectsListButton setCOSJSTargetFunction:undefined] 269 | [refreshProjectsButton setCOSJSTargetFunction:undefined] 270 | [cancelButton setCOSJSTargetFunction:undefined] 271 | }] 272 | accessoryView.addSubview(cancelButton) 273 | 274 | var syncButton = NSButton.alloc().initWithFrame(NSMakeRect(407.0, 10.0, 110.0, 26.0)) 275 | [syncButton setKeyEquivalent:"\r"] 276 | [syncButton setTitle:"Sync"] 277 | [syncButton setEnabled: (projectsList[0] != nil)] 278 | [syncButton setBezelStyle:NSRoundedBezelStyle] 279 | [syncButton setCOSJSTargetFunction:function(sender) { 280 | if(projectsList[0] != nil) { 281 | if(projectsList[[projectsListButton indexOfSelectedItem]].id){ 282 | prott.saveValueForKey(projectsList[[projectsListButton indexOfSelectedItem]].id, "projectToSync") 283 | prott.saveValueForKey([projectsListButton indexOfSelectedItem], "projectToSyncIndex") 284 | prott.saveValueForKey(prott.definations.ratioItems[[ratioList indexOfSelectedItem]].value, "ratio") 285 | prott.saveValueForKey([ratioList indexOfSelectedItem], "ratioIndex") 286 | } 287 | } 288 | [[NSApplication sharedApplication] stopModal] 289 | if(isAllArtboardSync) { 290 | prott.syncAllArtboards(context) 291 | }else{ 292 | prott.syncSelectedArtboards(context) 293 | } 294 | [optionWindow orderOut:self] 295 | [projectsListButton setCOSJSTargetFunction:undefined] 296 | [refreshProjectsButton setCOSJSTargetFunction:undefined] 297 | [syncButton setCOSJSTargetFunction:undefined] 298 | }] 299 | accessoryView.addSubview(syncButton) 300 | 301 | projectsList = prott.getProjectList(false) 302 | menu = prott.getProjectListMenu(false) 303 | [projectsListButton removeAllItems] 304 | [projectsListButton setMenu:menu] 305 | 306 | if(prott.getSavedValueFromKey("projectToSyncIndex")){ 307 | if (prott.getSavedValueFromKey("projectToSyncIndex") == 0) { 308 | [projectsListButton selectItemAtIndex:1] 309 | } else { 310 | [projectsListButton selectItemAtIndex:prott.getSavedValueFromKey("projectToSyncIndex")] 311 | } 312 | } else { 313 | [projectsListButton selectItemAtIndex:1] 314 | } 315 | prott.saveValueForKey(0, "UploadPlatform") 316 | 317 | var refreshProjectsButton = NSButton.alloc().initWithFrame(NSMakeRect(130.0, 81.0, 160.0, 26.0)) 318 | [refreshProjectsButton setTitle:"Refresh projects list"] 319 | [refreshProjectsButton setBezelStyle:NSRoundedBezelStyle] 320 | [refreshProjectsButton setCOSJSTargetFunction:function(sender) { 321 | prott.getProjectsFromServer() 322 | 323 | projects = prott.getSavedprojects() 324 | projectsList = [] 325 | var menu = NSMenu.alloc().init() 326 | separateFlg = false 327 | for(organzation in projects){ 328 | if (projects[organzation].projects.count() == 0) { 329 | continue 330 | } 331 | if (separateFlg) { 332 | projectsList.push({name: "separate", id: nil}) 333 | [menu addItem:[NSMenuItem separatorItem]] 334 | }else{ 335 | separateFlg = true 336 | } 337 | 338 | var accountMenuItem = [[NSMenuItem alloc] initWithTitle:projects[organzation].name action:nil keyEquivalent:""] 339 | [accountMenuItem setEnabled:false] 340 | [menu addItem:accountMenuItem] 341 | projectsList.push({name: projects[organzation].name, id: nil}) 342 | 343 | for(i = 0; i < projects[organzation].projects.count(); i++){ 344 | var projectMenuItem = [[NSMenuItem alloc] initWithTitle:" "+projects[organzation].projects[i].name action:nil keyEquivalent:""] 345 | [menu addItem:projectMenuItem] 346 | projectsList.push({name: projects[organzation].projects[i].name, id: projects[organzation].projects[i].id}) 347 | } 348 | } 349 | [menu setAutoenablesItems:false] 350 | [projectsListButton setMenu:menu] 351 | if(prott.getSavedValueFromKey("projectToSyncIndex")){ 352 | [projectsListButton selectItemAtIndex:prott.getSavedValueFromKey("projectToSyncIndex")] 353 | }else{ 354 | [projectsListButton selectItemAtIndex:1] 355 | } 356 | }] 357 | syncOptionsBox.addSubview(refreshProjectsButton) 358 | 359 | if(projects == nil || projects.count() == 0){ 360 | prott.showAlert("It seems you don't have any projects. Go to https://prottapp.com and create one, then click the 'Refresh projects list' button from the options.", context) 361 | } 362 | [optionWindow setContentView:accessoryView] 363 | [[NSApplication sharedApplication] runModalForWindow:optionWindow] 364 | }, 365 | 366 | "getProjectListMenu": function(refleshFlg){ 367 | if (refleshFlg) { 368 | prott.getProjectsFromServer() 369 | } 370 | 371 | var projects = prott.getSavedprojects() 372 | var menu = NSMenu.alloc().init() 373 | var separateFlg = false 374 | for(organzation in projects){ 375 | if (projects[organzation].projects.count() == 0) { 376 | continue 377 | } 378 | if (separateFlg) { 379 | [menu addItem:[NSMenuItem separatorItem]] 380 | }else{ 381 | separateFlg = true 382 | } 383 | 384 | var accountMenuItem = [[NSMenuItem alloc] initWithTitle:projects[organzation].name action:nil keyEquivalent:""] 385 | [accountMenuItem setEnabled:false] 386 | [menu addItem:accountMenuItem] 387 | 388 | for(i = 0; i < projects[organzation].projects.count(); i++){ 389 | var projectMenuItem = [[NSMenuItem alloc] initWithTitle:" "+projects[organzation].projects[i].name action:nil keyEquivalent:""] 390 | [menu addItem:projectMenuItem] 391 | } 392 | } 393 | [menu setAutoenablesItems:false] 394 | return menu 395 | }, 396 | 397 | "getProjectList": function(refleshFlg){ 398 | if (refleshFlg) { 399 | prott.getProjectsFromServer() 400 | } 401 | 402 | var projects = prott.getSavedprojects() 403 | var projectsList = [] 404 | var separateFlg = false 405 | for(organzation in projects){ 406 | if (projects[organzation].projects.count() == 0) { 407 | continue 408 | } 409 | if (separateFlg) { 410 | projectsList.push({name: "separate", id: nil}) 411 | }else{ 412 | separateFlg = true 413 | } 414 | projectsList.push({name: projects[organzation].name, id: nil}) 415 | 416 | for(i = 0; i < projects[organzation].projects.count(); i++){ 417 | projectsList.push({name: projects[organzation].projects[i].name, id: projects[organzation].projects[i].id}) 418 | } 419 | } 420 | return projectsList 421 | }, 422 | 423 | "isNewScreen": function(element) { 424 | var screens = prott.getAppScreens() 425 | if (!screens) { 426 | return true 427 | } 428 | 429 | var newScreenflg = true 430 | for(i = 0; i < [screens count]; i++){ 431 | var screen = screens[i] 432 | if ([[screen objectForKey:"fileName"] isEqualToString:[element objectForKey:"fileName"]]) { 433 | newScreenflg = false 434 | break 435 | } 436 | } 437 | return newScreenflg 438 | }, 439 | 440 | "getAppScreens": function() { 441 | var path = NSHomeDirectory() + prott.definations.uploadToAppBase + prott.getSavedValueFromKey("projectToSync") + "/" 442 | var fileManager = [NSFileManager defaultManager]; 443 | var screens = nil 444 | if ([fileManager fileExistsAtPath:path+"screens.json"]) { 445 | 446 | var data = [NSData dataWithContentsOfFile:path+"screens.json"] 447 | var array = [NSJSONSerialization JSONObjectWithData:data 448 | options:NSJSONReadingMutableContainers 449 | error:nil] 450 | screens = array 451 | } 452 | 453 | return screens 454 | }, 455 | 456 | "getAppProjects": function() { 457 | var path = NSHomeDirectory() + prott.definations.uploadToPluginBase 458 | var fileManager = [NSFileManager defaultManager]; 459 | var array = [] 460 | if ([fileManager fileExistsAtPath:path]) { 461 | 462 | var data = [NSData dataWithContentsOfFile:path+"projects.json"] 463 | array = [NSJSONSerialization JSONObjectWithData:data 464 | options:NSJSONReadingMutableContainers 465 | error:nil] 466 | } 467 | return array 468 | }, 469 | 470 | "getAppProjectListMenu": function(){ 471 | var projects = prott.getAppProjects() 472 | var menu = NSMenu.alloc().init() 473 | var userID = prott.getSavedValueFromKey("userID") 474 | for(i = 0; i < projects.count(); i++ ){ 475 | var project = projects[i] 476 | if([[project objectForKey:"userID"] isEqualToString:userID]){ 477 | var projectMenuItem = [[NSMenuItem alloc] initWithTitle:[project objectForKey:"name"] action:nil keyEquivalent:""] 478 | [menu addItem:projectMenuItem] 479 | } 480 | } 481 | [menu setAutoenablesItems:false] 482 | return menu 483 | }, 484 | 485 | "getAppProjectList": function(refleshFlg){ 486 | var projectsList = [] 487 | var projects = prott.getAppProjects() 488 | var menu = NSMenu.alloc().init() 489 | var userID = prott.getSavedValueFromKey("userID") 490 | 491 | for(i = 0; i < projects.count(); i++ ){ 492 | var project = projects[i] 493 | if([[project objectForKey:"userID"] isEqualToString:userID]){ 494 | projectsList.push({name: [project objectForKey:"name"], id: [project objectForKey:"id"]}) 495 | } 496 | } 497 | return projectsList 498 | }, 499 | 500 | "initialSettings": function(context, isAllArtboardSync){ 501 | var response = prott.showLoginDialog(context) 502 | if(response[0] == 1000){ 503 | var response = prott.loginWithEmailAndPassword(context, response[1], response[2]) 504 | if(response == 1){ 505 | prott.saveValueForKey(prott.definations.pluginVersion, "currentVersion") 506 | prott.showOptionDialog(context, isAllArtboardSync) 507 | }else{ 508 | prott.showAlert("Oops! Something went wrong. Please check your login details and try again.", context) 509 | prott.initialSettings(context, isAllArtboardSync) 510 | } 511 | } 512 | }, 513 | 514 | "loginWithEmailAndPassword": function(context, email, password){ 515 | var url = [NSURL URLWithString:prott.definations.apiBase+prott.definations.apiSignin] 516 | var request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60] 517 | [request setHTTPMethod:"POST"] 518 | [request setValue:"sketch" forHTTPHeaderField:"User-Agent"] 519 | [request setValue:"application/json" forHTTPHeaderField:"Content-Type"] 520 | [request setValue:"sketch" forHTTPHeaderField:"App-Type"] 521 | 522 | var subParameter = {"email": email, "password": password} 523 | var parameter = {"user": subParameter} 524 | var postData = [NSJSONSerialization dataWithJSONObject:parameter options:0 error:nil] 525 | [request setHTTPBody:postData] 526 | var response = MOPointer.alloc().init() 527 | var error = MOPointer.alloc().init() 528 | var data = [NSURLConnection sendSynchronousRequest:request returningResponse:response error:error] 529 | 530 | if (error.value() == nil && data != nil){ 531 | var res = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil] 532 | var cookie = [NSHTTPCookie cookiesWithResponseHeaderFields:response.value().valueForKey(@"allHeaderFields") forURL:[NSURL URLWithString:"/"]] 533 | var cookieForSave = prott.setArchivedObjectForData(cookie) 534 | var accountInfoValue = "" 535 | prott.saveValueForKey(cookieForSave, "cookie") 536 | prott.saveValueForKey(res.id, "userID") 537 | if (NSAppKitVersionNumber >= 1343) { 538 | accountInfoValue = res.uid+" ("+res.email+")" 539 | }else{ 540 | accountInfoValue = res.email 541 | } 542 | prott.saveValueForKey(accountInfoValue, "accountInfo") 543 | prott.getProjectsFromServer() 544 | return true 545 | }else{ 546 | return error.value() 547 | } 548 | }, 549 | 550 | "logoutFromSketch": function(){ 551 | prott.saveValueForKey(nil, "cookie") 552 | prott.saveValueForKey(nil, "accountInfo") 553 | prott.saveValueForKey(nil, "projects") 554 | prott.saveValueForKey(nil, "projectToSyncIndex") 555 | prott.saveValueForKey(nil, "userID") 556 | prott.saveValueForKey(nil, "currentVersion") 557 | 558 | var url = [NSURL URLWithString:prott.definations.apiBase+prott.definations.apiSignout] 559 | var request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60] 560 | [request setHTTPMethod:"DELETE"] 561 | [request setValue:"Sketch" forHTTPHeaderField:"User-Agent"] 562 | [request setValue:"application/json" forHTTPHeaderField:"Content-Type"] 563 | [request setValue:"sketch" forHTTPHeaderField:"App-Type"] 564 | 565 | var response = MOPointer.alloc().init() 566 | var error = MOPointer.alloc().init() 567 | var data = [NSURLConnection sendSynchronousRequest:request returningResponse:response error:error] 568 | }, 569 | 570 | "getProjectsFromServer": function(){ 571 | var url = [NSURL URLWithString:prott.definations.apiBase+prott.definations.apiProjects] 572 | 573 | var request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:60] 574 | [request setHTTPMethod:"GET"] 575 | [request setValue:"sketch" forHTTPHeaderField:"User-Agent"] 576 | [request setValue:"application/json" forHTTPHeaderField:"Content-Type"] 577 | [request setValue:"sketch" forHTTPHeaderField:"App-Type"] 578 | 579 | var cookie = prott.getSavedCookie() 580 | var header = [NSHTTPCookie requestHeaderFieldsWithCookies:cookie] 581 | [request setAllHTTPHeaderFields:header] 582 | 583 | var response = MOPointer.alloc().init() 584 | var error = MOPointer.alloc().init() 585 | var data = [NSURLConnection sendSynchronousRequest:request returningResponse:response error:error] 586 | if (error.value() == nil && data != nil){ 587 | var res = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil] 588 | var projectsForSave = prott.setArchivedObjectForData(res) 589 | prott.saveValueForKey(projectsForSave, "projects") 590 | return res 591 | }else{ 592 | var res = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil] 593 | return error.value() 594 | } 595 | }, 596 | 597 | "syncSelectedArtboards": function(context){ 598 | var doc = context.document 599 | var selections = context.selection 600 | var loop = [selections objectEnumerator] 601 | // Web 602 | if (prott.getSavedValueFromKey("UploadPlatform") == 0) { 603 | var count = 0 604 | var cookie = [NSHTTPCookie requestHeaderFieldsWithCookies:prott.getSavedCookie()] 605 | while (artboard = [loop nextObject]) { 606 | if (artboard.className() == "MSArtboardGroup") { 607 | if (! prott.uploadArtboard(doc, artboard, cookie.Cookie, context)) { 608 | break 609 | } 610 | count++ 611 | } 612 | } 613 | if (count == 0) { 614 | prott.showAlert("You must select at least one artboard to sync.", context) 615 | }else{ 616 | prott.showMessage("All selected artboards synced to Prott.", context) 617 | } 618 | } else if (prott.getSavedValueFromKey("UploadPlatform") == 1) { // Mac 619 | var array = prott.getAppScreens() ? prott.getAppScreens() : [[NSMutableArray alloc] initWithCapacity:0] 620 | while (artboard = [loop nextObject]) { 621 | if (artboard.className() == "MSArtboardGroup") { 622 | var fileName = artboard.objectID() + ".png" 623 | var artboardName = artboard.name() 624 | 625 | var screenID = artboard.objectID() 626 | var nameDictionary = {"fileName": fileName, "name": artboardName, "id": screenID} 627 | if (prott.isNewScreen(nameDictionary)) { 628 | [array addObject:nameDictionary] 629 | } 630 | 631 | prott.uploadArtboardToAppLibrary(doc, artboard, context) 632 | } 633 | } 634 | if (array.count() == 0) { 635 | prott.showAlert("You must select at least one artboard to sync.", context) 636 | }else{ 637 | // json出力 638 | var jsonData = [NSJSONSerialization dataWithJSONObject:array options:0 error:nil]; 639 | var localUploadDirPath = NSHomeDirectory() + prott.definations.uploadToAppBase + prott.getSavedValueFromKey("projectToSync") + "/" 640 | var jsonFilePath = localUploadDirPath + "/screens.json" 641 | [jsonData writeToFile:jsonFilePath atomically:true] 642 | prott.showMessage("All selected artboards synced to Prott for Mac.", context) 643 | } 644 | 645 | } 646 | }, 647 | 648 | "syncAllArtboards": function(context){ 649 | var doc = context.document 650 | var artboards = [[doc currentPage] artboards] 651 | var loop = [artboards objectEnumerator] 652 | if (prott.getSavedValueFromKey("UploadPlatform") == 0) { // Web 653 | var cookie = [NSHTTPCookie requestHeaderFieldsWithCookies:prott.getSavedCookie()] 654 | while (artboard = [loop nextObject]) { 655 | if (! prott.uploadArtboard(doc, artboard, cookie.Cookie, context)) { 656 | break 657 | } 658 | } 659 | } else if (prott.getSavedValueFromKey("UploadPlatform") == 1) { // Mac 660 | var array = prott.getAppScreens() ? prott.getAppScreens() : [[NSMutableArray alloc] initWithCapacity:0] 661 | while (artboard = [loop nextObject]) { 662 | var fileName = artboard.objectID() + ".png" 663 | var artboardName = artboard.name() 664 | var screenID = artboard.objectID() 665 | var nameDictionary = {"fileName": fileName, "name": artboardName, "id": screenID} 666 | if (prott.isNewScreen(nameDictionary)) { 667 | [array addObject:nameDictionary] 668 | } 669 | 670 | prott.uploadArtboardToAppLibrary(doc, artboard, context) 671 | } 672 | // json出力 673 | var jsonData = [NSJSONSerialization dataWithJSONObject:array options:0 error:nil]; 674 | var localUploadDirPath = NSHomeDirectory() + prott.definations.uploadToAppBase + prott.getSavedValueFromKey("projectToSync") + "/" 675 | var jsonFilePath = localUploadDirPath + "/screens.json" 676 | [jsonData writeToFile:jsonFilePath atomically:true] 677 | } 678 | prott.showMessage("All artboards synced to Prott.", context) 679 | }, 680 | 681 | /** 682 | * Upload artboard to prott 683 | * @return {boolean} Return ture when the upload is successful 684 | */ 685 | "uploadArtboard": function(document, artboard, cookie, context){ 686 | var copy = [artboard duplicate] 687 | var slice = MSExportRequest.exportRequestsFromExportableLayer(copy).firstObject() 688 | [copy removeFromParent] 689 | slice.scale = prott.getSavedValueFromKey("ratio") 690 | slice.format = "png" 691 | var filePath = NSTemporaryDirectory() + "prott/" + artboard.objectID() + ".png" 692 | [document saveArtboardOrSlice:slice toFile: filePath] 693 | 694 | var task = NSTask.alloc().init() 695 | task.setLaunchPath("/usr/bin/curl") 696 | var args = ["-X", "POST", "-H", "Content-Type: multipart/form-data", "-H", "App-Type: sketch", "-H", "X-Accept: application/json", "-b", cookie, "-F", "project_id=" + prott.getSavedValueFromKey("projectToSync"), "-F", "screen[sketch_artboard_id]=" + artboard.objectID(), "-F", "screen[name]=" + artboard.name(), "-F", "Content-Disposition: form-data; name=file; filename=filename; Content-Type=image/png;", "-F", "screen[file]=@" + filePath, prott.definations.apiBase+prott.definations.apiScreens] 697 | task.setArguments(args) 698 | var outputPipe = [NSPipe pipe] 699 | [task setStandardOutput:outputPipe] 700 | task.launch() 701 | 702 | var outputData = [[outputPipe fileHandleForReading] readDataToEndOfFile] 703 | var outputString = [[[NSString alloc] initWithData:outputData encoding:NSUTF8StringEncoding]] 704 | var outputArray = [NSJSONSerialization JSONObjectWithData:outputData options:NSJSONReadingAllowFragments error:nil] 705 | if(outputString != "successed"){ 706 | if(outputString == " " || outputString == ""){ 707 | prott.showAlert("Oops! Something went wrong. Please check if the project you tried to sync exists or if you have editing permission for the project.", context) 708 | }else if(outputString == "The screen you tried to sync cannot be updated because it is currently locked."){ 709 | prott.showAlert("The screen you tried to sync cannot be updated because it is currently locked.", context) 710 | }else if(outputString == "The screen you tried to sync cannot be updated because it is currently archived."){ 711 | prott.showAlert("The screen you tried to sync cannot be updated because it is currently archived.", context) 712 | }else if(outputString == "Request is invalid."){ 713 | prott.showAlert("Request is invalid.", context) 714 | }else if(outputArray == nil){ 715 | prott.showAlert(outputString, context) 716 | } 717 | else{ 718 | prott.showAlert(outputArray["error"], context) 719 | if (outputArray["error"] == "セッションがタイムアウトしました。もう一度ログインしてください。" || 720 | outputArray["error"] == "You need to sign in or sign up before continuing.") { 721 | prott.logoutFromSketch() 722 | } 723 | } 724 | return false; 725 | } 726 | return true; 727 | }, 728 | 729 | "uploadArtboardToAppLibrary": function(document, artboard, context){ 730 | var copy = [artboard duplicate] 731 | var slice = MSExportRequest.exportRequestsFromExportableLayer(copy).firstObject() 732 | [copy removeFromParent] 733 | slice.scale = prott.getSavedValueFromKey("ratio") 734 | slice.format = "png" 735 | var fromPath = NSTemporaryDirectory() + "prott/" + artboard.objectID() + ".png" 736 | [document saveArtboardOrSlice:slice toFile: fromPath] 737 | 738 | var fileManager = [NSFileManager defaultManager] 739 | var localUploadDirPath = NSHomeDirectory() + prott.definations.uploadToAppBase + prott.getSavedValueFromKey("projectToSync") + "/" 740 | var toPath = localUploadDirPath + artboard.objectID() + ".png" 741 | 742 | if ([fileManager fileExistsAtPath: localUploadDirPath] == false) { 743 | [fileManager createDirectoryAtPath:localUploadDirPath withIntermediateDirectories:true attributes:nil error:nil] 744 | } 745 | if ([fileManager fileExistsAtPath: toPath]) { 746 | [fileManager removeItemAtPath:toPath error:nil] 747 | } 748 | [fileManager moveItemAtPath:fromPath toPath:toPath error:nil] 749 | } 750 | }; 751 | -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Sketch/logout.cocoascript: -------------------------------------------------------------------------------- 1 | /*The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Goodpatch, Inc. 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 | @import 'core.cocoascript' 24 | 25 | var onRun = function (context){ 26 | if(prott.getSavedCookie()){ 27 | prott.logoutFromSketch() 28 | } 29 | }; -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Prott Sketch plugin", 3 | "description": "Sync Sketch artboards to Prott like a boss.", 4 | "author": "Goodpatch, Inc.", 5 | "homepage": "https://docs.prottapp.com/article/156-how-to-use-sketch-plugin", 6 | "version": "2.0.9", 7 | "appcast": "https://raw.githubusercontent.com/goodpatch/Prott-Sketch-Plugin/master/.appcast.xml", 8 | "identifier": "com.goodpatch.prott.prott-sketch-plugin", 9 | "updateURL": "", 10 | "compatibleVersion": 3, 11 | "bundleVersion": 1, 12 | "commands": [ 13 | { 14 | "name": "Sync All Artboards to Prott", 15 | "identifier": "sync-all-artboards", 16 | "shortcut": "cmd shift option u", 17 | "handler": "onRun", 18 | "script": "sync-all-artboards.cocoascript" 19 | }, 20 | { 21 | "name": "Sync Selected Artboards to Prott", 22 | "identifier": "sync-selected-artboards", 23 | "shortcut": "cmd shift u", 24 | "handler": "onRun", 25 | "script": "sync-selected-artboards.cocoascript" 26 | }, 27 | { 28 | "name": "Logout", 29 | "identifier": "logout", 30 | "handler": "onRun", 31 | "script": "logout.cocoascript" 32 | } 33 | ], 34 | "menu": { 35 | "title": "Prott Sketch Plugin", 36 | "items": [ 37 | "sync-all-artboards", 38 | "sync-selected-artboards", 39 | "-", 40 | "logout" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Sketch/sync-all-artboards.cocoascript: -------------------------------------------------------------------------------- 1 | /*The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Goodpatch, Inc. 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 | @import 'core.cocoascript' 24 | 25 | var onRun = function (context){ 26 | if(!prott.isUpdated() && prott.getSavedCookie()) { 27 | prott.showOptionDialog(context, true) 28 | }else{ 29 | prott.initialSettings(context, true) 30 | } 31 | }; -------------------------------------------------------------------------------- /prott.sketchplugin/Contents/Sketch/sync-selected-artboards.cocoascript: -------------------------------------------------------------------------------- 1 | /*The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Goodpatch, Inc. 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 | @import 'core.cocoascript' 24 | 25 | var onRun = function (context){ 26 | if(!prott.isUpdated() && prott.getSavedCookie()) { 27 | prott.showOptionDialog(context, false) 28 | }else{ 29 | prott.initialSettings(context, false) 30 | } 31 | }; --------------------------------------------------------------------------------