├── .gitignore ├── README.md ├── app ├── component │ ├── app.js │ ├── feed.js │ ├── feeds.js │ ├── folder.js │ ├── folders.js │ ├── items.js │ └── meta.js ├── cookie.js ├── data │ └── .gitplaceholder ├── html │ ├── about.html │ ├── authorize.html │ ├── index.html │ └── setting.html ├── image │ ├── eLDR.icns │ ├── eLDR.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 │ ├── pin.png │ ├── pin@2x.png │ ├── splash.png │ └── splash@2x.png ├── setting.js ├── state.js └── style │ └── style.css ├── banner.png ├── capture1.png ├── capture2.png ├── capture3.png ├── gulpfile.js ├── main.js ├── package.json └── src ├── app.jsx ├── feed.jsx ├── feeds.jsx ├── folder.jsx ├── folders.jsx ├── items.jsx └── meta.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | packages 5 | app/data/cookies 6 | app/data/setting.json 7 | app/data/folders.json 8 | app/data/feeds.json 9 | app/data/items.json 10 | app/data/meta.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](banner.png) 2 | 3 | Live Dwango Reader(旧 Live Door Reader)を Electron でデスクトップアプリ化するプロジェクト。 4 | バイナリのダウンロードは [releases](https://github.com/k0sukey/Electron-LDR/releases) からどうぞ。 5 | 6 | ![capture1](capture1.png) 7 | 8 | ![capture2](capture2.png) 9 | 10 | ## うごかしかた 11 | 12 | GitHub からクローンします。 13 | 14 | ```sh 15 | $ git clone git@github.com:k0sukey/Electron-LDR.git 16 | $ cd Electron-LDR 17 | ``` 18 | 19 | 必要な npm パッケージをインストールします。 20 | 21 | ```sh 22 | $ npm install 23 | ``` 24 | 25 | コマンドラインで起動します。 26 | 27 | ```sh 28 | $ npm start 29 | ``` 30 | 31 | ## LDR API のつなぎ込み状況 32 | 33 | **参考** [http://zuzu.hateblo.jp/entry/20091011/1255337739](http://zuzu.hateblo.jp/entry/20091011/1255337739) 34 | 35 | - [x] **/api/feed/discover** POST ```{ feedlink: String }``` たぶんやらない → 設定に入れた 36 | - [x] **/api/feed/subscribe** POST ```{ feedlink: String }``` たぶんやらない → 設定に入れた 37 | - [x] **/api/feed/unsubscribe** POST ```{ subscribe_id: String }``` クッキーに ```reader_sid``` も必要 38 | - [x] **/api/subs** GET/POST ```{ unread: 0/1 }``` 39 | - [x] **/api/all** GET/POST ```{ subscribe_id: String, offset:Number, limit: Number}``` 40 | - [x] **/api/unread** GET/POST ```{ subscribe_id: String }``` 41 | - [x] **/api/touch_all** GET/POST ```{ subscribe_id: String }``` クッキーに ```reader_sid``` も必要 42 | - [x] **/api/feed/set_rate** POST ```{ subscribe_id: String, rate: [0-5] }``` たぶんやらない → 記事一覧の最上部で設定できるようにした 43 | - [x] **/api/folders** GET/POST たぶんやらない → サイドバーの横に入れた 44 | - [x] **/api/folder/create** POST ```{ name: String }``` たぶんやらない → 設定に入れた 45 | - [x] **/api/folder/delete** POST ```{ folder_id: String }``` たぶんやらない → 長押しで実装した 46 | - [x] **/api/feed/move** POST ```{ subscribe_id: String, to: String }``` たぶんやらない → ドラッグアンドドロップで実装した 47 | - [x] **/api/pin/all** POST ピンの付け外しに。たぶん一覧表示はやらないと思う 48 | - [x] **/api/pin/add** POST ```{ link: String, title: String }``` クッキーに ```reader_sid``` も必要 49 | - [x] **/api/pin/remove** POST ```{ link: String }``` クッキーに ```reader_sid``` も必要 50 | - [x] **/api/pin/clear** POST 一覧表示をやらないならこれもやらないと思う → メニュー内で実装した 51 | 52 | ## キーボードショートカットの実装状況 53 | 54 | - [x] j 次のアイテム 55 | - [x] k 前のアイテム 56 | - [x] space 下にスクロール 57 | - [x] shift + space 上にスクロール 58 | - [ ] < 過去の記事に移動 たぶんやらない 59 | - [ ] > 未来の記事に移動 たぶんやらない 60 | - [x] v 元記事を開く(独自:閉じる) 61 | - [x] p ピンを付ける / 外す 62 | - [x] o ピンを開く → メニュー内で実装した 63 | - [x] c 本文の表示 / 非表示 64 | - [x] delete 購読停止 65 | - [x] r フィード一覧の更新 66 | - [x] z マイフィードを畳む / 戻す 67 | - [x] f 検索ボックスに移動 68 | - [x] s 次のフィードに移動 69 | - [x] a 前のフィードに移動 70 | - [ ] w 最初の未読に移動 71 | - [ ] shift + w 最後の未読に移動 72 | - [x] ? ヘルプを表示 / 非表示 73 | 74 | **独自のキーボードショートカット** 75 | - [x] b 元記事をブラウザで開く 76 | - [x] command or control + 1-9 フォルダを移動 77 | - [x] command + , 設定を開く 78 | - [x] command + q アプリを終了 79 | 80 | ## 設定 81 | 82 | アプリのメニューか、キーボードショートカットで設定を開くことができます。 83 | フィードの登録、フィード一覧のアイコンの表示・非表示、未読のないフィードの表示・非表示、フォント、本文のフォントサイズを変更することができます。 84 | 85 | ![capture3](capture3.png) -------------------------------------------------------------------------------- /app/component/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | mousetrap = require('mousetrap'), 6 | progress = require('request-progress'), 7 | request = require('request'), 8 | React = require('react'), 9 | ReactDnD = require('react-dnd'), 10 | ReactTooltip = require('react-tooltip'), 11 | HTML5Backend = require('react-dnd/modules/backends/HTML5'), 12 | Cookie = require('../cookie'), 13 | Setting = require('../setting'), 14 | State = require('../state'), 15 | Feeds = require('../component/feeds'), 16 | Folders = require('../component/folders'); 17 | 18 | var ipc = electron.ipcRenderer, 19 | remote = electron.remote; 20 | 21 | var app = remote.app; 22 | 23 | if (!State.exists({ category: 'meta' })) { 24 | State.save({ 25 | category: 'meta', 26 | content: {} 27 | }); 28 | } 29 | 30 | var App = React.createClass({ 31 | displayName: 'app', 32 | getInitialState: function getInitialState() { 33 | return { 34 | order: 'modified_on', 35 | filter: '', 36 | folders: { 37 | name2id: {}, 38 | names: [] 39 | }, 40 | folder: '全て', 41 | feeds: [] 42 | }; 43 | }, 44 | doOrder: function doOrder() { 45 | var element = document.getElementById('order'), 46 | index = element.selectedIndex; 47 | 48 | var order = element.options[index].value; 49 | Setting.set('order', order); 50 | 51 | this.setState({ 52 | order: order 53 | }); 54 | 55 | this.doFeeds({ 56 | order: order 57 | }); 58 | }, 59 | doFilter: function doFilter() { 60 | var filter = document.getElementById('filter').value; 61 | 62 | this.setState({ 63 | filter: filter 64 | }); 65 | 66 | this.doFeeds({ 67 | filter: filter 68 | }); 69 | }, 70 | doFolders: function doFolders() { 71 | var folders = State.load({ 72 | category: 'folders' 73 | }); 74 | 75 | this.setState({ 76 | folders: folders 77 | }); 78 | 79 | var meta = State.load({ 80 | category: 'meta' 81 | }); 82 | 83 | if (_.has(meta, 'folder')) { 84 | this.setState({ 85 | folder: meta.folder 86 | }); 87 | 88 | this.doFeeds({ 89 | folder: meta.folder 90 | }); 91 | } else { 92 | this.doFeeds(); 93 | } 94 | }, 95 | doFeeds: function doFeeds(params) { 96 | params = params || {}; 97 | 98 | params = _.extend({ 99 | order: this.state.order, 100 | filter: this.state.filter, 101 | folder: this.state.folder 102 | }, params); 103 | 104 | var feeds = State.load({ 105 | category: 'feeds' 106 | }); 107 | 108 | if (process.platform === 'darwin') { 109 | var badge = 0; 110 | 111 | _.each(feeds, function (feed) { 112 | badge += feed.unread_count; 113 | }); 114 | 115 | app.dock.setBadge('' + badge); 116 | } 117 | 118 | if (params.folder === '未分類') { 119 | feeds = _.filter(feeds, function (feed) { 120 | return feed.folder === ''; 121 | }); 122 | } else if (params.folder !== '' && params.folder !== '全て') { 123 | feeds = _.filter(feeds, (function (feed) { 124 | return feed.folder === params.folder; 125 | }).bind(this)); 126 | } 127 | 128 | if (params.filter !== '') { 129 | var regex = new RegExp(params.filter, 'i'); 130 | 131 | feeds = _.filter(feeds, function (feed) { 132 | return regex.test(feed.title); 133 | }); 134 | } 135 | 136 | feeds = feeds.sort((function (a, b) { 137 | switch (params.order) { 138 | case 'modified_on': 139 | return b.modified_on - a.modified_on; 140 | case 'modified_on:reverse': 141 | return a.modified_on - b.modified_on; 142 | case 'unread_count': 143 | return b.unread_count - a.unread_count; 144 | case 'unread_count:reverse': 145 | return a.unread_count - b.unread_count; 146 | case 'title:reverse': 147 | return a.title - b.title; 148 | case 'rate': 149 | return b.rate - a.rate; 150 | case 'subscribers_count': 151 | return b.subscribers_count - a.subscribers_count; 152 | case 'subscribers_count:reverse': 153 | return a.subscribers_count - b.subscribers_count; 154 | } 155 | }).bind(this)); 156 | 157 | var setting = Setting.get(); 158 | 159 | if (_.has(setting, 'unread') && !setting.unread) { 160 | feeds = _.filter(feeds, function (feed) { 161 | return feed.unread_count; 162 | }); 163 | } 164 | 165 | this.setState({ 166 | feeds: feeds 167 | }); 168 | }, 169 | doFetch: function doFetch() { 170 | document.getElementById('progressbar').style.width = '0%'; 171 | 172 | progress(request.post('http://reader.livedoor.com/api/subs', { 173 | headers: { 174 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 175 | Cookie: Cookie.get() 176 | }, 177 | form: { 178 | unread: 0 179 | } 180 | }, (function (error, response, body) { 181 | if (error) { 182 | return; 183 | } 184 | 185 | var feeds; 186 | 187 | try { 188 | feeds = JSON.parse(body); 189 | } catch (e) { 190 | return; 191 | } 192 | 193 | State.save({ 194 | category: 'feeds', 195 | content: feeds 196 | }); 197 | 198 | document.getElementById('progressbar').style.width = '50%'; 199 | 200 | progress(request.post('http://reader.livedoor.com/api/folders', { 201 | headers: { 202 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 203 | Cookie: Cookie.get() 204 | } 205 | }, (function (error, response, body) { 206 | if (error) { 207 | return; 208 | } 209 | 210 | var folders; 211 | 212 | try { 213 | folders = JSON.parse(body); 214 | } catch (e) { 215 | return; 216 | } 217 | 218 | folders.name2id = _.extend({ 219 | '全て': '-1' 220 | }, folders.name2id); 221 | folders.name2id = _.extend({ 222 | '未分類': '0' 223 | }, folders.name2id); 224 | 225 | folders.names.unshift('未分類'); 226 | folders.names.unshift('全て'); 227 | 228 | State.save({ 229 | category: 'folders', 230 | content: folders 231 | }); 232 | 233 | document.getElementById('progressbar').style.width = '100%'; 234 | 235 | _.delay(function () { 236 | document.getElementById('progressbar').style.width = '0%'; 237 | }, 500); 238 | 239 | this.doFolders(); 240 | }).bind(this)), { 241 | throttle: 100 242 | }).on('progress', function (state) { 243 | document.getElementById('progressbar').style.width = state.percent * 0.5 + 50 + '%'; 244 | }); 245 | }).bind(this)), { 246 | throttle: 100 247 | }).on('progress', function (state) { 248 | document.getElementById('progressbar').style.width = state.percent * 0.5 + '%'; 249 | }); 250 | }, 251 | componentDidMount: function componentDidMount() { 252 | var setting = Setting.get(), 253 | meta = State.load({ 254 | category: 'meta' 255 | }); 256 | 257 | document.getElementsByTagName('body')[0].style.fontFamily = setting.fontfamily; 258 | 259 | _.each(document.getElementById('order').options, (function (item, index) { 260 | if (item.value === setting.order) { 261 | item.selected = 'selected'; 262 | } 263 | }).bind(this)); 264 | 265 | if (State.exists({ category: 'feeds' }) && State.exists({ category: 'folders' }) && _.has(meta, 'folder') && (_.isUndefined(setting, 'state') || setting.state)) { 266 | this.setState({ 267 | folders: State.load({ 268 | category: 'folders' 269 | }) 270 | }); 271 | 272 | this.setState({ 273 | folder: meta.folder 274 | }); 275 | 276 | this.doFeeds({ 277 | order: setting.order, 278 | folder: meta.folder 279 | }); 280 | } else { 281 | this.doFetch(); 282 | } 283 | 284 | ipc.on('folders', this.doFolders); 285 | ipc.on('setting', this.doFeeds); 286 | ipc.on('reload', this.doFetch); 287 | mousetrap.bind('r', this.doFetch); 288 | }, 289 | componentWillUnmount: function componentWillUnmount() { 290 | ipc.removeListener('folders', this.doFolders); 291 | ipc.removeListener('setting', this.doFeeds); 292 | ipc.removeListener('reload', this.doFetch); 293 | mousetrap.unbind('r'); 294 | mousetrap.unbind('?'); 295 | mousetrap.unbind('f'); 296 | }, 297 | render: function render() { 298 | return React.createElement( 299 | 'div', 300 | { id: 'wrapper' }, 301 | React.createElement( 302 | 'div', 303 | { id: 'extend' }, 304 | React.createElement( 305 | 'div', 306 | { id: 'folders' }, 307 | React.createElement(Folders, { folders: this.state.folders, folder: this.state.folder }) 308 | ) 309 | ), 310 | React.createElement( 311 | 'div', 312 | { id: 'sidebar' }, 313 | React.createElement( 314 | 'div', 315 | { id: 'tools' }, 316 | React.createElement( 317 | 'select', 318 | { id: 'order', onChange: this.doOrder }, 319 | React.createElement( 320 | 'option', 321 | { value: 'modified_on' }, 322 | '新着順' 323 | ), 324 | React.createElement( 325 | 'option', 326 | { value: 'modified_on:reverse' }, 327 | '旧着順' 328 | ), 329 | React.createElement( 330 | 'option', 331 | { value: 'unread_count' }, 332 | '未読が多い' 333 | ), 334 | React.createElement( 335 | 'option', 336 | { value: 'unread_count:reverse' }, 337 | '未読が少ない' 338 | ), 339 | React.createElement( 340 | 'option', 341 | { value: 'title:reverse' }, 342 | 'タイトル' 343 | ), 344 | React.createElement( 345 | 'option', 346 | { value: 'rate' }, 347 | 'レート' 348 | ), 349 | React.createElement( 350 | 'option', 351 | { value: 'subscribers_count' }, 352 | '読者が多い' 353 | ), 354 | React.createElement( 355 | 'option', 356 | { value: 'subscribers_count:reverse' }, 357 | '読者が少ない' 358 | ) 359 | ), 360 | React.createElement('input', { id: 'filter', type: 'test', onKeyUp: this.doFilter }) 361 | ), 362 | React.createElement( 363 | 'div', 364 | { id: 'feeds' }, 365 | React.createElement(Feeds, { feeds: this.state.feeds }) 366 | ) 367 | ), 368 | React.createElement( 369 | 'div', 370 | { id: 'content' }, 371 | React.createElement('div', { id: 'items', tabIndex: '-1' }) 372 | ), 373 | React.createElement(ReactTooltip, null) 374 | ); 375 | } 376 | }); 377 | App = ReactDnD.DragDropContext(HTML5Backend)(App); 378 | React.render(React.createElement(App), document.getElementById('app')); 379 | 380 | var doShortcut = function doShortcut() { 381 | var element = document.getElementById('help'); 382 | 383 | if (element.style.display === '' || element.style.display === 'none') { 384 | element.style.display = 'block'; 385 | } else { 386 | element.style.display = 'none'; 387 | } 388 | }; 389 | mousetrap.bind('?', doShortcut); 390 | remote.getCurrentWindow().on('shortcut', doShortcut); 391 | 392 | mousetrap.bind('f', function () { 393 | _.defer(function () { 394 | document.getElementById('filter').focus(); 395 | }); 396 | }); -------------------------------------------------------------------------------- /app/component/feed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | React = require('react'), 6 | ReactDnD = require('react-dnd'), 7 | State = require('../state'); 8 | 9 | var ipc = electron.ipcRenderer; 10 | 11 | var Feed = React.createClass({ 12 | displayName: 'feed', 13 | doMouseEnter: function doMouseEnter() { 14 | ipc.emit('feed:mouseenter', this.props.index); 15 | }, 16 | doMouseLeave: function doMouseLeave() { 17 | ipc.emit('feed:mouseleave', this.props.index); 18 | }, 19 | doClick: function doClick() { 20 | ipc.emit('feed:click', this.props.index); 21 | }, 22 | componentDidMount: function componentDidMount() { 23 | if (State.exists({ category: 'meta' })) { 24 | var meta = State.load({ 25 | category: 'meta' 26 | }); 27 | 28 | if (this.props.subscribe_id === meta.subscribe_id) { 29 | React.findDOMNode(this).style.color = '#7fdbff'; 30 | React.findDOMNode(this).style.backgroundColor = '#001f3f'; 31 | 32 | ipc.emit('feed:active', this.props.index); 33 | } 34 | } 35 | }, 36 | render: function render() { 37 | return this.props.connectDragSource(React.createElement( 38 | 'li', 39 | { id: this.props.subscribe_id, 40 | style: this.props.font, 41 | onMouseEnter: this.doMouseEnter, 42 | onMouseLeave: this.doMouseLeave, 43 | onClick: this.doClick }, 44 | React.createElement('img', { src: this.props.icon, style: this.props.favicon }), 45 | React.createElement( 46 | 'span', 47 | { className: this.props.feed }, 48 | this.props.title 49 | ), 50 | React.createElement( 51 | 'span', 52 | { className: this.props.badge }, 53 | this.props.unread_count 54 | ) 55 | )); 56 | } 57 | }); 58 | 59 | Feed = ReactDnD.DragSource('feed', { 60 | beginDrag: function beginDrag(props) { 61 | return { 62 | subscribe_id: props.subscribe_id, 63 | folder: props.folder 64 | }; 65 | } 66 | }, function (connect, monitor) { 67 | return { 68 | connectDragSource: connect.dragSource(), 69 | connectDragPreview: connect.dragPreview(), 70 | isDragging: monitor.isDragging() 71 | }; 72 | })(Feed); 73 | 74 | module.exports = Feed; -------------------------------------------------------------------------------- /app/component/feeds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | mousetrap = require('mousetrap'), 6 | progress = require('request-progress'), 7 | request = require('request'), 8 | React = require('react'), 9 | ReactDnD = require('react-dnd'), 10 | Cookie = require('../cookie'), 11 | Setting = require('../setting'), 12 | State = require('../state'), 13 | Feed = require('./feed'), 14 | Items = require('./items'); 15 | 16 | var ipc = electron.ipcRenderer, 17 | remote = electron.remote; 18 | 19 | var app = remote.app; 20 | 21 | module.exports = React.createClass({ 22 | displayName: 'feeds', 23 | getInitialState: function getInitialState() { 24 | return { 25 | active: null, 26 | toggle: true 27 | }; 28 | }, 29 | doActive: function doActive(index) { 30 | this.setState({ 31 | active: index 32 | }); 33 | }, 34 | doMouseEnter: function doMouseEnter(index) { 35 | if (index !== this.state.active) { 36 | var ul = document.getElementById('feeds').children[0]; 37 | React.findDOMNode(ul).childNodes[index].style.color = '#7fdbff'; 38 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = '#001f3f'; 39 | } 40 | }, 41 | doMouseLeave: function doMouseLeave(index) { 42 | if (index !== this.state.active) { 43 | var ul = document.getElementById('feeds').children[0]; 44 | React.findDOMNode(ul).childNodes[index].style.color = '#ffffff'; 45 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'transparent'; 46 | } 47 | }, 48 | doClick: function doClick(index) { 49 | this.setState({ 50 | active: index 51 | }); 52 | 53 | var me, 54 | ul = document.getElementById('feeds').children[0]; 55 | 56 | _.each(React.findDOMNode(ul).childNodes, function (child, i) { 57 | if (index === i) { 58 | me = child; 59 | 60 | child.style.color = '#7fdbff'; 61 | child.style.backgroundColor = '#001f3f'; 62 | } else { 63 | child.style.color = '#ffffff'; 64 | child.style.backgroundColor = 'transparent'; 65 | } 66 | }); 67 | 68 | if (process.platform === 'darwin') { 69 | app.dock.setBadge('' + (parseInt(app.dock.getBadge(), 10) - parseInt(me.children[2].textContent, 10))); 70 | } 71 | 72 | var url = 'http://reader.livedoor.com/api/unread', 73 | feed = this.props.feeds[index], 74 | pins = []; 75 | 76 | if (parseInt(me.children[2].textContent, 10) === 0) { 77 | url = 'http://reader.livedoor.com/api/all'; 78 | } 79 | 80 | document.getElementById('progressbar').style.width = '0%'; 81 | 82 | progress(request.post(url, { 83 | headers: { 84 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 85 | Cookie: Cookie.get() 86 | }, 87 | form: { 88 | subscribe_id: feed.subscribe_id 89 | } 90 | }, (function (error, response, body) { 91 | if (error) { 92 | return; 93 | } 94 | 95 | React.unmountComponentAtNode(document.getElementById('items')); 96 | 97 | var json; 98 | 99 | try { 100 | json = JSON.parse(body); 101 | State.save({ 102 | category: 'items', 103 | content: json.items 104 | }); 105 | } catch (e) { 106 | return; 107 | } 108 | 109 | request.post('http://reader.livedoor.com/api/pin/all', { 110 | headers: { 111 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 112 | Cookie: Cookie.get() 113 | } 114 | }, function (error, response, body) { 115 | if (!error) { 116 | try { 117 | pins = JSON.parse(body); 118 | } catch (e) {} 119 | } 120 | 121 | State.merge({ 122 | category: 'meta', 123 | content: { 124 | subscribe_id: feed.subscribe_id, 125 | title: feed.title, 126 | pins: pins 127 | } 128 | }); 129 | 130 | React.render(React.createElement(Items, { 131 | feed: feed, 132 | items: json.items, 133 | pins: pins 134 | }), document.getElementById('items')); 135 | }); 136 | 137 | me.children[2].textContent = 0; 138 | 139 | feed.unread_count = 0; 140 | State.merge({ 141 | category: 'feeds', 142 | content: feed 143 | }); 144 | 145 | if (!me.children[1].classList.contains('feed-zero')) { 146 | me.children[1].classList.add('feed-zero'); 147 | } 148 | 149 | if (!me.children[2].classList.contains('badge-zero')) { 150 | me.children[2].classList.add('badge-zero'); 151 | } 152 | 153 | request.post('http://reader.livedoor.com/api/touch_all', { 154 | headers: { 155 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 156 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 157 | }, 158 | form: { 159 | subscribe_id: feed.subscribe_id 160 | } 161 | }, function (error, response, body) {}); 162 | 163 | document.getElementById('progressbar').style.width = '100%'; 164 | _.delay(function () { 165 | document.getElementById('progressbar').style.width = '0%'; 166 | }, 500); 167 | }).bind(this)), { 168 | throttle: 100 169 | }).on('progress', function (state) { 170 | document.getElementById('progressbar').style.width = state.percent + '%'; 171 | }); 172 | }, 173 | doPrev: function doPrev() { 174 | var index = _.isNull(this.state.active) ? 0 : this.state.active - 1; 175 | if (index < 0) { 176 | index = 0; 177 | } 178 | 179 | document.getElementById(this.props.feeds[index].subscribe_id).scrollIntoView(); 180 | 181 | this.doClick(index); 182 | }, 183 | doNext: function doNext() { 184 | var index = _.isNull(this.state.active) ? 0 : this.state.active + 1; 185 | if (index >= this.props.feeds.length) { 186 | index = this.props.feeds.length - 1; 187 | } 188 | 189 | document.getElementById(this.props.feeds[index].subscribe_id).scrollIntoView(); 190 | 191 | this.doClick(index); 192 | }, 193 | doToggle: function doToggle() { 194 | document.getElementById('folders').style.display = this.state.toggle ? 'none' : 'block'; 195 | document.getElementById('sidebar').style.display = this.state.toggle ? 'none' : 'block'; 196 | document.getElementById('items').style.left = this.state.toggle ? 0 : '316px'; 197 | this.setState({ 198 | toggle: !this.state.toggle 199 | }); 200 | }, 201 | componentDidMount: function componentDidMount() { 202 | ipc.on('feed:mouseenter', this.doMouseEnter); 203 | ipc.on('feed:mouseleave', this.doMouseLeave); 204 | ipc.on('feed:click', this.doClick); 205 | ipc.on('feed:active', this.doActive); 206 | 207 | mousetrap.bind('a', this.doPrev); 208 | mousetrap.bind('s', this.doNext); 209 | mousetrap.bind('z', this.doToggle); 210 | 211 | var setting = Setting.get(); 212 | 213 | if (State.exists({ category: 'feeds' }) && State.exists({ category: 'items' }) && State.exists({ category: 'meta' }) && (_.isUndefined(setting, 'state') || setting.state)) { 214 | var feeds = State.load({ 215 | category: 'feeds' 216 | }), 217 | items = State.load({ 218 | category: 'items' 219 | }), 220 | meta = State.load({ 221 | category: 'meta' 222 | }); 223 | 224 | React.render(React.createElement(Items, { 225 | feed: _.filter(feeds, function (item) { 226 | return item.subscribe_id === meta.subscribe_id; 227 | })[0], 228 | items: items, 229 | pins: meta.pins 230 | }), document.getElementById('items')); 231 | } 232 | }, 233 | componentDidUpdate: function componentDidUpdate() { 234 | var setting = Setting.get(); 235 | 236 | if (State.exists({ category: 'feeds' }) && State.exists({ category: 'items' }) && State.exists({ category: 'meta' }) && (_.isUndefined(setting, 'state') || setting.state)) { 237 | var feeds = State.load({ 238 | category: 'feeds' 239 | }), 240 | items = State.load({ 241 | category: 'items' 242 | }), 243 | meta = State.load({ 244 | category: 'meta' 245 | }); 246 | 247 | React.render(React.createElement(Items, { 248 | feed: _.filter(feeds, function (item) { 249 | return item.subscribe_id === meta.subscribe_id; 250 | })[0], 251 | items: items, 252 | pins: meta.pins 253 | }), document.getElementById('items')); 254 | } 255 | }, 256 | componentWillReceiveProps: function componentWillReceiveProps() { 257 | this.setState({ 258 | active: null 259 | }); 260 | }, 261 | componentWillUnmount: function componentWillUnmount() { 262 | ipc.removeListener('feed:mouseenter', this.doMouseEnter); 263 | ipc.removeListener('feed:mouseleave', this.doMouseLeave); 264 | ipc.removeListener('feed:click', this.doClick); 265 | ipc.removeListener('feed:active', this.doActive); 266 | 267 | mousetrap.unbind('a'); 268 | mousetrap.unbind('s'); 269 | mousetrap.unbind('z'); 270 | }, 271 | render: function render() { 272 | var setting = Setting.get(), 273 | favicon = { 274 | display: setting.favicon ? 'inline' : 'none' 275 | }, 276 | font = { 277 | fontFamily: setting.fontfamily 278 | }; 279 | 280 | return React.createElement( 281 | 'ul', 282 | null, 283 | this.props.feeds.map(function (item, index) { 284 | var feed = 'feed', 285 | badge = 'badge'; 286 | 287 | if (item.unread_count === 0) { 288 | feed += ' feed-zero'; 289 | badge += ' badge-zero'; 290 | } 291 | 292 | return React.createElement(Feed, { key: item.subscribe_id, 293 | subscribe_id: item.subscribe_id, 294 | index: index, 295 | font: font, 296 | icon: item.icon, 297 | favicon: favicon, 298 | feed: feed, 299 | title: item.title, 300 | badge: badge, 301 | unread_count: item.unread_count, 302 | folder: item.folder }); 303 | }, this) 304 | ); 305 | } 306 | }); -------------------------------------------------------------------------------- /app/component/folder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | request = require('request'), 6 | React = require('react'), 7 | ReactDnD = require('react-dnd'), 8 | Cookie = require('../cookie'); 9 | 10 | var ipc = electron.ipcRenderer, 11 | remote = electron.remote; 12 | 13 | var Folder = React.createClass({ 14 | displayName: 'folder', 15 | doMouseEnter: function doMouseEnter() { 16 | ipc.emit('folder:mouseenter', this.props.index); 17 | }, 18 | doMouseLeave: function doMouseLeave() { 19 | ipc.emit('folder:mouseleave', this.props.index); 20 | }, 21 | onMouseDown: function onMouseDown() { 22 | ipc.emit('folder:mousedown', this.props.index); 23 | }, 24 | onMouseUp: function onMouseUp() { 25 | ipc.emit('folder:mouseup', this.props.index); 26 | }, 27 | render: function render() { 28 | var style = _.extend(this.props.style, { 29 | boxShadow: this.props.isOver && this.props.canDrop ? '0px 0px 0px 4px rgba(255, 255, 255, 0.8)' : 'none' 30 | }); 31 | 32 | return this.props.connectDropTarget(React.createElement( 33 | 'li', 34 | { style: style, 35 | onMouseEnter: this.doMouseEnter, 36 | onMouseLeave: this.doMouseLeave, 37 | onMouseDown: this.onMouseDown, 38 | onMouseUp: this.onMouseUp }, 39 | React.createElement( 40 | 'p', 41 | { 'data-value': this.props.name, 42 | 'data-tip': this.props.name, 43 | 'data-place': 'right', 44 | 'data-type': 'light', 45 | 'data-effect': 'solid' }, 46 | this.props.name.substr(0, 1) 47 | ) 48 | )); 49 | } 50 | }); 51 | 52 | Folder = ReactDnD.DropTarget('feed', { 53 | canDrop: function canDrop(props, monitor) { 54 | return props.folder_id !== '-1' && monitor.getItem().folder !== props.name || props.folder_id === 0; 55 | }, 56 | drop: function drop(props, monitor) { 57 | var to = props.name; 58 | 59 | if (to === '未分類') { 60 | to = ''; 61 | } 62 | 63 | var subscribe_id = monitor.getItem().subscribe_id; 64 | 65 | request.post('http://reader.livedoor.com/api/folders', { 66 | headers: { 67 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 68 | Cookie: Cookie.get() 69 | } 70 | }, function (error, response, body) { 71 | if (error) { 72 | return; 73 | } 74 | 75 | var folders; 76 | 77 | try { 78 | folders = JSON.parse(body); 79 | } catch (e) { 80 | return; 81 | } 82 | 83 | if (to !== '' && !_.has(folders.name2id, to)) { 84 | return; 85 | } 86 | 87 | request.post('http://reader.livedoor.com/api/feed/move', { 88 | headers: { 89 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 90 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 91 | }, 92 | form: { 93 | subscribe_id: subscribe_id, 94 | to: to 95 | } 96 | }, function (error, response, body) { 97 | if (error) { 98 | return; 99 | } 100 | 101 | var json; 102 | 103 | try { 104 | json = JSON.parse(body); 105 | } catch (e) { 106 | return; 107 | } 108 | 109 | if (json.isSuccess) { 110 | ipc.emit('reload'); 111 | } 112 | }); 113 | }); 114 | 115 | return; 116 | } 117 | }, function (connect, monitor) { 118 | return { 119 | connectDropTarget: connect.dropTarget(), 120 | isOver: monitor.isOver(), 121 | canDrop: monitor.canDrop() 122 | }; 123 | })(Folder); 124 | 125 | module.exports = Folder; -------------------------------------------------------------------------------- /app/component/folders.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | mousetrap = require('mousetrap'), 6 | request = require('request'), 7 | React = require('react'), 8 | Cookie = require('../cookie'), 9 | State = require('../state'), 10 | Folder = require('./folder'); 11 | 12 | var ipc = electron.ipcRenderer, 13 | remote = electron.remote; 14 | 15 | var dialog = remote.dialog; 16 | 17 | module.exports = React.createClass({ 18 | displayName: 'folders', 19 | getInitialState: function getInitialState() { 20 | return { 21 | active: null, 22 | touch: 0 23 | }; 24 | }, 25 | doMouseEnter: function doMouseEnter(index) { 26 | if (index !== this.state.active) { 27 | var ul = document.getElementById('folders').children[0]; 28 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'rgba(255, 255, 255, 1)'; 29 | } 30 | }, 31 | doMouseLeave: function doMouseLeave(index) { 32 | if (index !== this.state.active) { 33 | var ul = document.getElementById('folders').children[0]; 34 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; 35 | } 36 | }, 37 | doMouseDown: function doMouseDown(index) { 38 | this.setState({ 39 | touch: Date.now() 40 | }); 41 | }, 42 | doMouseUp: function doMouseUp(index) { 43 | if (Date.now() - this.state.touch < 1000) { 44 | this.doClick(index); 45 | return; 46 | } 47 | 48 | if (index === 0 || index === -1) { 49 | return; 50 | } 51 | 52 | dialog.showMessageBox(remote.getCurrentWindow(), { 53 | type: 'question', 54 | buttons: ['キャンセル', '削除する'], 55 | message: '「' + this.props.folders.names[index] + '」フォルダを削除しますか?(中のフィードは削除されません)', 56 | cancelId: 0 57 | }, (function (e) { 58 | if (e === 0) { 59 | return; 60 | } 61 | 62 | request.post('http://reader.livedoor.com/api/folders', { 63 | headers: { 64 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 65 | Cookie: Cookie.get() 66 | } 67 | }, (function (error, response, body) { 68 | if (error) { 69 | return; 70 | } 71 | 72 | var json; 73 | 74 | try { 75 | json = JSON.parse(body); 76 | } catch (e) { 77 | return; 78 | } 79 | 80 | if (!_.has(json.name2id, this.props.folders.names[index])) { 81 | return; 82 | } 83 | 84 | request.post('http://reader.livedoor.com/api/folder/delete', { 85 | headers: { 86 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 87 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 88 | }, 89 | form: { 90 | folder_id: json.name2id[this.props.folders.names[index]] 91 | } 92 | }, function (error, response, body) { 93 | if (error) { 94 | return; 95 | } 96 | 97 | ipc.emit('reload'); 98 | }); 99 | }).bind(this)); 100 | }).bind(this)); 101 | }, 102 | doClick: function doClick(index) { 103 | this.setState({ 104 | active: index 105 | }); 106 | 107 | var me, 108 | ul = document.getElementById('folders').children[0]; 109 | 110 | _.each(React.findDOMNode(ul).childNodes, function (child, i) { 111 | if (index === i) { 112 | me = child; 113 | 114 | child.style.backgroundColor = 'rgba(255, 255, 255, 1)'; 115 | } else { 116 | child.style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; 117 | } 118 | }); 119 | 120 | State.merge({ 121 | category: 'meta', 122 | content: { 123 | folder: this.props.folders.names[index] 124 | } 125 | }); 126 | 127 | ipc.emit('folders'); 128 | }, 129 | componentWillReceiveProps: function componentWillReceiveProps(props) { 130 | ipc.on('folder:mouseenter', this.doMouseEnter); 131 | ipc.on('folder:mouseleave', this.doMouseLeave); 132 | ipc.on('folder:mousedown', this.doMouseDown); 133 | ipc.on('folder:mouseup', this.doMouseUp); 134 | 135 | _.each(props.folders.names, (function (item, index) { 136 | if (item === props.folder) { 137 | this.setState({ 138 | active: index 139 | }); 140 | } 141 | }).bind(this)); 142 | }, 143 | componentWillUnmount: function componentWillUnmount() { 144 | ipc.removeListener('folder:mouseenter', this.doMouseEnter); 145 | ipc.removeListener('folder:mouseleave', this.doMouseLeave); 146 | ipc.removeListener('folder:mousedown', this.doMouseDown); 147 | ipc.removeListener('folder:mouseup', this.doMouseUp); 148 | 149 | var ul = document.getElementById('folders').children[0]; 150 | 151 | _.each(React.findDOMNode(ul).childNodes, (function (item, index) { 152 | if (index < 10) { 153 | mousetrap.unbind(['command+' + (index + 1), 'ctrl' + (index + 1)]); 154 | } 155 | }).bind(this)); 156 | }, 157 | render: function render() { 158 | var setting = Setting.get(); 159 | 160 | return React.createElement( 161 | 'ul', 162 | null, 163 | this.props.folders.names.map(function (item, index) { 164 | var style = { 165 | fontFamily: setting.fontfamily 166 | }; 167 | 168 | if (this.props.folder === item) { 169 | style.backgroundColor = 'rgba(255, 255, 255, 1)'; 170 | } 171 | 172 | if (index < 10) { 173 | mousetrap.bind(['command+' + (index + 1), 'ctrl' + (index + 1)], (function () { 174 | this.doClick(index); 175 | }).bind(this)); 176 | } 177 | 178 | return React.createElement(Folder, { key: this.props.folders.name2id[item], 179 | folder_id: this.props.folders.name2id[item], 180 | index: index, 181 | style: style, 182 | name: item }); 183 | }, this) 184 | ); 185 | } 186 | }); -------------------------------------------------------------------------------- /app/component/items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | moment = require('moment'), 6 | mousetrap = require('mousetrap'), 7 | request = require('request'), 8 | React = require('react'), 9 | Modal = require('react-modal'), 10 | Setting = require('../setting'), 11 | State = require('../state'), 12 | Cookie = require('../cookie'), 13 | Meta = require('../component/meta'); 14 | 15 | var remote = electron.remote, 16 | shell = electron.shell; 17 | 18 | var dialog = remote.dialog; 19 | 20 | moment.locale('ja'); 21 | 22 | var style = { 23 | title: { 24 | marginBottom: '4px', 25 | fontSize: '20px', 26 | fontWeight: 'bold', 27 | cursor: 'pointer' 28 | }, 29 | description: { 30 | marginBottom: '20px', 31 | fontSize: '14px', 32 | color: '#aaaaaa' 33 | }, 34 | created: { 35 | marginRight: '6px' 36 | }, 37 | author: { 38 | marginRight: '6px' 39 | }, 40 | category: { 41 | marginRight: '6px' 42 | }, 43 | modal: { 44 | content: { 45 | padding: 0 46 | } 47 | }, 48 | browser: { 49 | display: 'inline-block', 50 | width: '100%', 51 | height: '100%' 52 | } 53 | }; 54 | 55 | module.exports = React.createClass({ 56 | displayName: 'items', 57 | getInitialState: function getInitialState() { 58 | return { 59 | collapse: false, 60 | modalIsOpen: false, 61 | active: null, 62 | url: '' 63 | }; 64 | }, 65 | doOpen: function doOpen(index) { 66 | this.setState({ 67 | modalIsOpen: true, 68 | url: this.props.items[index].link 69 | }); 70 | 71 | document.getElementById('browser').addEventListener('dom-ready', function () { 72 | document.getElementById('browser').focus(); 73 | }); 74 | 75 | remote.getCurrentWindow().focusOnWebView(); 76 | }, 77 | doClose: function doClose() { 78 | remote.getCurrentWindow().blurWebView(); 79 | 80 | this.setState({ 81 | modalIsOpen: false 82 | }); 83 | }, 84 | doPrev: function doPrev() { 85 | var index = _.isNull(this.state.active) ? 0 : this.state.active - 1; 86 | if (index < 0) { 87 | index = 0; 88 | } 89 | 90 | document.getElementById(this.props.items[index].id).scrollIntoView(); 91 | this.setState({ 92 | active: index 93 | }); 94 | 95 | State.merge({ 96 | category: 'meta', 97 | content: { 98 | items: index 99 | } 100 | }); 101 | }, 102 | doNext: function doNext() { 103 | var index = _.isNull(this.state.active) ? 0 : this.state.active + 1; 104 | if (index >= this.props.items.length) { 105 | index = this.props.items.length - 1; 106 | } 107 | 108 | document.getElementById(this.props.items[index].id).scrollIntoView(); 109 | this.setState({ 110 | active: index 111 | }); 112 | 113 | State.merge({ 114 | category: 'meta', 115 | content: { 116 | items: index 117 | } 118 | }); 119 | }, 120 | doCollapse: function doCollapse() { 121 | this.props.items.map(function (item) { 122 | document.getElementById(item.id).children[2].style.display = !this.state.collapse ? 'none' : 'block'; 123 | }, this); 124 | 125 | this.setState({ 126 | collapse: !this.state.collapse 127 | }); 128 | }, 129 | doModal: function doModal() { 130 | if (this.state.modalIsOpen) { 131 | this.doClose(); 132 | document.getElementById('feeds').style.overflowY = 'auto'; 133 | document.getElementById('items').style.overflowY = 'auto'; 134 | } else { 135 | this.doOpen(_.isNull(this.state.active) ? 0 : this.state.active); 136 | document.getElementById('feeds').style.overflowY = 'hidden'; 137 | document.getElementById('items').style.overflowY = 'hidden'; 138 | } 139 | }, 140 | doPinning: function doPinning() { 141 | var index = _.isNull(this.state.active) ? 0 : this.state.active, 142 | item = this.props.items[index]; 143 | 144 | request.post('http://reader.livedoor.com/api/pin/all', { 145 | headers: { 146 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 147 | Cookie: Cookie.get() 148 | } 149 | }, (function (error, response, body) { 150 | if (error) { 151 | return; 152 | } 153 | 154 | var json, 155 | pins = this.props.pins; 156 | 157 | try { 158 | json = JSON.parse(body); 159 | } catch (e) { 160 | return; 161 | } 162 | 163 | var url = 'http://reader.livedoor.com/api/pin/', 164 | form = {}, 165 | haspin = _.where(json, { link: item.link }).length > 0; 166 | 167 | if (haspin) { 168 | url += 'remove'; 169 | form = { 170 | link: item.link 171 | }; 172 | } else { 173 | url += 'add'; 174 | form = { 175 | link: item.link, 176 | title: item.title 177 | }; 178 | } 179 | 180 | request.post(url, { 181 | headers: { 182 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 183 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 184 | }, 185 | form: form 186 | }, function (error, response, body) { 187 | if (error) { 188 | return; 189 | } 190 | 191 | var json; 192 | 193 | try { 194 | json = JSON.parse(body); 195 | } catch (e) { 196 | return; 197 | } 198 | 199 | if (json.isSuccess) { 200 | if (haspin) { 201 | document.getElementById(item.id).classList.remove('haspin'); 202 | 203 | State.merge({ 204 | category: 'meta', 205 | content: { 206 | pins: _.filter(pins, function (pin) { 207 | return pin.link !== item.link; 208 | }) 209 | } 210 | }); 211 | } else { 212 | document.getElementById(item.id).classList.add('haspin'); 213 | 214 | pins.push({ 215 | link: item.link, 216 | title: item.title 217 | }); 218 | 219 | State.merge({ 220 | category: 'meta', 221 | content: { 222 | pins: pins 223 | } 224 | }); 225 | } 226 | } 227 | }); 228 | }).bind(this)); 229 | }, 230 | doBrowser: function doBrowser() { 231 | var index = _.isNull(this.state.active) ? 0 : this.state.active; 232 | 233 | shell.openExternal(this.props.items[index].link); 234 | }, 235 | doUnsubscribe: function doUnsubscribe() { 236 | dialog.showMessageBox(remote.getCurrentWindow(), { 237 | type: 'question', 238 | buttons: ['キャンセル', '解除する'], 239 | message: '「' + this.props.feed.title + '」の登録を解除しますか?', 240 | cancelId: 0 241 | }, (function (e) { 242 | if (e === 0) { 243 | return; 244 | } 245 | 246 | request.post('http://reader.livedoor.com/api/all', { 247 | headers: { 248 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 249 | Cookie: Cookie.get() 250 | }, 251 | form: { 252 | subscribe_id: this.props.feed.subscribe_id, 253 | offset: 0, 254 | limit: 1 255 | } 256 | }, (function (error, response, body) { 257 | if (error) { 258 | return; 259 | } 260 | 261 | request.post('http://reader.livedoor.com/api/feed/unsubscribe', { 262 | headers: { 263 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 264 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 265 | }, 266 | form: { 267 | subscribe_id: this.props.feed.subscribe_id 268 | } 269 | }, function (error, response, body) {}); 270 | }).bind(this)); 271 | }).bind(this)); 272 | }, 273 | componentDidMount: function componentDidMount() { 274 | mousetrap.bind('k', this.doPrev); 275 | mousetrap.bind('j', this.doNext); 276 | mousetrap.bind('c', this.doCollapse); 277 | mousetrap.bind('v', this.doModal); 278 | mousetrap.bind('p', this.doPinning); 279 | mousetrap.bind('b', this.doBrowser); 280 | mousetrap.bind('del', this.doUnsubscribe); 281 | 282 | if (this.props.items[0]) { 283 | document.getElementById(this.props.items[0].id).scrollIntoView(); 284 | this.setState({ 285 | active: 0 286 | }); 287 | } 288 | 289 | var setting = Setting.get(); 290 | 291 | if (State.exists({ category: 'meta' }) && (_.isUndefined(setting, 'state') || setting.state)) { 292 | var meta = State.load({ 293 | category: 'meta' 294 | }); 295 | 296 | if (meta.items && this.props.items[meta.items]) { 297 | document.getElementById(this.props.items[meta.items].id).scrollIntoView(); 298 | this.setState({ 299 | active: meta.items 300 | }); 301 | } 302 | } 303 | 304 | document.getElementById('items').focus(); 305 | }, 306 | componentWillUnmount: function componentWillUnmount() { 307 | mousetrap.unbind('k'); 308 | mousetrap.unbind('j'); 309 | mousetrap.unbind('c'); 310 | mousetrap.unbind('v'); 311 | mousetrap.unbind('p'); 312 | mousetrap.unbind('b'); 313 | mousetrap.unbind('del'); 314 | 315 | State.merge({ 316 | category: 'meta', 317 | content: { 318 | items: 0 319 | } 320 | }); 321 | }, 322 | render: function render() { 323 | var setting = Setting.get(), 324 | font = { 325 | fontFamily: setting.fontfamily, 326 | fontSize: setting.fontsize 327 | }; 328 | 329 | return React.createElement( 330 | 'ul', 331 | null, 332 | React.createElement(Meta, { feed: this.props.feed }), 333 | this.props.items.map(function (item, index) { 334 | var haspin = ''; 335 | 336 | if (_.where(this.props.pins, { link: item.link }).length > 0) { 337 | haspin = ' haspin'; 338 | } 339 | 340 | return React.createElement( 341 | 'li', 342 | { id: item.id, className: haspin, key: item.id }, 343 | React.createElement( 344 | 'p', 345 | { style: style.title, onClick: this.doOpen.bind(this, index) }, 346 | item.title 347 | ), 348 | React.createElement( 349 | 'p', 350 | { style: style.description }, 351 | React.createElement( 352 | 'span', 353 | { style: style.created }, 354 | moment(item.created_on * 1000).fromNow() 355 | ), 356 | React.createElement( 357 | 'span', 358 | { style: style.author }, 359 | 'by ', 360 | item.author 361 | ), 362 | React.createElement( 363 | 'span', 364 | { style: style.category }, 365 | item.category 366 | ) 367 | ), 368 | React.createElement('div', { style: font, dangerouslySetInnerHTML: { __html: item.body } }) 369 | ); 370 | }, this), 371 | React.createElement( 372 | Modal, 373 | { isOpen: this.state.modalIsOpen, onRequestClose: this.doClose, style: style.modal }, 374 | React.createElement('webview', { id: 'browser', src: this.state.url, style: style.browser, autosize: 'on' }) 375 | ) 376 | ); 377 | } 378 | }); -------------------------------------------------------------------------------- /app/component/meta.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | electron = require('electron'), 5 | request = require('request'), 6 | React = require('react'), 7 | ReactRater = require('react-rater'), 8 | Cookie = require('../cookie'); 9 | 10 | var ipc = electron.ipcRenderer, 11 | remote = electron.remote; 12 | 13 | var style = { 14 | wrapper: { 15 | margin: '20px', 16 | padding: '20px', 17 | backgroundColor: '#f7f7f7', 18 | border: 0, 19 | borderRadius: '8px' 20 | }, 21 | title: { 22 | float: 'left', 23 | fontSize: '20px' 24 | }, 25 | uers: { 26 | clear: 'both' 27 | } 28 | }; 29 | 30 | module.exports = React.createClass({ 31 | displayName: 'meta', 32 | doRating: function doRating(rating) { 33 | request.post('http://reader.livedoor.com/api/subs', { 34 | headers: { 35 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 36 | Cookie: Cookie.get() 37 | } 38 | }, (function (error, response, body) { 39 | if (error) { 40 | return; 41 | } 42 | 43 | var json; 44 | 45 | try { 46 | json = JSON.parse(body); 47 | } catch (e) { 48 | return; 49 | } 50 | 51 | var feeds = _.filter(json, (function (item, index) { 52 | return item.subscribe_id === this.props.feed.subscribe_id; 53 | }).bind(this)); 54 | 55 | if (feeds.length === 0) { 56 | return; 57 | } 58 | 59 | request.post('http://reader.livedoor.com/api/feed/set_rate', { 60 | headers: { 61 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 62 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 63 | }, 64 | form: { 65 | subscribe_id: this.props.feed.subscribe_id, 66 | rate: rating 67 | } 68 | }, function (error, response, body) { 69 | if (error) { 70 | return; 71 | } 72 | 73 | var json; 74 | 75 | try { 76 | json = JSON.parse(body); 77 | } catch (e) { 78 | return; 79 | } 80 | 81 | if (json.isSuccess) { 82 | ipc.emit('reload'); 83 | } 84 | }); 85 | }).bind(this)); 86 | }, 87 | render: function render() { 88 | return React.createElement( 89 | 'li', 90 | { style: style.wrapper }, 91 | React.createElement( 92 | 'p', 93 | { style: style.title }, 94 | this.props.feed.title 95 | ), 96 | React.createElement(ReactRater, { total: 5, rating: this.props.feed.rate, onRate: this.doRating }), 97 | React.createElement( 98 | 'p', 99 | { style: style.uers }, 100 | '購読者数:', 101 | this.props.feed.subscribers_count, 102 | 'ユーザ' 103 | ) 104 | ); 105 | } 106 | }); -------------------------------------------------------------------------------- /app/cookie.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | fs = require('fs'), 3 | path = require('path'); 4 | 5 | exports.set = function(cookies){ 6 | fs.writeFileSync(path.join(__dirname, 'data', 'cookies'), JSON.stringify(cookies)); 7 | }; 8 | 9 | exports.get = function(){ 10 | if (!exports.exists()) { 11 | return ''; 12 | } 13 | 14 | var cookies = fs.readFileSync(path.join(__dirname, 'data', 'cookies')), 15 | result = []; 16 | 17 | try { 18 | _.each(JSON.parse(cookies), function(cookie){ 19 | var pairs = cookie.split(';'); 20 | 21 | _.each(pairs, function(pair){ 22 | var keyvalue = pair.split('='), 23 | key = keyvalue[0].trim(); 24 | 25 | if (key !== 'expires' && key !== 'path' && 26 | key !== 'domain' && key !== 'secure' && 27 | key !== 'HttpOnly') { 28 | result.push(key + '=' + keyvalue[1]); 29 | } 30 | }); 31 | }); 32 | } catch (e) {} 33 | 34 | return result.join('; '); 35 | }; 36 | 37 | exports.exists = function(){ 38 | return fs.existsSync(path.join(__dirname, 'data', 'cookies')); 39 | }; 40 | 41 | exports.clear = function(){ 42 | fs.unlinkSync(path.join(__dirname, 'data', 'cookies')); 43 | }; 44 | 45 | exports.parseApiKey = function(cookies){ 46 | var result = ''; 47 | 48 | _.each(cookies, function(cookie){ 49 | _.each(cookie.split(';'), function(pair){ 50 | var keyvalue = pair.split('='), 51 | key = keyvalue[0].trim(); 52 | 53 | if (key === 'reader_sid') { 54 | result = keyvalue[1]; 55 | } 56 | }); 57 | }); 58 | 59 | return result; 60 | }; -------------------------------------------------------------------------------- /app/data/.gitplaceholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/data/.gitplaceholder -------------------------------------------------------------------------------- /app/html/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 67 | 68 | 69 | 70 |
71 |

Electron-LDR

72 |

This software includes the following licenses.

73 |
74 |
Electron
75 |
Copyright (c) 2014 GitHub Inc.
 76 | 
 77 | Permission is hereby granted, free of charge, to any person obtaining
 78 | a copy of this software and associated documentation files (the
 79 | "Software"), to deal in the Software without restriction, including
 80 | without limitation the rights to use, copy, modify, merge, publish,
 81 | distribute, sublicense, and/or sell copies of the Software, and to
 82 | permit persons to whom the Software is furnished to do so, subject to
 83 | the following conditions:
 84 | 
 85 | The above copyright notice and this permission notice shall be
 86 | included in all copies or substantial portions of the Software.
 87 | 
 88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 91 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 92 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 93 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 94 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
95 |
React
96 |
BSD License
 97 | 
 98 | For React software
 99 | 
100 | Copyright (c) 2013-2015, Facebook, Inc.
101 | All rights reserved.
102 | 
103 | Redistribution and use in source and binary forms, with or without modification,
104 | are permitted provided that the following conditions are met:
105 | 
106 |  * Redistributions of source code must retain the above copyright notice, this
107 |    list of conditions and the following disclaimer.
108 | 
109 |  * Redistributions in binary form must reproduce the above copyright notice,
110 |    this list of conditions and the following disclaimer in the documentation
111 |    and/or other materials provided with the distribution.
112 | 
113 |  * Neither the name Facebook nor the names of its contributors may be used to
114 |    endorse or promote products derived from this software without specific
115 |    prior written permission.
116 | 
117 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
118 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
119 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
120 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
121 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
122 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
123 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
124 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
125 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
126 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
127 |
react-dnd
128 |
The MIT License (MIT)
129 | 
130 | Copyright (c) 2014 Dan Abramov
131 | 
132 | Permission is hereby granted, free of charge, to any person obtaining a copy
133 | of this software and associated documentation files (the "Software"), to deal
134 | in the Software without restriction, including without limitation the rights
135 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
136 | copies of the Software, and to permit persons to whom the Software is
137 | furnished to do so, subject to the following conditions:
138 | 
139 | The above copyright notice and this permission notice shall be included in all
140 | copies or substantial portions of the Software.
141 | 
142 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
143 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
144 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
145 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
146 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
147 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
148 | SOFTWARE.
149 |
react-modal
150 |
Copyright (c) 2014 Ryan Florence
151 | 
152 | Permission is hereby granted, free of charge, to any person obtaining a copy of
153 | this software and associated documentation files (the "Software"), to deal in
154 | the Software without restriction, including without limitation the rights to
155 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
156 | of the Software, and to permit persons to whom the Software is furnished to do
157 | so, subject to the following conditions:
158 | 
159 | The above copyright notice and this permission notice shall be included in all
160 | copies or substantial portions of the Software.
161 | 
162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
168 | SOFTWARE.
169 |
react-rater
170 |
Copyright (c) 2014, NdYAG
171 | All rights reserved.
172 | 
173 | Redistribution and use in source and binary forms, with or without
174 | modification, are permitted provided that the following conditions are met:
175 | 
176 | * Redistributions of source code must retain the above copyright notice, this
177 |   list of conditions and the following disclaimer.
178 | 
179 | * Redistributions in binary form must reproduce the above copyright notice,
180 |   this list of conditions and the following disclaimer in the documentation
181 |   and/or other materials provided with the distribution.
182 | 
183 | * Neither the name of react-rater nor the names of its
184 |   contributors may be used to endorse or promote products derived from
185 |   this software without specific prior written permission.
186 | 
187 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
188 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
189 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
190 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
191 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
192 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
193 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
194 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
195 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
196 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
197 |
react-tooltip
198 |
The MIT License (MIT)
199 | 
200 | Copyright (c) 2015 Wang Zixiao
201 | 
202 | Permission is hereby granted, free of charge, to any person obtaining a copy
203 | of this software and associated documentation files (the "Software"), to deal
204 | in the Software without restriction, including without limitation the rights
205 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
206 | copies of the Software, and to permit persons to whom the Software is
207 | furnished to do so, subject to the following conditions:
208 | 
209 | The above copyright notice and this permission notice shall be included in all
210 | copies or substantial portions of the Software.
211 | 
212 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
213 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
214 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
215 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
216 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
217 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
218 | SOFTWARE.
219 |
lodash
220 |
Copyright 2012-2015 The Dojo Foundation 
221 | Based on Underscore.js, copyright 2009-2015 Jeremy Ashkenas,
222 | DocumentCloud and Investigative Reporters & Editors 
223 | 
224 | Permission is hereby granted, free of charge, to any person obtaining
225 | a copy of this software and associated documentation files (the
226 | "Software"), to deal in the Software without restriction, including
227 | without limitation the rights to use, copy, modify, merge, publish,
228 | distribute, sublicense, and/or sell copies of the Software, and to
229 | permit persons to whom the Software is furnished to do so, subject to
230 | the following conditions:
231 | 
232 | The above copyright notice and this permission notice shall be
233 | included in all copies or substantial portions of the Software.
234 | 
235 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
236 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
237 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
238 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
239 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
240 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
241 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
242 |
Moment.js
243 |
Copyright (c) 2011-2015 Tim Wood, Iskren Chernev, Moment.js contributors
244 | 
245 | Permission is hereby granted, free of charge, to any person
246 | obtaining a copy of this software and associated documentation
247 | files (the "Software"), to deal in the Software without
248 | restriction, including without limitation the rights to use,
249 | copy, modify, merge, publish, distribute, sublicense, and/or sell
250 | copies of the Software, and to permit persons to whom the
251 | Software is furnished to do so, subject to the following
252 | conditions:
253 | 
254 | The above copyright notice and this permission notice shall be
255 | included in all copies or substantial portions of the Software.
256 | 
257 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
258 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
259 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
260 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
261 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
262 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
263 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
264 | OTHER DEALINGS IN THE SOFTWARE.
265 |
Mousetrap
266 |
created by Craig Campbell in 2012-2015
267 |
request
268 |
Apache License
269 | 
270 | Version 2.0, January 2004
271 | 
272 | http://www.apache.org/licenses/
273 | 
274 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
275 | 
276 | 1. Definitions.
277 | 
278 | "License" shall mean the terms and conditions for use, reproduction,
279 | and distribution as defined by Sections 1 through 9 of this document.
280 | 
281 | "Licensor" shall mean the copyright owner or entity authorized by
282 | the copyright owner that is granting the License.
283 | 
284 | "Legal Entity" shall mean the union of the acting entity and all other
285 | entities that control, are controlled by, or are under common control
286 | with that entity. For the purposes of this definition, "control"
287 | means (i) the power, direct or indirect, to cause the direction or
288 | management of such entity, whether by contract or otherwise,
289 | or (ii) ownership of fifty percent (50%) or more of the outstanding shares,
290 | or (iii) beneficial ownership of such entity.
291 | 
292 | "You" (or "Your") shall mean an individual or Legal Entity exercising
293 | permissions granted by this License.
294 | 
295 | "Source" form shall mean the preferred form for making modifications,
296 | including but not limited to software source code, documentation source,
297 | and configuration files.
298 | 
299 | "Object" form shall mean any form resulting from mechanical transformation
300 | or translation of a Source form, including but not limited to compiled
301 | object code, generated documentation, and conversions to other media types.
302 | 
303 | "Work" shall mean the work of authorship, whether in Source or Object form,
304 | made available under the License, as indicated by a copyright notice that is
305 | included in or attached to the work (an example is provided in the Appendix below).
306 | 
307 | "Derivative Works" shall mean any work, whether in Source or Object form,
308 | that is based on (or derived from) the Work and for which the editorial
309 | revisions, annotations, elaborations, or other modifications represent,
310 | as a whole, an original work of authorship. For the purposes of this License,
311 | Derivative Works shall not include works that remain separable from, or merely
312 | link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
313 | 
314 | "Contribution" shall mean any work of authorship, including the original
315 | version of the Work and any modifications or additions to that Work or
316 | Derivative Works thereof, that is intentionally submitted to Licensor for
317 | inclusion in the Work by the copyright owner or by an individual or Legal
318 | Entity authorized to submit on behalf of the copyright owner. For the purposes
319 | of this definition, "submitted" means any form of electronic, verbal,
320 | or written communication sent to the Licensor or its representatives,
321 | including but not limited to communication on electronic mailing lists,
322 | source code control systems, and issue tracking systems that are managed by,
323 | or on behalf of, the Licensor for the purpose of discussing and improving the Work,
324 | but excluding communication that is conspicuously marked or otherwise designated
325 | in writing by the copyright owner as "Not a Contribution."
326 | 
327 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
328 | of whom a Contribution has been received by Licensor and subsequently incorporated
329 | within the Work.
330 | 
331 | 2. Grant of Copyright License. Subject to the terms and conditions of this License,
332 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
333 | no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
334 | Derivative Works of, publicly display, publicly perform, sublicense, and distribute
335 | the Work and such Derivative Works in Source or Object form.
336 | 
337 | 3. Grant of Patent License. Subject to the terms and conditions of this License,
338 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
339 | no-charge, royalty-free, irrevocable (except as stated in this section) patent
340 | license to make, have made, use, offer to sell, sell, import, and otherwise
341 | transfer the Work, where such license applies only to those patent claims licensable
342 | by such Contributor that are necessarily infringed by their Contribution(s) alone or
343 | by combination of their Contribution(s) with the Work to which such Contribution(s)
344 | was submitted. If You institute patent litigation against any entity
345 | (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or
346 | a Contribution incorporated within the Work constitutes direct or contributory
347 | patent infringement, then any patent licenses granted to You under this License
348 | for that Work shall terminate as of the date such litigation is filed.
349 | 
350 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative
351 | Works thereof in any medium, with or without modifications, and in Source or Object form,
352 | provided that You meet the following conditions:
353 | 
354 | You must give any other recipients of the Work or Derivative Works a copy of
355 | this License; and
356 | 
357 | You must cause any modified files to carry prominent notices stating that You
358 | changed the files; and
359 | 
360 | You must retain, in the Source form of any Derivative Works that You distribute,
361 | all copyright, patent, trademark, and attribution notices from the Source form of the Work,
362 | excluding those notices that do not pertain to any part of the Derivative Works; and
363 | 
364 | If the Work includes a "NOTICE" text file as part of its distribution, then any
365 | Derivative Works that You distribute must include a readable copy of the attribution
366 | notices contained within such NOTICE file, excluding those notices that do not pertain
367 | to any part of the Derivative Works, in at least one of the following places: within
368 | a NOTICE text file distributed as part of the Derivative Works; within the Source form
369 | or documentation, if provided along with the Derivative Works; or, within a display
370 | generated by the Derivative Works, if and wherever such third-party notices normally
371 | appear. The contents of the NOTICE file are for informational purposes only and
372 | do not modify the License. You may add Your own attribution notices within Derivative
373 | Works that You distribute, alongside or as an addendum to the NOTICE text from the Work,
374 | provided that such additional attribution notices cannot be construed as modifying
375 | the License. You may add Your own copyright statement to Your modifications and may
376 | provide additional or different license terms and conditions for use, reproduction,
377 | or distribution of Your modifications, or for any such Derivative Works as a whole,
378 | provided Your use, reproduction, and distribution of the Work otherwise complies with
379 | the conditions stated in this License.
380 | 
381 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution
382 | intentionally submitted for inclusion in the Work by You to the Licensor shall be under
383 | the terms and conditions of this License, without any additional terms or conditions.
384 | Notwithstanding the above, nothing herein shall supersede or modify the terms of any
385 | separate license agreement you may have executed with Licensor regarding suchContributions.
386 | 
387 | 6. Trademarks. This License does not grant permission to use the trade names,
388 | trademarks, service marks, or product names of the Licensor, except as required
389 | for reasonable and customary use in describing the origin of the Work and reproducing
390 | the content of the NOTICE file.
391 | 
392 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing,
393 | Licensor provides the Work (and each Contributor provides its Contributions) on an
394 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
395 | including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
396 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
397 | determining the appropriateness of using or redistributing the Work and assume any risks
398 | associated with Your exercise of permissions under this License.
399 | 
400 | 8. Limitation of Liability. In no event and under no legal theory,
401 | whether in tort (including negligence), contract, or otherwise, unless required
402 | by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing,
403 | shall any Contributor be liable to You for damages, including any direct, indirect,
404 | special, incidental, or consequential damages of any character arising as a result
405 | of this License or out of the use or inability to use the Work (including but not limited
406 | to damages for loss of goodwill, work stoppage, computer failure or malfunction,
407 | or any and all other commercial damages or losses), even if such Contributor has been
408 | advised of the possibility of such damages.
409 | 
410 | 9. Accepting Warranty or Additional Liability. While redistributing the Work
411 | or Derivative Works thereof, You may choose to offer, and charge a fee for,
412 | acceptance of support, warranty, indemnity, or other liability obligations
413 | and/or rights consistent with this License. However, in accepting such obligations,
414 | You may act only on Your own behalf and on Your sole responsibility, not on behalf
415 | of any other Contributor, and only if You agree to indemnify, defend, and hold each
416 | Contributor harmless for any liability incurred by, or claims asserted against,
417 | such Contributor by reason of your accepting any such warranty or additional liability.
418 | 
419 | END OF TERMS AND CONDITIONS
420 |
request-progress
421 |
Copyright (c) 2012 IndigoUnited
422 | 
423 | Permission is hereby granted, free of charge, to any person obtaining a copy
424 | of this software and associated documentation files (the "Software"), to deal
425 | in the Software without restriction, including without limitation the rights
426 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
427 | copies of the Software, and to permit persons to whom the Software is furnished
428 | to do so, subject to the following conditions:
429 | 
430 | The above copyright notice and this permission notice shall be included in all
431 | copies or substantial portions of the Software.
432 | 
433 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
434 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
435 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
436 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
437 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
438 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
439 | THE SOFTWARE.
440 |
441 |
442 | 443 | -------------------------------------------------------------------------------- /app/html/authorize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 53 | 103 | 104 | 105 |
106 | 107 | 108 | 109 |
110 | 111 | -------------------------------------------------------------------------------- /app/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 | 19 | 23 | 27 | 31 | 32 | 33 | 37 | 41 | 45 | 49 | 50 | 51 | 55 | 59 | 63 | 67 | 68 | 69 |
16 | 次のフィードに移動: 17 | 18 | 20 | 次のアイテム: 21 | 22 | 24 | 前のフィードに移動: 25 | 26 | 28 | 前のアイテム: 29 | 30 |
34 | ピンを付ける / 外す: 35 | 36 | 38 | 元記事を開く / 閉じる: 39 | 40 | 42 | ブラウザで開く: 43 | 44 | 46 | フィード一覧の更新: 47 | 48 |
52 | 本文の表示 / 非表示: 53 | 54 | 56 | フィード一覧を畳む / 戻す: 57 | 58 | 60 | 検索ボックスに移動: 61 | 62 | 64 | ヘルプを表示 / 非表示: 65 | 66 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /app/html/setting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 68 | 271 | 272 | 273 |

フィードの追加

274 |
275 |
276 | 277 | 278 | 279 |
280 |
281 |

新規フォルダ作成

282 |
283 |
284 | 285 | 286 | 287 |
288 |
289 |

表示の設定

290 |
291 |
292 | 293 | 294 | 295 | 296 | 297 |
298 |
299 | 300 | 301 | 302 | 303 | 304 |
305 |
306 | 307 | 313 |
314 |
315 | 316 | 331 |
332 |
333 |

起動時

334 |
335 |
336 | 337 | 338 | 339 | 340 | 341 |
342 |
343 | 344 | -------------------------------------------------------------------------------- /app/image/eLDR.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.icns -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_128x128.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_16x16.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_256x256.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_32x32.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_512x512.png -------------------------------------------------------------------------------- /app/image/eLDR.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/eLDR.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /app/image/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/pin.png -------------------------------------------------------------------------------- /app/image/pin@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/pin@2x.png -------------------------------------------------------------------------------- /app/image/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/splash.png -------------------------------------------------------------------------------- /app/image/splash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/app/image/splash@2x.png -------------------------------------------------------------------------------- /app/setting.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | fs = require('fs'), 3 | path = require('path'); 4 | 5 | exports.initialize = function(){ 6 | if (!fs.existsSync(path.join(__dirname, 'data'))) { 7 | fs.mkdirSync(path.join(__dirname, 'data')); 8 | } 9 | 10 | if (!fs.existsSync(path.join(__dirname, 'data', 'setting.json'))) { 11 | fs.writeFileSync(path.join(__dirname, 'data', 'setting.json'), JSON.stringify({ 12 | favicon: true, 13 | unread: true, 14 | fontfamily: 'Noto Sans Japanese', 15 | fontsize: '14px', 16 | order: 'modified_on', 17 | state: true 18 | })); 19 | } 20 | }; 21 | 22 | exports.get = function(){ 23 | delete require.cache[path.join(__dirname, 'data', 'setting.json')]; 24 | return require('./data/setting'); 25 | }; 26 | 27 | exports.set = function(name, value){ 28 | var setting = exports.get(); 29 | setting[name] = value; 30 | fs.writeFileSync(path.join(__dirname, 'data', 'setting.json'), JSON.stringify(setting)); 31 | } -------------------------------------------------------------------------------- /app/state.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | fs = require('fs'), 3 | path = require('path'); 4 | 5 | exports.exists = function(params){ 6 | params = params || {}; 7 | 8 | if (params.category === '') { 9 | return false; 10 | } 11 | 12 | return fs.existsSync(path.join(__dirname, 'data', params.category + '.json')); 13 | } 14 | 15 | exports.merge = function(params){ 16 | params = params || {}; 17 | 18 | if (params.category === '' || 19 | params.content === '') { 20 | return; 21 | } 22 | 23 | var data = exports.load({ 24 | category: params.category 25 | }); 26 | 27 | if (params.category === 'feeds') { 28 | _.each(data, function(item, index){ 29 | if (item.subscribe_id === params.content.subscribe_id) { 30 | data[index] = params.content; 31 | } 32 | }); 33 | } else if (params.category === 'items') { 34 | _.each(data, function(item, index){ 35 | if (item.id === params.content.id) { 36 | data[index] = params.content; 37 | } 38 | }); 39 | } else if (params.category === 'meta') { 40 | _.each(params.content, function(item, index){ 41 | data[index] = item; 42 | }); 43 | } 44 | 45 | exports.save({ 46 | category: params.category, 47 | content: data 48 | }); 49 | }; 50 | 51 | exports.save = function(params){ 52 | params = params || {}; 53 | 54 | if (params.category === '' || 55 | params.content === '') { 56 | return; 57 | } 58 | 59 | fs.writeFileSync(path.join(__dirname, 'data', params.category + '.json'), JSON.stringify(params.content)); 60 | }; 61 | 62 | exports.load = function(params){ 63 | params = params || {}; 64 | 65 | if (params.category === '') { 66 | return; 67 | } 68 | 69 | delete require.cache[path.join(__dirname, 'data', params.category + '.json')]; 70 | return require('./data/' + params.category); 71 | }; 72 | 73 | exports.destroy = function(params){ 74 | params = params || {}; 75 | 76 | if (params.category === '') { 77 | return; 78 | } 79 | 80 | if (!exports.exists(params)) { 81 | return; 82 | } 83 | 84 | fs.unlinkSync(path.join(__dirname, 'data', params.category + '.json')); 85 | } -------------------------------------------------------------------------------- /app/style/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | html, 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | font-family: 'Noto Sans Japanese', sans-serif; 10 | color: #111111; 11 | overflow: hidden; 12 | } 13 | #progressbar { 14 | position: fixed; 15 | top: 0px; 16 | left: 0px; 17 | max-width: 100%; 18 | height: 4px; 19 | background-color: #0074d9; 20 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 21 | transition-delay: 0s, 0s; 22 | transition-duration: 0.4s, 0.4s; 23 | transition-property: width, background-color; 24 | transition-timing-function: ease, ease; 25 | z-index: 1001 26 | } 27 | #wrapper { 28 | display: flex; 29 | } 30 | #extend { 31 | width: 76px; 32 | -webkit-user-select: none; 33 | } 34 | #folders { 35 | position: fixed; 36 | top: 0; 37 | bottom: 0; 38 | width: 76px; 39 | background-color: #002f4a; 40 | overflow-x: hidden; 41 | overflow-y: auto; 42 | -webkit-overflow-scrolling: touch; 43 | } 44 | #folders ul { 45 | padding: 40px 0 0; 46 | list-style-type: none; 47 | } 48 | #folders ul li { 49 | margin: 0 0 20px 16px; 50 | width: 44px; 51 | height: 44px; 52 | background-color: rgba(255, 255, 255, 0.4); 53 | cursor: pointer; 54 | -webkit-border-radius: 4px; 55 | } 56 | #folders ul li p { 57 | width: 44px; 58 | height: 44px; 59 | font-size: 26px; 60 | line-height: 44px; 61 | text-align: center; 62 | color: #001f3f; 63 | } 64 | #sidebar { 65 | width: 240px; 66 | -webkit-user-select: none; 67 | } 68 | #tools { 69 | position: fixed; 70 | top: 0px; 71 | width: 240px; 72 | height: 100px; 73 | background-color: #268fc7; 74 | z-index: 100; 75 | } 76 | #order { 77 | position: absolute; 78 | top: 20px; 79 | left: 20px; 80 | width: 200px; 81 | border: 1px solid #268fc7; 82 | font-size: 16px; 83 | } 84 | #filter { 85 | position: absolute; 86 | top: 60px; 87 | left: 20px; 88 | width: 200px; 89 | height: 30px; 90 | padding: 4px 10px; 91 | border: 1px solid #268fc7; 92 | font-size: 16px; 93 | -webkit-border-radius: 4px; 94 | -webkit-box-sizing: border-box; 95 | } 96 | #feeds { 97 | position: fixed; 98 | top: 100px; 99 | bottom: 0; 100 | width: 240px; 101 | background-color: #268fc7; 102 | overflow-x: hidden; 103 | overflow-y: auto; 104 | z-index: 90; 105 | -webkit-overflow-scrolling: touch; 106 | } 107 | #feeds ul { 108 | margin: 0 0 20px; 109 | list-style-type: none; 110 | } 111 | #feeds ul li { 112 | display: flex; 113 | margin: 0 8px 0 0; 114 | padding: 6px 4px 6px 10px; 115 | cursor: pointer; 116 | -webkit-border-radius: 0 4px 4px 0; 117 | } 118 | #feeds ul li img { 119 | width: 16px; 120 | height: 16px; 121 | margin: 2px 6px 0 0; 122 | } 123 | #feeds ul li .feed { 124 | flex: 1; 125 | color: #ffffff; 126 | font-size: 12px; 127 | } 128 | #feeds ul li .feed-zero { 129 | color: #7fdbff; 130 | } 131 | #feeds ul li .badge { 132 | display: inline-block; 133 | max-width: 18px; 134 | min-width: 8px; 135 | height: 16px; 136 | margin: 0 0 0 6px; 137 | padding: 2px 6px; 138 | background-color: #ffffff; 139 | border-radius: 10px; 140 | color: #268fc7; 141 | font-size: 10px; 142 | font-weight: 700; 143 | text-align: center; 144 | vertical-align: middle; 145 | } 146 | #feeds ul li .badge-zero { 147 | background-color: #7fdbff; 148 | } 149 | #content { 150 | flex: 1; 151 | } 152 | #items { 153 | position: fixed; 154 | top: 0; 155 | right: 0; 156 | bottom: 0; 157 | left: 316px; 158 | overflow-x: hidden; 159 | overflow-y: auto; 160 | -webkit-overflow-scrolling: touch; 161 | } 162 | #items ul { 163 | list-style-type: none; 164 | } 165 | #items ul li { 166 | margin: 6px 0; 167 | padding: 10px 20px; 168 | border-bottom: 1px solid #dddddd; 169 | color: #3d3c40; 170 | font-size: 14px; 171 | } 172 | #items ul li.haspin { 173 | background: url('../image/pin.png') no-repeat right top 20px; 174 | } 175 | #help { 176 | display: none; 177 | position: absolute; 178 | bottom: 0; 179 | left: 0; 180 | width: 100%; 181 | height: 200px; 182 | background-color: rgba(0, 30, 64, 0.9); 183 | color: #ffffff; 184 | z-index: 1000; 185 | } 186 | #help table { 187 | width: 100%; 188 | height: 100%; 189 | } 190 | #help td { 191 | width: 25%; 192 | height: 33%; 193 | } 194 | #help .description { 195 | display: inline-block; 196 | width: 75%; 197 | font-size: 14px; 198 | text-align: right; 199 | } 200 | #help .shortcut { 201 | display: inline-block; 202 | width: 20%; 203 | text-align: left; 204 | } 205 | #help .shortcut .keybord { 206 | display: inline-block; 207 | padding: 3px 5px; 208 | background-color: #fcfcfc; 209 | border: solid 1px #cccccc; 210 | border-bottom-color: #bbbbbb; 211 | border-radius: 3px; 212 | vertical-align: middle; 213 | font-size: 16px; 214 | color: #555555; 215 | box-shadow: inset 0 -1px 0 #bbbbbb; 216 | } 217 | .ReactModal__Overlay { 218 | z-index: 999; 219 | } 220 | .react-rater { 221 | float: right; 222 | display: inline-block; 223 | direction: rtl; 224 | line-height: normal; 225 | text-align: left; 226 | unicode-bidi: bidi-override; 227 | } 228 | .react-rater a { 229 | color: #cccccc; 230 | cursor: pointer; 231 | } 232 | .react-rater a:hover { 233 | color: #ffdd99; 234 | } 235 | .react-rater a:hover ~ a { 236 | color: #ffdd99; 237 | } 238 | .react-rater a.is-active { 239 | color: #ffcc66 !important; 240 | } 241 | .react-rater a.is-active ~ a { 242 | color: #ffcc66; 243 | } 244 | .react-rater a.is-disabled { 245 | color: #cccccc !important; 246 | cursor: not-allowed; 247 | } 248 | #folders::-webkit-scrollbar { 249 | width: 0; 250 | } 251 | #feeds::-webkit-scrollbar, 252 | #items::-webkit-scrollbar { 253 | width: 8px; 254 | } 255 | #feeds::-webkit-scrollbar-track, 256 | #items::-webkit-scrollbar-track { 257 | -webkit-border-radius: 4px; 258 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 259 | } 260 | #feeds::-webkit-scrollbar-thumb, 261 | #items::-webkit-scrollbar-thumb { 262 | background: rgba(0, 31, 63, 0.8); 263 | -webkit-border-radius: 4px; 264 | -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5); 265 | } 266 | #feeds::-webkit-scrollbar-thumb:window-inactive, 267 | #items::-webkit-scrollbar-thumb:window-inactive { 268 | background: rgba(0, 116, 217, 0.8); 269 | } -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/banner.png -------------------------------------------------------------------------------- /capture1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/capture1.png -------------------------------------------------------------------------------- /capture2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/capture2.png -------------------------------------------------------------------------------- /capture3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0sukey/Electron-LDR/8130e7a0c44dc64efcc64b3333ec253d8805bb6b/capture3.png -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | $ = require('gulp-load-plugins')(); 3 | 4 | gulp.task('compile', function(){ 5 | return gulp.src('src/**/*.{js,jsx}').pipe($.babel({ 6 | stage: 0 7 | })).pipe(gulp.dest('app/component')); 8 | }); -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | request = require('request'), 6 | Menu = require('menu'), 7 | Cookie = require('./app/cookie'), 8 | State = require('./app/state'); 9 | 10 | var app = electron.app, 11 | ipc = electron.ipcMain, 12 | shell = electron.shell, 13 | BrowserWindow = electron.BrowserWindow; 14 | 15 | require('crash-reporter').start(); 16 | 17 | ipc.setMaxListeners(Infinity); 18 | 19 | require('./app/setting').initialize(); 20 | 21 | var window = null, 22 | splash = null, 23 | about = null, 24 | setting = null, 25 | template = [ 26 | { 27 | label: app.getName(), 28 | submenu: [ 29 | { 30 | label: app.getName() + ' について', 31 | click: function(){ 32 | if (!_.isNull(about)) { 33 | return; 34 | } 35 | 36 | about = new BrowserWindow({ 37 | title: app.getName() + ' について', 38 | width: 640, 39 | height: 480, 40 | resizable: false 41 | }); 42 | 43 | about.on('closed', function(){ 44 | about = null; 45 | }); 46 | 47 | about.loadURL('file://' + path.join(__dirname, 'app', 'html', 'about.html')); 48 | } 49 | }, 50 | { 51 | type: 'separator' 52 | }, 53 | { 54 | label: '環境設定...', 55 | accelerator: 'Cmd+,', 56 | click: function(){ 57 | if (!_.isNull(setting)) { 58 | return; 59 | } 60 | 61 | setting = new BrowserWindow({ 62 | title: '環境設定', 63 | width: 640, 64 | height: 600, 65 | resizable: false, 66 | 'always-on-top': true 67 | }); 68 | 69 | setting.on('closed', function(){ 70 | setting = null; 71 | }); 72 | 73 | setting.loadURL('file://' + path.join(__dirname, 'app', 'html', 'setting.html')); 74 | } 75 | }, 76 | { 77 | type: 'separator' 78 | }, 79 | { 80 | label: 'ピンをすべて削除する', 81 | click: function(){ 82 | if (!window || !Cookie.exists) { 83 | return; 84 | } 85 | 86 | request.post('http://reader.livedoor.com/api/pin/all', { 87 | headers: { 88 | 'User-Agent': window.webContents.getUserAgent(), 89 | Cookie: Cookie.get() 90 | } 91 | }, function(error, response, body){ 92 | if (error) { 93 | return; 94 | } 95 | 96 | request.post('http://reader.livedoor.com/api/pin/clear', { 97 | headers: { 98 | 'User-Agent': window.webContents.getUserAgent(), 99 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 100 | } 101 | }, function(error, response, body){ 102 | if (error) { 103 | return; 104 | } 105 | 106 | State.merge({ 107 | category: 'meta', 108 | content: { 109 | pins: [] 110 | } 111 | }); 112 | }); 113 | }); 114 | } 115 | }, 116 | { 117 | label: 'ログアウト', 118 | click: function(){ 119 | ipc.removeAllListeners('feed:mouseenter'); 120 | ipc.removeAllListeners('feed:mouseleave'); 121 | ipc.removeAllListeners('feed:click'); 122 | ipc.removeAllListeners('feed:active'); 123 | ipc.removeAllListeners('folder:mouseenter'); 124 | ipc.removeAllListeners('folder:mouseleave'); 125 | ipc.removeAllListeners('folder:mousedown'); 126 | ipc.removeAllListeners('folder:mouseup'); 127 | ipc.removeAllListeners('folders'); 128 | ipc.removeAllListeners('setting'); 129 | ipc.removeAllListeners('reload'); 130 | 131 | State.destroy({ category: 'meta' }); 132 | State.destroy({ category: 'folders' }); 133 | State.destroy({ category: 'feeds' }); 134 | State.destroy({ category: 'items' }); 135 | 136 | Cookie.clear(); 137 | app.emit('ready'); 138 | } 139 | }, 140 | { 141 | type: 'separator' 142 | }, 143 | { 144 | label: app.getName() + ' を終了', 145 | accelerator: 'Cmd+Q', 146 | click: function(){ 147 | app.quit(); 148 | } 149 | } 150 | ] 151 | }, 152 | { 153 | label: '編集', 154 | submenu: [ 155 | { 156 | label: '取り消す', 157 | accelerator: 'CmdOrCtrl+Z', 158 | role: 'undo' 159 | }, 160 | { 161 | label: 'やり直す', 162 | accelerator: 'Shift+CmdOrCtrl+Z', 163 | role: 'redo' 164 | }, 165 | { 166 | type: 'separator' 167 | }, 168 | { 169 | label: '切り取り', 170 | accelerator: 'CmdOrCtrl+X', 171 | role: 'cut' 172 | }, 173 | { 174 | label: 'コピー', 175 | accelerator: 'CmdOrCtrl+C', 176 | role: 'copy' 177 | }, 178 | { 179 | label: '貼り付け', 180 | accelerator: 'CmdOrCtrl+V', 181 | role: 'paste' 182 | }, 183 | { 184 | label: 'すべてを選択', 185 | accelerator: 'CmdOrCtrl+A', 186 | role: 'selectall' 187 | }, 188 | ] 189 | }, 190 | { 191 | label: '表示', 192 | submenu: [ 193 | { 194 | label: '全画面表示にする', 195 | accelerator: (function(){ 196 | if (process.platform === 'darwin') { 197 | return 'Ctrl+Command+F'; 198 | } else { 199 | return 'F11'; 200 | } 201 | })(), 202 | click: function(){ 203 | if (window) { 204 | window.setFullScreen(!window.isFullScreen()); 205 | } 206 | } 207 | } 208 | ] 209 | }, 210 | { 211 | label: 'ウィンドウ', 212 | role: 'window', 213 | submenu: [ 214 | { 215 | label: '最小化', 216 | accelerator: 'CmdOrCtrl+M', 217 | role: 'minimize' 218 | } 219 | ] 220 | }, 221 | { 222 | label: 'ヘルプ', 223 | role: 'help', 224 | submenu: [ 225 | { 226 | label: '問題の報告...', 227 | click: function(){ 228 | shell.openExternal('https://github.com/k0sukey/Electron-LDR/issues'); 229 | } 230 | }, 231 | { 232 | label: 'キーボードショートカット', 233 | click: function(){ 234 | if (window) { 235 | window.emit('shortcut'); 236 | } 237 | } 238 | } 239 | ] 240 | } 241 | ], 242 | menu = Menu.buildFromTemplate(template); 243 | 244 | app.on('window-all-closed', function(){ 245 | app.quit(); 246 | }); 247 | 248 | app.on('ready', function(){ 249 | Menu.setApplicationMenu(menu); 250 | 251 | splash = new BrowserWindow({ 252 | title: app.getName(), 253 | width: 640, 254 | height: 480, 255 | frame: false, 256 | transparent: true, 257 | resizable: false 258 | }); 259 | 260 | splash.on('closed', function(){ 261 | splash = null; 262 | }); 263 | 264 | splash.on('authorized', function(){ 265 | window = new BrowserWindow({ 266 | title: app.getName(), 267 | width: 1024, 268 | height: 768, 269 | 'title-bar-style': 'hidden-inset' 270 | }); 271 | 272 | window.webContents.once('did-finish-load', function(){ 273 | splash.close(); 274 | }); 275 | 276 | window.on('closed', function(){ 277 | window = null; 278 | }); 279 | 280 | window.loadURL('file://' + path.join(__dirname, 'app', 'html', 'index.html')); 281 | }); 282 | 283 | splash.webContents.once('did-finish-load', function(){ 284 | if (window) { 285 | window.close(); 286 | } 287 | }); 288 | 289 | splash.loadURL('file://' + path.join(__dirname, 'app', 'html', 'authorize.html')); 290 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eLDR", 3 | "version": "1.0.7", 4 | "description": "", 5 | "main": "main.js", 6 | "engines": { 7 | "node": ">=0.12.0" 8 | }, 9 | "scripts": { 10 | "start": "gulp compile && ./node_modules/.bin/electron .", 11 | "compile": "gulp compile", 12 | "package": "npm run package-darwin && npm run package-linux && npm run package-win32", 13 | "package-darwin": "gulp compile && electron-packager . eLDR --icon app/image/eLDR.icns --arch=x64 --out=packages/v1.0.7 --platform=darwin --version=0.33.6 --ignore=packages/* --ignore=src/* --ignore=app/data/* --overwrite", 14 | "package-linux": "gulp compile && electron-packager . eLDR --arch=x64 --out=packages/v1.0.7 --platform=linux --version=0.33.6 --ignore=packages/* --ignore=src/* --ignore=app/data/* --overwrite", 15 | "package-win32": "gulp compile && electron-packager . eLDR --arch=x64 --out=packages/v1.0.7 --platform=win32 --version=0.33.6 --ignore=packages/* --ignore=src/* --ignore=app/data/* --overwrite", 16 | "zip": "npm run zip-darwin && npm run zip-linux && npm run zip-win32", 17 | "zip-darwin": "cd packages/v1.0.7 && zip -r eLDR-darwin-x64.zip eLDR-darwin-x64", 18 | "zip-linux": "cd packages/v1.0.7 && zip -r eLDR-linux-x64.zip eLDR-linux-x64", 19 | "zip-win32": "cd packages/v1.0.7 && zip -r eLDR-win32-x64.zip eLDR-win32-x64" 20 | }, 21 | "author": "Kosuke Isobe", 22 | "license": "MIT", 23 | "dependencies": { 24 | "lodash": "^3.10.1", 25 | "moment": "^2.10.6", 26 | "mousetrap": "^1.5.3", 27 | "react": "^0.13.3", 28 | "react-dnd": "^1.1.8", 29 | "react-modal": "^0.5.0", 30 | "react-rater": "^0.1.0", 31 | "react-tooltip": "^0.6.4", 32 | "request": "^2.61.0", 33 | "request-progress": "^0.3.1" 34 | }, 35 | "devDependencies": { 36 | "electron-packager": "^5.1.1", 37 | "electron-prebuilt": "^0.35.4", 38 | "gulp": "^3.9.0", 39 | "gulp-babel": "^5.2.1", 40 | "gulp-load-plugins": "^0.10.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | mousetrap = require('mousetrap'), 4 | progress = require('request-progress'), 5 | request = require('request'), 6 | React = require('react'), 7 | ReactDnD = require('react-dnd'), 8 | ReactTooltip = require('react-tooltip'), 9 | HTML5Backend = require('react-dnd/modules/backends/HTML5'), 10 | Cookie = require('../cookie'), 11 | Setting = require('../setting'), 12 | State = require('../state'), 13 | Feeds = require('../component/feeds'), 14 | Folders = require('../component/folders'); 15 | 16 | var ipc = electron.ipcRenderer, 17 | remote = electron.remote; 18 | 19 | var app = remote.app; 20 | 21 | if (!State.exists({ category: 'meta'})) { 22 | State.save({ 23 | category: 'meta', 24 | content: {} 25 | }); 26 | } 27 | 28 | var App = React.createClass({ 29 | displayName: 'app', 30 | getInitialState: function(){ 31 | return { 32 | order: 'modified_on', 33 | filter: '', 34 | folders: { 35 | name2id: {}, 36 | names: [] 37 | }, 38 | folder: '全て', 39 | feeds: [] 40 | }; 41 | }, 42 | doOrder: function(){ 43 | var element = document.getElementById('order'), 44 | index = element.selectedIndex; 45 | 46 | var order = element.options[index].value; 47 | Setting.set('order', order); 48 | 49 | this.setState({ 50 | order: order 51 | }); 52 | 53 | this.doFeeds({ 54 | order: order 55 | }); 56 | }, 57 | doFilter: function(){ 58 | var filter = document.getElementById('filter').value; 59 | 60 | this.setState({ 61 | filter: filter 62 | }); 63 | 64 | this.doFeeds({ 65 | filter: filter 66 | }); 67 | }, 68 | doFolders: function(){ 69 | var folders = State.load({ 70 | category: 'folders' 71 | }); 72 | 73 | this.setState({ 74 | folders: folders 75 | }); 76 | 77 | var meta = State.load({ 78 | category: 'meta' 79 | }); 80 | 81 | if (_.has(meta, 'folder')) { 82 | this.setState({ 83 | folder: meta.folder 84 | }); 85 | 86 | this.doFeeds({ 87 | folder: meta.folder 88 | }); 89 | } else { 90 | this.doFeeds(); 91 | } 92 | }, 93 | doFeeds: function(params){ 94 | params = params || {}; 95 | 96 | params = _.extend({ 97 | order: this.state.order, 98 | filter: this.state.filter, 99 | folder: this.state.folder 100 | }, params); 101 | 102 | var feeds = State.load({ 103 | category: 'feeds' 104 | }); 105 | 106 | if (process.platform === 'darwin') { 107 | var badge = 0; 108 | 109 | _.each(feeds, function(feed){ 110 | badge += feed.unread_count 111 | }); 112 | 113 | app.dock.setBadge('' + badge); 114 | } 115 | 116 | if (params.folder === '未分類') { 117 | feeds = _.filter(feeds, function(feed){ 118 | return feed.folder === ''; 119 | }); 120 | } else if (params.folder !== '' && params.folder !== '全て') { 121 | feeds = _.filter(feeds, function(feed){ 122 | return feed.folder === params.folder; 123 | }.bind(this)); 124 | } 125 | 126 | if (params.filter !== '') { 127 | var regex = new RegExp(params.filter, 'i'); 128 | 129 | feeds = _.filter(feeds, function(feed){ 130 | return regex.test(feed.title); 131 | }); 132 | } 133 | 134 | feeds = feeds.sort(function(a, b){ 135 | switch (params.order) { 136 | case 'modified_on': 137 | return b.modified_on - a.modified_on; 138 | case 'modified_on:reverse': 139 | return a.modified_on - b.modified_on; 140 | case 'unread_count': 141 | return b.unread_count - a.unread_count; 142 | case 'unread_count:reverse': 143 | return a.unread_count - b.unread_count; 144 | case 'title:reverse': 145 | return a.title - b.title; 146 | case 'rate': 147 | return b.rate - a.rate; 148 | case 'subscribers_count': 149 | return b.subscribers_count - a.subscribers_count; 150 | case 'subscribers_count:reverse': 151 | return a.subscribers_count - b.subscribers_count; 152 | } 153 | }.bind(this)); 154 | 155 | var setting = Setting.get(); 156 | 157 | if (_.has(setting, 'unread') && !setting.unread) { 158 | feeds = _.filter(feeds, function(feed){ 159 | return feed.unread_count; 160 | }); 161 | } 162 | 163 | this.setState({ 164 | feeds: feeds 165 | }); 166 | }, 167 | doFetch: function(){ 168 | document.getElementById('progressbar').style.width = '0%'; 169 | 170 | progress(request.post('http://reader.livedoor.com/api/subs', { 171 | headers: { 172 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 173 | Cookie: Cookie.get() 174 | }, 175 | form: { 176 | unread: 0 177 | } 178 | }, function(error, response, body){ 179 | if (error) { 180 | return; 181 | } 182 | 183 | var feeds; 184 | 185 | try { 186 | feeds = JSON.parse(body); 187 | } catch (e) { 188 | return; 189 | } 190 | 191 | State.save({ 192 | category: 'feeds', 193 | content: feeds 194 | }); 195 | 196 | document.getElementById('progressbar').style.width = '50%'; 197 | 198 | progress(request.post('http://reader.livedoor.com/api/folders', { 199 | headers: { 200 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 201 | Cookie: Cookie.get() 202 | } 203 | }, function(error, response, body){ 204 | if (error) { 205 | return; 206 | } 207 | 208 | var folders; 209 | 210 | try { 211 | folders = JSON.parse(body); 212 | } catch (e) { 213 | return; 214 | } 215 | 216 | folders.name2id = _.extend({ 217 | '全て': '-1' 218 | }, folders.name2id); 219 | folders.name2id = _.extend({ 220 | '未分類': '0' 221 | }, folders.name2id); 222 | 223 | folders.names.unshift('未分類'); 224 | folders.names.unshift('全て'); 225 | 226 | State.save({ 227 | category: 'folders', 228 | content: folders 229 | }); 230 | 231 | document.getElementById('progressbar').style.width = '100%'; 232 | 233 | _.delay(function(){ 234 | document.getElementById('progressbar').style.width = '0%'; 235 | }, 500); 236 | 237 | this.doFolders(); 238 | }.bind(this)), { 239 | throttle: 100 240 | }).on('progress', function(state){ 241 | document.getElementById('progressbar').style.width = (state.percent * 0.5 + 50) + '%'; 242 | }); 243 | }.bind(this)), { 244 | throttle: 100 245 | }).on('progress', function(state){ 246 | document.getElementById('progressbar').style.width = (state.percent * 0.5) + '%'; 247 | }); 248 | }, 249 | componentDidMount: function(){ 250 | var setting = Setting.get(), 251 | meta = State.load({ 252 | category: 'meta' 253 | }); 254 | 255 | document.getElementsByTagName('body')[0].style.fontFamily = setting.fontfamily; 256 | 257 | _.each(document.getElementById('order').options, function(item, index){ 258 | if (item.value === setting.order) { 259 | item.selected = 'selected'; 260 | } 261 | }.bind(this)); 262 | 263 | 264 | if (State.exists({ category: 'feeds'}) && 265 | State.exists({ category: 'folders'}) && 266 | _.has(meta, 'folder') && 267 | (_.isUndefined(setting, 'state') || setting.state)) { 268 | this.setState({ 269 | folders: State.load({ 270 | category: 'folders' 271 | }) 272 | }); 273 | 274 | this.setState({ 275 | folder: meta.folder 276 | }); 277 | 278 | this.doFeeds({ 279 | order: setting.order, 280 | folder: meta.folder 281 | }); 282 | } else { 283 | this.doFetch(); 284 | } 285 | 286 | ipc.on('folders', this.doFolders); 287 | ipc.on('setting', this.doFeeds); 288 | ipc.on('reload', this.doFetch); 289 | mousetrap.bind('r', this.doFetch); 290 | }, 291 | componentWillUnmount: function(){ 292 | ipc.removeListener('folders', this.doFolders); 293 | ipc.removeListener('setting', this.doFeeds); 294 | ipc.removeListener('reload', this.doFetch); 295 | mousetrap.unbind('r'); 296 | mousetrap.unbind('?'); 297 | mousetrap.unbind('f'); 298 | }, 299 | render: function(){ 300 | return ( 301 |
302 |
303 |
304 | 305 |
306 |
307 | 325 |
326 |
327 |
328 | 329 |
330 | ); 331 | } 332 | }); 333 | App = ReactDnD.DragDropContext(HTML5Backend)(App); 334 | React.render(React.createElement(App), document.getElementById('app')); 335 | 336 | var doShortcut = function(){ 337 | var element = document.getElementById('help'); 338 | 339 | if (element.style.display === '' || 340 | element.style.display === 'none') { 341 | element.style.display = 'block'; 342 | } else { 343 | element.style.display = 'none'; 344 | } 345 | }; 346 | mousetrap.bind('?', doShortcut); 347 | remote.getCurrentWindow().on('shortcut', doShortcut); 348 | 349 | mousetrap.bind('f', function(){ 350 | _.defer(function(){ 351 | document.getElementById('filter').focus(); 352 | }); 353 | }); -------------------------------------------------------------------------------- /src/feed.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | React = require('react'), 4 | ReactDnD = require('react-dnd'), 5 | State = require('../state'); 6 | 7 | var ipc = electron.ipcRenderer; 8 | 9 | var Feed = React.createClass({ 10 | displayName: 'feed', 11 | doMouseEnter: function(){ 12 | ipc.emit('feed:mouseenter', this.props.index); 13 | }, 14 | doMouseLeave: function(){ 15 | ipc.emit('feed:mouseleave', this.props.index); 16 | }, 17 | doClick: function(){ 18 | ipc.emit('feed:click', this.props.index); 19 | }, 20 | componentDidMount: function(){ 21 | if (State.exists({ category: 'meta' })) { 22 | var meta = State.load({ 23 | category: 'meta' 24 | }); 25 | 26 | if (this.props.subscribe_id === meta.subscribe_id) { 27 | React.findDOMNode(this).style.color = '#7fdbff'; 28 | React.findDOMNode(this).style.backgroundColor = '#001f3f'; 29 | 30 | ipc.emit('feed:active', this.props.index); 31 | } 32 | } 33 | }, 34 | render: function(){ 35 | return this.props.connectDragSource( 36 |
  • 41 | 42 | {this.props.title} 43 | {this.props.unread_count} 44 |
  • 45 | ); 46 | } 47 | }); 48 | 49 | Feed = ReactDnD.DragSource('feed', { 50 | beginDrag: function(props){ 51 | return { 52 | subscribe_id: props.subscribe_id, 53 | folder: props.folder 54 | }; 55 | } 56 | }, function(connect, monitor){ 57 | return { 58 | connectDragSource: connect.dragSource(), 59 | connectDragPreview: connect.dragPreview(), 60 | isDragging: monitor.isDragging() 61 | }; 62 | })(Feed); 63 | 64 | module.exports = Feed; -------------------------------------------------------------------------------- /src/feeds.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | mousetrap = require('mousetrap'), 4 | progress = require('request-progress'), 5 | request = require('request'), 6 | React = require('react'), 7 | ReactDnD = require('react-dnd'), 8 | Cookie = require('../cookie'), 9 | Setting = require('../setting'), 10 | State = require('../state'), 11 | Feed = require('./feed'), 12 | Items = require('./items'); 13 | 14 | var ipc = electron.ipcRenderer, 15 | remote = electron.remote; 16 | 17 | var app = remote.app; 18 | 19 | module.exports = React.createClass({ 20 | displayName: 'feeds', 21 | getInitialState: function(){ 22 | return { 23 | active: null, 24 | toggle: true 25 | }; 26 | }, 27 | doActive: function(index){ 28 | this.setState({ 29 | active: index 30 | }); 31 | }, 32 | doMouseEnter: function(index){ 33 | if (index !== this.state.active) { 34 | var ul = document.getElementById('feeds').children[0]; 35 | React.findDOMNode(ul).childNodes[index].style.color = '#7fdbff'; 36 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = '#001f3f'; 37 | } 38 | }, 39 | doMouseLeave: function(index){ 40 | if (index !== this.state.active) { 41 | var ul = document.getElementById('feeds').children[0]; 42 | React.findDOMNode(ul).childNodes[index].style.color = '#ffffff'; 43 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'transparent'; 44 | } 45 | }, 46 | doClick: function(index){ 47 | this.setState({ 48 | active: index 49 | }); 50 | 51 | var me, 52 | ul = document.getElementById('feeds').children[0]; 53 | 54 | _.each(React.findDOMNode(ul).childNodes, function(child, i){ 55 | if (index === i) { 56 | me = child; 57 | 58 | child.style.color = '#7fdbff'; 59 | child.style.backgroundColor = '#001f3f'; 60 | } else { 61 | child.style.color = '#ffffff'; 62 | child.style.backgroundColor = 'transparent'; 63 | } 64 | }); 65 | 66 | if (process.platform === 'darwin') { 67 | app.dock.setBadge('' + (parseInt(app.dock.getBadge(), 10) - parseInt(me.children[2].textContent, 10))); 68 | } 69 | 70 | var url = 'http://reader.livedoor.com/api/unread', 71 | feed = this.props.feeds[index], 72 | pins = []; 73 | 74 | if (parseInt(me.children[2].textContent, 10) === 0) { 75 | url = 'http://reader.livedoor.com/api/all'; 76 | } 77 | 78 | document.getElementById('progressbar').style.width = '0%'; 79 | 80 | progress(request.post(url, { 81 | headers: { 82 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 83 | Cookie: Cookie.get() 84 | }, 85 | form: { 86 | subscribe_id: feed.subscribe_id 87 | } 88 | }, function(error, response, body){ 89 | if (error) { 90 | return; 91 | } 92 | 93 | React.unmountComponentAtNode(document.getElementById('items')); 94 | 95 | var json; 96 | 97 | try { 98 | json = JSON.parse(body); 99 | State.save({ 100 | category: 'items', 101 | content: json.items 102 | }); 103 | } catch (e) { 104 | return; 105 | } 106 | 107 | request.post('http://reader.livedoor.com/api/pin/all', { 108 | headers: { 109 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 110 | Cookie: Cookie.get() 111 | } 112 | }, function(error, response, body){ 113 | if (!error) { 114 | try { 115 | pins = JSON.parse(body); 116 | } catch (e) {} 117 | } 118 | 119 | State.merge({ 120 | category: 'meta', 121 | content: { 122 | subscribe_id: feed.subscribe_id, 123 | title: feed.title, 124 | pins: pins 125 | } 126 | }); 127 | 128 | React.render(React.createElement(Items, { 129 | feed: feed, 130 | items: json.items, 131 | pins: pins 132 | }), document.getElementById('items')); 133 | }); 134 | 135 | me.children[2].textContent = 0; 136 | 137 | feed.unread_count = 0; 138 | State.merge({ 139 | category: 'feeds', 140 | content: feed 141 | }); 142 | 143 | if (!me.children[1].classList.contains('feed-zero')) { 144 | me.children[1].classList.add('feed-zero'); 145 | } 146 | 147 | if (!me.children[2].classList.contains('badge-zero')) { 148 | me.children[2].classList.add('badge-zero'); 149 | } 150 | 151 | request.post('http://reader.livedoor.com/api/touch_all', { 152 | headers: { 153 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 154 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 155 | }, 156 | form: { 157 | subscribe_id: feed.subscribe_id 158 | } 159 | }, function(error, response, body){}); 160 | 161 | document.getElementById('progressbar').style.width = '100%'; 162 | _.delay(function(){ 163 | document.getElementById('progressbar').style.width = '0%'; 164 | }, 500); 165 | }.bind(this)), { 166 | throttle: 100 167 | }).on('progress', function(state){ 168 | document.getElementById('progressbar').style.width = state.percent + '%'; 169 | }); 170 | }, 171 | doPrev: function(){ 172 | var index = _.isNull(this.state.active) ? 0 : this.state.active - 1; 173 | if (index < 0) { 174 | index = 0; 175 | } 176 | 177 | document.getElementById(this.props.feeds[index].subscribe_id).scrollIntoView(); 178 | 179 | this.doClick(index); 180 | }, 181 | doNext: function(){ 182 | var index = _.isNull(this.state.active) ? 0 : this.state.active + 1; 183 | if (index >= this.props.feeds.length) { 184 | index = this.props.feeds.length - 1; 185 | } 186 | 187 | document.getElementById(this.props.feeds[index].subscribe_id).scrollIntoView(); 188 | 189 | this.doClick(index); 190 | }, 191 | doToggle: function(){ 192 | document.getElementById('folders').style.display = this.state.toggle ? 'none' : 'block'; 193 | document.getElementById('sidebar').style.display = this.state.toggle ? 'none' : 'block'; 194 | document.getElementById('items').style.left = this.state.toggle ? 0 : '316px'; 195 | this.setState({ 196 | toggle: !this.state.toggle 197 | }); 198 | }, 199 | componentDidMount: function(){ 200 | ipc.on('feed:mouseenter', this.doMouseEnter); 201 | ipc.on('feed:mouseleave', this.doMouseLeave); 202 | ipc.on('feed:click', this.doClick); 203 | ipc.on('feed:active', this.doActive); 204 | 205 | mousetrap.bind('a', this.doPrev); 206 | mousetrap.bind('s', this.doNext); 207 | mousetrap.bind('z', this.doToggle); 208 | 209 | var setting = Setting.get(); 210 | 211 | if (State.exists({ category: 'feeds' }) && 212 | State.exists({ category: 'items' }) && 213 | State.exists({ category: 'meta' }) && 214 | (_.isUndefined(setting, 'state') || setting.state)) { 215 | var feeds = State.load({ 216 | category: 'feeds' 217 | }), 218 | items = State.load({ 219 | category: 'items' 220 | }), 221 | meta = State.load({ 222 | category: 'meta' 223 | }); 224 | 225 | React.render(React.createElement(Items, { 226 | feed: _.filter(feeds, function(item){ 227 | return item.subscribe_id === meta.subscribe_id; 228 | })[0], 229 | items: items, 230 | pins: meta.pins 231 | }), document.getElementById('items')); 232 | } 233 | }, 234 | componentDidUpdate: function(){ 235 | var setting = Setting.get(); 236 | 237 | if (State.exists({ category: 'feeds' }) && 238 | State.exists({ category: 'items' }) && 239 | State.exists({ category: 'meta' }) && 240 | (_.isUndefined(setting, 'state') || setting.state)) { 241 | var feeds = State.load({ 242 | category: 'feeds' 243 | }), 244 | items = State.load({ 245 | category: 'items' 246 | }), 247 | meta = State.load({ 248 | category: 'meta' 249 | }); 250 | 251 | React.render(React.createElement(Items, { 252 | feed: _.filter(feeds, function(item){ 253 | return item.subscribe_id === meta.subscribe_id; 254 | })[0], 255 | items: items, 256 | pins: meta.pins 257 | }), document.getElementById('items')); 258 | } 259 | }, 260 | componentWillReceiveProps: function(){ 261 | this.setState({ 262 | active: null 263 | }); 264 | }, 265 | componentWillUnmount: function(){ 266 | ipc.removeListener('feed:mouseenter', this.doMouseEnter); 267 | ipc.removeListener('feed:mouseleave', this.doMouseLeave); 268 | ipc.removeListener('feed:click', this.doClick); 269 | ipc.removeListener('feed:active', this.doActive); 270 | 271 | mousetrap.unbind('a'); 272 | mousetrap.unbind('s'); 273 | mousetrap.unbind('z'); 274 | }, 275 | render: function(){ 276 | var setting = Setting.get(), 277 | favicon = { 278 | display: setting.favicon ? 'inline' : 'none' 279 | }, 280 | font = { 281 | fontFamily: setting.fontfamily 282 | }; 283 | 284 | return ( 285 | 309 | ); 310 | } 311 | }); -------------------------------------------------------------------------------- /src/folder.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | request = require('request'), 4 | React = require('react'), 5 | ReactDnD = require('react-dnd'), 6 | Cookie = require('../cookie'); 7 | 8 | var ipc = electron.ipcRenderer, 9 | remote = electron.remote; 10 | 11 | var Folder = React.createClass({ 12 | displayName: 'folder', 13 | doMouseEnter: function(){ 14 | ipc.emit('folder:mouseenter', this.props.index); 15 | }, 16 | doMouseLeave: function(){ 17 | ipc.emit('folder:mouseleave', this.props.index); 18 | }, 19 | onMouseDown: function(){ 20 | ipc.emit('folder:mousedown', this.props.index); 21 | }, 22 | onMouseUp: function(){ 23 | ipc.emit('folder:mouseup', this.props.index); 24 | }, 25 | render: function(){ 26 | var style = _.extend(this.props.style, { 27 | boxShadow: this.props.isOver && this.props.canDrop ? '0px 0px 0px 4px rgba(255, 255, 255, 0.8)' : 'none' 28 | }); 29 | 30 | return this.props.connectDropTarget( 31 |
  • 36 |

    {this.props.name.substr(0, 1)}

    41 |
  • 42 | ); 43 | } 44 | }); 45 | 46 | Folder = ReactDnD.DropTarget('feed', { 47 | canDrop: function(props, monitor){ 48 | return (props.folder_id !== '-1' && monitor.getItem().folder !== props.name) || props.folder_id === 0; 49 | }, 50 | drop: function(props, monitor){ 51 | var to = props.name; 52 | 53 | if (to === '未分類') { 54 | to = ''; 55 | } 56 | 57 | var subscribe_id = monitor.getItem().subscribe_id; 58 | 59 | request.post('http://reader.livedoor.com/api/folders', { 60 | headers: { 61 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 62 | Cookie: Cookie.get() 63 | } 64 | }, function(error, response, body){ 65 | if (error) { 66 | return; 67 | } 68 | 69 | var folders; 70 | 71 | try { 72 | folders = JSON.parse(body); 73 | } catch (e) { 74 | return; 75 | } 76 | 77 | if (to !== '' && !_.has(folders.name2id, to)) { 78 | return; 79 | } 80 | 81 | request.post('http://reader.livedoor.com/api/feed/move', { 82 | headers: { 83 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 84 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 85 | }, 86 | form: { 87 | subscribe_id: subscribe_id, 88 | to: to 89 | } 90 | }, function(error, response, body){ 91 | if (error) { 92 | return; 93 | } 94 | 95 | var json; 96 | 97 | try { 98 | json = JSON.parse(body); 99 | } catch (e) { 100 | return; 101 | } 102 | 103 | if (json.isSuccess) { 104 | ipc.emit('reload'); 105 | } 106 | }); 107 | }); 108 | 109 | return; 110 | } 111 | }, function(connect, monitor){ 112 | return { 113 | connectDropTarget: connect.dropTarget(), 114 | isOver: monitor.isOver(), 115 | canDrop: monitor.canDrop() 116 | }; 117 | })(Folder); 118 | 119 | module.exports = Folder; -------------------------------------------------------------------------------- /src/folders.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | mousetrap = require('mousetrap'), 4 | request = require('request'), 5 | React = require('react'), 6 | Cookie = require('../cookie'), 7 | State = require('../state'), 8 | Folder = require('./folder'); 9 | 10 | var ipc = electron.ipcRenderer, 11 | remote = electron.remote; 12 | 13 | var dialog = remote.dialog; 14 | 15 | module.exports = React.createClass({ 16 | displayName: 'folders', 17 | getInitialState: function(){ 18 | return { 19 | active: null, 20 | touch: 0 21 | }; 22 | }, 23 | doMouseEnter: function(index){ 24 | if (index !== this.state.active) { 25 | var ul = document.getElementById('folders').children[0]; 26 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'rgba(255, 255, 255, 1)'; 27 | } 28 | }, 29 | doMouseLeave: function(index){ 30 | if (index !== this.state.active) { 31 | var ul = document.getElementById('folders').children[0]; 32 | React.findDOMNode(ul).childNodes[index].style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; 33 | } 34 | }, 35 | doMouseDown: function(index){ 36 | this.setState({ 37 | touch: Date.now() 38 | }); 39 | }, 40 | doMouseUp: function(index){ 41 | if (Date.now() - this.state.touch < 1000) { 42 | this.doClick(index); 43 | return; 44 | } 45 | 46 | if (index === 0 || index === -1) { 47 | return; 48 | } 49 | 50 | dialog.showMessageBox(remote.getCurrentWindow(), { 51 | type: 'question', 52 | buttons: ['キャンセル', '削除する'], 53 | message: '「' + this.props.folders.names[index] + '」フォルダを削除しますか?(中のフィードは削除されません)', 54 | cancelId: 0 55 | }, (function (e) { 56 | if (e === 0) { 57 | return; 58 | } 59 | 60 | request.post('http://reader.livedoor.com/api/folders', { 61 | headers: { 62 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 63 | Cookie: Cookie.get() 64 | } 65 | }, function(error, response, body){ 66 | if (error) { 67 | return; 68 | } 69 | 70 | var json; 71 | 72 | try { 73 | json = JSON.parse(body); 74 | } catch (e) { 75 | return; 76 | } 77 | 78 | if (!_.has(json.name2id, this.props.folders.names[index])) { 79 | return; 80 | } 81 | 82 | request.post('http://reader.livedoor.com/api/folder/delete', { 83 | headers: { 84 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 85 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 86 | }, 87 | form: { 88 | folder_id: json.name2id[this.props.folders.names[index]] 89 | } 90 | }, function(error, response, body){ 91 | if (error) { 92 | return; 93 | } 94 | 95 | ipc.emit('reload'); 96 | }); 97 | }.bind(this)); 98 | }).bind(this)); 99 | }, 100 | doClick: function(index){ 101 | this.setState({ 102 | active: index 103 | }); 104 | 105 | var me, 106 | ul = document.getElementById('folders').children[0]; 107 | 108 | _.each(React.findDOMNode(ul).childNodes, function(child, i){ 109 | if (index === i) { 110 | me = child; 111 | 112 | child.style.backgroundColor = 'rgba(255, 255, 255, 1)'; 113 | } else { 114 | child.style.backgroundColor = 'rgba(255, 255, 255, 0.4)'; 115 | } 116 | }); 117 | 118 | State.merge({ 119 | category: 'meta', 120 | content: { 121 | folder: this.props.folders.names[index] 122 | } 123 | }); 124 | 125 | ipc.emit('folders'); 126 | }, 127 | componentWillReceiveProps: function(props){ 128 | ipc.on('folder:mouseenter', this.doMouseEnter); 129 | ipc.on('folder:mouseleave', this.doMouseLeave); 130 | ipc.on('folder:mousedown', this.doMouseDown); 131 | ipc.on('folder:mouseup', this.doMouseUp); 132 | 133 | _.each(props.folders.names, function(item, index){ 134 | if (item === props.folder) { 135 | this.setState({ 136 | active: index 137 | }); 138 | } 139 | }.bind(this)); 140 | }, 141 | componentWillUnmount: function(){ 142 | ipc.removeListener('folder:mouseenter', this.doMouseEnter); 143 | ipc.removeListener('folder:mouseleave', this.doMouseLeave); 144 | ipc.removeListener('folder:mousedown', this.doMouseDown); 145 | ipc.removeListener('folder:mouseup', this.doMouseUp); 146 | 147 | var ul = document.getElementById('folders').children[0]; 148 | 149 | _.each(React.findDOMNode(ul).childNodes, function(item, index){ 150 | if (index < 10) { 151 | mousetrap.unbind([ 152 | 'command+' + (index + 1), 153 | 'ctrl' + (index + 1) 154 | ]); 155 | } 156 | }.bind(this)); 157 | }, 158 | render: function(){ 159 | var setting = Setting.get(); 160 | 161 | return ( 162 | 189 | ); 190 | } 191 | }); -------------------------------------------------------------------------------- /src/items.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | moment = require('moment'), 4 | mousetrap = require('mousetrap'), 5 | request = require('request'), 6 | React = require('react'), 7 | Modal = require('react-modal'), 8 | Setting = require('../setting'), 9 | State = require('../state'), 10 | Cookie = require('../cookie'), 11 | Meta = require('../component/meta'); 12 | 13 | var remote = electron.remote, 14 | shell = electron.shell; 15 | 16 | var dialog = remote.dialog; 17 | 18 | moment.locale('ja'); 19 | 20 | var style = { 21 | title: { 22 | marginBottom: '4px', 23 | fontSize: '20px', 24 | fontWeight: 'bold', 25 | cursor: 'pointer' 26 | }, 27 | description: { 28 | marginBottom: '20px', 29 | fontSize: '14px', 30 | color: '#aaaaaa' 31 | }, 32 | created: { 33 | marginRight: '6px' 34 | }, 35 | author: { 36 | marginRight: '6px' 37 | }, 38 | category: { 39 | marginRight: '6px' 40 | }, 41 | modal: { 42 | content: { 43 | padding: 0 44 | } 45 | }, 46 | browser: { 47 | display: 'inline-block', 48 | width: '100%', 49 | height: '100%' 50 | } 51 | }; 52 | 53 | module.exports = React.createClass({ 54 | displayName: 'items', 55 | getInitialState: function() { 56 | return { 57 | collapse: false, 58 | modalIsOpen: false, 59 | active: null, 60 | url: '' 61 | }; 62 | }, 63 | doOpen: function(index){ 64 | this.setState({ 65 | modalIsOpen: true, 66 | url: this.props.items[index].link 67 | }); 68 | 69 | document.getElementById('browser').addEventListener('dom-ready', function(){ 70 | document.getElementById('browser').focus(); 71 | }); 72 | 73 | remote.getCurrentWindow().focusOnWebView(); 74 | }, 75 | doClose: function(){ 76 | remote.getCurrentWindow().blurWebView(); 77 | 78 | this.setState({ 79 | modalIsOpen: false 80 | }); 81 | }, 82 | doPrev: function(){ 83 | var index = _.isNull(this.state.active) ? 0 : this.state.active - 1; 84 | if (index < 0) { 85 | index = 0; 86 | } 87 | 88 | document.getElementById(this.props.items[index].id).scrollIntoView(); 89 | this.setState({ 90 | active: index 91 | }); 92 | 93 | State.merge({ 94 | category: 'meta', 95 | content: { 96 | items: index 97 | } 98 | }); 99 | }, 100 | doNext: function(){ 101 | var index = _.isNull(this.state.active) ? 0 : this.state.active + 1; 102 | if (index >= this.props.items.length) { 103 | index = this.props.items.length - 1; 104 | } 105 | 106 | document.getElementById(this.props.items[index].id).scrollIntoView(); 107 | this.setState({ 108 | active: index 109 | }); 110 | 111 | State.merge({ 112 | category: 'meta', 113 | content: { 114 | items: index 115 | } 116 | }); 117 | }, 118 | doCollapse: function(){ 119 | this.props.items.map(function(item){ 120 | document.getElementById(item.id).children[2].style.display = !this.state.collapse ? 'none' : 'block' 121 | }, this); 122 | 123 | this.setState({ 124 | collapse: !this.state.collapse 125 | }); 126 | }, 127 | doModal: function(){ 128 | if (this.state.modalIsOpen) { 129 | this.doClose(); 130 | document.getElementById('feeds').style.overflowY = 'auto'; 131 | document.getElementById('items').style.overflowY = 'auto'; 132 | } else { 133 | this.doOpen(_.isNull(this.state.active) ? 0 : this.state.active); 134 | document.getElementById('feeds').style.overflowY = 'hidden'; 135 | document.getElementById('items').style.overflowY = 'hidden'; 136 | } 137 | }, 138 | doPinning: function(){ 139 | var index = _.isNull(this.state.active) ? 0 : this.state.active, 140 | item = this.props.items[index]; 141 | 142 | request.post('http://reader.livedoor.com/api/pin/all', { 143 | headers: { 144 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 145 | Cookie: Cookie.get() 146 | } 147 | }, function(error, response, body){ 148 | if (error) { 149 | return; 150 | } 151 | 152 | var json, 153 | pins = this.props.pins; 154 | 155 | try { 156 | json = JSON.parse(body); 157 | } catch (e) { 158 | return; 159 | } 160 | 161 | var url = 'http://reader.livedoor.com/api/pin/', 162 | form = {}, 163 | haspin = (_.where(json, { link: item.link }).length > 0); 164 | 165 | if (haspin) { 166 | url += 'remove'; 167 | form = { 168 | link: item.link 169 | }; 170 | } else { 171 | url += 'add'; 172 | form = { 173 | link: item.link, 174 | title: item.title 175 | }; 176 | } 177 | 178 | request.post(url, { 179 | headers: { 180 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 181 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 182 | }, 183 | form: form 184 | }, function(error, response, body){ 185 | if (error) { 186 | return; 187 | } 188 | 189 | var json; 190 | 191 | try { 192 | json = JSON.parse(body); 193 | } catch (e) { 194 | return; 195 | } 196 | 197 | if (json.isSuccess) { 198 | if (haspin) { 199 | document.getElementById(item.id).classList.remove('haspin'); 200 | 201 | State.merge({ 202 | category: 'meta', 203 | content: { 204 | pins: _.filter(pins, function(pin){ 205 | return pin.link !== item.link 206 | }) 207 | } 208 | }); 209 | } else { 210 | document.getElementById(item.id).classList.add('haspin'); 211 | 212 | pins.push({ 213 | link: item.link, 214 | title: item.title 215 | }); 216 | 217 | State.merge({ 218 | category: 'meta', 219 | content: { 220 | pins: pins 221 | } 222 | }); 223 | } 224 | } 225 | }); 226 | }.bind(this)); 227 | }, 228 | doBrowser: function(){ 229 | var index = _.isNull(this.state.active) ? 0 : this.state.active; 230 | 231 | shell.openExternal(this.props.items[index].link); 232 | }, 233 | doUnsubscribe: function(){ 234 | dialog.showMessageBox(remote.getCurrentWindow(), { 235 | type: 'question', 236 | buttons: [ 'キャンセル', '解除する' ], 237 | message: '「' + this.props.feed.title + '」の登録を解除しますか?', 238 | cancelId: 0 239 | }, function(e){ 240 | if (e === 0) { 241 | return; 242 | } 243 | 244 | request.post('http://reader.livedoor.com/api/all', { 245 | headers: { 246 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 247 | Cookie: Cookie.get() 248 | }, 249 | form: { 250 | subscribe_id: this.props.feed.subscribe_id, 251 | offset: 0, 252 | limit: 1 253 | } 254 | }, function(error, response, body){ 255 | if (error) { 256 | return; 257 | } 258 | 259 | request.post('http://reader.livedoor.com/api/feed/unsubscribe', { 260 | headers: { 261 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 262 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 263 | }, 264 | form: { 265 | subscribe_id: this.props.feed.subscribe_id 266 | } 267 | }, function(error, response, body){}); 268 | }.bind(this)); 269 | }.bind(this)); 270 | }, 271 | componentDidMount: function(){ 272 | mousetrap.bind('k', this.doPrev); 273 | mousetrap.bind('j', this.doNext); 274 | mousetrap.bind('c', this.doCollapse); 275 | mousetrap.bind('v', this.doModal); 276 | mousetrap.bind('p', this.doPinning); 277 | mousetrap.bind('b', this.doBrowser); 278 | mousetrap.bind('del', this.doUnsubscribe); 279 | 280 | if (this.props.items[0]) { 281 | document.getElementById(this.props.items[0].id).scrollIntoView(); 282 | this.setState({ 283 | active: 0 284 | }); 285 | } 286 | 287 | var setting = Setting.get(); 288 | 289 | if (State.exists({ category: 'meta' }) && 290 | (_.isUndefined(setting, 'state') || setting.state)) { 291 | var meta = State.load({ 292 | category: 'meta' 293 | }); 294 | 295 | if (meta.items && this.props.items[meta.items]) { 296 | document.getElementById(this.props.items[meta.items].id).scrollIntoView(); 297 | this.setState({ 298 | active: meta.items 299 | }); 300 | } 301 | } 302 | 303 | document.getElementById('items').focus(); 304 | }, 305 | componentWillUnmount: function(){ 306 | mousetrap.unbind('k'); 307 | mousetrap.unbind('j'); 308 | mousetrap.unbind('c'); 309 | mousetrap.unbind('v'); 310 | mousetrap.unbind('p'); 311 | mousetrap.unbind('b'); 312 | mousetrap.unbind('del'); 313 | 314 | State.merge({ 315 | category: 'meta', 316 | content: { 317 | items: 0 318 | } 319 | }); 320 | }, 321 | render: function(){ 322 | var setting = Setting.get(), 323 | font = { 324 | fontFamily: setting.fontfamily, 325 | fontSize: setting.fontsize 326 | }; 327 | 328 | return ( 329 | 353 | ); 354 | } 355 | }); -------------------------------------------------------------------------------- /src/meta.jsx: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | electron = require('electron'), 3 | request = require('request'), 4 | React = require('react'), 5 | ReactRater = require('react-rater'), 6 | Cookie = require('../cookie'); 7 | 8 | var ipc = electron.ipcRenderer, 9 | remote = electron.remote; 10 | 11 | var style = { 12 | wrapper: { 13 | margin: '20px', 14 | padding: '20px', 15 | backgroundColor: '#f7f7f7', 16 | border: 0, 17 | borderRadius: '8px' 18 | }, 19 | title: { 20 | float: 'left', 21 | fontSize: '20px' 22 | }, 23 | uers: { 24 | clear: 'both' 25 | } 26 | }; 27 | 28 | module.exports = React.createClass({ 29 | displayName: 'meta', 30 | doRating: function(rating){ 31 | request.post('http://reader.livedoor.com/api/subs', { 32 | headers: { 33 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 34 | Cookie: Cookie.get() 35 | } 36 | }, function(error, response, body){ 37 | if (error) { 38 | return; 39 | } 40 | 41 | var json; 42 | 43 | try { 44 | json = JSON.parse(body); 45 | } catch (e) { 46 | return; 47 | } 48 | 49 | var feeds = _.filter(json, function(item, index){ 50 | return item.subscribe_id === this.props.feed.subscribe_id; 51 | }.bind(this)); 52 | 53 | if (feeds.length === 0) { 54 | return; 55 | } 56 | 57 | request.post('http://reader.livedoor.com/api/feed/set_rate', { 58 | headers: { 59 | 'User-Agent': remote.getCurrentWindow().webContents.getUserAgent(), 60 | Cookie: Cookie.get() + '; reader_sid=' + Cookie.parseApiKey(response.headers['set-cookie']) 61 | }, 62 | form: { 63 | subscribe_id: this.props.feed.subscribe_id, 64 | rate: rating 65 | } 66 | }, function(error, response, body){ 67 | if (error) { 68 | return; 69 | } 70 | 71 | var json; 72 | 73 | try { 74 | json = JSON.parse(body); 75 | } catch (e) { 76 | return; 77 | } 78 | 79 | if (json.isSuccess) { 80 | ipc.emit('reload'); 81 | } 82 | }); 83 | }.bind(this)); 84 | }, 85 | render: function(){ 86 | return ( 87 |
  • 88 |

    {this.props.feed.title}

    89 | 90 |

    購読者数:{this.props.feed.subscribers_count}ユーザ

    91 |
  • 92 | ); 93 | } 94 | }); --------------------------------------------------------------------------------