Electron で Live Dwango Reader（旧 Live Door Reader）クライアントを作ってみる、その四

その四です。API のつなぎ込みが完了しました。[レポジトリはこちら](https://github.com/k0sukey/Electron-LDR)。パッケージ化済みのアプリをダウンロードできるようになりました。[releases](https://github.com/k0sukey/Electron-LDR/releases) からどうぞ。大きな追加は、フィードのフォルダ移動がドラッグアンドドロップでできるようになりました。ドラッグアンドドロップができると一気にアプリっぽくなってきますね！

![スクリーンショット 2015-10-17 11.44.23.png](https://qiita-image-store.s3.amazonaws.com/0/47686/9c6949f0-b853-ef4a-b8aa-7a267f5a3e87.png)

### 色々な React のコンポーネントを導入した

#### react-rater

[レポジトリはこちら](https://github.com/NdYAG/react-rater)。レーティングできるようになります。星をいくつ付けるか、ですね。他にも似たようなコンポーネントはいくつかあるのですが、これが一番シンプルで使いやすかったです。星を他のものに変えることもできたりするので、くわしくはレポジトリをご覧ください。

```jsx
var React = require('react'),
  ReactRater = require('react-rater');

module.exports = React.createClass({
  doRating: function(rating){
    // do stuff
  },
  render: function(){
    return(
      <ReactRater total={5} rating={3} onRate={this.doRating}/>
    );
  }
});
```

#### react-tooltip

[レポジトリはこちら](https://github.com/wwayne/react-tooltip)。フィードの一覧のさらに横にフォルダの一覧を追加したのですが、フォルダの頭文字をアイコン風にしても何がなんだかわからないので、カーソルが上に来るとツールチップを表示させるのに使っています。色々と設定ができるので、詳しくはレポジトリをご覧ください。

```jsx
var React = require('react'),
  ReactTooltip = require('react-tooltip');

module.exports = React.createClass({
  render: function(){
    return(
      <div>
        <p data-tip='フォルダ'>フ</p>
        <ReactTooltip/>
      </div>
    );
  }
});
```
#### react-dnd

[レポジトリはこちら](https://github.com/gaearon/react-dnd)。今回追加したコンポーネントの中で一番複雑でした...。Browser-Window 側のエントリーポイントである app.js も jsx で書き直しました。フィード一覧のフィードを、隣りにあるフォルダ一覧のフォルダへドラッグアンドドロップできるようにしてくれるのが、このコンポーネントです。ドキュメントを見るとやれることはいっぱいあるのですが、今回は最小限にしてあります（力尽きました）。細かく設定すると、ドラッグしたコンポーネントのレイヤーを変更できたりします。すごいですね。

##### app.jsx

一番大元のこのアプリのコンポーネントです。この範囲でドラッグアンドドロップできるようにコンテキストを読み込みます。

```jsx
var React = require('react'),
  ReactDnD = require('react-dnd'),
  HTML5Backend = require('react-dnd/modules/backends/HTML5');

var App = React.createClass({
  render: function(){
    return (<div></div>);
  }
});
App = ReactDnD.DragDropContext(HTML5Backend)(App);
React.render(React.createElement(App), document.getElementById('app'));

```

##### feed.jsx

ドラッグするコンポーネントです。ドラッグソースを適用することでドラッグすることが可能になります。

```jsx
var React = require('react'),
  ReactDnD = require('react-dnd');

var Feed = React.createClass({
  render: function(){
    return this.props.connectDragSource(
      <li>フィード</li>
    );
  }
});
Feed = ReactDnD.DragSource('feed', {
  // ドロップ先に渡したいプロパティを返却
  beginDrag: function(props){
    return {
      subscribe_id: props.subscribe_id, // フィードの ID
      folder: props.folder              // フォルダ名
    };
  }
}, function(connect, monitor){
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
})(Feed);

module.exports = Feed;
```

##### folder.jsx

ドロップするコンポーネントです。ドロップターゲットを適用することでドロップすることが可能になります。

```jsx
var React = require('react'),
  ReactDnD = require('react-dnd');

var Folder = React.createClass({
  render: function(){
    var style = _.extend(this.props.style, {
      boxShadow: this.props.isOver && this.props.canDrop ? '0px 0px 0px 4px rgba(255, 255, 255, 0.8)' : 'none'
    });

    return this.props.connectDropTarget(
      <li style={style}>フォルダ</li>
    );
  }
});
Folder = ReactDnD.DropTarget('feed', {
  // ドロップを可能にするかどうか。今回はドロップ先が今所属しているフォルダの場合、
  // もしくは全てのフィードを表示するフォルダの場合、ドロップを禁止しています
  canDrop: function(props, monitor){
    return (props.folder_id !== '-1' && monitor.getItem().folder !== props.name) || props.folder_id === 0;
  },
  // ドロップ後の処理。フォルダ移動の API を叩いています。ここの最後は return 必須です
  drop: function(props, monitor){
    // do stuff
    return;
  }
}, function(connect, monitor){
  // ここで返却する isOver と canDrop は Folder コンポーネント内で参照できるようにするためです
  // この値を見て Folder コンポーネントの render でボックスシャドウを付けるか付けないか判定します
  return {
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
    canDrop: monitor.canDrop()
  };
})(Feed);

module.exports = Folder;
```

### フォルダを追加した

前述のコンポーネントの紹介でも書いてある通り、フォルダを追加しました。フォルダの登録は詳細設定から行えます。フォルダアイコンを長押しすると削除することができます。フィードのフォルダ移動も前述のとおり、ドラッグアンドドロップでおこなえます。もちろん、Slack の様に、⌘＋数字でフォルダを移動することができます。

### アプリ終了時の状態を復元するようにした

ブラウザで LDR を使っている時もよくやって残念な気持ちになるのが、記事を見ていてのリロード（もしくはブラウザの終了）です。途中どこまで見ていたのか、探して戻るのが辛いです。というわけで、状態を保存するようにしました。通信結果やクリックした時など、状態を全て JSON で書き出しています。それを元に、起動時に JSON ファイルがあれば復元しているだけです。自分で使っていて、地味に嬉しいので復元できるようにして大正解でした。詳細設定で復元しない設定もありますので、いらない場合のケアもバッチリですね。

ローカルストレージを使えばウェブサービスもできそうなのですが、本家 LDR のアップデートは今後行われないのでしょうか？

### フィード一覧から元記事一覧を開いた時のフォーカス

スペースでスクロールできなかったので、なんとかフォーカスできないものかと悩んでいたのですが、```<div></div>``` はフォーカスメソッドがないので、```tabIndex="-1"``` を指定してフォーカスできるようになりました。

### インジケータをつけた

react-progress パッケージを利用するのも良いですが、DOM で簡単に追加できたのでそれで良しとします。request-progress パッケージの ```progress``` イベントが通信状況（state）を拾えるので、それを用いて width を動かしています。

```html
<div id="progressbar"></div>
```

```css
#progressbar {
  position: fixed;
  top: 0px;
  left: 0px;
  max-width: 100%;
  height: 4px;
  background-color: #0074d9;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
  transition-delay: 0s, 0s;
  transition-duration: 0.4s, 0.4s;
  transition-property: width, background-color;
  transition-timing-function: ease, ease;
}
```

```js
var request = require('request'),
  progress = require('request-progress');

progress(request.post('http://...', function(error, response, body){
  // do stuff
}), {
  throttle: 100
}).on('progress', function(state){
  document.getElementById('progressbar').style.width = state + '%';
});
```

### フィードの未読件数をドックのバッヂに表示するようにした

[ドキュメントを見るとちゃんと書いてあるのですが](http://electron.atom.io/docs/v0.34.0/api/app/#app-dock-setbadge-text-os-x)、セットするのは文字列です。よく読まずに数値をセットして叱られました。ごめんなさい。

```js
var app = require('app');
app.dock.setBadge('88888');
```

### 設定の監視

watchr から ipc でイベントを待ち受けるように変更して、時差が発生することはなくなりました。ただ、ウィンドウを開き直す（ログアウト→ログイン）時にイベントを全て解除しないといけません。```ipc.removeAllListeners('イベント名')``` でログアウト時にすべてのイベントを解除するようにしてあります（というか、```ipc.on``` しようとすると、コンソールにエラーが吐かれます）。ドキュメントにはイベントを発火させる時 ```ipc.send ``` と書かれているのですが、そんなメソッドはないと言われてしまいます。ipc の中を見ると ```ipc.emit``` でした。これはどういうことだろう...。

ipc のイベントが便利なので調子に乗って貼りまくっていたら リスナの最大値が突破していると叱られました。これは ```ipc.setMaxListeners(Infinity);``` です。心が濁ってきました。

ちなみに今の設定画面はこんな感じになっています。

![スクリーンショット 2015-10-17 10.08.59.png](https://qiita-image-store.s3.amazonaws.com/0/47686/4e39936d-c6af-f269-c0ba-76dd88d7a8a4.png)

### CSS を見なおした

フローティングレイアウトは古いらしく（？） flexbox でレイアウトするように変更しました。しかしすっごく楽ですね、これ。今更ながら知りました...。

### アプリのアイコンを追加した

Mac だけですが...。がんばって描きました。作り方は、各サイズのアイコンを *.iconset フォルダにまとめて、ターミナルからコマンド一発です。

```bash
$ iconutil -c icns Electron-LDR.iconset
```

このファイルを electron-packager のオプションで指定してあげれば OK です。

```bash
$ electron-packager . Electron-LDR --icon Electron-LDR.icns
```

## まとめ

ほぼ React のコンポーネントの紹介でした。次は詳細設定画面を話題の [Photon](https://github.com/connors/photon) で書きなおしてみたいです。そろそろ LDR のアプリとしては自分では満足してきたので、後は細々としたアップデートをしていこうかと思います。おわり。