├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── app ├── index.html ├── static │ └── img │ │ ├── brackets-alt.png │ │ ├── brackets.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── obj-icon.png └── storage │ ├── settings.json │ └── snippets.json ├── gulpfile.js ├── package.json ├── sass ├── _AddMode.scss ├── _App.scss ├── _Defaults.scss ├── _EmptyMode.scss ├── _Notification.scss ├── _Panel.scss ├── _PanelControls.scss ├── _PreviewMode.scss ├── _SearchBar.scss ├── _SearchItem.scss ├── _SearchList.scss ├── _Tags.scss ├── _Variables.scss └── main.scss └── src ├── actions ├── SettingsActions.js └── SnippetActions.js ├── app.js ├── components ├── AddMode.js ├── App.js ├── EditMode.js ├── EmptyMode.js ├── LanguageSelect.js ├── Panel.js ├── PanelButton.js ├── PanelControls.js ├── PreviewMode.js ├── SearchBar.js ├── SearchItem.js ├── SearchList.js └── Tags.js ├── constants └── SnippetBarConstants.js ├── data.js ├── dispatcher └── AppDispatcher.js ├── main.js ├── mb.js ├── stores ├── SettingsStore.js └── SnippetStore.js └── utils └── SnippetBarUtils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | #design folder 11 | design 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | npm-debug.log 32 | 33 | # Mac 34 | .DS_Store 35 | **/.DS_Store 36 | 37 | # project 38 | app/static/css 39 | app/static/fonts 40 | app/js 41 | app/actions 42 | app/components 43 | app/constants 44 | app/dispatcher 45 | app/stores 46 | app/utils 47 | app/*.js 48 | builds 49 | docs 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snippet Bar 2 | 3 | > A desktop menubar app for copying, pasting and re-using text snippets 4 | 5 | Our goal is to provide a home for your snippets - code or otherwise. Snippet Bar aims to give quick access to your most used snippets: 6 | - create and manage your snippets by giving them titles and tags 7 | - search and select a snippet to copy to your clipboard 8 | - enable optional syntax highlighting for viewing code snippets 9 | 10 | ![](https://cloud.githubusercontent.com/assets/12987958/12094722/388dfffe-b2d8-11e5-81c6-d94321cb645b.gif) 11 | 12 | ### Which platforms are supported? 13 | 14 | Snippet Bar works on Linux, Mac and Windows. But it's still in the testing phase. So far, we have Mac and Linux packages available for download on the [releases](https://github.com/teesloane/snippet-bar/releases) page. 15 | 16 | ### Are more features on the horizon? 17 | 18 | This is our first release. We have future ideas on how to improve and expand Menu Bar, however it might be a while before we get started. 19 | 20 | We welcome any pull requests, suggestions and logged issues! 21 | 22 | ### What technologies did you use? 23 | 24 | Snippet Bar is built with React, Flux, Electron and some other awesome packages including: 25 | - [Hightlight.js](https://highlightjs.org/) 26 | - [maxogden's menu-bar](https://github.com/maxogden/menubar) 27 | - [Clipboard.js](https://zenorocha.github.io/clipboard.js/) 28 | - [Font Awesome](https://fortawesome.github.io/Font-Awesome/) 29 | 30 | * * * 31 | 32 | Made with <3 and 100+ screenshares. By Tyler and Bronek. 33 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/static/img/brackets-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teesloane/snippet-bar/6d51e6ea095b00fbc2500c6cda4e9191b59f93d8/app/static/img/brackets-alt.png -------------------------------------------------------------------------------- /app/static/img/brackets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teesloane/snippet-bar/6d51e6ea095b00fbc2500c6cda4e9191b59f93d8/app/static/img/brackets.png -------------------------------------------------------------------------------- /app/static/img/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teesloane/snippet-bar/6d51e6ea095b00fbc2500c6cda4e9191b59f93d8/app/static/img/icon.icns -------------------------------------------------------------------------------- /app/static/img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teesloane/snippet-bar/6d51e6ea095b00fbc2500c6cda4e9191b59f93d8/app/static/img/icon.ico -------------------------------------------------------------------------------- /app/static/img/obj-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teesloane/snippet-bar/6d51e6ea095b00fbc2500c6cda4e9191b59f93d8/app/static/img/obj-icon.png -------------------------------------------------------------------------------- /app/storage/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntaxHighlighting": false 3 | } 4 | -------------------------------------------------------------------------------- /app/storage/snippets.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var plumber = require('gulp-plumber'); 3 | var babel = require('gulp-babel'); 4 | var sass = require('gulp-sass'); 5 | var cssnano = require('gulp-cssnano'); 6 | var uglify = require('gulp-uglify'); 7 | 8 | var paths = { 9 | app: 'app/', 10 | css: 'app/static/css', 11 | font: 'app/static/fonts' 12 | }; 13 | 14 | var tasks = [ 15 | 'js', 16 | 'sass', 17 | 'normalize', 18 | 'highlightstyles', 19 | 'fontstyles', 20 | 'fontfiles' 21 | ]; 22 | 23 | gulp.task('js', function() { 24 | gulp.src('src/**/*.js') 25 | .pipe(plumber()) 26 | .pipe(babel()) 27 | .pipe(uglify()) 28 | .pipe(gulp.dest(paths.app)) 29 | }); 30 | 31 | gulp.task('sass', function() { 32 | gulp.src('sass/main.scss') 33 | .pipe(plumber()) 34 | .pipe(sass({ 35 | outputStyle: 'compressed' 36 | })) 37 | .pipe(gulp.dest(paths.css)); 38 | }); 39 | 40 | gulp.task('normalize', function() { 41 | gulp.src('node_modules/normalize.css/normalize.css') 42 | .pipe(cssnano()) 43 | .pipe(gulp.dest(paths.css)); 44 | }); 45 | 46 | gulp.task('highlightstyles', function() { 47 | gulp.src('node_modules/highlight.js/styles/monokai-sublime.css') 48 | .pipe(cssnano()) 49 | .pipe(gulp.dest(paths.css)); 50 | }); 51 | 52 | gulp.task('fontstyles', function() { 53 | gulp.src('node_modules/font-awesome/css/font-awesome.min.css') 54 | .pipe(gulp.dest(paths.css)); 55 | }); 56 | 57 | gulp.task('fontfiles', function() { 58 | gulp.src('node_modules/font-awesome/fonts/*.*') 59 | .pipe(gulp.dest(paths.font)); 60 | }); 61 | 62 | gulp.task('default', tasks, function() { 63 | gulp.watch(['src/**/*.js'], ['js']); 64 | gulp.watch(['sass/**/*.scss'], ['sass']); 65 | }); 66 | 67 | gulp.task('compile', tasks); 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snippet-bar", 3 | "version": "0.1.1", 4 | "description": "A desktop menubar app for copying, pasting and re-using text snippets", 5 | "main": "app/main.js", 6 | "scripts": { 7 | "start": "electron app/main.js", 8 | "build-mac": "electron-packager . 'Snippet Bar' --platform=darwin --arch=x64 --out=./builds --version=0.36.2 --overwrite --icon='./app/static/img/icon' --prune --ignore='./docs' --ignore='./builds'", 9 | "build-linux": "electron-packager . 'Snippet Bar' --platform=linux --arch=x64 --out=./builds --version=0.36.2 --overwrite --prune --ignore='./docs' --ignore='./builds'" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/teesloane/snippet-bar" 14 | }, 15 | "author": "Tyler and Bronek", 16 | "license": "GPL-2.0", 17 | "bugs": { 18 | "url": "https://github.com/teesloane/snippet-bar/issues" 19 | }, 20 | "homepage": "https://github.com/teesloane/snippet-bar#readme", 21 | "dependencies": { 22 | "clipboard": "^1.5.5", 23 | "flux": "^2.1.1", 24 | "font-awesome": "^4.4.0", 25 | "highlight.js": "^9.1.0", 26 | "keymirror": "^0.1.1", 27 | "menubar": "~3.0.0", 28 | "normalize.css": "^3.0.3", 29 | "react": "^0.14.6", 30 | "react-addons-update": "^0.14.6", 31 | "react-dom": "^0.14.6" 32 | }, 33 | "devDependencies": { 34 | "babel-preset-es2015": "^6.1.18", 35 | "babel-preset-react": "^6.1.18", 36 | "electron-packager": "^5.2.0", 37 | "electron-prebuilt": "^0.36.2", 38 | "gulp": "^3.9.0", 39 | "gulp-babel": "^6.1.0", 40 | "gulp-cssnano": "^2.1.0", 41 | "gulp-plumber": "^1.0.1", 42 | "gulp-sass": "^2.1.0", 43 | "gulp-uglify": "^1.5.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sass/_AddMode.scss: -------------------------------------------------------------------------------- 1 | .add-snippet-mode { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | flex-grow: 1; 6 | 7 | .add-field { 8 | height: $input-height; 9 | outline: none; 10 | border: 0; 11 | border-bottom: $thin-black-border; 12 | padding-left: $input-padding + 1; 13 | background: #2F3132; 14 | width: 100%; 15 | } 16 | 17 | .add-textarea { 18 | font-family: 'Monaco', Arial; 19 | flex-grow: 2; 20 | background: #2A2B2C; 21 | border: 0; 22 | resize: none; 23 | padding: $input-padding + 1; 24 | tab-size: 2; 25 | } 26 | } 27 | 28 | textarea:focus { 29 | outline: none; 30 | } 31 | 32 | .title-and-language{ 33 | display: flex; 34 | justify-content: space-between; 35 | 36 | .show-hide-languages { 37 | display: flex; 38 | } 39 | 40 | .new-languages { 41 | background: #2F3132; 42 | color: rgb(228, 228, 228); 43 | outline: none; 44 | border: 0; 45 | border-radius: 0; 46 | padding: 0 10px; 47 | -webkit-appearance: none; 48 | appearance: none; 49 | border-bottom: $thin-black-border; 50 | border-left: $thin-black-border; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sass/_App.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | height: 100vh; 5 | width: 100%; 6 | background: #efefef; 7 | } 8 | -------------------------------------------------------------------------------- /sass/_Defaults.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: Helvetica, sans-serif; 7 | font-size: 12px; 8 | overflow: hidden; 9 | color: rgb(228, 228, 228); 10 | } 11 | 12 | a { 13 | color: rgb(199, 199, 199) ; 14 | } 15 | 16 | pre { 17 | tab-size: 29; 18 | } 19 | -------------------------------------------------------------------------------- /sass/_EmptyMode.scss: -------------------------------------------------------------------------------- 1 | .empty-snippet-mode { 2 | width: 100%; 3 | display: flex; 4 | flex-grow: 1; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | .empty-greeting { 9 | width: 85%; 10 | text-align: center; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /sass/_Notification.scss: -------------------------------------------------------------------------------- 1 | #overlay { 2 | 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | background: rgba(0, 0, 0, 0.7); 7 | width: 100%; 8 | height: 100%; 9 | z-index: -1; 10 | } 11 | 12 | #notification { 13 | position: absolute; 14 | width: 20%; 15 | padding: 0.5em 1em; 16 | transform: translate(-50%, -50%); 17 | top: 50%; 18 | left: 50%; 19 | background: #262629; 20 | text-align: center; 21 | z-index: -1; 22 | border-radius: 2px; 23 | } 24 | -------------------------------------------------------------------------------- /sass/_Panel.scss: -------------------------------------------------------------------------------- 1 | .panel-container { 2 | display: flex; 3 | flex-direction: column; 4 | width: 67%; 5 | height: 100vh; 6 | justify-content: center; 7 | align-items: center; 8 | background: #38393A; 9 | } 10 | 11 | .panel-mode { 12 | display: flex; 13 | flex-direction: column; 14 | height: 100vh; 15 | width: 100%; 16 | 17 | 18 | .selected-mode { 19 | display: flex; 20 | flex-direction: column; 21 | flex-grow: 1; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sass/_PanelControls.scss: -------------------------------------------------------------------------------- 1 | .panel-controls { 2 | display: flex; 3 | width: 100%; 4 | background: #2C2D2E; 5 | justify-content: space-between; 6 | height: 10vh; 7 | // border-top: 1px solid #38393A; 8 | border-top: solid #585858 2px; 9 | 10 | button, i { 11 | &:hover, 12 | &:focus { 13 | color: #6CBEFF; 14 | } 15 | 16 | &:active { 17 | color: #FFA63E; 18 | } 19 | } 20 | 21 | button { 22 | transition: all 0.2s ease; 23 | width: 100%; 24 | padding: 10px; 25 | background: none; 26 | border: none; 27 | color: rgb(221, 221, 221); 28 | font-size: 7px; 29 | } 30 | } 31 | 32 | #empty-snippet { 33 | visibility: hidden; 34 | } 35 | 36 | .button-text { 37 | padding-left: 10px; 38 | text-transform: uppercase; 39 | font-size: 11.5px; 40 | } 41 | -------------------------------------------------------------------------------- /sass/_PreviewMode.scss: -------------------------------------------------------------------------------- 1 | .preview-snippet-mode { 2 | font-size: 1em; 3 | font-family: "Monaco", Arial, sans-serif; 4 | width: 100%; 5 | height: 90vh; 6 | margin: 0 auto; 7 | display: flex; 8 | flex-direction: column; 9 | flex-grow: 1; 10 | flex-wrap: wrap; 11 | 12 | .snippet-text { 13 | margin: 0; 14 | padding: 3%; 15 | height: 90vh; 16 | font-size: 12px; 17 | white-space: pre-wrap; 18 | } 19 | 20 | code { 21 | font-family: 'Monaco', monospace, Arial, sans-serif; 22 | } 23 | 24 | .nohighlight [class^="hljs-"] { 25 | color: inherit; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sass/_SearchBar.scss: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | display: flex; 3 | height: 11vh; 4 | align-items: center; 5 | border-right: $thin-black-border; 6 | border-bottom: $thin-black-border; 7 | background: #2F3132; 8 | 9 | .search-bar-field { 10 | width: 100%; 11 | height: 100%; 12 | outline: none; 13 | border: 0; 14 | padding-left: 5%; 15 | background: none; 16 | } 17 | 18 | .search-bar-clear-button { 19 | margin-left: 2.5%; 20 | margin-right: 2.5%; 21 | background: none; 22 | color: #fff; 23 | border: 0; 24 | font-size: 1.2em; 25 | line-height: 0.5; 26 | padding: 2.5%; 27 | text-align: center; 28 | vertical-align: middle; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sass/_SearchItem.scss: -------------------------------------------------------------------------------- 1 | .search-item { 2 | display: flex; 3 | align-items: center; 4 | border-bottom: $thin-black-border; 5 | height: 12vh; 6 | overflow: hidden; 7 | cursor: pointer; 8 | position: relative; 9 | 10 | .search-item-body { 11 | margin-left: 5%; 12 | } 13 | 14 | // &.is-active:before { 15 | // content: ''; 16 | // position: absolute; 17 | // left: 0; 18 | // top: 0; 19 | // bottom: 0; 20 | // width: 3px; 21 | // background-color: #eee; 22 | // } 23 | 24 | &.is-active { 25 | background: rgb(43, 43, 43); 26 | } 27 | 28 | .search-item-title { 29 | width: 200px; 30 | font-size: 12px; 31 | font-weight: 400; 32 | margin-bottom: 2px; 33 | white-space: nowrap; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | 37 | a { 38 | text-decoration: none; 39 | } 40 | } 41 | 42 | .search-item-tags { 43 | margin-top: 2px; 44 | font-size: 10px; 45 | color: #7B7E82; 46 | text-overflow: ellpisis; 47 | white-space: nowrap; 48 | overflow: hidden; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sass/_SearchList.scss: -------------------------------------------------------------------------------- 1 | .searchlist-container { 2 | width: 33%; 3 | display: flex; 4 | flex-direction: column; 5 | background: #38393A; 6 | 7 | } 8 | 9 | .list-container { 10 | overflow-y: auto; 11 | height: 79vh; 12 | border-right: $thin-black-border; 13 | 14 | ul { 15 | list-style-type: none; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | } 20 | 21 | .no-snippets { 22 | text-align: center; 23 | } 24 | 25 | .menu-controls { 26 | display: flex; 27 | width: 100%; 28 | height: 10vh; 29 | background: #2C2D2E; 30 | border-top: solid #585858 2px; 31 | 32 | button { 33 | // width: 100%; 34 | padding: 10px; 35 | background: none; 36 | border: none; 37 | color: white; 38 | font-size: 7px; 39 | 40 | & + button { 41 | border-left: 1px solid #38393A; 42 | } 43 | } 44 | 45 | i { 46 | transition: all 0.1s ease; 47 | 48 | &:hover, 49 | &:focus { 50 | color: #6CBEFF; 51 | } 52 | 53 | &:active { 54 | color: #FFA63E; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sass/_Tags.scss: -------------------------------------------------------------------------------- 1 | $tag-y-spacing: 10px; 2 | $tag-x-spacing: 5px; 3 | $tag-button-width: 12px + $tag-x-spacing; 4 | $tag-border-radius: 15px; 5 | $tag-background: #ccc; 6 | $tag-color: black; 7 | $tag-min-height: 31px; 8 | 9 | .tags { 10 | display: flex; 11 | flex-wrap: wrap; 12 | min-height: 10.5vh; 13 | align-content: center; 14 | background: #2F3132; 15 | min-height: $tag-min-height + 5px; 16 | line-height: 1; 17 | font-size: 1em; 18 | border-bottom: $thin-black-border; 19 | padding: 0 $input-padding; 20 | } 21 | 22 | .tags-input { 23 | display: flex; 24 | width: 33%; 25 | line-height: 1; 26 | min-height: $tag-min-height; 27 | margin-left: 1%; 28 | border: 0; 29 | outline: none; 30 | background: none; 31 | color: #fff; 32 | } 33 | 34 | .tag { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | margin-top: $tag-x-spacing; 39 | margin-bottom: $tag-x-spacing; 40 | 41 | .tag + .tag { 42 | margin-left: $tag-x-spacing; 43 | } 44 | 45 | .tag-value { 46 | background: $tag-background; 47 | color: $tag-color; 48 | border-radius: $tag-border-radius 0 0 $tag-border-radius; 49 | padding: 6px 0 6px 10px; 50 | letter-spacing: 1px; 51 | } 52 | 53 | .tag-delete-button { 54 | text-align: center; 55 | border: 0; 56 | background: $tag-background; 57 | color: $tag-color; 58 | padding: 6px 10px; 59 | line-height: 0.9; 60 | border-radius: 0 $tag-border-radius $tag-border-radius 0; 61 | outline: none; 62 | margin-right: 10px; 63 | border: 0 solid $tag-background; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sass/_Variables.scss: -------------------------------------------------------------------------------- 1 | /* Forms */ 2 | 3 | $input-height: 11vh; 4 | $input-padding: 5%; 5 | 6 | /* Borders */ 7 | 8 | $thin-black-border: 1px solid #262626; 9 | 10 | $thick-border: 5px solid #262626; 11 | -------------------------------------------------------------------------------- /sass/main.scss: -------------------------------------------------------------------------------- 1 | /*! by Tyler and Bronek */ 2 | 3 | @import "_Defaults"; 4 | @import "_Variables"; 5 | 6 | @import "_Notification"; 7 | @import "_App"; 8 | @import "_Panel"; 9 | @import "_EmptyMode"; 10 | @import "_AddMode"; 11 | @import "_PreviewMode"; 12 | @import "_SearchBar"; 13 | @import "_SearchList"; 14 | @import "_SearchItem"; 15 | @import "_PanelControls"; 16 | @import "_AddMode"; 17 | @import "_Tags"; 18 | -------------------------------------------------------------------------------- /src/actions/SettingsActions.js: -------------------------------------------------------------------------------- 1 | const AppDispatcher = require('../dispatcher/AppDispatcher'); 2 | const SnippetBarConstants = require('../constants/SnippetBarConstants'); 3 | 4 | module.exports = { 5 | load(settings) { 6 | AppDispatcher.dispatch({ 7 | actionType: SnippetBarConstants.LOAD_SETTINGS, 8 | settings 9 | }); 10 | }, 11 | 12 | syntaxToggle() { 13 | AppDispatcher.dispatch({ 14 | actionType: SnippetBarConstants.SYNTAX_TOGGLE 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/actions/SnippetActions.js: -------------------------------------------------------------------------------- 1 | const AppDispatcher = require('../dispatcher/AppDispatcher'); 2 | const SnippetBarConstants = require('../constants/SnippetBarConstants'); 3 | 4 | const noop = function(){}; 5 | 6 | module.exports = { 7 | load(snippets) { 8 | AppDispatcher.dispatch({ 9 | actionType: SnippetBarConstants.LOAD_SNIPPETS, 10 | snippets 11 | }); 12 | }, 13 | 14 | create(values, callback = noop) { 15 | AppDispatcher.dispatch({ 16 | actionType: SnippetBarConstants.SNIPPET_CREATE, 17 | values, 18 | callback 19 | }); 20 | }, 21 | 22 | update(values, callback = noop) { 23 | AppDispatcher.dispatch({ 24 | actionType: SnippetBarConstants.SNIPPET_UPDATE, 25 | values, 26 | callback 27 | }); 28 | }, 29 | 30 | destroy(callback = noop) { 31 | AppDispatcher.dispatch({ 32 | actionType: SnippetBarConstants.SNIPPET_DELETE, 33 | callback 34 | }); 35 | }, 36 | 37 | setActive(id) { 38 | AppDispatcher.dispatch({ 39 | actionType: SnippetBarConstants.SNIPPET_SET_ACTIVE, 40 | id 41 | }); 42 | }, 43 | 44 | setMode(mode) { 45 | AppDispatcher.dispatch({ 46 | actionType: SnippetBarConstants.MODE_SET_ACTIVE, 47 | mode 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | 4 | const SnippetBarUtils = require('./utils/SnippetBarUtils'); 5 | const SettingsStore = require('./stores/SettingsStore'); 6 | 7 | const App = require('./components/App'); 8 | 9 | SnippetBarUtils.readSnippets(); 10 | SnippetBarUtils.readSettings(SettingsStore.init); 11 | 12 | ReactDOM.render( 13 | , 14 | document.getElementById('react-mount') 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/AddMode.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SnippetActions = require('../actions/SnippetActions'); 4 | 5 | const Tags = require('./Tags'); 6 | const PanelControls = require('./PanelControls'); 7 | const LanguageSelect = require('./LanguageSelect'); 8 | 9 | const AddMode = React.createClass({ 10 | propTypes: { 11 | showNotification: React.PropTypes.func.isRequired, 12 | enableTabbing: React.PropTypes.func.isRequired 13 | }, 14 | 15 | componentDidMount(){ 16 | this.props.enableTabbing(); 17 | }, 18 | 19 | createSnippet(event) { 20 | event.preventDefault(); 21 | 22 | let title = this.refs.title.value.trim(); 23 | let tags = this.refs.tags.state.tags; 24 | let text = this.refs.text.value; 25 | let language = this.refs.languages.state.language; 26 | let values = {}; 27 | 28 | if (title) values.title = title; 29 | if (tags.length) values.tags = tags; 30 | if (text) values.text = text; 31 | if (language) values.lang = language; 32 | 33 | SnippetActions.create(values, () => { 34 | this.props.showNotification('Snippet Created!'); 35 | }); 36 | }, 37 | 38 | render() { 39 | return( 40 |
41 |
42 |
43 | 44 |
45 | 51 | 52 |
53 | 54 |
55 |
56 | 57 | 60 | 61 | 66 | 67 | 70 | 71 |
72 |
73 | ); 74 | } 75 | }); 76 | 77 | module.exports = AddMode; 78 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SearchList = require('./SearchList'); 4 | const Panel = require('./Panel'); 5 | 6 | const App = React.createClass({ 7 | showNotification(message) { 8 | let overlay = this.refs.overlay; 9 | let notification = this.refs.notification; 10 | 11 | overlay.style.zIndex = 10; 12 | notification.style.zIndex = 11; 13 | 14 | notification.textContent = message; 15 | 16 | setTimeout(() => { 17 | overlay.style.zIndex = -1; 18 | notification.style.zIndex = -2; 19 | }, 1000); 20 | }, 21 | 22 | render() { 23 | return ( 24 |
25 |
26 |
27 | 28 | 29 | 30 |
31 | ); 32 | } 33 | }); 34 | 35 | module.exports = App; 36 | -------------------------------------------------------------------------------- /src/components/EditMode.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SnippetActions = require('../actions/SnippetActions'); 4 | const SnippetStore = require('../stores/SnippetStore'); 5 | 6 | const Tags = require('./Tags'); 7 | const PanelControls = require('./PanelControls'); 8 | const LanguageSelect = require('./LanguageSelect'); 9 | 10 | const EditMode = React.createClass({ 11 | propTypes: { 12 | showNotification: React.PropTypes.func.isRequired, 13 | enableTabbing: React.PropTypes.func.isRequired 14 | }, 15 | 16 | getInitialState() { 17 | return { 18 | activeSnippet: SnippetStore.getActiveSnippet() 19 | }; 20 | }, 21 | 22 | _onChange() { 23 | if (this.isMounted()) { 24 | this.setState({ 25 | activeSnippet: SnippetStore.getActiveSnippet() 26 | }); 27 | } 28 | }, 29 | 30 | componentDidMount() { 31 | this.props.enableTabbing(); 32 | 33 | SnippetStore.addChangeListener(this._onChange); 34 | }, 35 | 36 | componentWillUnmount() { 37 | SnippetStore.removeChangeListener(this._onChange); 38 | }, 39 | 40 | updateSnippet(event) { 41 | event.preventDefault(); 42 | 43 | let title = this.refs.title.value.trim(); 44 | let tags = this.refs.tags.state.tags; 45 | let text = this.refs.text.value; 46 | let language = this.refs.languages.state.language 47 | 48 | let values = {}; 49 | 50 | if (title) values.title = title; 51 | if (tags.length) values.tags = tags; 52 | if (text) values.text = text; 53 | if (language) values.lang = language; 54 | 55 | SnippetActions.update(values, () => { 56 | this.props.showNotification('Snippet Updated!'); 57 | }); 58 | }, 59 | 60 | render() { 61 | return( 62 |
63 |
64 |
65 | 66 |
67 | 74 | 75 |
76 | 77 |
78 |
79 | 80 | 84 | 85 | 91 | 92 | 95 | 96 |
97 |
98 | ); 99 | } 100 | }); 101 | 102 | module.exports = EditMode; 103 | -------------------------------------------------------------------------------- /src/components/EmptyMode.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const PanelControls = require('./PanelControls'); 4 | 5 | const EmptyMode = React.createClass({ 6 | render() { 7 | return( 8 |
9 |
10 |
11 | Don't forget to be awesome! 12 |
13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | }); 20 | 21 | module.exports = EmptyMode; 22 | -------------------------------------------------------------------------------- /src/components/LanguageSelect.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SettingsStore = require('../stores/SettingsStore'); 4 | 5 | const LanguageSelect = React.createClass({ 6 | propTypes: { 7 | editLang: React.PropTypes.string 8 | }, 9 | 10 | getInitialState() { 11 | return { 12 | languages: SettingsStore.getLanguages(), 13 | syntax: SettingsStore.getSyntax(), 14 | language: null 15 | } 16 | }, 17 | 18 | _onChange() { 19 | if (this.isMounted()) { 20 | this.setState({ 21 | languages: SettingsStore.getLanguages(), 22 | syntax: SettingsStore.getSyntax() 23 | }); 24 | } 25 | }, 26 | 27 | componentDidMount() { 28 | this.getSelectedLanguage(); 29 | this.setLanguage(); 30 | 31 | SettingsStore.addChangeListener(this._onChange); 32 | }, 33 | 34 | componentWillUnmount() { 35 | SettingsStore.removeChangeListener(this._onChange); 36 | }, 37 | 38 | //copying setTags(tags) to try and make lang load saved selection in edit mode. 39 | setLanguage() { 40 | let language = this.props.editLang; 41 | 42 | if (language && language.length) this.setState({ language }); 43 | }, 44 | 45 | // Gets the selected Language from the Element */ 61 | display = { 62 | display: (this.state.syntax ? 'flex' : 'none') 63 | } 64 | 65 | return( 66 | 75 | ); 76 | } 77 | }); 78 | 79 | module.exports = LanguageSelect; 80 | -------------------------------------------------------------------------------- /src/components/Panel.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SnippetActions = require('../actions/SnippetActions'); 4 | const SnippetStore = require('../stores/SnippetStore'); 5 | const SettingsStore = require('../stores/SettingsStore'); 6 | 7 | const PanelControls = require('./PanelControls'); 8 | const PreviewMode = require('./PreviewMode'); 9 | const EmptyMode = require('./EmptyMode'); 10 | const AddMode = require('./AddMode'); 11 | const EditMode = require('./EditMode'); 12 | 13 | const Panel = React.createClass({ 14 | propTypes: { 15 | showNotification: React.PropTypes.func.isRequired 16 | }, 17 | 18 | getInitialState() { 19 | return { 20 | modes: SnippetStore.getModes(), 21 | activeMode: SnippetStore.getActiveMode() 22 | } 23 | }, 24 | 25 | _onChange() { 26 | if (this.isMounted()) { 27 | this.setState({ 28 | modes: SnippetStore.getModes(), 29 | activeMode: SnippetStore.getActiveMode() 30 | }); 31 | } 32 | }, 33 | 34 | componentDidMount() { 35 | SnippetStore.addChangeListener(this._onChange); 36 | SettingsStore.addChangeListener(this._onChange); 37 | }, 38 | 39 | componentWillUnmount() { 40 | SnippetStore.removeChangeListener(this._onChange); 41 | SettingsStore.removeChangeListener(this._onChange); 42 | }, 43 | 44 | enableTabbing() { 45 | document.querySelector("textarea").addEventListener('keydown',function(e) { 46 | if (e.keyCode === 9) { // tab was pressed 47 | // get caret position/selection 48 | 49 | var start = this.selectionStart; 50 | var end = this.selectionEnd; 51 | 52 | var target = e.target; 53 | var value = target.value; 54 | 55 | // set textarea value to: text before caret + tab + text after caret 56 | target.value = value.substring(0, start) 57 | + " " 58 | + value.substring(end); 59 | 60 | // put caret at right position again (add one for the tab) 61 | this.selectionStart = this.selectionEnd = start + 2; 62 | 63 | // prevent the focus lose 64 | e.preventDefault(); 65 | } 66 | },false); 67 | }, 68 | 69 | setContent() { 70 | let modes = this.state.modes; 71 | let activeMode = this.state.activeMode; 72 | 73 | switch (activeMode) { 74 | case modes.edit: 75 | return ; 76 | break; 77 | 78 | case modes.preview: 79 | return ; 80 | break; 81 | 82 | case modes.add: 83 | return ; 84 | break; 85 | 86 | default: 87 | return 88 | } 89 | }, 90 | 91 | render() { 92 | let panelContent = this.setContent(); 93 | 94 | return( 95 |
96 | {panelContent} 97 |
98 | ); 99 | } 100 | }); 101 | 102 | module.exports = Panel; 103 | -------------------------------------------------------------------------------- /src/components/PanelButton.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const PanelButton = React.createClass({ 4 | propTypes: { 5 | kind: React.PropTypes.string.isRequired, 6 | type: React.PropTypes.string, 7 | click: React.PropTypes.func 8 | }, 9 | 10 | getInitialState() { 11 | return { 12 | defaultKind: 'standard', 13 | kinds: { 14 | copy: { 15 | text: 'copy', 16 | icon: 'fa fa-clone fa-2x', 17 | title: 'Copy Snippet' 18 | }, 19 | edit: { 20 | text: 'edit', 21 | icon: 'fa fa-pencil fa-2x', 22 | title: 'Edit Snippet' 23 | }, 24 | update: { 25 | text: 'update', 26 | icon: 'fa fa-floppy-o fa-2x', 27 | title: 'Update Snippet' 28 | }, 29 | save: { 30 | text: 'save', 31 | icon: 'fa fa-floppy-o fa-2x', 32 | title: 'Edit Snippet' 33 | }, 34 | add: { 35 | text: 'new', 36 | icon: 'fa fa-plus fa-2x', 37 | title: 'New Snippet' 38 | }, 39 | del: { 40 | text: 'delete', 41 | icon: 'fa fa-trash fa-2x', 42 | title: 'Delete Snippet' 43 | } 44 | } 45 | }; 46 | }, 47 | 48 | getKind() { 49 | let kindProp = this.props.kind; 50 | 51 | if(kindProp in this.state.kinds) { 52 | return kindProp; 53 | } 54 | 55 | return this.state.defaultKind; 56 | }, 57 | 58 | click(event) { 59 | if (!this.props.type) { 60 | event.stopPropagation(); 61 | } 62 | 63 | if (this.props.click) { 64 | this.props.click(event); 65 | } 66 | }, 67 | 68 | render() { 69 | let kind = this.getKind(); 70 | let text = 'standard'; 71 | let icon = 'standard-icon'; 72 | let title = ''; 73 | let type = this.props.type === 'submit' ? 'submit' : 'button'; 74 | let dataClipboard = {}; 75 | 76 | if (kind !== this.state.defaultKind) { 77 | let kindObj = this.state.kinds[kind]; 78 | 79 | text = kindObj.text; 80 | icon = kindObj.icon; 81 | title = kindObj.title; 82 | 83 | if (kind === 'copy') { 84 | dataClipboard['data-clipboard-action'] = 'copy'; 85 | dataClipboard['data-clipboard-target'] = '.copy-text'; 86 | } 87 | } 88 | 89 | return ( 90 | 99 | ); 100 | } 101 | 102 | }); 103 | 104 | module.exports = PanelButton; 105 | -------------------------------------------------------------------------------- /src/components/PanelControls.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const Clipboard = require('clipboard'); 3 | 4 | const SnippetActions = require('../actions/SnippetActions'); 5 | const SnippetStore = require('../stores/SnippetStore') 6 | 7 | const PanelButton = require('./PanelButton'); 8 | 9 | const PanelControls = React.createClass({ 10 | propTypes: { 11 | mode: React.PropTypes.string.isRequired, 12 | showNotification: React.PropTypes.func 13 | }, 14 | 15 | getInitialState() { 16 | return { 17 | modes: SnippetStore.getModes() 18 | }; 19 | }, 20 | 21 | componentDidUpdate(){ 22 | new Clipboard('.copy-btn'); 23 | }, 24 | 25 | copySnippet() { 26 | this.props.showNotification("Snippet Copied!"); 27 | }, 28 | 29 | confirmDelete() { 30 | if (confirm("You sure you want to delete this snippet?")) { 31 | SnippetActions.destroy(() => { 32 | this.props.showNotification('Snippet Deleted!'); 33 | }); 34 | } 35 | }, 36 | 37 | showButtons() { 38 | let mode = this.props.mode; 39 | let modes = this.state.modes; 40 | 41 | let copy = ; 42 | let del = ; 43 | let edit = ; 44 | let add = ; 45 | let save = ; 46 | let update = ; 47 | 48 | if (mode === modes.empty) return add; 49 | else if (mode === modes.add) return save; 50 | else if (mode === modes.preview) return [copy, edit, del, add]; 51 | else if (mode === modes.edit) return [update, add]; 52 | }, 53 | 54 | render() { 55 | return( 56 |
57 | {this.showButtons()} 58 |
59 | ); 60 | } 61 | }); 62 | 63 | module.exports = PanelControls; 64 | -------------------------------------------------------------------------------- /src/components/PreviewMode.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const hljs = require('highlight.js'); 3 | 4 | const SnippetStore = require('../stores/SnippetStore'); 5 | const SettingsStore = require('../stores/SettingsStore'); 6 | 7 | const PanelControls = require('./PanelControls'); 8 | 9 | const PreviewMode = React.createClass({ 10 | propTypes: { 11 | showNotification: React.PropTypes.func.isRequired 12 | }, 13 | 14 | getInitialState() { 15 | return { 16 | activeSnippet: SnippetStore.getActiveSnippet(), 17 | languages: SettingsStore.getLanguages(), 18 | syntax: SettingsStore.getSyntax() 19 | }; 20 | }, 21 | 22 | _onChange() { 23 | if (this.isMounted()) { 24 | this.setState({ 25 | activeSnippet: SnippetStore.getActiveSnippet(), 26 | languages: SettingsStore.getLanguages(), 27 | syntax: SettingsStore.getSyntax() 28 | }); 29 | } 30 | }, 31 | 32 | componentDidMount() { 33 | SnippetStore.addChangeListener(this._onChange); 34 | SettingsStore.addChangeListener(this._onChange); 35 | }, 36 | 37 | componentWillUnmount() { 38 | SnippetStore.removeChangeListener(this._onChange); 39 | SettingsStore.removeChangeListener(this._onChange); 40 | }, 41 | 42 | render() { 43 | let activeSnippet = this.state.activeSnippet; 44 | let syntax = this.state.syntax; 45 | let text = null; 46 | let language = ''; 47 | 48 | if (activeSnippet) { 49 | text = activeSnippet.text; 50 | language = (!syntax || activeSnippet.lang === 'text' ? 'nohighlight' : activeSnippet.lang); 51 | } 52 | 53 | return( 54 |
55 |
56 |
57 |
 {
60 |                   if (preElement) hljs.highlightBlock(preElement);
61 |                 }
62 |               }>
63 |               {text}
64 |             
65 |
66 | 67 | 70 |
71 |
72 | ); 73 | } 74 | }); 75 | 76 | module.exports = PreviewMode; 77 | -------------------------------------------------------------------------------- /src/components/SearchBar.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | 4 | const SearchBar = React.createClass({ 5 | propTypes: { 6 | filterSnippets: React.PropTypes.func.isRequired 7 | }, 8 | 9 | getInitialState() { 10 | return { 11 | showClearButton: false, 12 | KEY: { 13 | esc: 27 14 | } 15 | }; 16 | }, 17 | 18 | toggleClearButton(event) { 19 | this.state.showClearButton = event.target.value.length > 0; 20 | 21 | this.props.filterSnippets(event); 22 | }, 23 | 24 | clearSearchBar() { 25 | this.refs.search.value = ''; 26 | this.state.showClearButton = false; 27 | 28 | this.props.filterSnippets(); 29 | }, 30 | 31 | onKeyDown(event) { 32 | let keyPressed = event.which; 33 | 34 | if (keyPressed === this.state.KEY.esc) this.clearSearchBar(); 35 | }, 36 | 37 | render() { 38 | let clearButton = null; 39 | 40 | if (this.state.showClearButton) { 41 | clearButton = ( 42 | 45 | ); 46 | } 47 | 48 | return ( 49 |
50 | 57 | 58 | {clearButton} 59 |
60 | ); 61 | } 62 | }); 63 | 64 | module.exports = SearchBar; 65 | -------------------------------------------------------------------------------- /src/components/SearchItem.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SnippetActions = require('../actions/SnippetActions'); 4 | 5 | 6 | const SearchItem = React.createClass({ 7 | propTypes: { 8 | snippet: React.PropTypes.object.isRequired, 9 | isActive: React.PropTypes.bool 10 | }, 11 | 12 | render() { 13 | let snippet = this.props.snippet; 14 | let isActiveClass = this.props.isActive ? ' is-active' : ''; 15 | 16 | return ( 17 |
  • 18 |
    19 |

    20 | {snippet.title} 21 |

    22 | 23 |

    {snippet.tags.join(', ')}

    24 |
    25 |
  • 26 | ); 27 | } 28 | }); 29 | 30 | module.exports = SearchItem; 31 | -------------------------------------------------------------------------------- /src/components/SearchList.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const SnippetStore = require('../stores/SnippetStore'); 4 | 5 | const SearchBar = require('./SearchBar'); 6 | const SearchItem = require('./SearchItem'); 7 | 8 | 9 | const SearchList = React.createClass({ 10 | getInitialState() { 11 | return { 12 | snippets: SnippetStore.getSnippets(), 13 | activeSnippet: SnippetStore.getActiveSnippet(), 14 | filtered: null 15 | } 16 | }, 17 | 18 | _onChange() { 19 | if (this.isMounted()) { 20 | this.setState({ 21 | snippets: SnippetStore.getSnippets(), 22 | activeSnippet: SnippetStore.getActiveSnippet() 23 | }); 24 | } 25 | }, 26 | 27 | componentDidMount() { 28 | SnippetStore.addChangeListener(this._onChange); 29 | }, 30 | 31 | componentWillUnmount() { 32 | SnippetStore.removeChangeListener(this._onChange); 33 | }, 34 | 35 | filterSnippets(event) { 36 | let searchValue = event ? event.target.value.toLowerCase().trim() : null; 37 | 38 | if (!searchValue) { 39 | this.clearFilter(); 40 | return; 41 | } 42 | 43 | let filtered = this.state.snippets.filter(snippet => { 44 | // joined by a character unlikely to be inputted by the user 45 | let tagsString = snippet.tags.join('|'); 46 | 47 | let titleMatch = snippet.title.toLowerCase().indexOf(searchValue) > -1; 48 | let tagsMatch = null; 49 | let textMatch = null; 50 | 51 | if (!titleMatch) tagsMatch = tagsString.indexOf(searchValue) > -1; 52 | if (!titleMatch && !tagsMatch) textMatch = snippet.text.toLowerCase().indexOf(searchValue) > -1; 53 | 54 | // order of importance 55 | return titleMatch || tagsMatch || textMatch; 56 | }); 57 | 58 | this.setState({ 59 | filtered: filtered 60 | }); 61 | }, 62 | 63 | clearFilter() { 64 | this.setState({ 65 | filtered: null 66 | }); 67 | }, 68 | 69 | render() { 70 | let filteredList = this.state.filtered ? this.state.filtered : this.state.snippets; 71 | let activeSnippet = this.state.activeSnippet; 72 | 73 | let snippets = filteredList.map((snippet, index) => { 74 | return ( 75 | 79 | ); 80 | }); 81 | 82 | return ( 83 |
    84 |
    85 | 86 | 87 |
    88 |
      { 89 | snippets.length ? 90 | snippets : 91 |
      92 |

      No snippets...

      93 |
      94 | } 95 |
    96 |
    97 |
    98 | 99 |
    100 | 101 |
    102 |
    103 | ); 104 | } 105 | }); 106 | 107 | module.exports = SearchList; 108 | -------------------------------------------------------------------------------- /src/components/Tags.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const update = require('react-addons-update'); 3 | 4 | const Tags = React.createClass({ 5 | propTypes: { 6 | tags: React.PropTypes.array, 7 | max: React.PropTypes.oneOfType([ 8 | React.PropTypes.number, 9 | React.PropTypes.string 10 | ]), 11 | maxlength: React.PropTypes.oneOfType([ 12 | React.PropTypes.number, 13 | React.PropTypes.string 14 | ]), 15 | placeholder: React.PropTypes.string 16 | }, 17 | 18 | getDefaultProps() { 19 | return { 20 | placeholder: 'Add a tag...', 21 | maxlength: 20 22 | }; 23 | }, 24 | 25 | getInitialState() { 26 | return { 27 | tags: [], 28 | KEY: { 29 | backspace: 8, 30 | tab: 9, 31 | enter: 13 32 | }, 33 | // only allow letters, numbers and spaces inbetween words 34 | INVALID_CHARS: /[^a-zA-Z0-9 ]/g 35 | }; 36 | }, 37 | 38 | componentDidMount() { 39 | this.setTags(this.props.tags); 40 | }, 41 | 42 | componentWillReceiveProps(nextProps) { 43 | this.setTags(nextProps.tags); 44 | }, 45 | 46 | setTags(tags) { 47 | if (tags && tags.length) this.setState({ tags }); 48 | }, 49 | 50 | onKeyDown(event) { 51 | let keyPressed = event.which; 52 | 53 | if (keyPressed === this.state.KEY.enter || 54 | (keyPressed === this.state.KEY.tab && event.target.value)) { 55 | event.preventDefault(); 56 | this.updateTags(event); 57 | } else if (keyPressed === this.state.KEY.backspace) { 58 | let tags = this.state.tags; 59 | 60 | if (!event.target.value && tags.length) { 61 | this.deleteTag(tags[tags.length - 1]); 62 | } 63 | } 64 | }, 65 | 66 | clearInvalidChars(event) { 67 | let value = event.target.value; 68 | 69 | if (this.state.INVALID_CHARS.test(value)) { 70 | event.target.value = value.replace(this.state.INVALID_CHARS, ''); 71 | } else if (value.length > this.props.maxlength) { 72 | event.target.value = value.substr(0, this.props.maxlength); 73 | } 74 | }, 75 | 76 | updateTags(event) { 77 | if (!this.props.max || 78 | this.state.tags.length < parseInt(this.props.max)) { 79 | let value = event.target.value; 80 | 81 | if (!value) return; 82 | 83 | let tag = value.trim().toLowerCase(); 84 | 85 | if (tag && this.state.tags.indexOf(tag) < 0) { 86 | this.setState({ 87 | tags: update( 88 | this.state.tags, 89 | { 90 | $push: [tag] 91 | } 92 | ) 93 | }); 94 | } 95 | } 96 | 97 | event.target.value = ''; 98 | }, 99 | 100 | deleteTag(tag) { 101 | let index = this.state.tags.indexOf(tag); 102 | 103 | if (index >= 0) { 104 | this.setState({ 105 | tags: update( 106 | this.state.tags, 107 | { 108 | $splice: [[index, 1]] 109 | } 110 | ) 111 | }); 112 | } 113 | }, 114 | 115 | focusInput(event) { 116 | let children = event.target.children; 117 | 118 | if (children.length) children[children.length - 1].focus(); 119 | }, 120 | 121 | render() { 122 | let tags = this.state.tags.map((tag, index) => { 123 | return ( 124 | 125 | {tag} 126 | 129 | 130 | ); 131 | }); 132 | 133 | let placeholder = !this.props.max || tags.length < this.props.max ? this.props.placeholder : ''; 134 | 135 | return ( 136 |
    137 | {tags} 138 | 139 |
    140 | ); 141 | } 142 | }); 143 | 144 | module.exports = Tags; 145 | -------------------------------------------------------------------------------- /src/constants/SnippetBarConstants.js: -------------------------------------------------------------------------------- 1 | const keyMirror = require('keymirror'); 2 | 3 | module.exports = keyMirror({ 4 | LOAD_SNIPPETS: null, 5 | LOAD_SETTINGS: null, 6 | 7 | SNIPPET_CREATE: null, 8 | SNIPPET_UPDATE: null, 9 | SNIPPET_DELETE: null, 10 | SNIPPET_SET_ACTIVE: null, 11 | 12 | MODE_SET_ACTIVE: null, 13 | 14 | SYNTAX_TOGGLE: null 15 | }); 16 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const filePaths = { 4 | 'snippets': __dirname + '/storage/snippets.json', 5 | 'settings': __dirname + '/storage/settings.json' 6 | }; 7 | 8 | const noop = () => {}; 9 | 10 | let localSnippets = []; 11 | 12 | function read(file, callback = noop) { 13 | if (!file || !filePaths.hasOwnProperty(file)) return; 14 | 15 | fs.readFile(filePaths[file], (err, data) => { 16 | if (err) throw err; 17 | 18 | let jData = JSON.parse(data); 19 | 20 | if (file === 'snippets') localSnippets = jData; 21 | 22 | callback(jData); 23 | }); 24 | } 25 | 26 | function write(file, data, callback = noop) { 27 | if (!data || !file || !filePaths.hasOwnProperty(file)) return; 28 | 29 | let sData = JSON.stringify(data, null, 2); 30 | 31 | fs.writeFile(filePaths[file], sData, (err) => { 32 | if (err) throw err; 33 | 34 | if (file === 'snippets') localSnippets = data; 35 | 36 | callback(sData); 37 | }); 38 | } 39 | 40 | function snippetExists(text = '') { 41 | return text.length && localSnippets && localSnippets.some(snippet => text === snippet.text); 42 | } 43 | 44 | function settingsModel(syntaxHighlighting) { 45 | if (typeof syntaxHighlighting === 'undefined') return null; 46 | 47 | return { 48 | syntaxHighlighting 49 | }; 50 | } 51 | 52 | function snippetModel(title = 'untitled', text, tags = [], lang = 'text') { 53 | if (!text) return null; 54 | 55 | let now = Date.now(); 56 | 57 | let snippet = { 58 | id: 'ID' + now, 59 | title: title, 60 | text: text, 61 | tags: tags, 62 | lang: lang 63 | }; 64 | 65 | return snippet; 66 | } 67 | 68 | module.exports = { 69 | read, 70 | write, 71 | snippetExists, 72 | settingsModel, 73 | snippetModel 74 | }; 75 | -------------------------------------------------------------------------------- /src/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | const Dispatcher = require('flux').Dispatcher; 2 | 3 | module.exports = new Dispatcher(); 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ipcMain = require('electron').ipcMain; 4 | const menubar = require('menubar'); 5 | 6 | const Menu = require('menu'); 7 | 8 | const appPath = __dirname; 9 | 10 | const config = { 11 | openDevTools: false, 12 | title: 'snippets', 13 | icon: appPath + '/static/img/brackets.png', 14 | iconAlt: appPath + '/static/img/brackets-alt.png' 15 | }; 16 | 17 | const mb = menubar({ 18 | dir: appPath, 19 | icon: config.icon, 20 | width: 600, 21 | height: 370, 22 | preloadWindow: true, 23 | 'always-on-top': true, 24 | resizable: false 25 | }); 26 | 27 | const shortcuts = [{ 28 | submenu: [ 29 | { label: "Undo", accelerator: "CmdOrCctrl+Z", selector: "undo:" }, 30 | { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, 31 | { type: "separator" }, 32 | { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, 33 | { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, 34 | { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, 35 | { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" } 36 | ] 37 | }]; 38 | 39 | if (config.openDevTools) { 40 | mb.on('after-create-window', () => { 41 | mb.window.openDevTools(); 42 | }); 43 | } 44 | 45 | // Quit when all windows are closed. 46 | mb.app.on('window-all-closed', () => { 47 | if (process.platform != 'darwin') { 48 | mb.app.quit(); 49 | } 50 | }); 51 | 52 | // This method will be called when Electron has finished 53 | // initialization and is ready to create browser windows. 54 | mb.app.on('ready', () => { 55 | mb.tray.setToolTip(config.title); 56 | mb.tray.setPressedImage(config.iconAlt); 57 | 58 | Menu.setApplicationMenu(Menu.buildFromTemplate(shortcuts)); 59 | 60 | ipcMain.on('mb-app', (event, arg) => { 61 | if (arg === "quit") { 62 | console.log('goodbye!'); 63 | mb.app.quit(); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/mb.js: -------------------------------------------------------------------------------- 1 | const ipcRenderer = require('electron').ipcRenderer; 2 | 3 | function quit() { 4 | ipcRenderer.send('mb-app', 'quit'); 5 | } 6 | 7 | module.exports = { 8 | quit 9 | }; 10 | -------------------------------------------------------------------------------- /src/stores/SettingsStore.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const assign = require('react/lib/Object.assign'); 3 | 4 | const AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | const SnippetBarConstants = require('../constants/SnippetBarConstants'); 6 | 7 | const SettingsActions = require('../actions/SettingsActions'); 8 | 9 | const remote = require('electron').remote; 10 | const Menu = remote.Menu; 11 | const MenuItem = remote.MenuItem; 12 | const hljs = require('highlight.js'); 13 | 14 | const data = require('../data'); 15 | const mb = require('../mb'); 16 | 17 | const CHANGE_EVENT = 'change'; 18 | 19 | 20 | // 21 | // Store 22 | // 23 | const _languages = [ 24 | 'text', 25 | 'bash', 26 | 'csharp', 27 | 'c++', 28 | 'css', 29 | 'elm', 30 | 'go', 31 | 'html', 32 | 'haskell', 33 | 'java', 34 | 'javascript', 35 | 'json', 36 | 'markdown', 37 | 'obj-c', 38 | 'php', 39 | 'python', 40 | 'ruby', 41 | 'scss' 42 | ]; 43 | let _syntax = false; 44 | 45 | 46 | function getLanguages() { 47 | return _languages; 48 | } 49 | 50 | function getSyntax() { 51 | return _syntax; 52 | } 53 | 54 | function toggleSyntax() { 55 | _syntax = !_syntax; 56 | 57 | let settings = data.settingsModel(_syntax); 58 | 59 | if (settings) data.write('settings', settings); 60 | } 61 | 62 | function createElectronMenu() { 63 | let cog = document.getElementById('cog'); 64 | 65 | if (!cog) return; 66 | 67 | let menu = new Menu(); 68 | 69 | let separator = new MenuItem({ 70 | type: 'separator' 71 | }); 72 | 73 | let syntax = new MenuItem({ 74 | label: 'Syntax Highlighting', 75 | type: 'checkbox', 76 | checked: _syntax, 77 | click: SettingsActions.syntaxToggle 78 | }); 79 | 80 | let quit = new MenuItem({ 81 | label: 'Quit', 82 | click: mb.quit 83 | }); 84 | 85 | menu.append(syntax); 86 | menu.append(separator); 87 | menu.append(quit); 88 | 89 | cog.addEventListener('click', event => { 90 | event.preventDefault(); 91 | menu.popup(remote.getCurrentWindow()); 92 | }, false); 93 | } 94 | 95 | 96 | let SettingsStore = assign({}, EventEmitter.prototype, { 97 | getLanguages, 98 | getSyntax, 99 | 100 | init() { 101 | hljs.configure({ 102 | languages: _languages 103 | }); 104 | 105 | createElectronMenu(); 106 | }, 107 | 108 | emitChange() { 109 | SettingsStore.emit(CHANGE_EVENT); 110 | }, 111 | 112 | addChangeListener(callback) { 113 | SettingsStore.on(CHANGE_EVENT, callback); 114 | }, 115 | 116 | removeChangeListener(callback) { 117 | SettingsStore.removeListener(CHANGE_EVENT, callback); 118 | } 119 | }); 120 | 121 | SettingsStore.dispatchToken = AppDispatcher.register(action => { 122 | switch(action.actionType) { 123 | case SnippetBarConstants.LOAD_SETTINGS: 124 | if (action.settings) { 125 | _syntax = action.settings.syntaxHighlighting; 126 | SettingsStore.emitChange(); 127 | } 128 | break; 129 | 130 | case SnippetBarConstants.SYNTAX_TOGGLE: 131 | toggleSyntax(); 132 | SettingsStore.emitChange(); 133 | break; 134 | } 135 | }); 136 | 137 | module.exports = SettingsStore; 138 | -------------------------------------------------------------------------------- /src/stores/SnippetStore.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter; 2 | const assign = require('react/lib/Object.assign'); 3 | 4 | const AppDispatcher = require('../dispatcher/AppDispatcher'); 5 | const SnippetBarConstants = require('../constants/SnippetBarConstants'); 6 | 7 | const data = require('../data'); 8 | 9 | const CHANGE_EVENT = 'change'; 10 | const noop = function(){}; 11 | 12 | 13 | // 14 | // Store 15 | // 16 | const _modes = { 17 | empty: 'empty', 18 | preview: 'preview', 19 | add: 'add', 20 | edit: 'edit' 21 | }; 22 | let _activeMode = _modes.empty; 23 | let _activeSnippet = null; 24 | let _snippets = []; 25 | 26 | 27 | function getModes() { 28 | return _modes; 29 | } 30 | 31 | function getActiveMode() { 32 | return _activeMode; 33 | } 34 | 35 | function setActiveMode(mode, callback = noop) { 36 | let availableMode = _modes[mode]; 37 | 38 | if (availableMode) { 39 | if (availableMode === _modes.add) resetActiveSnippet(); 40 | 41 | _activeMode = availableMode; 42 | 43 | callback(); 44 | } 45 | } 46 | 47 | function getActiveSnippet() { 48 | return _activeSnippet; 49 | } 50 | 51 | function setActiveSnippet(id, callback = noop) { 52 | let snippet = getSnippetById(id); 53 | 54 | if (!_activeSnippet || _activeSnippet.id !== snippet.id) { 55 | if (_activeMode !== _modes.preview) _activeMode = _modes.preview; 56 | 57 | _activeSnippet = snippet; 58 | 59 | callback(); 60 | } 61 | } 62 | 63 | function resetActiveSnippet() { 64 | _activeSnippet = null; 65 | } 66 | 67 | function getSnippets() { 68 | return _snippets; 69 | } 70 | 71 | function getSnippetById(id) { 72 | let snippet = null; 73 | let len = _snippets.length; 74 | 75 | for (let i = 0; i < len; i++) { 76 | if (_snippets[i].id === id) { 77 | snippet = _snippets[i]; 78 | break; 79 | } 80 | } 81 | 82 | return snippet; 83 | } 84 | 85 | function create(values, callback = noop) { 86 | let isDuplicate = data.snippetExists(values.text); 87 | 88 | if (isDuplicate) return; 89 | 90 | let snippet = data.snippetModel(values.title, values.text, values.tags, values.lang); 91 | 92 | if (snippet) { 93 | _snippets.push(snippet); 94 | 95 | data.write('snippets', _snippets, () => { 96 | setActiveSnippet(snippet.id); 97 | setActiveMode(_modes.preview); 98 | 99 | callback(); 100 | }); 101 | } 102 | } 103 | 104 | function update(values, callback) { 105 | let userValues = data.snippetModel(values.title, values.text, values.tags, values.lang); 106 | 107 | if (!userValues) return; 108 | 109 | for (let i = 0; i < _snippets.length; i++) { 110 | if (_activeSnippet.id === _snippets[i].id) { 111 | 112 | _snippets[i].title = userValues.title; 113 | _snippets[i].text = userValues.text; 114 | _snippets[i].tags = userValues.tags; 115 | _snippets[i].lang = userValues.lang; 116 | 117 | data.write('snippets', _snippets, () => { 118 | setActiveSnippet(_activeSnippet.id); 119 | setActiveMode(_modes.preview); 120 | 121 | callback(); 122 | }); 123 | 124 | break; 125 | } 126 | } 127 | } 128 | 129 | function destroy(callback) { 130 | let len = _snippets.length; 131 | 132 | if (!_activeSnippet) return; 133 | 134 | for (let i = 0; i < len; i++) { 135 | if (_snippets[i].id === _activeSnippet.id) { 136 | resetActiveSnippet(); 137 | setActiveMode(_modes.empty); 138 | 139 | _snippets.splice(i, 1); 140 | 141 | data.write('snippets', _snippets, callback); 142 | 143 | break; 144 | } 145 | } 146 | } 147 | 148 | 149 | let SnippetStore = assign({}, EventEmitter.prototype, { 150 | getModes, 151 | getActiveMode, 152 | getActiveSnippet, 153 | getSnippets, 154 | getSnippetById, 155 | 156 | emitChange() { 157 | SnippetStore.emit(CHANGE_EVENT); 158 | }, 159 | 160 | addChangeListener(callback) { 161 | SnippetStore.on(CHANGE_EVENT, callback); 162 | }, 163 | 164 | removeChangeListener(callback) { 165 | SnippetStore.removeListener(CHANGE_EVENT, callback); 166 | } 167 | }); 168 | 169 | SnippetStore.dispatchToken = AppDispatcher.register(action => { 170 | switch(action.actionType) { 171 | case SnippetBarConstants.LOAD_SNIPPETS: 172 | if (action.snippets) { 173 | _snippets = action.snippets; 174 | SnippetStore.emitChange(); 175 | } 176 | break; 177 | 178 | case SnippetBarConstants.SNIPPET_CREATE: 179 | create(action.values, () => { 180 | action.callback(); 181 | SnippetStore.emitChange(); 182 | }); 183 | break; 184 | 185 | case SnippetBarConstants.SNIPPET_UPDATE: 186 | update(action.values, () => { 187 | action.callback(); 188 | SnippetStore.emitChange(); 189 | }); 190 | break; 191 | 192 | case SnippetBarConstants.SNIPPET_DELETE: 193 | destroy(() => { 194 | action.callback(); 195 | SnippetStore.emitChange(); 196 | }); 197 | break; 198 | 199 | case SnippetBarConstants.SNIPPET_SET_ACTIVE: 200 | setActiveSnippet(action.id, SnippetStore.emitChange); 201 | break; 202 | 203 | case SnippetBarConstants.MODE_SET_ACTIVE: 204 | setActiveMode(action.mode, SnippetStore.emitChange); 205 | break; 206 | } 207 | }); 208 | 209 | module.exports = SnippetStore; 210 | -------------------------------------------------------------------------------- /src/utils/SnippetBarUtils.js: -------------------------------------------------------------------------------- 1 | const data = require('../data'); 2 | 3 | const SnippetActions = require('../actions/SnippetActions'); 4 | const SettingsActions = require('../actions/SettingsActions'); 5 | 6 | const noop = function(){}; 7 | 8 | module.exports = { 9 | readSnippets(callback = noop) { 10 | data.read('snippets', snippets => { 11 | SnippetActions.load(snippets); 12 | callback(snippets); 13 | }); 14 | }, 15 | 16 | readSettings(callback = noop) { 17 | data.read('settings', settings => { 18 | SettingsActions.load(settings); 19 | callback(settings); 20 | }); 21 | } 22 | }; 23 | --------------------------------------------------------------------------------