├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── boards.icns ├── github.js ├── index.html ├── main.js ├── menus.js ├── package.json └── prompt.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Ramsey Nasser 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGER-FLAGS=. Boards --asar --ignore "out" 2 | 3 | all: darwin windows linux 4 | 5 | darwin: Boards-darwin-x64 6 | Boards-darwin-x64: 7 | electron-packager $(PACKAGER-FLAGS) --icon=boards.icns --platform=darwin --arch=x64 8 | mkdir -p out 9 | cp -r Boards-darwin-x64 out/ 10 | rm -fr Boards-darwin-x64 11 | 12 | linux: Boards-linux-ia32 Boards-linux-x64 13 | Boards-linux-ia32: 14 | electron-packager $(PACKAGER-FLAGS) --platform=linux --arch=ia32 15 | mkdir -p out 16 | cp -r Boards-linux-ia32 out/ 17 | rm -fr Boards-linux-ia32 18 | 19 | Boards-linux-x64: 20 | electron-packager $(PACKAGER-FLAGS) --platform=linux --arch=x64 21 | mkdir -p out 22 | cp -r Boards-linux-x64 out/ 23 | rm -fr Boards-linux-x64 24 | 25 | windows: Boards-win32-ia32 Boards-win32-x64 26 | Boards-win32-ia32: 27 | electron-packager $(PACKAGER-FLAGS) --platform=win32 --arch=ia32 28 | mkdir -p out 29 | cp -r Boards-win32-ia32 out/ 30 | rm -fr Boards-win32-ia32 31 | 32 | Boards-win32-x64: 33 | electron-packager $(PACKAGER-FLAGS) --platform=win32 --arch=x64 34 | mkdir -p out 35 | cp -r Boards-win32-x64 out/ 36 | rm -fr Boards-win32-x64 37 | 38 | clean: 39 | rm -fr Boards-* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boards 2 | 3 | Infinite whiteboards 4 | 5 | ![](http://i.imgur.com/WSaE9LJ.gif) 6 | 7 | ## Status 8 | 9 | Not finished 10 | 11 | ## Usage 12 | 13 | Best used with a tablet. Boards are saved to local storage and restored whenever you start the app. 14 | 15 | * Click and drag to draw. There is only one color and thickness. There is no eraser. 16 | * Right click and drag the mouse or two-finger scroll your trackpad to move the drawing around. The space is infinite. 17 | * Press `Cmd+Z` to undo your last pen stroke. You have infinite undos. There is no redo. 18 | * Press `Cmd+N` to start a new blank board. 19 | * Press `Cmd+U` to upload to a gist. You will be prompted for your GitHub username, password, and title. A shareable [RawGit](https://rawgit.com/) URL will be opened in your default browser. 20 | * Press `Cmd+S` to save the board to disk. 21 | 22 | ## Building 23 | 24 | ``` 25 | $ npm install 26 | $ make 27 | ``` 28 | 29 | Requires `npm` and [`electron-packager`](https://github.com/electron-userland/electron-packager). Only tested on OSX. Prebuilt packages soon. 30 | 31 | ## License 32 | 33 | Copyright © Ramsey Nasser 2015-2016. Provided under the [MIT License](http://opensource.org/licenses/MIT). 34 | -------------------------------------------------------------------------------- /boards.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasser/boards/83be9ef2188d3d8bac8d2dbc503a518516005e1a/boards.icns -------------------------------------------------------------------------------- /github.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @overview Github.js 3 | * 4 | * @copyright (c) 2013 Michael Aufreiter, Development Seed 5 | * Github.js is freely distributable. 6 | * 7 | * @license Licensed under MIT license 8 | * 9 | * For all details and documentation: 10 | * http://substance.io/michael/github 11 | */ 12 | 13 | (function() { 14 | 'use strict'; 15 | 16 | // Initial Setup 17 | // ------------- 18 | 19 | var XMLHttpRequest, btoa; 20 | /* istanbul ignore else */ 21 | if (typeof exports !== 'undefined') { 22 | XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; 23 | if (typeof btoa === 'undefined') { 24 | btoa = require('btoa'); //jshint ignore:line 25 | } 26 | } else { 27 | btoa = window.btoa; 28 | } 29 | 30 | //prefer native XMLHttpRequest always 31 | /* istanbul ignore if */ 32 | if (typeof window !== 'undefined' && typeof window.XMLHttpRequest !== 'undefined'){ 33 | XMLHttpRequest = window.XMLHttpRequest; 34 | } 35 | 36 | 37 | 38 | var Github = function(options) { 39 | var API_URL = options.apiUrl || 'https://api.github.com'; 40 | 41 | // HTTP Request Abstraction 42 | // ======= 43 | // 44 | // I'm not proud of this and neither should you be if you were responsible for the XMLHttpRequest spec. 45 | 46 | function _request(method, path, data, cb, raw, sync) { 47 | function getURL() { 48 | var url = path.indexOf('//') >= 0 ? path : API_URL + path; 49 | url += ((/\?/).test(url) ? '&' : '?'); 50 | // Fix #195 about XMLHttpRequest.send method and GET/HEAD request 51 | if (data && typeof data === "object" && ['GET', 'HEAD'].indexOf(method) > -1) { 52 | url += '&' + Object.keys(data).map(function (k) { 53 | return k + '=' + data[k]; 54 | }).join('&'); 55 | } 56 | return url + '&' + (new Date()).getTime(); 57 | } 58 | 59 | var xhr = new XMLHttpRequest(); 60 | 61 | 62 | xhr.open(method, getURL(), !sync); 63 | if (!sync) { 64 | xhr.onreadystatechange = function () { 65 | if (this.readyState === 4) { 66 | if (this.status >= 200 && this.status < 300 || this.status === 304) { 67 | cb(null, raw ? this.responseText : this.responseText ? JSON.parse(this.responseText) : true, this); 68 | } else { 69 | cb({path: path, request: this, error: this.status}); 70 | } 71 | } 72 | }; 73 | } 74 | 75 | if (!raw) { 76 | xhr.dataType = 'json'; 77 | xhr.setRequestHeader('Accept','application/vnd.github.v3+json'); 78 | } else { 79 | xhr.setRequestHeader('Accept','application/vnd.github.v3.raw+json'); 80 | } 81 | 82 | xhr.setRequestHeader('Content-Type','application/json;charset=UTF-8'); 83 | if ((options.token) || (options.username && options.password)) { 84 | var authorization = options.token ? 'token ' + options.token : 'Basic ' + btoa(options.username + ':' + options.password); 85 | xhr.setRequestHeader('Authorization', authorization); 86 | } 87 | if (data) { 88 | xhr.send(JSON.stringify(data)); 89 | } else { 90 | xhr.send(); 91 | } 92 | if (sync) { 93 | return xhr.response; 94 | } 95 | } 96 | 97 | function _requestAllPages(path, cb) { 98 | var results = []; 99 | (function iterate() { 100 | _request('GET', path, null, function(err, res, xhr) { 101 | if (err) { 102 | return cb(err); 103 | } 104 | 105 | results.push.apply(results, res); 106 | 107 | var links = (xhr.getResponseHeader('link') || '').split(/\s*,\s*/g), 108 | next = null; 109 | links.forEach(function(link) { 110 | next = /rel="next"/.test(link) ? link : next; 111 | }); 112 | 113 | if (next) { 114 | next = (/<(.*)>/.exec(next) || [])[1]; 115 | } 116 | 117 | if (!next) { 118 | cb(err, results); 119 | } else { 120 | path = next; 121 | iterate(); 122 | } 123 | }); 124 | })(); 125 | } 126 | 127 | 128 | // User API 129 | // ======= 130 | 131 | Github.User = function() { 132 | this.repos = function(cb) { 133 | // Github does not always honor the 1000 limit so we want to iterate over the data set. 134 | _requestAllPages('/user/repos?type=all&per_page=1000&sort=updated', function(err, res) { 135 | cb(err, res); 136 | }); 137 | }; 138 | 139 | // List user organizations 140 | // ------- 141 | 142 | this.orgs = function(cb) { 143 | _request("GET", '/user/orgs', null, function(err, res) { 144 | cb(err, res); 145 | }); 146 | }; 147 | 148 | // List authenticated user's gists 149 | // ------- 150 | 151 | this.gists = function(cb) { 152 | _request("GET", '/gists', null, function(err, res) { 153 | cb(err,res); 154 | }); 155 | }; 156 | 157 | // List authenticated user's unread notifications 158 | // ------- 159 | 160 | this.notifications = function(cb) { 161 | _request("GET", '/notifications', null, function(err, res) { 162 | cb(err,res); 163 | }); 164 | }; 165 | 166 | // Show user information 167 | // ------- 168 | 169 | this.show = function(username, cb) { 170 | var command = username ? '/users/' + username : '/user'; 171 | 172 | _request('GET', command, null, function(err, res) { 173 | cb(err, res); 174 | }); 175 | }; 176 | 177 | // List user repositories 178 | // ------- 179 | 180 | this.userRepos = function(username, cb) { 181 | // Github does not always honor the 1000 limit so we want to iterate over the data set. 182 | _requestAllPages('/users/' + username + '/repos?type=all&per_page=1000&sort=updated', function(err, res) { 183 | cb(err, res); 184 | }); 185 | }; 186 | 187 | // List a user's gists 188 | // ------- 189 | 190 | this.userGists = function(username, cb) { 191 | _request('GET', '/users/' + username + '/gists', null, function(err, res) { 192 | cb(err,res); 193 | }); 194 | }; 195 | 196 | // List organization repositories 197 | // ------- 198 | 199 | this.orgRepos = function(orgname, cb) { 200 | // Github does not always honor the 1000 limit so we want to iterate over the data set. 201 | _requestAllPages('/orgs/' + orgname + '/repos?type=all&&page_num=1000&sort=updated&direction=desc', function(err, res) { 202 | cb(err, res); 203 | }); 204 | }; 205 | 206 | // Follow user 207 | // ------- 208 | 209 | this.follow = function(username, cb) { 210 | _request('PUT', '/user/following/' + username, null, function(err, res) { 211 | cb(err, res); 212 | }); 213 | }; 214 | 215 | // Unfollow user 216 | // ------- 217 | 218 | this.unfollow = function(username, cb) { 219 | _request('DELETE', '/user/following/' + username, null, function(err, res) { 220 | cb(err, res); 221 | }); 222 | }; 223 | 224 | // Create a repo 225 | // ------- 226 | this.createRepo = function(options, cb) { 227 | _request('POST', '/user/repos', options, cb); 228 | }; 229 | 230 | }; 231 | 232 | // Repository API 233 | // ======= 234 | 235 | Github.Repository = function(options) { 236 | var repo = options.name; 237 | var user = options.user; 238 | 239 | var that = this; 240 | var repoPath = '/repos/' + user + '/' + repo; 241 | 242 | var currentTree = { 243 | 'branch': null, 244 | 'sha': null 245 | }; 246 | 247 | 248 | // Delete a repo 249 | // -------- 250 | 251 | this.deleteRepo = function(cb) { 252 | _request('DELETE', repoPath, options, cb); 253 | }; 254 | 255 | // Uses the cache if branch has not been changed 256 | // ------- 257 | 258 | function updateTree(branch, cb) { 259 | if (branch === currentTree.branch && currentTree.sha) { 260 | return cb(null, currentTree.sha); 261 | } 262 | 263 | that.getRef('heads/' + branch, function(err, sha) { 264 | currentTree.branch = branch; 265 | currentTree.sha = sha; 266 | cb(err, sha); 267 | }); 268 | } 269 | 270 | // Get a particular reference 271 | // ------- 272 | 273 | this.getRef = function(ref, cb) { 274 | _request('GET', repoPath + '/git/refs/' + ref, null, function(err, res) { 275 | if (err) { 276 | return cb(err); 277 | } 278 | 279 | cb(null, res.object.sha); 280 | }); 281 | }; 282 | 283 | // Create a new reference 284 | // -------- 285 | // 286 | // { 287 | // "ref": "refs/heads/my-new-branch-name", 288 | // "sha": "827efc6d56897b048c772eb4087f854f46256132" 289 | // } 290 | 291 | this.createRef = function(options, cb) { 292 | _request('POST', repoPath + '/git/refs', options, cb); 293 | }; 294 | 295 | // Delete a reference 296 | // -------- 297 | // 298 | // repo.deleteRef('heads/gh-pages') 299 | // repo.deleteRef('tags/v1.0') 300 | 301 | this.deleteRef = function(ref, cb) { 302 | _request('DELETE', repoPath + '/git/refs/' + ref, options, cb); 303 | }; 304 | 305 | // Create a repo 306 | // ------- 307 | 308 | this.createRepo = function(options, cb) { 309 | _request('POST', '/user/repos', options, cb); 310 | }; 311 | 312 | // Delete a repo 313 | // -------- 314 | 315 | this.deleteRepo = function(cb) { 316 | _request('DELETE', repoPath, options, cb); 317 | }; 318 | 319 | // List all tags of a repository 320 | // ------- 321 | 322 | this.listTags = function(cb) { 323 | _request('GET', repoPath + '/tags', null, function(err, tags) { 324 | if (err) { 325 | return cb(err); 326 | } 327 | 328 | cb(null, tags); 329 | }); 330 | }; 331 | 332 | // List all pull requests of a respository 333 | // ------- 334 | 335 | this.listPulls = function(state, cb) { 336 | _request('GET', repoPath + "/pulls" + (state ? '?state=' + state : ''), null, function(err, pulls) { 337 | if (err) return cb(err); 338 | cb(null, pulls); 339 | }); 340 | }; 341 | 342 | // Gets details for a specific pull request 343 | // ------- 344 | 345 | this.getPull = function(number, cb) { 346 | _request("GET", repoPath + "/pulls/" + number, null, function(err, pull) { 347 | if (err) return cb(err); 348 | cb(null, pull); 349 | }); 350 | }; 351 | 352 | // Retrieve the changes made between base and head 353 | // ------- 354 | 355 | this.compare = function(base, head, cb) { 356 | _request("GET", repoPath + "/compare/" + base + "..." + head, null, function(err, diff) { 357 | if (err) return cb(err); 358 | cb(null, diff); 359 | }); 360 | }; 361 | 362 | // List all branches of a repository 363 | // ------- 364 | 365 | this.listBranches = function(cb) { 366 | _request("GET", repoPath + "/git/refs/heads", null, function(err, heads) { 367 | if (err) return cb(err); 368 | cb(null, heads.map(function(head) { 369 | var headParts = head.ref.split('/'); 370 | return headParts[headParts.length - 1]; 371 | })); 372 | }); 373 | }; 374 | 375 | // Retrieve the contents of a blob 376 | // ------- 377 | 378 | this.getBlob = function(sha, cb) { 379 | _request("GET", repoPath + "/git/blobs/" + sha, null, cb, 'raw'); 380 | }; 381 | 382 | // For a given file path, get the corresponding sha (blob for files, tree for dirs) 383 | // ------- 384 | 385 | this.getCommit = function(branch, sha, cb) { 386 | _request("GET", repoPath + "/git/commits/"+sha, null, function(err, commit) { 387 | if (err) return cb(err); 388 | cb(null, commit); 389 | }); 390 | }; 391 | 392 | // For a given file path, get the corresponding sha (blob for files, tree for dirs) 393 | // ------- 394 | 395 | this.getSha = function(branch, path, cb) { 396 | if (!path || path === "") return that.getRef("heads/"+branch, cb); 397 | _request("GET", repoPath + "/contents/" + path + (branch ? "?ref=" + branch : ""), null, function(err, pathContent) { 398 | if (err) return cb(err); 399 | cb(null, pathContent.sha); 400 | }); 401 | }; 402 | 403 | // Retrieve the tree a commit points to 404 | // ------- 405 | 406 | this.getTree = function(tree, cb) { 407 | _request("GET", repoPath + "/git/trees/"+tree, null, function(err, res) { 408 | if (err) return cb(err); 409 | cb(null, res.tree); 410 | }); 411 | }; 412 | 413 | // Post a new blob object, getting a blob SHA back 414 | // ------- 415 | 416 | this.postBlob = function(content, cb) { 417 | if (typeof(content) === "string") { 418 | content = { 419 | "content": content, 420 | "encoding": "utf-8" 421 | }; 422 | } else { 423 | content = { 424 | "content": btoa(String.fromCharCode.apply(null, new Uint8Array(content))), 425 | "encoding": "base64" 426 | }; 427 | } 428 | 429 | _request("POST", repoPath + "/git/blobs", content, function(err, res) { 430 | if (err) return cb(err); 431 | cb(null, res.sha); 432 | }); 433 | }; 434 | 435 | // Update an existing tree adding a new blob object getting a tree SHA back 436 | // ------- 437 | 438 | this.updateTree = function(baseTree, path, blob, cb) { 439 | var data = { 440 | "base_tree": baseTree, 441 | "tree": [ 442 | { 443 | "path": path, 444 | "mode": "100644", 445 | "type": "blob", 446 | "sha": blob 447 | } 448 | ] 449 | }; 450 | _request("POST", repoPath + "/git/trees", data, function(err, res) { 451 | if (err) return cb(err); 452 | cb(null, res.sha); 453 | }); 454 | }; 455 | 456 | // Post a new tree object having a file path pointer replaced 457 | // with a new blob SHA getting a tree SHA back 458 | // ------- 459 | 460 | this.postTree = function(tree, cb) { 461 | _request("POST", repoPath + "/git/trees", { "tree": tree }, function(err, res) { 462 | if (err) return cb(err); 463 | cb(null, res.sha); 464 | }); 465 | }; 466 | 467 | // Create a new commit object with the current commit SHA as the parent 468 | // and the new tree SHA, getting a commit SHA back 469 | // ------- 470 | 471 | this.commit = function(parent, tree, message, cb) { 472 | var user = new Github.User(); 473 | user.show(null, function(err, userData){ 474 | if (err) return cb(err); 475 | var data = { 476 | "message": message, 477 | "author": { 478 | "name": options.user, 479 | "email": userData.email 480 | }, 481 | "parents": [ 482 | parent 483 | ], 484 | "tree": tree 485 | }; 486 | _request("POST", repoPath + "/git/commits", data, function(err, res) { 487 | if (err) return cb(err); 488 | currentTree.sha = res.sha; // update latest commit 489 | cb(null, res.sha); 490 | }); 491 | }); 492 | }; 493 | 494 | // Update the reference of your head to point to the new commit SHA 495 | // ------- 496 | 497 | this.updateHead = function(head, commit, cb) { 498 | _request("PATCH", repoPath + "/git/refs/heads/" + head, { "sha": commit }, function(err) { 499 | cb(err); 500 | }); 501 | }; 502 | 503 | // Show repository information 504 | // ------- 505 | 506 | this.show = function(cb) { 507 | _request("GET", repoPath, null, cb); 508 | }; 509 | 510 | // Show repository contributors 511 | // ------- 512 | 513 | this.contributors = function (cb, retry) { 514 | retry = retry || 1000; 515 | var self = this; 516 | _request("GET", repoPath + "/stats/contributors", null, function (err, data, response) { 517 | if (err) return cb(err); 518 | if (response.status === 202) { 519 | setTimeout( 520 | function () { 521 | self.contributors(cb, retry); 522 | }, 523 | retry 524 | ); 525 | } else { 526 | cb(err, data); 527 | } 528 | }); 529 | }; 530 | 531 | // Get contents 532 | // -------- 533 | 534 | this.contents = function(ref, path, cb) { 535 | path = encodeURI(path); 536 | _request("GET", repoPath + "/contents" + (path ? "/" + path : ""), { ref: ref }, cb); 537 | }; 538 | 539 | // Fork repository 540 | // ------- 541 | 542 | this.fork = function(cb) { 543 | _request("POST", repoPath + "/forks", null, cb); 544 | }; 545 | 546 | // Branch repository 547 | // -------- 548 | 549 | this.branch = function(oldBranch,newBranch,cb) { 550 | if(arguments.length === 2 && typeof arguments[1] === "function") { 551 | cb = newBranch; 552 | newBranch = oldBranch; 553 | oldBranch = "master"; 554 | } 555 | this.getRef("heads/" + oldBranch, function(err,ref) { 556 | if(err && cb) return cb(err); 557 | that.createRef({ 558 | ref: "refs/heads/" + newBranch, 559 | sha: ref 560 | },cb); 561 | }); 562 | }; 563 | 564 | // Create pull request 565 | // -------- 566 | 567 | this.createPullRequest = function(options, cb) { 568 | _request("POST", repoPath + "/pulls", options, cb); 569 | }; 570 | 571 | // List hooks 572 | // -------- 573 | 574 | this.listHooks = function(cb) { 575 | _request("GET", repoPath + "/hooks", null, cb); 576 | }; 577 | 578 | // Get a hook 579 | // -------- 580 | 581 | this.getHook = function(id, cb) { 582 | _request("GET", repoPath + "/hooks/" + id, null, cb); 583 | }; 584 | 585 | // Create a hook 586 | // -------- 587 | 588 | this.createHook = function(options, cb) { 589 | _request("POST", repoPath + "/hooks", options, cb); 590 | }; 591 | 592 | // Edit a hook 593 | // -------- 594 | 595 | this.editHook = function(id, options, cb) { 596 | _request("PATCH", repoPath + "/hooks/" + id, options, cb); 597 | }; 598 | 599 | // Delete a hook 600 | // -------- 601 | 602 | this.deleteHook = function(id, cb) { 603 | _request("DELETE", repoPath + "/hooks/" + id, null, cb); 604 | }; 605 | 606 | // Read file at given path 607 | // ------- 608 | 609 | this.read = function(branch, path, cb) { 610 | _request("GET", repoPath + "/contents/"+encodeURI(path) + (branch ? "?ref=" + branch : ""), null, function(err, obj) { 611 | if (err && err.error === 404) return cb("not found", null, null); 612 | 613 | if (err) return cb(err); 614 | cb(null, obj); 615 | }, true); 616 | }; 617 | 618 | 619 | // Remove a file 620 | // ------- 621 | 622 | this.remove = function(branch, path, cb) { 623 | that.getSha(branch, path, function(err, sha) { 624 | if (err) return cb(err); 625 | _request("DELETE", repoPath + "/contents/" + path, { 626 | message: path + " is removed", 627 | sha: sha, 628 | branch: branch 629 | }, cb); 630 | }); 631 | }; 632 | 633 | // Delete a file from the tree 634 | // ------- 635 | 636 | this.delete = function(branch, path, cb) { 637 | that.getSha(branch, path, function(err, sha) { 638 | if (!sha) return cb("not found", null); 639 | var delPath = repoPath + "/contents/" + path; 640 | var params = { 641 | "message": "Deleted " + path, 642 | "sha": sha 643 | }; 644 | delPath += "?message=" + encodeURIComponent(params.message); 645 | delPath += "&sha=" + encodeURIComponent(params.sha); 646 | delPath += '&branch=' + encodeURIComponent(branch); 647 | _request("DELETE", delPath, null, cb); 648 | }); 649 | }; 650 | 651 | // Move a file to a new location 652 | // ------- 653 | 654 | this.move = function(branch, path, newPath, cb) { 655 | updateTree(branch, function(err, latestCommit) { 656 | that.getTree(latestCommit+"?recursive=true", function(err, tree) { 657 | // Update Tree 658 | tree.forEach(function(ref) { 659 | if (ref.path === path) ref.path = newPath; 660 | if (ref.type === "tree") delete ref.sha; 661 | }); 662 | 663 | that.postTree(tree, function(err, rootTree) { 664 | that.commit(latestCommit, rootTree, 'Deleted '+path , function(err, commit) { 665 | that.updateHead(branch, commit, function(err) { 666 | cb(err); 667 | }); 668 | }); 669 | }); 670 | }); 671 | }); 672 | }; 673 | 674 | // Write file contents to a given branch and path 675 | // ------- 676 | 677 | this.write = function(branch, path, content, message, cb) { 678 | that.getSha(branch, encodeURI(path), function(err, sha) { 679 | if (err && err.error !== 404) return cb(err); 680 | _request("PUT", repoPath + "/contents/" + encodeURI(path), { 681 | message: message, 682 | content: btoa(content), 683 | branch: branch, 684 | sha: sha 685 | }, cb); 686 | }); 687 | }; 688 | 689 | // List commits on a repository. Takes an object of optional paramaters: 690 | // sha: SHA or branch to start listing commits from 691 | // path: Only commits containing this file path will be returned 692 | // since: ISO 8601 date - only commits after this date will be returned 693 | // until: ISO 8601 date - only commits before this date will be returned 694 | // ------- 695 | 696 | this.getCommits = function(options, cb) { 697 | options = options || {}; 698 | var url = repoPath + "/commits"; 699 | var params = []; 700 | if (options.sha) { 701 | params.push("sha=" + encodeURIComponent(options.sha)); 702 | } 703 | if (options.path) { 704 | params.push("path=" + encodeURIComponent(options.path)); 705 | } 706 | if (options.since) { 707 | var since = options.since; 708 | if (since.constructor === Date) { 709 | since = since.toISOString(); 710 | } 711 | params.push("since=" + encodeURIComponent(since)); 712 | } 713 | if (options.until) { 714 | var until = options.until; 715 | if (until.constructor === Date) { 716 | until = until.toISOString(); 717 | } 718 | params.push("until=" + encodeURIComponent(until)); 719 | } 720 | if (options.page) { 721 | params.push("page=" + options.page); 722 | } 723 | if (options.perpage) { 724 | params.push("per_page=" + options.perpage); 725 | } 726 | if (params.length > 0) { 727 | url += "?" + params.join("&"); 728 | } 729 | _request("GET", url, null, cb); 730 | }; 731 | }; 732 | 733 | // Gists API 734 | // ======= 735 | 736 | Github.Gist = function(options) { 737 | var id = options.id; 738 | var gistPath = "/gists/"+id; 739 | 740 | // Read the gist 741 | // -------- 742 | 743 | this.read = function(cb) { 744 | _request("GET", gistPath, null, function(err, gist) { 745 | cb(err, gist); 746 | }); 747 | }; 748 | 749 | // Create the gist 750 | // -------- 751 | // { 752 | // "description": "the description for this gist", 753 | // "public": true, 754 | // "files": { 755 | // "file1.txt": { 756 | // "content": "String file contents" 757 | // } 758 | // } 759 | // } 760 | 761 | this.create = function(options, cb){ 762 | _request("POST","/gists", options, cb); 763 | }; 764 | 765 | // Delete the gist 766 | // -------- 767 | 768 | this.delete = function(cb) { 769 | _request("DELETE", gistPath, null, function(err,res) { 770 | cb(err,res); 771 | }); 772 | }; 773 | 774 | // Fork a gist 775 | // -------- 776 | 777 | this.fork = function(cb) { 778 | _request("POST", gistPath+"/fork", null, function(err,res) { 779 | cb(err,res); 780 | }); 781 | }; 782 | 783 | // Update a gist with the new stuff 784 | // -------- 785 | 786 | this.update = function(options, cb) { 787 | _request("PATCH", gistPath, options, function(err,res) { 788 | cb(err,res); 789 | }); 790 | }; 791 | 792 | // Star a gist 793 | // -------- 794 | 795 | this.star = function(cb) { 796 | _request("PUT", gistPath+"/star", null, function(err,res) { 797 | cb(err,res); 798 | }); 799 | }; 800 | 801 | // Untar a gist 802 | // -------- 803 | 804 | this.unstar = function(cb) { 805 | _request("DELETE", gistPath+"/star", null, function(err,res) { 806 | cb(err,res); 807 | }); 808 | }; 809 | 810 | // Check if a gist is starred 811 | // -------- 812 | 813 | this.isStarred = function(cb) { 814 | _request("GET", gistPath+"/star", null, function(err,res) { 815 | cb(err,res); 816 | }); 817 | }; 818 | }; 819 | 820 | // Issues API 821 | // ========== 822 | 823 | Github.Issue = function(options) { 824 | var path = "/repos/" + options.user + "/" + options.repo + "/issues"; 825 | 826 | this.list = function(options, cb) { 827 | var query = []; 828 | for (var key in options) { 829 | if (options.hasOwnProperty(key)) { 830 | query.push(encodeURIComponent(key) + "=" + encodeURIComponent(options[key])); 831 | } 832 | } 833 | _requestAllPages(path + '?' + query.join("&"), cb); 834 | }; 835 | }; 836 | 837 | // Top Level API 838 | // ------- 839 | 840 | this.getIssues = function(user, repo) { 841 | return new Github.Issue({user: user, repo: repo}); 842 | }; 843 | 844 | this.getRepo = function(user, repo) { 845 | return new Github.Repository({user: user, name: repo}); 846 | }; 847 | 848 | this.getUser = function() { 849 | return new Github.User(); 850 | }; 851 | 852 | this.getGist = function(id) { 853 | return new Github.Gist({id: id}); 854 | }; 855 | }; 856 | 857 | /* istanbul ignore else */ 858 | if (typeof exports !== 'undefined') { 859 | module.exports = Github; 860 | } else { 861 | window.Github = Github; 862 | } 863 | }).call(this); 864 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Boards 5 | 59 | 60 | 61 | 442 | 443 | 444 | 445 |
446 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {BrowserWindow, app} = require('electron'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | 5 | let win; 6 | 7 | function createWindow() { 8 | win = new BrowserWindow({width: 800, height: 600, titleBarStyle: 'hidden-inset'}); 9 | win.loadURL(`file:///${__dirname}/index.html`); 10 | } 11 | 12 | app.on('ready', createWindow); 13 | 14 | app.on('window-all-closed', () => { 15 | if (process.platform !== 'darwin') { 16 | app.quit(); 17 | } 18 | }); 19 | 20 | app.on('activate', () => { 21 | if (win === null) { 22 | createWindow(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /menus.js: -------------------------------------------------------------------------------- 1 | const {Menu, MenuItem} = require("electron").remote; 2 | 3 | const template = [ 4 | { 5 | label:'Board', 6 | submenu: [ 7 | { 8 | label: 'New', 9 | enabled: false 10 | }, 11 | { 12 | label: 'Clear', 13 | accelerator: 'CmdOrCtrl+N', 14 | click(item, focusedWindow) { 15 | newBoard(); 16 | } 17 | }, 18 | { 19 | label: 'Save HTML...', 20 | accelerator: 'CmdOrCtrl+S', 21 | click(item, focusedWindow) { 22 | download(); 23 | } 24 | }, 25 | { 26 | label: 'Save SVG...', 27 | enabled: false 28 | }, 29 | { 30 | label: 'Upload Gist...', 31 | accelerator: 'CmdOrCtrl+U', 32 | click(item, focusedWindow) { 33 | save(); 34 | } 35 | } 36 | ] 37 | }, 38 | { 39 | label: 'Edit', 40 | submenu: [ 41 | { 42 | label: 'Undo', 43 | accelerator: 'CmdOrCtrl+Z', 44 | click() { 45 | root().removeChild(lastMark()); 46 | } 47 | }, 48 | { 49 | type: 'separator' 50 | }, 51 | { 52 | label: 'Copy Image', 53 | enabled: false 54 | }, 55 | { 56 | label: 'Copy SVG', 57 | enabled: false 58 | }, 59 | // { 60 | // role: 'cut' 61 | // }, 62 | // { 63 | // role: 'copy' 64 | // }, 65 | { 66 | role: 'paste' 67 | }, 68 | // { 69 | // role: 'pasteandmatchstyle' 70 | // }, 71 | // { 72 | // role: 'delete' 73 | // }, 74 | // { 75 | // role: 'selectall' 76 | // }, 77 | ] 78 | }, 79 | { 80 | label: 'View', 81 | submenu: [ 82 | { 83 | label: 'Reload', 84 | accelerator: 'CmdOrCtrl+R', 85 | click(item, focusedWindow) { 86 | if (focusedWindow) focusedWindow.reload(); 87 | } 88 | }, 89 | { 90 | role: 'togglefullscreen' 91 | }, 92 | { 93 | label: 'Toggle Developer Tools', 94 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 95 | click(item, focusedWindow) { 96 | if (focusedWindow) 97 | focusedWindow.webContents.toggleDevTools(); 98 | } 99 | }, 100 | ] 101 | }, 102 | { 103 | role: 'window', 104 | submenu: [ 105 | { 106 | role: 'minimize' 107 | }, 108 | { 109 | role: 'close' 110 | }, 111 | ] 112 | }, 113 | { 114 | role: 'help', 115 | submenu: [ 116 | { 117 | label: 'Learn More', 118 | click() { require('electron').shell.openExternal('https://github.com/nasser/boards'); } 119 | }, 120 | { 121 | label: 'Open Introduction Board', 122 | enabled: false 123 | }, 124 | ] 125 | }, 126 | ]; 127 | 128 | if (process.platform === 'darwin') { 129 | const name = require('electron').remote.app.getName(); 130 | template.unshift({ 131 | label: name, 132 | submenu: [ 133 | { 134 | role: 'about' 135 | }, 136 | { 137 | type: 'separator' 138 | }, 139 | { 140 | role: 'services', 141 | submenu: [] 142 | }, 143 | { 144 | type: 'separator' 145 | }, 146 | { 147 | role: 'hide' 148 | }, 149 | { 150 | role: 'hideothers' 151 | }, 152 | { 153 | role: 'unhide' 154 | }, 155 | { 156 | type: 'separator' 157 | }, 158 | { 159 | role: 'quit' 160 | }, 161 | ] 162 | }); 163 | // Window menu. 164 | template[4].submenu = [ 165 | { 166 | label: 'Close', 167 | accelerator: 'CmdOrCtrl+W', 168 | role: 'close' 169 | }, 170 | { 171 | label: 'Minimize', 172 | accelerator: 'CmdOrCtrl+M', 173 | role: 'minimize' 174 | }, 175 | { 176 | label: 'Zoom', 177 | role: 'zoom' 178 | }, 179 | { 180 | type: 'separator' 181 | }, 182 | { 183 | label: 'Bring All to Front', 184 | role: 'front' 185 | } 186 | ]; 187 | } 188 | 189 | const menu = Menu.buildFromTemplate(template); 190 | Menu.setApplicationMenu(menu); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boards", 3 | "version": "0.0.1", 4 | "description": "Infinite Whiteboards", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nasser/boards.git" 12 | }, 13 | "author": "Ramsey Nasser", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/nasser/boards/issues" 17 | }, 18 | "homepage": "https://github.com/nasser/boards#readme", 19 | "devDependencies": { 20 | "electron-prebuilt": "^1.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /prompt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * simple sexy full screen prompts 3 | * 4 | * ramsey nasser, sep 2015 5 | */ 6 | 7 | function fullScreenPrompt (placeholder, type, cb) { 8 | var input = document.createElement("input"); 9 | input.setAttribute("type", type); 10 | input.setAttribute("placeholder", placeholder); 11 | input.setAttribute("class", "fullscreen"); 12 | input.setAttribute("style", "position: fixed;\ 13 | top: 0;\ 14 | left: 0;\ 15 | z-index: 100;\ 16 | width: 100%;\ 17 | height: 100%;\ 18 | text-align: center;\ 19 | border: 0;\ 20 | opacity: 0.9;\ 21 | font-size: 15vw;"); 22 | 23 | input.onkeydown = function(event) { 24 | if (event.keyCode == 13) { 25 | document.body.removeChild(input); 26 | cb(input.value); 27 | } 28 | } 29 | 30 | document.body.appendChild(input); 31 | input.focus(); 32 | } 33 | 34 | function textPrompt (placeholder, cb) { 35 | fullScreenPrompt(placeholder, "text", cb); 36 | } 37 | 38 | function passwordPrompt (placeholder, cb) { 39 | fullScreenPrompt(placeholder, "password", cb); 40 | } --------------------------------------------------------------------------------