├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── manifest.json ├── package.json ├── resources ├── fillmurray.icns ├── lorempixel.icns ├── placecage.icns ├── placeholdit.icns ├── placekitten.icns └── unsplashit.icns ├── scripts └── release.js └── src ├── components ├── Alert.js ├── Label.js ├── PopUpButton.js └── TextField.js ├── handlers ├── fillMurray.js ├── placeCage.js ├── placeHoldIt.js ├── placeKitten.js └── unsplashIt.js ├── index.cocoascript └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "modules": true, 4 | "experimentalObjectRestSpread": true 5 | }, 6 | "rules": { 7 | "accessor-pairs": 0, 8 | "comma-dangle": [1, "never"], 9 | "comma-spacing": [1, {"before": false, "after": true}], 10 | "comma-style": [2, "last"], 11 | "consistent-return": 2, 12 | "dot-location": [ 13 | 2, 14 | "property" 15 | ], 16 | "dot-notation": 2, 17 | "eol-last": 2, 18 | "indent": [ 19 | 2, 20 | 2, 21 | { 22 | "SwitchCase": 1 23 | } 24 | ], 25 | "keyword-spacing": 2, 26 | "no-bitwise": 0, 27 | "no-console": 2, 28 | "no-const-assign": 2, 29 | "no-debugger": 2, 30 | "no-empty": 2, 31 | 'no-missing-import': 0, 32 | "no-multi-spaces": 2, 33 | "no-shadow": 0, 34 | "no-undef": 2, 35 | "no-unreachable": 1, 36 | "no-unused-expressions": 2, 37 | "no-unused-vars": [ 38 | 2, 39 | { 40 | "args": "none", 41 | "varsIgnorePattern": "_", 42 | "argsIgnorePattern": "_" 43 | } 44 | ], 45 | "object-curly-spacing": [1, "never"], 46 | "quotes": [ 47 | 1, 48 | "single", 49 | "avoid-escape" 50 | ], 51 | "semi": 2, 52 | "space-before-blocks": 2, 53 | "space-in-parens": [1, "never"], 54 | "strict": [ 55 | 2, 56 | "never" 57 | ] 58 | }, 59 | "env": { 60 | "es6": true 61 | }, 62 | "globals": { 63 | Alert: true, 64 | Label: true, 65 | PopUpButton: true, 66 | TextField: true, 67 | createConfirmHandler: true, 68 | createPluginHandler: true, 69 | lowLevelSetImage: true, 70 | NSAlert: true, 71 | NSView: true, 72 | NSMakeRect: true, 73 | NSImage: true, 74 | NSURL: true, 75 | MSImageData: true, 76 | NSTextField: true, 77 | NSPopUpButton: true, 78 | log: true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | Day Player.sketchplugin 4 | DayPlayer.zip 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 5.12.0 4 | sudo: false 5 | cache: 6 | directories: 7 | - /home/travis/.local 8 | - node_modules 9 | - $(npm config get cache) 10 | before_install: 11 | - npm config set progress false 12 | - npm config set spin false 13 | install: 14 | - pip install --user -U awscli 15 | - npm install 16 | script: 17 | - make lint 18 | deploy: 19 | - provider: script 20 | script: make publish 21 | on: 22 | tags: true 23 | env: 24 | global: 25 | - AWS_ACCESS_KEY_ID=AKIAIRMRYU635FO4D26A 26 | - secure: PqKc8FFSOiIQvEMkQtPv9b55NwdMreHEF4njCnnk/pcBgUIep8Ew2JYmdp/KuSx1gyZBRys0hJqSTJ6FVZrCkI7pHD3bkKrNGq807c5yVFHtTzTUoQZ62zYEC/fnsbvKdd6sGtznABB1boiuiV1Crj0v2gtkA/kUJ431XUFqBw4= 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tyler Gaw 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PLUGIN_PATH := ~/Library/Application\ Support/com.bohemiancoding.sketch3/Plugins/ 2 | PLUGIN_DIR := Day\ Player.sketchplugin 3 | PLUGIN_NAME := Day Player 4 | ZIP_NAME := DayPlayer.zip 5 | S3_BUCKET := s3://day-player 6 | 7 | SRC_DIR := ./src 8 | RESOURCES_DIR := ./resources 9 | LICENSE := LICENSE.md 10 | MANIFEST := manifest.json 11 | 12 | clean: 13 | @rm -rf $(PLUGIN_DIR) 14 | @rm -f $(ZIP_NAME) 15 | @rm -rf $(PLUGIN_PATH)$(PLUGIN_DIR) 16 | 17 | build: 18 | @make clean 19 | @mkdir $(PLUGIN_DIR) 20 | @mkdir $(PLUGIN_DIR)/Contents 21 | @cp $(LICENSE) $(PLUGIN_DIR)/Contents/$(LICENSE) 22 | @cp -r $(RESOURCES_DIR) $(PLUGIN_DIR)/Contents/Resources 23 | @cp -r $(SRC_DIR) $(PLUGIN_DIR)/Contents/Sketch 24 | @cp -r $(MANIFEST) $(PLUGIN_DIR)/Contents/Sketch/$(MANIFEST) 25 | 26 | install: 27 | @make clean 28 | @echo "Installing $(PLUGIN_NAME)..." 29 | @make build 30 | @mv $(PLUGIN_DIR) $(PLUGIN_PATH) 31 | @echo "$(PLUGIN_NAME) installed" 32 | 33 | package: 34 | make build 35 | @zip -rm $(ZIP_NAME) $(PLUGIN_DIR) 36 | @echo "$(ZIP_NAME) created" 37 | 38 | changed: 39 | @echo "Changes detected..." 40 | @make install 41 | 42 | lint: 43 | @echo "Linting with eslint..." 44 | @./node_modules/.bin/eslint ./src/**/*.js 45 | 46 | watch: 47 | @echo "Watching src directory for changes..." 48 | @./node_modules/.bin/watch 'make changed' ./src ./resources 49 | 50 | release: 51 | @node scripts/release.js 52 | 53 | publish: 54 | @make package 55 | @aws s3 cp $(ZIP_NAME) $(S3_BUCKET)/releases/DayPlayer-$(TRAVIS_TAG).zip 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Day Player Download](https://d3vv6lp55qjaqc.cloudfront.net/items/1u3H0M0L1j281F0R2E39/dayplayer-sketch.png) Download the latest version (3.0.0)](http://day-player.s3-website-us-east-1.amazonaws.com/releases/DayPlayer-3.0.0.zip) 2 | 3 | ## Day Player [![Build Status](https://travis-ci.org/tylergaw/day-player.svg)](https://travis-ci.org/tylergaw/day-player) 4 | A Sketch Plugin for creating placeholder images from online services. 5 | 6 | ## Installation 7 | 8 | - [ Download the latest version (3.0.0)](http://day-player.s3-website-us-east-1.amazonaws.com/releases/DayPlayer-3.0.0.zip) 9 | - Unzip the file 10 | - Double-click Day Player.sketchplugin to install 11 | 12 | ## What Does It Do? 13 | It allows you to insert customized placeholder images into any Sketch document from a number 14 | of different placeholder image services: 15 | 16 | - [unsplash.it](http://unsplash.it/) 17 | - [placehold.it](http://placehold.it/) 18 | - [fillmurray.com](http://www.fillmurray.com/) 19 | - [placecage.com](http://www.placecage.com/) 20 | - [placekitten.com](http://placekitten.com/) 21 | 22 | ## How to use it 23 | - Open a new or existing Sketch document 24 | - Plugins > Day Player > Service... 25 | - Update the options to your liking, OK/Enter 26 | - The image is created on the canvas 27 | 28 | ![Animated gif showing basic Day Player usage](https://d3vv6lp55qjaqc.cloudfront.net/items/1q2S3E2B333G2m382A1v/Screen%20Recording%202016-11-13%20at%2001.52%20PM.gif) 29 | 30 | ### Appending images to an Artboard or Group 31 | - Open a new or existing Sketch document 32 | - Select the Artboard or Group 33 | - Plugins > Day Player > Service... 34 | - Update the options to your liking, OK/Enter 35 | - The image is created within the Artboard or Group 36 | - Image will be placed in the top left corner of the Artboard or Group 37 | 38 | **Artboard:** 39 | 40 | ![Animated gif showing Day Player usage on Artboard](https://d3vv6lp55qjaqc.cloudfront.net/items/2P1n0t0H1o0y0E1J1I3t/Screen%20Recording%202016-11-13%20at%2001.58%20PM.gif) 41 | 42 | **Group:** 43 | 44 | ![Animated gif showing Day Player usage on Group](https://d3vv6lp55qjaqc.cloudfront.net/items/1y2c3Z3m3K2F0b1T2720/Screen%20Recording%202016-11-13%20at%2003.20%20PM.gif) 45 | 46 | ### Creating images with dimensions and position of existing Layers 47 | - Open a new or existing Sketch document 48 | - Select the desired Layer 49 | - Plugins > Day Player > Service... 50 | - Update the options to your liking, OK/Enter 51 | - The image is created in the parent group of the selected layer 52 | - Image will inherit the x, y, width, and height of the selected layer 53 | 54 | ![Animated gif showing Day Player existing layer usage](https://d3vv6lp55qjaqc.cloudfront.net/items/0o1M3n07223o223D2C3R/Screen%20Recording%202016-11-13%20at%2003.23%20PM.gif) 55 | 56 | ### Advanced Service Options 57 | 58 | All services offer width, height, and black & white / color options. Unsplash.it and Placehold.it offer further options to customize the placeholder images. 59 | 60 | **Unsplash.it:** 61 | 62 | ![Animated gif showing Day Player Unsplash.it usage](https://d3vv6lp55qjaqc.cloudfront.net/items/3a1g161P1S0r0J2g030Y/Screen%20Recording%202016-11-13%20at%2002.15%20PM.gif) 63 | 64 | **Placehold.it:** 65 | 66 | ![Animated gif showing Day Player Placehold.it usage](https://d3vv6lp55qjaqc.cloudfront.net/items/2h0x3M1S250N1i1g081M/Screen%20Recording%202016-11-13%20at%2003.26%20PM.gif) 67 | 68 | ------- 69 | 70 | ## Contributing to this project 71 | 72 | As with most open source projects, pull requests for bug fixes, and new functionality are always welcome. 73 | 74 | Prerequisites 75 | 76 | - Node `5.x.x`+ 77 | 78 | Fork this repo and clone a local copy of your fork. 79 | 80 | Install dependencies: 81 | 82 | ``` 83 | npm install 84 | ``` 85 | 86 | Create necessary application bundle from source by running: 87 | 88 | ``` 89 | make install 90 | ``` 91 | 92 | Watch the `src` and `resources` directories and recompile when changes are made by running: 93 | 94 | ``` 95 | make watch 96 | ``` 97 | 98 | `make install` and `make watch` will copy the application bundle to the default Sketch plugins location `~/Library/Application Support/com.bohemiancoding.sketch3/Plugins/` as `Day Player.sketchplugin`. 99 | 100 | See the `Makefile` for further details on the build process. 101 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Day Player", 3 | "description": "A Plugin for creating images from placeholder services.", 4 | "author": "Tyler Gaw", 5 | "homepage": "https://github.com/tylergaw/day-player", 6 | "version": "3.0.0", 7 | "identifier": "com.sketchapp.tylergaw.day-player", 8 | "compatibleVersion": 41, 9 | "commands" : [ 10 | { 11 | "name": "Unsplash.it", 12 | "script": "index.cocoascript", 13 | "handler": "unsplashIt", 14 | "identifier": "unsplash-it" 15 | }, 16 | { 17 | "name": "Fill Murray", 18 | "script": "index.cocoascript", 19 | "handler": "fillMurray", 20 | "identifier": "fill-murray" 21 | }, 22 | { 23 | "name": "Place Cage", 24 | "script": "index.cocoascript", 25 | "handler": "placeCage", 26 | "identifier": "place-cage" 27 | }, 28 | { 29 | "name": "Placehold.it", 30 | "script": "index.cocoascript", 31 | "handler": "placeHoldIt", 32 | "identifier": "place-hold-it" 33 | }, 34 | { 35 | "name": "Placekitten", 36 | "script": "index.cocoascript", 37 | "handler": "placeKitten", 38 | "identifier": "place-kitten" 39 | } 40 | ], 41 | "menu": { 42 | "title": "Day Player", 43 | "items": [ 44 | "unsplash-it", 45 | "place-hold-it", 46 | "fill-murray", 47 | "place-cage", 48 | "place-kitten" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "day-player-plugin-sketchapp", 3 | "version": "3.0.0", 4 | "description": "A Sketch Plugin for creating placeholder images", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/tylergaw/day-player.git" 8 | }, 9 | "scripts": { 10 | "test": "make lint" 11 | }, 12 | "keywords": [ 13 | "sketch", 14 | "placeholder", 15 | "images" 16 | ], 17 | "author": "Tyler Gaw", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/tylergaw/day-player/issues" 21 | }, 22 | "homepage": "https://github.com/tylergaw/day-player#readme", 23 | "devDependencies": { 24 | "eslint": "3.8.0", 25 | "pre-commit": "1.1.3", 26 | "prompt": "1.0.0", 27 | "watch": "1.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/fillmurray.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/fillmurray.icns -------------------------------------------------------------------------------- /resources/lorempixel.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/lorempixel.icns -------------------------------------------------------------------------------- /resources/placecage.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/placecage.icns -------------------------------------------------------------------------------- /resources/placeholdit.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/placeholdit.icns -------------------------------------------------------------------------------- /resources/placekitten.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/placekitten.icns -------------------------------------------------------------------------------- /resources/unsplashit.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergaw/day-player/357566230d3fa6423449cd948cb24e80f8663132/resources/unsplashit.icns -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint-disable strict, no-console */ 3 | 'use strict'; 4 | 5 | const version = require('../package.json').version; 6 | const prompt = require('prompt'); 7 | const exec = require('child_process').exec; 8 | const dryRun = process.env.DRY_RUN || false; 9 | const schema = { 10 | properties: { 11 | confirmation: { 12 | required: true, 13 | pattern: /^(y|n|yes|no)+$/ig, 14 | description: `You are about to release version ${version}, is that OK? (yes|no)` 15 | } 16 | } 17 | }; 18 | 19 | const pushTag = tag => { 20 | const cmd = `git push origin ${tag}`; 21 | 22 | exec(cmd, (error, stdout, stderr) => { 23 | console.log('Tag pushed to origin', tag); 24 | if (error !== null) { 25 | console.log(stderr); 26 | } 27 | }); 28 | }; 29 | 30 | const createTag = n => { 31 | const cmd = `git tag -a ${n} -m "Releasing version: ${n}"`; 32 | 33 | if (dryRun) { 34 | console.log('Pretending to create new tag', n); 35 | } else { 36 | exec(cmd, (error, stdout, stderr) => { 37 | console.log('New git tag created', n); 38 | pushTag(n); 39 | if (error !== null) { 40 | console.log(stderr); 41 | } 42 | }); 43 | } 44 | }; 45 | 46 | prompt.start(); 47 | prompt.get(schema, (err, result) => { 48 | if (err) { 49 | throw new Error(err); 50 | } 51 | 52 | const res = result.confirmation.toLowerCase(); 53 | 54 | if (res === 'y' || res === 'yes') { 55 | createTag(version); 56 | } else { 57 | console.log('Release aborted'); 58 | process.exit(0); 59 | } 60 | 61 | return true; 62 | }); 63 | -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Alert A facade for NSAlert. Includes a number of convenience methods and a 3 | * chainable interface. 4 | * 5 | * @param {Object} props Options for building the Alert and handling user input 6 | * @return {Object} alert 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | const Alert = function(props) { 10 | // eslint-disable-next-line no-unused-vars 11 | const propTypes = { 12 | message: String, 13 | info: String, 14 | iconUrl: String, 15 | onConfirm: Function, 16 | onCancel: Function 17 | }; 18 | 19 | const alert = { 20 | views: [], 21 | el: NSAlert.alloc().init(), 22 | is: function(type) { 23 | return type === 'alert'; 24 | } 25 | }; 26 | 27 | const buttons = props.buttons || ['OK', 'Cancel']; 28 | 29 | alert.layout = function() { 30 | var containerHeight = 1; 31 | const container = NSView.alloc().initWithFrame( 32 | NSMakeRect(25, 100, 350, containerHeight) 33 | ); 34 | 35 | alert.views.forEach(function(view) { 36 | const el = view.el; 37 | const bounds = el.bounds(); 38 | bounds.origin.y = containerHeight; 39 | containerHeight += bounds.size.height + 8; 40 | el.setFrame(bounds); 41 | container.addSubview(el); 42 | }); 43 | 44 | const containerFrame = container.frame(); 45 | containerFrame.size.height = containerHeight; 46 | container.setFrame(containerFrame); 47 | 48 | alert.el.setAccessoryView(container); 49 | }; 50 | 51 | alert.runModal = function() { 52 | // Call layout before running opening the modal to account for any 53 | // dynamic layout 54 | alert.layout(); 55 | 56 | const onConfirm = props.onConfirm || function() {}; 57 | const onCancel = props.onCancel || function() {}; 58 | const res = parseInt(alert.el.runModal(), 10); 59 | const resHandler = (res === 1000) ? onConfirm : onCancel; 60 | resHandler(alert); 61 | 62 | return alert; 63 | }; 64 | 65 | /** 66 | * append Adds an element or elements to the Alert 67 | * 68 | * @param {Object|Array} newEl Either a single element or an array of elements. 69 | * @return {Object} alert 70 | */ 71 | alert.append = function(newEl) { 72 | const newViews = Array.isArray(newEl) ? newEl.reverse() : [newEl]; 73 | 74 | newViews.forEach(function(el) { 75 | alert.views.push(el); 76 | }); 77 | 78 | return alert; 79 | }; 80 | 81 | if (buttons.length) { 82 | for (var i = 0; i < buttons.length; i += 1) { 83 | alert.el.addButtonWithTitle(buttons[i]); 84 | } 85 | } 86 | 87 | if (props.message) { 88 | alert.el.setMessageText(props.message); 89 | } 90 | 91 | if (props.info) { 92 | alert.el.setInformativeText(props.info); 93 | } 94 | 95 | if (props.iconUrl) { 96 | alert.el.setIcon(NSImage.alloc().initByReferencingURL(props.iconUrl)); 97 | } 98 | 99 | return alert; 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/Label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Label A facade for NSTextField without any editable styling. Includes a 3 | * number of convenience methods and a chainable interface. 4 | * 5 | * @param {Object} props Options for building the Label 6 | * @return {Object} label 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | const Label = function(props) { 10 | // eslint-disable-next-line no-unused-vars 11 | const propTypes = { 12 | frame: Object, 13 | value: String 14 | }; 15 | 16 | /** 17 | * createLabel Internal method for creating new NSTextField 18 | * 19 | * @return {Object} NSTextField 20 | */ 21 | const createLabel = function(frame) { 22 | const f = frame || { 23 | x: 4, 24 | y: 100, 25 | width: 350, 26 | height: 16 27 | }; 28 | 29 | const textField = NSTextField.alloc().initWithFrame( 30 | NSMakeRect(f.x, f.y, f.width, f.height)); 31 | 32 | textField.setDrawsBackground(false); 33 | textField.setEditable(false); 34 | textField.setBezeled(false); 35 | textField.setSelectable(true); 36 | 37 | return textField; 38 | }; 39 | 40 | const label = { 41 | el: createLabel(props.frame), 42 | is: function(type) { 43 | return type === 'label'; 44 | } 45 | }; 46 | 47 | /** 48 | * setStringValue Sets the string value for the label. 49 | * 50 | * @param {String} value 51 | * @return {Object} label 52 | */ 53 | label.setStringValue = function(value) { 54 | label.el.setStringValue(value); 55 | return label; 56 | }; 57 | 58 | if (props.value) { 59 | label.setStringValue(props.value); 60 | } 61 | 62 | return label; 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/PopUpButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PopUpButton A facade for NSPopUpButton. Includes a number of convenience 3 | * methods and a chainable interface. 4 | * 5 | * @param {Object} props Options for building the PopUpButton 6 | * @return {Object} popUpButton 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | const PopUpButton = function(props) { 10 | // eslint-disable-next-line no-unused-vars 11 | const propTypes = { 12 | items: Array 13 | }; 14 | 15 | const popUpButton = { 16 | // NOTE: The documented signature is initWithFrame:pullsDown: but that 17 | // is undefined here. Think that's maybe a difference in CocoaScript? 18 | el: NSPopUpButton.alloc().initWithFrame( 19 | NSMakeRect(25, 100, 350, 28) 20 | ), 21 | is: function(type) { 22 | return type === 'select'; 23 | }, 24 | name: props.name || '', 25 | val: function() { 26 | return this.el.titleOfSelectedItem(); 27 | } 28 | }; 29 | 30 | if (props.items) { 31 | popUpButton.el.addItemsWithTitles(props.items); 32 | } 33 | 34 | return popUpButton; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/TextField.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TextField A facade for NSTextField. Includes a number of convenience 3 | * methods and a chainable interface. 4 | * 5 | * @param {Object} props Options for building the TextField 6 | * @return {Object} textField 7 | */ 8 | // eslint-disable-next-line no-unused-vars 9 | const TextField = function(props) { 10 | // eslint-disable-next-line no-unused-vars 11 | const propTypes = { 12 | frame: Object, 13 | value: String 14 | }; 15 | 16 | /** 17 | * createTextField Internal method for creating new NSTextField 18 | * 19 | * @return {Object} NSTextField 20 | */ 21 | const createTextField = function(frame) { 22 | const f = frame || { 23 | x: 25, 24 | y: 100, 25 | width: 350, 26 | height: 24 27 | }; 28 | 29 | return NSTextField.alloc().initWithFrame( 30 | NSMakeRect(f.x, f.y, f.width, f.height)); 31 | }; 32 | 33 | const textField = { 34 | el: createTextField(props.frame), 35 | is: function(type) { 36 | return type === 'input'; 37 | }, 38 | name: props.name || '', 39 | val: function() { 40 | return this.el.stringValue(); 41 | } 42 | }; 43 | 44 | /** 45 | * setStringValue Sets the string value for the textField. 46 | * 47 | * @param {String} value 48 | * @return {Object} textField 49 | */ 50 | textField.setStringValue = function(value) { 51 | textField.el.setStringValue(value); 52 | return textField; 53 | }; 54 | 55 | if (props.value) { 56 | textField.setStringValue(props.value); 57 | } 58 | 59 | return textField; 60 | }; 61 | -------------------------------------------------------------------------------- /src/handlers/fillMurray.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const fillMurray = createPluginHandler(function(props) { 3 | const GREY_TITLE = 'Black & white'; 4 | const initOpts = props.newImgFrame; 5 | 6 | const elements = [ 7 | new Label({ 8 | value: 'Width:' 9 | }), 10 | new TextField({ 11 | name: 'width', 12 | value: initOpts.width 13 | }), 14 | new Label({ 15 | value: 'Height:' 16 | }), 17 | new TextField({ 18 | name: 'height', 19 | value: initOpts.height 20 | }), 21 | new Label({ 22 | value: 'Type:' 23 | }), 24 | new PopUpButton({ 25 | name: 'type', 26 | items: ['Color', GREY_TITLE] 27 | }) 28 | ]; 29 | 30 | const onConfirm = createConfirmHandler({ 31 | api: props.api, 32 | group: props.target.group, 33 | host: 'fillmurray.com', 34 | initOpts: initOpts, 35 | urlBuilder: function(parts) { 36 | const base = `${parts.protocol}${parts.host}`; 37 | // Cast as a string because the value coming back is an object 38 | const type = (String(parts.allParts.type) === GREY_TITLE) ? '/g' : ''; 39 | return `${base}${type}/${parts.width}/${parts.height}`; 40 | } 41 | }); 42 | 43 | new Alert({ 44 | message: 'Fill Murray Options', 45 | info: 'Customize the wonderful Bill Murray image that will be created.', 46 | iconUrl: props.api.resourceNamed('fillmurray.icns'), 47 | onConfirm: onConfirm 48 | }).append(elements).runModal(); 49 | }); 50 | -------------------------------------------------------------------------------- /src/handlers/placeCage.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const placeCage = createPluginHandler(function(props) { 3 | const GREY_TITLE = 'Black & white'; 4 | const initOpts = props.newImgFrame; 5 | 6 | const elements = [ 7 | new Label({ 8 | value: 'Width:' 9 | }), 10 | new TextField({ 11 | name: 'width', 12 | value: initOpts.width 13 | }), 14 | new Label({ 15 | value: 'Height:' 16 | }), 17 | new TextField({ 18 | name: 'height', 19 | value: initOpts.height 20 | }), 21 | new Label({ 22 | value: 'Type:' 23 | }), 24 | new PopUpButton({ 25 | name: 'type', 26 | items: ['Color', GREY_TITLE] 27 | }) 28 | ]; 29 | 30 | const onConfirm = createConfirmHandler({ 31 | api: props.api, 32 | group: props.target.group, 33 | host: 'placecage.com', 34 | initOpts: initOpts, 35 | urlBuilder: function(parts) { 36 | const base = `${parts.protocol}${parts.host}`; 37 | // Cast as a string because the value coming back is an object 38 | const type = (String(parts.allParts.type) === GREY_TITLE) ? '/g' : ''; 39 | return `${base}${type}/${parts.width}/${parts.height}`; 40 | } 41 | }); 42 | 43 | new Alert({ 44 | message: 'Place Cage Options', 45 | info: 'Customize the image of the best actor of all time that will be created.', 46 | iconUrl: props.api.resourceNamed('placecage.icns'), 47 | onConfirm: onConfirm 48 | }).append(elements).runModal(); 49 | }); 50 | -------------------------------------------------------------------------------- /src/handlers/placeHoldIt.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const placeHoldIt = createPluginHandler(function(props) { 3 | const initOpts = props.newImgFrame; 4 | 5 | const elements = [ 6 | new Label({ 7 | value: 'Width:' 8 | }), 9 | new TextField({ 10 | name: 'width', 11 | value: initOpts.width 12 | }), 13 | new Label({ 14 | value: 'Height:' 15 | }), 16 | new TextField({ 17 | name: 'height', 18 | value: initOpts.height 19 | }), 20 | new Label({ 21 | value: 'Text:' 22 | }), 23 | new TextField({ 24 | name: 'text', 25 | value: 'placeholder' 26 | }), 27 | new Label({ 28 | value: 'Background color:' 29 | }), 30 | new TextField({ 31 | name: 'bgColor', 32 | value: 'aeaeae' 33 | }), 34 | new Label({ 35 | value: 'Text color:' 36 | }), 37 | new TextField({ 38 | name: 'color', 39 | value: '949494' 40 | }) 41 | ]; 42 | 43 | const onConfirm = createConfirmHandler({ 44 | api: props.api, 45 | group: props.target.group, 46 | host: 'placehold.it', 47 | initOpts: initOpts, 48 | urlBuilder: function(parts) { 49 | var url = `${parts.protocol}${parts.host}/${parts.width}x${parts.height}/${parts.allParts.bgColor}/${parts.allParts.color}`; 50 | const text = parts.allParts.text; 51 | 52 | if (text.length) { 53 | url += `?text=${encodeURIComponent(text)}`; 54 | } 55 | 56 | return url; 57 | } 58 | }); 59 | 60 | new Alert({ 61 | message: 'Placehold.it Options', 62 | info: 'Customize the image that will be created.', 63 | iconUrl: props.api.resourceNamed('placeholdit.icns'), 64 | onConfirm: onConfirm 65 | }).append(elements).runModal(); 66 | }); 67 | -------------------------------------------------------------------------------- /src/handlers/placeKitten.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const placeKitten = createPluginHandler(function(props) { 3 | const GREY_TITLE = 'Black & white'; 4 | const initOpts = props.newImgFrame; 5 | 6 | const elements = [ 7 | new Label({ 8 | value: 'Width:' 9 | }), 10 | new TextField({ 11 | name: 'width', 12 | value: initOpts.width 13 | }), 14 | new Label({ 15 | value: 'Height:' 16 | }), 17 | new TextField({ 18 | name: 'height', 19 | value: initOpts.height 20 | }), 21 | new Label({ 22 | value: 'Type:' 23 | }), 24 | new PopUpButton({ 25 | name: 'type', 26 | items: ['Color', GREY_TITLE] 27 | }) 28 | ]; 29 | 30 | const onConfirm = createConfirmHandler({ 31 | api: props.api, 32 | group: props.target.group, 33 | host: 'placekitten.com', 34 | initOpts: initOpts, 35 | urlBuilder: function(parts) { 36 | const base = `${parts.protocol}${parts.host}`; 37 | // Cast as a string because the value coming back is an object 38 | const type = (String(parts.allParts.type) === GREY_TITLE) ? '/g' : ''; 39 | return `${base}${type}/${parts.width}/${parts.height}`; 40 | } 41 | }); 42 | 43 | new Alert({ 44 | message: 'Place Kitten Options', 45 | info: 'Customize the kewl kitten image that will be created.', 46 | iconUrl: props.api.resourceNamed('placekitten.icns'), 47 | onConfirm: onConfirm 48 | }).append(elements).runModal(); 49 | }); 50 | -------------------------------------------------------------------------------- /src/handlers/unsplashIt.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const unsplashIt = createPluginHandler(function(props) { 3 | const GREY_TITLE = 'Black & white'; 4 | const BLUR_TITLE = 'Blurry'; 5 | const initOpts = props.newImgFrame; 6 | 7 | const elements = [ 8 | new Label({ 9 | value: 'Width:' 10 | }), 11 | new TextField({ 12 | name: 'width', 13 | value: initOpts.width 14 | }), 15 | new Label({ 16 | value: 'Height:' 17 | }), 18 | new TextField({ 19 | name: 'height', 20 | value: initOpts.height 21 | }), 22 | new Label({ 23 | value: 'Type:' 24 | }), 25 | new PopUpButton({ 26 | name: 'type', 27 | items: ['Color', GREY_TITLE] 28 | }), 29 | new Label({ 30 | value: 'Sharpness:' 31 | }), 32 | new PopUpButton({ 33 | name: 'blur', 34 | items: ['Focused', BLUR_TITLE] 35 | }) 36 | ]; 37 | 38 | const onConfirm = createConfirmHandler({ 39 | api: props.api, 40 | group: props.target.group, 41 | host: 'unsplash.it', 42 | initOpts: initOpts, 43 | urlBuilder: function(parts) { 44 | const base = `${parts.protocol}${parts.host}`; 45 | // Cast as a string because the value coming back is an object 46 | const type = (String(parts.allParts.type) === GREY_TITLE) ? '/g' : ''; 47 | const blur = (String(parts.allParts.blur) === BLUR_TITLE) ? '&blur' : ''; 48 | return `${base}${type}/${parts.width}/${parts.height}?random${blur}`; 49 | } 50 | }); 51 | 52 | new Alert({ 53 | message: 'Unsplash.it Options', 54 | info: 'Customize the image that will be created.', 55 | iconUrl: props.api.resourceNamed('unsplashit.icns'), 56 | onConfirm: onConfirm 57 | }).append(elements).runModal(); 58 | }); 59 | -------------------------------------------------------------------------------- /src/index.cocoascript: -------------------------------------------------------------------------------- 1 | @import './utils.js'; 2 | @import './components/Label.js'; 3 | @import './components/TextField.js'; 4 | @import './components/PopUpButton.js'; 5 | @import './components/Alert.js'; 6 | @import './handlers/fillMurray.js'; 7 | @import './handlers/placeCage.js'; 8 | @import './handlers/placeHoldIt.js'; 9 | @import './handlers/placeKitten.js'; 10 | @import './handlers/unsplashIt.js'; 11 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * createPluginHandler - Creates a handler function that takes the context 3 | * parameter required by Sketch plugins and enhances it with a number of 4 | * helpful properties. 5 | * 6 | * @param {Function} func 7 | * @return {Function} A function suitable to be used as a Plugin handler 8 | */ 9 | // eslint-disable-next-line no-unused-vars 10 | const createPluginHandler = function(func) { 11 | return function(context) { 12 | const api = context.api(); 13 | const document = api.selectedDocument; 14 | const selection = document.selectedLayers; 15 | const target = (selection.isEmpty) ? {group: document.selectedPage} : 16 | getTargetLayer(selection, context); 17 | 18 | const newImgFrame = (target.frame) ? { 19 | x: target.frame.x, 20 | y: target.frame.y, 21 | width: target.frame.width, 22 | height: target.frame.height 23 | } : { 24 | x: 0, 25 | y: 0, 26 | width: 400, 27 | height: 300 28 | }; 29 | 30 | const props = { 31 | api: api, 32 | document: document, 33 | page: document.selectedPage, 34 | selection: selection, 35 | target: target, 36 | newImgFrame: newImgFrame 37 | }; 38 | 39 | func(props); 40 | }; 41 | }; 42 | 43 | /** 44 | * createConfirmHandler - Creates a standard Day Player alert confirmation handler 45 | * function that takes a single param, the Alert being used at the time. 46 | * 47 | * @param {Object} props Configuration for the handler 48 | * @param {Function} func Optional function to run after all standard bits 49 | * @return {Function} A function suitable to be used as an Alert.onConfirm 50 | */ 51 | // eslint-disable-next-line no-unused-vars 52 | const createConfirmHandler = function(props, func) { 53 | const urlBuilder = props.urlBuilder || function(parts) { 54 | return `${parts.protocol}${parts.host}/${parts.width}/${parts.height}`; 55 | }; 56 | 57 | return function(alert) { 58 | const userChosenOptions = alert.views.filter(function(view) { 59 | return view.is('input') || view.is('select'); 60 | }).reduce(function(obj, view, i) { 61 | obj[view.name] = view.val(); 62 | return obj; 63 | }, {}); 64 | 65 | const opts = Object.assign({}, props.initOpts, userChosenOptions); 66 | const sizeDisplay = `${opts.width}x${opts.height}`; 67 | props.api.message(`Creating a ${sizeDisplay}px image from ${props.host}...`); 68 | 69 | const url = urlBuilder({ 70 | protocol: 'https://', 71 | host: props.host, 72 | width: opts.width, 73 | height: opts.height, 74 | allParts: opts 75 | }); 76 | 77 | const img = props.group.newImage({ 78 | frame: new props.api.Rectangle(opts.x, opts.y, opts.width, opts.height), 79 | name: `${props.host}-${sizeDisplay}` 80 | }); 81 | 82 | img.imageURL = NSURL.URLWithString(url); 83 | }; 84 | }; 85 | 86 | /** 87 | * Determine if appending image to an artboard, group, layer, or none. 88 | * 89 | * @param {Selection} selection The current Selection 90 | * @param {Object} context The current context 91 | * @return {Object} target The selected layers 92 | */ 93 | const getTargetLayer = function(selection, context) { 94 | var layers = []; 95 | 96 | selection.iterate(function(layer) { 97 | layers.push(layer); 98 | }); 99 | 100 | // TODO: Currently we'll only create an image for a single layer selected. We 101 | // only grab the first one off the list of selections. In the future, we should 102 | // create an image for each selection. 103 | const firstLayer = layers[0]; 104 | 105 | var target = { 106 | selection: selection, 107 | frame: (firstLayer.isShape) ? firstLayer.frame : null 108 | }; 109 | 110 | if (firstLayer.isGroup) { 111 | target.group = firstLayer; 112 | } else { 113 | // FIXME: If the user has selected layer(s) that do not count as a group; 114 | // (shape, text, line, etc) we need to set the target.group to the parent 115 | // group of the selected layer(s). 116 | // The JS API does not currently offer a way to access the parentGroup of 117 | // a Layer object. To get around this, we use the low-level _object and 118 | // parentGroup() method. 119 | // Doing so gives us an unwrapped Sketch Object. We must use wrapObject to 120 | // ensure we return a wrapped object for the target.group. 121 | // In the future, it would be good to have a Layer.parentGroup getter. 122 | const api = context.api(); 123 | const document = api.selectedDocument; 124 | target.group = api.wrapObject(firstLayer._object.parentGroup(), document); 125 | } 126 | 127 | return target; 128 | }; 129 | 130 | /** 131 | * dumpObj - Introspect objects 132 | * @param {Object} obj 133 | */ 134 | // eslint-disable-next-line no-unused-vars 135 | const dumpObj = function(obj) { 136 | log('------------------------'); 137 | log('## Dumping object ' + obj); 138 | log('------------------------'); 139 | log('obj.properties:'); 140 | log(obj.class().mocha().properties()); 141 | log('obj.propertiesWithAncestors:'); 142 | log(obj.class().mocha().propertiesWithAncestors()); 143 | log('obj.classMethods:'); 144 | log(obj.class().mocha().classMethods()); 145 | log('obj.classMethodsWithAncestors:'); 146 | log(obj.class().mocha().classMethodsWithAncestors()); 147 | log('obj.instanceMethods:'); 148 | log(obj.class().mocha().instanceMethods()); 149 | log('obj.instanceMethodsWithAncestors:'); 150 | log(obj.class().mocha().instanceMethodsWithAncestors()); 151 | log('obj.protocols:'); 152 | log(obj.class().mocha().protocols()); 153 | log('obj.protocolsWithAncestors:'); 154 | log(obj.class().mocha().protocolsWithAncestors()); 155 | log('obj.treeAsDictionary():'); 156 | log(obj.treeAsDictionary()); 157 | }; 158 | --------------------------------------------------------------------------------