├── Fontily.sketchplugin └── Contents │ └── Sketch │ ├── FontilyDialog.cocoascript │ ├── Layout.cocoascript │ ├── LayoutView.cocoascript │ ├── fontily.cocoascript │ └── manifest.json └── README.md /Fontily.sketchplugin/Contents/Sketch/FontilyDialog.cocoascript: -------------------------------------------------------------------------------- 1 | function FontilyDialog(title) { 2 | this.alert = NSAlert.alloc().init(); 3 | this.alert.setAlertStyle(NSWarningAlertStyle); 4 | if (title) { 5 | this.setMessageText(title); 6 | } 7 | }; 8 | FontilyDialog.prototype.setMessageText = function (text) { 9 | this.alert.setMessageText(text); 10 | }; 11 | FontilyDialog.prototype.setInformativeText = function (text) { 12 | this.alert.setInformativeText(text); 13 | }; 14 | FontilyDialog.prototype.addButtonWithTitle = function (text) { 15 | this.alert.addButtonWithTitle(text); 16 | }; 17 | FontilyDialog.prototype.addAccessoryView = function (view) { 18 | this.alert.setAccessoryView(view); 19 | }; 20 | FontilyDialog.prototype.runModal = function () { 21 | return this.alert.runModal(); 22 | }; -------------------------------------------------------------------------------- /Fontily.sketchplugin/Contents/Sketch/Layout.cocoascript: -------------------------------------------------------------------------------- 1 | var Layout = { 2 | setPos: function (view, x, y) { 3 | var f = view.frame(); 4 | view.setFrame(NSMakeRect(x == undefined ? f.origin.x : x, y == undefined ? f.origin.y : y, f.size.width, f.size.height)); 5 | return view; 6 | }, 7 | setSize: function (view, w, h) { 8 | var f = view.frame(); 9 | view.setFrame(NSMakeRect(f.origin.x, f.origin.y, w == undefined ? f.size.width : w, h == undefined ? f.size.height : h)); 10 | return view; 11 | }, 12 | measureView: function (view) { 13 | return view.intrinsicContentSize(); 14 | }, 15 | /** 16 | * Creates NSTextView 17 | * @param text 18 | * @param size 19 | * @param color 20 | * @param rect_height 21 | * @returns {*} 22 | */ 23 | createTextView: function (text, size, color, rect_height) { 24 | var tf = NSTextView.alloc().initWithFrame(NSMakeRect(0, 0, 300, rect_height == undefined || rect_height == "auto" ? 16 : rect_height)); 25 | 26 | if (typeof color != "undefined") { 27 | tf.setTextColor(color); 28 | } 29 | if (typeof size != "undefined") { 30 | tf.setFont(NSFont.systemFontOfSize(size)); 31 | } 32 | tf.setString(text); 33 | return tf; 34 | }, 35 | 36 | 37 | /** 38 | * Creates NSTextView 39 | * @param text 40 | * @param size 41 | * @param color 42 | * @param rect_height 43 | * @returns {*} 44 | */ 45 | createTextInput: function (rows, placeholder) { 46 | if (typeof rows == "undefined") { 47 | rows = 1; 48 | } 49 | var multiline = rows > 1; 50 | var tf = null; 51 | if (multiline) { 52 | tf = NSTextView.alloc().initWithFrame(NSMakeRect(0, 0, 300, rows * 16)); 53 | } else { 54 | tf = NSTextField.alloc().initWithFrame(NSMakeRect(0, 0, 300, 24)); 55 | if (typeof placeholder == "string") { 56 | tf.cell().setPlaceholderString(placeholder); 57 | } 58 | } 59 | if (multiline) { 60 | return {input: tf, scroller: Layout.createScroll("", undefined, undefined, 100, tf)}; 61 | } else { 62 | return {input: tf, scroller: null}; 63 | } 64 | }, 65 | 66 | /** 67 | * Creates NSScrollView with text layers' names inside it. 68 | * @param layers 69 | * @returns {*} 70 | */ 71 | createFontList: function (layers, useScroll) { 72 | var text = []; 73 | for (var index = 0; index < layers.length; ++index) { 74 | text.push("Layer #" + (index + 1) + ": " + layers[index].name()); 75 | } 76 | text.push(''); 77 | text = text.join("\r\n"); 78 | 79 | var tf = Layout.createTextView(text, 12, undefined, 150); 80 | tf.setEditable(false); 81 | Layout.setSize(tf, undefined, layers.length * 15); 82 | 83 | if (useScroll) { 84 | return Layout.createScroll(text, 12, undefined, 150, tf); 85 | } else { 86 | tf.setTextColor(colorFromRGB(100, 100, 100)); 87 | 88 | return tf; 89 | } 90 | }, 91 | 92 | /** 93 | * Creates custom NSTextField 94 | * @param text 95 | * @param size - font size 96 | * @param color - nscolor 97 | * @param rect_height 98 | * @returns {*} 99 | */ 100 | createLabel: function (text, size, color, rect_width, rect_height) { 101 | if (rect_width == undefined) { 102 | rect_width = 300; 103 | } 104 | if (rect_height == undefined) { 105 | rect_height = 16; 106 | } 107 | var tf = NSTextField.alloc().initWithFrame(NSMakeRect(0, 0, rect_width, rect_height == "auto" ? 16 : rect_height)); 108 | tf.setDrawsBackground(false); 109 | tf.setEditable(false); 110 | tf.setBezeled(false); 111 | tf.setSelectable(false); 112 | if (typeof color != "undefined") { 113 | tf.setTextColor(color); 114 | } 115 | if (typeof size != "undefined") { 116 | tf.setFont(NSFont.systemFontOfSize(size)); 117 | } 118 | tf.setStringValue(text); 119 | 120 | if (rect_height == "auto") { 121 | //handle wrapping 122 | //supports only 2 lines 123 | if (Layout.measureView(tf).width > rect_width) { 124 | Layout.setSize(tf, undefined, 32); 125 | } else { 126 | Layout.setSize(tf, undefined, Layout.measureView(tf).height); 127 | } 128 | } 129 | return tf; 130 | }, 131 | 132 | /** 133 | * Creates NSScrollView with textview inside it. 134 | * @param text 135 | * @param size 136 | * @param color 137 | * @param rect_height 138 | * @returns {*} 139 | */ 140 | createScroll: function (text, size, color, rect_height, tf) { 141 | var scrollview = NSScrollView.alloc().initWithFrame(NSMakeRect(0, 0, 300, 100)); 142 | var contentSize = scrollview.contentSize(); 143 | 144 | //scrollview.setBorderType(NSGrooveBorder); 145 | scrollview.setBorderType(NSBezelBorder); 146 | scrollview.setHasVerticalScroller(false); 147 | scrollview.setHasHorizontalScroller(false); 148 | if (typeof tf == "undefined") { 149 | tf = Layout.createTextView(text, size, color, rect_height); 150 | } 151 | tf.setMinSize(NSMakeSize(0, contentSize.height)); 152 | 153 | tf.setVerticallyResizable(false); 154 | tf.setHorizontallyResizable(false); 155 | tf.setAutoresizingMask(NSViewWidthSizable); 156 | scrollview.setDocumentView(tf); 157 | return scrollview; 158 | }, 159 | 160 | /** 161 | * @returns {*} 162 | */ 163 | createMainScrollView: function (width, height, hasBorder) { 164 | var scrollview = NSScrollView.alloc().initWithFrame(NSMakeRect(0, 0, width, height)); 165 | if (hasBorder) { 166 | scrollview.setBorderType(NSBezelBorder); 167 | } else { 168 | scrollview.setBorderType(NSNoBorder); 169 | } 170 | scrollview.setHasVerticalScroller(true); 171 | scrollview.setHasHorizontalScroller(false); 172 | scrollview.setDrawsBackground(true); 173 | scrollview.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable); 174 | return scrollview; 175 | }, 176 | 177 | 178 | /** 179 | * Create Select Box for dialog window 180 | * @param {Array} options Options for the select 181 | * @param {Int} selectedItem Default selected item 182 | * @return {NSComboBox} Complete select box 183 | */ 184 | createSelect: function (options, selectedItem) { 185 | var selectedItemIndex = 0; 186 | for (var i = 0; i < options.length; ++i) { 187 | if (options[i] == selectedItem) { 188 | selectedItemIndex = i; 189 | break; 190 | } 191 | } 192 | var select = NSComboBox.alloc().initWithFrame(NSMakeRect(2, 0, 200, 25)); 193 | select.addItemsWithObjectValues(options); 194 | select.selectItemAtIndex(selectedItemIndex); 195 | select.setNumberOfVisibleItems(20); 196 | 197 | return select; 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /Fontily.sketchplugin/Contents/Sketch/LayoutView.cocoascript: -------------------------------------------------------------------------------- 1 | function LayoutView(x, y, width, height) { 2 | this.prevFrame = NSMakeRect(0, 0, 0, 0); 3 | this._views = []; 4 | var _view = NSView.alloc().initWithFrame(NSMakeRect(x, y, width, height == undefined ? 0 : height)); 5 | this.view = function () { 6 | return _view; 7 | }; 8 | return this; 9 | } 10 | LayoutView.prototype.add = function (view, layout) { 11 | if (view instanceof LayoutView) { 12 | return this.add(view.view(), layout); 13 | } 14 | var f = view.frame(); 15 | var newFrame = f; 16 | if (layout === undefined) { 17 | //position it under previous element, and resize parent 18 | this.increaseHeight(f.size.height); 19 | newFrame = NSMakeRect(f.origin.x, 0, f.size.width, f.size.height); 20 | view.setFrame(newFrame); 21 | } else if (layout === "absolute/resize") { 22 | //position it absolutely, and resize parent if necessary 23 | var delta = f.size.height - this.height(); 24 | if (delta > 0) { 25 | this.increaseHeight(delta); 26 | } 27 | } else if (layout === "absolute") { 28 | //just position it absolutely without resizing 29 | } 30 | this.prevFrame = newFrame; 31 | this.view().addSubview(view); 32 | this._views.push(view); 33 | 34 | return this; 35 | }; 36 | LayoutView.prototype.addPadding = function (px) { 37 | //adds vertical padding at current position 38 | this.increaseHeight(px); 39 | }; 40 | LayoutView.prototype.increaseHeight = function (delta) { 41 | var f = this.view().frame(); 42 | 43 | //resize main view 44 | this.view().setFrame(NSMakeRect(f.origin.x, f.origin.y, f.size.width, f.size.height + delta)); 45 | 46 | //translate each subview 47 | for (var i = 0; i < this._views.length; ++i) { 48 | var v = this._views[i]; 49 | f = v.frame(); 50 | v.setFrame(NSMakeRect(f.origin.x, f.origin.y + delta, f.size.width, f.size.height)); 51 | } 52 | if (this._views.length) { 53 | //update prevFrame val 54 | this.prevFrame = this._views[this._views.length - 1].frame(); 55 | } 56 | }; 57 | LayoutView.prototype.height = function () { 58 | if (this._views.length == 0) { 59 | return 0; 60 | } 61 | var f = this._views[0].frame(); 62 | return Math.abs(f.origin.y) + f.size.height; 63 | }; 64 | -------------------------------------------------------------------------------- /Fontily.sketchplugin/Contents/Sketch/fontily.cocoascript: -------------------------------------------------------------------------------- 1 | @import "FontilyDialog.cocoascript"; 2 | @import "Layout.cocoascript"; 3 | @import "LayoutView.cocoascript"; 4 | 5 | /** 6 | * Entry point for font replacer 7 | * @param context 8 | */ 9 | function replaceMissingFonts(context) { 10 | var result = findFontLayers(context); 11 | openFontilyDialog(context, result.fonts, result.count, true); 12 | } 13 | 14 | /** 15 | * Entry point for font lister 16 | * @param context 17 | */ 18 | function showFontLayers(context) { 19 | var result = findFontLayers(context); 20 | openFontilyDialog(context, result.fonts, result.count, false); 21 | } 22 | 23 | /** 24 | * Entry point for feature requests 25 | * @param context 26 | */ 27 | function requestFeature(context) { 28 | 29 | // Create the interface 30 | var emailInput = null, msgInput = null; 31 | var modal = createUserInterface(); 32 | // Show it and process the form 33 | handleAlertResponse(modal, modal.runModal()); 34 | 35 | function createUserInterface() { 36 | var userInterface = COSAlertWindow.new(); 37 | 38 | userInterface.setMessageText('Request new feature'); 39 | userInterface.setInformativeText("Didn't found what you were looking for? No problem! Write it down, and I will try to implement it in the nearest version of Fontily :)"); 40 | 41 | userInterface.addTextLabelWithValue("Contact Email (optional)"); 42 | userInterface.addAccessoryView(emailInput = Layout.createTextInput(1, "john.appleseed@sketchapp.com").input); 43 | 44 | var msg = Layout.createTextInput(5); 45 | msgInput = msg.input; 46 | userInterface.addTextLabelWithValue("Feature description"); 47 | userInterface.addAccessoryView(msg.scroller); 48 | 49 | userInterface.addButtonWithTitle('OK'); 50 | userInterface.addButtonWithTitle('Cancel'); 51 | 52 | emailInput.setNextKeyView(msgInput); 53 | 54 | return userInterface; 55 | } 56 | 57 | /** 58 | * Collect user input from alert window 59 | * @param {COSAlertWindow} alert The alert window 60 | * @param {Int} responseCode Alert window response code 61 | * @return {Object} Alert window results 62 | */ 63 | function handleAlertResponse(alert, responseCode) { 64 | if (responseCode == "1000") { 65 | var query = []; 66 | query.push(["email", encodeURIComponent(emailInput.stringValue())].join('=')); 67 | query.push(["msg", encodeURIComponent(msgInput.textStorage().string())].join('=')); 68 | var response = httpRequest("http://partyka.io/fontily/request.php?" + query.join('&')); 69 | 70 | var success; 71 | try { 72 | response = JSON.parse(response); 73 | success = response.success; 74 | } catch (e) { 75 | success = "no"; 76 | } 77 | var userInterface = COSAlertWindow.new(); 78 | 79 | userInterface.setMessageText('Request new feature'); 80 | var text = ""; 81 | if (success == "yes") { 82 | text = "Thank you for your input! Message has been sent."; 83 | } else { 84 | text = "There was a problem submitting your request, message has not been sent.\r\n\r\nTry again soon, or write me at maciek@partyka.io"; 85 | } 86 | userInterface.setInformativeText(text); 87 | 88 | userInterface.addButtonWithTitle('Close'); 89 | userInterface.runModal(); 90 | 91 | } 92 | return null; 93 | } 94 | } 95 | 96 | function httpRequest(queryURL) { 97 | var request = NSMutableURLRequest.new(); 98 | request.setHTTPMethod("GET"); 99 | request.setURL(NSURL.URLWithString(queryURL)); 100 | 101 | var error = NSError.new(); 102 | var responseCode = null; 103 | 104 | var oResponseData = NSURLConnection.sendSynchronousRequest_returningResponse_error_(request, responseCode, error); 105 | 106 | var dataString = NSString.alloc().initWithData_encoding_(oResponseData, NSUTF8StringEncoding); 107 | print("[" + Date.now() + "] " + dataString); 108 | return dataString; 109 | } 110 | /** 111 | * Iterates over all document, looking for text layers. Saves font, and checks whether it may be missing. 112 | * @param context 113 | * @returns {{fonts: {}, count: number}} 114 | */ 115 | function findFontLayers(context) { 116 | var doc = context.document; 117 | var pages = doc.pages(); 118 | var page, textLayer, foundFonts = {}, count = 0; 119 | 120 | var loop = pages.objectEnumerator(); 121 | while (page = loop.nextObject()) { 122 | var scope = page.children(); 123 | var predicate = NSPredicate.predicateWithFormat("className == %@", 'MSTextLayer'); 124 | var queryResult = scope.filteredArrayUsingPredicate(predicate); 125 | if (queryResult.count() > 0) { 126 | var loop2 = queryResult.objectEnumerator(); 127 | while (textLayer = loop2.nextObject()) { 128 | var text = textLayer.stringValue(); 129 | var font = textLayer.fontPostscriptName(); 130 | if (typeof foundFonts[font] == "undefined") { 131 | foundFonts[font] = {layers: [], isMissing: !textLayer.canScale()}; 132 | count += 1; 133 | } 134 | foundFonts[font].layers.push(textLayer); 135 | } 136 | } 137 | } 138 | return {fonts: foundFonts, count: count}; 139 | } 140 | 141 | /** 142 | * Get system fonts 143 | * @return {Array} 144 | */ 145 | function getFonts() { 146 | var fontManager = NSFontManager.sharedFontManager(); 147 | var fonts = []; 148 | var sys_fonts = fontManager.availableFonts(); 149 | //has to convert them to normal array, as {sys_fonts} is array-like object, and is persistent, so when modified, changes stay between runs of script 150 | for (var i = 0; i < sys_fonts.length; ++i) { 151 | fonts.push(sys_fonts[i]); 152 | } 153 | return fonts; 154 | } 155 | 156 | /** 157 | * Create NSColor 158 | * @param r 159 | * @param g 160 | * @param b 161 | * @returns {*} 162 | */ 163 | function colorFromRGB(r, g, b) { 164 | return NSColor.colorWithDeviceRed_green_blue_alpha_(r / 255.0, g / 255.0, b / 255.0, 1); 165 | } 166 | 167 | /** 168 | * Opens main dialog, which allows user to select which fonts to replace 169 | * @param context 170 | * @param textLayers 171 | * @param foundCount 172 | */ 173 | function openFontilyDialog(context, textLayers, foundCount, enableReplace) { 174 | var fontKeys = []; 175 | var inputs = []; 176 | var SELECT_FONT_LABEL = "Select font..."; 177 | // Create the interface 178 | var modal = createUserInterface(); 179 | // Show it and process the form 180 | handleAlertResponse(modal, modal.runModal()); 181 | 182 | function createUserInterface() { 183 | var modal = new FontilyDialog('Fontily ' + (enableReplace ? 'Replacer' : 'Listing')); 184 | 185 | if (foundCount == 0) { 186 | modal.setInformativeText('No fonts found.'); 187 | modal.addButtonWithTitle('OK'); 188 | } else { 189 | //fonts list 190 | var avail_fonts = getFonts(); 191 | avail_fonts.unshift(SELECT_FONT_LABEL); 192 | 193 | //'constraints' 194 | var sizes = { 195 | leftCol: {width: 225}, 196 | rightCol: {width: 300, marginLeft: 10, marginRight: 25}, 197 | scroll: {height: 300}, 198 | missingLabel: {height: 12, fontSize: 9} 199 | }; 200 | if (!enableReplace) { 201 | sizes.leftCol.width = 300; 202 | sizes.rightCol.width = 200; 203 | } 204 | //calculate modal width based on given constraints 205 | sizes.section = { 206 | spaceBetween: 20, 207 | width: 1 + sizes.leftCol.width + sizes.rightCol.width + sizes.rightCol.marginLeft + sizes.rightCol.marginRight 208 | }; 209 | 210 | var scrollView = Layout.createMainScrollView(sizes.section.width, sizes.scroll.height, false), 211 | scrollContent = new LayoutView(0, 0, sizes.section.width); 212 | 213 | var sectionCount = 0, countMissing = 0; 214 | var totalHeight = 0; 215 | var selectReplaceInput; 216 | for (var font_name in textLayers) { 217 | if (textLayers.hasOwnProperty(font_name) == false) { 218 | continue; 219 | } 220 | var textLayer = textLayers[font_name]; 221 | 222 | var fontHeader = new LayoutView(0, 0, sizes.section.width); 223 | var fontSection = new LayoutView(0, totalHeight, sizes.section.width); 224 | 225 | if (sectionCount > 0) { 226 | //add spacing between sections 227 | //if font is missing, add smaller padding ('missing label' already has some height) 228 | scrollContent.addPadding(textLayer.isMissing ? (sizes.section.spaceBetween - sizes.missingLabel.height) : sizes.section.spaceBetween); 229 | } 230 | 231 | if (textLayer.isMissing) { 232 | //missing label 233 | scrollContent.add(Layout.createLabel('Missing font! ', sizes.missingLabel.fontSize, colorFromRGB(255, 0, 0), null, sizes.missingLabel.height)); 234 | ++countMissing; 235 | } 236 | 237 | //Header section 238 | fontHeader.add(Layout.createLabel((enableReplace ? 'Change font for "' + font_name + '"' : 'Font "' + font_name + '"'), 12, textLayer.isMissing ? colorFromRGB(255, 0, 0) : undefined, sizes.leftCol.width, "auto"), "absolute/resize"); 239 | fontHeader.add(Layout.setPos(Layout.createLabel('Layers that use this font (' + textLayers[font_name].layers.length + ' in total)', 10, colorFromRGB(150, 150, 150)), sizes.leftCol.width + sizes.rightCol.marginLeft, -2), "absolute"); 240 | 241 | //Main section 242 | if (enableReplace) { 243 | selectReplaceInput = Layout.createSelect(avail_fonts, font_name); 244 | fontSection.add(selectReplaceInput); 245 | fontSection.add(Layout.setPos(Layout.createFontList(textLayers[font_name].layers, true), sizes.leftCol.width + sizes.rightCol.marginLeft, 0), "absolute/resize"); 246 | } else { 247 | fontSection.addPadding(5); 248 | fontSection.add(Layout.setPos(Layout.setSize(Layout.createFontList(textLayers[font_name].layers, false), sizes.section.width, undefined), 0, 0)); 249 | fontSection.addPadding(5); 250 | } 251 | //Add to main scroll view 252 | scrollContent.add(fontHeader); 253 | scrollContent.add(fontSection); 254 | 255 | ++sectionCount; 256 | totalHeight += fontSection.height(); 257 | 258 | //save for later 259 | if (enableReplace) { 260 | inputs.push(selectReplaceInput); 261 | selectReplaceInput = null; 262 | } 263 | fontKeys.push(font_name); 264 | } 265 | 266 | modal.setInformativeText('Found ' + foundCount + ' fonts' + (countMissing > 0 ? ', ' + countMissing + ' missing' : '')); 267 | modal.addButtonWithTitle(enableReplace ? 'Replace' : 'OK'); 268 | modal.addButtonWithTitle('Cancel'); 269 | 270 | scrollView.setDocumentView(scrollContent.view()); 271 | modal.addAccessoryView(scrollView); 272 | 273 | //scroll to top 274 | scrollView.documentView().scrollPoint(NSMakePoint(0.0, NSMaxY(scrollView.documentView().frame()) - NSHeight(scrollView.contentView().bounds()))); 275 | } 276 | 277 | return modal; 278 | } 279 | 280 | /** 281 | * Collect user input from alert window 282 | * @param {COSAlertWindow} alert The alert window 283 | * @param {Int} responseCode Alert window response code 284 | * @return {Object} Alert window results 285 | */ 286 | function handleAlertResponse(alert, responseCode) { 287 | if (!enableReplace) { 288 | return; 289 | } 290 | if (responseCode == "1000") { 291 | for (var i = 0; i < foundCount; ++i) { 292 | var new_font = inputs[i].stringValue(); 293 | if (new_font == SELECT_FONT_LABEL) { 294 | continue; 295 | } 296 | var key = fontKeys[i]; 297 | var layers = textLayers[key].layers; 298 | if (new_font == key) { 299 | continue; 300 | } 301 | print("Replacing: " + key + "->" + new_font); 302 | for (var f = 0; f < layers.length; ++f) { 303 | var layer = layers[f]; 304 | layer.setFontPostscriptName(new_font); 305 | } 306 | } 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /Fontily.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author" : "Maciej Partyka", 3 | "commands" : [ 4 | { 5 | "script" : "fontily.cocoascript", 6 | "handler" : "replaceMissingFonts", 7 | "shortcut" : "command p", 8 | "name" : "Find & Replace Fonts", 9 | "identifier" : "replaceMissingFonts" 10 | }, 11 | { 12 | "script" : "fontily.cocoascript", 13 | "handler" : "showFontLayers", 14 | "shortcut" : "", 15 | "name" : "List fonts used", 16 | "identifier" : "showFontLayers" 17 | }, 18 | { 19 | "script" : "fontily.cocoascript", 20 | "handler" : "requestFeature", 21 | "shortcut" : "", 22 | "name" : "Request new feature", 23 | "identifier" : "requestFeature" 24 | } 25 | ], 26 | "menu": { 27 | "items": [ 28 | "replaceMissingFonts", 29 | "showFontLayers", 30 | "-", 31 | "requestFeature" 32 | ] 33 | }, 34 | "identifier" : "com.sketchapp.partyka.fontily", 35 | "version" : "1.2", 36 | "description" : "Missing sketch plugin for replacing fonts", 37 | "authorEmail" : "maciek@partyka.io", 38 | "name" : "Fontily" 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fontily: missing sketch plugin 2 | 3 | Sketch plugin for finding & replacing fonts for Sketch 3+ 4 | 5 | ## Replace fonts 6 | 7 | Fontily will list all the fonts in document, and allow you to change selected ones. It tries to detect missing fonts, but Sketch API doesn't provide info whether this font is missing or not, so it may give false positives. 8 | 9 | 10 | Shortcut: `cmd+p` 11 | 12 | ![Screenshot](http://partyka.io/fontily/screenshot11_a.png) 13 | 14 | 15 | ## List fonts 16 | 17 | List all the fonts in document. 18 | 19 | ![Screenshot](http://partyka.io/fontily/screenshot11_b.png) 20 | 21 | ## Installation 22 | 23 | Make sure you have the latest version of Sketch 3 installed. 24 | 25 | 1. [Download the ZIP file of this repository](https://github.com/partyka1/Fontily/archive/master.zip) 26 | 2. Double click on `Fontily.sketchplugin` 27 | 28 | ## Contribute 29 | 30 | If you have ideas how to make this plugin better or found an error, create an issue or email me at maciek@partyka.io 31 | --------------------------------------------------------------------------------