モバイルWEB用の Qiita 新着投稿リーダーを作った


**[2014/01/23追記]**

デザインを刷新＆検索機能をつけました。
⇒ [[電車やカフェでの暇つぶしに] Qiita 新着投稿リーダーに検索機能をつけた](http://qiita.com/hkusu/items/e088e189ac287ab31a8a)

--

[ionic](http://ionicframework.com/) を利用して作ってみました。Qiita の投稿の取得には [Qiita API v1](https://qiita.com/api/v1/docs) を利用しています。
（`v2` だとクロスオリジン制約にひっかかるので。）

<img src="https://qiita-image-store.s3.amazonaws.com/0/35733/d5361710-7694-f57f-444e-ead7e324e5cc.png" width=250>

## URL

静的リソース(JavaScript/CSS/HTML/画像)のみで構成されてるので、GitHub Pages から配信しています。

http://hkusu.github.io/qiita-newpost-reader/dist

新着投稿を引っ張ってくるだけの単純なものですが、Qiita API を駆使すればもっと色々できそう。

ソースコードは GitHub に置いてあります。ご自由にどうぞ。
⇒ https://github.com/hkusu/qiita-newpost-reader

## 環境

- 基本となるプロジェクト構成や、ionicの導入手順についてはこちら。
	- ⇒ [ionic でリッチなモバイルWEBサイトを実現する](http://qiita.com/hkusu/items/528b906320305b1f5be4)
- マークダウン(コードハイライト付き)と絵文字の表示についてはこちら。
	- ⇒ [JavaScript で Markdown 文書を表示してみた](http://qiita.com/hkusu/items/f77211edf0f4ab7561ca)

## ソースコードについて

ほぼ ionic の機能で実装してるので、凝ったことはしていないのです。主要なコードは下記の２ファイルのみ。

### Controller

ファットコントローラーになっていますが.. AngularJS をぼーっと使ってると、どうしてもコントローラは大きくなっちゃいますね。

```javascript:src/app/main/main.controller.js
'use strict';

angular.module('qiitaNewpostReader')
  .controller('MainCtrl', function ($scope, $http, $ionicLoading, $ionicModal, $ionicScrollDelegate, $timeout) {
    // ページ下部の infinite-scroll を表示するか否かのフラグ
    $scope.initial_loaded = false;

    $scope.load = function(_page){
      // Loading... を表示
      $ionicLoading.show({
        template: 'Loading...',
        noBackdrop: true
      });
      $http.get('https://qiita.com/api/v1/items?per_page=10&page=' + _page).success(function(items) {
        // 初期表示とページ上部のリフレッシュの際はクリア
        if(_page == 1){
          $scope.items= [];
        }
        // 既存の内容に結合
        $scope.items = $scope.items.concat(items);
        // Loading... を隠す
        $ionicLoading.hide();
        // ページ上部のリフレッシュ表示を終了
        $scope.$broadcast('scroll.refreshComplete');
        // 初期表示で infinite-scroll が有効になってしまうので、数秒まつ
        $timeout(function() {
          $scope.initial_loaded = true;
        }, 1000);
        // 数秒まってからページ下部の infinite-scroll を終了する。でないとループしてしまうため
        $timeout(function() {
          $scope.$broadcast('scroll.infiniteScrollComplete');
        }, 3000);

        $scope.next_page = _page + 1;
      });
    };

    // 初期表示
    $scope.load(1);

    // 記事詳細 モーダル表示
    $scope.modal_title = "";
    $scope.modal_body = ""; // bodyの方はマークダウン(コードハイライト付き)＆絵文字を表示
    $ionicModal.fromTemplateUrl("post.modal.html", {
      scope: $scope,
      animation: "slide-in-up"
    }).then(function(modal) {
      $scope.post_modal = modal;
    });
    $scope.openPostModal = function(index) {
      $scope.modal_title = $scope.items[index].title;
      $scope.modal_body = $scope.items[index].raw_body;
      // スクロールをTOPへ。ハンドルを指定して、背後の ScrollView がTOPにスクロールされないようにする
      $ionicScrollDelegate.$getByHandle('subScroll').scrollTop();
      $scope.post_modal.show();
    };
    $scope.closePostModal = function() {
      $scope.post_modal.hide();
    };
    $scope.$on("$destroy", function() {
      $scope.post_modal.remove();
    });

    // info モーダル表示
    $ionicModal.fromTemplateUrl("info.modal.html", {
      scope: $scope,
      animation: "slide-in-up"
    }).then(function(modal) {
      $scope.info_modal = modal;
    });
    $scope.openInfoModal = function(index) {
      $scope.info_modal.show();
    };
    $scope.closeInfoModal = function() {
      $scope.info_modal.hide();
    };
    $scope.$on("$destroy", function() {
      $scope.info_modal.remove();
    });
  });
```

### View

こちらも３画面分を１テンプレートに詰め込んでいるので、ファットになっています。ちゃんとやるなら分割した方がよいです。

Markdown を表示する部分は `<div class="markdown" marked="modal_body|emoji"></div>` です。利用しているライブラリは [angular-marked](https://github.com/Hypercubed/angular-marked) と [Highlight.js](https://github.com/components/highlightjs) 、[AngularJS Emoji Filter](https://github.com/globaldev/angular-emoji-filter) です。
（導入方法は、この投稿の `環境` の項からリンクしている記事を参照ください。）

```html:src/app/main/main.html
<ion-header-bar align-title="center" class="bar-balanced">
  <div class="buttons">
    <a
      class="button button-clear button-light button-small"
      ng-click="openInfoModal()"
      >
      <i class="ion-information-circled"></i>
    </a>
  </div>
  <h1 class="title">Qiita 新着投稿</h1>
  <div class="buttons">
    <a
      class="button button-outline button-light button-small"
      ng-href="http://qiita.com"
      >
      <font size="1px">
        本サイトへ
      </font>
    </a>
  </div>
</ion-header-bar>
<ion-content
  delegate-handle="mainScroll"
  padding="true"
  style="background: floralwhite; margin-top: -10px"
  >
  <ion-refresher
    on-refresh="load(1)"
    >
  </ion-refresher>
  <div align="center">
    <font size="1px" color="gray">
      pull to refresh..
    </font>
  </div>

  <ion-list>
    <ion-item ng-repeat="item in items"
              can-swipe="false"
              class="item-thumbnail-left" ng-click="openPostModal($index)">
      <img ng-src="{{item.user.profile_image_url}}">
      <h5>
        <font color="gray">
          {{item.user.url_name}} が {{item.created_at_in_words}}前 に投稿しました。
        </font>
      </h5>
      <h3><font color="#3cb371">{{item.title}}</font></h3>
      <h5>
        <font color="gray">
          <i class="ion-ios-folder-outline"></i> {{item.stock_count}}
          &nbsp;&nbsp;<i class="ion-ios-chatbubble-outline"></i> {{item.comment_count}}
        </font>
      </h5>
      <h6><br></h6>
      <h4>
        <font color="gray">
          {{item.raw_body}}
        </font>
      </h4>
    </ion-item>
  </ion-list>
  <ion-infinite-scroll
    ng-if="initial_loaded"
    on-infinite="load(next_page)"
    distance="20%"
    >
  </ion-infinite-scroll>

  <!-- 記事詳細 モーダル表示 -->
  <script id="post.modal.html" type="text/ng-template">
    <ion-modal-view>
      <ion-header-bar align-title="center" class="bar-balanced">
        <h1 class="title">{{modal_title}}</h1>
        <div class="buttons">
          <button class="button button-clear" ng-click="closePostModal()">
            <i class="ion-close"></i>
          </button>
        </div>
      </ion-header-bar>
      <ion-content
        delegate-handle="subScroll"
        padding="true"
        style="background: floralwhite; padding: 0 10px 10px 10px"
        ng-click="closePostModal()"
        >
        <h4><font color="#3cb371">{{modal_title}}</font></h4>
        <hr>
        <div class="markdown" marked="modal_body|emoji"></div>
      </ion-content>
    </ion-modal-view>
  </script>

  <!-- info モーダル表示 -->
  <script id="info.modal.html" type="text/ng-template">
    <ion-modal-view>
      <ion-header-bar align-title="center" class="bar-balanced">
        <h1 class="title">About</h1>
        <div class="buttons">
          <button class="button button-clear" ng-click="closeInfoModal()">
            <i class="ion-close"></i>
          </button>
        </div>
      </ion-header-bar>
      <ion-content
        padding="true"
        style="background: floralwhite; padding: 30px 10px 10px 70px"
        ng-click="closeInfoModal()"
        >
        <div align="left">
          <i class="ion-social-octocat"></i>
          <a ng-href="https://github.com/hkusu/qiita-newpost-reader"
             target="_blank"
            >
            hkusu/qiita-newpost-reader
          </a>
          <br>
          <i class="ion-person"></i>
          <a ng-href="https://hkusu.github.io"
             target="_blank"
            >
            hkusu.github.io
          </a>
        </div>
      </ion-content>
    </ion-modal-view>
  </script>
</ion-content>
```

あと、ionic と Markdown のライブラリが競合して、Markdown がうまい具合に表示されない問題がありました。なので次のような CSS を用意して、`<code>` と `<pre>` タグをキャッチして Markdown ぽく表示されるように工夫しています。

```css:src/app/index.css
.markdown img {
  max-width: 100%;
  height: auto;
}

.markdown code {
  background-color: #f5f5f5;
  padding: 2px 2px 2px 2px;
  border-radius: 2px;
  -webkit-border-radius: 2px;
  -moz-border-radius: 2px;
}

.markdown pre {
  background-color: #f5f5f5;
  padding: 5px 5px 5px 5px;
  border-radius: 5px;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  margin: 10px 0 10px 0;
  font-size: 0.8em;
}

.markdown i.emoji {
  vertical-align: middle;
}

a:link { color: gray; }
a:visited { color: gray; }
a:hover { color: #ff0000; }
a:active { color: #ff8000; }
```

## おわりに

扱うデータが大きいので、実際にモバイル端末で表示するとちょっと重いかな^^;

Qiita API は認証すればもっと色々できるので、次回はここからストックしたり、あとは Cordova 等でネィティブアプリにパッケージしてみようと思います。