├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── resource ├── comet.icns ├── comet.ico ├── comet.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_256x256.png │ ├── icon_256x256@2x.png │ ├── icon_32x32.png │ ├── icon_32x32@2x.png │ ├── icon_512x512.png │ ├── icon_512x512@2x.png │ ├── icon_64x64.png │ └── icon_64x64@2x.png ├── comet.png ├── demo.gif ├── title-bar.png └── tray.png └── src ├── index.html ├── main.js ├── renderer.js ├── style.css ├── tray.js ├── twitter-stream.js └── twitter-window.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Masashi SHIBATA 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 | ![TITLE LOGO](./resource/title-bar.png) 2 | 3 | NicoNico style tweet viewer for Desktop. 4 | 5 | - Show hashtag tweets when you talk at conference. 6 | - Show your timeline when you are at work. 7 | 8 | ![Demo gif animation](./resource/demo.gif) 9 | 10 | ## How to run 11 | 12 | Set environment variables: 13 | 14 | ``` 15 | export HASHTAG=#GoodMorning 16 | export TWITTER_CONSUMER_KEY=XXXXXXXXXXXXXXXXXXXXXXXXX 17 | export TWITTER_CONSUMER_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 18 | export TWITTER_ACCESS_TOKEN_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 19 | export TWITTER_ACCESS_TOKEN_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 20 | ``` 21 | 22 | And Run: 23 | 24 | ``` 25 | $ npm install 26 | $ npm run electron 27 | ``` 28 | 29 | ## License 30 | 31 | MIT @ [Masashi Shibata](http://c-bata.link) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comet", 3 | "version": "0.0.1", 4 | "description": "NicoNico style tweet viewer for Desktop", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "electron": "electron ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/c-bata/shooting-tweets.git" 13 | }, 14 | "keywords": [ 15 | "electron", 16 | "twitter" 17 | ], 18 | "author": "c-bata ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/c-bata/shooting-tweets/issues" 22 | }, 23 | "homepage": "https://github.com/c-bata/shooting-tweets#readme", 24 | "dependencies": { 25 | "electron-prebuilt": "^1.4.3", 26 | "twitter": "^1.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resource/comet.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.icns -------------------------------------------------------------------------------- /resource/comet.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.ico -------------------------------------------------------------------------------- /resource/comet.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_128x128.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_16x16.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_256x256.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_32x32.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_512x512.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_64x64.png -------------------------------------------------------------------------------- /resource/comet.iconset/icon_64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.iconset/icon_64x64@2x.png -------------------------------------------------------------------------------- /resource/comet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/comet.png -------------------------------------------------------------------------------- /resource/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/demo.gif -------------------------------------------------------------------------------- /resource/title-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/title-bar.png -------------------------------------------------------------------------------- /resource/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-bata/comet/6f4f983b2d1e22bf83a01e370e5b9e18bb26b7cb/resource/tray.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const {app} = require('electron') 2 | const createTray = require('./tray') 3 | 4 | app.on('ready', () => { 5 | createTray() 6 | }) 7 | 8 | // Quit when all windows are closed. 9 | app.on('window-all-closed', () => { 10 | if (process.platform !== 'darwin') { 11 | app.quit() 12 | } 13 | }) 14 | 15 | app.on('activate', () => {}) 16 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer} = require('electron'); 2 | 3 | class Tweet { 4 | constructor(text) { 5 | this.text = text; 6 | this.y = Math.random() * canvas.height; 7 | this.x = canvas.width; 8 | this.dx = - 2 - (Math.random() * 5); 9 | } 10 | 11 | isDelete(ctx) { 12 | let thresholdX = - ctx.measureText(this.text).width; 13 | return thresholdX > this.x 14 | } 15 | 16 | update() { 17 | this.x += this.dx; 18 | } 19 | } 20 | 21 | class CanvasManager { 22 | constructor(canvas) { 23 | this.tweetList = []; 24 | this.canvas = canvas; 25 | // These are not canvas.width and canvas.height; 26 | this.canvas.width = window.parent.screen.width; 27 | this.canvas.height = window.parent.screen.height; 28 | 29 | this.ctx = canvas.getContext('2d'); 30 | this.ctx.fillStyle = 'white'; 31 | this.ctx.strokeStyle = 'black'; 32 | this.ctx.font = "40px 'Sans-serif'"; 33 | this.ctx.lineWidth = 5; 34 | } 35 | 36 | pushTweet(tweet) { 37 | this.tweetList.push(tweet); 38 | } 39 | 40 | draw() { 41 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 42 | 43 | for (let i = this.tweetList.length - 1; i >= 0; i -= 1) { 44 | if (this.tweetList[i].isDelete(this.ctx)) { 45 | this.tweetList.splice(i, 1); 46 | } else { 47 | let tweet = this.tweetList[i]; 48 | tweet.update(); 49 | this.tweetList[i] = tweet; 50 | this.ctx.strokeText(tweet.text, tweet.x, tweet.y); 51 | this.ctx.fillText(tweet.text, tweet.x, tweet.y); 52 | } 53 | } 54 | } 55 | } 56 | 57 | function setIpcRenderer(canvasManager) { 58 | ipcRenderer.on('tweet', function(event, args) { 59 | var tweet = new Tweet(args); 60 | canvasManager.pushTweet(tweet); 61 | event.sender.send('tweet-reply', 'pong'); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100% 4 | } 5 | body { 6 | margin: 0; 7 | height: 100%; 8 | width: 100%; 9 | /* background-color: rgba(0, 0, 0, 0.0); */ 10 | } 11 | canvas { 12 | height: 100%; 13 | width: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /src/tray.js: -------------------------------------------------------------------------------- 1 | const {app, Tray, Menu, ipcMain} = require('electron') 2 | const {startStream, stopStream} = require('./twitter-stream') 3 | const {createWindow, closeWindow} = require('./twitter-window') 4 | 5 | let trayTemplate = [ 6 | { 7 | label: 'Settings', 8 | click: () => { 9 | ipcMain.send('show-settings-dialog') 10 | } 11 | }, 12 | { 13 | label: 'Start', 14 | click: () => { 15 | console.log('Start twitter streaming.') 16 | let win = createWindow() 17 | startStream(win) 18 | } 19 | }, 20 | { 21 | label: 'Stop', 22 | click: () => { 23 | console.log('Stop twitter streaming.') 24 | closeWindow() 25 | stopStream() 26 | } 27 | }, 28 | { 29 | label: 'Quit', 30 | click: () => { 31 | console.log('Bye!') 32 | app.quit() 33 | } 34 | } 35 | ] 36 | 37 | module.exports = function createTray() { 38 | const tray = new Tray(`${__dirname}/../resource/tray.png`) 39 | const contextMenu = Menu.buildFromTemplate(trayTemplate) 40 | tray.setToolTip('This is my application.') 41 | tray.setContextMenu(contextMenu) 42 | } 43 | -------------------------------------------------------------------------------- /src/twitter-stream.js: -------------------------------------------------------------------------------- 1 | const Twitter = require('twitter') 2 | const {ipcMain} = require('electron') 3 | 4 | // If use let, this variable cleaned up by runtime(GC). 5 | var _twitter_stream = null 6 | let hashTag = process.env.HASHTAG 7 | let client = new Twitter({ 8 | consumer_key: process.env.TWITTER_CONSUMER_KEY, 9 | consumer_secret: process.env.TWITTER_CONSUMER_SECRET, 10 | access_token_key: process.env.TWITTER_ACCESS_TOKEN_KEY, 11 | access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET 12 | }) 13 | 14 | function startStream(win) { 15 | // Set timelilne for retrieving hashtag 16 | client.stream('statuses/filter', {track: hashTag}, function(stream) { 17 | _twitter_stream = stream 18 | stream.on('data', function(tweet) { 19 | win.webContents.send('tweet', tweet.text) 20 | console.log(tweet.text) 21 | }) 22 | // Handle errors 23 | stream.on('error', function (error) { 24 | console.log(error) 25 | }) 26 | }) 27 | } 28 | 29 | function stopStream() { 30 | _twitter_stream.destroy() 31 | } 32 | 33 | module.exports = { 34 | startStream: startStream, 35 | stopStream: stopStream 36 | } 37 | -------------------------------------------------------------------------------- /src/twitter-window.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const {BrowserWindow} = require('electron') 3 | 4 | var win = null 5 | 6 | function createWindow () { 7 | // Create the browser window. 8 | let size = electron.screen.getPrimaryDisplay().size 9 | win = new BrowserWindow({ 10 | left: 0, 11 | top: 0, 12 | width: size.width, 13 | height: size.height, 14 | frame: false, 15 | show: true, 16 | transparent: true, 17 | resizable: false 18 | }) 19 | 20 | win.setIgnoreMouseEvents(true) 21 | win.setAlwaysOnTop(true) 22 | win.loadURL(`file://${__dirname}/index.html`) 23 | 24 | // Emitted when the window is closed. 25 | win.on('closed', () => { 26 | win = null 27 | }) 28 | return win 29 | } 30 | 31 | function closeWindow () { 32 | win.close() 33 | } 34 | 35 | module.exports = { 36 | createWindow: createWindow, 37 | closeWindow: closeWindow 38 | } 39 | --------------------------------------------------------------------------------