読者です 読者をやめる 読者になる 読者になる

JavaFXとtwitter4jで変態発見用Twitterクライアントを作ってみた

変態アドベントカレンダー11月16日分です。

大して面白いネタが思い浮かびもしなかったので、以前途中まで作りかけたTwitterクライアントに手を加えて、自分のフォローしている人たちの発言から"変態"を含む発言を表示するクライアントを作って見ました。
「お巡りさん、この人です!」するボタンを付けたかったのですが、間に合わなかったので是非付けてあげてください。
しかし、慣れていないとなかなか思い通りのデザインにするのには時間がかかりますね。


以下簡単な解説。
まずはアプリケーション開始用のクラス、JavaFXのお約束通りApplicationを継承し、オーバーライドしたstart()中でStageを初期化しています。

Startup.java

package hr;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import twitter4j.TwitterException;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;

import java.io.File;
import java.io.IOException;

public class Startup extends Application {

    private TwitterService twitterService;
    private StatusListView statusListView;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() {
        try {
            File userFile = new File("user.xml");
            twitterService = TwitterService.createTwitterService( userFile );
        } catch (IOException ignored) {
        }

    }

    @Override
    public void start(Stage stage) throws Exception {


        statusListView = new StatusListView();
        Scene scene = new Scene(statusListView, 400, 640);
        stage.setResizable(false);

        stage.setScene(scene);
        stage.setTitle("HENTAI Report");
        stage.show();

        if (twitterService != null) {
            twitterService.addHomeTimelineListener(statusListView.getController());
        } else {
            authlization();

        }

    }

    private void authlization() throws TwitterException {
        final RequestToken token = TwitterService.authlization();
        String url = token.getAuthenticationURL();

        BorderPane borderPane = new BorderPane();
        HBox hbox = new HBox();
        final TextField textField = new TextField();
        Button button = new Button("認証");

        hbox.getChildren().addAll( textField,button);


        final WebView webView = new WebView();
        final Stage stage = new Stage(StageStyle.UTILITY);

        borderPane.setCenter(webView);
        borderPane.setBottom(hbox);
        Scene scene = new Scene(borderPane, 400, 640);
        stage.setScene(scene);
        stage.setTitle("認証");
        stage.show();

        webView.getEngine().load(url);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                String pin = textField.getText();
                AccessToken accessToken = TwitterService.enterPin(token, pin);
                twitterService = TwitterService.createTwitterService(accessToken);
                twitterService.addHomeTimelineListener(statusListView.getController());
                stage.hide();
            }
        });

    }

}


StatusViewは一つ一つのStatusを表示するクラスです。
BorderPaneの左側にプロフィール画像、中央にテキストを表示しています。


StatusListViewクラスは、上のStatusViewを新しいものを上から順に並べて表示するクラスです。
ScrollPane を継承し、内部のコンテンツには要素を縦に並べるVBoxを配置しています。

StatusListView.java

package hr;

import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;

public class StatusListView extends ScrollPane {

    private final StatusListController controller;
    private VBox scrollPaneContent;

    public StatusListView() {
        setFitToWidth(true);
        setVbarPolicy(ScrollBarPolicy.ALWAYS);
        setContent(getscrollPaneContent());
        controller = new StatusListController(this);
    }

    private VBox getscrollPaneContent() {
        if (scrollPaneContent == null) {
            scrollPaneContent = new VBox();
            scrollPaneContent.setFillWidth(true);
        }
        return scrollPaneContent;
    }

    public StatusListController getController() {
        return controller;
    }

    public void addStatusView(Node view, int index) {
        getscrollPaneContent().getChildren().add(index, view);
    }

    public void removeStatusView(int maxStatusNum) {
        getscrollPaneContent().getChildren().remove(maxStatusNum);
    }
}


StatusListControllerクラスはStatusListViewクラスのコントローラです。
StatusList、StatusListViewは見ての通りsetter/getter程度の単純なメソッドしか持たせず、Viewとロジックを分離しています。
StatusListController.java

package hr;

import javafx.application.Platform;
import javafx.scene.image.Image;
import twitter4j.Status;
import twitter4j.UserStreamAdapter;

import java.util.ArrayList;
import java.util.List;

public class StatusListController extends UserStreamAdapter {
    private static final int MAX_STATUS_NUM = 150;
    private final StatusListView view;
    private List<Long> sorter = new ArrayList<Long>(MAX_STATUS_NUM);

    public StatusListController(StatusListView view) {
        this.view = view;
    }

    public void addStatus(Status status) {
        if (status.isRetweet()) {
            status = status.getRetweetedStatus();
        }

        if (!status.getText().contains("変態")) {
            return;
        }

        int index = getIndexToView(status.getId());
        if (index < 0) {
            return;
        }

        StatusView newStatusView = createStatusView(status);
        view.addStatusView(newStatusView, index);

        int count = sorter.size();
        if (count > MAX_STATUS_NUM) {
            view.removeStatusView(MAX_STATUS_NUM);
        }

    }

    private StatusView createStatusView(Status status) {
        ProfileImageManager imageManager = ProfileImageManager.getInstance();

        Image image = imageManager.getProfileImage(status.getUser());
        String text = status.getText();

        StatusView newStatusView = new StatusView();
        newStatusView.setProfileImage(image);
        newStatusView.setText(text);

        return newStatusView;
    }

    @Override
    public void onStatus(final Status status) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                addStatus(status);
            }
        });

    }

    /**
     * @param id status の ID
     * @return 挿入位置
     */
    private int getIndexToView(long id) {
        int len = sorter.size();
        if (len == 0) {
            sorter.add(0, id);
            return 0;
        }
        int index = 0;
        for (; index < len; index++) {
            if (id > sorter.get(index)) {
                break;
            }
        }

        if (index < MAX_STATUS_NUM) {
            sorter.add(index, id);
            return index;
        }

        return -1;
    }
}

githubはこちら
https://github.com/hakurai/hentaireport

以下使い方。
まず動かすためにはJavaFXおよびTwitterのCONSUMER_KEYとCONSUMER_SECRETが必要なので、ダウンロード及び取得してください。ここで既にハードルが高い気がしなくもない。


まず起動すると、初回は認証画面がWebViewで表示されるので、ログインしてください。


ログインが成功すると、PINコードが表示されるので、左下の入力欄に入力して、認証ボタンをクリックします。

認証が成功すると、"変態"の文字を含む発言が表示され始めるはずです。しかしそのような発言をする方がフォローしている人たちの中に居なければ、残念(?)ながら何も表示されませんが・・・。