├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── capt_disclosure.png ├── electron-modules.json ├── gulpfile.js ├── package.json └── src ├── app.js ├── browser ├── menu │ ├── appMenu.js │ └── submenus │ │ └── capture.js └── twitter.js ├── index.html ├── renderer ├── bootstrap.js ├── components │ ├── imageList.jsx │ └── main.jsx └── services │ ├── capture.js │ ├── timer.js │ └── twitterWrapper.js └── styles └── main.scss /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | dist/ 4 | release/ 5 | .serve/ 6 | cache/ 7 | node_modules/ 8 | bower_components/ 9 | qiita.md 10 | react-devtools/ 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yosuke Kurami 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-disclosure 2 | 3 | This is a sample application with [github Electron](http://electron.atom.io/). 4 | 5 | It captures your desktop by 5 minutes and posts tweet with the captured images. 6 | (Don't worry, post tweets requires your authentication) 7 | 8 | ![capture](./capt_disclosure.png) 9 | 10 | ## Why? 11 | This is useless app. I made it to tell the following: 12 | 13 | * How to capture desktop image in an Electron app 14 | * How to connect resources controlled by OAuth in an Electron app 15 | 16 | 17 | ## Install 18 | 19 | ```sh 20 | npm install 21 | ``` 22 | 23 | ## Run 24 | 25 | ```sh 26 | gulp serve 27 | ``` 28 | 29 | ### Enable develop menu 30 | 31 | Execute the following command, so you can use develop menu(Reload and Toggle dev tools) in the reneder process. 32 | 33 | ```sh 34 | export NODE_ENV=develop 35 | ``` 36 | 37 | ## Packaging 38 | 39 | ```sh 40 | gulp package 41 | ``` 42 | 43 | This task makes application distribution packages under the `./release` directory. 44 | 45 | ## License 46 | This software is released under the MIT License, see LICENSE.txt. 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-disclosure", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Quramy " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "font-awesome": "~4.3.0", 17 | "sanitize-css": "~1.1.0" 18 | }, 19 | "devDependencies": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /capt_disclosure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quramy/electron-disclosure/e8518b148c281e55731aa34ca8fce6a86d7b5385/capt_disclosure.png -------------------------------------------------------------------------------- /electron-modules.json: -------------------------------------------------------------------------------- 1 | [ 2 | "app", 3 | "auto-updater", 4 | "browser-window", 5 | "content-tracing", 6 | "dialog", 7 | "global-shortcut", 8 | "ipc", 9 | "menu", 10 | "menu-item", 11 | "power-monitor", 12 | "protocol", 13 | "tray", 14 | "remote", 15 | "web-frame", 16 | "clipboard", 17 | "crash-reporter", 18 | "native-image", 19 | "screen", 20 | "shell" 21 | ] 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var gulp = require('gulp'); 5 | var $ = require('gulp-load-plugins')(); 6 | var browserify = require('browserify'); 7 | var source = require('vinyl-source-stream'); 8 | var buffer = require('vinyl-buffer'); 9 | var merge = require('merge2'); 10 | var mainBowerFiles = require('main-bower-files'); 11 | var del = require('del'); 12 | var packageJson = require('./package.json'); 13 | var electronProcess = require('electron-connect').server.create(); 14 | var packager = require('electron-packager'); 15 | var path = require('path'); 16 | 17 | var serveDir = '.serve'; 18 | var distDir = 'dist'; 19 | var releaseDir = 'release'; 20 | 21 | // Compile *.scss files with sourcemaps 22 | gulp.task('compile:styles', function () { 23 | return gulp.src(['src/styles/**/*.scss']) 24 | .pipe($.sourcemaps.init()) 25 | .pipe($.sass()) 26 | .pipe($.sourcemaps.write('.')) 27 | .pipe(gulp.dest(serveDir + '/styles')) 28 | ; 29 | }); 30 | 31 | // Inject *.css(compiled and depedent) files into *.html 32 | gulp.task('inject:css', ['compile:styles'], function() { 33 | return gulp.src('src/**/*.html') 34 | .pipe($.inject(gulp.src(mainBowerFiles().concat([serveDir + '/styles/**/*.css'])), { 35 | relative: true, 36 | ignorePath: ['../.serve/'] 37 | })) 38 | .pipe(gulp.dest(serveDir)) 39 | ; 40 | }); 41 | 42 | // Transform from ES6 fashion JSX files to ES5 JavaScript files 43 | gulp.task('compile:scripts:watch', function (done) { 44 | gulp.src(['src/**/*.js', 'src/**/*.jsx']) 45 | .pipe($.watch(['src/**/*.js', 'src/**/*.jsx'])) 46 | //.pipe($.plumber()) 47 | .pipe($.sourcemaps.init()) 48 | .pipe($.babel({ 49 | stage: 0 50 | })) 51 | .pipe($.sourcemaps.write('.')) 52 | .pipe(gulp.dest(serveDir)) 53 | ; 54 | done(); 55 | }); 56 | 57 | gulp.task('compile:scripts', function () { 58 | return gulp.src(['src/**/*.js', 'src/**/*.jsx']) 59 | .pipe($.babel({ 60 | stage: 0 61 | })) 62 | .pipe($.uglify()) 63 | .pipe(gulp.dest(distDir)) 64 | ; 65 | }); 66 | 67 | // Copy font file. 68 | // You don't need copy *.svg, *eot, *.ttf. 69 | gulp.task('fonts', function () { 70 | return gulp.src(['bower_components/**/fonts/*.woff']) 71 | .pipe($.flatten()) 72 | .pipe(gulp.dest(distDir + '/fonts')) 73 | ; 74 | }); 75 | 76 | // Inject renderer bundle file and concatnate *.css files 77 | gulp.task('html', ['inject:css', 'fonts'], function () { 78 | var assets = $.useref.assets(); 79 | return gulp.src([serveDir + '/**/*.html']) 80 | .pipe(assets) 81 | //.pipe(gulpif('*.css', minifyCss())) 82 | .pipe(assets.restore()) 83 | .pipe($.useref()) 84 | .pipe(gulp.dest(distDir)) 85 | ; 86 | }); 87 | 88 | // Minify dependent modules. 89 | gulp.task('bundle:dependencies', function () { 90 | var streams = [], dependencies = []; 91 | var defaultModules = ['assert', 'buffer', 'console', 'constants', 'crypto', 'domain', 'events', 'http', 'https', 'os', 'path', 'punycode', 'querystring', 'stream', 'string_decoder', 'timers', 'tty', 'url', 'util', 'vm', 'zlib'], 92 | electronModules = ['app', 'auto-updater', 'browser-window', 'content-tracing', 'dialog', 'global-shortcut', 'ipc', 'menu', 'menu-item', 'power-monitor', 'protocol', 'tray', 'remote', 'web-frame', 'clipboard', 'crash-reporter', 'native-image', 'screen', 'shell']; 93 | 94 | // Because Electron's node integration, bundle files don't need to include browser-specific shim. 95 | var excludeModules = defaultModules.concat(electronModules); 96 | 97 | for(var name in packageJson.dependencies) { 98 | dependencies.push(name); 99 | } 100 | 101 | // create a list of dependencies' main files 102 | var modules = dependencies.map(function (dep) { 103 | var packageJson = require(dep + '/package.json'); 104 | var main; 105 | if(!packageJson.main) { 106 | main = ['index.js']; 107 | }else if(Array.isArray(packageJson.main)){ 108 | main = packageJson.main; 109 | }else{ 110 | main = [packageJson.main]; 111 | } 112 | return {name: dep, main: main.map(function (it) {return path.basename(it);})}; 113 | }); 114 | 115 | // add babel/polyfill module 116 | modules.push({name: 'babel', main: ['polyfill.js']}); 117 | 118 | // create bundle file and minify for each main files 119 | modules.forEach(function (it) { 120 | it.main.forEach(function (entry) { 121 | var b = browserify( 122 | 'node_modules/' + it.name + '/' + entry, { 123 | detectGlobal: false, 124 | standalone: entry 125 | }); 126 | excludeModules.forEach(function(moduleName) {b.exclude(moduleName)}); 127 | streams.push(b.bundle() 128 | .pipe(source(entry)) 129 | .pipe(buffer()) 130 | .pipe($.uglify()) 131 | .pipe(gulp.dest(distDir + '/node_modules/' + it.name)) 132 | ); 133 | }); 134 | streams.push( 135 | // copy modules' package.json 136 | gulp.src('node_modules/' + it.name + '/package.json') 137 | .pipe(gulp.dest(distDir + '/node_modules/' + it.name)) 138 | ); 139 | }); 140 | 141 | return merge(streams); 142 | }); 143 | 144 | // Replace 'main' property of package.json 145 | gulp.task('packageJson', ['bundle:dependencies'], function (done) { 146 | var fs = require('fs'); 147 | var copied = _.cloneDeep(packageJson); 148 | copied.main = 'app.js'; 149 | fs.writeFile('dist/package.json', JSON.stringify(copied), function () { 150 | done(); 151 | }); 152 | }); 153 | 154 | // Package for each platforms 155 | gulp.task('package', ['win32', 'darwin', 'linux'].map(function (platform) { 156 | var taskName = 'package:' + platform; 157 | gulp.task(taskName, ['build'], function (done) { 158 | packager({ 159 | dir: distDir, 160 | name: 'Disclosure', 161 | arch: 'x64', 162 | platform: platform, 163 | out: releaseDir + '/' + platform, 164 | version: '0.28.1' 165 | }, function (err) { 166 | done(); 167 | }); 168 | }); 169 | return taskName; 170 | })); 171 | 172 | gulp.task('build', ['html', 'packageJson', 'compile:scripts']); 173 | 174 | gulp.task('watch', function () { 175 | gulp.watch(['src/styles/**/*.scss'], ['inject:css']); 176 | }); 177 | 178 | gulp.task('serve', ['inject:css', 'compile:scripts:watch', 'watch'], function () { 179 | electronProcess.start(); 180 | gulp.watch([serveDir + '/app.js', serveDir + '/browser/**/*.js'], electronProcess.restart); 181 | gulp.watch([serveDir + '/renderer/**.js', serveDir + '/index.html', serveDir + '/styles/**.css'], electronProcess.reload); 182 | }); 183 | 184 | gulp.task('clean', function (done) { 185 | del([serveDir, distDir, releaseDir], function () { 186 | done(); 187 | }); 188 | }); 189 | 190 | gulp.task('default', ['build']); 191 | 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-disclosure", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": ".serve/app.js", 6 | "scripts": { 7 | "build": "gulp", 8 | "start": "gulp serve", 9 | "install": "bower install", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "Quramy", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel": "^5.4.7", 17 | "browserify": "^10.2.3", 18 | "del": "^1.1.1", 19 | "electron-connect": "^0.2.0", 20 | "electron-download": "^1.0.5", 21 | "electron-packager": "^4.1.3", 22 | "electron-prebuilt": "^0.27.2", 23 | "gulp": "^3.8.11", 24 | "gulp-asar": "0.0.2", 25 | "gulp-babel": "^5.1.0", 26 | "gulp-flatten": "0.0.4", 27 | "gulp-if": "^1.2.5", 28 | "gulp-inject": "^1.2.0", 29 | "gulp-load-plugins": "^0.10.0", 30 | "gulp-minify-css": "^1.1.3", 31 | "gulp-plumber": "^1.0.1", 32 | "gulp-sass": "^2.0.1", 33 | "gulp-sourcemaps": "^1.5.2", 34 | "gulp-uglify": "^1.2.0", 35 | "gulp-useref": "^1.2.0", 36 | "gulp-watch": "^4.2.4", 37 | "main-bower-files": "^2.8.0", 38 | "merge2": "^0.3.5", 39 | "vinyl-buffer": "^1.0.0", 40 | "vinyl-source-stream": "^1.1.0" 41 | }, 42 | "dependencies": { 43 | "classnames": "^2.1.2", 44 | "electron-debug": "^0.1.0", 45 | "lodash": "^3.9.3", 46 | "node-twitter-api": "^1.6.0", 47 | "react": "^0.13.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('babel/polyfill'); 3 | 4 | import app from 'app'; 5 | import BrowserWindow from 'browser-window'; 6 | import Menu from 'menu'; 7 | import MenuItem from 'menu-item'; 8 | import crashReporter from 'crash-reporter'; 9 | import appMenu from './browser/menu/appMenu'; 10 | import twitter from './browser/twitter'; 11 | import debug from 'electron-debug'; 12 | 13 | let mainWindow = null; 14 | if(process.env.NODE_ENV === 'develop'){ 15 | crashReporter.start(); 16 | debug(); 17 | } 18 | 19 | app.on('window-all-closed', () => { 20 | app.quit(); 21 | }); 22 | 23 | app.on('start', () => { 24 | if(mainWindow) mainWindow.emit('start'); 25 | }); 26 | 27 | app.on('stop', () => { 28 | if(mainWindow) mainWindow.emit('stop'); 29 | }); 30 | 31 | app.on('ready', () => { 32 | Menu.setApplicationMenu(appMenu); 33 | mainWindow = new BrowserWindow({ 34 | width: 720, 35 | height: 600 36 | }); 37 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /src/browser/menu/appMenu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from 'app'; 4 | import Menu from 'menu'; 5 | import MenuItem from 'menu-item'; 6 | import capture from './submenus/capture'; 7 | 8 | let template = [{ 9 | label: 'Disclosure', 10 | submenu: [{ 11 | label: 'Quit', 12 | accelerator: 'Command+Q', 13 | click: function () {app.quit()} 14 | }] 15 | }]; 16 | 17 | let appMenu = Menu.buildFromTemplate(template); 18 | appMenu.append(capture); 19 | 20 | module.exports = appMenu; 21 | -------------------------------------------------------------------------------- /src/browser/menu/submenus/capture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import app from 'app'; 4 | import Menu from 'menu'; 5 | import MenuItem from 'menu-item'; 6 | 7 | let submenu = new Menu(); 8 | let startMenu = new MenuItem({ 9 | label: 'Start', 10 | accelerator: 'Command + Z', 11 | click: (menu) => { 12 | menu.enabled= false; 13 | stopMenu.enabled = true; 14 | app.emit('start'); 15 | } 16 | }); 17 | let stopMenu = new MenuItem({ 18 | label: 'Stop', 19 | accelerator: 'Command + S', 20 | enabled: false, 21 | click: (menu) => { 22 | menu.enabled = false; 23 | startMenu.enabled = true; 24 | app.emit('stop'); 25 | } 26 | }); 27 | submenu.append(startMenu); 28 | submenu.append(stopMenu); 29 | let capMenu = new MenuItem({ 30 | label: 'Capture', 31 | type: 'submenu', 32 | submenu: submenu 33 | }); 34 | 35 | module.exports = capMenu; 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/browser/twitter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('app'); 4 | var twitterAPI = require('node-twitter-api'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var BrowserWindow = require('browser-window'); 8 | 9 | var CACHE_FILE = path.join(app.getPath('cache'), 'electron-bot/token_twitter.json'); 10 | 11 | var twitter = new twitterAPI({ 12 | consumerKey: "b3NBO7StTwNDRHnrt7IeJHjEo", 13 | consumerSecret: "ngIS5HinqH7BxmIo3NWWzBkyUDdZs5tmvUPa66b1NyvWzGMlF2", 14 | callback: 'https://twitter.com/' 15 | }); 16 | 17 | var _token = null; 18 | var _requestToken = null; 19 | var _requestTokenSecret = null; 20 | 21 | var auth = { 22 | getUrl: function(cb) { 23 | twitter.getRequestToken(function (error, requestToken, requestTokenSecret, results) { 24 | if(error) { 25 | typeof cb === 'function' && cb(error, null); 26 | return; 27 | } 28 | _requestToken = requestToken; 29 | _requestTokenSecret = requestTokenSecret 30 | typeof cb === 'function' && cb(null, twitter.getAuthUrl(requestToken)); 31 | }); 32 | }, 33 | getToken: function (cb) { 34 | if(_token) { 35 | typeof cb === 'function' && cb(null, _token); 36 | return; 37 | } 38 | fs.readFile(CACHE_FILE, 'utf8', function (err, txt) { 39 | if(err) { 40 | typeof cb === 'function' && cb(err, null); 41 | return; 42 | } 43 | if(!err && txt) { 44 | _token = JSON.parse(txt); 45 | typeof cb === 'function' && cb(null, _token); 46 | return; 47 | } 48 | typeof cb === 'function' && cb('Not login', null); 49 | }); 50 | }, 51 | requestToken: function (arg1, arg2) { 52 | var opt, cb; 53 | if(arg2) { 54 | opt = arg1, cb = arg2; 55 | } else { 56 | opt = {}; 57 | cb = arg1; 58 | } 59 | auth.getToken(function (err, token) { 60 | if(!opt.force && token) { 61 | typeof cb === 'function' && cb(null, token); 62 | return; 63 | } 64 | auth.getUrl(function (error, url) { 65 | var loginWindow = new BrowserWindow({ 66 | width: 600, 67 | height:800, 68 | resizable: false, 69 | 'always-on-top': true, 70 | 'web-preferences': { 71 | 'web-security': false 72 | } 73 | }); 74 | loginWindow.webContents.on('will-navigate', function(preventDefault, url){ 75 | //console.log(url); 76 | var matched; 77 | if(matched = url.match(/\?oauth_token=([^&]*)&oauth_verifier=([^&]*)/)) { 78 | twitter.getAccessToken(_requestToken, _requestTokenSecret, matched[2], function (error, accessToken, accessTokenSecret, result) { 79 | console.log(accessTokenSecret, accessToken); 80 | _token = { 81 | accessToken: accessToken, 82 | accessTokenSecret: accessTokenSecret 83 | }; 84 | fs.writeFileSync(CACHE_FILE, JSON.stringify(_token), 'utf8'); 85 | typeof cb === 'function' && cb(null, _token); 86 | setTimeout(function () {loginWindow.close();}, 0); 87 | return; 88 | }); 89 | } 90 | }); 91 | 92 | loginWindow.loadUrl(url); 93 | }); 94 | }); 95 | 96 | }, 97 | client: function () { 98 | return twitter; 99 | }, 100 | callApi: function(nameSpace, action, params, cb) { 101 | if(!cb && !params) { 102 | twitter[nameSpace](_token.accessToken, _token.accessTokenSecret, action); 103 | }else if(!cb) { 104 | twitter[nameSpace](action, _token.accessToken, _token.accessTokenSecret, params); 105 | }else{ 106 | twitter[nameSpace](action, params, _token.accessToken, _token.accessTokenSecret, cb); 107 | } 108 | } 109 | }; 110 | 111 | module.exports = auth; 112 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disclosure 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/renderer/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require("babel/polyfill"); 3 | 4 | import React from 'react'; 5 | import {Main} from './components/main'; 6 | 7 | let container = document.getElementById("container"); 8 | React.render(React.createElement(Main), container); 9 | -------------------------------------------------------------------------------- /src/renderer/components/imageList.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {Timer} from '../services/timer'; 5 | 6 | export class ImageList extends React.Component { 7 | constructor () { 8 | super(); 9 | /* 10 | this.propTypes = { 11 | list: React.PropTypes.array 12 | };*/ 13 | this.componentDidUpdate = this.componentDidUpdate.bind(this); 14 | } 15 | static propTypes = { 16 | list: React.PropTypes.array 17 | }; 18 | componentDidUpdate () { 19 | if(this.props.list && this.props.list.length) { 20 | // animate scrollLeft to last-item 21 | let container = React.findDOMNode(this); 22 | let lastChild = container.querySelector('.image-group>a:last-child'); 23 | if(!lastChild) return; 24 | let start = container.scrollLeft; 25 | let end = lastChild.offsetLeft; 26 | let delta = (end - start) / 10.0; 27 | let pre; 28 | let timer = new Timer(() => { 29 | if(container.scrollLeft <= end && container.scrollLeft !== pre) { 30 | pre = container.scrollLeft; 31 | container.scrollLeft += delta; 32 | }else{ 33 | timer.cancel(); 34 | } 35 | }, 40); 36 | timer.start(); 37 | } 38 | } 39 | render() { 40 | let imageList = this.props.list && this.props.list.map((item) => { 41 | return ( 42 | 43 |
  • 44 | 45 |
  • 46 |
    47 | ); 48 | }); 49 | return ( 50 |
    51 | 54 |
    55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/renderer/components/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import remote from 'remote'; 3 | import shell from 'shell'; 4 | import * as _ from 'lodash'; 5 | import cx from 'classnames'; 6 | import {ImageList} from './imageList'; 7 | import {Capture} from '../services/capture'; 8 | import {Twitter} from '../services/twitterWrapper'; 9 | import {Timer} from '../services/timer'; 10 | 11 | let screen = remote.require('screen'); 12 | 13 | let bindAll = object => { 14 | Object.getOwnPropertyNames(object.constructor.prototype) 15 | .filter(key => typeof object[key] === 'function') 16 | .forEach(methodName => {object[methodName].bind(object);console.log(methodName)}); 17 | }; 18 | 19 | export class Main extends React.Component{ 20 | state = { 21 | timerStatus: false, 22 | enableTweet: false, 23 | me: null, 24 | imageList: [] 25 | }; 26 | constructor () { 27 | super(); 28 | 29 | // Binding event handlers 30 | //bindAll(this); 31 | this.capture = this.capture.bind(this); 32 | this.toggleTweet = this.toggleTweet.bind(this); 33 | this.start = this.start.bind(this); 34 | this.stop = this.stop.bind(this); 35 | this.initTwitter = this.initTwitter.bind(this); 36 | this.initCapture = this.initCapture.bind(this); 37 | 38 | this.initTwitter(); 39 | this.initCapture(); 40 | this.timer = new Timer(()=>{ 41 | this.capture(); 42 | }, 1000 * 60 * 5); 43 | } 44 | initCapture() { 45 | Capture.init(screen.getPrimaryDisplay().size, 0.5).then( () => { 46 | remote.getCurrentWindow().on('start', () => this.start()); 47 | remote.getCurrentWindow().on('stop', () => this.stop()); 48 | }); 49 | 50 | } 51 | async initTwitter() { 52 | this.twitter = new Twitter(); 53 | await this.twitter.hasToken(); 54 | let me = await this.twitter.verifyCredentials(); 55 | this.setState({me: me}); 56 | } 57 | capture() { 58 | let capture = new Capture(); 59 | let imageHolder = capture.getImage(); 60 | let url = imageHolder.toDataURL(); 61 | this.setState({ 62 | imageList: this.state.imageList.concat([{ 63 | key: new Date() - 0, 64 | url: url 65 | }]) 66 | }); 67 | 68 | // upload captured image and tweet 69 | if(this.state.enableTweet) { 70 | this.twitter.mediaUpload({media: imageHolder.toDataString(), isBase64: true}) 71 | .then(res => { 72 | let d = new Date(); 73 | // console.log('Upload media: ', res.media_id_string); 74 | return this.twitter.statuesUpdate({ 75 | status: 'Captured by https://github.com/Quramy/electron-disclosure at ' + d.toGMTString(), 76 | media_ids: res.media_id_string 77 | }); 78 | }) 79 | .then(data => { 80 | // Notify with Notification API 81 | let link = 'https://twitter.com/' + this.state.me.name + '/status/' + data.id_str; 82 | let n = new Notification('Tweet done. ', { 83 | body: link, 84 | }); 85 | n.onclick = function () { 86 | shell.openExternal(link); 87 | }; 88 | }); 89 | } 90 | } 91 | start() { 92 | this.timer.start(2000); 93 | this.setState({timerStatus: true}); 94 | } 95 | stop() { 96 | this.timer.cancel(); 97 | this.setState({timerStatus: false}); 98 | } 99 | toggleTweet() { 100 | this.setState({ 101 | enableTweet: !this.state.enableTweet 102 | }); 103 | this.twitter.requestToken().then(() => { 104 | }); 105 | } 106 | render() { 107 | return ( 108 |
    109 |
    110 |
    111 | 112 | 113 | 114 |
    115 |

    {this.state.timerStatus ? 'Stop capture' : 'Start to capture by 5 minutes'}

    116 |
    117 |
    118 |
    119 | 120 | 121 | 122 |
    123 |

    One-time Capture

    124 |
    125 |
    126 |
    127 | 128 | 131 |
    132 |

    Tweet when captured

    133 |
    134 |
    135 | {this.state.me ? this.state.me.screen_name : 'Sign in'} 136 |
    137 |
    138 |
    139 | 140 |
    141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/renderer/services/capture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var streamUrl; 4 | var video = document.getElementById('video'); 5 | 6 | let _size, _scale; 7 | 8 | var init = (size, scale) => { 9 | if(scale > 1.0) scale = 1.0; 10 | [_size, _scale] = [size, scale]; 11 | video.width = (size.width || 800) * scale; 12 | video.height = (size.height || 600) * scale; 13 | console.log(_size); 14 | return new Promise((resolve, reject) => { 15 | navigator.webkitGetUserMedia({ 16 | audio: false, 17 | video: { 18 | mandatory: { 19 | chromeMediaSource: 'screen', 20 | minWidth: 800, 21 | maxWidth: 2560, 22 | minHeight: 600, 23 | maxHeight: 1440 24 | } 25 | } 26 | }, (stream) => { 27 | streamUrl = URL.createObjectURL(stream); 28 | video.src = streamUrl; 29 | video.play(); 30 | setTimeout( () => { 31 | resolve(video); 32 | }, 200); 33 | }, (reason) => { 34 | console.log(reason); 35 | reject(reason); 36 | }); 37 | }); 38 | 39 | }; 40 | 41 | class ImageHolder { 42 | url; 43 | constructor (url) { 44 | this.url = url; 45 | } 46 | toDataURL() { 47 | return this.url; 48 | } 49 | toDataString() { 50 | return this.url.replace('data:image/png;base64,', ''); 51 | } 52 | } 53 | 54 | export class Capture { 55 | static init(size, scale) { 56 | return init(size, scale); 57 | } 58 | constructor () {} 59 | getImage() { 60 | if(!streamUrl) return; 61 | let canvas = document.createElement('canvas'); 62 | let context = canvas.getContext('2d'); 63 | canvas.width = _size.width * _scale; 64 | canvas.height = _size.height * _scale; 65 | context.drawImage(video, 0, 0, canvas.width, canvas.height); 66 | let url = canvas.toDataURL(); 67 | return new ImageHolder(url); 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/renderer/services/timer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export class Timer { 4 | constructor(fn, interval) { 5 | if(!interval) interval = 0; 6 | this.interval = interval; 7 | this.fn = fn; 8 | } 9 | start(delay) { 10 | this.status = true; 11 | if(delay) { 12 | setTimeout(()=> this.run(), delay); 13 | }else{ 14 | this.run(); 15 | } 16 | return this; 17 | } 18 | run () { 19 | if(this.status) { 20 | this.fn(); 21 | setTimeout(() => { 22 | this.run() 23 | }, this.interval); 24 | } 25 | } 26 | cancel() { 27 | this.status = false; 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/services/twitterWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import remote from 'remote'; 4 | 5 | let promisify = (fn) => { 6 | return new Promise((resolve, reject) => { 7 | fn((error, data, response) => { 8 | if(error) { 9 | reject({error:error, reason: response}); 10 | }else{ 11 | resolve(data); 12 | } 13 | }); 14 | }); 15 | }; 16 | 17 | export class Twitter { 18 | constructor() { 19 | this.client = remote.require('./browser/twitter'); 20 | } 21 | hasToken() { 22 | return promisify(this.client.getToken); 23 | } 24 | requestToken() { 25 | return promisify(this.client.requestToken); 26 | } 27 | verifyCredentials() { 28 | return promisify(cb => this.client.callApi('verifyCredentials', cb)); 29 | } 30 | statuesUpdate(params) { 31 | return promisify(cb => this.client.callApi('statuses', 'update', params, cb)); 32 | } 33 | mediaUpload(params) { 34 | return promisify(cb => this.client.callApi('uploadMedia', params, cb)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | a, a:hover, a:visited, a:active, a:focus, button { 3 | cursor: pointer; 4 | } 5 | 6 | label, input[type="checkbox"] { 7 | cursor: pointer; 8 | } 9 | 10 | /* base */ 11 | body { 12 | -webkit-user-select: none; 13 | color: #808080; 14 | background-color: #444; 15 | width: 100%; 16 | height: 100%; 17 | font-family: "Hiragino Kaku Gothic ProN", "游ゴシック", YuGothic, Meiryo, sans-serif; 18 | } 19 | 20 | /* layout */ 21 | .app-container { 22 | left: 0; 23 | right: 0; 24 | top: 0; 25 | position: relative; 26 | bottom: 0; 27 | } 28 | 29 | .app-controll { 30 | background-color: #222; 31 | position: fixed; 32 | left: 0; 33 | right: 0; 34 | top: 0; 35 | height: 60px; 36 | padding: 5px; 37 | white-space: nowrap; 38 | z-index: 10; 39 | } 40 | 41 | .slider-container { 42 | padding-top: 50px; 43 | width: 100%; 44 | overflow-x: scroll; 45 | } 46 | 47 | 48 | /* component */ 49 | 50 | .menu-item { 51 | display: inline-block; 52 | position: relative; 53 | text-align: center; 54 | vertical-align: middle; 55 | padding: 0 20px 0 20px; 56 | height: 50px; 57 | 58 | input[type="checkbox"] { 59 | display: none; 60 | } 61 | } 62 | 63 | .icon { 64 | transition:0.2s linear all; 65 | font-size: 45px; 66 | display: inline-block; 67 | text-align: center; 68 | vertical-align: middle; 69 | z-index: 30; 70 | &:hover { 71 | color: #fff; 72 | &+.menu-description { 73 | opacity: 1.0; 74 | } 75 | } 76 | &+.menu-description { 77 | transition:0.2s linear all; 78 | font-size: 12px; 79 | display: block; 80 | background-color: #222; 81 | z-index: 0; 82 | opacity: 0; 83 | position: absolute; 84 | border-width: 0 2000px 0 2000px; 85 | border-style: solid; 86 | border-color: #222; 87 | box-sizing: content-box; 88 | top: 50px; 89 | padding: 0 20px 0 20px; 90 | left: 0; 91 | margin-left: -2000px; 92 | } 93 | } 94 | 95 | .icon-status.on { 96 | color: #f02000; 97 | } 98 | 99 | .icon-camera { 100 | color: #808080; 101 | &:hover:active { 102 | transition:0.2s linear all; 103 | color: #f02000; 104 | } 105 | } 106 | 107 | input:checked+.icon-twitter { 108 | color: #f02000; 109 | } 110 | 111 | .button { 112 | background-color: #ffa033; 113 | color: #fff; 114 | padding: 5px; 115 | border-radius: 2px; 116 | } 117 | 118 | .image-content { 119 | } 120 | 121 | .image-item { 122 | height: 400px; 123 | padding: 15px; 124 | list-style: none; 125 | display: inline-block; 126 | position: relative; 127 | margin-bottom: 75px; 128 | -webkit-box-reflect: below 5px -webkit-gradient(linear, left bottom, left top, from(rgba(255, 255, 255, .5)), color-stop(0.2, transparent), to(transparent)); 129 | img { 130 | transition:0.1s linear all; 131 | height: 100%; 132 | } 133 | } 134 | 135 | .image-group { 136 | padding-top: 30px; 137 | padding-left: 50px; 138 | padding-right: 50px; 139 | padding-bottom: 30px; 140 | text-align: center; 141 | vertical-align: bottom; 142 | white-space: nowrap; 143 | } 144 | 145 | video.hidden { 146 | position: absolute; 147 | left: -5000px; 148 | } 149 | 150 | .account { 151 | margin-left: 3px; 152 | display: inline-block; 153 | vertical-align: middle; 154 | } 155 | .account img { 156 | margin-top: 7px; 157 | width: 32px; 158 | height: 32px; 159 | } 160 | 161 | .account-screen-name { 162 | display: inline-block; 163 | margin-top: 25px; 164 | } 165 | 166 | --------------------------------------------------------------------------------